From 14080b1f885efea8878263e4ce1d3768bff428d7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 13:14:10 -0400 Subject: [PATCH] Element methods are now more consistent and have less bool flags Still need to update most examples... --- elements/box.go | 66 ++++- elements/cell.go | 6 + elements/document.go | 42 +++- elements/label.go | 13 +- elements/lerpslider.go | 12 +- elements/list.go | 11 +- elements/notdone/list.go | 455 ---------------------------------- elements/notdone/listentry.go | 104 -------- elements/progressbar.go | 4 + elements/scroll.go | 19 +- elements/scrollbar.go | 16 +- elements/slider.go | 9 +- elements/spacer.go | 15 +- examples/align/main.go | 17 +- examples/checkbox/main.go | 33 +-- examples/clipboard/main.go | 20 +- popups/dialog.go | 20 +- 17 files changed, 209 insertions(+), 653 deletions(-) delete mode 100644 elements/notdone/list.go delete mode 100644 elements/notdone/listentry.go diff --git a/elements/box.go b/elements/box.go index e72755f..29b3e13 100644 --- a/elements/box.go +++ b/elements/box.go @@ -6,6 +6,21 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/default/theme" +// Space is a list of spacing configurations that can be passed to some +// containers. +type Space int; const ( + SpaceNone = 0 + SpacePadding = 1 + SpaceMargin = 2 + SpaceBoth = SpacePadding | SpaceMargin +) + +// Includes returns whether a spacing value has been or'd with another spacing +// value. +func (space Space) Includes (sub Space) bool { + return (space & sub) > 0 +} + type scratchEntry struct { expand bool minSize float64 @@ -26,17 +41,21 @@ type Box struct { } // NewHBox creates a new horizontal box. -func NewHBox (padding, margin bool) (element *Box) { - element = &Box { padding: padding, margin: margin } +func NewHBox (space Space, children ...tomo.Element) (element *Box) { + element = &Box { + padding: space.Includes(SpacePadding), + margin: space.Includes(SpaceMargin), + } element.scratch = make(map[tomo.Element] scratchEntry) element.theme.Case = tomo.C("tomo", "box") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.Adopt(children...) return } // NewHBox creates a new vertical box. -func NewVBox (padding, margin bool) (element *Box) { - element = NewHBox(padding, margin) +func NewVBox (space Space) (element *Box) { + element = NewHBox(space) element.vertical = true return } @@ -101,19 +120,33 @@ func (element *Box) Layout () { } } -func (element *Box) Adopt (child tomo.Element, expand bool) { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: expand } +func (element *Box) Adopt (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: false } + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -func (element *Box) Disown (child tomo.Element) { - index := element.entity.IndexOf(child) - if index < 0 { return } - element.entity.Disown(index) - delete(element.scratch, child) +func (element *Box) AdoptExpand (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: true } + } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Box) Disown (children ...tomo.Element) { + for _, child := range children { + index := element.entity.IndexOf(child) + if index < 0 { continue } + element.entity.Disown(index) + delete(element.scratch, child) + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -132,6 +165,15 @@ func (element *Box) DisownAll () { element.entity.InvalidateLayout() } +func (element *Box) Child (index int) tomo.Element { + if index < 0 || index >= element.entity.CountChildren() { return nil } + return element.entity.Child(index) +} + +func (element *Box) CountChildren () int { + return element.entity.CountChildren() +} + func (element *Box) HandleChildMinimumSizeChange (child tomo.Element) { element.updateMinimumSize() element.entity.Invalidate() diff --git a/elements/cell.go b/elements/cell.go index c030a6e..f9da0db 100644 --- a/elements/cell.go +++ b/elements/cell.go @@ -83,6 +83,12 @@ func (element *Cell) Adopt (child tomo.Element) { element.entity.InvalidateLayout() } +// Child returns this element's child. If there is no child, this method will +// return nil. +func (element *Cell) Child () tomo.Element { + return element.child +} + // Enabled returns whether this cell is enabled or not. func (element *Cell) Enabled () bool { return element.enabled diff --git a/elements/document.go b/elements/document.go index d090d23..ec916a2 100644 --- a/elements/document.go +++ b/elements/document.go @@ -23,11 +23,12 @@ type Document struct { onScrollBoundsChange func () } -func NewDocument () (element *Document) { +func NewDocument (children ...tomo.Element) (element *Document) { element = &Document { } element.scratch = make(map[tomo.Element] scratchEntry) element.theme.Case = tomo.C("tomo", "document") element.entity = tomo.NewEntity(element).(documentEntity) + element.Adopt(children...) return } @@ -109,19 +110,33 @@ func (element *Document) Layout () { } } -func (element *Document) Adopt (child tomo.Element, expand bool) { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: expand } +func (element *Document) Adopt (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: true } + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -func (element *Document) Disown (child tomo.Element) { - index := element.entity.IndexOf(child) - if index < 0 { return } - element.entity.Disown(index) - delete(element.scratch, child) +func (element *Document) AdoptInline (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: false } + } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) Disown (children ...tomo.Element) { + for _, child := range children { + index := element.entity.IndexOf(child) + if index < 0 { return } + element.entity.Disown(index) + delete(element.scratch, child) + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -140,6 +155,15 @@ func (element *Document) DisownAll () { element.entity.InvalidateLayout() } +func (element *Document) Child (index int) tomo.Element { + if index < 0 || index >= element.entity.CountChildren() { return nil } + return element.entity.Child(index) +} + +func (element *Document) CountChildren () int { + return element.entity.CountChildren() +} + func (element *Document) HandleChildMinimumSizeChange (child tomo.Element) { element.updateMinimumSize() element.entity.Invalidate() diff --git a/elements/label.go b/elements/label.go index ec3b478..f48b088 100644 --- a/elements/label.go +++ b/elements/label.go @@ -24,20 +24,25 @@ type Label struct { theme theme.Wrapped } -// NewLabel creates a new label. If wrap is set to true, the text inside will be -// wrapped. -func NewLabel (text string, wrap bool) (element *Label) { +// NewLabel creates a new label. +func NewLabel (text string) (element *Label) { element = &Label { } element.theme.Case = tomo.C("tomo", "label") element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) - element.SetWrap(wrap) element.SetText(text) return } +// NewLabelWrapped creates a new label with text wrapping on. +func NewLabelWrapped (text string) (element *Label) { + element = NewLabel(text) + element.SetWrap(true) + return +} + // Entity returns this element's entity. func (element *Label) Entity () tomo.Entity { return element.entity diff --git a/elements/lerpslider.go b/elements/lerpslider.go index f55f190..b230b0f 100644 --- a/elements/lerpslider.go +++ b/elements/lerpslider.go @@ -15,16 +15,20 @@ type LerpSlider[T Numeric] struct { max T } -// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If -// vertical is set to true, the slider will be vertical instead of horizontal. -func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) { +// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. +func NewLerpSlider[T Numeric] ( + min, max T, value T, + orientation Orientation, +) ( + element *LerpSlider[T], +) { if min > max { temp := max max = min min = temp } element = &LerpSlider[T] { - Slider: NewSlider(0, vertical), + Slider: NewSlider(0, orientation), min: min, max: max, } diff --git a/elements/list.go b/elements/list.go index bec6c71..6926835 100644 --- a/elements/list.go +++ b/elements/list.go @@ -29,7 +29,7 @@ type List struct { onScrollBoundsChange func () } -func NewList (columns int, children ...tomo.Selectable) (element *List) { +func NewList (columns int, children ...tomo.Element) (element *List) { if columns < 1 { columns = 1 } element = &List { selected: -1 } element.scratch = make(map[tomo.Element] scratchEntry) @@ -152,6 +152,15 @@ func (element *List) DisownAll () { element.entity.InvalidateLayout() } +func (element *List) Child (index int) tomo.Element { + if index < 0 || index >= element.entity.CountChildren() { return nil } + return element.entity.Child(index) +} + +func (element *List) CountChildren () int { + return element.entity.CountChildren() +} + func (element *List) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { if child, ok := child.(tomo.Selectable); ok { index := element.entity.IndexOf(child) diff --git a/elements/notdone/list.go b/elements/notdone/list.go deleted file mode 100644 index 75d32eb..0000000 --- a/elements/notdone/list.go +++ /dev/null @@ -1,455 +0,0 @@ -package elements - -import "fmt" -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -type listEntity interface { - tomo.FlexibleEntity - tomo.ContainerEntity - tomo.ScrollableEntity -} - -// List is an element that contains several objects that a user can select. -type List struct { - entity listEntity - pressed bool - - contentHeight int - forcedMinimumWidth int - forcedMinimumHeight int - - selectedEntry int - scroll int - - config config.Wrapped - theme theme.Wrapped - - onNoEntrySelected func () - onScrollBoundsChange func () -} - -// NewList creates a new list element with the specified entries. -func NewList (entries ...ListEntry) (element *List) { - element = &List { selectedEntry: -1 } - element.theme.Case = tomo.C("tomo", "list") - - element.entries = make([]ListEntry, len(entries)) - for index, entry := range entries { - element.entries[index] = entry - } - return -} - -// SetTheme sets the element's theme. -func (element *List) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - for index, entry := range element.entries { - entry.SetTheme(element.theme.Theme) - element.entries[index] = entry - } - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *List) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - for index, entry := range element.entries { - entry.SetConfig(element.config) - element.entries[index] = entry - } - element.updateMinimumSize() - element.redo() -} - -// Collapse forces a minimum width and height upon the list. If a zero value is -// given for a dimension, its minimum will be determined by the list's content. -// If the list's height goes beyond the forced size, it will need to be accessed -// via scrolling. If an entry's width goes beyond the forced size, its text will -// be truncated so that it fits. -func (element *List) Collapse (width, height int) { - if - element.forcedMinimumWidth == width && - element.forcedMinimumHeight == height { - - return - } - - element.forcedMinimumWidth = width - element.forcedMinimumHeight = height - element.updateMinimumSize() - - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - element.redo() -} - -func (element *List) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - if !element.Focused() { element.Focus() } - if button != input.ButtonLeft { return } - element.pressed = true - if element.selectUnderMouse(x, y) && element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft { return } - element.pressed = false -} - -func (element *List) HandleMotion (x, y int) { - if element.pressed { - if element.selectUnderMouse(x, y) && element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - } -} - -func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if !element.Enabled() { return } - - altered := false - switch key { - case input.KeyLeft, input.KeyUp: - altered = element.changeSelectionBy(-1) - - case input.KeyRight, input.KeyDown: - altered = element.changeSelectionBy(1) - - case input.KeyEscape: - altered = element.selectEntry(-1) - } - - if altered && element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } - -// ScrollContentBounds returns the full content size of the element. -func (element *List) ScrollContentBounds () (bounds image.Rectangle) { - return image.Rect ( - 0, 0, - 1, element.contentHeight) -} - -// ScrollViewportBounds returns the size and position of the element's viewport -// relative to ScrollBounds. -func (element *List) ScrollViewportBounds () (bounds image.Rectangle) { - return image.Rect ( - 0, element.scroll, - 0, element.scroll + element.scrollViewportHeight()) -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *List) ScrollTo (position image.Point) { - element.scroll = position.Y - if element.scroll < 0 { - element.scroll = 0 - } else if element.scroll > element.maxScrollHeight() { - element.scroll = element.maxScrollHeight() - } - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *List) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -// OnNoEntrySelected sets a function to be called when the user chooses to -// deselect the current selected entry by clicking on empty space within the -// list or by pressing the escape key. -func (element *List) OnNoEntrySelected (callback func ()) { - element.onNoEntrySelected = callback -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *List) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -// CountEntries returns the amount of entries in the list. -func (element *List) CountEntries () (count int) { - return len(element.entries) -} - -// Append adds one or more entries to the end of the list. -func (element *List) Append (entries ...ListEntry) { - // append - for index, entry := range entries { - entry = element.resizeEntryToFit(entry) - entry.SetTheme(element.theme.Theme) - entry.SetConfig(element.config) - entries[index] = entry - } - element.entries = append(element.entries, entries...) - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// EntryAt returns the entry at the specified index. If the index is out of -// bounds, it panics. -func (element *List) EntryAt (index int) (entry ListEntry) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index)) - } - return element.entries[index] -} - -// Insert inserts an entry into the list at the speified index. If the index is -// out of bounds, it is constrained either to zero or len(entries). -func (element *List) Insert (index int, entry ListEntry) { - if index < 0 { index = 0 } - if index > len(element.entries) { index = len(element.entries) } - - // insert - element.entries = append ( - element.entries[:index + 1], - element.entries[index:]...) - entry = element.resizeEntryToFit(entry) - element.entries[index] = entry - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Remove removes the entry at the specified index. If the index is out of -// bounds, it panics. -func (element *List) Remove (index int) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.Remove index out of range: ", index)) - } - - // delete - element.entries = append ( - element.entries[:index], - element.entries[index + 1:]...) - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Clear removes all entries from the list. -func (element *List) Clear () { - element.entries = nil - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Replace replaces the entry at the specified index with another. If the index -// is out of bounds, it panics. -func (element *List) Replace (index int, entry ListEntry) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.Replace index out of range: ", index)) - } - - // replace - entry = element.resizeEntryToFit(entry) - element.entries[index] = entry - - // redraw - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Select selects a specific item in the list. If the index is out of bounds, -// no items will be selecected. -func (element *List) Select (index int) { - if element.selectEntry(index) { - element.redo() - } -} - -func (element *List) selectUnderMouse (x, y int) (updated bool) { - padding := element.theme.Padding(tomo.PatternSunken) - bounds := padding.Apply(element.Bounds()) - mousePoint := image.Pt(x, y) - dot := image.Pt ( - bounds.Min.X, - bounds.Min.Y - element.scroll) - - newlySelectedEntryIndex := -1 - for index, entry := range element.entries { - entryPosition := dot - dot.Y += entry.Bounds().Dy() - if entryPosition.Y > bounds.Max.Y { break } - if mousePoint.In(entry.Bounds().Add(entryPosition)) { - newlySelectedEntryIndex = index - break - } - } - - return element.selectEntry(newlySelectedEntryIndex) -} - -func (element *List) selectEntry (index int) (updated bool) { - if element.selectedEntry == index { return false } - element.selectedEntry = index - if element.selectedEntry < 0 { - if element.onNoEntrySelected != nil { - element.onNoEntrySelected() - } - } else { - element.entries[element.selectedEntry].RunSelect() - } - return true -} - -func (element *List) changeSelectionBy (delta int) (updated bool) { - newIndex := element.selectedEntry + delta - if newIndex < 0 { newIndex = len(element.entries) - 1 } - if newIndex >= len(element.entries) { newIndex = 0 } - return element.selectEntry(newIndex) -} - -func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - bounds := element.Bounds() - padding := element.theme.Padding(tomo.PatternSunken) - entry.Resize(padding.Apply(bounds).Dx()) - return entry -} - -func (element *List) updateMinimumSize () { - element.contentHeight = 0 - for _, entry := range element.entries { - element.contentHeight += entry.Bounds().Dy() - } - - minimumWidth := element.forcedMinimumWidth - minimumHeight := element.forcedMinimumHeight - - if minimumWidth == 0 { - for _, entry := range element.entries { - entryWidth := entry.MinimumWidth() - if entryWidth > minimumWidth { - minimumWidth = entryWidth - } - } - } - - if minimumHeight == 0 { - minimumHeight = element.contentHeight - } - - padding := element.theme.Padding(tomo.PatternSunken) - minimumHeight += padding[0] + padding[2] - - element.core.SetMinimumSize(minimumWidth, minimumHeight) -} - -func (element *List) scrollBoundsChange () { - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *List) scrollViewportHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - return element.Bounds().Dy() - padding[0] - padding[2] -} - -func (element *List) maxScrollHeight () (height int) { - height = - element.contentHeight - - element.scrollViewportHeight() - if height < 0 { height = 0 } - return -} - -func (element *List) Layout () { - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - if element.scroll > element.maxScrollHeight() { - element.scroll = element.maxScrollHeight() - } - element.draw() - element.scrollBoundsChange() -} - -func (element *List) Draw (destination canvas.Canvas) { - bounds := element.Bounds() - padding := element.theme.Padding(tomo.PatternSunken) - innerBounds := padding.Apply(bounds) - state := tomo.State { - Disabled: !element.Enabled(), - Focused: element.Focused(), - } - - dot := image.Point { - innerBounds.Min.X, - innerBounds.Min.Y - element.scroll, - } - innerCanvas := canvas.Cut(element.core, innerBounds) - for index, entry := range element.entries { - entryPosition := dot - dot.Y += entry.Bounds().Dy() - if dot.Y < innerBounds.Min.Y { continue } - if entryPosition.Y > innerBounds.Max.Y { break } - entry.Draw ( - innerCanvas, entryPosition, - element.Focused(), element.selectedEntry == index) - } - - covered := image.Rect ( - 0, 0, - innerBounds.Dx(), element.contentHeight, - ).Add(innerBounds.Min).Intersect(innerBounds) - pattern := element.theme.Pattern(tomo.PatternSunken, state) - artist.DrawShatter ( - element.core, pattern, bounds, covered) -} diff --git a/elements/notdone/listentry.go b/elements/notdone/listentry.go deleted file mode 100644 index d98cf37..0000000 --- a/elements/notdone/listentry.go +++ /dev/null @@ -1,104 +0,0 @@ -package elements - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// ListEntry is an item that can be added to a list. -type ListEntry struct { - drawer textdraw.Drawer - bounds image.Rectangle - text string - width int - minimumWidth int - - config config.Wrapped - theme theme.Wrapped - - onSelect func () -} - -func NewListEntry (text string, onSelect func ()) (entry ListEntry) { - entry = ListEntry { - text: text, - onSelect: onSelect, - } - entry.theme.Case = tomo.C("tomo", "listEntry") - entry.drawer.SetFace (entry.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - entry.drawer.SetText([]rune(text)) - entry.updateBounds() - return -} - -func (entry *ListEntry) SetTheme (new tomo.Theme) { - if new == entry.theme.Theme { return } - entry.theme.Theme = new - entry.drawer.SetFace (entry.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - entry.updateBounds() -} - -func (entry *ListEntry) SetConfig (new tomo.Config) { - if new == entry.config.Config { return } - entry.config.Config = new -} - -func (entry *ListEntry) updateBounds () { - padding := entry.theme.Padding(tomo.PatternRaised) - entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds()) - entry.bounds = entry.bounds.Sub(entry.bounds.Min) - entry.minimumWidth = entry.bounds.Dx() - entry.bounds.Max.X = entry.width -} - -func (entry *ListEntry) Draw ( - destination canvas.Canvas, - offset image.Point, - focused bool, - on bool, -) ( - updatedRegion image.Rectangle, -) { - state := tomo.State { - Focused: focused, - On: on, - } - - pattern := entry.theme.Pattern(tomo.PatternRaised, state) - padding := entry.theme.Padding(tomo.PatternRaised) - bounds := entry.Bounds().Add(offset) - pattern.Draw(destination, bounds) - - foreground := entry.theme.Color (tomo.ColorForeground, state) - return entry.drawer.Draw ( - destination, - foreground, - offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])). - Sub(entry.drawer.LayoutBounds().Min)) -} - -func (entry *ListEntry) RunSelect () { - if entry.onSelect != nil { - entry.onSelect() - } -} - -func (entry *ListEntry) Bounds () (bounds image.Rectangle) { - return entry.bounds -} - -func (entry *ListEntry) Resize (width int) { - entry.width = width - entry.updateBounds() -} - -func (entry *ListEntry) MinimumWidth () (width int) { - return entry.minimumWidth -} diff --git a/elements/progressbar.go b/elements/progressbar.go index c0d2963..5c96daa 100644 --- a/elements/progressbar.go +++ b/elements/progressbar.go @@ -18,6 +18,8 @@ type ProgressBar struct { // NewProgressBar creates a new progress bar displaying the given progress // level. func NewProgressBar (progress float64) (element *ProgressBar) { + if progress < 0 { progress = 0 } + if progress > 1 { progress = 1 } element = &ProgressBar { progress: progress } element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "progressBar") @@ -48,6 +50,8 @@ func (element *ProgressBar) Draw (destination canvas.Canvas) { // SetProgress sets the progress level of the bar. func (element *ProgressBar) SetProgress (progress float64) { + if progress < 0 { progress = 0 } + if progress > 1 { progress = 1 } if progress == element.progress { return } element.progress = progress element.entity.Invalidate() diff --git a/elements/scroll.go b/elements/scroll.go index 7ecfb25..929743b 100644 --- a/elements/scroll.go +++ b/elements/scroll.go @@ -7,6 +7,19 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +type ScrollMode int; const ( + ScrollNeither ScrollMode = 0 + ScrollVertical = 1 + ScrollHorizontal = 2 + ScrollBoth = ScrollVertical | ScrollHorizontal +) + +// Includes returns whether a scroll mode has been or'd with another scroll +// mode. +func (mode ScrollMode) Includes (sub ScrollMode) bool { + return (mode & sub) > 0 +} + type Scroll struct { entity tomo.ContainerEntity @@ -18,12 +31,12 @@ type Scroll struct { theme theme.Wrapped } -func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scroll) { +func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) { element = &Scroll { } element.theme.Case = tomo.C("tomo", "scroll") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) - if horizontal { + if mode.Includes(ScrollHorizontal) { element.horizontal = NewScrollBar(false) element.horizontal.OnScroll (func (viewport image.Point) { if element.child != nil { @@ -37,7 +50,7 @@ func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scro }) element.entity.Adopt(element.horizontal) } - if vertical { + if mode.Includes(ScrollVertical) { element.vertical = NewScrollBar(true) element.vertical.OnScroll (func (viewport image.Point) { if element.child != nil { diff --git a/elements/scrollbar.go b/elements/scrollbar.go index 8835bde..f64731b 100644 --- a/elements/scrollbar.go +++ b/elements/scrollbar.go @@ -7,6 +7,13 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +// Orientation represents an orientation configuration that can be passed to +// scrollbars and sliders. +type Orientation bool; const ( + Vertical Orientation = true + Horizontal = false +) + // ScrollBar is an element similar to Slider, but it has special behavior that // makes it well suited for controlling the viewport position on one axis of a // scrollable element. Instead of having a value from zero to one, it stores @@ -37,14 +44,13 @@ type ScrollBar struct { onScroll func (viewport image.Point) } -// NewScrollBar creates a new scroll bar. If vertical is set to true, the scroll -// bar will be vertical instead of horizontal. -func NewScrollBar (vertical bool) (element *ScrollBar) { +// NewScrollBar creates a new scroll bar. +func NewScrollBar (orientation Orientation) (element *ScrollBar) { element = &ScrollBar { - vertical: vertical, + vertical: bool(orientation), enabled: true, } - if vertical { + if orientation == Vertical { element.theme.Case = tomo.C("tomo", "scrollBarHorizontal") } else { element.theme.Case = tomo.C("tomo", "scrollBarVertical") diff --git a/elements/slider.go b/elements/slider.go index 1e6da8e..f14903b 100644 --- a/elements/slider.go +++ b/elements/slider.go @@ -26,14 +26,13 @@ type Slider struct { onRelease func () } -// NewSlider creates a new slider with the specified value. If vertical is set -// to true, -func NewSlider (value float64, vertical bool) (element *Slider) { +// NewSlider creates a new slider with the specified value. +func NewSlider (value float64, orientation Orientation) (element *Slider) { element = &Slider { value: value, - vertical: vertical, + vertical: bool(orientation), } - if vertical { + if orientation == Vertical { element.theme.Case = tomo.C("tomo", "sliderVertical") } else { element.theme.Case = tomo.C("tomo", "sliderHorizontal") diff --git a/elements/spacer.go b/elements/spacer.go index a382b44..9b0ff4d 100644 --- a/elements/spacer.go +++ b/elements/spacer.go @@ -15,17 +15,22 @@ type Spacer struct { theme theme.Wrapped } -// NewSpacer creates a new spacer. If line is set to true, the spacer will be -// filled with a line color, and if compressed to its minimum width or height, -// will appear as a line. -func NewSpacer (line bool) (element *Spacer) { - element = &Spacer { line: line } +// NewSpacer creates a new spacer. +func NewSpacer () (element *Spacer) { + element = &Spacer { } element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "spacer") element.updateMinimumSize() return } +// NewLine creates a new line separator. +func NewLine () (element *Spacer) { + element = NewSpacer() + element.SetLine(true) + return +} + // Entity returns this element's entity. func (element *Spacer) Entity () tomo.Entity { return element.entity diff --git a/examples/align/main.go b/examples/align/main.go index ecee3fe..1857b42 100644 --- a/examples/align/main.go +++ b/examples/align/main.go @@ -13,23 +13,18 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256)) window.SetTitle("Text alignment") - container := elements.NewDocument() - - left := elements.NewLabel(text, true) - center := elements.NewLabel(text, true) - right := elements.NewLabel(text, true) - justify := elements.NewLabel(text, true) + left := elements.NewLabelWrapped(text) + center := elements.NewLabelWrapped(text) + right := elements.NewLabelWrapped(text) + justify := elements.NewLabelWrapped(text) left.SetAlign(textdraw.AlignLeft) center.SetAlign(textdraw.AlignCenter) right.SetAlign(textdraw.AlignRight) justify.SetAlign(textdraw.AlignJustify) - container.Adopt(left, true) - container.Adopt(center, true) - container.Adopt(right, true) - container.Adopt(justify, true) - window.Adopt(elements.NewScroll(container, false, true)) + window.Adopt (elements.NewScroll (elements.ScrollVertical, + elements.NewDocument(left, center, right, justify))) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index 519e82d..549e54d 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -13,23 +13,15 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Checkboxes") - container := elements.NewVBox(true, true) - window.Adopt(container) - - introText := elements.NewLabel ( + introText := elements.NewLabelWrapped ( "We advise you to not read thPlease listen to me. I am " + "trapped inside the example code. This is the only way for " + - "me to communicate.", true) + "me to communicate.") introText.EmCollapse(0, 5) - container.Adopt(introText, true) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(elements.NewCheckbox("Oh god", false), false) - container.Adopt(elements.NewCheckbox("Can you hear them", true), false) - container.Adopt(elements.NewCheckbox("They are in the walls", false), false) - container.Adopt(elements.NewCheckbox("They are coming for us", false), false) + disabledCheckbox := elements.NewCheckbox("We are but their helpless prey", false) disabledCheckbox.SetEnabled(false) - container.Adopt(disabledCheckbox, false) + vsync := elements.NewCheckbox("Enable vsync", false) vsync.OnToggle (func () { if vsync.Value() { @@ -40,12 +32,23 @@ func run () { "That doesn't do anything.") } }) - container.Adopt(vsync, false) + button := elements.NewButton("What") button.OnClick(tomo.Stop) - container.Adopt(button, false) - button.Focus() + + box := elements.NewVBox(elements.SpaceBoth) + box.AdoptExpand(introText) + box.Adopt ( + elements.NewLine(), + elements.NewCheckbox("Oh god", false), + elements.NewCheckbox("Can you hear them", true), + elements.NewCheckbox("They are in the walls", false), + elements.NewCheckbox("They are coming for us", false), + disabledCheckbox, + vsync, button) + window.Adopt(box) + button.Focus() window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index f11cab5..cfdc921 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -25,9 +25,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) window.SetTitle("Clipboard") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) textInput := elements.NewTextBox("", "") - controlRow := elements.NewHBox(false, true) + controlRow := elements.NewHBox(elements.SpaceMargin) copyButton := elements.NewButton("Copy") copyButton.SetIcon(tomo.IconCopy) pasteButton := elements.NewButton("Paste") @@ -107,11 +107,11 @@ func run () { window.Paste(imageClipboardCallback, validImageTypes...) }) - container.Adopt(textInput, true) - controlRow.Adopt(copyButton, true) - controlRow.Adopt(pasteButton, true) - controlRow.Adopt(pasteImageButton, true) - container.Adopt(controlRow, false) + container.AdoptExpand(textInput) + controlRow.AdoptExpand(copyButton) + controlRow.AdoptExpand(pasteButton) + controlRow.AdoptExpand(pasteImageButton) + container.Adopt(controlRow) window.Adopt(container) window.OnClose(tomo.Stop) @@ -121,13 +121,13 @@ func run () { func imageWindow (parent tomo.Window, image image.Image) { window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Clipboard Image") - container := containers.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) closeButton := elements.NewButton("Ok") closeButton.SetIcon(tomo.IconYes) closeButton.OnClick(window.Close) - container.Adopt(elements.NewImage(image), true) - container.Adopt(closeButton, false) + container.AdoptExpand(elements.NewImage(image)) + container.Adopt(closeButton) window.Adopt(container) window.Show() } diff --git a/popups/dialog.go b/popups/dialog.go index af91ead..722cc6f 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -43,9 +43,9 @@ func NewDialog ( } window.SetTitle(title) - box := elements.NewVBox(true, true) - messageRow := elements.NewHBox(false, true) - controlRow := elements.NewHBox(false, true) + box := elements.NewVBox(elements.SpaceBoth) + messageRow := elements.NewHBox(elements.SpaceMargin) + controlRow := elements.NewHBox(elements.SpaceMargin) iconId := tomo.IconInformation switch kind { @@ -55,19 +55,19 @@ func NewDialog ( case DialogKindError: iconId = tomo.IconError } - messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false) - messageRow.Adopt(elements.NewLabel(message, false), true) + messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge)) + messageRow.AdoptExpand(elements.NewLabel(message)) - controlRow.Adopt(elements.NewSpacer(false), true) - box.Adopt(messageRow, true) - box.Adopt(controlRow, false) + controlRow.AdoptExpand(elements.NewSpacer()) + box.AdoptExpand(messageRow) + box.Adopt(controlRow) window.Adopt(box) if len(buttons) == 0 { button := elements.NewButton("OK") button.SetIcon(tomo.IconYes) button.OnClick(window.Close) - controlRow.Adopt(button, false) + controlRow.Adopt(button) button.Focus() } else { var button *elements.Button @@ -78,7 +78,7 @@ func NewDialog ( buttonDescriptor.OnPress() window.Close() }) - controlRow.Adopt(button, false) + controlRow.Adopt(button) } button.Focus() }