Hi Aliaksei,
this is a good and interesting read, especially the feedback on making
it fast (solution to some performance problems in Morphic).
However, I have the feeling that the css inspired layout syntax is a
step backward on what is/was Morphic (which is using other morphs to
constrain and adjust layout in the same way as TeX/LaTeX instead of
adding padding, align). Was that syntax choosen because layouting with
filler Morphs is considered too slow or too complex to be understandable?
Regards,
Thierry
Le 23/03/2015 20:54, Aliaksei Syrel a écrit :
Hi,
Sorry if my reply will be too long, but I tried to summarise our
experience with Morphic/Brick and give some useful feedback or even
provide ideas. Who wants will read :)
Brick is not a thin layer anymore.
It depends on what you mean under 'a thin layer' ;) Anyway, Brick was
born out of need. Spotter is completely built using Brick and Rubric. No
morphs are used at all (except Rubric of course). All Bricks in Spotter
in the end are subclasses of GLMBrick, which uses from original Morph
only Canvas (without drawing) and MorphicEvents. Everything else was
rewritten from scratch. During some period of time Brick was able to
render itself on Athens canvas, but we dropped it because of Font issues
in athens.
Spotter is not the only tool written using Brick, GLMPager - a pane
pager in GT-Inspector, GT-Playground and GT-Debugger is also completely
done using Brick. Almost all tab labels and tab selector in GT tools are
Bricks.
I could say that we collected quite some experience building it. Now I
would like to enumerate features of UI framework we used to solve
encountered problems.
* Local bound coordinates.
Nothing to say more here :) It's somehow obvious to have.
* Detects graph of Bricks that should be invalidated when Brick
triggers invalidation. So there is no need to invalidate the whole
graph.
o For example if Brick fills parent it means that it doesn't
depend on children (simple and most common situation). So if any
child triggers invalidation there is no need to invalidate
parent as it doesn't depend on them. The same works in reversed way.
o Build graph separately for each dimension: width and height. If
only height of Brick was changed and children depend only on
width, there is no need to invalidate them => x2 faster.
o Invalidate resizing and re layouting separately. If there is no
need to reposition bricks when size changes we just skip it.
* Link subbricks in a DoubleLinked list. So subbrickAfter and
subbrickBefore could be done in O(1). Morphic does it in O(n). It
iterates over the whole submorphs collection of the owner finds
index if current morph and then returns submorphs at: index +/-1.
submorphAfter is used when morphic tries to detect which morph
should take focus. Take a look at: Morph>>nextMorphWantingFocus. It
iterates over owner's subbricks and for each asks submorphAfter
(O(n)) - resulting time O(n^2)! That is why opening a window with a
lot of submorphs takes so many time, especially system browser for
big classes.
* Native support of /paddings/ with css-like syntax:
padding: anArrayOrInteger
paddingTop: anInteger
paddingRight: anInteger
paddingBottom: anInteger
paddingLeft: anInteger
padding: 10. "top, right, bottom, left padding are 10px"
padding: #(10 20). "top, bottom padding 10px. left, right padding 20px"
padding: #(10 20 30). "top 10px. left, right 20px. bottom 30px"
padding: #(10 20 30 40). "top 10px, right 20px, bottom 30px, left 40px"
* Native support of /margins/ with css-like syntax:
margin: anArrayOrInteger
marginTop: anInteger
marginRight: anInteger
marginBottom: anInteger
marginLeft: anInteger
Have the same syntax as padding.
* Native support of dynamic /minHeight, minWidth, maxHeight, maxWidth/
minWidth: anIntegerOrBlock "actually any object that
implements brickValue:"
maxHeight: anIntegerOrBlock
minHeight: [ : brick | brick label width * 2 ].
minHeight: 200.
maxWidth: [ self height / 2 ].
maxWidth: 100.
* Native support of /z-index/, so we don't rely on subbricks' order
any more if one brick should be drawn above/below others. Works the
same as css one.
zIndex: 2. "default value is 1"
* Native support of /floating /bricks. Floating bricks don't
influence on any other neighbour bricks in layout.
floating: aSymbol
floating: #right. "top right brick's corner matches top right
parent's corner + corresponding paddings and margins"
floating: #center. "will be in the center of the parent and doesn't
care about other subbricks"
Example use case is scroll bar in spotter - it has floating:
#right and zIndex: 2 so it appears above all list elements. (+ some
margins to make it fancy).
* Native support of horizontal and vertical /aligning/.
hAlign: aSymbol
vAlign: aSymbol
hAlign: #right.
vAlign: #center.
* Horizontal and vertical /shrinking/ to fit children. It is
possible to have in one brick two subbricks where one shrink wraps
and other fills parent.
hShrinkWrap.
vShrinkWrap.
* Horizontal and vertical /filling/ with support of percents and
min/max height/width. If in brick one child shrink wraps width (or
have static width) and other fills 100% of parent's width, filling
one will take only available space left by shrinking (or static) brick.
hSpaceFill: aFloat - fills aFloat percents of parent's width
hSpaceFill. - alias for 100 percents.
vSpaceFill: aFloat - fills aFloat percents of parent's height
vSpaceFill. - alias for 100 percents.
* Transparent /border/ styling with possibility to separately
configure each side.
borderWidth: anArrayOrInteger.
borderColor: anArrayOrColor
The same syntax as used for paddings/margins
* Native support of smooth gradient /box-shadows/ with rounded corners:
shadowColor: aColor.
shadowWidth: anInteger.
* Layout events such as:
onLayouted - "called when brick was completely resized and
layouted in its parent"
onChildrenLayouted - "called when all children are completely
resized and layouted"
Use case: implementing flexible and responsive UI. Used in
GLMLabelBrick to shrink text and add '...' when it doesn't fit in
parent. Example is tab labels in Playground. Morphic version of tabs
shrinks text and change ui of the tab during drawing. WAT?
* Application dependent themes. To make app work perfect all UI
elements should have the same style. In case of spotter, item
preview is created outside of spotter and there is no way using
existing morph theme mechanism to customize its look&feel especially
for spotter, because widgets always refer to global UITheme. Brick
extends theming mechanism and allows to extend existing themes
specifically for needed application and it will be applied to the
whole tree of subbricks. For example spotter can completely change
theme in runtime to dark (without even closing it).
"in context of SpotterMorph" self themer:
GTSpotterBrickDarkThemer new. (or just replace first line in
initialize)
Even having white theme chosen in settings. (all rubric fields
become dark too).
* What we really want to have in Bloc is an easy way to detect that
user clicked outside of specific morph.
What do you want with brick ? A new framework or just an
"implementation detail" for spotter?
It is *really* important to clarify this.
Now it becomes more stable, api changes less and less. We already have
quite a number of tests for layouting logic and documentation for basic
widgets.
What we want from brick... I think we want to have a way to build fast
and cheap unique and complex UIs. While it will fit our needs in GT Team
we will use it. We really would like to move all tools to Bloc as soon
as possible, but we just need something right now that works and can be
used in current version. Of course it would be great to cooperate to
make Bloc release sooner.
Some time ago I tried to port Brick on top of Bloc saving all business
logic untouched and only changing events from morphic ones to bloc and
FormCanvas to AthensCanvas. It was rather successful. I will be happy if
it would be possible to do everything or almost everything mentioned
above in Bloc. I think we will never (but who knows) announce Brick as
separate project. In our minds it is just useful helper library.
Thanks for reading :)
Cheers,
Alex