diff --git a/elements/button.go b/elements/button.go index abeb4a6..36552d9 100644 --- a/elements/button.go +++ b/elements/button.go @@ -44,6 +44,63 @@ func (element *Button) Entity () tomo.Entity { return element.entity } +// Draw causes the element to draw to the specified destination canvas. +func (element *Button) Draw (destination canvas.Canvas) { + state := element.state() + bounds := element.entity.Bounds() + pattern := element.theme.Pattern(tomo.PatternButton, state) + + pattern.Draw(destination, bounds) + + foreground := element.theme.Color(tomo.ColorForeground, state) + sink := element.theme.Sink(tomo.PatternButton) + margin := element.theme.Margin(tomo.PatternButton) + + offset := image.Pt ( + bounds.Dx() / 2, + bounds.Dy() / 2).Add(bounds.Min) + + if element.showText { + textBounds := element.drawer.LayoutBounds() + offset.X -= textBounds.Dx() / 2 + offset.Y -= textBounds.Dy() / 2 + offset.Y -= textBounds.Min.Y + offset.X -= textBounds.Min.X + } + + if element.hasIcon { + icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + if icon != nil { + iconBounds := icon.Bounds() + addedWidth := iconBounds.Dx() + iconOffset := offset + + if element.showText { + addedWidth += margin.X + } + + iconOffset.X -= addedWidth / 2 + iconOffset.Y = + bounds.Min.Y + + (bounds.Dy() - + iconBounds.Dy()) / 2 + if element.pressed { + iconOffset = iconOffset.Add(sink) + } + offset.X += addedWidth / 2 + + icon.Draw(destination, foreground, iconOffset) + } + } + + if element.showText { + if element.pressed { + offset = offset.Add(sink) + } + element.drawer.Draw(destination, foreground, offset) + } +} + // OnClick sets the function to be called when the button is clicked. func (element *Button) OnClick (callback func ()) { element.onClick = callback @@ -116,63 +173,6 @@ func (element *Button) SetConfig (new tomo.Config) { element.entity.Invalidate() } -// Draw causes the element to draw to the specified destination canvas. -func (element *Button) Draw (destination canvas.Canvas) { - state := element.state() - bounds := element.entity.Bounds() - pattern := element.theme.Pattern(tomo.PatternButton, state) - - pattern.Draw(destination, bounds) - - foreground := element.theme.Color(tomo.ColorForeground, state) - sink := element.theme.Sink(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) - - offset := image.Pt ( - bounds.Dx() / 2, - bounds.Dy() / 2).Add(bounds.Min) - - if element.showText { - textBounds := element.drawer.LayoutBounds() - offset.X -= textBounds.Dx() / 2 - offset.Y -= textBounds.Dy() / 2 - offset.Y -= textBounds.Min.Y - offset.X -= textBounds.Min.X - } - - if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) - if icon != nil { - iconBounds := icon.Bounds() - addedWidth := iconBounds.Dx() - iconOffset := offset - - if element.showText { - addedWidth += margin.X - } - - iconOffset.X -= addedWidth / 2 - iconOffset.Y = - bounds.Min.Y + - (bounds.Dy() - - iconBounds.Dy()) / 2 - if element.pressed { - iconOffset = iconOffset.Add(sink) - } - offset.X += addedWidth / 2 - - icon.Draw(destination, foreground, iconOffset) - } - } - - if element.showText { - if element.pressed { - offset = offset.Add(sink) - } - element.drawer.Draw(destination, foreground, offset) - } -} - func (element *Button) HandleFocusChange () { element.entity.Invalidate() } diff --git a/elements/checkbox.go b/elements/checkbox.go index cd8adbe..58e37e0 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -41,6 +41,36 @@ func (element *Checkbox) Entity () tomo.Entity { return element.entity } +// Draw causes the element to draw to the specified destination canvas. +func (element *Checkbox) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) + + state := tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.pressed, + On: element.checked, + } + + element.entity.DrawBackground(destination) + + pattern := element.theme.Pattern(tomo.PatternButton, state) + pattern.Draw(destination, boxBounds) + + textBounds := element.drawer.LayoutBounds() + margin := element.theme.Margin(tomo.PatternBackground) + offset := bounds.Min.Add(image.Point { + X: bounds.Dy() + margin.X, + }) + + offset.Y -= textBounds.Min.Y + offset.X -= textBounds.Min.X + + foreground := element.theme.Color(tomo.ColorForeground, state) + element.drawer.Draw(destination, foreground, offset) +} + // OnToggle sets the function to be called when the checkbox is toggled. func (element *Checkbox) OnToggle (callback func ()) { element.onToggle = callback @@ -96,36 +126,6 @@ func (element *Checkbox) SetConfig (new tomo.Config) { element.entity.Invalidate() } -// Draw causes the element to draw to the specified destination canvas. -func (element *Checkbox) Draw (destination canvas.Canvas) { - bounds := element.entity.Bounds() - boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) - - state := tomo.State { - Disabled: !element.Enabled(), - Focused: element.entity.Focused(), - Pressed: element.pressed, - On: element.checked, - } - - element.entity.DrawBackground(destination) - - pattern := element.theme.Pattern(tomo.PatternButton, state) - pattern.Draw(destination, boxBounds) - - textBounds := element.drawer.LayoutBounds() - margin := element.theme.Margin(tomo.PatternBackground) - offset := bounds.Min.Add(image.Point { - X: bounds.Dy() + margin.X, - }) - - offset.Y -= textBounds.Min.Y - offset.X -= textBounds.Min.X - - foreground := element.theme.Color(tomo.ColorForeground, state) - element.drawer.Draw(destination, foreground, offset) -} - func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() diff --git a/elements/notdone/doc.go b/elements/doc.go similarity index 100% rename from elements/notdone/doc.go rename to elements/doc.go diff --git a/elements/notdone/switch.go b/elements/switch.go similarity index 73% rename from elements/notdone/switch.go rename to elements/switch.go index f80763f..e340e61 100644 --- a/elements/notdone/switch.go +++ b/elements/switch.go @@ -3,6 +3,7 @@ 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/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" @@ -10,8 +11,10 @@ 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 { + entity tomo.FocusableEntity drawer textdraw.Drawer + enabled bool pressed bool checked bool text string @@ -26,12 +29,11 @@ type Switch struct { func NewSwitch (text string, on bool) (element *Switch) { element = &Switch { checked: on, - text: text, + text: text, + enabled: true, } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.theme.Case = tomo.C("tomo", "switch") - 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,129 +42,25 @@ func NewSwitch (text string, on bool) (element *Switch) { return } -func (element *Switch) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - element.Focus() - element.pressed = true - element.redo() +// Entity returns this element's entity. +func (element *Switch) Entity () tomo.Entity { + return element.entity } -func (element *Switch) 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 *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter { - element.pressed = true - element.redo() - } -} - -func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter && element.pressed { - element.pressed = false - element.checked = !element.checked - element.redo() - if element.onToggle != nil { - element.onToggle() - } - } -} - -// OnToggle sets the function to be called when the switch is flipped. -func (element *Switch) OnToggle (callback func ()) { - element.onToggle = callback -} - -// Value reports whether or not the switch is currently on. -func (element *Switch) Value () (on bool) { - return element.checked -} - -// SetEnabled sets whether this switch can be flipped or not. -func (element *Switch) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) -} - -// SetText sets the checkbox's label text. -func (element *Switch) SetText (text string) { - if element.text == text { return } - - element.text = text - element.drawer.SetText([]rune(text)) - element.updateMinimumSize() - element.redo() -} - -// SetTheme sets the element's theme. -func (element *Switch) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.redo() -} - -// SetConfig sets the element's configuration. -func (element *Switch) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.updateMinimumSize() - element.redo() -} - -func (element *Switch) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *Switch) updateMinimumSize () { - textBounds := element.drawer.LayoutBounds() - lineHeight := element.drawer.LineHeight().Round() - - if element.text == "" { - element.core.SetMinimumSize(lineHeight * 2, lineHeight) - } else { - element.core.SetMinimumSize ( - lineHeight * 2 + - element.theme.Margin(tomo.PatternBackground).X + - textBounds.Dx(), - lineHeight) - } -} - -func (element *Switch) draw () { - bounds := element.Bounds() +// Draw causes the element to draw to the specified destination canvas. +func (element *Switch) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, 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) if element.checked { handleBounds.Min.X += bounds.Dy() @@ -180,11 +78,11 @@ func (element *Switch) draw () { gutterPattern := element.theme.Pattern ( tomo.PatternGutter, state) - gutterPattern.Draw(element.core, gutterBounds) + gutterPattern.Draw(destination, gutterBounds) handlePattern := element.theme.Pattern ( tomo.PatternHandle, state) - handlePattern.Draw(element.core, handleBounds) + handlePattern.Draw(destination, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { @@ -196,5 +94,117 @@ func (element *Switch) 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 *Switch) HandleMouseDown (x, y int, button input.Button) { + if !element.Enabled() { return } + element.Focus() + element.pressed = true + element.entity.Invalidate() +} + +func (element *Switch) HandleMouseUp (x, y int, button input.Button) { + 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 *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.checked = !element.checked + element.entity.Invalidate() + if element.onToggle != nil { + element.onToggle() + } + } +} + +// OnToggle sets the function to be called when the switch is flipped. +func (element *Switch) OnToggle (callback func ()) { + element.onToggle = callback +} + +// Value reports whether or not the switch is currently on. +func (element *Switch) Value () (on bool) { + return element.checked +} + +// Focus gives this element input focus. +func (element *Switch) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this switch is enabled or not. +func (element *Switch) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this switch can be toggled or not. +func (element *Switch) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + +// SetText sets the checkbox's label text. +func (element *Switch) SetText (text string) { + if element.text == text { return } + + element.text = text + element.drawer.SetText([]rune(text)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *Switch) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetConfig sets the element's configuration. +func (element *Switch) SetConfig (new tomo.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.entity.Invalidate() +} + +func (element *Switch) updateMinimumSize () { + textBounds := element.drawer.LayoutBounds() + lineHeight := element.drawer.LineHeight().Round() + + if element.text == "" { + element.entity.SetMinimumSize(lineHeight * 2, lineHeight) + } else { + element.entity.SetMinimumSize ( + lineHeight * 2 + + element.theme.Margin(tomo.PatternBackground).X + + textBounds.Dx(), + lineHeight) + } } diff --git a/elements/notdone/textbox.go b/elements/textbox.go similarity index 82% rename from elements/notdone/textbox.go rename to elements/textbox.go index 35af69c..7cd7b08 100644 --- a/elements/notdone/textbox.go +++ b/elements/textbox.go @@ -15,8 +15,16 @@ import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +type textBoxEntity interface { + tomo.FocusableEntity + tomo.ScrollableEntity +} + // TextBox is a single-line text input. type TextBox struct { + entity textBoxEntity + + enabled bool lastClick time.Time dragging int dot textmanip.Dot @@ -42,14 +50,7 @@ type TextBox struct { func NewTextBox (placeholder, value string) (element *TextBox) { element = &TextBox { } element.theme.Case = tomo.C("tomo", "textBox") - 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.entity = tomo.NewEntity(element).(textBoxEntity) element.placeholder = placeholder element.placeholderDrawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, @@ -63,17 +64,79 @@ func NewTextBox (placeholder, value string) (element *TextBox) { return } -func (element *TextBox) handleResize () { +// Entity returns this element's entity. +func (element *TextBox) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *TextBox) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() element.scrollToCursor() - element.draw() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) + + state := element.state() + pattern := element.theme.Pattern(tomo.PatternInput, state) + padding := element.theme.Padding(tomo.PatternInput) + innerCanvas := canvas.Cut(destination, padding.Apply(bounds)) + pattern.Draw(destination, bounds) + offset := element.textOffset() + + if element.entity.Focused() && !element.dot.Empty() { + // draw selection bounds + accent := element.theme.Color(tomo.ColorAccent, state) + canon := element.dot.Canon() + foff := fixedutil.Pt(offset) + start := element.valueDrawer.PositionAt(canon.Start).Add(foff) + end := element.valueDrawer.PositionAt(canon.End).Add(foff) + end.Y += element.valueDrawer.LineHeight() + shapes.FillColorRectangle ( + innerCanvas, + accent, + image.Rectangle { + fixedutil.RoundPt(start), + fixedutil.RoundPt(end), + }) + } + + if len(element.text) == 0 { + // draw placeholder + textBounds := element.placeholderDrawer.LayoutBounds() + foreground := element.theme.Color ( + tomo.ColorForeground, + tomo.State { Disabled: true }) + element.placeholderDrawer.Draw ( + innerCanvas, + foreground, + offset.Sub(textBounds.Min)) + } else { + // draw input value + textBounds := element.valueDrawer.LayoutBounds() + foreground := element.theme.Color(tomo.ColorForeground, state) + element.valueDrawer.Draw ( + innerCanvas, + foreground, + offset.Sub(textBounds.Min)) + } + + if element.entity.Focused() && element.dot.Empty() { + // draw cursor + foreground := element.theme.Color(tomo.ColorForeground, state) + cursorPosition := fixedutil.RoundPt ( + element.valueDrawer.PositionAt(element.dot.End)) + shapes.ColorLine ( + innerCanvas, + foreground, 1, + cursorPosition.Add(offset), + image.Pt ( + cursorPosition.X, + cursorPosition.Y + element.valueDrawer. + LineHeight().Round()).Add(offset)) } } func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } - if !element.Focused() { element.Focus() } + element.Focus() if button == input.ButtonLeft { runeIndex := element.atPosition(image.Pt(x, y)) @@ -88,7 +151,7 @@ func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { element.lastClick = time.Now() } - element.redo() + element.entity.Invalidate() } } @@ -100,7 +163,7 @@ func (element *TextBox) HandleMotion (x, y int) { runeIndex := element.atPosition(image.Pt(x, y)) if runeIndex > -1 { element.dot.End = runeIndex - element.redo() + element.entity.Invalidate() } case 2: @@ -119,14 +182,14 @@ func (element *TextBox) HandleMotion (x, y int) { element.text, runeIndex) } - element.redo() + element.entity.Invalidate() } } } func (element *TextBox) textOffset () image.Point { padding := element.theme.Padding(tomo.PatternInput) - bounds := element.Bounds() + bounds := element.entity.Bounds() innerBounds := padding.Apply(bounds) textHeight := element.valueDrawer.LineHeight().Round() return bounds.Min.Add (image.Pt ( @@ -221,7 +284,7 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.clipboardPut(element.dot.Slice(element.text)) case key == 'v' && modifiers.Control: - window := element.core.Window() + window := element.entity.Window() if window == nil { break } window.Paste (func (d data.Data, err error) { if err != nil { return } @@ -256,25 +319,17 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) } if (textChanged || scrollMemory != element.scroll) { - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } + element.entity.NotifyScrollBoundsChange() } if altered { - element.redo() - } -} - -func (element *TextBox) clipboardPut (text []rune) { - window := element.core.Window() - if window != nil { - window.Copy(data.Bytes(data.MimePlain, []byte(string(text)))) + element.entity.Invalidate() } } func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } +// SetPlaceholder sets the element's placeholder text. func (element *TextBox) SetPlaceholder (placeholder string) { if element.placeholder == placeholder { return } @@ -282,9 +337,10 @@ func (element *TextBox) SetPlaceholder (placeholder string) { element.placeholderDrawer.SetText([]rune(placeholder)) element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } +// SetValue sets the input's value. func (element *TextBox) SetValue (text string) { // if element.text == text { return } @@ -295,27 +351,35 @@ func (element *TextBox) SetValue (text string) { element.dot = textmanip.EmptyDot(element.valueDrawer.Length()) } element.scrollToCursor() - element.redo() + element.entity.Invalidate() } +// Value returns the input's value. func (element *TextBox) Value () (value string) { return string(element.text) } +// Filled returns whether or not this element has a value. func (element *TextBox) Filled () (filled bool) { return len(element.text) > 0 } +// OnKeyDown specifies a function to be called when a key is pressed within the +// text input. func (element *TextBox) OnKeyDown ( callback func (key input.Key, modifiers input.Modifiers) (handled bool), ) { element.onKeyDown = callback } +// OnEnter specifies a function to be called when the enter key is pressed +// within this input. func (element *TextBox) OnEnter (callback func ()) { element.onEnter = callback } +// OnChange specifies a function to be called when the value of this input +// changes. func (element *TextBox) OnChange (callback func ()) { element.onChange = callback } @@ -326,6 +390,23 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) { element.onScrollBoundsChange = callback } +// Focus gives this element input focus. +func (element *TextBox) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this label can be edited or not. +func (element *TextBox) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this label can be edited or not. +func (element *TextBox) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + // ScrollContentBounds returns the full content size of the element. func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) { bounds = element.valueDrawer.LayoutBounds() @@ -342,11 +423,6 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) { 0) } -func (element *TextBox) scrollViewportWidth () (width int) { - padding := element.theme.Padding(tomo.PatternInput) - return padding.Apply(element.Bounds()).Dx() -} - // ScrollTo scrolls the viewport to the specified point relative to // ScrollBounds. func (element *TextBox) ScrollTo (position image.Point) { @@ -359,10 +435,8 @@ func (element *TextBox) ScrollTo (position image.Point) { maxPosition := contentBounds.Max.X - element.scrollViewportWidth() if element.scroll > maxPosition { element.scroll = maxPosition } - element.redo() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } + element.entity.Invalidate() + element.entity.NotifyScrollBoundsChange() } // ScrollAxes returns the supported axes for scrolling. @@ -370,32 +444,6 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) { return true, false } -func (element *TextBox) runOnChange () { - if element.onChange != nil { - element.onChange() - } -} - -func (element *TextBox) scrollToCursor () { - if !element.core.HasImage() { return } - - padding := element.theme.Padding(tomo.PatternInput) - bounds := padding.Apply(element.Bounds()) - bounds = bounds.Sub(bounds.Min) - bounds.Max.X -= element.valueDrawer.Em().Round() - cursorPosition := fixedutil.RoundPt ( - element.valueDrawer.PositionAt(element.dot.End)) - cursorPosition.X -= element.scroll - maxX := bounds.Max.X - minX := maxX - if cursorPosition.X > maxX { - element.scroll += cursorPosition.X - maxX - } else if cursorPosition.X < minX { - element.scroll -= minX - cursorPosition.X - if element.scroll < 0 { element.scroll = 0 } - } -} - // SetTheme sets the element's theme. func (element *TextBox) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } @@ -406,7 +454,7 @@ func (element *TextBox) SetTheme (new tomo.Theme) { element.placeholderDrawer.SetFace(face) element.valueDrawer.SetFace(face) element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -414,13 +462,46 @@ func (element *TextBox) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() +} + +func (element *TextBox) runOnChange () { + if element.onChange != nil { + element.onChange() + } +} + +func (element *TextBox) scrollViewportWidth () (width int) { + padding := element.theme.Padding(tomo.PatternInput) + return padding.Apply(element.entity.Bounds()).Dx() +} + +func (element *TextBox) scrollToCursor () { + padding := element.theme.Padding(tomo.PatternInput) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min) + bounds.Max.X -= element.valueDrawer.Em().Round() + cursorPosition := fixedutil.RoundPt ( + element.valueDrawer.PositionAt(element.dot.End)) + cursorPosition.X -= element.scroll + maxX := bounds.Max.X + minX := maxX + if cursorPosition.X > maxX { + element.scroll += cursorPosition.X - maxX + element.entity.NotifyScrollBoundsChange() + element.entity.Invalidate() + } else if cursorPosition.X < minX { + element.scroll -= minX - cursorPosition.X + if element.scroll < 0 { element.scroll = 0 } + element.entity.Invalidate() + element.entity.NotifyScrollBoundsChange() + } } func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() padding := element.theme.Padding(tomo.PatternInput) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal() + textBounds.Dx(), padding.Vertical() + element.placeholderDrawer.LineHeight().Round()) @@ -430,81 +511,19 @@ func (element *TextBox) notifyAsyncTextChange () { element.runOnChange() element.valueDrawer.SetText(element.text) element.scrollToCursor() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - element.redo() + element.entity.Invalidate() } -func (element *TextBox) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() +func (element *TextBox) clipboardPut (text []rune) { + window := element.entity.Window() + if window != nil { + window.Copy(data.Bytes(data.MimePlain, []byte(string(text)))) } } -func (element *TextBox) draw () { - bounds := element.Bounds() - - state := tomo.State { +func (element *TextBox) state () tomo.State { + return tomo.State { Disabled: !element.Enabled(), - Focused: element.Focused(), - } - pattern := element.theme.Pattern(tomo.PatternInput, state) - padding := element.theme.Padding(tomo.PatternInput) - innerCanvas := canvas.Cut(element.core, padding.Apply(bounds)) - pattern.Draw(element.core, bounds) - offset := element.textOffset() - - if element.Focused() && !element.dot.Empty() { - // draw selection bounds - accent := element.theme.Color(tomo.ColorAccent, state) - canon := element.dot.Canon() - foff := fixedutil.Pt(offset) - start := element.valueDrawer.PositionAt(canon.Start).Add(foff) - end := element.valueDrawer.PositionAt(canon.End).Add(foff) - end.Y += element.valueDrawer.LineHeight() - shapes.FillColorRectangle ( - innerCanvas, - accent, - image.Rectangle { - fixedutil.RoundPt(start), - fixedutil.RoundPt(end), - }) - } - - if len(element.text) == 0 { - // draw placeholder - textBounds := element.placeholderDrawer.LayoutBounds() - foreground := element.theme.Color ( - tomo.ColorForeground, - tomo.State { Disabled: true }) - element.placeholderDrawer.Draw ( - innerCanvas, - foreground, - offset.Sub(textBounds.Min)) - } else { - // draw input value - textBounds := element.valueDrawer.LayoutBounds() - foreground := element.theme.Color(tomo.ColorForeground, state) - element.valueDrawer.Draw ( - innerCanvas, - foreground, - offset.Sub(textBounds.Min)) - } - - if element.Focused() && element.dot.Empty() { - // draw cursor - foreground := element.theme.Color(tomo.ColorForeground, state) - cursorPosition := fixedutil.RoundPt ( - element.valueDrawer.PositionAt(element.dot.End)) - shapes.ColorLine ( - innerCanvas, - foreground, 1, - cursorPosition.Add(offset), - image.Pt ( - cursorPosition.X, - cursorPosition.Y + element.valueDrawer. - LineHeight().Round()).Add(offset)) + Focused: element.entity.Focused(), } }