diff --git a/elements/button.go b/elements/button.go index 09946f2..caba419 100644 --- a/elements/button.go +++ b/elements/button.go @@ -1,24 +1,19 @@ package elements import "image" -// import "runtime/debug" 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/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" -// import "git.tebibyte.media/sashakoshka/tomo/artist" -// import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" // Button is a clickable button. type Button struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity drawer textdraw.Drawer + enabled bool pressed bool text string @@ -36,9 +31,6 @@ type Button struct { func NewButton (text string) (element *Button) { element = &Button { showText: true } element.theme.Case = tomo.C("tomo", "button") - element.Core, element.core = core.NewCore(element, element.drawAll) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) @@ -46,42 +38,11 @@ func NewButton (text string) (element *Button) { return } -func (element *Button) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - if !element.Focused() { element.Focus() } - if button != input.ButtonLeft { return } - element.pressed = true - element.drawAndPush() -} - -func (element *Button) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft { return } - element.pressed = false - within := image.Point { x, y }. - In(element.Bounds()) - if element.Enabled() && within && element.onClick != nil { - element.onClick() - } - element.drawAndPush() -} - -func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if !element.Enabled() { return } - if key == input.KeyEnter { - element.pressed = true - element.drawAndPush() - } -} - -func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter && element.pressed { - element.pressed = false - element.drawAndPush() - if !element.Enabled() { return } - if element.onClick != nil { - element.onClick() - } - } +// Bind binds this element to an entity. +func (element *Button) Bind (entity tomo.Entity) { + if entity == nil { element.entity = nil; return } + element.entity = entity.(tomo.FocusableEntity) + element.updateMinimumSize() } // OnClick sets the function to be called when the button is clicked. @@ -89,19 +50,33 @@ func (element *Button) OnClick (callback func ()) { element.onClick = callback } +// Focus gives this element input focus. +func (element *Button) Focus () { + if element.entity == nil { return } + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this button is enabled or not. +func (element *Button) Enabled () bool { + return element.enabled +} + // SetEnabled sets whether this button can be clicked or not. func (element *Button) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) + if element.enabled == enabled { return } + element.enabled = enabled + if element.entity == nil { return } + element.entity.Invalidate() } // SetText sets the button's label text. func (element *Button) SetText (text string) { if element.text == text { return } - element.text = text element.drawer.SetText([]rune(text)) + if element.entity == nil { return } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } // SetIcon sets the icon of the button. Passing theme.IconNone removes the @@ -109,23 +84,22 @@ func (element *Button) SetText (text string) { func (element *Button) SetIcon (id tomo.Icon) { if id == tomo.IconNone { element.hasIcon = false - element.updateMinimumSize() - element.drawAndPush() } else { if element.hasIcon && element.iconId == id { return } element.hasIcon = true element.iconId = id } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } // ShowText sets whether or not the button's text will be displayed. func (element *Button) ShowText (showText bool) { if element.showText == showText { return } element.showText = showText + if element.entity == nil { return } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -135,74 +109,30 @@ func (element *Button) SetTheme (new tomo.Theme) { element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) + if element.entity == nil { return } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Button) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new + if element.entity == nil { return } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } -func (element *Button) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) - - textBounds := element.drawer.LayoutBounds() - minimumSize := textBounds.Sub(textBounds.Min) +// Draw causes the element to draw to the specified destination canvas. +func (element *Button) Draw (destination canvas.Canvas) { + if element.entity == nil { return } - if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) - if icon != nil { - bounds := icon.Bounds() - if element.showText { - minimumSize.Max.X += bounds.Dx() - minimumSize.Max.X += margin.X - } else { - minimumSize.Max.X = bounds.Dx() - } - } - } - - minimumSize = padding.Inverse().Apply(minimumSize) - element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) -} - -func (element *Button) state () tomo.State { - return tomo.State { - Disabled: !element.Enabled(), - Focused: element.Focused(), - Pressed: element.pressed, - } -} - -func (element *Button) drawAndPush () { - if element.core.HasImage () { - element.drawAll() - element.core.DamageAll() - } -} - -func (element *Button) drawAll () { - element.drawBackground() - element.drawText() -} - -func (element *Button) drawBackground () []image.Rectangle { state := element.state() - bounds := element.Bounds() + bounds := element.entity.Bounds() pattern := element.theme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, bounds) - return []image.Rectangle { bounds } -} - -func (element *Button) drawText () { - state := element.state() - bounds := element.Bounds() + pattern.Draw(destination, bounds) + foreground := element.theme.Color(tomo.ColorForeground, state) sink := element.theme.Sink(tomo.PatternButton) margin := element.theme.Margin(tomo.PatternButton) @@ -240,7 +170,7 @@ func (element *Button) drawText () { } offset.X += addedWidth / 2 - icon.Draw(element.core, foreground, iconOffset) + icon.Draw(destination, foreground, iconOffset) } } @@ -248,6 +178,84 @@ func (element *Button) drawText () { if element.pressed { offset = offset.Add(sink) } - element.drawer.Draw(element.core, foreground, offset) + element.drawer.Draw(destination, foreground, offset) + } +} + +func (element *Button) HandleFocusChange () { + if element.entity == nil { return } + element.entity.Invalidate() +} + +func (element *Button) HandleMouseDown (x, y int, button input.Button) { + if element.entity == nil { return } + if !element.Enabled() { return } + element.Focus() + if button != input.ButtonLeft { return } + element.pressed = true + element.entity.Invalidate() +} + +func (element *Button) HandleMouseUp (x, y int, button input.Button) { + if element.entity == nil { return } + if button != input.ButtonLeft { return } + element.pressed = false + within := image.Point { x, y }.In(element.entity.Bounds()) + if element.Enabled() && within && element.onClick != nil { + element.onClick() + } + element.entity.Invalidate() +} + +func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if element.entity == nil { return } + if !element.Enabled() { return } + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { + if element.entity == nil { return } + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.entity.Invalidate() + if !element.Enabled() { return } + if element.onClick != nil { + element.onClick() + } + } +} + +func (element *Button) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternButton) + margin := element.theme.Margin(tomo.PatternButton) + + textBounds := element.drawer.LayoutBounds() + minimumSize := textBounds.Sub(textBounds.Min) + + if element.hasIcon { + icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + if icon != nil { + bounds := icon.Bounds() + if element.showText { + minimumSize.Max.X += bounds.Dx() + minimumSize.Max.X += margin.X + } else { + minimumSize.Max.X = bounds.Dx() + } + } + } + + minimumSize = padding.Inverse().Apply(minimumSize) + element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) +} + +func (element *Button) state () tomo.State { + return tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.pressed, } } diff --git a/elements/checkbox.go b/elements/checkbox.go index f90b90d..74bdf97 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -3,19 +3,17 @@ package elements 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/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Checkbox is a toggle-able checkbox with a label. type Checkbox struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity drawer textdraw.Drawer + enabled bool pressed bool checked bool text string @@ -30,9 +28,6 @@ type Checkbox struct { func NewCheckbox (text string, checked bool) (element *Checkbox) { element = &Checkbox { checked: checked } element.theme.Case = tomo.C("tomo", "checkbox") - element.Core, element.core = core.NewCore(element, element.draw) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) @@ -40,57 +35,11 @@ func NewCheckbox (text string, checked bool) (element *Checkbox) { return } -func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - element.Focus() - element.pressed = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft || !element.pressed { return } - - element.pressed = false - within := image.Point { x, y }. - In(element.Bounds()) - if within { - element.checked = !element.checked - } - - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - if within && element.onToggle != nil { - element.onToggle() - } -} - -func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter { - element.pressed = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - } -} - -func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter && element.pressed { - element.pressed = false - element.checked = !element.checked - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - if element.onToggle != nil { - element.onToggle() - } - } +// Bind binds this element to an entity. +func (element *Checkbox) Bind (entity tomo.Entity) { + if entity == nil { element.entity = nil; return } + element.entity = entity.(tomo.FocusableEntity) + element.updateMinimumSize() } // OnToggle sets the function to be called when the checkbox is toggled. @@ -103,23 +52,33 @@ func (element *Checkbox) Value () (checked bool) { return element.checked } +// Focus gives this element input focus. +func (element *Checkbox) Focus () { + if element.entity == nil { return } + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this checkbox is enabled or not. +func (element *Checkbox) Enabled () bool { + return element.enabled +} + // SetEnabled sets whether this checkbox can be toggled or not. func (element *Checkbox) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) + if element.enabled == enabled { return } + element.enabled = enabled + if element.entity == nil { return } + element.entity.Invalidate() } // SetText sets the checkbox's label text. func (element *Checkbox) SetText (text string) { if element.text == text { return } - element.text = text element.drawer.SetText([]rune(text)) + if element.entity == nil { return } element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -129,53 +88,38 @@ func (element *Checkbox) SetTheme (new tomo.Theme) { element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) + if element.entity == nil { return } element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Checkbox) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new + if element.entity == nil { return } element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } -func (element *Checkbox) updateMinimumSize () { - textBounds := element.drawer.LayoutBounds() - if element.text == "" { - element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) - } else { - margin := element.theme.Margin(tomo.PatternBackground) - element.core.SetMinimumSize ( - textBounds.Dy() + margin.X + textBounds.Dx(), - textBounds.Dy()) - } -} - -func (element *Checkbox) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *Checkbox) draw () { - bounds := element.Bounds() +// Draw causes the element to draw to the specified destination canvas. +func (element *Checkbox) Draw (destination canvas.Canvas) { + if element.entity == nil { return } + + bounds := element.entity.Bounds() boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) state := tomo.State { Disabled: !element.Enabled(), - Focused: element.Focused(), + Focused: element.entity.Focused(), Pressed: element.pressed, On: element.checked, } - element.core.DrawBackground ( - element.theme.Pattern(tomo.PatternBackground, state)) + element.entity.DrawBackground(destination, bounds) pattern := element.theme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, boxBounds) + pattern.Draw(destination, boxBounds) textBounds := element.drawer.LayoutBounds() margin := element.theme.Margin(tomo.PatternBackground) @@ -187,5 +131,61 @@ func (element *Checkbox) draw () { offset.X -= textBounds.Min.X foreground := element.theme.Color(tomo.ColorForeground, state) - element.drawer.Draw(element.core, foreground, offset) + element.drawer.Draw(destination, foreground, offset) +} + +func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { + if element.entity == nil { return } + if !element.Enabled() { return } + element.Focus() + element.pressed = true + element.entity.Invalidate() +} + +func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) { + if element.entity == nil { return } + if button != input.ButtonLeft || !element.pressed { return } + + element.pressed = false + within := image.Point { x, y }.In(element.entity.Bounds()) + if within { + element.checked = !element.checked + } + + element.entity.Invalidate() + if within && element.onToggle != nil { + element.onToggle() + } +} + +func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if element.entity == nil { return } + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) { + if element.entity == nil { return } + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.checked = !element.checked + element.entity.Invalidate() + if element.onToggle != nil { + element.onToggle() + } + } +} + +func (element *Checkbox) updateMinimumSize () { + textBounds := element.drawer.LayoutBounds() + if element.text == "" { + element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) + } else { + margin := element.theme.Margin(tomo.PatternBackground) + element.entity.SetMinimumSize ( + textBounds.Dy() + margin.X + textBounds.Dx(), + textBounds.Dy()) + } } diff --git a/elements/icon.go b/elements/icon.go index ba52d8a..9d4cc93 100644 --- a/elements/icon.go +++ b/elements/icon.go @@ -2,47 +2,72 @@ 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/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +// Icon is an element capable of displaying a singular icon. type Icon struct { - *core.Core - core core.CoreControl - theme theme.Wrapped - id tomo.Icon - size tomo.IconSize + entity tomo.Entity + theme theme.Wrapped + id tomo.Icon + size tomo.IconSize } +// Icon creates a new icon element. func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) { element = &Icon { id: id, size: size, } element.theme.Case = tomo.C("tomo", "icon") - element.Core, element.core = core.NewCore(element, element.draw) - element.updateMinimumSize() return } +// Bind binds this element to an entity. +func (element *Icon) Bind (entity tomo.Entity) { + if entity == nil { element.entity = nil; return } + element.entity = entity + element.updateMinimumSize() +} + +// SetIcon sets the element's icon. func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) { element.id = id element.size = size + if element.entity == nil { return } element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. func (element *Icon) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new + if element.entity == nil { return } element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() + element.entity.Invalidate() +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Icon) Draw (destination canvas.Canvas) { + if element.entity == nil { return } + + bounds := element.entity.Bounds() + state := tomo.State { } + element.theme. + Pattern(tomo.PatternBackground, state). + Draw(destination, bounds) + icon := element.icon() + if icon != nil { + iconBounds := icon.Bounds() + offset := image.Pt ( + (bounds.Dx() - iconBounds.Dx()) / 2, + (bounds.Dy() - iconBounds.Dy()) / 2) + icon.Draw ( + destination, + element.theme.Color(tomo.ColorForeground, state), + bounds.Min.Add(offset)) } } @@ -53,28 +78,9 @@ func (element *Icon) icon () artist.Icon { func (element *Icon) updateMinimumSize () { icon := element.icon() if icon == nil { - element.core.SetMinimumSize(0, 0) + element.entity.SetMinimumSize(0, 0) } else { bounds := icon.Bounds() - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) - } -} - -func (element *Icon) draw () { - bounds := element.Bounds() - state := tomo.State { } - element.theme. - Pattern(tomo.PatternBackground, state). - Draw(element.core, bounds) - icon := element.icon() - if icon != nil { - iconBounds := icon.Bounds() - offset := image.Pt ( - (bounds.Dx() - iconBounds.Dx()) / 2, - (bounds.Dy() - iconBounds.Dy()) / 2) - icon.Draw ( - element.core, - element.theme.Color(tomo.ColorForeground, state), - bounds.Min.Add(offset)) + element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) } } diff --git a/elements/image.go b/elements/image.go index ab5d767..97a6f89 100644 --- a/elements/image.go +++ b/elements/image.go @@ -1,25 +1,35 @@ package elements import "image" +import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" +// TODO: this element is lame need to make it better + +// Image is an element capable of displaying an image. type Image struct { - *core.Core - core core.CoreControl + entity tomo.Entity buffer canvas.Canvas } +// NewImage creates a new image element. func NewImage (image image.Image) (element *Image) { element = &Image { buffer: canvas.FromImage(image) } - element.Core, element.core = core.NewCore(element, element.draw) - bounds := image.Bounds() - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) return } -func (element *Image) draw () { - (patterns.Texture { Canvas: element.buffer }). - Draw(element.core, element.Bounds()) +// Bind binds this element to an entity. +func (element *Image) Bind (entity tomo.Entity) { + if entity == nil { element.entity = nil; return } + element.entity = entity + bounds := element.buffer.Bounds() + element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Image) Draw (destination canvas.Canvas) { + if element.entity == nil { return } + (patterns.Texture { Canvas: element.buffer }). + Draw(destination, element.entity.Bounds()) } diff --git a/elements/label.go b/elements/label.go index 412d636..2e1cca9 100644 --- a/elements/label.go +++ b/elements/label.go @@ -2,16 +2,15 @@ package elements import "golang.org/x/image/math/fixed" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Label is a simple text box. type Label struct { - *core.Core - core core.CoreControl - + entity tomo.FlexibleEntity + align textdraw.Align wrap bool text string @@ -19,11 +18,10 @@ type Label struct { forcedColumns int forcedRows int + minHeight int config config.Wrapped theme theme.Wrapped - - onFlexibleHeightChange func () } // NewLabel creates a new label. If wrap is set to true, the text inside will be @@ -31,7 +29,6 @@ type Label struct { func NewLabel (text string, wrap bool) (element *Label) { element = &Label { } element.theme.Case = tomo.C("tomo", "label") - element.Core, element.core = core.NewCore(element, element.handleResize) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) @@ -40,29 +37,11 @@ func NewLabel (text string, wrap bool) (element *Label) { return } -func (element *Label) redo () { - face := element.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal) - element.drawer.SetFace(face) +// Bind binds this element to an entity. +func (element *Label) Bind (entity tomo.Entity) { + if entity == nil { element.entity = nil; return } + element.entity = entity.(tomo.FlexibleEntity) element.updateMinimumSize() - bounds := element.Bounds() - if element.wrap { - element.drawer.SetMaxWidth(bounds.Dx()) - element.drawer.SetMaxHeight(bounds.Dy()) - } - element.draw() - element.core.DamageAll() -} - -func (element *Label) handleResize () { - bounds := element.Bounds() - if element.wrap { - element.drawer.SetMaxWidth(bounds.Dx()) - element.drawer.SetMaxHeight(bounds.Dy()) - } - element.draw() - return } // EmCollapse forces a minimum width and height upon the label. The width is @@ -73,6 +52,7 @@ func (element *Label) handleResize () { func (element *Label) EmCollapse (columns int, rows int) { element.forcedColumns = columns element.forcedRows = rows + if element.entity == nil { return } element.updateMinimumSize() } @@ -82,29 +62,19 @@ func (element *Label) FlexibleHeightFor (width int) (height int) { if element.wrap { return element.drawer.ReccomendedHeightFor(width) } else { - _, height = element.MinimumSize() - return + return element.minHeight } } -// OnFlexibleHeightChange sets a function to be called when the parameters -// affecting this element's flexible height are changed. -func (element *Label) OnFlexibleHeightChange (callback func ()) { - element.onFlexibleHeightChange = callback -} - // SetText sets the label's text. func (element *Label) SetText (text string) { if element.text == text { return } element.text = text element.drawer.SetText([]rune(text)) + if element.entity == nil { return } element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetWrap sets wether or not the label's text wraps. If the text is set to @@ -118,26 +88,19 @@ func (element *Label) SetWrap (wrap bool) { element.drawer.SetMaxHeight(0) } element.wrap = wrap + if element.entity == nil { return } element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetAlign sets the alignment method of the label. func (element *Label) SetAlign (align textdraw.Align) { if align == element.align { return } - element.align = align element.drawer.SetAlign(align) + if element.entity == nil { return } element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -147,24 +110,38 @@ func (element *Label) SetTheme (new tomo.Theme) { element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) + if element.entity == nil { return } element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Label) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new + if element.entity == nil { return } element.updateMinimumSize() + element.entity.Invalidate() +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Label) Draw (destination canvas.Canvas) { + if element.entity == nil { return } - if element.core.HasImage () { - element.draw() - element.core.DamageAll() + bounds := element.entity. Bounds() + + if element.wrap { + element.drawer.SetMaxWidth(bounds.Dx()) + element.drawer.SetMaxHeight(bounds.Dy()) } + + element.entity.DrawBackground(destination, bounds) + + textBounds := element.drawer.LayoutBounds() + foreground := element.theme.Color ( + tomo.ColorForeground, + tomo.State { }) + element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) } func (element *Label) updateMinimumSize () { @@ -176,9 +153,8 @@ func (element *Label) updateMinimumSize () { em = element.theme.Padding(tomo.PatternBackground)[0] } width, height = em, element.drawer.LineHeight().Round() - if element.onFlexibleHeightChange != nil { - element.onFlexibleHeightChange() - } + // FIXME we shoudl not have to pass in the element here + element.entity.NotifyFlexibleHeightChange(element) } else { bounds := element.drawer.LayoutBounds() width, height = bounds.Dx(), bounds.Dy() @@ -196,18 +172,6 @@ func (element *Label) updateMinimumSize () { Mul(fixed.I(element.forcedRows)).Floor() } - element.core.SetMinimumSize(width, height) -} - -func (element *Label) draw () { - element.core.DrawBackground ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { })) - - bounds := element.Bounds() - textBounds := element.drawer.LayoutBounds() - - foreground := element.theme.Color ( - tomo.ColorForeground, - tomo.State { }) - element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min)) + element.minHeight = height + element.entity.SetMinimumSize(width, height) } diff --git a/elements/doc.go b/elements/notdone/doc.go similarity index 100% rename from elements/doc.go rename to elements/notdone/doc.go diff --git a/elements/notdone/label.go b/elements/notdone/label.go new file mode 100644 index 0000000..f980395 --- /dev/null +++ b/elements/notdone/label.go @@ -0,0 +1,177 @@ +package elements + +import "golang.org/x/image/math/fixed" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/textdraw" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" + +// Label is a simple text box. +type Label struct { + entity tomo.FlexibleEntity + + align textdraw.Align + wrap bool + text string + drawer textdraw.Drawer + + forcedColumns int + forcedRows int + minHeight int + + config config.Wrapped + 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) { + element = &Label { } + element.theme.Case = tomo.C("tomo", "label") + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + element.SetWrap(wrap) + element.SetText(text) + return +} + +// Bind binds this element to an entity. +func (element *Label) Bind (entity tomo.Entity) { + element.entity = entity.(tomo.FlexibleEntity) + if element.entity == nil { return } + element.updateMinimumSize() +} + +// EmCollapse forces a minimum width and height upon the label. The width is +// measured in emspaces, and the height is measured in lines. If a zero value is +// given for a dimension, its minimum will be determined by the label's content. +// If the label's content is greater than these dimensions, it will be truncated +// to fit. +func (element *Label) EmCollapse (columns int, rows int) { + element.forcedColumns = columns + element.forcedRows = rows + if element.entity == nil { return } + element.updateMinimumSize() +} + +// FlexibleHeightFor returns the reccomended height for this element based on +// the given width in order to allow the text to wrap properly. +func (element *Label) FlexibleHeightFor (width int) (height int) { + if element.wrap { + return element.drawer.ReccomendedHeightFor(width) + } else { + return element.minHeight + } +} + +// SetText sets the label's text. +func (element *Label) SetText (text string) { + if element.text == text { return } + + element.text = text + element.drawer.SetText([]rune(text)) + if element.entity == nil { return } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetWrap sets wether or not the label's text wraps. If the text is set to +// wrap, the element will have a minimum size of a single character and +// automatically wrap its text. If the text is set to not wrap, the element will +// have a minimum size that fits its text. +func (element *Label) SetWrap (wrap bool) { + if wrap == element.wrap { return } + if !wrap { + element.drawer.SetMaxWidth(0) + element.drawer.SetMaxHeight(0) + } + element.wrap = wrap + if element.entity == nil { return } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetAlign sets the alignment method of the label. +func (element *Label) SetAlign (align textdraw.Align) { + if align == element.align { return } + element.align = align + element.drawer.SetAlign(align) + if element.entity == nil { return } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *Label) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + if element.entity == nil { return } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetConfig sets the element's configuration. +func (element *Label) SetConfig (new tomo.Config) { + if new == element.config.Config { return } + element.config.Config = new + if element.entity == nil { return } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Label) Draw (destination canvas.Canvas) { + if element.entity == nil { return } + + bounds := element.entity. Bounds() + + if element.wrap { + element.drawer.SetMaxWidth(bounds.Dx()) + element.drawer.SetMaxHeight(bounds.Dy()) + } + + element.entity.DrawBackground(destination, bounds) + + textBounds := element.drawer.LayoutBounds() + foreground := element.theme.Color ( + tomo.ColorForeground, + tomo.State { }) + element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) +} + +func (element *Label) updateMinimumSize () { + var width, height int + + if element.wrap { + em := element.drawer.Em().Round() + if em < 1 { + em = element.theme.Padding(tomo.PatternBackground)[0] + } + width, height = em, element.drawer.LineHeight().Round() + // FIXME we shoudl not have to pass in the element here + element.entity.NotifyFlexibleHeightChange(element) + } else { + bounds := element.drawer.LayoutBounds() + width, height = bounds.Dx(), bounds.Dy() + } + + if element.forcedColumns > 0 { + width = + element.drawer.Em(). + Mul(fixed.I(element.forcedColumns)).Floor() + } + + if element.forcedRows > 0 { + height = + element.drawer.LineHeight(). + Mul(fixed.I(element.forcedRows)).Floor() + } + + element.minHeight = height + element.entity.SetMinimumSize(width, height) +} diff --git a/elements/lerpslider.go b/elements/notdone/lerpslider.go similarity index 100% rename from elements/lerpslider.go rename to elements/notdone/lerpslider.go diff --git a/elements/list.go b/elements/notdone/list.go similarity index 93% rename from elements/list.go rename to elements/notdone/list.go index fb03f7c..75d32eb 100644 --- a/elements/list.go +++ b/elements/notdone/list.go @@ -6,17 +6,18 @@ 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/elements/core" 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 { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - + entity listEntity pressed bool contentHeight int @@ -25,7 +26,6 @@ type List struct { selectedEntry int scroll int - entries []ListEntry config config.Wrapped theme theme.Wrapped @@ -38,36 +38,14 @@ type List struct { func NewList (entries ...ListEntry) (element *List) { element = &List { selectedEntry: -1 } element.theme.Case = tomo.C("tomo", "list") - element.Core, element.core = core.NewCore(element, element.handleResize) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore (element.core, func () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - }) element.entries = make([]ListEntry, len(entries)) for index, entry := range entries { element.entries[index] = entry } - - element.updateMinimumSize() return } -func (element *List) handleResize () { - 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() -} - // SetTheme sets the element's theme. func (element *List) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } @@ -77,7 +55,7 @@ func (element *List) SetTheme (new tomo.Theme) { element.entries[index] = entry } element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -92,18 +70,6 @@ func (element *List) SetConfig (new tomo.Config) { element.redo() } -func (element *List) redo () { - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - // 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 @@ -213,19 +179,6 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { return false, true } -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 -} - // 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. @@ -443,7 +396,32 @@ func (element *List) scrollBoundsChange () { } } -func (element *List) draw () { +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) diff --git a/elements/listentry.go b/elements/notdone/listentry.go similarity index 100% rename from elements/listentry.go rename to elements/notdone/listentry.go diff --git a/elements/progressbar.go b/elements/notdone/progressbar.go similarity index 96% rename from elements/progressbar.go rename to elements/notdone/progressbar.go index 3286214..5a20347 100644 --- a/elements/progressbar.go +++ b/elements/notdone/progressbar.go @@ -2,14 +2,11 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // ProgressBar displays a visual indication of how far along a task is. type ProgressBar struct { - *core.Core - core core.CoreControl progress float64 config config.Wrapped diff --git a/elements/scrollbar.go b/elements/notdone/scrollbar.go similarity index 98% rename from elements/scrollbar.go rename to elements/notdone/scrollbar.go index 89b8262..d92815e 100644 --- a/elements/scrollbar.go +++ b/elements/notdone/scrollbar.go @@ -3,7 +3,6 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" @@ -19,9 +18,6 @@ import "git.tebibyte.media/sashakoshka/tomo/default/config" // Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is // better for most cases. type ScrollBar struct { - *core.Core - core core.CoreControl - vertical bool enabled bool dragging bool diff --git a/elements/slider.go b/elements/notdone/slider.go similarity index 97% rename from elements/slider.go rename to elements/notdone/slider.go index e7d38b8..0f344c3 100644 --- a/elements/slider.go +++ b/elements/notdone/slider.go @@ -3,17 +3,11 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Slider is a slider control with a floating point value between zero and one. type Slider struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - value float64 vertical bool dragging bool diff --git a/elements/spacer.go b/elements/notdone/spacer.go similarity index 95% rename from elements/spacer.go rename to elements/notdone/spacer.go index f1655d0..bbcef24 100644 --- a/elements/spacer.go +++ b/elements/notdone/spacer.go @@ -1,14 +1,11 @@ package elements import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Spacer can be used to put space between two elements.. type Spacer struct { - *core.Core - core core.CoreControl line bool config config.Wrapped diff --git a/elements/switch.go b/elements/notdone/switch.go similarity index 97% rename from elements/switch.go rename to elements/notdone/switch.go index 77bbc84..f80763f 100644 --- a/elements/switch.go +++ b/elements/notdone/switch.go @@ -4,17 +4,12 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Switch is a toggle-able on/off switch with an optional label. It is // functionally identical to Checkbox, but plays a different semantic role. type Switch struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl drawer textdraw.Drawer pressed bool diff --git a/elements/textbox.go b/elements/notdone/textbox.go similarity index 98% rename from elements/textbox.go rename to elements/notdone/textbox.go index 1903686..35af69c 100644 --- a/elements/textbox.go +++ b/elements/notdone/textbox.go @@ -12,17 +12,11 @@ import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/fixedutil" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // TextBox is a single-line text input. type TextBox struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - lastClick time.Time dragging int dot textmanip.Dot