[Cuis-dev] Rerendering when touched: Experiments in simplifying UI development in Cuis inspired by Mithril.js

Paul D. Fernhout pdfernhout at kurtz-fernhout.com
Thu May 4 20:53:46 PDT 2023


Hi Cuis developers. First time Cuis list poster here -- someone who is 
new to Cuis but old to Smalltalk and programming in general. Really 
liking Cuis for the focus on simplicity. But one thing seems still too 
complex, which is that you need to build Morphic UIs with an observer 
pattern using special models instead of just rerendering the UI anytime 
the UI is touched or otherwise "dirty". Attached is some demo code with 
a Proof of Concept experiment of that (arguably) simpler approach in 
Cuis. Below is an explanation of why that design pattern may be of interest.

--Paul Fernhout

==== More details

Let's say you are building a UI in Cuis like, say, a window where you 
can enter a temperature in Fahrenheit, click a button, and have the 
value converted to Celsius. (I know, not great UX...)

To do that now in Cuis, you would make a model (like one derived from 
ActiveModel) and hook up dependencies (like using when:send:to:) so that 
whenever the model containing those temperature values changes, then any 
morph who is interested in that change is notified and can update their 
display or sometimes their internal state.

I started writing an example like that to learn more about Cuis using 
two TextModelMorphs and a PluggableButtonMorph. And after getting to the 
point where I needed to hook up all the dependencies, I thought, I don't 
want to go back thirty years to the worse part of my ObjectWorks and 
VisualWorks days (adding endless #changed messages or similar everywhere 
for example, and some other observer/dependency challenges) when I have 
been happier in recent years developing UIs using a different design 
pattern.

I know there may be some performance efficiency advantages to that 
observer/model pattern. There may also be some UI extension benefits 
like being able to hook up events to other Morphs like Dan Ingalls 
demonstrates with Pronto or is used with VisualAge Smalltalk.

But there is a lot of complexity there too involving defining special 
model classes, establishing dependencies, and avoiding ringing where one 
dependency triggers another that re-triggers the original dependency. 
People may end up with complicated buildMorphicWindow methods in Cuis -- 
and then, when you want to add a new part to your window, you presumably 
usually need to close the window and open it again to rerun that 
definition method to hook up all the dependencies just right.

One alternative to using a model and observer dependencies is, like in 
many 3D graphics system, just re-rendering the entire UI frequently. How 
frequently? For games the rendering may be done every display refresh 
cycle, so like 60 times a second if possible (or more for higher refresh 
screens).

The advantage of rerendering frequently is that you can just draw the UI 
based on the latest data available. That makes the UI much easier to 
reason about because you only need to understand how a drawing method 
takes available data and draws something. You don't have to try to 
reason about dependencies (usually). Also, you can just modify the 
drawing code (which can include defining self-contained components) 
which on a redraw will update your entire UI (including new UI elements 
if needed, depending). And best of all, there are no special models 
needed. You just put your data anywhere in the image in no special way, 
and your drawing algorithm just fetches it as needed.

Of course, rerendering that many times sounds inefficient, especially on 
constrained mobile devices. So, to optimize rerendering, instead of 
brute-force high refresh rates, libraries like Mithril.js for JavaScript 
instead more elegantly use an approach of considering some widget on the 
screen as dirty and needing to be redrawn if it was listening for an 
event that it received. (This is sort of like having handlesMouseDown: 
return true for a morph in Cuis.)

So, if a widget is listening for a mouse down, and it gets one, the 
widget will rerender entirely after handling the mouse down. Same for 
keystrokes or mouse move events. Network events and timers can also 
potentially be hooked into this automatic redraw system, although 
sometimes you just need to add a redraw call yourself for special cases 
or to avoid modifying the base system too much. Also, you may want to 
override the default behavior of automatic redrawing in special cases 
like if you listen for keystrokes but only care about one specific one 
and want to avoid redrawing on others.

