[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