[Cuis-dev] Rerendering when touched: Experiments in simplifying UI development in Cuis inspired by Mithril.js
Paul D. Fernhout
pdfernhout at kurtz-fernhout.com
Mon May 8 20:50:59 PDT 2023
Hi Juan,
Thanks for the words of welcome and also for mentioning Cuis encouraging
experimentation.
On point 1, I definitely am suggesting avoiding using dependencies. But
I am OK with custom objects to store data. And I am also OK with writing
event-driven code of various sorts.
On point 2, I think composing reusable components is a fine idea. Sorry
I did not make that clear. The first version of the Proof of Concept of
redrawing on a click did not have a composition of morphs, but later
versions do. This third version has both a TextModelMorph and a
PluggableButtonMorph (even if there is some workaround code included to
use them without changes or without alternatively changing the HandMorph
to trigger redraw events perhaps).
https://github.com/pdfernhout/Cuis-Smalltalk-RedrawWhenClicked/blob/main/KfMithrilInspiredExperimentMorph-v3.st
A related snippet from the later Proof of Concept showing a first cut at
supporting updating morphs if they exists (which is called from the
"drawOn:" method):
subcomponentFor: symbol create: createBlock configure: configureBlock
| component |
component := subcomponents at: symbol ifAbsentPut: createBlock.
configureBlock notNil ifTrue: [configureBlock value: component].
^ component
I realized the other day -- when playing with a Wheel morph in the UI
library that Hilaire pointed me to which was connected to a scroll bar
in one of the examples -- that part of the experiment is essentially a
question of pushing data versus pulling data. Or, at least, when is each
of pull or push more appropriate? The classical Smalltalk dependency
approach essentially pushes specific custom events about changes to make
UIs update. The push starte from a changed message triggered usually by
trying to change an ivar via a method call. That change likely was
triggered by a user action or a timer or a network event -- but that
ultimate origin of the push is essentially ignored. In contrast, I'm
trying to make the Cuis UI model more into a pull model instead. I am
trying to get Cuis UIs to pull data when they need it -- which would
happen whenever the user has generated a UI event being listened for
like a mouse click or a key press. This leverages the idea that the
ultimate origin of any changed method in the other approach is an event
(from a mouse, keyboard, timer, or network). So, that way, hopefully,
the UI behavior will be easier to reason about -- because you won't have
to understand or debug any "push" plumbing set up earlier (because there
won't be any). You just redraw essentially directly in response to
events. Although admittedly you may also redraw somewhat more often and
more widely than you might strictly have to, trading off CPU efficiency
for programmer efficiency.
Thanks for the concise counter-example. Some thoughts on it:
* There is an implict "model" in the example you provided in the sense
that the two temperature values are stored in the two
SimpleNumberEntryMorph instances held onto by the window via the layout
morph. While that obviously works and would be maintainable in this
simple case, I would argue it is less conceptually clear or maintainable
when UIs get more complicated than having those values explicitly stored
directly in an dedicated easily-inspectable object like an application
window or, alternatively, in some application-specific Smalltalk object
that just stores the raw data and has perhaps support methods for
conversion.
* To iteratively develop with such an approach in a live way, blocks
pose a problem in Smalltalk. A specific current version of a block is
stored somewhere when such code is first run. Afterwards, modifying the
setup code won't make existing morphs use any new version of the block
(even if new morphs would get the new block). That is why later
Smalltalk UI approaches lean more towards using method selectors when
initializing UI components. With selectors, the methods referenced by
the selector can be iteratively improved and then UIs that are running
use the new behavior in the updated method.
Coding with stored blocks is essentially a form of "early binding"
compared to methods called via selectors which are a form of "late
binding". And Alan Kay has essentially said that late binding is a key
idea in Smalltalk.
https://stackoverflow.com/questions/367411/early-binding-vs-late-binding-what-are-the-comparative-benefits-and-disadvanta
But the downside to that selector approach is you have to split up all
the behavior into lots of tiny named methods instead of nameless inline
blocks. Having lots of tiny methods is often considered best practice in
the Smalltalk culture -- but it still can also be problematical and is
sometimes derided as a form of spaghetti coding by developers who use
other languages (compared to some sweet spot typically of fewer but
somewhat bigger methods). It's been said that naming things is one of
the two hard parts of programming, so requiring names for lots of
methods potentially creates more unneeded "accidental complexity".
One alternative to lots of these tiny methods is, as Hilaire
insightfully referenced, "to re-construct the whole or part of the GUI
when an event occurs". If we do that, then new blocks (actually, block
closures) can potentially be created and installed into existing
instances of morphs. And then there is no conflict between having a
larger method with a lot of nameless blocks while still also having a
live coding experience in Smalltalk where you can iteratively improve
the behavior of a running UI by modifying that method.
What I ultimately want is more like some hybrid of the example you
supplied (including the use of multiple morphs in a layout) and the
example I supplied (state fetched from ivars).
That might be something close to your example, but defined in, say, a
"buildView" method of SystemWindow subclass like, say,
"TemperatureConverterWindow". The "integerDefault:" values would come
from something like "temperatureF" and "temperatureC" ivars in an
instance of that class. The first SimpleNumberEntryMorph (e1) would
store its changes back to that window instance temperatureF ivar. And
every time someone clicked on the button, that block would fetch the
temperatureF ivar value from the window instance (not the
SimpleNumberEntryMorph) and then set the temperatureC ivar. And then a
rebuild would be triggered (due to the window being "dirty" because
there was a mouse click in it) and as part of the rebuilding process,
the e2 morph would get the updated value from the temperatureC ivar,
notice that value had changed from when it was initialized, and update
itself.
There is, however, nothing much to motivate this seeming extra
complexity of rebuilding on a mouse click in the above (especially in
preference to the concise example you supplied). And that is one limit
of starting with a simple Proof of Concept. The use of the button also
makes things somewhat easier compared to triggering the conversion when
the edited text changes (as in the Mithril examples I linked to for
currency conversion).
So, here is an extra twist to the POC. Imagine there was a "advanced"
section of the UI for displaying the temperature value in Kelvin. So
there might be a toggle whose state was stored in an "showAdvanced" ivar
in the window instance. And when "showAdvanced" was true, then (and only
then) the build process would include a third morph in the layout which
was also initialized from the "temperatureC" ivar in the buildView
method, but which transforming temperatureC into the corresponding
Kelvin value by adding 273.15. And, as one further twist, we want to add
that extra UI behavior while our POC is already running in a window. :-)
I'll play around with the example code you have supplied a bit more in
days to come and see if I can eventually make a better proof of concept
in Cuis more like outlined above, with dynamic morph creation and
removal in response to a mouse event
Thanks for your help moving this experiment forward.
And thanks also for the interesting video link for "Cuis-Smalltalk
Meeting 5/4/2023 - Unicode with Cuis" with the section on drawing the
star. I had previously read your paper on adding full Unicode support to
Cuis (an impressive accomplishment!) but had not seen that video.
Watching the section you pointed to makes me realize that I am
essentially trying to bring some of the feel of the liveliness of
writing drawing code for Smalltalk shapes (which generally pull all
their data each time they are drawn) to writing complex form-like and
browser-like UIs (which could also pull all their data whenever they are
drawn).
--Paul Fernhout (pdfernhout.net)
"The biggest challenge of the 21st century is the irony of technologies
of abundance in the hands of those still thinking in terms of scarcity."
On 5/8/23 10:59, Juan Vuletich via Cuis-dev wrote:
> Hi Paul,
>
> Welcome to the Cuis community!
>
> I derived my understanding more from the code you sent, than from your
> message. First, let me say that neither Cuis nor Morphic in general
> mandate a single style in programming. A fundamental objective of Cuis
> is to enable and encourage experimentation. If you develop a style that
> works for you, that's ok.
>
> If I got you correctly, there are two main ideas in the approach you
> exemplify with your code.
>
> 1) Don't use a separate model. Don't use dependency or events for updates.
> The Browser and most tools included in Cuis use PluggableMorphs, with a
> programming style that comes straight from PluggableViews in MVC. These
> work well, and have served us for a long time. But we can replace them
> if we find a better way. We haven't devoted much energy to this quest so
> far. We've been focused on "lower level" stuff so far, like Morphic
> itself, code handling, Unicode support, and massive cleanup.
>
> 2) Don't use composition of Morphs. Use a single morph that handles all
> events and does all drawing.
> I think that reusable objects are a good idea. What else can I say?
>
> In any case, I gave a shot to your example. Be sure to have the
> 'Cuis-Smalltalk-Widgets' repo. Paste this in a Workspace and evaluate it:
> Feature require: 'Widget-Entry'. "Repo Cuis-Smalltalk-Widgets"
> w := SystemWindow new.
> e1 := SimpleNumberEntryMorph integerDefault: 40 maxNumChars: 9.
> e2 := SimpleNumberEntryMorph integerDefault: 0 maxNumChars: 9.
> b := PluggableButtonMorph
> model: [e2 setValue: (e1 value-32/9*5) asFloat rounded]
> stateGetter: nil
> action: #value
> label: 'Convert'.
> w layoutMorph
> addMorph: e1;
> addMorph: e2;
> addMorph: b.
> w openInWorld.
> w morphExtent: 200 at 160.
> b morphExtent: 140 at 30.
>
> That's all. It doesn't have a model. Doesn't use events or dependency.
> But it does use widgets that help us.
>
> To learn a bit more about what has been the main focus of my work on
> Cuis, see https://www.youtube.com/watch?v=fWJyLsv1mUw especially
> starting at 14:15
More information about the Cuis-dev
mailing list