diff --git a/backend.go b/backend.go index b6d9e83..238a877 100644 --- a/backend.go +++ b/backend.go @@ -17,6 +17,9 @@ type Backend interface { // possible. This method must be safe to call from other threads. Do (callback func ()) + // NewEntity creates a new entity for the specified element. + NewEntity (owner Element) Entity + // NewWindow creates a new window within the specified bounding // rectangle. The position on screen may be overridden by the backend or // operating system. diff --git a/backends/x/entity.go b/backends/x/entity.go index d24008d..9ee447e 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -19,29 +19,46 @@ type entity struct { isContainer bool } -func bind (parent *entity, window *window, element tomo.Element) *entity { - entity := &entity { - window: window, - parent: parent, - element: element, - } - entity.Invalidate() - if _, ok := element.(tomo.Container); ok { +func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity { + entity := &entity { element: owner } + if _, ok := owner.(tomo.Container); ok { entity.isContainer = true entity.InvalidateLayout() } - - element.Bind(entity) return entity } -func (entity *entity) unbind () { - entity.element.Bind(nil) - for _, childEntity := range entity.children { - childEntity.unbind() +func (ent *entity) unlink () { + ent.propagate (func (child *entity) bool { + child.window = nil + delete(ent.window.system.drawingInvalid, child) + return true + }) + + delete(ent.window.system.drawingInvalid, ent) + ent.parent = nil + ent.window = nil +} + +func (entity *entity) link (parent *entity) { + entity.parent = parent + if parent.window != nil { + entity.setWindow(parent.window) } } +func (ent *entity) setWindow (window *window) { + ent.window = window + ent.Invalidate() + ent.InvalidateLayout() + ent.propagate (func (child *entity) bool { + child.window = window + ent.Invalidate() + ent.InvalidateLayout() + return true + }) +} + func (entity *entity) propagate (callback func (*entity) bool) { for _, child := range entity.children { if callback(child) { break } @@ -61,6 +78,7 @@ func (entity *entity) childAt (point image.Point) *entity { // ----------- Entity ----------- // func (entity *entity) Invalidate () { + if entity.window == nil { return } if entity.window.system.invalidateIgnore { return } entity.window.drawingInvalid.Add(entity) } @@ -77,39 +95,54 @@ func (entity *entity) SetMinimumSize (width, height int) { entity.minWidth = width entity.minHeight = height if entity.parent == nil { - entity.window.setMinimumSize(width, height) + if entity.window != nil { + entity.window.setMinimumSize(width, height) + } } else { entity.parent.element.(tomo.Container). HandleChildMinimumSizeChange(entity.element) } } -func (entity *entity) DrawBackground (destination canvas.Canvas, bounds image.Rectangle) { - if entity.parent == nil { return } - entity.parent.element.(tomo.Container).DrawBackground(destination, bounds) +func (entity *entity) DrawBackground (destination canvas.Canvas) { + if entity.parent != nil { + entity.parent.element.(tomo.Container).DrawBackground(destination) + } else if entity.window != nil { + entity.window.system.theme.Pattern ( + tomo.PatternBackground, + tomo.State { }).Draw ( + destination, + entity.window.canvas.Bounds()) + } } // ----------- ContainerEntity ----------- // func (entity *entity) InvalidateLayout () { + if entity.window == nil { return } if !entity.isContainer { return } entity.layoutInvalid = true entity.window.system.anyLayoutInvalid = true } -func (entity *entity) Adopt (child tomo.Element) { - entity.children = append(entity.children, bind(entity, entity.window, child)) +func (ent *entity) Adopt (child tomo.Element) { + childEntity, ok := child.Entity().(*entity) + if !ok || childEntity == nil { return } + childEntity.link(ent) + ent.children = append(ent.children, childEntity) } -func (entity *entity) Insert (index int, child tomo.Element) { - entity.children = append ( - entity.children[:index + 1], - entity.children[index:]...) - entity.children[index] = bind(entity, entity.window, child) +func (ent *entity) Insert (index int, child tomo.Element) { + childEntity, ok := child.Entity().(*entity) + if !ok || childEntity == nil { return } + ent.children = append ( + ent.children[:index + 1], + ent.children[index:]...) + ent.children[index] = childEntity } func (entity *entity) Disown (index int) { - entity.children[index].unbind() + entity.children[index].unlink() entity.children = append ( entity.children[:index], entity.children[index + 1:]...) @@ -138,9 +171,7 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { child.bounds = bounds child.clippedBounds = entity.bounds.Intersect(bounds) child.Invalidate() - if child.isContainer { - child.InvalidateLayout() - } + child.InvalidateLayout() } func (entity *entity) ChildMinimumSize (index int) (width, height int) { @@ -151,10 +182,12 @@ func (entity *entity) ChildMinimumSize (index int) (width, height int) { // ----------- FocusableEntity ----------- // func (entity *entity) Focused () bool { + if entity.window == nil { return false } return entity.window.focused == entity } func (entity *entity) Focus () { + if entity.window == nil { return } previous := entity.window.focused entity.window.focused = entity if previous != nil { diff --git a/backends/x/system.go b/backends/x/system.go index c9265c5..a9f33ce 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -3,6 +3,8 @@ package x import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" type entitySet map[*entity] struct { } @@ -24,8 +26,8 @@ type system struct { focused *entity canvas canvas.BasicCanvas - theme tomo.Theme - config tomo.Config + theme theme.Wrapped + config config.Wrapped invalidateIgnore bool drawingInvalid entitySet @@ -41,7 +43,7 @@ func (system *system) initialize () { } func (system *system) SetTheme (theme tomo.Theme) { - system.theme = theme + system.theme.Theme = theme system.propagate (func (entity *entity) bool { if child, ok := system.child.element.(tomo.Themeable); ok { child.SetTheme(theme) @@ -51,7 +53,7 @@ func (system *system) SetTheme (theme tomo.Theme) { } func (system *system) SetConfig (config tomo.Config) { - system.config = config + system.config.Config = config system.propagate (func (entity *entity) bool { if child, ok := system.child.element.(tomo.Configurable); ok { child.SetConfig(config) diff --git a/backends/x/window.go b/backends/x/window.go index 2bd12f3..f5368bc 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -67,6 +67,7 @@ func (backend *Backend) newWindow ( window.system.initialize() window.system.pushFunc = window.pasteAndPush + window.theme.Case = tomo.C("tomo", "window") window.xWindow, err = xwindow.Generate(backend.connection) if err != nil { return } @@ -142,14 +143,18 @@ func (window *window) Window () tomo.Window { func (window *window) Adopt (child tomo.Element) { // disown previous child if window.child != nil { - window.child.unbind() + window.child.unlink() window.child = nil } // adopt new child if child != nil { - window.child = bind(nil, window, child) - window.resizeChildToFit() + childEntity, ok := child.Entity().(*entity) + if ok && childEntity != nil { + window.child = childEntity + childEntity.setWindow(window) + window.resizeChildToFit() + } } } diff --git a/element.go b/element.go index 38a9ec2..3b4a132 100644 --- a/element.go +++ b/element.go @@ -6,13 +6,13 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" // Element represents a basic on-screen object. type Element interface { - // Bind assigns an Entity to this element. - Bind (Entity) - // Draw causes the element to draw to the specified canvas. The bounds // of this canvas specify the area that is actually drawn to, while the // Entity bounds specify the actual area of the element. Draw (canvas.Canvas) + + // Entity returns this element's entity. + Entity () Entity } // Container is an element capable of containing child elements. @@ -22,9 +22,11 @@ type Container interface { // Layout causes this element to arrange its children. Layout () - // DrawBackground draws this element's background pattern at the - // specified bounds to any canvas. - DrawBackground (destination canvas.Canvas, bounds image.Rectangle) + // DrawBackground causes the element to draw its background pattern to + // the specified canvas. The bounds of this canvas specify the area that + // is actually drawn to, while the Entity bounds specify the actual area + // of the element. + DrawBackground (canvas.Canvas) // HandleChildMinimumSizeChange is called when a child's minimum size is // changed. diff --git a/elements/button.go b/elements/button.go index a396d92..abeb4a6 100644 --- a/elements/button.go +++ b/elements/button.go @@ -30,6 +30,7 @@ type Button struct { // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { element = &Button { showText: true, enabled: true } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.theme.Case = tomo.C("tomo", "button") element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, @@ -38,11 +39,9 @@ func NewButton (text string) (element *Button) { return } -// 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() +// Entity returns this element's entity. +func (element *Button) Entity () tomo.Entity { + return element.entity } // OnClick sets the function to be called when the button is clicked. @@ -52,7 +51,6 @@ func (element *Button) OnClick (callback func ()) { // Focus gives this element input focus. func (element *Button) Focus () { - if element.entity == nil { return } if !element.entity.Focused() { element.entity.Focus() } } @@ -65,7 +63,6 @@ func (element *Button) Enabled () bool { func (element *Button) SetEnabled (enabled bool) { if element.enabled == enabled { return } element.enabled = enabled - if element.entity == nil { return } element.entity.Invalidate() } @@ -74,7 +71,6 @@ 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.entity.Invalidate() } @@ -97,7 +93,6 @@ func (element *Button) SetIcon (id tomo.Icon) { func (element *Button) ShowText (showText bool) { if element.showText == showText { return } element.showText = showText - if element.entity == nil { return } element.updateMinimumSize() element.entity.Invalidate() } @@ -109,7 +104,6 @@ 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.entity.Invalidate() } @@ -118,14 +112,12 @@ func (element *Button) SetTheme (new tomo.Theme) { 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.entity.Invalidate() } // Draw causes the element to draw to the specified destination canvas. func (element *Button) Draw (destination canvas.Canvas) { - if element.entity == nil { return } state := element.state() bounds := element.entity.Bounds() pattern := element.theme.Pattern(tomo.PatternButton, state) @@ -182,7 +174,7 @@ func (element *Button) Draw (destination canvas.Canvas) { } func (element *Button) HandleFocusChange () { - if element.entity != nil { element.entity.Invalidate() } + element.entity.Invalidate() } func (element *Button) HandleMouseDown (x, y int, button input.Button) { @@ -190,32 +182,31 @@ func (element *Button) HandleMouseDown (x, y int, button input.Button) { element.Focus() if button != input.ButtonLeft { return } element.pressed = true - if element.entity != nil { element.entity.Invalidate() } + 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() } - if element.entity != nil { element.entity.Invalidate() } + element.entity.Invalidate() } func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if !element.Enabled() { return } if key == input.KeyEnter { element.pressed = true - if element.entity != nil { element.entity.Invalidate() } + element.entity.Invalidate() } } func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter && element.pressed { element.pressed = false - if element.entity != nil { element.entity.Invalidate() } + element.entity.Invalidate() if !element.Enabled() { return } if element.onClick != nil { element.onClick() diff --git a/elements/checkbox.go b/elements/checkbox.go index 42445a4..ed9ec97 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -27,6 +27,7 @@ type Checkbox struct { // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { element = &Checkbox { checked: checked, enabled: true } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.theme.Case = tomo.C("tomo", "checkbox") element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, @@ -35,11 +36,9 @@ func NewCheckbox (text string, checked bool) (element *Checkbox) { return } -// 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() +// Entity returns this element's entity. +func (element *Checkbox) Entity () tomo.Entity { + return element.entity } // OnToggle sets the function to be called when the checkbox is toggled. @@ -54,7 +53,6 @@ func (element *Checkbox) Value () (checked bool) { // Focus gives this element input focus. func (element *Checkbox) Focus () { - if element.entity == nil { return } if !element.entity.Focused() { element.entity.Focus() } } @@ -76,7 +74,6 @@ 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() element.entity.Invalidate() } @@ -88,7 +85,6 @@ 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.entity.Invalidate() } @@ -97,15 +93,12 @@ func (element *Checkbox) SetTheme (new tomo.Theme) { 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.entity.Invalidate() } // 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) @@ -116,7 +109,7 @@ func (element *Checkbox) Draw (destination canvas.Canvas) { On: element.checked, } - element.entity.DrawBackground(destination, bounds) + element.entity.DrawBackground(destination) pattern := element.theme.Pattern(tomo.PatternButton, state) pattern.Draw(destination, boxBounds) @@ -135,7 +128,6 @@ func (element *Checkbox) Draw (destination canvas.Canvas) { } func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { - if element.entity == nil { return } if !element.Enabled() { return } element.Focus() element.pressed = true @@ -143,7 +135,6 @@ func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { } 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 @@ -167,7 +158,6 @@ func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers } 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 diff --git a/elements/icon.go b/elements/icon.go index 9d4cc93..03f0e9c 100644 --- a/elements/icon.go +++ b/elements/icon.go @@ -20,15 +20,15 @@ func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) { id: id, size: size, } + element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "icon") + 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() +// Entity returns this element's entity. +func (element *Icon) Entity () tomo.Entity { + return element.entity } // SetIcon sets the element's icon. diff --git a/elements/image.go b/elements/image.go index 97a6f89..7ad3122 100644 --- a/elements/image.go +++ b/elements/image.go @@ -16,15 +16,15 @@ type Image struct { // NewImage creates a new image element. func NewImage (image image.Image) (element *Image) { element = &Image { buffer: canvas.FromImage(image) } + element.entity = tomo.NewEntity(element) + bounds := element.buffer.Bounds() + element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) return } -// 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()) +// Entity returns this element's entity. +func (element *Image) Entity () tomo.Entity { + return element.entity } // Draw causes the element to draw to the specified destination canvas. diff --git a/elements/label.go b/elements/label.go index a0951c6..a733dc3 100644 --- a/elements/label.go +++ b/elements/label.go @@ -29,6 +29,7 @@ type Label struct { func NewLabel (text string, wrap bool) (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)) @@ -37,11 +38,9 @@ func NewLabel (text string, wrap bool) (element *Label) { return } -// 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() +// Entity returns this element's entity. +func (element *Label) Entity () tomo.Entity { + return element.entity } // EmCollapse forces a minimum width and height upon the label. The width is @@ -128,14 +127,14 @@ func (element *Label) SetConfig (new tomo.Config) { func (element *Label) Draw (destination canvas.Canvas) { if element.entity == nil { return } - bounds := element.entity. Bounds() + bounds := element.entity.Bounds() if element.wrap { element.drawer.SetMaxWidth(bounds.Dx()) element.drawer.SetMaxHeight(bounds.Dy()) } - element.entity.DrawBackground(destination, bounds) + element.entity.DrawBackground(destination) textBounds := element.drawer.LayoutBounds() foreground := element.theme.Color ( diff --git a/entity.go b/entity.go index ff371c4..4bda6d9 100644 --- a/entity.go +++ b/entity.go @@ -24,9 +24,11 @@ type Entity interface { SetMinimumSize (width, height int) // DrawBackground asks the parent element to draw its background pattern - // within the specified rectangle. This should be used for transparent - // elements like text labels. - DrawBackground (destination canvas.Canvas, bounds image.Rectangle) + // to a canvas. This should be used for transparent elements like text + // labels. If there is no parent element (that is, the element is + // directly inside of the window), the backend will draw a default + // background pattern. + DrawBackground (canvas.Canvas) } // ContainerEntity is given to elements that support the Container interface. diff --git a/tomo.go b/tomo.go index 28a57a3..a8763ca 100644 --- a/tomo.go +++ b/tomo.go @@ -29,6 +29,12 @@ func Do (callback func ()) { backend.Do(callback) } +// NewEntity generates an entity for an element using the current backend. +func NewEntity (owner Element) Entity { + assertBackend() + return backend.NewEntity(owner) +} + // NewWindow creates a new window using the current backend, and returns it as a // MainWindow. If the window could not be created, an error is returned // explaining why.