Most of the Gumbo component skins just depend on (the default) BasicLayout. The geometry of skin parts is defined with a combination of left/right/top/bottom constraints, as well as x,y width, height values. Some component skin layouts include skin parts whose geometry depends on their host component's properties as well as the post-layout bounds of other skin parts. Satisfying these additional dependencies is problematic. The Slider and ScrollBar classes are good examples of the problem.
Slider and Scrollbar and their vertical,horizontal subclasses, are derived from TrackBase.
There are two slider/scrollbar thumb dependencies that aren't captured by their skin's layout:
The thumb's position, size, and visibility depend on the scrollbar's track size.
The thumb's position, size, and visibility depend on scrollbar properties, like value, minimum, and maximum.
The ScrollBar and Slider classes do not override measure(), so the scrollbar's measured size properties don't reflect these dependencies. Note also: at the moment the implementation doesn't mark the thumb includeInLayout=false, it hides the thumb with visible=false, which means that the measured size unconditionally includes the thumb.
ScrollBar and Slider deal with these dependencies at updateDisplayList() time in a way that's not easy to understand or reproduce. In other words, if Slider or ScrollBar are typical of custom skinnable components that our developers want to create, then our developers are likely to be scratching their heads and swearing and taking our names in vain.
Presently the thumb's size and position are updated after the track skinPart layout is complete, in TrackBase::track_updateCompleteHandler(), and in TrackBase::updateDisplayList() (note that ScrollBar is-a TrackBase):
protected function track_updateCompleteHandler(event:Event):void { if (trackSize != tempTrackSize) { thumbSize = calculateThumbSize(); sizeThumb(thumbSize); positionThumb(valueToPosition(value)); tempTrackSize = trackSize; // tempTrackSize is initially NaN } } override protected function updateDisplayList(w:Number, h:Number):void { super.updateDisplayList(w, h); thumbSize = calculateThumbSize(); sizeThumb(thumbSize); positionThumb(valueToPosition(value)); }
To understand what's going on above one must remember that the LayoutManager runs components' updateDisplayList() methods top down, first the parent and then the children, first the SkinnableComponent and then its skin. The SkinnableComponent's updateDisplayList() method just resizes the skin to match its size. The skin's updateDisplayMethod() (i.e. the skin's layout) is responsible for laying out the skin parts.
The goal of these two methods is to fix up the thumb's size/position after the scrollbar's track has been laid out, since the the thumb's size and position depend on the track's size. And the problem with doing that in (just) the track skin's updateComplete callback, is that oftentimes changes to the scrollbar's properties will not cause the track's display list to be updated, i.e. the scrollbar's layout will not resize the track skin part. For example changing the value of a scrollbar neccessitates moving the thumb, but will not cause the track size to change. The updateDisplayList() override handles that because setting the scrollbar's value causes a call to invalidateDisplayList(). Similarly, if the explicit height of the vertical scrollbar is set, the thumb needs to be moved and resized based on the new track size, however in this case the scrollbar's updateDisplayList() method runs before the track skinPart has been laid out. In that case the track's updateCompleteHandler fixes things up.
An alternative to adding the messy cooperative layout linkage in the TrackBase class would be to put it in a custom BasicLayout subclass instead. Sliders and Scrollers would force their skins to employ this layout, as Scroller does with ScrollerLayout. Skin developers wouldn't notice any difference, although skin parts would have to be immediate descendants of the skin to avoid the problem (more below) of an invalid skin part not necessarily invalidating its skin. Presently none of the Spark skins take advantage of the fact that, in theory, skin parts only need to be descendants of the skin.
This approach doesn't address the larger problem, which is that building cooperative layouts is difficult.
A component's updateComplete() event runs after its subtree's layout has finished, so long as the component itself was invalidated. This would be sufficient for cooperative layout except for cases where an element of the subtree updates but the component itself does not. Invalidating the size of an element only triggers a potentially recursive invalidation of the size and display list of its parent, if the element's measured size changes. So, referring to slider/scrollbar, if a change in the track size didn't cause a change in the measured size of the skin, the component itself (the skin's parent) would not be invalidated.
To ensure that invalidating any element of a component's subtree also causes the component itself to invalidate, we'd need an API that allowed a component to request as much. Logically this would require invalidateSize() and invalidateDisplayList() to walk up the tree and invalidate components that had made the request. Since most components will not make this request an efficient (and likely more complex) implementation would be needed.
The existing layout system has several significant limitations in addition to the one outlined here (see below). Rather than patching limitations piecemeal, it might make more sense to make more sweeping revisions in a future release.
At this point, nearing the end of Gumbo API development, it seems prudent to avoid changing the API if the change's benefit is small, or if the change might be obviated by an API revision in the future.
The first option doesn't really address the root problem, since writing a component-specific BasicLayout subclass isn't particularly easy or obvious. A small improvement to the implementation of TrackBase might be realized by using the updateComplete event on the component, rather than the skin, per option 2. Without the addition of the invalidateMeToo API, we'd still have to listen for updates on the Track. So the recommendation is to make the small improvement per option 2, but leave the API alone, per option 3.
In addition the layout lacuna addressed here, there some other well known problem areas.
Much of the complexity in the "cooperative" connection between the slider/scrollbar component and the layout, is support for binding the thumb to a (linear) path during a click-drag-release gesture that begins on the thumb, and keeping the thumb's value in sync and valid per the min/max/etc constraints. If layout's provided explicit support for this, it might simplify APIs as well as simplifying creating this kind of component in the first place.
The support for the "auto" scroll policy in ScrollerLayout requires an extra layout pass, since showing one scrollbar can necessitate showing the other. Although updateDisplayList() can trigger a second layout pass by calling invalidateDisplayList(), doing so can be cumbersome because flags are required to inform the next layout pass that it's not the first. Temporary state is also sometimes needed to communicate information from on pass to the next.
Presently it's possible to ask a component for it's (often cached) preferred size, but not for its preferred size given either the width or height. The is a significant limitation when laying out text, since a text component's height is often tightly linked to its width.