While that rerendering approach works OK usually in a web browser page 
where your data usually only affects that window, admittedly it is more 
problematical in a Smalltalk environment where in theory any window can 
draw data from anywhere. Of course, that is what the "Restore display" 
menu item is for in a worst case. But that issue is an admitted weakness 
in this idea. How serious a weakness is still in question.

Mithril is where I first learned this redrawing-when-touched design 
pattern, but a similar approach is also in Elm (which predates Mithril).

For those familiar with some common JavaScript libraries, what Mithril 
does is different than what React does. React essentially only rerenders 
when you call a method to modify state data in a widget. In the end that 
requires all sorts of complex plumbing like Redux to share state across 
an application and only update widgets when the special shared state 
changes. Essentially React in practice using Redux or similar for a 
complex application end up being somewhat like using an observer pattern 
with a special model (a Redux store).

Angular is much closer to this rerendering approach, but rather than 
using the elegant approach Mithril uses of hooking functions that 
connect up UI events so they have an automatic redraw, Angular uses more 
complex interventions in the heart of JavaScript with "Zones" that 
trigger rerenders that makes such applications harder to debug with 
endless extra low-level confusing layers to wade through for any UI 
event handler.

Attached is a simple demonstration of a Mithril-inspired rerendering 
idea I made today in Cuis of a "KfMithrilInspiredExperimentMorph". You 
can click on some areas of a BoxedMorph subclass to enter a temperature 
and to convert it. As the class comment shows, use 
"KfMithrilInspiredExperimentMorph new openInWorld" to start it.

Right now, rather than a button to do the conversion, an area of the 
screen occupied by some text "CONVERT" is used to trigger the 
conversion. Ideally there would be some way to bridge between existing 
Cuis morphs for buttons and text editors and this re-rendering approach 
(at least at first).

To get a sense of how this development process feels, try modifying the 
drawOn: method in some way and clicking on the morph to force a redraw. 
Of course, once you could include other re-rendering Morphs like a 
button or text area in the drawOn: method things would get more 
interesting, but this proof of concept is not there yet.

You could also make changes to the mouseButton1Down:localPosition: 
method -- where unfortunately approximate bounding boxes are currently 
hardcoded for areas with text. Ideally those would be defined as nested 
components somehow. Mithril works by using existing HMTL DOM elements, 
so in theory wrapping existing Cuis morphs at first should be feasible 
(although Mithril uses a VDOM approach to do that, which it would be 
best to avoid in Cuis).

Hoping to spark some ideas and more experiments including by others 
towards further simplifications of Cuis along these lines -- at least as 
an alternative at first for UI building. Someone who already knows Cuis 
well might be able to take this rerendering-on-touched design pattern 
much further much faster than I can. Then maybe if those experiments 
prove the concept then the core Cuis browsers and so on maybe could be 
rewritten this way to increase the simplicity of the core system (at 
least, simplicity from one perspective, even if it might use more CPU 
cycles sometimes perhaps, to be evaluated).

Of course, this idea might not work out in practice. It's an experiment 
after all. Maybe such a transformation of Cuis to this design pattern 
and its quantitative evaluation might even make an interesting academic 
thesis project for someone?

Don't know of any modern Smalltalks that use this design pattern, 
although would like to hear of one if such a thing existed already. 
Perhaps the earliest Smalltalks might have been a bit closer to this 
approach by using polling for events which could lead to a redraw? But 
that is less efficient and has other issues than being purely event-driven.

My thanks go to Juan and everyone else here for remaking Squeak in a 
simpler way to become Cuis -- including because it makes experiments 
like this one more feasible.

--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."
-------------- next part --------------
A non-text attachment was scrubbed...
Name: KfMithrilInspiredExperimentMorph.st
Type: application/vnd.sailingtracker.track
Size: 2313 bytes
Desc: not available
URL: <http://lists.cuis.st/mailman/archives/cuis-dev/attachments/20230504/3c67e5d6/attachment.bin>


More information about the Cuis-dev mailing list