Module: duim-layouts-internals Synopsis: DUIM layouts Author: Scott McKay, Andy Armstrong Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND /// Layout Protocol // The superclass of all panes that do layout on their children, // for example, , , and . // Panes that merely implement 'do-compose-space' (such as all the // gadgets) aren't subclasses of . define protocol-class layout () end protocol-class layout; define constant = one-of(#"top", #"bottom"); define constant = one-of(#"left", #"right"); define constant = one-of(#"top", #"bottom", #"center", #"centre", #"baseline"); define constant = one-of(#"left", #"right", #"center", #"centre"); define protocol <> () function compose-space (pane :: , #key width, height) => (space-req :: ); function do-compose-space (pane :: , #key width, height) => (space-req :: ); function allocate-space (pane :: , width :: , height :: ) => (); function do-allocate-space (pane :: , width :: , height :: ) => (); function default-space-requirement (sheet :: , #key width, min-width, max-width, height, min-height, max-height, #all-keys) => (space-req :: ); function invalidate-space-requirements (sheet :: ) => (); end protocol <>; /// Default methods for layout protocol define method compose-space (pane :: , #key width, height) => (space-req :: ) let space-req = do-compose-space(pane, width: width, height: height); //--- Maybe remove this if the efficiency loss is too huge... let (w, w-, w+, h, h-, h+) = space-requirement-components(pane, space-req); assert("Space requirement components are all integers", instance?(w, ) & instance?(w-, ) & instance?(w+, ) & instance?(h, ) & instance?(h-, ) & instance?(h+, )); space-req end method compose-space; define method allocate-space (pane :: , width :: , height :: ) => () do-allocate-space(pane, width, height); sheet-layed-out?(pane) := #t end method allocate-space; //--- This seems dubious... define method do-allocate-space (pane :: , width :: , height :: ) => () ignore(width, height); #f end method do-allocate-space; /// Layout Mixin define open abstract class () end class ; define open abstract class () end class ; define open abstract class () end class ; // When the region of a sheet gets changed during layout, we want to // re-layout its children to conform to the new geometry define method relayout-children (pane :: ) => () let (width, height) = box-size(pane); allocate-space(pane, width, height) end method relayout-children; define method sheet-layed-out-to-size? (pane :: , width :: , height :: ) => (layed-out? :: ) let (old-width, old-height) = box-size(pane); sheet-layed-out?(pane) & width = old-width & height = old-height end method sheet-layed-out-to-size?; /// Space Requirement Mixin // This class manages a space requirement object for panes that don't // have a 'compose-space' method. It's most useful with . define open abstract class () sealed slot pane-space-requirement :: false-or() = #f, init-keyword: space-requirement:; end class ; define method pane-space-requirement (sheet :: ) => (space-req) #f end method pane-space-requirement; define method initialize (pane :: , #rest initargs, #key space-requirement, width, min-width, max-width, height, min-height, max-height) dynamic-extent(initargs); ignore(width, min-width, max-width, height, min-height, max-height); next-method(); let space-req = space-requirement | apply(default-space-requirement, pane, initargs); pane-space-requirement(pane) := space-req end method initialize; define method validate-sheet-size (pane :: , width :: , height :: ) => () let space-req = pane-space-requirement(pane); when (space-req) let (w, w-, w+, h, h-, h+) = space-requirement-components(pane, space-req); ignore(w, w+, h, h+); when (width < w- | height < h-) warn("Resizing sheet %= to be too small -- %dX%d, not %dX%d", pane, width, height, w-, h-) end end end method validate-sheet-size; define method do-compose-space (pane :: , #rest keys, #key width, height) => (space-req :: ) dynamic-extent(keys); ignore(width, height); pane-space-requirement(pane) | apply(default-space-requirement, pane, keys) end method do-compose-space; define method default-space-requirement (sheet :: , #key width, min-width, max-width, height, min-height, max-height) => (space-req :: ) make(, width: width | $default-sheet-size, height: height | $default-sheet-size, min-width: min-width | 0, min-height: min-height | 0, max-width: max-width | $fill, max-height: max-height | $fill) end method default-space-requirement; // Resets and decaches the space requirement for a single pane define method reset-space-requirement (pane :: , #key space-requirement :: false-or() = #f) => () ignore(space-requirement); #f end method reset-space-requirement; define method reset-space-requirement (pane :: , #key space-requirement :: false-or() = #f) => () pane-space-requirement(pane) := space-requirement end method reset-space-requirement; /// Cached Space Requirements // If both and // appear in the same CPL, then must // precede define open abstract class () // Contains a triple of [space-req, width, height] sealed slot %space-requirement-cache = #f; end class ; define method validate-sheet-size (pane :: , width :: , height :: ) => () let cache = pane.%space-requirement-cache; let space-req = (cache & cache[0]) | pane-space-requirement(pane); when (space-req) let (w, w-, w+, h, h-, h+) = space-requirement-components(pane, space-req); ignore(w, w+, h, h+); when (width < w- | height < h-) warn("Resizing sheet %= to be too small -- %dX%d, not %dX%d", pane, width, height, w-, h-) end end end method validate-sheet-size; // Yes, this really is a wrapper on 'compose-space', not on 'DO-compose-space'. // It's here so we can try using the cache before doing 'do-compose-space' define method compose-space (pane :: , #key width, height) => (space-req :: ) let cache = pane.%space-requirement-cache; without-bounds-checks if (cache & (~width | width = cache[1]) & (~height | height = cache[2])) cache[0] else let space-req = next-method(); unless (cache) cache := vector(#f, #f, #f); pane.%space-requirement-cache := cache end; cache[0] := space-req; cache[1] := width; cache[2] := height; space-req end end end method compose-space; define method sheet-layed-out?-setter (layed-out? == #f, pane :: ) => (layed-out? :: ) pane.%space-requirement-cache := #f; next-method() end method sheet-layed-out?-setter; // Reset an entire sheet hierarchy to an un-layed-out state define method invalidate-space-requirements (sheet :: ) => () local method invalidate (sheet :: ) => () sheet-layed-out?(sheet) := #f end method; do-sheet-tree(invalidate, sheet) end method invalidate-space-requirements; define method reset-space-requirement (pane :: , #key space-requirement :: false-or() = #f) ignore(space-requirement); next-method(); pane.%space-requirement-cache := #f end method reset-space-requirement; /// Client Overridability // Bits 15..16 of 'sheet-flags' are reserved for fixed space requirements define constant %fixed_width :: = #o100000; define constant %fixed_height :: = #o200000; // The idea here is that a user can specify an "overriding" space req for // a pane. Any non-#f component in the space req is used to override the // space requirement that would normally be used for the pane. // Note that this needs to precede some class, such as , that // implements a method for 'compose-space' define open abstract class () sealed slot %override-space-requirement :: false-or() = #f; end class ; define method initialize (pane :: , #key space-requirement, width, min-width, max-width, height, min-height, max-height, resizable? = #t, fixed-width? = ~resizable?, fixed-height? = ~resizable?) next-method(); let bits = logior(if (fixed-width?) %fixed_width else 0 end, if (fixed-height?) %fixed_height else 0 end); sheet-flags(pane) := logior(sheet-flags(pane), bits); when (space-requirement | width | min-width | max-width | height | min-height | max-height) pane.%override-space-requirement := space-requirement | make(, width: width, min-width: min-width, max-width: max-width, height: height, min-height: min-height, max-height: max-height) end end method initialize; define sealed inline method sheet-force-fixed-width? (pane :: ) => (fixed-width? :: ) logand(sheet-flags(pane), %fixed_width) = %fixed_width end method sheet-force-fixed-width?; define sealed inline method sheet-force-fixed-height? (pane :: ) => (fixed-height? :: ) logand(sheet-flags(pane), %fixed_height) = %fixed_height end method sheet-force-fixed-height?; define inline function constrain-size (preferred-size :: , min-size :: , max-size :: ) => (preferred-size :: ) max(min-size, min(max-size, preferred-size)) end function constrain-size; // Yes, this really is a wrapper on 'compose-space', not on 'DO-compose-space' // Note that 'compose-space' constrains the width and height to be within the // newly defined bounds. define method compose-space (pane :: , #key width, height) => (space-req :: ) let fixed-width? = sheet-force-fixed-width?(pane); let fixed-height? = sheet-force-fixed-height?(pane); let space-req = next-method(pane, // Don't take top-down advice for fixed width or height width: ~fixed-width? & width, height: ~fixed-height? & height); let override-space-req = pane.%override-space-requirement; if (override-space-req | fixed-width? | fixed-height?) let (w, w-, w+, h, h-, h+) = space-requirement-components(pane, space-req); let (ow, ow-, ow+, oh, oh-, oh+) = space-requirement-components(pane, override-space-req | space-req); let nmin-width = ow- | w-; let nmax-width = ow+ | w+; let nmin-height = oh- | h-; let nmax-height = oh+ | h+; let nwidth = constrain-size(ow | w, nmin-width, nmax-width); let nheight = constrain-size(oh | h, nmin-height, nmax-height); let (best-width, min-width, max-width) = if (fixed-width?) values(nwidth, nwidth, nwidth) else values(nwidth, nmin-width, nmax-width) end; let (best-height, min-height, max-height) = if (fixed-height?) values(nheight, nheight, nheight) else values(nheight, nmin-height, nmax-height) end; make(, width: best-width, min-width: min-width, max-width: max-width, height: best-height, min-height: min-height, max-height: max-height) else space-req end end method compose-space; /// Wrapping Layout Mixin // This class gets used when the pane in question uses exactly the same // space requirements as the "sum" of its children's requirements. Viewport // panes are good examples of this. define open abstract class () end class ; define method do-compose-space (pane :: , #key width, height) => (space-req :: ) let children = sheet-children(pane); case empty?(children) => default-space-requirement(pane, width: width, height: height); size(children) = 1 => // optimize a very common case... compose-space(children[0], width: width, height: height); otherwise => let (w :: , w- :: , w+ :: , h :: , h- :: , h+ :: ) = space-requirement-components(pane, compose-space(children[0])); for (child :: in children) unless (sheet-withdrawn?(child)) let (srw :: , srw- :: , srw+ :: , srh :: , srh- :: , srh+ :: ) = space-requirement-components(child, compose-space(child)); let (x, y) = values(0, 0); //--- sheet-position(child)... max!(w, srw + x); max!(h, srh + y); max!(w-, srw- + x); max!(h-, srh- + y); max!(w+, srw+ + x); max!(h+, srh+ + y) end end; let w = constrain-size(width | w, w-, w+); let h = constrain-size(height | h, h-, h+); make(, width: w, min-width: w-, max-width: w+, height: h, min-height: h-, max-height: h+); end end method do-compose-space; define method do-allocate-space (pane :: , width :: , height :: ) => () let children = sheet-children(pane); if (size(children) = 1) //--- Do we also want to set the position to (0,0)? let child :: = children[0]; set-sheet-size(child, width, height) else for (child :: in children) unless (sheet-withdrawn?(child)) let space-req = compose-space(child); let (w, w-, w+, h, h-, h+) = space-requirement-components(child, space-req); ignore(w-, w+, h-, h+); set-sheet-size(child, w, h) end end end end method do-allocate-space; /// Generally useful layout function /// Used all over to satisfy constraints // This supa dupa version works by calculating the sizes required // by the ratios and then constrains these sizes by the max and min // sizes. Depending on whether the result is larger or smaller than // the overall required size, the items that cannot be adjusted // in the right direction to help the fit are then fixed at their // limiting size. The algorithm then loops back trying to fit the // remaining items into the remaining space. // // SHEET is the sheet on whose behalf the composition is being done. // DESIRED-SIZE is the desired size for all of the items, and SPACE-REQ // is a space requirement that describes the items' parent. The three // size/min/max functions pull apart space requirements. ITEMS is a // sequence of items (sheets or space requirements), and ITEM-COMPOSER // generates a space requirement from an item. (For example, when ITEMS // is a set of sheets, this function is 'compose-space'; when ITEMS is a // set of space reqs, this function is 'identity'.) define function compose-space-for-items (sheet :: , desired-size :: , space-req :: , items :: , size-function :: , min-function :: , max-function :: , item-composer :: , #key ratios) => (sizes :: ) let n-items :: = size(items); let sizes :: = make(, size: n-items); let desired-ratios :: = make(, size: n-items); let sized? :: limited(, of: , size: n-items) = make(limited(, of: , size: n-items)); let constrained-size :: = 0; let ratio-denominator :: = 0; // Calculate ratios and note withdrawn items for (item in items, index :: from 0) let child = sheet?(item) & item; if (child & sheet-withdrawn?(child)) sizes[index] := 0; sized?[index] := #t; else let ratio = (ratios & index < size(ratios) & ratios[index]) | 1; desired-ratios[index] := ratio; ratio-denominator := ratio-denominator + ratio end end; let done? :: = #f; // Loop until constraints satisfied let size-left :: = desired-size; until (done?) // Calculate desired sizes and note violators let constrained-size :: = 0; let constraining-mins :: = #(); let constraining-maxs :: = #(); for (item in items, item-sized? in sized?, ratio in desired-ratios, index :: from 0) unless (item-sized?) let child = sheet?(item) & item; let item-sr = item-composer(item); let desired-item-size :: = truncate/(size-left * ratio, ratio-denominator); let item-max = max-function(child | sheet, item-sr); let item-min = min-function(child | sheet, item-sr); let constrained-item-size :: = desired-item-size; case (desired-item-size < item-min) => constraining-mins := pair(index, constraining-mins); constrained-item-size := item-min; (desired-item-size > item-max) => constraining-maxs := pair(index, constraining-maxs); constrained-item-size := item-max; otherwise => #f; end; sizes[index] := constrained-item-size; constrained-size := constrained-size + constrained-item-size; end end; // Nail down the ones that can't resize in the right direction to help fit case ((constrained-size < size-left) & ~empty?(constraining-maxs)) => for (index in constraining-maxs) ratio-denominator := ratio-denominator - desired-ratios[index]; size-left := size-left - sizes[index]; sized?[index] := #t; end; ((constrained-size > size-left) & ~empty?(constraining-mins)) => for (index in constraining-mins) ratio-denominator := ratio-denominator - desired-ratios[index]; size-left := size-left - sizes[index]; sized?[index] := #t; end; otherwise => done? := #t; end end; sizes end function compose-space-for-items; /// The basic layout pane // The internal layout panes (boxes, tables) are all built on this. define open abstract class (, , , , ) end class ; /// Horizontal and vertical layout mixins define open abstract class () sealed slot layout-x-spacing :: = 0, init-keyword: x-spacing:; // The sequence here allows different alignments for each row in a table sealed slot layout-y-alignment :: type-union(, ) = #"top", init-keyword: y-alignment:; sealed slot layout-x-ratios :: false-or() = #f, init-keyword: x-ratios:; end class ; define method initialize (pane :: , #key spacing = $unsupplied, ratios = $unsupplied) next-method(); when (supplied?(spacing)) layout-x-spacing(pane) := spacing end; when (supplied?(ratios)) layout-x-ratios(pane) := ratios end; end method initialize; define open abstract class () sealed slot layout-y-spacing :: = 0, init-keyword: y-spacing:; // The sequence here allows different alignments for each row in a table sealed slot layout-x-alignment :: type-union(, ) = #"left", init-keyword: x-alignment:; sealed slot layout-y-ratios :: false-or() = #f, init-keyword: y-ratios:; end class ; define method initialize (pane :: , #key spacing = $unsupplied, ratios = $unsupplied) next-method(); when (supplied?(spacing)) layout-y-spacing(pane) := spacing end; when (supplied?(ratios)) layout-y-ratios(pane) := ratios end; end method initialize; /// Layout borders define open abstract class () sealed slot layout-border :: = 0, init-keyword: border:; end class ; /// Alignment hacking define method sheet-interpret-alignment (sheet :: , alignment :: , key) alignment end method sheet-interpret-alignment; define method sheet-interpret-alignment (sheet :: , alignment :: , key) alignment[key] end method sheet-interpret-alignment; define method sheet-x-alignment (sheet :: , alignment, left :: , right :: , #key key) => (x-position :: ) select (sheet-interpret-alignment(sheet, alignment, key)) #"left" => left; #"right" => right; #"center", #"centre" => floor/(left + right, 2); end end method sheet-x-alignment; define method sheet-y-alignment (sheet :: , alignment, top :: , bottom :: , #key key) => (y-position :: ) select (sheet-interpret-alignment(sheet, alignment, key)) #"top" => top; #"bottom" => bottom; #"center", #"centre" => floor/(top + bottom, 2); end end method sheet-y-alignment; // Returns the aligned Y value for a sheet define method layout-align-sheet-x (layout :: , sheet :: , left :: , right :: , #key key) sheet-x-alignment(sheet, layout-x-alignment(layout), left, right, key: key) end method layout-align-sheet-x; // Returns the aligned Y value for a sheet define method layout-align-sheet-y (layout :: , sheet :: , top :: , bottom :: , #key key) sheet-y-alignment(sheet, layout-y-alignment(layout), top, bottom, key: key) end method layout-align-sheet-y; /// Fixed layouts // Fixed layouts can have any number of children, but there's no layout // policy at all -- the kids worry about their own geometry define open abstract class () end class ; define method do-compose-space (pane :: , #key width, height) => (space-req :: ) default-space-requirement(pane, width: width, height: height) end method do-compose-space; define method do-allocate-space (pane :: , width :: , height :: ) => () ignore(width, height); #f end method do-allocate-space; /// Default implementation define sealed class () keyword accepts-focus?: = #f; end class ; define method class-for-make-pane (framem :: , class == , #key) => (class :: , options :: false-or()) values(, #f) end method class-for-make-pane; define sealed domain make (singleton()); define sealed domain initialize (); /// Pinboards // Pinboards are like fixed layouts, except that they enforce // the space constraints of their children. define open abstract class () end class ; define method do-compose-space (pane :: , #key width, height) => (space-req :: ) default-space-requirement(pane, width: width, height: height) end method do-compose-space; define method do-allocate-space (pane :: , width :: , height :: ) => () ignore(width, height); for (child :: in sheet-children(pane)) unless (sheet-withdrawn?(child)) let space-req = compose-space(child); let (w, w-, w+, h, h-, h+) = space-requirement-components(child, space-req); ignore(w-, w+, h-, h+); set-sheet-size(child, w, h) end end end method do-allocate-space; /// Default implementation define sealed class () keyword accepts-focus?: = #f; end class ; define method class-for-make-pane (framem :: , class == , #key) => (class :: , options :: false-or()) values(, #f) end method class-for-make-pane; define sealed domain make (singleton()); define sealed domain initialize (); /// Stacks // Stack layouts position all of their children at the top-left // one on top of the other. They are primarily useful for creating // tab-controls or wizards where only one child is visible at a time. define open abstract class (, ) end class ; define protocol <> () getter stack-layout-mapped-page (stack :: ) => (page :: false-or()); setter stack-layout-mapped-page-setter (page :: false-or(), stack :: ) => (page :: false-or()); end protocol <>; define method initialize (pane :: , #key mapped-page :: false-or()) => () next-method(); let mapped-page = mapped-page | begin let children = sheet-children(pane); ~empty?(children) & children[0] end; stack-layout-mapped-page(pane) := mapped-page end method initialize; //--- Children get added withdrawn, and need to be mapped using //--- stack-layout-mapped-page-setter. define method note-child-added (sheet :: , child :: ) => () next-method(); sheet-withdrawn?(child) := #t end method note-child-added; define method do-compose-space (pane :: , #key width: requested-width, height: requested-height) => (space-req :: ) let children = sheet-children(pane); if (empty?(children)) default-space-requirement(pane, width: requested-width, height: requested-height) else let border*2 = layout-border(pane) * 2; let extra-width :: = border*2; let extra-height :: = border*2; let child-width = requested-width & (requested-width - extra-width); let child-height = requested-height & (requested-height - extra-height); let width :: = 0; let height :: = 0; let min-width :: = 0; let min-height :: = 0; let max-width :: = 0; let max-height :: = 0; for (child in children) let space-req = compose-space(child, width: child-width, height: child-height); let (w, w-, w+, h, h-, h+) = space-requirement-components(child, space-req); max!(width, w); max!(min-width, w-); max!(max-width, w+); max!(height, h); max!(min-height, h-); max!(max-height, h+) end; inc!(width, extra-width); inc!(min-width, extra-width); inc!(max-width, extra-width); inc!(height, extra-height); inc!(min-height, extra-height); inc!(max-height, extra-height); let width = requested-width | width; let height = requested-height | height; let best-width = constrain-size(width, min-width, max-width); let best-height = constrain-size(height, min-height, max-height); make(, width: best-width, min-width: min-width, max-width: max-width, height: best-height, min-height: min-height, max-height: max-height) end end method do-compose-space; define method do-allocate-space (pane :: , width :: , height :: ) => () let border = layout-border(pane); let border*2 = border * 2; let width = width - border*2; let height = height - border*2; for (child in sheet-children(pane)) let space-req = compose-space(child, width: width, height: height); let (w, w-, w+, h, h-, h+) = space-requirement-components(child, space-req); ignore(w-, w+, h-, h+); set-sheet-edges(child, border, border, border + w, border + h) end end method do-allocate-space; /// Default implementation define sealed class () keyword accepts-focus?: = #f; end class ; define method class-for-make-pane (framem :: , class == , #key) => (class :: , options :: false-or()) values(, #f) end method class-for-make-pane; define sealed domain make (singleton()); define sealed domain initialize (); define sealed method stack-layout-mapped-page (stack :: ) => (page :: false-or()) block (return) for (child :: in sheet-children(stack)) unless (sheet-withdrawn?(child)) return(child) end end end end method stack-layout-mapped-page; define sealed method stack-layout-mapped-page-setter (page :: false-or(), stack :: ) => (page :: false-or()) let damaged-width :: = 0; let damaged-height :: = 0; let old-page = #f; // Be conservative and ensure that we start with everything withdrawn for (child :: in sheet-children(stack)) if (~sheet-withdrawn?(child)) old-page := child; if (child ~= page) sheet-withdrawn?(child, do-repaint?: #f) := #t; let (child-width, child-height) = sheet-size(child); max!(damaged-width, child-width); max!(damaged-height, child-height); end end end; when (page & page ~= old-page) // Un-withdraw the new child so that we can do layout, if necessary sheet-withdrawn?(page) := #f; when (sheet-attached?(stack)) let (width, height) = sheet-size(stack); // Note that this apparent re-layout will be quite inexpensive // because we've already layed out all the pages in the stack let space-req = compose-space(page, width: width, height: height); let (w, w-, w+, h, h-, h+) = space-requirement-components(page, space-req); ignore(w-, w+, h-, h+); // We repaint if the child isn't mirrored, or if the old damaged // region extends beyond the new child's own region. if (~sheet-direct-mirror(page) | damaged-width > w | damaged-height > h) clear-box(stack, 0, 0, damaged-width, damaged-height) end; set-sheet-edges(page, 0, 0, w, h); //---*** Can this be removed if we fix 'set-sheet-edges'? update-all-mirror-positions(page); // Now we can finally display the new child! sheet-mapped?(page, clear?: #f) := sheet-mapped?(stack); end end; page end method stack-layout-mapped-page-setter;