diff --git a/artist/text.go b/artist/text.go index 2953c03..1cbd460 100644 --- a/artist/text.go +++ b/artist/text.go @@ -14,10 +14,11 @@ type characterLayout struct { } type wordLayout struct { - position image.Point - width int - spaceAfter int - text []characterLayout + position image.Point + width int + spaceAfter int + breaksAfter int + text []characterLayout } // Align specifies a text alignment method. @@ -167,13 +168,16 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) { func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) { if !drawer.layoutClean { drawer.recalculate() } metrics := drawer.face.Metrics() - dot := fixed.Point26_6 { 0, 0 } + dot := fixed.Point26_6 { 0, metrics.Height } for _, word := range drawer.layout { - dot.X += fixed.Int26_6((word.width + word.spaceAfter) << 6) - - if word.width + word.position.X > width && word.position.X > 0 { + if word.width + dot.X.Round() > width { dot.Y += metrics.Height - dot.X = fixed.Int26_6(word.width << 6) + dot.X = 0 + } + dot.X += fixed.I(word.width + word.spaceAfter) + if word.breaksAfter > 0 { + dot.Y += fixed.I(word.breaksAfter).Mul(metrics.Height) + dot.X = 0 } } @@ -246,6 +250,7 @@ func (drawer *TextDrawer) recalculate () { if character == '\n' { dot.Y += metrics.Height dot.X = 0 + word.breaksAfter ++ previousCharacter = character index ++ } else { @@ -290,7 +295,6 @@ func (drawer *TextDrawer) recalculate () { if drawer.wrap { drawer.layoutBounds.Max.X = drawer.width - println("aaa") } else { drawer.layoutBounds.Max.X = horizontalExtent } diff --git a/backends/x/window.go b/backends/x/window.go index aeffb03..58fa4df 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -89,6 +89,7 @@ func (window *Window) Adopt (child tomo.Element) { child.SetParentHooks (tomo.ParentHooks { Draw: window.childDrawCallback, MinimumSizeChange: window.childMinimumSizeChangeCallback, + FlexibleHeightChange: window.resizeChildToFit, SelectionRequest: window.childSelectionRequestCallback, }) @@ -202,11 +203,27 @@ func (window *Window) redrawChildEntirely () { func (window *Window) resizeChildToFit () { window.skipChildDrawCallback = true - if child, ok := window.child.(tomo.Expanding); ok { + if child, ok := window.child.(tomo.Flexible); ok { minimumHeight := child.MinimumHeightFor(window.metrics.width) - _, minimumWidth := child.MinimumSize() - window.childMinimumSizeChangeCallback ( - minimumWidth, minimumHeight) + minimumWidth, _ := child.MinimumSize() + + icccm.WmNormalHintsSet ( + window.backend.connection, + window.xWindow.Id, + &icccm.NormalHints { + Flags: icccm.SizeHintPMinSize, + MinWidth: uint(minimumWidth), + MinHeight: uint(minimumHeight), + }) + + if window.metrics.height >= minimumHeight && + window.metrics.width >= minimumWidth { + + window.child.Resize ( + window.metrics.width, + window.metrics.height) + window.redrawChildEntirely() + } } else { window.child.Resize ( window.metrics.width, @@ -261,6 +278,20 @@ func (window *Window) childSelectionRequestCallback () (granted bool) { return true } +func (window *Window) childSelectionMotionRequestCallback ( + direction tomo.SelectionDirection, +) ( + granted bool, +) { + if child, ok := window.child.(tomo.Selectable); ok { + if !child.HandleSelection(direction) { + child.HandleDeselection() + } + return true + } + return true +} + func (window *Window) pushRegion (region image.Rectangle) { if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) diff --git a/element.go b/element.go index 61a4b1f..06e37be 100644 --- a/element.go +++ b/element.go @@ -3,7 +3,8 @@ package tomo // ParentHooks is a struct that contains callbacks that let child elements send // information to their parent element without the child element knowing // anything about the parent element or containing any reference to it. When a -// parent element adopts a child element, it must set these callbacks. +// parent element adopts a child element, it must set these callbacks. They are +// allowed to be nil. type ParentHooks struct { // Draw is called when a part of the child element's surface is updated. // The updated region will be passed to the callback as a sub-image. @@ -14,12 +15,20 @@ type ParentHooks struct { // have already been resized and there is no need to send it a resize // event. MinimumSizeChange func (width, height int) + + // FlexibleHeightChange is called when the parameters affecting the + // element's expanding height have changed. + FlexibleHeightChange func () // SelectionRequest is called when the child element element wants // itself to be selected. If the parent element chooses to grant the // request, it must send the child element a selection event and return // true. SelectionRequest func () (granted bool) + + // SelectionMotionRequest is called when the child element wants the + // parent element to select the previous/next element in relation to it. + SelectionMotionRequest func (direction SelectionDirection) (granted bool) } // RunDraw runs the Draw hook if it is not nil. If it is nil, it does nothing. @@ -37,6 +46,14 @@ func (hooks ParentHooks) RunMinimumSizeChange (width, height int) { } } +// RunFlexibleHeightChange runs the ExpandingHeightChange hook if it is not +// nil. If it is nil, it does nothing. +func (hooks ParentHooks) RunFlexibleHeightChange () { + if hooks.FlexibleHeightChange != nil { + hooks.FlexibleHeightChange() + } +} + // RunSelectionRequest runs the SelectionRequest hook if it is not nil. If it is // nil, it does nothing. func (hooks ParentHooks) RunSelectionRequest () (granted bool) { @@ -46,6 +63,19 @@ func (hooks ParentHooks) RunSelectionRequest () (granted bool) { return } +// RunSelectionMotionRequest runs the SelectionMotionRequest hook if it is not +// nil. If it is nil, it does nothing. +func (hooks ParentHooks) RunSelectionMotionRequest ( + direction SelectionDirection, +) ( + granted bool, +) { + if hooks.SelectionMotionRequest != nil { + granted = hooks.SelectionMotionRequest(direction) + } + return +} + // Element represents a basic on-screen object. type Element interface { // Element must implement the Canvas interface. Elements should start @@ -153,9 +183,9 @@ type MouseTarget interface { HandleScroll (x, y int, deltaX, deltaY float64) } -// Expanding represents an element who's preferred minimum height can change in +// Flexible represents an element who's preferred minimum height can change in // response to its width. -type Expanding interface { +type Flexible interface { Element // HeightForWidth returns what the element's minimum height would be if @@ -170,6 +200,6 @@ type Expanding interface { // minimum size that the element may be resized to. // // It is important to note that if a parent container checks for - // expanding chilren, it itself will likely need to be expanding. + // flexible chilren, it itself will likely need to be flexible. MinimumHeightFor (width int) (height int) } diff --git a/elements/basic/button.go b/elements/basic/button.go index bb03733..9bfe0ed 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -103,6 +103,7 @@ func (element *Button) HandleSelection ( ) ( accepted bool, ) { + direction = direction.Canon() if !element.enabled { return false } if element.selected && direction != tomo.SelectionDirectionNeutral { return false diff --git a/elements/basic/container.go b/elements/basic/container.go index e8d58c9..1e4e785 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -18,6 +18,7 @@ type Container struct { warping bool selected bool selectable bool + flexible bool } // NewContainer creates a new container. @@ -43,16 +44,24 @@ func (element *Container) SetLayout (layout tomo.Layout) { // whatever way is defined by the current layout. func (element *Container) Adopt (child tomo.Element, expand bool) { child.SetParentHooks (tomo.ParentHooks { + Draw: func (region tomo.Canvas) { + element.drawChildRegion(child, region) + }, MinimumSizeChange: func (int, int) { element.updateMinimumSize() }, + FlexibleHeightChange: element.updateMinimumSize, SelectionRequest: func () (granted bool) { child, selectable := child.(tomo.Selectable) if !selectable { return } return element.childSelectionRequestCallback(child) }, - Draw: func (region tomo.Canvas) { - element.drawChildRegion(child, region) + SelectionMotionRequest: func ( + direction tomo.SelectionDirection, + ) ( + granted bool, + ) { + return element.core.RequestSelectionMotion(direction) }, }) element.children = append (element.children, tomo.LayoutEntry { @@ -61,7 +70,7 @@ func (element *Container) Adopt (child tomo.Element, expand bool) { }) element.updateMinimumSize() - element.updateSelectable() + element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.recalculate() element.draw() @@ -106,7 +115,7 @@ func (element *Container) Disown (child tomo.Element) { } element.updateMinimumSize() - element.updateSelectable() + element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.recalculate() element.draw() @@ -119,7 +128,7 @@ func (element *Container) DisownAll () { element.children = nil element.updateMinimumSize() - element.updateSelectable() + element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.recalculate() element.draw() @@ -176,8 +185,6 @@ func (element *Container) Resize (width, height int) { element.draw() } -// TODO: implement KeyboardTarget - func (element *Container) HandleMouseDown (x, y int, button tomo.Button) { child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget) if !handlesMouse { return } @@ -223,7 +230,7 @@ func (element *Container) HandleKeyDown ( }) } -func (element *Container) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { +func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { element.forSelected (func (child tomo.Selectable) bool { child0, handlesKeyboard := child.(tomo.KeyboardTarget) if handlesKeyboard { @@ -293,6 +300,10 @@ func (element *Container) HandleSelection (direction tomo.SelectionDirection) (o return false } +func (element *Container) MinimumHeightFor (width int) (height int) { + return element.layout.MinimumHeightFor(element.children, width) +} + func (element *Container) HandleDeselection () { element.selected = false element.forSelected (func (child tomo.Selectable) bool { @@ -319,6 +330,15 @@ func (element *Container) forSelectable (callback func (child tomo.Selectable) b } } +func (element *Container) forFlexible (callback func (child tomo.Flexible) bool) { + for _, entry := range element.children { + child, selectable := entry.Element.(tomo.Flexible) + if selectable { + if !callback(child) { break } + } + } +} + func (element *Container) forSelectableBackward (callback func (child tomo.Selectable) bool) { for index := len(element.children) - 1; index >= 0; index -- { child, selectable := element.children[index].Element.(tomo.Selectable) @@ -338,12 +358,17 @@ func (element *Container) firstSelected () (index int) { return -1 } -func (element *Container) updateSelectable () { +func (element *Container) reflectChildProperties () { element.selectable = false element.forSelectable (func (tomo.Selectable) bool { element.selectable = true return false }) + element.flexible = false + element.forFlexible (func (tomo.Flexible) bool { + element.flexible = true + return false + }) if !element.selectable { element.selected = false } @@ -367,8 +392,11 @@ func (element *Container) childSelectionRequestCallback ( } func (element *Container) updateMinimumSize () { - element.core.SetMinimumSize ( - element.layout.MinimumSize(element.children, 1e9)) + width, height := element.layout.MinimumSize(element.children) + if element.flexible { + height = element.layout.MinimumHeightFor(element.children, width) + } + element.core.SetMinimumSize(width, height) } func (element *Container) recalculate () { diff --git a/elements/basic/label.go b/elements/basic/label.go index 1b33373..b497efc 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -27,6 +27,7 @@ func NewLabel (text string, wrap bool) (element *Label) { return } +// Resize resizes the label and re-wraps the text if wrapping is enabled. func (element *Label) Resize (width, height int) { element.core.AllocateCanvas(width, height) if element.wrap { @@ -37,6 +38,17 @@ func (element *Label) Resize (width, height int) { return } +// MinimumHeightFor returns the reccomended height for this element based on the +// given width in order to allow the text to wrap properly. +func (element *Label) MinimumHeightFor (width int) (height int) { + if element.wrap { + return element.drawer.ReccomendedHeightFor(width) + } else { + _, height = element.MinimumSize() + return + } +} + // SetText sets the label's text. func (element *Label) SetText (text string) { if element.text == text { return } @@ -76,6 +88,7 @@ func (element *Label) updateMinimumSize () { if em < 1 { em = theme.Padding() } element.core.SetMinimumSize ( em, element.drawer.LineHeight().Round()) + element.core.NotifyFlexibleHeightChange() } else { bounds := element.drawer.LayoutBounds() element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) diff --git a/elements/core/core.go b/elements/core/core.go index 9f0fd3e..91ceac9 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -80,6 +80,18 @@ func (control CoreControl) RequestSelection () (granted bool) { return control.core.hooks.RunSelectionRequest() } +// RequestSelectionMotion requests that the element's parent deselect this +// element and select the one to the left or right of it, depending on the +// direction. If the requests was granted, it returns true. If it was denied, it +// returns false. +func (control CoreControl) RequestSelectionMotion ( + direction tomo.SelectionDirection, +) ( + granted bool, +) { + return control.core.hooks.RunSelectionMotionRequest(direction) +} + // HasImage returns true if the core has an allocated image buffer, and false if // it doesn't. func (control CoreControl) HasImage () (has bool) { @@ -101,7 +113,6 @@ func (control CoreControl) PushAll () { // AllocateCanvas resizes the canvas, constraining the width and height so that // they are not less than the specified minimum width and height. func (control *CoreControl) AllocateCanvas (width, height int) { - width, height, _ = control.ConstrainSize(width, height) control.core.canvas = tomo.NewBasicCanvas(width, height) control.BasicCanvas = control.core.canvas } @@ -132,6 +143,12 @@ func (control CoreControl) SetMinimumSize (width, height int) { } } +// NotifyFlexibleHeightChange notifies the parent element that this element's +// flexible height has changed. +func (control CoreControl) NotifyFlexibleHeightChange () { + control.core.hooks.RunFlexibleHeightChange() +} + // ConstrainSize contstrains the specified width and height to the minimum width // and height, and returns wether or not anything ended up being constrained. func (control CoreControl) ConstrainSize ( diff --git a/examples/flow/main.go b/examples/flow/main.go index 5d9d7ef..40f09dd 100644 --- a/examples/flow/main.go +++ b/examples/flow/main.go @@ -21,7 +21,7 @@ func run () { world.Stages = map [string] func () { "start": func () { label := basic.NewLabel ( - "you are standing next to a river.", false) + "you are standing next to a river.", true) button0 := basic.NewButton("go in the river") button0.OnClick(world.SwitchFunc("wet")) @@ -41,7 +41,7 @@ func run () { "wet": func () { label := basic.NewLabel ( "you get completely soaked.\n" + - "you die of hypothermia.", false) + "you die of hypothermia.", true) button0 := basic.NewButton("try again") button0.OnClick(world.SwitchFunc("start")) @@ -58,7 +58,7 @@ func run () { "house": func () { label := basic.NewLabel ( "you are standing in front of a delapidated " + - "house.", false) + "house.", true) button1 := basic.NewButton("go inside") button1.OnClick(world.SwitchFunc("inside")) @@ -78,7 +78,7 @@ func run () { "it is dark, but rays of light stream " + "through the window.\n" + "there is nothing particularly interesting " + - "here.", false) + "here.", true) button0 := basic.NewButton("go back outside") button0.OnClick(world.SwitchFunc("house")) @@ -92,7 +92,7 @@ func run () { "bear": func () { label := basic.NewLabel ( "you come face to face with a bear.\n" + - "it eats you (it was hungry).", false) + "it eats you (it was hungry).", true) button0 := basic.NewButton("try again") button0.OnClick(world.SwitchFunc("start")) diff --git a/examples/horizontalLayout/main.go b/examples/horizontalLayout/main.go index 28f1699..f35bf54 100644 --- a/examples/horizontalLayout/main.go +++ b/examples/horizontalLayout/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements/basic" -import "git.tebibyte.media/sashakoshka/tomo/elements/testing" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" func main () { @@ -11,15 +10,15 @@ func main () { } func run () { - window, _ := tomo.NewWindow(2, 2) + window, _ := tomo.NewWindow(256, 2) window.SetTitle("horizontal stack") container := basic.NewContainer(layouts.Horizontal { true, true }) window.Adopt(container) - container.Adopt(testing.NewMouse(), true) - container.Adopt(basic.NewLabel("<- left\nright ->", false), false) - container.Adopt(testing.NewMouse(), true) + container.Adopt(basic.NewLabel("this is sample text", true), true) + container.Adopt(basic.NewLabel("this is sample text", true), true) + container.Adopt(basic.NewLabel("this is sample text", true), true) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/label/main.go b/examples/label/main.go index 3e5611e..9a0f59b 100644 --- a/examples/label/main.go +++ b/examples/label/main.go @@ -9,7 +9,7 @@ func main () { } func run () { - window, _ := tomo.NewWindow(480, 360) + window, _ := tomo.NewWindow(480, 2) window.SetTitle("example label") window.Adopt(basic.NewLabel(text, true)) window.OnClose(tomo.Stop) diff --git a/examples/verticalLayout/main.go b/examples/verticalLayout/main.go index c96b86c..287d239 100644 --- a/examples/verticalLayout/main.go +++ b/examples/verticalLayout/main.go @@ -17,7 +17,7 @@ func run () { container := basic.NewContainer(layouts.Vertical { true, true }) window.Adopt(container) - label := basic.NewLabel("it is a label hehe", false) + label := basic.NewLabel("it is a label hehe", true) button := basic.NewButton("drawing pad") okButton := basic.NewButton("OK") button.OnClick (func () { diff --git a/layout.go b/layout.go index 7a3e458..4482d2e 100644 --- a/layout.go +++ b/layout.go @@ -20,8 +20,11 @@ type Layout interface { Arrange (entries []LayoutEntry, width, height int) // MinimumSize returns the minimum width and height that the layout - // needs to properly arrange the given slice of layout entries, given a - // "suqeeze" width so that the height can be determined for elements - // fulfilling the Expanding interface. - MinimumSize (entries []LayoutEntry, squeeze int) (width, height int) + // needs to properly arrange the given slice of layout entries. + MinimumSize (entries []LayoutEntry) (width, height int) + + // MinimumHeightFor Returns the minimum height the layout needs to lay + // out the specified elements at the given width, taking into account + // flexible elements. + MinimumHeightFor (entries []LayoutEntry, squeeze int) (height int) } diff --git a/layouts/horizontal.go b/layouts/horizontal.go index 5ca5000..ebe3a2a 100644 --- a/layouts/horizontal.go +++ b/layouts/horizontal.go @@ -72,7 +72,6 @@ func (layout Horizontal) Arrange (entries []tomo.LayoutEntry, width, height int) // arrange the given list of entries. func (layout Horizontal) MinimumSize ( entries []tomo.LayoutEntry, - squeeze int, ) ( width, height int, ) { @@ -93,3 +92,62 @@ func (layout Horizontal) MinimumSize ( } return } + +func (layout Horizontal) MinimumHeightFor ( + entries []tomo.LayoutEntry, + width int, +) ( + height int, +) { + // TODO: maybe put calculating the expanding element width in a separate + // method + if layout.Pad { + width -= theme.Padding() * 2 + } + freeSpace := width + expandingElements := 0 + + // count the number of expanding elements and the amount of free space + // for them to collectively occupy + for index, entry := range entries { + if entry.Expand { + expandingElements ++ + } else { + entryMinWidth, _ := entry.MinimumSize() + freeSpace -= entryMinWidth + } + if index > 0 && layout.Gap { + freeSpace -= theme.Padding() + } + } + expandingElementWidth := 0 + if expandingElements > 0 { + expandingElementWidth = freeSpace / expandingElements + } + + x, y := 0, 0 + if layout.Pad { + x += theme.Padding() + y += theme.Padding() + } + + // set the size and position of each element + for index, entry := range entries { + entryWidth, entryHeight := entry.MinimumSize() + if entry.Expand { + entryWidth = expandingElementWidth + } + if child, flexible := entry.Element.(tomo.Flexible); flexible { + entryHeight = child.MinimumHeightFor(entryWidth) + } + if entryHeight > height { height = entryHeight } + + x += entryWidth + if index > 0 && layout.Gap { x += theme.Padding() } + } + + if layout.Pad { + height += theme.Padding() * 2 + } + return +} diff --git a/layouts/vertical.go b/layouts/vertical.go index c366419..e3f4a50 100644 --- a/layouts/vertical.go +++ b/layouts/vertical.go @@ -26,12 +26,21 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) { expandingElements := 0 // count the number of expanding elements and the amount of free space - // for them to collectively occupy + // for them to collectively occupy, while gathering minimum heights. + minimumHeights := make([]int, len(entries)) for index, entry := range entries { + var entryMinHeight int + + if child, flexible := entry.Element.(tomo.Flexible); flexible { + entryMinHeight = child.MinimumHeightFor(width) + } else { + _, entryMinHeight = entry.MinimumSize() + } + minimumHeights[index] = entryMinHeight + if entry.Expand { expandingElements ++ } else { - _, entryMinHeight := entry.MinimumSize() freeSpace -= entryMinHeight } if index > 0 && layout.Gap { @@ -58,7 +67,7 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) { if entry.Expand { entryHeight = expandingElementHeight } else { - _, entryHeight = entry.MinimumSize() + entryHeight = minimumHeights[index] } y += entryHeight entryBounds := entry.Bounds() @@ -72,7 +81,6 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) { // arrange the given list of entries. func (layout Vertical) MinimumSize ( entries []tomo.LayoutEntry, - squeeze int, ) ( width, height int, ) { @@ -93,3 +101,32 @@ func (layout Vertical) MinimumSize ( } return } + +// MinimumHeightFor Returns the minimum height the layout needs to lay out the +// specified elements at the given width, taking into account flexible elements. +func (layout Vertical) MinimumHeightFor ( + entries []tomo.LayoutEntry, + width int, +) ( + height int, +) { + if layout.Pad { + width -= theme.Padding() * 2 + height += theme.Padding() * 2 + } + + for index, entry := range entries { + child, flexible := entry.Element.(tomo.Flexible) + if flexible { + height += child.MinimumHeightFor(width) + } else { + _, entryHeight := entry.MinimumSize() + height += entryHeight + } + + if layout.Gap && index > 0 { + height += theme.Padding() + } + } + return +}