From fa898be0467379d86fb895262a0c113f46f34487 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 12 Apr 2023 23:21:34 -0400 Subject: [PATCH 01/41] Updated the core tomo interfaces to support the ECS architecture --- element.go | 63 +++++++++++--------------------- entity.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++ layout.go | 37 ------------------- parent.go | 71 ------------------------------------ 4 files changed, 125 insertions(+), 150 deletions(-) create mode 100644 entity.go delete mode 100644 layout.go delete mode 100644 parent.go diff --git a/element.go b/element.go index b86a876..5045462 100644 --- a/element.go +++ b/element.go @@ -6,54 +6,33 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" // Element represents a basic on-screen object. type Element interface { - // Bounds reports the element's bounding box. This must reflect the - // bounding last given to the element by DrawTo. - Bounds () image.Rectangle + // Bind assigns an Entity to this element. + Bind (Entity) - // MinimumSize specifies the minimum amount of pixels this element's - // width and height may be set to. If the element is given a resize - // event with dimensions smaller than this, it will use its minimum - // instead of the offending dimension(s). - MinimumSize () (width, height int) - - // SetParent sets the parent container of the element. This should only - // be called by the parent when the element is adopted. If parent is set - // to nil, it will mark itself as not having a parent. If this method is - // passed a non-nil value and the element already has a parent, it will - // panic. - SetParent (Parent) - - // DrawTo gives the element a canvas to draw on, along with a bounding - // box to be used for laying out the element. This should only be called - // by the parent element. This is typically a region of the parent - // element's canvas. - DrawTo (canvas canvas.Canvas, bounds image.Rectangle, onDamage func (region image.Rectangle)) + // 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) } -// Focusable represents an element that has keyboard navigation support. This -// includes inputs, buttons, sliders, etc. as well as any elements that have -// children (so keyboard navigation events can be propagated downward). +// Container is an element capable of containing child elements. +type Container interface { + Element + + // 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) +} + +// Focusable represents an element that has keyboard navigation support. type Focusable interface { Element - // Focused returns whether or not this element or any of its children - // are currently focused. - Focused () bool - - // Focus focuses this element, if its parent element grants the - // request. - Focus () - - // HandleFocus causes this element to mark itself as focused. If the - // element does not have children, it is disabled, or there are no more - // selectable children in the given direction, it should return false - // and do nothing. Otherwise, it should select itself and any children - // (if applicable) and return true. - HandleFocus (direction input.KeynavDirection) (accepted bool) - - // HandleDeselection causes this element to mark itself and all of its - // children as unfocused. - HandleUnfocus () + // HandleFocusChange is called when the element is focused or unfocused. + HandleFocusChange () } // KeyboardTarget represents an element that can receive keyboard input. diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..27bddab --- /dev/null +++ b/entity.go @@ -0,0 +1,104 @@ +package tomo + +import "image" + +// Entity is a handle given to elements by the backend. Different types of +// entities may be assigned to elements that support different capabilities. +type Entity interface { + // Invalidate marks the element's current visual as invalid. At the end + // of every event, the backend will ask all invalid entities to redraw + // themselves. + Invalidate () + + // Bounds returns the bounds of the element to be used for drawing and + // layout. + Bounds () image.Rectangle + + // Window returns the window that the element is in. + Window () Window + + // SetMinimumSize reports to the system what the element's minimum size + // can be. The minimum size of child elements should be taken into + // account when calculating this. + 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 (bounds image.Rectangle) +} + +// ContainerEntity is given to elements that support the Container interface. +type ContainerEntity interface { + Entity + + // Adopt adds an element as a child. + Adopt (child Element) + + // Insert inserts an element in the child list at the specified + // location. + Insert (index int, child Element) + + // Disown removes the child at the specified index. + Disown (index int) + + // IndexOf returns the index of the specified child. + IndexOf (child Element) int + + // Child returns the child at the specified index. + Child (index int) Element + + // CountChildren returns the amount of children the element has. + CountChildren () int + + // PlaceChild sets the size and position of the child at the specified + // index to a bounding rectangle. + PlaceChild (index int, bounds image.Rectangle) + + // ChildMinimumSize returns the minimum size of the child at the + // specified index. + ChildMinimumSize (index int) (width, height int) +} + +// FocusableEntity is given to elements that support the Focusable interface. +type FocusableEntity interface { + Entity + + // Focused returns whether the element currently has input focus. + Focused () bool + + // Focus sets this element as focused. If this succeeds, the element will + // recieve a HandleFocus call. + Focus () + + // FocusNext causes the focus to move to the next element. If this + // succeeds, the element will recieve a HandleUnfocus call. + FocusNext () + + // FocusPrevious causes the focus to move to the next element. If this + // succeeds, the element will recieve a HandleUnfocus call. + FocusPrevious () +} + +// FlexibleEntity is given to elements that support the Flexible interface. +type FlexibleEntity interface { + Entity + + // NotifyFlexibleHeightChange notifies the system that the parameters + // affecting a child's flexible height have changed. This method is + // expected to be called by flexible child element when their content + // changes. + NotifyFlexibleHeightChange (child Flexible) +} + +// ScrollableEntity is given to elements that support the Scrollable interface. +type ScrollableEntity interface { + Entity + + // NotifyScrollBoundsChange notifies the parent that a child's scroll + // content bounds or viewport bounds have changed. This is expected to + // be called by child elements when they change their supported scroll + // axes, their scroll position (either autonomously or as a result of a + // call to ScrollTo()), or their content size. + NotifyScrollBoundsChange (child Scrollable) +} diff --git a/layout.go b/layout.go deleted file mode 100644 index 0eff659..0000000 --- a/layout.go +++ /dev/null @@ -1,37 +0,0 @@ -package tomo - -import "image" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -// LayoutEntry associates an element with layout and positioning information so -// it can be arranged by a Layout. -type LayoutEntry struct { - Element - Bounds image.Rectangle - Expand bool -} - -// Layout is capable of arranging elements within a container. It is also able -// to determine the minimum amount of room it needs to do so. -type Layout interface { - // Arrange takes in a slice of entries and a bounding width and height, - // and changes the position of the entiries in the slice so that they - // are properly laid out. The given width and height should not be less - // than what is returned by MinimumSize. - Arrange ( - entries []LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, - ) - - // MinimumSize returns the minimum width and height that the layout - // needs to properly arrange the given slice of layout entries. - MinimumSize ( - entries []LayoutEntry, - margin image.Point, - padding artist.Inset, - ) ( - width, height int, - ) -} diff --git a/parent.go b/parent.go deleted file mode 100644 index 57aa45d..0000000 --- a/parent.go +++ /dev/null @@ -1,71 +0,0 @@ -package tomo - -import "image" - -// Parent represents a type capable of containing child elements. -type Parent interface { - // NotifyMinimumSizeChange notifies the container that a child element's - // minimum size has changed. This method is expected to be called by - // child elements when their minimum size changes. - NotifyMinimumSizeChange (child Element) - - // Window returns the window containing the parent. - Window () Window -} - -// FocusableParent represents a parent with keyboard navigation support. -type FocusableParent interface { - Parent - - // RequestFocus notifies the parent that a child element is requesting - // keyboard focus. If the parent grants the request, the method will - // return true and the child element should behave as if a HandleFocus - // call was made. - RequestFocus (child Focusable) (granted bool) - - // RequestFocusMotion notifies the parent that a child element wants the - // focus to be moved to the next focusable element. - RequestFocusNext (child Focusable) - - // RequestFocusMotion notifies the parent that a child element wants the - // focus to be moved to the previous focusable element. - RequestFocusPrevious (child Focusable) -} - -// FlexibleParent represents a parent that accounts for elements with -// flexible height. -type FlexibleParent interface { - Parent - - // NotifyFlexibleHeightChange notifies the parent that the parameters - // affecting a child's flexible height have changed. This method is - // expected to be called by flexible child element when their content - // changes. - NotifyFlexibleHeightChange (child Flexible) -} - -// ScrollableParent represents a parent that can change the scroll -// position of its child element(s). -type ScrollableParent interface { - Parent - - // NotifyScrollBoundsChange notifies the parent that a child's scroll - // content bounds or viewport bounds have changed. This is expected to - // be called by child elements when they change their supported scroll - // axes, their scroll position (either autonomously or as a result of a - // call to ScrollTo()), or their content size. - NotifyScrollBoundsChange (child Scrollable) -} - -// BackgroundParent represents a parent that is able to re-draw a portion of its -// background upon request. This is intended to be used by transparent elements -// that want to adopt their parent's background pattern. If a parent implements -// this interface, it should call a child's DrawTo method when its area of the -// background is affected. -type BackgroundParent interface { - Parent - - // DrawBackground draws a portion of the parent's background pattern - // within the specified bounds. The parent will not push these changes. - DrawBackground (bounds image.Rectangle) -} From b190f01a7114bf5331e21884ca7c1cd198723c0b Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 12 Apr 2023 23:25:08 -0400 Subject: [PATCH 02/41] It might be time to put layouts to bed --- layouts/dialog.go | 152 ------------------------------------------ layouts/doc.go | 2 - layouts/horizontal.go | 110 ------------------------------ layouts/vertical.go | 113 ------------------------------- 4 files changed, 377 deletions(-) delete mode 100644 layouts/dialog.go delete mode 100644 layouts/doc.go delete mode 100644 layouts/horizontal.go delete mode 100644 layouts/vertical.go diff --git a/layouts/dialog.go b/layouts/dialog.go deleted file mode 100644 index bf61e99..0000000 --- a/layouts/dialog.go +++ /dev/null @@ -1,152 +0,0 @@ -package layouts - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -// Dialog arranges elements in the form of a dialog box. The first element is -// positioned above as the main focus of the dialog, and is set to expand -// regardless of whether it is expanding or not. The remaining elements are -// arranged at the bottom in a row called the control row, which is aligned to -// the right, the last element being the rightmost one. -type Dialog struct { - // If Mergin is true, a margin will be placed between each element. - Gap bool - - // If Pad is true, there will be padding running along the inside of the - // layout's border. - Pad bool -} - -// Arrange arranges a list of entries into a dialog. -func (layout Dialog) Arrange ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, -) { - if layout.Pad { bounds = padding.Apply(bounds) } - - controlRowWidth, controlRowHeight := 0, 0 - if len(entries) > 1 { - controlRowWidth, - controlRowHeight = layout.minimumSizeOfControlRow ( - entries[1:], margin, padding) - } - - if len(entries) > 0 { - main := entries[0] - main.Bounds.Min = bounds.Min - mainHeight := bounds.Dy() - controlRowHeight - if layout.Gap { - mainHeight -= margin.Y - } - main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight)) - entries[0] = main - } - - if len(entries) > 1 { - freeSpace := bounds.Dx() - expandingElements := 0 - - // count the number of expanding elements and the amount of free - // space for them to collectively occupy - for index, entry := range entries[1:] { - if entry.Expand { - expandingElements ++ - } else { - entryMinWidth, _ := entry.MinimumSize() - freeSpace -= entryMinWidth - } - if index > 0 && layout.Gap { - freeSpace -= margin.X - } - } - expandingElementWidth := 0 - if expandingElements > 0 { - expandingElementWidth = freeSpace / expandingElements - } - - // determine starting position and dimensions for control row - dot := image.Pt(bounds.Min.X, bounds.Max.Y - controlRowHeight) - if expandingElements == 0 { - dot.X = bounds.Max.X - controlRowWidth - } - - // set the size and position of each element in the control row - for index, entry := range entries[1:] { - if index > 0 && layout.Gap { dot.X += margin.X } - - entry.Bounds.Min = dot - entryWidth := 0 - if entry.Expand { - entryWidth = expandingElementWidth - } else { - entryWidth, _ = entry.MinimumSize() - } - dot.X += entryWidth - entryBounds := entry.Bounds - if entryBounds.Dy() != controlRowHeight || - entryBounds.Dx() != entryWidth { - entry.Bounds.Max = entryBounds.Min.Add ( - image.Pt(entryWidth, controlRowHeight)) - } - entries[index + 1] = entry - } - } - - -} - -// MinimumSize returns the minimum width and height that will be needed to -// arrange the given list of entries. -func (layout Dialog) MinimumSize ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - if len(entries) > 0 { - mainChildHeight := 0 - width, mainChildHeight = entries[0].MinimumSize() - height += mainChildHeight - } - - if len(entries) > 1 { - if layout.Gap { height += margin.X } - additionalWidth, - additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin, padding) - height += additionalHeight - if additionalWidth > width { - width = additionalWidth - } - } - - if layout.Pad { - width += padding.Horizontal() - height += padding.Vertical() - } - return -} - -func (layout Dialog) minimumSizeOfControlRow ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entryHeight > height { - height = entryHeight - } - width += entryWidth - if layout.Gap && index > 0 { - width += margin.X - } - } - return -} diff --git a/layouts/doc.go b/layouts/doc.go deleted file mode 100644 index bd7126c..0000000 --- a/layouts/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package layouts provides a set of pre-made layouts. -package layouts diff --git a/layouts/horizontal.go b/layouts/horizontal.go deleted file mode 100644 index f36f7a4..0000000 --- a/layouts/horizontal.go +++ /dev/null @@ -1,110 +0,0 @@ -package layouts - -import "image" -import "golang.org/x/image/math/fixed" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/fixedutil" - -// Horizontal arranges elements horizontally. Elements at the start of the entry -// list will be positioned on the left, and elements at the end of the entry -// list will positioned on the right. All elements have the same height. -type Horizontal struct { - // If Gap is true, a gap will be placed between each element. - Gap bool - - // If Pad is true, there will be padding running along the inside of the - // layout's border. - Pad bool -} - -// Arrange arranges a list of entries horizontally. -func (layout Horizontal) Arrange ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, -) { - if layout.Pad { bounds = padding.Apply(bounds) } - - // get width of expanding elements - expandingElementWidth := layout.expandingElementWidth ( - entries, margin, padding, bounds.Dx()) - - // set the size and position of each element - dot := fixedutil.Pt(bounds.Min) - for index, entry := range entries { - if index > 0 && layout.Gap { dot.X += fixed.I(margin.X) } - - entry.Bounds.Min = fixedutil.FloorPt(dot) - entryWidth := fixed.Int26_6(0) - if entry.Expand { - entryWidth = expandingElementWidth - } else { - min, _ := entry.MinimumSize() - entryWidth = fixed.I(min) - } - dot.X += entryWidth - entry.Bounds.Max = entry.Bounds.Min.Add ( - image.Pt(entryWidth.Floor(), bounds.Dy())) - - entries[index] = entry - } -} - -// MinimumSize returns the minimum width and height that will be needed to -// arrange the given list of entries. -func (layout Horizontal) MinimumSize ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entryHeight > height { - height = entryHeight - } - width += entryWidth - if layout.Gap && index > 0 { - width += margin.X - } - } - - if layout.Pad { - width += padding.Horizontal() - height += padding.Vertical() - } - return -} - -func (layout Horizontal) expandingElementWidth ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - freeSpace int, -) ( - width fixed.Int26_6, -) { - expandingElements := 0 - - // count the number of expanding elements and the amount of free space - // for them to collectively occupy - for index, entry := range entries { - if entry.Expand { - expandingElements ++ - } else { - entryMinWidth, _ := entry.MinimumSize() - freeSpace -= entryMinWidth - } - if index > 0 && layout.Gap { - freeSpace -= margin.X - } - } - - if expandingElements > 0 { - width = fixed.I(freeSpace) / fixed.Int26_6(expandingElements) - } - return -} diff --git a/layouts/vertical.go b/layouts/vertical.go deleted file mode 100644 index c3daa4b..0000000 --- a/layouts/vertical.go +++ /dev/null @@ -1,113 +0,0 @@ -package layouts - -import "image" -import "golang.org/x/image/math/fixed" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/fixedutil" - -// Vertical arranges elements vertically. Elements at the start of the entry -// list will be positioned at the top, and elements at the end of the entry list -// will positioned at the bottom. All elements have the same width. -type Vertical struct { - // If Gap is true, a gap will be placed between each element. - Gap bool - - // If Pad is true, there will be padding running along the inside of the - // layout's border. - Pad bool -} - -// Arrange arranges a list of entries vertically. -func (layout Vertical) Arrange ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, -) { - if layout.Pad { bounds = padding.Apply(bounds) } - - // get height of expanding elements - expandingElementHeight, minimumHeights := layout.expandingElementHeight ( - entries, margin, padding, bounds.Dy()) - - // set the size and position of each element - dot := fixedutil.Pt(bounds.Min) - for index, entry := range entries { - if index > 0 && layout.Gap { dot.Y += fixed.I(margin.Y) } - - entry.Bounds.Min = fixedutil.FloorPt(dot) - entryHeight := fixed.Int26_6(0) - if entry.Expand { - entryHeight = expandingElementHeight - } else { - entryHeight = fixed.I(minimumHeights[index]) - } - dot.Y += entryHeight - entryBounds := entry.Bounds - entry.Bounds.Max = entryBounds.Min.Add ( - image.Pt(bounds.Dx(), - entryHeight.Floor())) - entries[index] = entry - } -} - -// MinimumSize returns the minimum width and height that will be needed to -// arrange the given list of entries. -func (layout Vertical) MinimumSize ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entryWidth > width { - width = entryWidth - } - height += entryHeight - if layout.Gap && index > 0 { - height += margin.Y - } - } - - if layout.Pad { - width += padding.Horizontal() - height += padding.Vertical() - } - return -} - -func (layout Vertical) expandingElementHeight ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - freeSpace int, -) ( - height fixed.Int26_6, - minimumHeights []int, -) { - // count the number of expanding elements and the amount of free space - // for them to collectively occupy, while gathering minimum heights. - minimumHeights = make([]int, len(entries)) - expandingElements := 0 - for index, entry := range entries { - _, entryMinHeight := entry.MinimumSize() - minimumHeights[index] = entryMinHeight - - if entry.Expand { - expandingElements ++ - } else { - freeSpace -= entryMinHeight - } - if index > 0 && layout.Gap { - freeSpace -= margin.Y - } - } - - if expandingElements > 0 { - height = fixed.I(freeSpace) / fixed.Int26_6(expandingElements) - } - return -} From 99c890e6cdb53d409931b5015f4c20774d4ce520 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 12 Apr 2023 23:25:40 -0400 Subject: [PATCH 03/41] We won't be needing cores either --- elements/core/core.go | 235 ------------------------ elements/core/doc.go | 7 - elements/core/focusable.go | 101 ---------- elements/core/propagator.go | 355 ------------------------------------ 4 files changed, 698 deletions(-) delete mode 100644 elements/core/core.go delete mode 100644 elements/core/doc.go delete mode 100644 elements/core/focusable.go delete mode 100644 elements/core/propagator.go diff --git a/elements/core/core.go b/elements/core/core.go deleted file mode 100644 index 9811f14..0000000 --- a/elements/core/core.go +++ /dev/null @@ -1,235 +0,0 @@ -package core - -import "image" -import "image/color" -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/shatter" - -// Core is a struct that implements some core functionality common to most -// widgets. It is meant to be embedded directly into a struct. -type Core struct { - canvas canvas.Canvas - bounds image.Rectangle - parent tomo.Parent - outer tomo.Element - - metrics struct { - minimumWidth int - minimumHeight int - } - - drawSizeChange func () - onDamage func (region image.Rectangle) -} - -// NewCore creates a new element core and its corresponding control given the -// element that it will be a part of. If outer is nil, this function will return -// nil. -func NewCore ( - outer tomo.Element, - drawSizeChange func (), -) ( - core *Core, - control CoreControl, -) { - if outer == nil { return } - core = &Core { - outer: outer, - drawSizeChange: drawSizeChange, - } - control = CoreControl { core: core } - return -} - -// Bounds fulfills the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) Bounds () (bounds image.Rectangle) { - if core.canvas == nil { return } - return core.bounds -} - -// MinimumSize fulfils the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) MinimumSize () (width, height int) { - return core.metrics.minimumWidth, core.metrics.minimumHeight -} - -// MinimumSize fulfils the tomo.Element interface. This should not need to be -// overridden, unless you want to detect when the element is parented or -// unparented. -func (core *Core) SetParent (parent tomo.Parent) { - if parent != nil && core.parent != nil { - panic("core.SetParent: element already has a parent") - } - - core.parent = parent -} - -// DrawTo fulfills the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) DrawTo ( - canvas canvas.Canvas, - bounds image.Rectangle, - onDamage func (region image.Rectangle), -) { - core.canvas = canvas - core.bounds = bounds - core.onDamage = onDamage - if core.drawSizeChange != nil && core.canvas != nil { - core.drawSizeChange() - } -} - -// CoreControl is a struct that can exert control over a Core struct. It can be -// used as a canvas. It must not be directly embedded into an element, but -// instead kept as a private member. When a Core struct is created, a -// corresponding CoreControl struct is linked to it and returned alongside it. -type CoreControl struct { - core *Core -} - -// ColorModel fulfills the draw.Image interface. -func (control CoreControl) ColorModel () (model color.Model) { - return color.RGBAModel -} - -// At fulfills the draw.Image interface. -func (control CoreControl) At (x, y int) (pixel color.Color) { - if control.core.canvas == nil { return } - return control.core.canvas.At(x, y) -} - -// Bounds fulfills the draw.Image interface. -func (control CoreControl) Bounds () (bounds image.Rectangle) { - if control.core.canvas == nil { return } - return control.core.canvas.Bounds() -} - -// Set fulfills the draw.Image interface. -func (control CoreControl) Set (x, y int, c color.Color) () { - if control.core.canvas == nil { return } - control.core.canvas.Set(x, y, c) -} - -// Buffer fulfills the canvas.Canvas interface. -func (control CoreControl) Buffer () (data []color.RGBA, stride int) { - if control.core.canvas == nil { return } - return control.core.canvas.Buffer() -} - -// Parent returns the element's parent. -func (control CoreControl) Parent () tomo.Parent { - return control.core.parent -} - -// DrawBackground fills the element's canvas with the parent's background -// pattern, if the parent supports it. If it is not supported, the fallback -// pattern will be used instead. -func (control CoreControl) DrawBackground (fallback artist.Pattern) { - control.DrawBackgroundBounds(fallback, control.Bounds()) -} - -// DrawBackgroundBounds is like DrawBackground, but it takes in a bounding -// rectangle instead of using the element's bounds. -func (control CoreControl) DrawBackgroundBounds ( - fallback artist.Pattern, - bounds image.Rectangle, -) { - parent, ok := control.Parent().(tomo.BackgroundParent) - if ok { - parent.DrawBackground(bounds) - } else if fallback != nil { - fallback.Draw(canvas.Cut(control, bounds), control.Bounds()) - } -} - -// DrawBackgroundBoundsShatter is like DrawBackgroundBounds, but uses the -// shattering algorithm to avoid drawing in areas specified by rocks. -func (control CoreControl) DrawBackgroundBoundsShatter ( - fallback artist.Pattern, - bounds image.Rectangle, - rocks ...image.Rectangle, -) { - tiles := shatter.Shatter(bounds, rocks...) - for _, tile := range tiles { - control.DrawBackgroundBounds(fallback, tile) - } -} - -// Window returns the window containing the element. -func (control CoreControl) Window () tomo.Window { - parent := control.Parent() - if parent == nil { - return nil - } else { - return parent.Window() - } -} - -// Outer returns the outer element given when the control was constructed. -func (control CoreControl) Outer () tomo.Element { - return control.core.outer -} - -// HasImage returns true if the core has an allocated image buffer, and false if -// it doesn't. -func (control CoreControl) HasImage () (has bool) { - return control.core.canvas != nil && !control.core.canvas.Bounds().Empty() -} - -// DamageRegion pushes the selected region of pixels to the parent element. This -// does not need to be called when responding to a resize event. -func (control CoreControl) DamageRegion (regions ...image.Rectangle) { - if control.core.canvas == nil { return } - if control.core.onDamage != nil { - for _, region := range regions { - control.core.onDamage(region) - } - } -} - -// DamageAll pushes all pixels to the parent element. This does not need to be -// called when redrawing in response to a change in size. -func (control CoreControl) DamageAll () { - control.DamageRegion(control.core.Bounds()) -} - -// SetMinimumSize sets the minimum size of this element, notifying the parent -// element in the process. -func (control CoreControl) SetMinimumSize (width, height int) { - core := control.core - if width == core.metrics.minimumWidth && - height == core.metrics.minimumHeight { - return - } - - core.metrics.minimumWidth = width - core.metrics.minimumHeight = height - if control.core.parent != nil { - control.core.parent.NotifyMinimumSizeChange(control.core.outer) - } -} - -// ConstrainSize contstrains the specified width and height to the minimum width -// and height, and returns wether or not anything ended up being constrained. -func (control CoreControl) ConstrainSize ( - inWidth, inHeight int, -) ( - outWidth, outHeight int, - constrained bool, -) { - core := control.core - outWidth = inWidth - outHeight = inHeight - if outWidth < core.metrics.minimumWidth { - outWidth = core.metrics.minimumWidth - constrained = true - } - if outHeight < core.metrics.minimumHeight { - outHeight = core.metrics.minimumHeight - constrained = true - } - return -} diff --git a/elements/core/doc.go b/elements/core/doc.go deleted file mode 100644 index 5870f79..0000000 --- a/elements/core/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package core provides tools that allow elements to easily fulfill common -// interfaces without having to duplicate a ton of code. Each "core" is a type -// that can be embedded into an element directly, working to fulfill a -// particular interface. Each one comes with a corresponding core control, which -// provides an interface for elements to exert control over the core. Core -// controls should be kept private. -package core diff --git a/elements/core/focusable.go b/elements/core/focusable.go deleted file mode 100644 index ed3627f..0000000 --- a/elements/core/focusable.go +++ /dev/null @@ -1,101 +0,0 @@ -package core - -// import "runtime/debug" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" - -// FocusableCore is a struct that can be embedded into objects to make them -// focusable, giving them the default keynav behavior. -type FocusableCore struct { - core CoreControl - focused bool - enabled bool - drawFocusChange func () -} - -// NewFocusableCore creates a new focusability core and its corresponding -// control. If your element needs to visually update itself when it's focus -// state changes (which it should), a callback to draw and push the update can -// be specified. -func NewFocusableCore ( - core CoreControl, - drawFocusChange func (), -) ( - focusable *FocusableCore, - control FocusableCoreControl, -) { - focusable = &FocusableCore { - core: core, - drawFocusChange: drawFocusChange, - enabled: true, - } - control = FocusableCoreControl { core: focusable } - return -} - -// Focused returns whether or not this element is currently focused. -func (core *FocusableCore) Focused () (focused bool) { - return core.focused -} - -// Focus focuses this element, if its parent element grants the request. -func (core *FocusableCore) Focus () { - if !core.enabled || core.focused { return } - parent := core.core.Parent() - if parent, ok := parent.(tomo.FocusableParent); ok { - core.focused = parent.RequestFocus ( - core.core.Outer().(tomo.Focusable)) - } -} - -// HandleFocus causes this element to mark itself as focused, if it can -// currently be. Otherwise, it will return false and do nothing. -func (core *FocusableCore) HandleFocus ( - direction input.KeynavDirection, -) ( - accepted bool, -) { - direction = direction.Canon() - if !core.enabled { return false } - if core.focused && direction != input.KeynavDirectionNeutral { - return false - } - - if core.focused == false { - core.focused = true - if core.drawFocusChange != nil { core.drawFocusChange() } - } - return true -} - -// HandleUnfocus causes this element to mark itself as unfocused. -func (core *FocusableCore) HandleUnfocus () { - core.focused = false - // debug.PrintStack() - if core.drawFocusChange != nil { core.drawFocusChange() } -} - -// Enabled returns whether or not the element is enabled. -func (core *FocusableCore) Enabled () (enabled bool) { - return core.enabled -} - -// FocusableCoreControl is a struct that can be used to exert control over a -// focusability core. It must not be directly embedded into an element, but -// instead kept as a private member. When a FocusableCore struct is created, a -// corresponding FocusableCoreControl struct is linked to it and returned -// alongside it. -type FocusableCoreControl struct { - core *FocusableCore -} - -// SetEnabled sets whether the focusability core is enabled. If the state -// changes, this will call drawFocusChange. -func (control FocusableCoreControl) SetEnabled (enabled bool) { - if control.core.enabled == enabled { return } - control.core.enabled = enabled - if !enabled { control.core.focused = false } - if control.core.drawFocusChange != nil { - control.core.drawFocusChange() - } -} diff --git a/elements/core/propagator.go b/elements/core/propagator.go deleted file mode 100644 index 656fce1..0000000 --- a/elements/core/propagator.go +++ /dev/null @@ -1,355 +0,0 @@ -package core - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" - -// Container represents an object that can provide access to a list of child -// elements. -type Container interface { - Child (index int) tomo.Element - CountChildren () int -} - -// Propagator is a struct that can be embedded into elements that contain one or -// more children in order to propagate events to them without having to write -// all of the event handlers. It also implements standard behavior for focus -// propagation and keyboard navigation. -type Propagator struct { - core CoreControl - container Container - drags [10]tomo.MouseTarget - focused bool -} - -// NewPropagator creates a new event propagator that uses the specified -// container to access a list of child elements that will have events propagated -// to them. If container is nil, the function will return nil. -func NewPropagator (container Container, core CoreControl) (propagator *Propagator) { - if container == nil { return nil } - propagator = &Propagator { - core: core, - container: container, - } - return -} - -// ----------- Interface fulfillment methods ----------- // - -// Focused returns whether or not this element or any of its children -// are currently focused. -func (propagator *Propagator) Focused () (focused bool) { - return propagator.focused -} - -// Focus focuses this element, if its parent element grants the -// request. -func (propagator *Propagator) Focus () { - if propagator.focused == true { return } - parent := propagator.core.Parent() - if parent, ok := parent.(tomo.FocusableParent); ok && parent != nil { - propagator.focused = parent.RequestFocus ( - propagator.core.Outer().(tomo.Focusable)) - } -} - -// HandleFocus causes this element to mark itself as focused. If the -// element does not have children or there are no more focusable children in -// the given direction, it should return false and do nothing. Otherwise, it -// marks itself as focused along with any applicable children and returns -// true. -func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) { - direction = direction.Canon() - - firstFocused := propagator.firstFocused() - if firstFocused < 0 { - // no element is currently focused, so we need to focus either - // the first or last focusable element depending on the - // direction. - switch direction { - case input.KeynavDirectionForward: - // if we recieve a forward direction, focus the first - // focusable element. - return propagator.focusFirstFocusableElement(direction) - - case input.KeynavDirectionBackward: - // if we recieve a backward direction, focus the last - // focusable element. - return propagator.focusLastFocusableElement(direction) - - case input.KeynavDirectionNeutral: - // if we recieve a neutral direction, just focus this - // element and nothing else. - propagator.focused = true - return true - } - } else { - // an element is currently focused, so we need to move the - // focus in the specified direction - firstFocusedChild := - propagator.container.Child(firstFocused). - (tomo.Focusable) - - // before we move the focus, the currently focused child - // may also be able to move its focus. if the child is able - // to do that, we will let it and not move ours. - if firstFocusedChild.HandleFocus(direction) { - return true - } - - // find the previous/next focusable element relative to the - // currently focused element, if it exists. - for index := firstFocused + int(direction); - index < propagator.container.CountChildren() && index >= 0; - index += int(direction) { - - child, focusable := - propagator.container.Child(index). - (tomo.Focusable) - if focusable && child.HandleFocus(direction) { - // we have found one, so we now actually move - // the focus. - firstFocusedChild.HandleUnfocus() - propagator.focused = true - return true - } - } - } - - return false -} - -// RequestFocus notifies the parent that a child element is requesting -// keyboard focus. If the parent grants the request, the method will -// return true and the child element should behave as if a HandleFocus -// call was made. -func (propagator *Propagator) RequestFocus ( - child tomo.Focusable, -) ( - granted bool, -) { - if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok { - if parent.RequestFocus(propagator.core.Outer().(tomo.Focusable)) { - propagator.HandleUnfocus() - propagator.focused = true - granted = true - } - } - return -} - -// RequestFocusMotion notifies the parent that a child element wants the -// focus to be moved to the next focusable element. -func (propagator *Propagator) RequestFocusNext (child tomo.Focusable) { - if !propagator.focused { return } - if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok { - parent.RequestFocusNext(propagator.core.Outer().(tomo.Focusable)) - } -} - -// RequestFocusMotion notifies the parent that a child element wants the -// focus to be moved to the previous focusable element. -func (propagator *Propagator) RequestFocusPrevious (child tomo.Focusable) { - if !propagator.focused { return } - if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok { - parent.RequestFocusPrevious(propagator.core.Outer().(tomo.Focusable)) - } -} - -// HandleDeselection causes this element to mark itself and all of its children -// as unfocused. -func (propagator *Propagator) HandleUnfocus () { - propagator.forFocusable (func (child tomo.Focusable) bool { - child.HandleUnfocus() - return true - }) - propagator.focused = false -} - -// HandleKeyDown propogates the keyboard event to the currently selected child. -func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - propagator.forFocused (func (child tomo.Focusable) bool { - typedChild, handlesKeyboard := child.(tomo.KeyboardTarget) - if handlesKeyboard { - typedChild.HandleKeyDown(key, modifiers) - } - return true - }) -} - -// HandleKeyUp propogates the keyboard event to the currently selected child. -func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) { - propagator.forFocused (func (child tomo.Focusable) bool { - typedChild, handlesKeyboard := child.(tomo.KeyboardTarget) - if handlesKeyboard { - typedChild.HandleKeyUp(key, modifiers) - } - return true - }) -} - -// HandleMouseDown propagates the mouse event to the element under the mouse -// pointer. -func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) { - child, handlesMouse := - propagator.childAt(image.Pt(x, y)). - (tomo.MouseTarget) - if handlesMouse { - propagator.drags[button] = child - child.HandleMouseDown(x, y, button) - } -} - -// HandleMouseUp propagates the mouse event to the element that the released -// mouse button was originally pressed on. -func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) { - child := propagator.drags[button] - if child != nil { - propagator.drags[button] = nil - child.HandleMouseUp(x, y, button) - } -} - -// HandleMotion propagates the mouse event to the element that was last -// pressed down by the mouse if the mouse is currently being held down, else it -// propagates the event to whichever element is underneath the mouse pointer. -func (propagator *Propagator) HandleMotion (x, y int) { - handled := false - for _, child := range propagator.drags { - if child, ok := child.(tomo.MotionTarget); ok { - child.HandleMotion(x, y) - handled = true - } - } - - if !handled { - child := propagator.childAt(image.Pt(x, y)) - if child, ok := child.(tomo.MotionTarget); ok { - child.HandleMotion(x, y) - } - } -} - -// HandleScroll propagates the mouse event to the element under the mouse -// pointer. -func (propagator *Propagator) HandleScroll (x, y int, deltaX, deltaY float64) { - child := propagator.childAt(image.Pt(x, y)) - if child, ok := child.(tomo.ScrollTarget); ok { - child.HandleScroll(x, y, deltaX, deltaY) - } -} - -// SetTheme sets the theme of all children to the specified theme. -func (propagator *Propagator) SetTheme (theme tomo.Theme) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, themeable := child.(tomo.Themeable) - if themeable { - typedChild.SetTheme(theme) - } - return true - }) -} - -// SetConfig sets the theme of all children to the specified config. -func (propagator *Propagator) SetConfig (config tomo.Config) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, configurable := child.(tomo.Configurable) - if configurable { - typedChild.SetConfig(config) - } - return true - }) -} - -// ----------- Focusing utilities ----------- // - -func (propagator *Propagator) focusFirstFocusableElement ( - direction input.KeynavDirection, -) ( - ok bool, -) { - propagator.forFocusable (func (child tomo.Focusable) bool { - if child.HandleFocus(direction) { - propagator.focused = true - ok = true - return false - } - return true - }) - return -} - -func (propagator *Propagator) focusLastFocusableElement ( - direction input.KeynavDirection, -) ( - ok bool, -) { - propagator.forChildrenReverse (func (child tomo.Element) bool { - typedChild, focusable := child.(tomo.Focusable) - if focusable && typedChild.HandleFocus(direction) { - propagator.focused = true - ok = true - return false - } - return true - }) - return -} - -// ----------- Iterator utilities ----------- // - -func (propagator *Propagator) forChildren (callback func (child tomo.Element) bool) { - for index := 0; index < propagator.container.CountChildren(); index ++ { - child := propagator.container.Child(index) - if child == nil { continue } - if !callback(child) { break } - } -} - -func (propagator *Propagator) forChildrenReverse (callback func (child tomo.Element) bool) { - for index := propagator.container.CountChildren() - 1; index > 0; index -- { - child := propagator.container.Child(index) - if child == nil { continue } - if !callback(child) { break } - } -} - -func (propagator *Propagator) childAt (position image.Point) (child tomo.Element) { - propagator.forChildren (func (current tomo.Element) bool { - if position.In(current.Bounds()) { - child = current - } - return true - }) - return -} - -func (propagator *Propagator) forFocused (callback func (child tomo.Focusable) bool) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, focusable := child.(tomo.Focusable) - if focusable && typedChild.Focused() { - if !callback(typedChild) { return false } - } - return true - }) -} - -func (propagator *Propagator) forFocusable (callback func (child tomo.Focusable) bool) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, focusable := child.(tomo.Focusable) - if focusable { - if !callback(typedChild) { return false } - } - return true - }) -} - -func (propagator *Propagator) firstFocused () int { - for index := 0; index < propagator.container.CountChildren(); index ++ { - child, focusable := propagator.container.Child(index).(tomo.Focusable) - if focusable && child.Focused() { - return index - } - } - return -1 -} From 407b9576878f05cc5d2bef305228030187249cc6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 12 Apr 2023 23:46:29 -0400 Subject: [PATCH 04/41] Testing elements conform to new API --- elements/testing/artist.go | 83 ++++++++++++++++++----------------- elements/testing/mouse.go | 90 +++++++++++++++++--------------------- 2 files changed, 83 insertions(+), 90 deletions(-) diff --git a/elements/testing/artist.go b/elements/testing/artist.go index 15a26e9..cd2d848 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -4,11 +4,11 @@ import "fmt" import "time" import "image" import "image/color" +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/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" @@ -16,49 +16,50 @@ import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" // Artist is an element that displays shapes and patterns drawn by the artist // package in order to test it. type Artist struct { - *core.Core - core core.CoreControl + entity tomo.Entity } // NewArtist creates a new artist test element. -func NewArtist () (element *Artist) { - element = &Artist { } - element.Core, element.core = core.NewCore(element, element.draw) - element.core.SetMinimumSize(240, 240) - return +func NewArtist () *Artist { + return &Artist { } } -func (element *Artist) draw () { - bounds := element.Bounds() - patterns.Uhex(0x000000FF).Draw(element.core, bounds) +func (element *Artist) Bind (entity tomo.Entity) { + element.entity = entity + entity.SetMinimumSize(240, 240) +} + +func (element *Artist) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + patterns.Uhex(0x000000FF).Draw(destination, bounds) drawStart := time.Now() // 0, 0 - 3, 0 for x := 0; x < 4; x ++ { - element.colorLines(x + 1, element.cellAt(x, 0).Bounds()) + element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds()) } // 4, 0 - c40 := element.cellAt(4, 0) + c40 := element.cellAt(destination, 4, 0) shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1) shapes.ColorLine ( c40, artist.Hex(0xFF0000FF), 1, c40.Bounds().Min, c40.Bounds().Max) // 0, 1 - c01 := element.cellAt(0, 1) + c01 := element.cellAt(destination, 0, 1) shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1) - shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds()) + shapes.FillColorEllipse(destination, artist.Hex(0x00FF00FF), c01.Bounds()) // 1, 1 - 3, 1 for x := 1; x < 4; x ++ { - c := element.cellAt(x, 1) + c := element.cellAt(destination, x, 1) shapes.StrokeColorRectangle ( - element.core, artist.Hex(0x888888FF), + destination, artist.Hex(0x888888FF), c.Bounds(), 1) shapes.StrokeColorEllipse ( - element.core, + destination, []color.RGBA { artist.Hex(0xFF0000FF), artist.Hex(0x00FF00FF), @@ -68,7 +69,7 @@ func (element *Artist) draw () { } // 4, 1 - c41 := element.cellAt(4, 1) + c41 := element.cellAt(destination, 4, 1) shatterPos := c41.Bounds().Min rocks := []image.Rectangle { image.Rect(3, 12, 13, 23).Add(shatterPos), @@ -85,46 +86,46 @@ func (element *Artist) draw () { patterns.Uhex(0xFF00FFFF), patterns.Uhex(0xFFFF00FF), patterns.Uhex(0x00FFFFFF), - } [index % 5].Draw(element.core, tile) + } [index % 5].Draw(destination, tile) } // 0, 2 - c02 := element.cellAt(0, 2) + c02 := element.cellAt(destination, 0, 2) shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1) shapes.FillEllipse(c02, c41, c02.Bounds()) // 1, 2 - c12 := element.cellAt(1, 2) + c12 := element.cellAt(destination, 1, 2) shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1) shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5) // 2, 2 - c22 := element.cellAt(2, 2) + c22 := element.cellAt(destination, 2, 2) shapes.FillRectangle(c22, c41, c22.Bounds()) // 3, 2 - c32 := element.cellAt(3, 2) + c32 := element.cellAt(destination, 3, 2) shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5) // 4, 2 - c42 := element.cellAt(4, 2) + c42 := element.cellAt(destination, 4, 2) // 0, 3 - c03 := element.cellAt(0, 3) + c03 := element.cellAt(destination, 0, 3) patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, }.Draw(c03, c03.Bounds()) // 1, 3 - c13 := element.cellAt(1, 3) + c13 := element.cellAt(destination, 1, 3) patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, }.Draw(c13, c13.Bounds().Inset(10)) // 2, 3 - c23 := element.cellAt(2, 3) + c23 := element.cellAt(destination, 2, 3) patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, @@ -143,51 +144,51 @@ func (element *Artist) draw () { drawTime.Milliseconds(), drawTime.Microseconds()))) textDrawer.Draw ( - element.core, artist.Hex(0xFFFFFFFF), + destination, artist.Hex(0xFFFFFFFF), image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24)) } -func (element *Artist) colorLines (weight int, bounds image.Rectangle) { +func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds image.Rectangle) { bounds = bounds.Inset(4) c := artist.Hex(0xFFFFFFFF) - shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max) + shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Max.X, bounds.Min.Y), image.Pt(bounds.Min.X, bounds.Max.Y)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Max.X, bounds.Min.Y + 16), image.Pt(bounds.Min.X, bounds.Max.Y - 16)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X, bounds.Min.Y + 16), image.Pt(bounds.Max.X, bounds.Max.Y - 16)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X + 20, bounds.Min.Y), image.Pt(bounds.Max.X - 20, bounds.Max.Y)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Max.X - 20, bounds.Min.Y), image.Pt(bounds.Min.X + 20, bounds.Max.Y)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2), image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y), image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y)) } -func (element *Artist) cellAt (x, y int) (canvas.Canvas) { - bounds := element.Bounds() +func (element *Artist) cellAt (destination canvas.Canvas, x, y int) (canvas.Canvas) { + bounds := element.entity.Bounds() cellBounds := image.Rectangle { } cellBounds.Min = bounds.Min cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5 cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4 - return canvas.Cut (element.core, cellBounds.Add (image.Pt ( + return canvas.Cut (destination, cellBounds.Add (image.Pt ( x * cellBounds.Dx(), y * cellBounds.Dy()))) } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index f2ee908..56e09d7 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -4,17 +4,16 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/canvas" 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" // Mouse is an element capable of testing mouse input. When the mouse is clicked // and dragged on it, it draws a trail. type Mouse struct { - *core.Core - core core.CoreControl - drawing bool + entity tomo.Entity + pressed bool lastMousePos image.Point config config.Wrapped @@ -24,69 +23,62 @@ type Mouse struct { // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { element = &Mouse { } - element.theme.Case = tomo.C("tomo", "piano") - element.Core, element.core = core.NewCore(element, element.draw) - element.core.SetMinimumSize(32, 32) + element.theme.Case = tomo.C("tomo", "mouse") return } +func (element *Mouse) Bind (entity tomo.Entity) { + element.entity = entity + entity.SetMinimumSize(32, 32) +} + +func (element *Mouse) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + accent := element.theme.Color ( + tomo.ColorAccent, + tomo.State { }) + shapes.FillColorRectangle(destination, accent, bounds) + shapes.StrokeColorRectangle ( + destination, + artist.Hex(0x000000FF), + bounds, 1) + shapes.ColorLine ( + destination, artist.Hex(0xFFFFFFFF), 1, + bounds.Min.Add(image.Pt(1, 1)), + bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) + shapes.ColorLine ( + destination, artist.Hex(0xFFFFFFFF), 1, + bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)), + bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) + if element.pressed { + shapes.ColorLine ( + destination, artist.Hex(0x000000FF), 1, + bounds.Min, element.lastMousePos) + } +} + // SetTheme sets the element's theme. func (element *Mouse) SetTheme (new tomo.Theme) { element.theme.Theme = new - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Mouse) SetConfig (new tomo.Config) { element.config.Config = new - element.redo() -} - -func (element *Mouse) redo () { - if !element.core.HasImage() { return } - element.draw() - element.core.DamageAll() -} - -func (element *Mouse) draw () { - bounds := element.Bounds() - accent := element.theme.Color ( - tomo.ColorAccent, - tomo.State { }) - shapes.FillColorRectangle(element.core, accent, bounds) - shapes.StrokeColorRectangle ( - element.core, - artist.Hex(0x000000FF), - bounds, 1) - shapes.ColorLine ( - element.core, artist.Hex(0xFFFFFFFF), 1, - bounds.Min.Add(image.Pt(1, 1)), - bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) - shapes.ColorLine ( - element.core, artist.Hex(0xFFFFFFFF), 1, - bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)), - bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) + element.entity.Invalidate() } func (element *Mouse) HandleMouseDown (x, y int, button input.Button) { - element.drawing = true - element.lastMousePos = image.Pt(x, y) + element.pressed = true } func (element *Mouse) HandleMouseUp (x, y int, button input.Button) { - element.drawing = false - mousePos := image.Pt(x, y) - element.core.DamageRegion (shapes.ColorLine ( - element.core, artist.Hex(0x000000FF), 1, - element.lastMousePos, mousePos)) - element.lastMousePos = mousePos + element.pressed = false } func (element *Mouse) HandleMotion (x, y int) { - if !element.drawing { return } - mousePos := image.Pt(x, y) - element.core.DamageRegion (shapes.ColorLine ( - element.core, artist.Hex(0x000000FF), 1, - element.lastMousePos, mousePos)) - element.lastMousePos = mousePos + if !element.pressed { return } + element.lastMousePos = image.Pt(x, y) + element.entity.Invalidate() } From bb9c5df088b5c3ad6093efa8e4c162a4d958e1c3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 13 Apr 2023 02:22:54 -0400 Subject: [PATCH 05/41] X backend entity --- backends/x/entity.go | 154 +++++++++++++++++++++++++++++++++++++++++++ backends/x/window.go | 4 +- element.go | 24 +++++++ entity.go | 8 ++- 4 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 backends/x/entity.go diff --git a/backends/x/entity.go b/backends/x/entity.go new file mode 100644 index 0000000..6252d55 --- /dev/null +++ b/backends/x/entity.go @@ -0,0 +1,154 @@ +package x + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +type entity struct { + window *window + parent *entity + children []*entity + element tomo.Element + + drawDirty bool + layoutDirty bool + + bounds image.Rectangle + minWidth int + minHeight int +} + +func bind (element tomo.Element) *entity { + entity := &entity { drawDirty: true } + if _, ok := element.(tomo.Container); ok { + entity.layoutDirty = true + } + + element.Bind(entity) + return entity +} + +func (entity *entity) unbind () { + entity.element.Bind(nil) + for _, childEntity := range entity.children { + childEntity.unbind() + } +} + +// ----------- Entity ----------- // + +func (entity *entity) Invalidate () { + entity.drawDirty = true +} + +func (entity *entity) Bounds () image.Rectangle { + return entity.bounds +} + +func (entity *entity) Window () tomo.Window { + return entity.window +} + +func (entity *entity) SetMinimumSize (width, height int) { + entity.minWidth = width + entity.minHeight = height + if entity.parent == nil { return } + entity.parent.element.(tomo.Container).HandleChildMinimumSizeChange() +} + +func (entity *entity) DrawBackground (destination canvas.Canvas, bounds image.Rectangle) { + if entity.parent == nil { return } + entity.parent.element.(tomo.Container).DrawBackground(destination, bounds) +} + +// ----------- ContainerEntity ----------- // + +func (entity *entity) InvalidateLayout () { + entity.layoutDirty = true +} + +func (entity *entity) Adopt (child tomo.Element) { + entity.children = append(entity.children, bind(child)) +} + +func (entity *entity) Insert (index int, child tomo.Element) { + entity.children = append ( + entity.children[:index + 1], + entity.children[index:]...) + entity.children[index] = bind(child) +} + +func (entity *entity) Disown (index int) { + entity.children[index].unbind() + entity.children = append ( + entity.children[:index], + entity.children[index + 1:]...) +} + +func (entity *entity) IndexOf (child tomo.Element) int { + for index, childEntity := range entity.children { + if childEntity.element == child { + return index + } + } + + return -1 +} + +func (entity *entity) Child (index int) tomo.Element { + return entity.children[index].element +} + +func (entity *entity) CountChildren () int { + return len(entity.children) +} + +func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { + entity.children[index].bounds = bounds +} + +func (entity *entity) ChildMinimumSize (index int) (width, height int) { + childEntity := entity.children[index] + return childEntity.minWidth, childEntity.minHeight +} + +// ----------- FocusableEntity ----------- // + +func (entity *entity) Focused () bool { + return entity.window.focused == entity +} + +func (entity *entity) Focus () { + previous := entity.window.focused + entity.window.focused = entity + if previous != nil { + previous.element.(tomo.Focusable).HandleFocusChange() + } + entity.element.(tomo.Focusable).HandleFocusChange() +} + +func (entity *entity) FocusNext () { + // TODO +} + +func (entity *entity) FocusPrevious () { + // TODO +} + +// ----------- FlexibleEntity ----------- // + +func (entity *entity) NotifyFlexibleHeightChange () { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok { + parent.HandleChildFlexibleHeightChange() + } +} + +// ----------- ScrollableEntity ----------- // + +func (entity *entity) NotifyScrollBoundsChange () { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok { + parent.HandleChildScrollBoundsChange() + } +} diff --git a/backends/x/window.go b/backends/x/window.go index 980d63d..0253994 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -24,9 +24,9 @@ type window struct { xWindow *xwindow.Window xCanvas *xgraphics.Image canvas canvas.BasicCanvas - child tomo.Element + child *entity + focused *entity onClose func () - skipChildDrawCallback bool title, application string diff --git a/element.go b/element.go index 5045462..66b05a9 100644 --- a/element.go +++ b/element.go @@ -25,6 +25,10 @@ type Container interface { // DrawBackground draws this element's background pattern at the // specified bounds to any canvas. DrawBackground (destination canvas.Canvas, bounds image.Rectangle) + + // HandleChildMinimumSizeChange is called when a child's minimum size is + // changed. + HandleChildMinimumSizeChange () } // Focusable represents an element that has keyboard navigation support. @@ -104,6 +108,16 @@ type Flexible interface { FlexibleHeightFor (width int) int } +// FlexibleContainer represents an element that is capable of containing +// flexible children. +type FlexibleContainer interface { + Container + + // HandleChildFlexibleHeightChange is called when the parameters + // affecting a child's flexible height are changed. + HandleChildFlexibleHeightChange () +} + // Scrollable represents an element that can be scrolled. It acts as a viewport // through which its contents can be observed. type Scrollable interface { @@ -124,6 +138,16 @@ type Scrollable interface { ScrollAxes () (horizontal, vertical bool) } +// ScrollableContainer represents an element that is capable of containing +// scrollable children. +type ScrollableContainer interface { + Container + + // HandleChildScrollBoundsChange is called when the content bounds, + // viewport bounds, or scroll axes of a child are changed. + HandleChildScrollBoundsChange() +} + // Collapsible represents an element who's minimum width and height can be // manually resized. Scrollable elements should implement this if possible. type Collapsible interface { diff --git a/entity.go b/entity.go index 27bddab..3a1b611 100644 --- a/entity.go +++ b/entity.go @@ -1,6 +1,7 @@ package tomo import "image" +import "git.tebibyte.media/sashakoshka/tomo/canvas" // Entity is a handle given to elements by the backend. Different types of // entities may be assigned to elements that support different capabilities. @@ -25,13 +26,18 @@ type Entity interface { // 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 (bounds image.Rectangle) + DrawBackground (destination canvas.Canvas, bounds image.Rectangle) } // ContainerEntity is given to elements that support the Container interface. type ContainerEntity interface { Entity + // InvalidateLayout marks the element's layout as invalid. At the end of + // every event, the backend will ask all invalid containers to + // recalculate their layouts. + InvalidateLayout () + // Adopt adds an element as a child. Adopt (child Element) From e9317172417117745013391c2703098431c94925 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 14 Apr 2023 00:25:05 -0400 Subject: [PATCH 06/41] Basic support in X backend for new API --- backends/x/entity.go | 44 ++++++--- backends/x/event.go | 47 +++++---- backends/x/system.go | 109 ++++++++++++++++++++ backends/x/window.go | 197 +++++++++++-------------------------- backends/x/x.go | 3 + elements/testing/artist.go | 2 +- window.go | 3 - 7 files changed, 229 insertions(+), 176 deletions(-) create mode 100644 backends/x/system.go diff --git a/backends/x/entity.go b/backends/x/entity.go index 6252d55..78053f3 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -10,18 +10,25 @@ type entity struct { children []*entity element tomo.Element - drawDirty bool - layoutDirty bool - - bounds image.Rectangle - minWidth int - minHeight int + bounds image.Rectangle + clippedBounds image.Rectangle + minWidth int + minHeight int + + layoutInvalid bool + isContainer bool } -func bind (element tomo.Element) *entity { - entity := &entity { drawDirty: true } +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 { - entity.layoutDirty = true + entity.isContainer = true + entity.InvalidateLayout() } element.Bind(entity) @@ -38,7 +45,8 @@ func (entity *entity) unbind () { // ----------- Entity ----------- // func (entity *entity) Invalidate () { - entity.drawDirty = true + if entity.window.system.invalidateIgnore { return } + entity.window.drawingInvalid.Add(entity) } func (entity *entity) Bounds () image.Rectangle { @@ -64,18 +72,20 @@ func (entity *entity) DrawBackground (destination canvas.Canvas, bounds image.Re // ----------- ContainerEntity ----------- // func (entity *entity) InvalidateLayout () { - entity.layoutDirty = true + 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(child)) + entity.children = append(entity.children, bind(entity, entity.window, child)) } func (entity *entity) Insert (index int, child tomo.Element) { entity.children = append ( entity.children[:index + 1], entity.children[index:]...) - entity.children[index] = bind(child) + entity.children[index] = bind(entity, entity.window, child) } func (entity *entity) Disown (index int) { @@ -104,7 +114,13 @@ func (entity *entity) CountChildren () int { } func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { - entity.children[index].bounds = bounds + child := entity.children[index] + child.bounds = bounds + child.clippedBounds = entity.bounds.Intersect(bounds) + child.Invalidate() + if child.isContainer { + child.InvalidateLayout() + } } func (entity *entity) ChildMinimumSize (index int) (width, height int) { diff --git a/backends/x/event.go b/backends/x/event.go index 4675c2f..a796bb1 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -41,7 +41,6 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) { sum.x += scrollDistance } } - } func (window *window) handleExpose ( @@ -49,6 +48,7 @@ func (window *window) handleExpose ( event xevent.ExposeEvent, ) { _, region := window.compressExpose(*event.ExposeEvent) + window.system.afterEvent() window.pushRegion(region) } @@ -74,7 +74,6 @@ func (window *window) handleConfigureNotify ( window.updateBounds ( configureEvent.X, configureEvent.Y, configureEvent.Width, configureEvent.Height) - if sizeChanged { configureEvent = window.compressConfigureNotify(configureEvent) @@ -85,8 +84,11 @@ func (window *window) handleConfigureNotify ( window.resizeChildToFit() if !window.exposeEventFollows(configureEvent) { - window.redrawChildEntirely() + window.child.Invalidate() + window.child.InvalidateLayout() } + + window.system.afterEvent() } } @@ -136,21 +138,21 @@ func (window *window) handleKeyPress ( modifiers.NumberPad = numberPad if key == input.KeyTab && modifiers.Alt { - if child, ok := window.child.(tomo.Focusable); ok { - direction := input.KeynavDirectionForward - if modifiers.Shift { - direction = input.KeynavDirectionBackward - } - - if !child.HandleFocus(direction) { - child.HandleUnfocus() - } - } + // if child, ok := window.child.element.(tomo.Focusable); ok { + // direction := input.KeynavDirectionForward + // if modifiers.Shift { + // direction = input.KeynavDirectionBackward + // } +// + // // TODO + // } } else if key == input.KeyEscape && window.shy { window.Close() - } else if child, ok := window.child.(tomo.KeyboardTarget); ok { + } else if child, ok := window.child.element.(tomo.KeyboardTarget); ok { child.HandleKeyDown(key, modifiers) } + + window.system.afterEvent() } func (window *window) handleKeyRelease ( @@ -182,9 +184,11 @@ func (window *window) handleKeyRelease ( modifiers := window.modifiersFromState(keyEvent.State) modifiers.NumberPad = numberPad - if child, ok := window.child.(tomo.KeyboardTarget); ok { + if child, ok := window.child.element.(tomo.KeyboardTarget); ok { child.HandleKeyUp(key, modifiers) } + + window.system.afterEvent() } func (window *window) handleButtonPress ( @@ -205,7 +209,7 @@ func (window *window) handleButtonPress ( if !insideWindow && window.shy && !scrolling { window.Close() } else if scrolling { - if child, ok := window.child.(tomo.ScrollTarget); ok { + if child, ok := window.child.element.(tomo.ScrollTarget); ok { sum := scrollSum { } sum.add(buttonEvent.Detail, window, buttonEvent.State) window.compressScrollSum(buttonEvent, &sum) @@ -215,7 +219,7 @@ func (window *window) handleButtonPress ( float64(sum.x), float64(sum.y)) } } else { - if child, ok := window.child.(tomo.MouseTarget); ok { + if child, ok := window.child.element.(tomo.MouseTarget); ok { child.HandleMouseDown ( int(buttonEvent.EventX), int(buttonEvent.EventY), @@ -223,6 +227,7 @@ func (window *window) handleButtonPress ( } } + window.system.afterEvent() } func (window *window) handleButtonRelease ( @@ -231,7 +236,7 @@ func (window *window) handleButtonRelease ( ) { if window.child == nil { return } - if child, ok := window.child.(tomo.MouseTarget); ok { + if child, ok := window.child.element.(tomo.MouseTarget); ok { buttonEvent := *event.ButtonReleaseEvent if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } child.HandleMouseUp ( @@ -239,6 +244,8 @@ func (window *window) handleButtonRelease ( int(buttonEvent.EventY), input.Button(buttonEvent.Detail)) } + + window.system.afterEvent() } func (window *window) handleMotionNotify ( @@ -247,12 +254,14 @@ func (window *window) handleMotionNotify ( ) { if window.child == nil { return } - if child, ok := window.child.(tomo.MotionTarget); ok { + if child, ok := window.child.element.(tomo.MotionTarget); ok { motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) child.HandleMotion ( int(motionEvent.EventX), int(motionEvent.EventY)) } + + window.system.afterEvent() } func (window *window) handleSelectionNotify ( diff --git a/backends/x/system.go b/backends/x/system.go new file mode 100644 index 0000000..6b53011 --- /dev/null +++ b/backends/x/system.go @@ -0,0 +1,109 @@ +package x + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +type entitySet map[*entity] struct { } + +func (set entitySet) Empty () bool { + return len(set) == 0 +} + +func (set entitySet) Has (entity *entity) bool { + _, ok := set[entity] + return ok +} + +func (set entitySet) Add (entity *entity) { + set[entity] = struct { } { } +} + +type system struct { + child *entity + focused *entity + canvas canvas.BasicCanvas + + theme tomo.Theme + config tomo.Config + + invalidateIgnore bool + drawingInvalid entitySet + anyLayoutInvalid bool + + pushFunc func (image.Rectangle) +} + +func (system *system) initialize () { + system.drawingInvalid = make(entitySet) +} + +func (system *system) SetTheme (theme tomo.Theme) { + system.theme = theme + if system.child == nil { return } + if child, ok := system.child.element.(tomo.Themeable); ok { + child.SetTheme(theme) + } +} + +func (system *system) SetConfig (config tomo.Config) { + system.config = config + if system.child == nil { return } + if child, ok := system.child.element.(tomo.Configurable); ok { + child.SetConfig(config) + } +} + +func (system *system) resizeChildToFit () { + system.child.bounds = system.canvas.Bounds() + system.child.clippedBounds = system.child.bounds + system.child.Invalidate() + if system.child.isContainer { + system.child.InvalidateLayout() + } +} + +func (system *system) afterEvent () { + if system.anyLayoutInvalid { + system.layout(system.child, false) + system.anyLayoutInvalid = false + } + system.draw() +} + +func (system *system) layout (entity *entity, force bool) { + if entity == nil { return } + if entity.layoutInvalid == true || force { + entity.element.(tomo.Container).Layout() + entity.layoutInvalid = false + force = true + } + + for _, child := range entity.children { + system.layout(child, force) + } +} + +func (system *system) draw () { + finalBounds := image.Rectangle { } + + // ignore invalidations that result from drawing elements, because if an + // element decides to do that it really needs to rethink its life + // choices. + system.invalidateIgnore = true + defer func () { system.invalidateIgnore = false } () + + for entity := range system.drawingInvalid { + entity.element.Draw (canvas.Cut ( + system.canvas, + entity.clippedBounds)) + finalBounds = finalBounds.Union(entity.clippedBounds) + } + system.drawingInvalid = make(entitySet) + + // TODO: don't just union all the bounds together, we can definetly + // consolidateupdated regions more efficiently than this. + if !finalBounds.Empty() { + system.pushFunc(finalBounds) + } +} diff --git a/backends/x/window.go b/backends/x/window.go index 0253994..eb661ac 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -13,20 +13,16 @@ import "github.com/jezek/xgbutil/mousebind" import "github.com/jezek/xgbutil/xgraphics" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" -import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/canvas" -// import "runtime/debug" type mainWindow struct { *window } type menuWindow struct { *window } type window struct { + system + backend *Backend xWindow *xwindow.Window xCanvas *xgraphics.Image - canvas canvas.BasicCanvas - child *entity - focused *entity - onClose func () title, application string @@ -34,15 +30,14 @@ type window struct { hasModal bool shy bool - theme tomo.Theme - config tomo.Config - selectionRequest *selectionRequest selectionClaim *selectionClaim metrics struct { bounds image.Rectangle } + + onClose func () } func (backend *Backend) NewWindow ( @@ -53,6 +48,10 @@ func (backend *Backend) NewWindow ( ) { if backend == nil { panic("nil backend") } window, err := backend.newWindow(bounds, false) + + window.system.initialize() + window.system.pushFunc = window.paste + output = mainWindow { window } return output, err } @@ -136,69 +135,24 @@ func (backend *Backend) newWindow ( return } -func (window *window) NotifyMinimumSizeChange (child tomo.Element) { - window.childMinimumSizeChangeCallback(child.MinimumSize()) -} - func (window *window) Window () tomo.Window { return window } -func (window *window) RequestFocus ( - child tomo.Focusable, -) ( - granted bool, -) { - return true -} - -func (window *window) RequestFocusNext (child tomo.Focusable) { - if child, ok := window.child.(tomo.Focusable); ok { - if !child.HandleFocus(input.KeynavDirectionForward) { - child.HandleUnfocus() - } - } -} - -func (window *window) RequestFocusPrevious (child tomo.Focusable) { - if child, ok := window.child.(tomo.Focusable); ok { - if !child.HandleFocus(input.KeynavDirectionBackward) { - child.HandleUnfocus() - } - } -} - func (window *window) Adopt (child tomo.Element) { // disown previous child if window.child != nil { - window.child.SetParent(nil) - window.child.DrawTo(nil, image.Rectangle { }, nil) + window.child.unbind() + window.child = nil } + // adopt new child if child != nil { - // adopt new child - window.child = child - child.SetParent(window) - if newChild, ok := child.(tomo.Themeable); ok { - newChild.SetTheme(window.theme) - } - if newChild, ok := child.(tomo.Configurable); ok { - newChild.SetConfig(window.config) - } - if child != nil { - if !window.childMinimumSizeChangeCallback(child.MinimumSize()) { - window.resizeChildToFit() - window.redrawChildEntirely() - } - } + window.child = bind(nil, window, child) + window.resizeChildToFit() } } -func (window *window) Child () (child tomo.Element) { - child = window.child - return -} - func (window *window) SetTitle (title string) { window.title = title ewmh.WmNameSet ( @@ -317,43 +271,6 @@ func (window menuWindow) Pin () { // TODO iungrab keyboard and mouse } -func (window *window) grabInput () { - keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id) - mousebind.GrabPointer ( - window.backend.connection, - window.xWindow.Id, - window.backend.connection.RootWin(), 0) -} - -func (window *window) ungrabInput () { - keybind.UngrabKeyboard(window.backend.connection) - mousebind.UngrabPointer(window.backend.connection) -} - -func (window *window) inheritProperties (parent *window) { - window.SetApplicationName(parent.application) -} - -func (window *window) setType (ty string) error { - return ewmh.WmWindowTypeSet ( - window.backend.connection, - window.xWindow.Id, - []string { "_NET_WM_WINDOW_TYPE_" + ty }) -} - -func (window *window) setClientLeader (leader *window) error { - hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id) - if hints == nil { - hints = &icccm.Hints { } - } - hints.Flags |= icccm.HintWindowGroup - hints.WindowGroup = leader.xWindow.Id - return icccm.WmHintsSet ( - window.backend.connection, - window.xWindow.Id, - hints) -} - func (window *window) Show () { if window.child == nil { window.xCanvas.For (func (x, y int) xgraphics.BGRA { @@ -362,7 +279,7 @@ func (window *window) Show () { window.pushRegion(window.xCanvas.Bounds()) } - + window.xWindow.Map() if window.shy { window.grabInput() } } @@ -417,18 +334,41 @@ func (window *window) OnClose (callback func ()) { window.onClose = callback } -func (window *window) SetTheme (theme tomo.Theme) { - window.theme = theme - if child, ok := window.child.(tomo.Themeable); ok { - child.SetTheme(theme) - } +func (window *window) grabInput () { + keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id) + mousebind.GrabPointer ( + window.backend.connection, + window.xWindow.Id, + window.backend.connection.RootWin(), 0) } -func (window *window) SetConfig (config tomo.Config) { - window.config = config - if child, ok := window.child.(tomo.Configurable); ok { - child.SetConfig(config) +func (window *window) ungrabInput () { + keybind.UngrabKeyboard(window.backend.connection) + mousebind.UngrabPointer(window.backend.connection) +} + +func (window *window) inheritProperties (parent *window) { + window.SetApplicationName(parent.application) +} + +func (window *window) setType (ty string) error { + return ewmh.WmWindowTypeSet ( + window.backend.connection, + window.xWindow.Id, + []string { "_NET_WM_WINDOW_TYPE_" + ty }) +} + +func (window *window) setClientLeader (leader *window) error { + hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id) + if hints == nil { + hints = &icccm.Hints { } } + hints.Flags |= icccm.HintWindowGroup + hints.WindowGroup = leader.xWindow.Id + return icccm.WmHintsSet ( + window.backend.connection, + window.xWindow.Id, + hints) } func (window *window) reallocateCanvas () { @@ -464,26 +404,6 @@ func (window *window) reallocateCanvas () { } -func (window *window) redrawChildEntirely () { - window.paste(window.canvas.Bounds()) - window.pushRegion(window.canvas.Bounds()) -} - -func (window *window) resizeChildToFit () { - window.skipChildDrawCallback = true - window.child.DrawTo ( - window.canvas, - window.canvas.Bounds(), - window.childDrawCallback) - window.skipChildDrawCallback = false -} - -func (window *window) childDrawCallback (region image.Rectangle) { - if window.skipChildDrawCallback { return } - window.paste(region) - window.pushRegion(region) -} - func (window *window) paste (region image.Rectangle) { canvas := canvas.Cut(window.canvas, region) data, stride := canvas.Buffer() @@ -492,7 +412,6 @@ func (window *window) paste (region image.Rectangle) { dstStride := window.xCanvas.Stride dstData := window.xCanvas.Pix - // debug.PrintStack() for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { srcYComponent := y * stride dstYComponent := y * dstStride @@ -507,6 +426,18 @@ func (window *window) paste (region image.Rectangle) { } } +func (window *window) pushRegion (region image.Rectangle) { + if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } + image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) + if ok { + image.XDraw() + image.XExpPaint ( + window.xWindow.Id, + image.Bounds().Min.X, + image.Bounds().Min.Y) + } +} + func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) { icccm.WmNormalHintsSet ( window.backend.connection, @@ -528,15 +459,3 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize return false } - -func (window *window) pushRegion (region image.Rectangle) { - if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } - image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) - if ok { - image.XDraw() - image.XExpPaint ( - window.xWindow.Id, - image.Bounds().Min.X, - image.Bounds().Min.Y) - } -} diff --git a/backends/x/x.go b/backends/x/x.go index 40c3021..ec69708 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -67,6 +67,9 @@ func (backend *Backend) Run () (err error) { <- pingAfter case callback := <- backend.doChannel: callback() + for _, window := range backend.windows { + window.system.afterEvent() + } case <- pingQuit: return } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index cd2d848..91faa0a 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -26,7 +26,7 @@ func NewArtist () *Artist { func (element *Artist) Bind (entity tomo.Entity) { element.entity = entity - entity.SetMinimumSize(240, 240) + if entity != nil { entity.SetMinimumSize(240, 240) } } func (element *Artist) Draw (destination canvas.Canvas) { diff --git a/window.go b/window.go index 1813a81..a88b2c8 100644 --- a/window.go +++ b/window.go @@ -15,9 +15,6 @@ type Window interface { // these at one time. Adopt (Element) - // Child returns the root element of the window. - Child () Element - // SetTitle sets the title that appears on the window's title bar. This // method might have no effect with some backends. SetTitle (string) From 4c6f1f80e7dd5eb4993b7e7936113fa14c2d33a9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 14 Apr 2023 19:08:14 -0400 Subject: [PATCH 07/41] Proper keyboard and mouse event propagation --- backends/x/entity.go | 20 +++++++- backends/x/event.go | 114 ++++++++++++++++++++----------------------- backends/x/system.go | 38 ++++++++++++--- 3 files changed, 103 insertions(+), 69 deletions(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index 78053f3..3a3f5ef 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -42,6 +42,22 @@ func (entity *entity) unbind () { } } +func (entity *entity) propagate (callback func (*entity) bool) { + for _, child := range entity.children { + if callback(child) { break } + child.propagate(callback) + } +} + +func (entity *entity) childAt (point image.Point) *entity { + for _, child := range entity.children { + if point.In(child.bounds) { + return child + } + } + return entity +} + // ----------- Entity ----------- // func (entity *entity) Invalidate () { @@ -144,11 +160,11 @@ func (entity *entity) Focus () { } func (entity *entity) FocusNext () { - // TODO + entity.window.system.focusNext() } func (entity *entity) FocusPrevious () { - // TODO + entity.window.system.focusPrevious() } // ----------- FlexibleEntity ----------- // diff --git a/backends/x/event.go b/backends/x/event.go index a796bb1..4088b86 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -20,25 +20,17 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) { (state & window.backend.modifierMasks.shiftLock) > 0 if shift { switch button { - case 4: - sum.x -= scrollDistance - case 5: - sum.x += scrollDistance - case 6: - sum.y -= scrollDistance - case 7: - sum.y += scrollDistance + case 4: sum.x -= scrollDistance + case 5: sum.x += scrollDistance + case 6: sum.y -= scrollDistance + case 7: sum.y += scrollDistance } } else { switch button { - case 4: - sum.y -= scrollDistance - case 5: - sum.y += scrollDistance - case 6: - sum.x -= scrollDistance - case 7: - sum.x += scrollDistance + case 4: sum.y -= scrollDistance + case 5: sum.y += scrollDistance + case 6: sum.x -= scrollDistance + case 7: sum.x += scrollDistance } } } @@ -129,8 +121,8 @@ func (window *window) handleKeyPress ( connection *xgbutil.XUtil, event xevent.KeyPressEvent, ) { - if window.child == nil { return } - if window.hasModal { return } + if window.system.focused == nil { return } + if window.hasModal { return } keyEvent := *event.KeyPressEvent key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State) @@ -138,18 +130,16 @@ func (window *window) handleKeyPress ( modifiers.NumberPad = numberPad if key == input.KeyTab && modifiers.Alt { - // if child, ok := window.child.element.(tomo.Focusable); ok { - // direction := input.KeynavDirectionForward - // if modifiers.Shift { - // direction = input.KeynavDirectionBackward - // } -// - // // TODO - // } + if modifiers.Shift { + window.system.focusPrevious() + } else { + window.system.focusNext() + } } else if key == input.KeyEscape && window.shy { window.Close() - } else if child, ok := window.child.element.(tomo.KeyboardTarget); ok { - child.HandleKeyDown(key, modifiers) + } else if window.focused != nil { + focused, ok := window.focused.element.(tomo.KeyboardTarget) + if ok { focused.HandleKeyDown(key, modifiers) } } window.system.afterEvent() @@ -159,8 +149,7 @@ func (window *window) handleKeyRelease ( connection *xgbutil.XUtil, event xevent.KeyReleaseEvent, ) { - if window.child == nil { return } - + if window.system.focused == nil { return } keyEvent := *event.KeyReleaseEvent // do not process this event if it was generated from a key repeat @@ -184,9 +173,8 @@ func (window *window) handleKeyRelease ( modifiers := window.modifiersFromState(keyEvent.State) modifiers.NumberPad = numberPad - if child, ok := window.child.element.(tomo.KeyboardTarget); ok { - child.HandleKeyUp(key, modifiers) - } + focused, ok := window.focused.element.(tomo.KeyboardTarget) + if ok { focused.HandleKeyUp(key, modifiers) } window.system.afterEvent() } @@ -195,34 +183,31 @@ func (window *window) handleButtonPress ( connection *xgbutil.XUtil, event xevent.ButtonPressEvent, ) { - if window.child == nil { return } - if window.hasModal { return } + if window.hasModal { return } - buttonEvent := *event.ButtonPressEvent - - insideWindow := image.Pt ( - int(buttonEvent.EventX), - int(buttonEvent.EventY)).In(window.canvas.Bounds()) + buttonEvent := *event.ButtonPressEvent + point := image.Pt(int(buttonEvent.EventX), int(buttonEvent.EventY)) + insideWindow := point.In(window.canvas.Bounds()) + scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 - scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 - + underneath := window.system.childAt(point) + if !insideWindow && window.shy && !scrolling { window.Close() } else if scrolling { - if child, ok := window.child.element.(tomo.ScrollTarget); ok { + if child, ok := underneath.element.(tomo.ScrollTarget); ok { sum := scrollSum { } sum.add(buttonEvent.Detail, window, buttonEvent.State) window.compressScrollSum(buttonEvent, &sum) child.HandleScroll ( - int(buttonEvent.EventX), - int(buttonEvent.EventY), + point.X, point.Y, float64(sum.x), float64(sum.y)) } } else { - if child, ok := window.child.element.(tomo.MouseTarget); ok { + if child, ok := underneath.element.(tomo.MouseTarget); ok { + window.system.drags[buttonEvent.Detail] = child child.HandleMouseDown ( - int(buttonEvent.EventX), - int(buttonEvent.EventY), + point.X, point.Y, input.Button(buttonEvent.Detail)) } } @@ -234,11 +219,10 @@ func (window *window) handleButtonRelease ( connection *xgbutil.XUtil, event xevent.ButtonReleaseEvent, ) { - if window.child == nil { return } - - if child, ok := window.child.element.(tomo.MouseTarget); ok { - buttonEvent := *event.ButtonReleaseEvent - if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } + buttonEvent := *event.ButtonReleaseEvent + if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } + child := window.system.drags[buttonEvent.Detail] + if child != nil { child.HandleMouseUp ( int(buttonEvent.EventX), int(buttonEvent.EventY), @@ -252,13 +236,23 @@ func (window *window) handleMotionNotify ( connection *xgbutil.XUtil, event xevent.MotionNotifyEvent, ) { - if window.child == nil { return } - - if child, ok := window.child.element.(tomo.MotionTarget); ok { - motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) - child.HandleMotion ( - int(motionEvent.EventX), - int(motionEvent.EventY)) + motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) + x := int(motionEvent.EventX) + y :=int(motionEvent.EventY) + + handled := false + for _, child := range window.system.drags { + if child, ok := child.(tomo.MotionTarget); ok { + child.HandleMotion(x, y) + handled = true + } + } + + if !handled { + child := window.system.childAt(image.Pt(x, y)) + if child, ok := child.element.(tomo.MotionTarget); ok { + child.HandleMotion(x, y) + } } window.system.afterEvent() diff --git a/backends/x/system.go b/backends/x/system.go index 6b53011..c9265c5 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -30,6 +30,8 @@ type system struct { invalidateIgnore bool drawingInvalid entitySet anyLayoutInvalid bool + + drags [10]tomo.MouseTarget pushFunc func (image.Rectangle) } @@ -40,18 +42,40 @@ func (system *system) initialize () { func (system *system) SetTheme (theme tomo.Theme) { system.theme = theme - if system.child == nil { return } - if child, ok := system.child.element.(tomo.Themeable); ok { - child.SetTheme(theme) - } + system.propagate (func (entity *entity) bool { + if child, ok := system.child.element.(tomo.Themeable); ok { + child.SetTheme(theme) + } + return true + }) } func (system *system) SetConfig (config tomo.Config) { system.config = config + system.propagate (func (entity *entity) bool { + if child, ok := system.child.element.(tomo.Configurable); ok { + child.SetConfig(config) + } + return true + }) +} + +func (system *system) focusNext () { + // TODO +} + +func (system *system) focusPrevious () { + // TODO +} + +func (system *system) propagate (callback func (*entity) bool) { if system.child == nil { return } - if child, ok := system.child.element.(tomo.Configurable); ok { - child.SetConfig(config) - } + system.child.propagate(callback) +} + +func (system *system) childAt (point image.Point) *entity { + if system.child == nil { return nil } + return system.child.childAt(point) } func (system *system) resizeChildToFit () { From 68128c94d8a9cb3dbe5c87e2a6390ed5432bd2c7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 14 Apr 2023 22:03:22 -0400 Subject: [PATCH 08/41] Migrated over some elements --- elements/button.go | 228 +++++++++++++------------- elements/checkbox.go | 188 ++++++++++----------- elements/icon.go | 78 +++++---- elements/image.go | 28 +++- elements/label.go | 120 +++++--------- elements/{ => notdone}/doc.go | 0 elements/notdone/label.go | 177 ++++++++++++++++++++ elements/{ => notdone}/lerpslider.go | 0 elements/{ => notdone}/list.go | 90 ++++------ elements/{ => notdone}/listentry.go | 0 elements/{ => notdone}/progressbar.go | 3 - elements/{ => notdone}/scrollbar.go | 4 - elements/{ => notdone}/slider.go | 6 - elements/{ => notdone}/spacer.go | 3 - elements/{ => notdone}/switch.go | 5 - elements/{ => notdone}/textbox.go | 6 - 16 files changed, 526 insertions(+), 410 deletions(-) rename elements/{ => notdone}/doc.go (100%) create mode 100644 elements/notdone/label.go rename elements/{ => notdone}/lerpslider.go (100%) rename elements/{ => notdone}/list.go (93%) rename elements/{ => notdone}/listentry.go (100%) rename elements/{ => notdone}/progressbar.go (96%) rename elements/{ => notdone}/scrollbar.go (98%) rename elements/{ => notdone}/slider.go (97%) rename elements/{ => notdone}/spacer.go (95%) rename elements/{ => notdone}/switch.go (97%) rename elements/{ => notdone}/textbox.go (98%) 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 From 6e4310b9ad2653ef9bbfd2c2fc5241d4d43533c1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 14 Apr 2023 23:58:14 -0400 Subject: [PATCH 09/41] Some X backend fixes --- backends/x/entity.go | 9 ++++++--- backends/x/event.go | 16 ++++++++-------- backends/x/window.go | 20 ++++++++++++-------- elements/button.go | 17 ++++++----------- elements/checkbox.go | 2 +- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index 3a3f5ef..53b62ce 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -76,8 +76,11 @@ func (entity *entity) Window () tomo.Window { func (entity *entity) SetMinimumSize (width, height int) { entity.minWidth = width entity.minHeight = height - if entity.parent == nil { return } - entity.parent.element.(tomo.Container).HandleChildMinimumSizeChange() + if entity.parent == nil { + entity.window.setMinimumSize(width, height) + } else { + entity.parent.element.(tomo.Container).HandleChildMinimumSizeChange() + } } func (entity *entity) DrawBackground (destination canvas.Canvas, bounds image.Rectangle) { @@ -169,7 +172,7 @@ func (entity *entity) FocusPrevious () { // ----------- FlexibleEntity ----------- // -func (entity *entity) NotifyFlexibleHeightChange () { +func (entity *entity) NotifyFlexibleHeightChange (child tomo.Flexible) { if entity.parent == nil { return } if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok { parent.HandleChildFlexibleHeightChange() diff --git a/backends/x/event.go b/backends/x/event.go index 4088b86..73ea3c1 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -121,8 +121,7 @@ func (window *window) handleKeyPress ( connection *xgbutil.XUtil, event xevent.KeyPressEvent, ) { - if window.system.focused == nil { return } - if window.hasModal { return } + if window.hasModal { return } keyEvent := *event.KeyPressEvent key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State) @@ -149,7 +148,6 @@ func (window *window) handleKeyRelease ( connection *xgbutil.XUtil, event xevent.KeyReleaseEvent, ) { - if window.system.focused == nil { return } keyEvent := *event.KeyReleaseEvent // do not process this event if it was generated from a key repeat @@ -172,11 +170,13 @@ func (window *window) handleKeyRelease ( key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State) modifiers := window.modifiersFromState(keyEvent.State) modifiers.NumberPad = numberPad - - focused, ok := window.focused.element.(tomo.KeyboardTarget) - if ok { focused.HandleKeyUp(key, modifiers) } - - window.system.afterEvent() + + if window.focused != nil { + focused, ok := window.focused.element.(tomo.KeyboardTarget) + if ok { focused.HandleKeyUp(key, modifiers) } + + window.system.afterEvent() + } } func (window *window) handleButtonPress ( diff --git a/backends/x/window.go b/backends/x/window.go index eb661ac..2bd12f3 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -48,9 +48,6 @@ func (backend *Backend) NewWindow ( ) { if backend == nil { panic("nil backend") } window, err := backend.newWindow(bounds, false) - - window.system.initialize() - window.system.pushFunc = window.paste output = mainWindow { window } return output, err @@ -68,6 +65,9 @@ func (backend *Backend) newWindow ( window := &window { backend: backend } + window.system.initialize() + window.system.pushFunc = window.pasteAndPush + window.xWindow, err = xwindow.Generate(backend.connection) if err != nil { return } @@ -125,7 +125,7 @@ func (backend *Backend) newWindow ( window.SetConfig(backend.config) window.metrics.bounds = bounds - window.childMinimumSizeChangeCallback(8, 8) + window.setMinimumSize(8, 8) window.reallocateCanvas() @@ -404,6 +404,11 @@ func (window *window) reallocateCanvas () { } +func (window *window) pasteAndPush (region image.Rectangle) { + window.paste(region) + window.pushRegion(region) +} + func (window *window) paste (region image.Rectangle) { canvas := canvas.Cut(window.canvas, region) data, stride := canvas.Buffer() @@ -438,7 +443,9 @@ func (window *window) pushRegion (region image.Rectangle) { } } -func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) { +func (window *window) setMinimumSize (width, height int) { + if width < 8 { width = 8 } + if height < 8 { height = 8 } icccm.WmNormalHintsSet ( window.backend.connection, window.xWindow.Id, @@ -454,8 +461,5 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize if newWidth != window.metrics.bounds.Dx() || newHeight != window.metrics.bounds.Dy() { window.xWindow.Resize(newWidth, newHeight) - return true } - - return false } diff --git a/elements/button.go b/elements/button.go index caba419..a396d92 100644 --- a/elements/button.go +++ b/elements/button.go @@ -29,7 +29,7 @@ type Button struct { // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { - element = &Button { showText: true } + element = &Button { showText: true, enabled: true } element.theme.Case = tomo.C("tomo", "button") element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, @@ -126,7 +126,6 @@ func (element *Button) SetConfig (new tomo.Config) { // 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) @@ -183,17 +182,15 @@ func (element *Button) Draw (destination canvas.Canvas) { } func (element *Button) HandleFocusChange () { - if element.entity == nil { return } - element.entity.Invalidate() + if element.entity != nil { 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() + if element.entity != nil { element.entity.Invalidate() } } func (element *Button) HandleMouseUp (x, y int, button input.Button) { @@ -204,23 +201,21 @@ func (element *Button) HandleMouseUp (x, y int, button input.Button) { if element.Enabled() && within && element.onClick != nil { element.onClick() } - element.entity.Invalidate() + if element.entity != nil { 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() + if element.entity != nil { 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.entity != nil { element.entity.Invalidate() } if !element.Enabled() { return } if element.onClick != nil { element.onClick() diff --git a/elements/checkbox.go b/elements/checkbox.go index 74bdf97..42445a4 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -26,7 +26,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 } + element = &Checkbox { checked: checked, enabled: true } element.theme.Case = tomo.C("tomo", "checkbox") element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, From 5cf0b162c048916c4da62b4cc3708854c3033f31 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 00:02:30 -0400 Subject: [PATCH 10/41] Child property change events make more sense now --- backends/x/entity.go | 11 +++++++---- element.go | 6 +++--- elements/label.go | 3 +-- entity.go | 19 +++++++++---------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index 53b62ce..d24008d 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -79,7 +79,8 @@ func (entity *entity) SetMinimumSize (width, height int) { if entity.parent == nil { entity.window.setMinimumSize(width, height) } else { - entity.parent.element.(tomo.Container).HandleChildMinimumSizeChange() + entity.parent.element.(tomo.Container). + HandleChildMinimumSizeChange(entity.element) } } @@ -172,10 +173,11 @@ func (entity *entity) FocusPrevious () { // ----------- FlexibleEntity ----------- // -func (entity *entity) NotifyFlexibleHeightChange (child tomo.Flexible) { +func (entity *entity) NotifyFlexibleHeightChange () { if entity.parent == nil { return } if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok { - parent.HandleChildFlexibleHeightChange() + parent.HandleChildFlexibleHeightChange ( + entity.element.(tomo.Flexible)) } } @@ -184,6 +186,7 @@ func (entity *entity) NotifyFlexibleHeightChange (child tomo.Flexible) { func (entity *entity) NotifyScrollBoundsChange () { if entity.parent == nil { return } if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok { - parent.HandleChildScrollBoundsChange() + parent.HandleChildScrollBoundsChange ( + entity.element.(tomo.Scrollable)) } } diff --git a/element.go b/element.go index 66b05a9..38a9ec2 100644 --- a/element.go +++ b/element.go @@ -28,7 +28,7 @@ type Container interface { // HandleChildMinimumSizeChange is called when a child's minimum size is // changed. - HandleChildMinimumSizeChange () + HandleChildMinimumSizeChange (child Element) } // Focusable represents an element that has keyboard navigation support. @@ -115,7 +115,7 @@ type FlexibleContainer interface { // HandleChildFlexibleHeightChange is called when the parameters // affecting a child's flexible height are changed. - HandleChildFlexibleHeightChange () + HandleChildFlexibleHeightChange (child Flexible) } // Scrollable represents an element that can be scrolled. It acts as a viewport @@ -145,7 +145,7 @@ type ScrollableContainer interface { // HandleChildScrollBoundsChange is called when the content bounds, // viewport bounds, or scroll axes of a child are changed. - HandleChildScrollBoundsChange() + HandleChildScrollBoundsChange (child Scrollable) } // Collapsible represents an element who's minimum width and height can be diff --git a/elements/label.go b/elements/label.go index 2e1cca9..a0951c6 100644 --- a/elements/label.go +++ b/elements/label.go @@ -153,8 +153,7 @@ func (element *Label) updateMinimumSize () { 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) + element.entity.NotifyFlexibleHeightChange() } else { bounds := element.drawer.LayoutBounds() width, height = bounds.Dx(), bounds.Dy() diff --git a/entity.go b/entity.go index 3a1b611..ff371c4 100644 --- a/entity.go +++ b/entity.go @@ -91,20 +91,19 @@ type FlexibleEntity interface { Entity // NotifyFlexibleHeightChange notifies the system that the parameters - // affecting a child's flexible height have changed. This method is - // expected to be called by flexible child element when their content - // changes. - NotifyFlexibleHeightChange (child Flexible) + // affecting the element's flexible height have changed. This method is + // expected to be called by flexible elements when their content changes. + NotifyFlexibleHeightChange () } // ScrollableEntity is given to elements that support the Scrollable interface. type ScrollableEntity interface { Entity - // NotifyScrollBoundsChange notifies the parent that a child's scroll - // content bounds or viewport bounds have changed. This is expected to - // be called by child elements when they change their supported scroll - // axes, their scroll position (either autonomously or as a result of a - // call to ScrollTo()), or their content size. - NotifyScrollBoundsChange (child Scrollable) + // NotifyScrollBoundsChange notifies the system that the element's + // scroll content bounds or viewport bounds have changed. This is + // expected to be called by scrollable elements when they change their + // supported scroll axes, their scroll position (either autonomously or + // as a result of a call to ScrollTo()), or their content size. + NotifyScrollBoundsChange () } From 437aef0c273720032608823e2d37490c4ea821ee Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 01:14:36 -0400 Subject: [PATCH 11/41] Redid the entity system a bit to make it more reliable Now it supports things like parenting elements before they are added to a window and elements no longer have to constantly check for a nil entity --- backend.go | 3 ++ backends/x/entity.go | 91 ++++++++++++++++++++++++++++++-------------- backends/x/system.go | 10 +++-- backends/x/window.go | 11 ++++-- element.go | 14 ++++--- elements/button.go | 27 +++++-------- elements/checkbox.go | 20 +++------- elements/icon.go | 10 ++--- elements/image.go | 12 +++--- elements/label.go | 13 +++---- entity.go | 8 ++-- tomo.go | 6 +++ 12 files changed, 129 insertions(+), 96 deletions(-) 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. From a43f5ce595da6e52c2f6684c4b48ee3a90b21390 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 01:19:39 -0400 Subject: [PATCH 12/41] Window now checks for minimum size on adopt --- backends/x/window.go | 3 +++ elements/checkbox.go | 2 -- elements/label.go | 8 -------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/backends/x/window.go b/backends/x/window.go index f5368bc..ad39a79 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -153,6 +153,9 @@ func (window *window) Adopt (child tomo.Element) { if ok && childEntity != nil { window.child = childEntity childEntity.setWindow(window) + window.setMinimumSize ( + childEntity.minWidth, + childEntity.minHeight) window.resizeChildToFit() } } diff --git a/elements/checkbox.go b/elements/checkbox.go index ed9ec97..cd8adbe 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -65,7 +65,6 @@ func (element *Checkbox) Enabled () bool { func (element *Checkbox) SetEnabled (enabled bool) { if element.enabled == enabled { return } element.enabled = enabled - if element.entity == nil { return } element.entity.Invalidate() } @@ -150,7 +149,6 @@ func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) { } 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() diff --git a/elements/label.go b/elements/label.go index a733dc3..ec3b478 100644 --- a/elements/label.go +++ b/elements/label.go @@ -51,7 +51,6 @@ func (element *Label) Entity () tomo.Entity { func (element *Label) EmCollapse (columns int, rows int) { element.forcedColumns = columns element.forcedRows = rows - if element.entity == nil { return } element.updateMinimumSize() } @@ -71,7 +70,6 @@ func (element *Label) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) - if element.entity == nil { return } element.updateMinimumSize() element.entity.Invalidate() } @@ -87,7 +85,6 @@ func (element *Label) SetWrap (wrap bool) { element.drawer.SetMaxHeight(0) } element.wrap = wrap - if element.entity == nil { return } element.updateMinimumSize() element.entity.Invalidate() } @@ -97,7 +94,6 @@ 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() } @@ -109,7 +105,6 @@ func (element *Label) 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,15 +113,12 @@ func (element *Label) SetTheme (new tomo.Theme) { 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 { From ca863285067b3f1ec6ad0051f1049cda6d0c8506 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 01:45:11 -0400 Subject: [PATCH 13/41] Migrated some more elements --- elements/{notdone => }/lerpslider.go | 0 elements/notdone/label.go | 177 -------------------------- elements/{notdone => }/progressbar.go | 58 ++++----- elements/{notdone => }/scrollbar.go | 76 ++++++----- elements/{notdone => }/slider.go | 131 ++++++++++--------- elements/{notdone => }/spacer.go | 63 +++++---- 6 files changed, 165 insertions(+), 340 deletions(-) rename elements/{notdone => }/lerpslider.go (100%) delete mode 100644 elements/notdone/label.go rename elements/{notdone => }/progressbar.go (76%) rename elements/{notdone => }/scrollbar.go (90%) rename elements/{notdone => }/slider.go (77%) rename elements/{notdone => }/spacer.go (71%) diff --git a/elements/notdone/lerpslider.go b/elements/lerpslider.go similarity index 100% rename from elements/notdone/lerpslider.go rename to elements/lerpslider.go diff --git a/elements/notdone/label.go b/elements/notdone/label.go deleted file mode 100644 index f980395..0000000 --- a/elements/notdone/label.go +++ /dev/null @@ -1,177 +0,0 @@ -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/notdone/progressbar.go b/elements/progressbar.go similarity index 76% rename from elements/notdone/progressbar.go rename to elements/progressbar.go index 5a20347..c0d2963 100644 --- a/elements/notdone/progressbar.go +++ b/elements/progressbar.go @@ -2,11 +2,13 @@ package elements 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" // ProgressBar displays a visual indication of how far along a task is. type ProgressBar struct { + entity tomo.Entity progress float64 config config.Wrapped @@ -17,20 +19,38 @@ type ProgressBar struct { // level. func NewProgressBar (progress float64) (element *ProgressBar) { element = &ProgressBar { progress: progress } + element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "progressBar") - element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *ProgressBar) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *ProgressBar) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + + pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) + padding := element.theme.Padding(tomo.PatternSunken) + pattern.Draw(destination, bounds) + bounds = padding.Apply(bounds) + meterBounds := image.Rect ( + bounds.Min.X, bounds.Min.Y, + bounds.Min.X + int(float64(bounds.Dx()) * element.progress), + bounds.Max.Y) + mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { }) + mercury.Draw(destination, meterBounds) +} + // SetProgress sets the progress level of the bar. func (element *ProgressBar) SetProgress (progress float64) { if progress == element.progress { return } element.progress = progress - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -38,7 +58,7 @@ func (element *ProgressBar) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -46,35 +66,13 @@ func (element *ProgressBar) SetConfig (new tomo.Config) { if new == nil || new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } func (element *ProgressBar) updateMinimumSize() { padding := element.theme.Padding(tomo.PatternSunken) innerPadding := element.theme.Padding(tomo.PatternMercury) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal() + innerPadding.Horizontal(), padding.Vertical() + innerPadding.Vertical()) } - -func (element *ProgressBar) redo () { - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *ProgressBar) draw () { - bounds := element.Bounds() - - pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) - padding := element.theme.Padding(tomo.PatternSunken) - pattern.Draw(element.core, bounds) - bounds = padding.Apply(bounds) - meterBounds := image.Rect ( - bounds.Min.X, bounds.Min.Y, - bounds.Min.X + int(float64(bounds.Dx()) * element.progress), - bounds.Max.Y) - mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { }) - mercury.Draw(element.core, meterBounds) -} diff --git a/elements/notdone/scrollbar.go b/elements/scrollbar.go similarity index 90% rename from elements/notdone/scrollbar.go rename to elements/scrollbar.go index d92815e..8835bde 100644 --- a/elements/notdone/scrollbar.go +++ b/elements/scrollbar.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/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" @@ -18,6 +19,8 @@ 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 { + entity tomo.ContainerEntity + vertical bool enabled bool dragging bool @@ -46,16 +49,31 @@ func NewScrollBar (vertical bool) (element *ScrollBar) { } else { element.theme.Case = tomo.C("tomo", "scrollBarVertical") } - element.Core, element.core = core.NewCore(element, element.handleResize) + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) element.updateMinimumSize() return } -func (element *ScrollBar) handleResize () { - if element.core.HasImage() { - element.recalculate() - element.draw() +// Entity returns this element's entity. +func (element *ScrollBar) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *ScrollBar) Draw (destination canvas.Canvas) { + element.recalculate() + + bounds := element.entity.Bounds() + state := tomo.State { + Disabled: !element.Enabled(), + Pressed: element.dragging, } + element.theme.Pattern(tomo.PatternGutter, state).Draw ( + destination, + bounds) + element.theme.Pattern(tomo.PatternHandle, state).Draw ( + destination, + element.bar) } func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) { @@ -65,10 +83,10 @@ func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) { if point.In(element.bar) { // the mouse is pressed down within the bar's handle element.dragging = true - element.drawAndPush() + element.entity.Invalidate() element.dragOffset = point.Sub(element.bar.Min). - Add(element.Bounds().Min) + Add(element.entity.Bounds().Min) element.dragTo(point) } else { // the mouse is pressed down within the bar's gutter @@ -108,7 +126,7 @@ func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) { func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) { if element.dragging { element.dragging = false - element.drawAndPush() + element.entity.Invalidate() } } @@ -130,7 +148,7 @@ func (element *ScrollBar) HandleScroll (x, y int, deltaX, deltaY float64) { func (element *ScrollBar) SetEnabled (enabled bool) { if element.enabled == enabled { return } element.enabled = enabled - element.drawAndPush() + element.entity.Invalidate() } // Enabled returns whether or not the element is enabled. @@ -142,8 +160,7 @@ func (element *ScrollBar) Enabled () (enabled bool) { func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) { element.contentBounds = content element.viewportBounds = viewport - element.recalculate() - element.drawAndPush() + element.entity.Invalidate() } // OnScroll sets a function to be called when the user tries to move the scroll @@ -159,7 +176,7 @@ func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) { func (element *ScrollBar) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - element.drawAndPush() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -167,7 +184,7 @@ func (element *ScrollBar) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } func (element *ScrollBar) isAfterHandle (point image.Point) bool { @@ -180,10 +197,10 @@ func (element *ScrollBar) isAfterHandle (point image.Point) bool { func (element *ScrollBar) fallbackDragOffset () image.Point { if element.vertical { - return element.Bounds().Min. + return element.entity.Bounds().Min. Add(image.Pt(0, element.bar.Dy() / 2)) } else { - return element.Bounds().Min. + return element.entity.Bounds().Min. Add(image.Pt(element.bar.Dx() / 2, 0)) } } @@ -232,7 +249,7 @@ func (element *ScrollBar) recalculate () { } func (element *ScrollBar) recalculateVertical () { - bounds := element.Bounds() + bounds := element.entity.Bounds() padding := element.theme.Padding(tomo.PatternGutter) element.track = padding.Apply(bounds) @@ -259,7 +276,7 @@ func (element *ScrollBar) recalculateVertical () { } func (element *ScrollBar) recalculateHorizontal () { - bounds := element.Bounds() + bounds := element.entity.Bounds() padding := element.theme.Padding(tomo.PatternGutter) element.track = padding.Apply(bounds) @@ -289,33 +306,12 @@ func (element *ScrollBar) updateMinimumSize () { gutterPadding := element.theme.Padding(tomo.PatternGutter) handlePadding := element.theme.Padding(tomo.PatternHandle) if element.vertical { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal(), gutterPadding.Vertical() + handlePadding.Vertical() * 2) } else { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal() * 2, gutterPadding.Vertical() + handlePadding.Vertical()) } } - -func (element *ScrollBar) drawAndPush () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *ScrollBar) draw () { - bounds := element.Bounds() - state := tomo.State { - Disabled: !element.Enabled(), - Pressed: element.dragging, - } - element.theme.Pattern(tomo.PatternGutter, state).Draw ( - element.core, - bounds) - element.theme.Pattern(tomo.PatternHandle, state).Draw ( - element.core, - element.bar) -} diff --git a/elements/notdone/slider.go b/elements/slider.go similarity index 77% rename from elements/notdone/slider.go rename to elements/slider.go index 0f344c3..0810be4 100644 --- a/elements/notdone/slider.go +++ b/elements/slider.go @@ -3,17 +3,21 @@ 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/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 { - value float64 - vertical bool - dragging bool + entity tomo.FocusableEntity + + value float64 + vertical bool + dragging bool + enabled bool dragOffset int - track image.Rectangle - bar image.Rectangle + track image.Rectangle + bar image.Rectangle config config.Wrapped theme theme.Wrapped @@ -34,13 +38,62 @@ func NewSlider (value float64, vertical bool) (element *Slider) { } else { element.theme.Case = tomo.C("tomo", "sliderHorizontal") } - element.Core, element.core = core.NewCore(element, element.draw) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *Slider) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Slider) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds) + if element.vertical { + barSize := element.track.Dx() + element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) + barOffset := + float64(element.track.Dy() - barSize) * + (1 - element.value) + element.bar = element.bar.Add(image.Pt(0, int(barOffset))) + } else { + barSize := element.track.Dy() + element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) + barOffset := + float64(element.track.Dx() - barSize) * + element.value + element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) + } + + state := tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.dragging, + } + element.theme.Pattern(tomo.PatternGutter, state).Draw(destination, bounds) + element.theme.Pattern(tomo.PatternHandle, state).Draw(destination, bounds) +} + +// Focus gives this element input focus. +func (element *Slider) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this slider can be dragged or not. +func (element *Slider) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this slider can be dragged or not. +func (element *Slider) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + func (element *Slider) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() @@ -50,7 +103,7 @@ func (element *Slider) HandleMouseDown (x, y int, button input.Button) { if element.onSlide != nil { element.onSlide() } - element.redo() + element.entity.Invalidate() } } @@ -60,7 +113,7 @@ func (element *Slider) HandleMouseUp (x, y int, button input.Button) { if element.onRelease != nil { element.onRelease() } - element.redo() + element.entity.Invalidate() } func (element *Slider) HandleMotion (x, y int) { @@ -70,7 +123,7 @@ func (element *Slider) HandleMotion (x, y int) { if element.onSlide != nil { element.onSlide() } - element.redo() + element.entity.Invalidate() } } @@ -104,11 +157,6 @@ func (element *Slider) Value () (value float64) { return element.value } -// SetEnabled sets whether or not the slider can be interacted with. -func (element *Slider) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) -} - // SetValue sets the slider's value. func (element *Slider) SetValue (value float64) { if value < 0 { value = 0 } @@ -120,7 +168,7 @@ func (element *Slider) SetValue (value float64) { if element.onRelease != nil { element.onRelease() } - element.redo() + element.entity.Invalidate() } // OnSlide sets a function to be called every time the slider handle changes @@ -138,7 +186,7 @@ func (element *Slider) OnRelease (callback func ()) { func (element *Slider) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -146,7 +194,7 @@ func (element *Slider) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } func (element *Slider) changeValue (delta float64) { @@ -160,7 +208,7 @@ func (element *Slider) changeValue (delta float64) { if element.onRelease != nil { element.onRelease() } - element.redo() + element.entity.Invalidate() } func (element *Slider) valueFor (x, y int) (value float64) { @@ -184,51 +232,12 @@ func (element *Slider) updateMinimumSize () { gutterPadding := element.theme.Padding(tomo.PatternGutter) handlePadding := element.theme.Padding(tomo.PatternHandle) if element.vertical { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal(), gutterPadding.Vertical() + handlePadding.Vertical() * 2) } else { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal() * 2, gutterPadding.Vertical() + handlePadding.Vertical()) } } - -func (element *Slider) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *Slider) draw () { - bounds := element.Bounds() - element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds) - if element.vertical { - barSize := element.track.Dx() - element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) - barOffset := - float64(element.track.Dy() - barSize) * - (1 - element.value) - element.bar = element.bar.Add(image.Pt(0, int(barOffset))) - } else { - barSize := element.track.Dy() - element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) - barOffset := - float64(element.track.Dx() - barSize) * - element.value - element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) - } - - state := tomo.State { - Focused: element.Focused(), - Disabled: !element.Enabled(), - Pressed: element.dragging, - } - element.theme.Pattern(tomo.PatternGutter, state).Draw ( - element.core, - bounds) - element.theme.Pattern(tomo.PatternHandle, state).Draw ( - element.core, - element.bar) -} diff --git a/elements/notdone/spacer.go b/elements/spacer.go similarity index 71% rename from elements/notdone/spacer.go rename to elements/spacer.go index bbcef24..a382b44 100644 --- a/elements/notdone/spacer.go +++ b/elements/spacer.go @@ -1,11 +1,14 @@ package elements 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" // Spacer can be used to put space between two elements.. type Spacer struct { + entity tomo.Entity + line bool config config.Wrapped @@ -17,67 +20,63 @@ type Spacer struct { // will appear as a line. func NewSpacer (line bool) (element *Spacer) { element = &Spacer { line: line } + element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "spacer") - element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *Spacer) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Spacer) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + + if element.line { + pattern := element.theme.Pattern ( + tomo.PatternLine, + tomo.State { }) + pattern.Draw(destination, bounds) + } else { + pattern := element.theme.Pattern ( + tomo.PatternBackground, + tomo.State { }) + pattern.Draw(destination, bounds) + } +} + /// SetLine sets whether or not the spacer will appear as a colored line. func (element *Spacer) SetLine (line bool) { if element.line == line { return } element.line = line element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. func (element *Spacer) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Spacer) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new - element.redo() + element.entity.Invalidate() } func (element *Spacer) updateMinimumSize () { if element.line { padding := element.theme.Padding(tomo.PatternLine) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal(), padding.Vertical()) } else { - element.core.SetMinimumSize(1, 1) - } -} - -func (element *Spacer) redo () { - if !element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *Spacer) draw () { - bounds := element.Bounds() - - if element.line { - pattern := element.theme.Pattern ( - tomo.PatternLine, - tomo.State { }) - pattern.Draw(element.core, bounds) - } else { - pattern := element.theme.Pattern ( - tomo.PatternBackground, - tomo.State { }) - pattern.Draw(element.core, bounds) + element.entity.SetMinimumSize(1, 1) } } From 9e16f7b53209fb0b445267fb018283f83b0d98e2 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 12:35:00 -0400 Subject: [PATCH 14/41] Migrated TextBox --- elements/button.go | 114 ++++++------ elements/checkbox.go | 60 +++--- elements/{notdone => }/doc.go | 0 elements/{notdone => }/switch.go | 250 +++++++++++++------------ elements/{notdone => }/textbox.go | 297 ++++++++++++++++-------------- 5 files changed, 375 insertions(+), 346 deletions(-) rename elements/{notdone => }/doc.go (100%) rename elements/{notdone => }/switch.go (73%) rename elements/{notdone => }/textbox.go (82%) 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(), } } From 986315d5dbb7c07851ea0516de3b76c92839bba1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 18:09:49 -0400 Subject: [PATCH 15/41] Vertical layout partially works --- backends/x/entity.go | 6 +- backends/x/system.go | 2 +- elements/containers/container.go | 258 ------------------ elements/containers/{ => notdone}/document.go | 0 elements/containers/{ => notdone}/scroll.go | 0 elements/containers/{ => notdone}/table.go | 0 elements/containers/vbox.go | 178 ++++++++++++ examples/verticalLayout/main.go | 9 +- 8 files changed, 187 insertions(+), 266 deletions(-) delete mode 100644 elements/containers/container.go rename elements/containers/{ => notdone}/document.go (100%) rename elements/containers/{ => notdone}/scroll.go (100%) rename elements/containers/{ => notdone}/table.go (100%) create mode 100644 elements/containers/vbox.go diff --git a/backends/x/entity.go b/backends/x/entity.go index 9ee447e..7e77be2 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -34,8 +34,10 @@ func (ent *entity) unlink () { delete(ent.window.system.drawingInvalid, child) return true }) - - delete(ent.window.system.drawingInvalid, ent) + + if ent.window != nil { + delete(ent.window.system.drawingInvalid, ent) + } ent.parent = nil ent.window = nil } diff --git a/backends/x/system.go b/backends/x/system.go index a9f33ce..a9c7187 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -98,7 +98,7 @@ func (system *system) afterEvent () { } func (system *system) layout (entity *entity, force bool) { - if entity == nil { return } + if entity == nil || !entity.isContainer { return } if entity.layoutInvalid == true || force { entity.element.(tomo.Container).Layout() entity.layoutInvalid = false diff --git a/elements/containers/container.go b/elements/containers/container.go deleted file mode 100644 index c10b5ba..0000000 --- a/elements/containers/container.go +++ /dev/null @@ -1,258 +0,0 @@ -package containers - -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/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// Container is an element capable of containg other elements, and arranging -// them in a layout. -type Container struct { - *core.Core - *core.Propagator - core core.CoreControl - - layout tomo.Layout - children []tomo.LayoutEntry - warping bool - - config config.Wrapped - theme theme.Wrapped - - onFocusRequest func () (granted bool) - onFocusMotionRequest func (input.KeynavDirection) (granted bool) -} - -// NewContainer creates a new container. -func NewContainer (layout tomo.Layout) (element *Container) { - element = &Container { } - element.theme.Case = tomo.C("tomo", "container") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - element.SetLayout(layout) - return -} - -// SetLayout sets the layout of this container. -func (element *Container) SetLayout (layout tomo.Layout) { - element.layout = layout - element.updateMinimumSize() - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// Adopt adds a new child element to the container. If expand is set to true, -// the element will expand (instead of contract to its minimum size), in -// whatever way is defined by the current layout. -func (element *Container) Adopt (child tomo.Element, expand bool) { - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(element.config.Config) - } - child.SetParent(element) - - // add child - element.children = append (element.children, tomo.LayoutEntry { - Element: child, - Expand: expand, - }) - - // refresh stale data - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Warp runs the specified callback, deferring all layout and rendering updates -// until the callback has finished executing. This allows for aplications to -// perform batch gui updates without flickering and stuff. -func (element *Container) Warp (callback func ()) { - if element.warping { - callback() - return - } - - element.warping = true - callback() - element.warping = false - - // TODO: create some sort of task list so we don't do a full recalculate - // and redraw every time, because although that is the most likely use - // case, it is not the only one. - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// Disown removes the given child from the container if it is contained within -// it. -func (element *Container) Disown (child tomo.Element) { - for index, entry := range element.children { - if entry.Element == child { - element.clearChildEventHandlers(entry.Element) - element.children = append ( - element.children[:index], - element.children[index + 1:]...) - break - } - } - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -func (element *Container) clearChildEventHandlers (child tomo.Element) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} - -// DisownAll removes all child elements from the container at once. -func (element *Container) DisownAll () { - for _, entry := range element.children { - element.clearChildEventHandlers(entry.Element) - } - element.children = nil - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Children returns a slice containing this element's children. -func (element *Container) Children () (children []tomo.Element) { - children = make([]tomo.Element, len(element.children)) - for index, entry := range element.children { - children[index] = entry.Element - } - return -} - -// CountChildren returns the amount of children contained within this element. -func (element *Container) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *Container) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].Element -} - -// ChildAt returns the child that contains the specified x and y coordinates. If -// there are no children at the coordinates, this method will return nil. -func (element *Container) ChildAt (point image.Point) (child tomo.Element) { - for _, entry := range element.children { - if point.In(entry.Bounds) { - child = entry.Element - } - } - return -} - -func (element *Container) redoAll () { - if !element.core.HasImage() { return } - - // remove child canvasses so that any operations done in here will not - // cause a child to draw to a wack ass canvas. - for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds, nil) - } - - // do a layout - element.doLayout() - - // draw a background - rocks := make([]image.Rectangle, len(element.children)) - for index, entry := range element.children { - rocks[index] = entry.Bounds - } - - element.core.DrawBackgroundBoundsShatter ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - element.Bounds(), - rocks...) - - // cut our canvas up and give peices to child elements - for _, entry := range element.children { - entry.DrawTo ( - canvas.Cut(element.core, entry.Bounds), - entry.Bounds, func (region image.Rectangle) { - element.core.DamageRegion(region) - }) - } -} - -func (element *Container) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *Container) NotifyMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.redoAll() - element.core.DamageAll() -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *Container) DrawBackground (bounds image.Rectangle) { - element.core.DrawBackgroundBounds ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - bounds) -} - -// SetTheme sets the element's theme. -func (element *Container) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.updateMinimumSize() - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *Container) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.updateMinimumSize() - element.redoAll() -} - -func (element *Container) updateMinimumSize () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) - width, height := element.layout.MinimumSize ( - element.children, margin, padding) - element.core.SetMinimumSize(width, height) -} - -func (element *Container) doLayout () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) - element.layout.Arrange ( - element.children, margin, - padding, element.Bounds()) -} diff --git a/elements/containers/document.go b/elements/containers/notdone/document.go similarity index 100% rename from elements/containers/document.go rename to elements/containers/notdone/document.go diff --git a/elements/containers/scroll.go b/elements/containers/notdone/scroll.go similarity index 100% rename from elements/containers/scroll.go rename to elements/containers/notdone/scroll.go diff --git a/elements/containers/table.go b/elements/containers/notdone/table.go similarity index 100% rename from elements/containers/table.go rename to elements/containers/notdone/table.go diff --git a/elements/containers/vbox.go b/elements/containers/vbox.go new file mode 100644 index 0000000..e6b9215 --- /dev/null +++ b/elements/containers/vbox.go @@ -0,0 +1,178 @@ +package containers + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +type scratchEntry struct { + expand bool + minimum float64 +} + +type VBox struct { + entity tomo.ContainerEntity + scratch map[tomo.Element] scratchEntry + theme theme.Wrapped + padding bool + margin bool +} + +func NewVBox (padding, margin bool) (element *VBox) { + element = &VBox { padding: padding, margin: margin } + element.scratch = make(map[tomo.Element] scratchEntry) + element.theme.Case = tomo.C("tomo", "vBox") + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + return +} + +func (element *VBox) Entity () tomo.Entity { + return element.entity +} + +func (element *VBox) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.entity.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *VBox) Layout () { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + bounds := element.entity.Bounds() + if element.padding { bounds = padding.Apply(bounds) } + + freeSpace, nExpanding := element.freeSpace() + expandingElementHeight := freeSpace / nExpanding + + // set the size and position of each element + x := float64(bounds.Min.X) + y := float64(bounds.Min.Y) + for index := 0; index < element.entity.CountChildren(); index ++ { + entry := element.scratch[element.entity.Child(index)] + + var height float64; if entry.expand { + height = expandingElementHeight + } else { + height = entry.minimum + } + + element.entity.PlaceChild (index, tomo.Bounds ( + int(x), int(y), + bounds.Dx(), int(height))) + + y += height + if element.margin { y += float64(margin.Y) } + } + +} + +func (element *VBox) Adopt (child tomo.Element, expand bool) { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: expand } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *VBox) Disown (child tomo.Element) { + index := element.entity.IndexOf(child) + if index < 0 { return } + element.entity.Disown(index) + delete(element.scratch, child) + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *VBox) DisownAll () { + func () { + for index := 0; index < element.entity.CountChildren(); index ++ { + index := index + defer element.entity.Disown(index) + } + } () + element.scratch = make(map[tomo.Element] scratchEntry) + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *VBox) HandleChildMinimumSizeChange (child tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *VBox) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +// SetTheme sets the element's theme. +func (element *VBox) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *VBox) freeSpace () (space float64, nExpanding float64) { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + space = float64(element.entity.Bounds().Dy()) + + for _, entry := range element.scratch { + if entry.expand { + nExpanding ++; + } else { + space -= float64(entry.minimum) + } + } + + if element.padding { + space -= float64(padding.Vertical()) + } + if element.margin { + space -= float64(margin.Y * len(element.scratch) - 1) + } + + return +} + +func (element *VBox) updateMinimumSize () { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + var width, height int + + for index := 0; index < element.entity.CountChildren(); index ++ { + childWidth, childHeight := element.entity.ChildMinimumSize(index) + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minimum = float64(childHeight) + element.scratch[key] = entry + + if childWidth > width { + width = childWidth + } + height += childHeight + if element.margin && index > 0 { + height += margin.Y + } + } + + if element.padding { + width += padding.Horizontal() + height += padding.Vertical() + } + + element.entity.SetMinimumSize(width, height) +} diff --git a/examples/verticalLayout/main.go b/examples/verticalLayout/main.go index ddced0a..7860ec5 100644 --- a/examples/verticalLayout/main.go +++ b/examples/verticalLayout/main.go @@ -1,9 +1,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/testing" +// import "git.tebibyte.media/sashakoshka/tomo/elements/testing" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -15,8 +14,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128)) window.SetTitle("vertical stack") - container := containers.NewContainer(layouts.Vertical { true, true }) - window.Adopt(container) + container := containers.NewVBox(true, true) label := elements.NewLabel("it is a label hehe", true) button := elements.NewButton("drawing pad") @@ -24,7 +22,7 @@ func run () { button.OnClick (func () { container.DisownAll() container.Adopt(elements.NewLabel("Draw here:", false), false) - container.Adopt(testing.NewMouse(), true) + // container.Adopt(testing.NewMouse(), true) container.Adopt(okButton, false) okButton.Focus() }) @@ -35,6 +33,7 @@ func run () { container.Adopt(okButton, false) okButton.Focus() + window.Adopt(container) window.OnClose(tomo.Stop) window.Show() } From 9d78a599aa06eb8ca6152034caaafe3b92fefc42 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 18:24:16 -0400 Subject: [PATCH 16/41] Migrated fun elements --- elements/fun/clock.go | 70 +++++++--------- elements/fun/piano.go | 143 +++++++++++++++++--------------- examples/verticalLayout/main.go | 4 +- 3 files changed, 109 insertions(+), 108 deletions(-) diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 23397e2..a23e828 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -5,18 +5,15 @@ import "math" import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" 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" // AnalogClock can display the time of day in an analog format. type AnalogClock struct { - *core.Core - core core.CoreControl - time time.Time - - config config.Wrapped + entity tomo.Entity + time time.Time theme theme.Wrapped } @@ -24,40 +21,18 @@ type AnalogClock struct { func NewAnalogClock (newTime time.Time) (element *AnalogClock) { element = &AnalogClock { } element.theme.Case = tomo.C("tomo", "clock") - element.Core, element.core = core.NewCore(element, element.draw) + element.entity = tomo.NewEntity(element) element.core.SetMinimumSize(64, 64) return } -// SetTime changes the time that the clock displays. -func (element *AnalogClock) SetTime (newTime time.Time) { - if newTime == element.time { return } - element.time = newTime - element.redo() +// Entity returns this element's entity. +func (element *AnalogClock) Entity () tomo.Entity { + return element.entity } -// SetTheme sets the element's theme. -func (element *AnalogClock) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.redo() -} - -// SetConfig sets the element's configuration. -func (element *AnalogClock) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.redo() -} - -func (element *AnalogClock) redo () { - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *AnalogClock) draw () { +// Draw causes the element to draw to the specified destination canvas. +func (element *AnalogClock) Draw (destination canvas.Canvas) { bounds := element.Bounds() state := tomo.State { } @@ -80,24 +55,39 @@ func (element *AnalogClock) draw () { minute := float64(element.time.Minute()) + second / 60 hour := float64(element.time.Hour()) + minute / 60 - element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi) - element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi) - element.radialLine(accent, 0, 0.7, (second - 15) / 30 * math.Pi) + element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi) + element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi) + element.radialLine(destination, accent, 0, 0.7, (second - 15) / 30 * math.Pi) +} + +// SetTime changes the time that the clock displays. +func (element *AnalogClock) SetTime (newTime time.Time) { + if newTime == element.time { return } + element.time = newTime + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *AnalogClock) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.entity.Invalidate() } func (element *AnalogClock) radialLine ( + destination canvas.Canvas, source color.RGBA, inner float64, outer float64, radian float64, ) { - bounds := element.Bounds() + bounds := element.entity.Bounds() width := float64(bounds.Dx()) / 2 height := float64(bounds.Dy()) / 2 - min := element.Bounds().Min.Add(image.Pt ( + min := bounds.Min.Add(image.Pt ( int(math.Cos(radian) * inner * width + width), int(math.Sin(radian) * inner * height + height))) - max := element.Bounds().Min.Add(image.Pt ( + max := bounds.Min.Add(image.Pt ( int(math.Cos(radian) * outer * width + width), int(math.Sin(radian) * outer * height + height))) shapes.ColorLine(element.core, source, 1, min, max) diff --git a/elements/fun/piano.go b/elements/fun/piano.go index 9097fc7..5776ed1 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -3,8 +3,8 @@ package fun import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music" @@ -18,21 +18,19 @@ type pianoKey struct { // Piano is an element that can be used to input midi notes. type Piano struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - low, high music.Octave - + entity tomo.FocusableEntity + config config.Wrapped theme theme.Wrapped flatTheme theme.Wrapped sharpTheme theme.Wrapped + low, high music.Octave flatKeys []pianoKey sharpKeys []pianoKey contentBounds image.Rectangle + enabled bool pressed *pianoKey keynavPressed map[music.Note] bool @@ -43,11 +41,7 @@ type Piano struct { // NewPiano returns a new piano element with a lowest and highest octave, // inclusive. If low is greater than high, they will be swapped. func NewPiano (low, high music.Octave) (element *Piano) { - if low > high { - temp := low - low = high - high = temp - } + if low > high { low, high = high, low } element = &Piano { low: low, @@ -58,16 +52,68 @@ func NewPiano (low, high music.Octave) (element *Piano) { element.theme.Case = tomo.C("tomo", "piano") element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey") element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey") - element.Core, element.core = core.NewCore (element, func () { - element.recalculate() - element.draw() - }) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *Piano) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Piano) Draw (destination canvas.Canvas) { + element.recalculate() + + state := tomo.State { + Focused: element.entity.Focused(), + Disabled: !element.Enabled(), + } + + for _, key := range element.flatKeys { + _, keynavPressed := element.keynavPressed[key.Note] + element.drawFlat ( + destination, + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note || keynavPressed, + state) + } + for _, key := range element.sharpKeys { + _, keynavPressed := element.keynavPressed[key.Note] + element.drawSharp ( + destination, + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note || keynavPressed, + state) + } + + pattern := element.theme.Pattern(tomo.PatternPinboard, state) + artist.DrawShatter ( + destination, pattern, element.entity.Bounds(), + element.contentBounds) +} + +// Focus gives this element input focus. +func (element *Piano) Focus () { + element.entity.Focus() +} + +// Enabled returns whether this piano can be played or not. +func (element *Piano) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this piano can be played or not. +func (element *Piano) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + + // OnPress sets a function to be called when a key is pressed. func (element *Piano) OnPress (callback func (note music.Note)) { element.onPress = callback @@ -90,7 +136,7 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) { element.onRelease((*element.pressed).Note) } element.pressed = nil - element.redo() + element.entity.Invalidate() } func (element *Piano) HandleMotion (x, y int) { @@ -126,7 +172,7 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) { if element.onPress != nil { element.onPress((*element.pressed).Note) } - element.redo() + element.entity.Invalidate() } } @@ -186,7 +232,7 @@ func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if element.onPress != nil { element.onPress(note) } - element.redo() + element.entity.Invalidate() } } @@ -199,7 +245,7 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) { if element.onRelease != nil { element.onRelease(note) } - element.redo() + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -209,8 +255,7 @@ func (element *Piano) SetTheme (new tomo.Theme) { element.flatTheme.Theme = new element.sharpTheme.Theme = new element.updateMinimumSize() - element.recalculate() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -218,13 +263,12 @@ func (element *Piano) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.recalculate() - element.redo() + element.entity.Invalidate() } func (element *Piano) updateMinimumSize () { padding := element.theme.Padding(tomo.PatternPinboard) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( pianoKeyWidth * 7 * element.countOctaves() + padding.Horizontal(), 64 + padding.Vertical()) @@ -242,19 +286,12 @@ func (element *Piano) countSharps () int { return element.countOctaves() * 5 } -func (element *Piano) redo () { - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - func (element *Piano) recalculate () { element.flatKeys = make([]pianoKey, element.countFlats()) element.sharpKeys = make([]pianoKey, element.countSharps()) - padding := element.theme.Padding(tomo.PatternPinboard) - bounds := padding.Apply(element.Bounds()) + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) dot := bounds.Min note := element.low.Note(0) @@ -285,50 +322,24 @@ func (element *Piano) recalculate () { } } -func (element *Piano) draw () { - state := tomo.State { - Focused: element.Focused(), - Disabled: !element.Enabled(), - } - - for _, key := range element.flatKeys { - _, keynavPressed := element.keynavPressed[key.Note] - element.drawFlat ( - key.Rectangle, - element.pressed != nil && - (*element.pressed).Note == key.Note || keynavPressed, - state) - } - for _, key := range element.sharpKeys { - _, keynavPressed := element.keynavPressed[key.Note] - element.drawSharp ( - key.Rectangle, - element.pressed != nil && - (*element.pressed).Note == key.Note || keynavPressed, - state) - } - - pattern := element.theme.Pattern(tomo.PatternPinboard, state) - artist.DrawShatter ( - element.core, pattern, element.Bounds(), element.contentBounds) -} - func (element *Piano) drawFlat ( + destination canvas.Canvas, bounds image.Rectangle, pressed bool, state tomo.State, ) { state.Pressed = pressed pattern := element.flatTheme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, bounds) + pattern.Draw(destination, bounds) } func (element *Piano) drawSharp ( + destination canvas.Canvas, bounds image.Rectangle, pressed bool, state tomo.State, ) { state.Pressed = pressed pattern := element.sharpTheme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, bounds) + pattern.Draw(destination, bounds) } diff --git a/examples/verticalLayout/main.go b/examples/verticalLayout/main.go index 7860ec5..7b9c7b7 100644 --- a/examples/verticalLayout/main.go +++ b/examples/verticalLayout/main.go @@ -2,7 +2,7 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" -// import "git.tebibyte.media/sashakoshka/tomo/elements/testing" +import "git.tebibyte.media/sashakoshka/tomo/elements/testing" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -22,7 +22,7 @@ func run () { button.OnClick (func () { container.DisownAll() container.Adopt(elements.NewLabel("Draw here:", false), false) - // container.Adopt(testing.NewMouse(), true) + container.Adopt(testing.NewMouse(), true) container.Adopt(okButton, false) okButton.Focus() }) From bb50c7d7a73a94dbe691ab2a0426eb48106c7c50 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 18:30:22 -0400 Subject: [PATCH 17/41] Lol --- elements/fun/clock.go | 10 +++++----- elements/testing/artist.go | 12 +++++++----- elements/testing/mouse.go | 7 ++++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/elements/fun/clock.go b/elements/fun/clock.go index a23e828..0b4468d 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -8,7 +8,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" // AnalogClock can display the time of day in an analog format. type AnalogClock struct { @@ -22,7 +21,7 @@ func NewAnalogClock (newTime time.Time) (element *AnalogClock) { element = &AnalogClock { } element.theme.Case = tomo.C("tomo", "clock") element.entity = tomo.NewEntity(element) - element.core.SetMinimumSize(64, 64) + element.entity.SetMinimumSize(64, 64) return } @@ -33,12 +32,12 @@ func (element *AnalogClock) Entity () tomo.Entity { // Draw causes the element to draw to the specified destination canvas. func (element *AnalogClock) Draw (destination canvas.Canvas) { - bounds := element.Bounds() + bounds := element.entity.Bounds() state := tomo.State { } pattern := element.theme.Pattern(tomo.PatternSunken, state) padding := element.theme.Padding(tomo.PatternSunken) - pattern.Draw(element.core, bounds) + pattern.Draw(destination, bounds) bounds = padding.Apply(bounds) @@ -47,6 +46,7 @@ func (element *AnalogClock) Draw (destination canvas.Canvas) { for hour := 0; hour < 12; hour ++ { element.radialLine ( + destination, foreground, 0.8, 0.9, float64(hour) / 6 * math.Pi) } @@ -90,5 +90,5 @@ func (element *AnalogClock) radialLine ( max := bounds.Min.Add(image.Pt ( int(math.Cos(radian) * outer * width + width), int(math.Sin(radian) * outer * height + height))) - shapes.ColorLine(element.core, source, 1, min, max) + shapes.ColorLine(destination, source, 1, min, max) } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index 91faa0a..a7546a2 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -20,13 +20,15 @@ type Artist struct { } // NewArtist creates a new artist test element. -func NewArtist () *Artist { - return &Artist { } +func NewArtist () (element *Artist) { + element = &Artist { } + element.entity = tomo.NewEntity(element) + element.entity.SetMinimumSize(240, 240) + return } -func (element *Artist) Bind (entity tomo.Entity) { - element.entity = entity - if entity != nil { entity.SetMinimumSize(240, 240) } +func (element *Artist) Entity () tomo.Entity { + return element.entity } func (element *Artist) Draw (destination canvas.Canvas) { diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 56e09d7..fa22b9a 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -24,12 +24,13 @@ type Mouse struct { func NewMouse () (element *Mouse) { element = &Mouse { } element.theme.Case = tomo.C("tomo", "mouse") + element.entity = tomo.NewEntity(element) + element.entity.SetMinimumSize(32, 32) return } -func (element *Mouse) Bind (entity tomo.Entity) { - element.entity = entity - entity.SetMinimumSize(32, 32) +func (element *Mouse) Entity () tomo.Entity { + return element.entity } func (element *Mouse) Draw (destination canvas.Canvas) { From 1044c8299ac67ddb06199ad4eff417c811ac6ade Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 18:33:37 -0400 Subject: [PATCH 18/41] Literally one set of parentheses --- elements/containers/vbox.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elements/containers/vbox.go b/elements/containers/vbox.go index e6b9215..bab8306 100644 --- a/elements/containers/vbox.go +++ b/elements/containers/vbox.go @@ -141,7 +141,7 @@ func (element *VBox) freeSpace () (space float64, nExpanding float64) { space -= float64(padding.Vertical()) } if element.margin { - space -= float64(margin.Y * len(element.scratch) - 1) + space -= float64(margin.Y * (len(element.scratch) - 1)) } return From c0b205c6f0b1258ca229a01ec16a03910aabc758 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 18:49:02 -0400 Subject: [PATCH 19/41] This is what happens when you dont test anything oh my god --- backends/x/entity.go | 2 +- examples/checkbox/main.go | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index 7e77be2..3f3af47 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -63,7 +63,7 @@ func (ent *entity) setWindow (window *window) { func (entity *entity) propagate (callback func (*entity) bool) { for _, child := range entity.children { - if callback(child) { break } + if !callback(child) { break } child.propagate(callback) } } diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index a0dfbf4..f161cb6 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -1,8 +1,7 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" +// import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -15,7 +14,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Checkboxes") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) introText := elements.NewLabel ( @@ -35,11 +34,11 @@ func run () { vsync := elements.NewCheckbox("Enable vsync", false) vsync.OnToggle (func () { if vsync.Value() { - popups.NewDialog ( - popups.DialogKindInfo, - window, - "Ha!", - "That doesn't do anything.") + // popups.NewDialog ( + // popups.DialogKindInfo, + // window, + // "Ha!", + // "That doesn't do anything.") } }) container.Adopt(vsync, false) From 0cd7fb9be9f44313b3914153228e06a2ec1958a6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 18:51:42 -0400 Subject: [PATCH 20/41] Coherent commit messages are for weaklings --- elements/checkbox.go | 4 ++++ elements/slider.go | 4 ++++ elements/switch.go | 4 ++++ elements/textbox.go | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/elements/checkbox.go b/elements/checkbox.go index 58e37e0..9ae8295 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -126,6 +126,10 @@ func (element *Checkbox) SetConfig (new tomo.Config) { element.entity.Invalidate() } +func (element *Checkbox) HandleFocusChange () { + element.entity.Invalidate() +} + func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() diff --git a/elements/slider.go b/elements/slider.go index 0810be4..1e6da8e 100644 --- a/elements/slider.go +++ b/elements/slider.go @@ -94,6 +94,10 @@ func (element *Slider) SetEnabled (enabled bool) { element.entity.Invalidate() } +func (element *Slider) HandleFocusChange () { + element.entity.Invalidate() +} + func (element *Slider) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() diff --git a/elements/switch.go b/elements/switch.go index e340e61..70516eb 100644 --- a/elements/switch.go +++ b/elements/switch.go @@ -97,6 +97,10 @@ func (element *Switch) Draw (destination canvas.Canvas) { element.drawer.Draw(destination, foreground, offset) } +func (element *Switch) HandleFocusChange () { + element.entity.Invalidate() +} + func (element *Switch) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() diff --git a/elements/textbox.go b/elements/textbox.go index 7cd7b08..861ed11 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -134,6 +134,10 @@ func (element *TextBox) Draw (destination canvas.Canvas) { } } +func (element *TextBox) HandleFocusChange () { + element.entity.Invalidate() +} + func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() From 0a21f605fb97dd14763b4cb9a5f915f71599e645 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 19:14:44 -0400 Subject: [PATCH 21/41] Added support for horizontal layouts --- elements/containers/{vbox.go => box.go} | 132 +++++++++++++++++------- examples/checkbox/main.go | 12 +-- popups/dialog.go | 24 +++-- 3 files changed, 113 insertions(+), 55 deletions(-) rename elements/containers/{vbox.go => box.go} (52%) diff --git a/elements/containers/vbox.go b/elements/containers/box.go similarity index 52% rename from elements/containers/vbox.go rename to elements/containers/box.go index bab8306..516e9ed 100644 --- a/elements/containers/vbox.go +++ b/elements/containers/box.go @@ -11,27 +11,40 @@ type scratchEntry struct { minimum float64 } -type VBox struct { - entity tomo.ContainerEntity - scratch map[tomo.Element] scratchEntry - theme theme.Wrapped - padding bool - margin bool +// Box is a container that lays out its children horizontally or vertically. +// Child elements can be set to contract to their minimum size, or expand to +// fill remaining space. Boxes can be nested and used together to create more +// complex layouts. +type Box struct { + entity tomo.ContainerEntity + scratch map[tomo.Element] scratchEntry + theme theme.Wrapped + padding bool + margin bool + vertical bool } -func NewVBox (padding, margin bool) (element *VBox) { - element = &VBox { padding: padding, margin: margin } +// NewHBox creates a new horizontal box. +func NewHBox (padding, margin bool) (element *Box) { + element = &Box { padding: padding, margin: margin } element.scratch = make(map[tomo.Element] scratchEntry) - element.theme.Case = tomo.C("tomo", "vBox") + element.theme.Case = tomo.C("tomo", "box") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) return } -func (element *VBox) Entity () tomo.Entity { +// NewHBox creates a new vertical box. +func NewVBox (padding, margin bool) (element *Box) { + element = NewHBox(padding, margin) + element.vertical = true + return +} + +func (element *Box) Entity () tomo.Entity { return element.entity } -func (element *VBox) Draw (destination canvas.Canvas) { +func (element *Box) Draw (destination canvas.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { rocks[index] = element.entity.Child(index).Entity().Bounds() @@ -43,14 +56,20 @@ func (element *VBox) Draw (destination canvas.Canvas) { } } -func (element *VBox) Layout () { +func (element *Box) Layout () { margin := element.theme.Margin(tomo.PatternBackground) padding := element.theme.Padding(tomo.PatternBackground) bounds := element.entity.Bounds() if element.padding { bounds = padding.Apply(bounds) } + var marginSize float64; if element.vertical { + marginSize = float64(margin.Y) + } else { + marginSize = float64(margin.X) + } + freeSpace, nExpanding := element.freeSpace() - expandingElementHeight := freeSpace / nExpanding + expandingElementSize := freeSpace / nExpanding // set the size and position of each element x := float64(bounds.Min.X) @@ -58,23 +77,31 @@ func (element *VBox) Layout () { for index := 0; index < element.entity.CountChildren(); index ++ { entry := element.scratch[element.entity.Child(index)] - var height float64; if entry.expand { - height = expandingElementHeight + var size float64; if entry.expand { + size = expandingElementSize } else { - height = entry.minimum + size = entry.minimum } - element.entity.PlaceChild (index, tomo.Bounds ( - int(x), int(y), - bounds.Dx(), int(height))) - - y += height - if element.margin { y += float64(margin.Y) } + var childBounds image.Rectangle; if element.vertical { + childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size)) + } else { + childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy()) + } + element.entity.PlaceChild(index, childBounds) + + if element.vertical { + y += size + if element.margin { y += marginSize } + } else { + x += size + if element.margin { x += marginSize } + } } } -func (element *VBox) Adopt (child tomo.Element, expand bool) { +func (element *Box) Adopt (child tomo.Element, expand bool) { element.entity.Adopt(child) element.scratch[child] = scratchEntry { expand: expand } element.updateMinimumSize() @@ -82,7 +109,7 @@ func (element *VBox) Adopt (child tomo.Element, expand bool) { element.entity.InvalidateLayout() } -func (element *VBox) Disown (child tomo.Element) { +func (element *Box) Disown (child tomo.Element) { index := element.entity.IndexOf(child) if index < 0 { return } element.entity.Disown(index) @@ -92,7 +119,7 @@ func (element *VBox) Disown (child tomo.Element) { element.entity.InvalidateLayout() } -func (element *VBox) DisownAll () { +func (element *Box) DisownAll () { func () { for index := 0; index < element.entity.CountChildren(); index ++ { index := index @@ -105,18 +132,18 @@ func (element *VBox) DisownAll () { element.entity.InvalidateLayout() } -func (element *VBox) HandleChildMinimumSizeChange (child tomo.Element) { +func (element *Box) HandleChildMinimumSizeChange (child tomo.Element) { element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -func (element *VBox) DrawBackground (destination canvas.Canvas) { +func (element *Box) DrawBackground (destination canvas.Canvas) { element.entity.DrawBackground(destination) } // SetTheme sets the element's theme. -func (element *VBox) SetTheme (theme tomo.Theme) { +func (element *Box) SetTheme (theme tomo.Theme) { if theme == element.theme.Theme { return } element.theme.Theme = theme element.updateMinimumSize() @@ -124,10 +151,21 @@ func (element *VBox) SetTheme (theme tomo.Theme) { element.entity.InvalidateLayout() } -func (element *VBox) freeSpace () (space float64, nExpanding float64) { +func (element *Box) freeSpace () (space float64, nExpanding float64) { margin := element.theme.Margin(tomo.PatternBackground) padding := element.theme.Padding(tomo.PatternBackground) - space = float64(element.entity.Bounds().Dy()) + + var marginSize int; if element.vertical { + marginSize = margin.Y + } else { + marginSize = margin.X + } + + if element.vertical { + space = float64(element.entity.Bounds().Dy()) + } else { + space = float64(element.entity.Bounds().Dx()) + } for _, entry := range element.scratch { if entry.expand { @@ -141,34 +179,50 @@ func (element *VBox) freeSpace () (space float64, nExpanding float64) { space -= float64(padding.Vertical()) } if element.margin { - space -= float64(margin.Y * (len(element.scratch) - 1)) + space -= float64(marginSize * (len(element.scratch) - 1)) } return } -func (element *VBox) updateMinimumSize () { +func (element *Box) updateMinimumSize () { margin := element.theme.Margin(tomo.PatternBackground) padding := element.theme.Padding(tomo.PatternBackground) - var width, height int + var breadth, size int + var marginSize int; if element.vertical { + marginSize = margin.Y + } else { + marginSize = margin.X + } for index := 0; index < element.entity.CountChildren(); index ++ { - childWidth, childHeight := element.entity.ChildMinimumSize(index) + childWidth, childHeight := element.entity.ChildMinimumSize(index) + var childBreadth, childSize int; if element.vertical { + childBreadth, childSize = childWidth, childHeight + } else { + childBreadth, childSize = childHeight, childWidth + } key := element.entity.Child(index) entry := element.scratch[key] - entry.minimum = float64(childHeight) + entry.minimum = float64(childSize) element.scratch[key] = entry - if childWidth > width { - width = childWidth + if childBreadth > breadth { + breadth = childBreadth } - height += childHeight + size += childSize if element.margin && index > 0 { - height += margin.Y + size += marginSize } } + var width, height int; if element.vertical { + width, height = breadth, size + } else { + width, height = size, breadth + } + if element.padding { width += padding.Horizontal() height += padding.Vertical() diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index f161cb6..3bb199f 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -1,7 +1,7 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -// import "git.tebibyte.media/sashakoshka/tomo/popups" +import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -34,11 +34,11 @@ func run () { vsync := elements.NewCheckbox("Enable vsync", false) vsync.OnToggle (func () { if vsync.Value() { - // popups.NewDialog ( - // popups.DialogKindInfo, - // window, - // "Ha!", - // "That doesn't do anything.") + popups.NewDialog ( + popups.DialogKindInfo, + window, + "Ha!", + "That doesn't do anything.") } }) container.Adopt(vsync, false) diff --git a/popups/dialog.go b/popups/dialog.go index 8339c2c..ab9bae3 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -2,7 +2,6 @@ package popups import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -16,6 +15,8 @@ const ( DialogKindError ) +// TODO: add ability to have an icon for buttons + // Button represents a dialog response button. type Button struct { // Name contains the text to display on the button. @@ -42,11 +43,11 @@ func NewDialog ( window, _ = parent.NewModal(image.Rectangle { }) } window.SetTitle(title) + + box := containers.NewVBox(true, true) + messageRow := containers.NewHBox(false, true) + controlRow := containers.NewHBox(false, true) - container := containers.NewContainer(layouts.Dialog { true, true }) - window.Adopt(container) - - messageContainer := containers.NewContainer(layouts.Horizontal { true, false }) iconId := tomo.IconInformation switch kind { case DialogKindInfo: iconId = tomo.IconInformation @@ -55,15 +56,15 @@ func NewDialog ( case DialogKindError: iconId = tomo.IconError } - messageContainer.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false) - messageContainer.Adopt(elements.NewLabel(message, false), true) - container.Adopt(messageContainer, true) + messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false) + messageRow.Adopt(elements.NewLabel(message, false), true) + controlRow.Adopt(elements.NewSpacer(false), true) if len(buttons) == 0 { button := elements.NewButton("OK") button.SetIcon(tomo.IconYes) button.OnClick(window.Close) - container.Adopt(button, false) + controlRow.Adopt(button, false) button.Focus() } else { var button *elements.Button @@ -74,11 +75,14 @@ func NewDialog ( buttonDescriptor.OnPress() window.Close() }) - container.Adopt(button, false) + controlRow.Adopt(button, false) } button.Focus() } + box.Adopt(messageRow, true) + box.Adopt(controlRow, false) + window.Adopt(box) window.Show() return } From e16195d27412e274efd9e6addec8d918b584b493 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 21:49:40 -0400 Subject: [PATCH 22/41] The system can now focus previous, next --- backends/x/entity.go | 21 ++++++++++---------- backends/x/system.go | 47 ++++++++++++++++++++++++++++++++++++++++++-- element.go | 3 +++ 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index 3f3af47..c15a797 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -30,8 +30,10 @@ func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity { func (ent *entity) unlink () { ent.propagate (func (child *entity) bool { + if child.window != nil { + delete(ent.window.system.drawingInvalid, child) + } child.window = nil - delete(ent.window.system.drawingInvalid, child) return true }) @@ -61,17 +63,19 @@ func (ent *entity) setWindow (window *window) { }) } -func (entity *entity) propagate (callback func (*entity) bool) { +func (entity *entity) propagate (callback func (*entity) bool) bool { for _, child := range entity.children { - if !callback(child) { break } - child.propagate(callback) + if !child.propagate(callback) { + return false + } } + return callback(entity) } func (entity *entity) childAt (point image.Point) *entity { for _, child := range entity.children { if point.In(child.bounds) { - return child + return child.childAt(point) } } return entity @@ -190,12 +194,7 @@ func (entity *entity) Focused () bool { func (entity *entity) Focus () { if entity.window == nil { return } - previous := entity.window.focused - entity.window.focused = entity - if previous != nil { - previous.element.(tomo.Focusable).HandleFocusChange() - } - entity.element.(tomo.Focusable).HandleFocusChange() + entity.window.system.focus(entity) } func (entity *entity) FocusNext () { diff --git a/backends/x/system.go b/backends/x/system.go index a9c7187..50b598f 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -62,12 +62,55 @@ func (system *system) SetConfig (config tomo.Config) { }) } +func (system *system) focus (entity *entity) { + previous := system.focused + system.focused = entity + if previous != nil { + previous.element.(tomo.Focusable).HandleFocusChange() + } + if entity != nil { + entity.element.(tomo.Focusable).HandleFocusChange() + } +} + func (system *system) focusNext () { - // TODO + found := system.focused == nil + focused := false + system.propagate (func (entity *entity) bool { + if found { + // looking for the next element to select + child, ok := entity.element.(tomo.Focusable) + if ok && child.Enabled() { + // found it + entity.Focus() + focused = true + return false + } + } else { + // looking for the current focused element + if entity == system.focused { + // found it + found = true + } + } + return true + }) + + if !focused { system.focus(nil) } } func (system *system) focusPrevious () { - // TODO + var behind *entity + system.propagate (func (entity *entity) bool { + if entity == system.focused { + return false + } + + child, ok := entity.element.(tomo.Focusable) + if ok && child.Enabled() { behind = entity } + return true + }) + system.focus(behind) } func (system *system) propagate (callback func (*entity) bool) { diff --git a/element.go b/element.go index 3b4a132..e91f57f 100644 --- a/element.go +++ b/element.go @@ -37,6 +37,9 @@ type Container interface { type Focusable interface { Element + // Enabled returns whether or not the element can currently accept focus. + Enabled () bool + // HandleFocusChange is called when the element is focused or unfocused. HandleFocusChange () } From ed6de3a36f16d34d2943f38325d54fa79ed2baab Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 15 Apr 2023 22:23:08 -0400 Subject: [PATCH 23/41] Got a bunch of examples working --- elements/textbox.go | 2 +- examples/clipboard/main.go | 7 +- examples/dialogLayout/main.go | 30 ---- examples/flow/main.go | 53 +++--- examples/goroutines/main.go | 3 +- examples/{horizontalLayout => hbox}/main.go | 3 +- examples/icons/main.go | 10 +- examples/image/image.go | 20 +-- examples/input/main.go | 3 +- examples/panels/main.go | 5 +- examples/popups/main.go | 6 +- examples/progress/main.go | 3 +- examples/raycaster/game.go | 30 ++-- examples/raycaster/main.go | 7 +- examples/raycaster/raycaster.go | 188 ++++++++++---------- examples/spacer/main.go | 3 +- examples/switch/main.go | 3 +- examples/test/main.go | 17 -- examples/{verticalLayout => vbox}/main.go | 0 popups/dialog.go | 7 +- 20 files changed, 166 insertions(+), 234 deletions(-) delete mode 100644 examples/dialogLayout/main.go rename examples/{horizontalLayout => hbox}/main.go (84%) delete mode 100644 examples/test/main.go rename examples/{verticalLayout => vbox}/main.go (100%) diff --git a/elements/textbox.go b/elements/textbox.go index 861ed11..f4c0106 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -48,7 +48,7 @@ type TextBox struct { // a value. When the value is empty, the placeholder will be displayed in gray // text. func NewTextBox (placeholder, value string) (element *TextBox) { - element = &TextBox { } + element = &TextBox { enabled: true } element.theme.Case = tomo.C("tomo", "textBox") element.entity = tomo.NewEntity(element).(textBoxEntity) element.placeholder = placeholder diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index 40eb0df..a730b50 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -8,7 +8,6 @@ import _ "image/jpeg" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -27,9 +26,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) window.SetTitle("Clipboard") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) textInput := elements.NewTextBox("", "") - controlRow := containers.NewContainer(layouts.Horizontal { true, false }) + controlRow := containers.NewHBox(false, true) copyButton := elements.NewButton("Copy") copyButton.SetIcon(tomo.IconCopy) pasteButton := elements.NewButton("Paste") @@ -123,7 +122,7 @@ func run () { func imageWindow (parent tomo.Window, image image.Image) { window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Clipboard Image") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) closeButton := elements.NewButton("Ok") closeButton.SetIcon(tomo.IconYes) closeButton.OnClick(window.Close) diff --git a/examples/dialogLayout/main.go b/examples/dialogLayout/main.go deleted file mode 100644 index 519ae4a..0000000 --- a/examples/dialogLayout/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" - -func main () { - tomo.Run(run) -} - -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) - window.SetTitle("dialog") - - container := containers.NewContainer(layouts.Dialog { true, true }) - window.Adopt(container) - - container.Adopt(elements.NewLabel("you will explode", false), true) - cancel := elements.NewButton("Cancel") - cancel.SetEnabled(false) - container.Adopt(cancel, false) - okButton := elements.NewButton("OK") - container.Adopt(okButton, false) - okButton.Focus() - - window.OnClose(tomo.Stop) - window.Show() -} diff --git a/examples/flow/main.go b/examples/flow/main.go index 565f614..b7873f7 100644 --- a/examples/flow/main.go +++ b/examples/flow/main.go @@ -2,7 +2,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/flow" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,7 +13,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192)) window.SetTitle("adventure") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) var world flow.Flow @@ -31,13 +30,11 @@ func run () { button2 := elements.NewButton("turn around") button2.OnClick(world.SwitchFunc("bear")) - container.Warp ( func () { - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - container.Adopt(button2, false) - button0.Focus() - }) + container.Adopt(label, true) + container.Adopt(button0, false) + container.Adopt(button1, false) + container.Adopt(button2, false) + button0.Focus() }, "wet": func () { label := elements.NewLabel ( @@ -49,12 +46,10 @@ func run () { button1 := elements.NewButton("exit") button1.OnClick(tomo.Stop) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - button0.Focus() - }) + container.Adopt(label, true) + container.Adopt(button0, false) + container.Adopt(button1, false) + button0.Focus() }, "house": func () { label := elements.NewLabel ( @@ -66,12 +61,10 @@ func run () { button0 := elements.NewButton("turn back") button0.OnClick(world.SwitchFunc("start")) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button1, false) - container.Adopt(button0, false) - button1.Focus() - }) + container.Adopt(label, true) + container.Adopt(button1, false) + container.Adopt(button0, false) + button1.Focus() }, "inside": func () { label := elements.NewLabel ( @@ -84,11 +77,9 @@ func run () { button0 := elements.NewButton("go back outside") button0.OnClick(world.SwitchFunc("house")) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button0, false) - button0.Focus() - }) + container.Adopt(label, true) + container.Adopt(button0, false) + button0.Focus() }, "bear": func () { label := elements.NewLabel ( @@ -100,12 +91,10 @@ func run () { button1 := elements.NewButton("exit") button1.OnClick(tomo.Stop) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - button0.Focus() - }) + container.Adopt(label, true) + container.Adopt(button0, false) + container.Adopt(button1, false) + button0.Focus() }, } world.Switch("start") diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index caa41bb..0d8eb76 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -3,7 +3,6 @@ package main import "os" import "time" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/fun" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -17,7 +16,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) window.SetTitle("Clock") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) clock := fun.NewAnalogClock(time.Now()) diff --git a/examples/horizontalLayout/main.go b/examples/hbox/main.go similarity index 84% rename from examples/horizontalLayout/main.go rename to examples/hbox/main.go index c55a424..100bbdf 100644 --- a/examples/horizontalLayout/main.go +++ b/examples/hbox/main.go @@ -1,7 +1,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,7 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("horizontal stack") - container := containers.NewContainer(layouts.Horizontal { true, true }) + container := containers.NewHBox(true, true) window.Adopt(container) container.Adopt(elements.NewLabel("this is sample text", true), true) diff --git a/examples/icons/main.go b/examples/icons/main.go index 52c45cf..16b3be2 100644 --- a/examples/icons/main.go +++ b/examples/icons/main.go @@ -1,7 +1,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,7 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("Icons") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) container.Adopt(elements.NewLabel("Just some of the wonderful icons we have:", false), false) @@ -24,9 +23,8 @@ func run () { container.Adopt(icons(tomo.IconOpen, tomo.IconRemoveFavorite), true) container.Adopt(icons(tomo.IconCursor, tomo.IconDistort), true) - closeButton := elements.NewButton("Ok") + closeButton := elements.NewButton("Yes verynice") closeButton.SetIcon(tomo.IconYes) - closeButton.ShowText(false) closeButton.OnClick(tomo.Stop) container.Adopt(closeButton, false) @@ -34,8 +32,8 @@ func run () { window.Show() } -func icons (min, max tomo.Icon) (container *containers.Container) { - container = containers.NewContainer(layouts.Horizontal { true, false }) +func icons (min, max tomo.Icon) (container *containers.Box) { + container = containers.NewHBox(false, true) for index := min; index <= max; index ++ { container.Adopt(elements.NewIcon(index, tomo.IconSizeSmall), true) } diff --git a/examples/image/image.go b/examples/image/image.go index 7d78756..5731ac9 100644 --- a/examples/image/image.go +++ b/examples/image/image.go @@ -7,7 +7,6 @@ import _ "image/png" import "github.com/jezek/xgbutil/gopher" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -17,7 +16,7 @@ func main () { } func run () { - window, _ := tomo.NewWindow(2, 2) + window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Tomo Logo") file, err := os.Open("assets/banner.png") @@ -26,16 +25,17 @@ func run () { file.Close() if err != nil { fatalError(window, err); return } - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) logoImage := elements.NewImage(logo) button := elements.NewButton("Show me a gopher instead") - button.OnClick (func () { container.Warp (func () { - container.DisownAll() - gopher, _, err := - image.Decode(bytes.NewReader(gopher.GopherPng())) - if err != nil { fatalError(window, err); return } - container.Adopt(elements.NewImage(gopher),true) - }) }) + button.OnClick (func () { + window.SetTitle("Not the Tomo Logo") + container.DisownAll() + gopher, _, err := + image.Decode(bytes.NewReader(gopher.GopherPng())) + if err != nil { fatalError(window, err); return } + container.Adopt(elements.NewImage(gopher),true) + }) container.Adopt(logoImage, true) container.Adopt(button, false) diff --git a/examples/input/main.go b/examples/input/main.go index 1016493..00577ac 100644 --- a/examples/input/main.go +++ b/examples/input/main.go @@ -2,7 +2,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,7 +13,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Enter Details") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) // create inputs diff --git a/examples/panels/main.go b/examples/panels/main.go index 4997c8f..0d7284b 100644 --- a/examples/panels/main.go +++ b/examples/panels/main.go @@ -3,7 +3,6 @@ package main import "fmt" import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -16,7 +15,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256)) window.SetTitle("Main") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) container.Adopt(elements.NewLabel("Main window", false), true) window.Adopt(container) @@ -33,7 +32,7 @@ func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) { window, _ := parent.NewPanel(bounds) title := fmt.Sprint("Panel #", id) window.SetTitle(title) - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) container.Adopt(elements.NewLabel(title, false), true) window.Adopt(container) window.Show() diff --git a/examples/popups/main.go b/examples/popups/main.go index 414681d..a07e728 100644 --- a/examples/popups/main.go +++ b/examples/popups/main.go @@ -2,7 +2,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -16,7 +15,7 @@ func run () { if err != nil { panic(err.Error()) } window.SetTitle("Dialog Boxes") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) container.Adopt(elements.NewLabel("Try out different dialogs:", false), true) @@ -67,9 +66,10 @@ func run () { menuButton := elements.NewButton("menu") menuButton.OnClick (func () { + // TODO: make a better way to get the bounds of something menu, err := window.NewMenu ( tomo.Bounds(0, 0, 64, 64). - Add(menuButton.Bounds().Min)) + Add(menuButton.Entity().Bounds().Min)) if err != nil { println(err.Error()) } menu.Adopt(elements.NewLabel("I'm a shy window...", true)) menu.Show() diff --git a/examples/progress/main.go b/examples/progress/main.go index 8e0a630..18268f2 100644 --- a/examples/progress/main.go +++ b/examples/progress/main.go @@ -3,7 +3,6 @@ package main import "time" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -15,7 +14,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Approaching") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) container.Adopt (elements.NewLabel ( diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go index 2c66cf3..6c7843e 100644 --- a/examples/raycaster/game.go +++ b/examples/raycaster/game.go @@ -1,9 +1,7 @@ package main import "time" -import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" type Game struct { *Raycaster @@ -31,21 +29,17 @@ func NewGame (world World, textures Textures) (game *Game) { return } -func (game *Game) DrawTo ( - canvas canvas.Canvas, - bounds image.Rectangle, - onDamage func (image.Rectangle), -) { - if canvas == nil { - select { - case game.stopChan <- true: - default: - } - } else if !game.running { - game.running = true - go game.run() - } - game.Raycaster.DrawTo(canvas, bounds, onDamage) +func (game *Game) Start () { + if game.running == true { return } + game.running = true + go game.run() +} + +func (game *Game) Stop () { + select { + case game.stopChan <- true: + default: + } } func (game *Game) Stamina () float64 { @@ -110,7 +104,7 @@ func (game *Game) tick () { game.stamina = 0 } - tomo.Do(game.Draw) + tomo.Do(game.Invalidate) if statUpdate && game.onStatUpdate != nil { tomo.Do(game.onStatUpdate) } diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go index 69256d7..0e741fb 100644 --- a/examples/raycaster/main.go +++ b/examples/raycaster/main.go @@ -5,7 +5,6 @@ import _ "embed" import _ "image/png" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -17,11 +16,13 @@ func main () { tomo.Run(run) } +// FIXME this entire example seems to be broken + func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480)) window.SetTitle("Raycaster") - container := containers.NewContainer(layouts.Vertical { false, false }) + container := containers.NewVBox(false, false) window.Adopt(container) wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes)) @@ -48,7 +49,7 @@ func run () { wallTexture, }) - topBar := containers.NewContainer(layouts.Horizontal { true, true }) + topBar := containers.NewHBox(true, true) staminaBar := elements.NewProgressBar(game.Stamina()) healthBar := elements.NewProgressBar(game.Health()) diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go index bef8070..4b6e902 100644 --- a/examples/raycaster/raycaster.go +++ b/examples/raycaster/raycaster.go @@ -4,10 +4,11 @@ package main import "math" import "image" import "image/color" +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/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/config" type ControlState struct { @@ -21,10 +22,8 @@ type ControlState struct { } type Raycaster struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity + config config.Wrapped Camera @@ -49,31 +48,105 @@ func NewRaycaster (world World, textures Textures) (element *Raycaster) { textures: textures, renderDistance: 8, } - element.Core, element.core = core.NewCore(element, element.drawAll) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.Draw) - element.core.SetMinimumSize(64, 64) + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) + element.entity.SetMinimumSize(64, 64) return } +func (element *Raycaster) Entity () tomo.Entity { + return element.entity +} + +func (element *Raycaster) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + // artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds) + width := bounds.Dx() + height := bounds.Dy() + halfway := bounds.Max.Y - height / 2 + + ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 } + + for x := 0; x < width; x ++ { + ray.X = element.Camera.X + ray.Y = element.Camera.Y + + distance, hitPoint, wall, horizontal := ray.Cast ( + element.world, element.renderDistance) + distance *= math.Cos(ray.Angle - element.Camera.Angle) + textureX := math.Mod(hitPoint.X + hitPoint.Y, 1) + if textureX < 0 { textureX += 1 } + + wallHeight := height + if distance > 0 { + wallHeight = int((float64(height) / 2.0) / float64(distance)) + } + + shade := 1.0 + if horizontal { + shade *= 0.8 + } + shade *= 1 - distance / float64(element.renderDistance) + if shade < 0 { shade = 0 } + + ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF } + floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF } + + // draw + data, stride := destination.Buffer() + wallStart := halfway - wallHeight + wallEnd := halfway + wallHeight + + for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { + switch { + case y < wallStart: + data[y * stride + x + bounds.Min.X] = ceilingColor + + case y < wallEnd: + textureY := + float64(y - halfway) / + float64(wallEnd - wallStart) + 0.5 + // fmt.Println(textureY) + + wallColor := element.textures.At (wall, Vector { + textureX, + textureY, + }) + wallColor = shadeColor(wallColor, shade) + data[y * stride + x + bounds.Min.X] = wallColor + + default: + data[y * stride + x + bounds.Min.X] = floorColor + } + } + + // increment angle + ray.Angle += element.Camera.Fov / float64(width) + } + + // element.drawMinimap() +} + +func (element *Raycaster) Invalidate () { + element.entity.Invalidate() +} + func (element *Raycaster) OnControlStateChange (callback func (ControlState)) { element.onControlStateChange = callback } -func (element *Raycaster) Draw () { - if element.core.HasImage() { - element.drawAll() - element.core.DamageAll() - } +func (element *Raycaster) Focus () { + element.entity.Focus() } +func (element *Raycaster) Enabled () bool { return true } + +func (element *Raycaster) HandleFocusChange () { } + func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) { - if !element.Focused() { element.Focus() } + element.entity.Focus() } func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { } -func (element *Raycaster) HandleMouseMove (x, y int) { } -func (element *Raycaster) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) { switch key { @@ -109,75 +182,6 @@ func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers) } } -func (element *Raycaster) drawAll () { - bounds := element.Bounds() - // artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds) - width := bounds.Dx() - height := bounds.Dy() - halfway := bounds.Max.Y - height / 2 - - ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 } - - for x := 0; x < width; x ++ { - ray.X = element.Camera.X - ray.Y = element.Camera.Y - - distance, hitPoint, wall, horizontal := ray.Cast ( - element.world, element.renderDistance) - distance *= math.Cos(ray.Angle - element.Camera.Angle) - textureX := math.Mod(hitPoint.X + hitPoint.Y, 1) - if textureX < 0 { textureX += 1 } - - wallHeight := height - if distance > 0 { - wallHeight = int((float64(height) / 2.0) / float64(distance)) - } - - shade := 1.0 - if horizontal { - shade *= 0.8 - } - shade *= 1 - distance / float64(element.renderDistance) - if shade < 0 { shade = 0 } - - ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF } - floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF } - - // draw - data, stride := element.core.Buffer() - wallStart := halfway - wallHeight - wallEnd := halfway + wallHeight - - for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - switch { - case y < wallStart: - data[y * stride + x + bounds.Min.X] = ceilingColor - - case y < wallEnd: - textureY := - float64(y - halfway) / - float64(wallEnd - wallStart) + 0.5 - // fmt.Println(textureY) - - wallColor := element.textures.At (wall, Vector { - textureX, - textureY, - }) - wallColor = shadeColor(wallColor, shade) - data[y * stride + x + bounds.Min.X] = wallColor - - default: - data[y * stride + x + bounds.Min.X] = floorColor - } - } - - // increment angle - ray.Angle += element.Camera.Fov / float64(width) - } - - // element.drawMinimap() -} - func shadeColor (c color.RGBA, brightness float64) color.RGBA { return color.RGBA { uint8(float64(c.R) * brightness), @@ -187,8 +191,8 @@ func shadeColor (c color.RGBA, brightness float64) color.RGBA { } } -func (element *Raycaster) drawMinimap () { - bounds := element.Bounds() +func (element *Raycaster) drawMinimap (destination canvas.Canvas) { + bounds := element.entity.Bounds() scale := 8 for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ { for x := 0; x < element.world.Stride; x ++ { @@ -204,7 +208,7 @@ func (element *Raycaster) drawMinimap () { cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF } } shapes.FillColorRectangle ( - element.core, + destination, cellColor, cellBounds.Inset(1)) }} @@ -219,16 +223,16 @@ func (element *Raycaster) drawMinimap () { playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8) shapes.FillColorEllipse ( - element.core, + destination, artist.Hex(0xFFFFFFFF), playerBounds) shapes.ColorLine ( - element.core, + destination, artist.Hex(0xFFFFFFFF), 1, playerPt, playerAnglePt) shapes.ColorLine ( - element.core, + destination, artist.Hex(0x00FF00FF), 1, playerPt, hitPt) diff --git a/examples/spacer/main.go b/examples/spacer/main.go index 92d6855..ba20293 100644 --- a/examples/spacer/main.go +++ b/examples/spacer/main.go @@ -1,7 +1,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,7 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Spaced Out") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) container.Adopt (elements.NewLabel("This is at the top", false), false) diff --git a/examples/switch/main.go b/examples/switch/main.go index 2c7ccda..66a9413 100644 --- a/examples/switch/main.go +++ b/examples/switch/main.go @@ -1,7 +1,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,7 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Switches") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) container.Adopt(elements.NewSwitch("hahahah", false), false) diff --git a/examples/test/main.go b/examples/test/main.go deleted file mode 100644 index d4c352f..0000000 --- a/examples/test/main.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" - -func main () { - tomo.Run(run) -} - -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128)) - window.SetTitle("hellorld!") - window.Adopt(testing.NewMouse()) - window.OnClose(tomo.Stop) - window.Show() -} diff --git a/examples/verticalLayout/main.go b/examples/vbox/main.go similarity index 100% rename from examples/verticalLayout/main.go rename to examples/vbox/main.go diff --git a/popups/dialog.go b/popups/dialog.go index ab9bae3..514ea22 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -60,6 +60,10 @@ func NewDialog ( messageRow.Adopt(elements.NewLabel(message, false), true) controlRow.Adopt(elements.NewSpacer(false), true) + box.Adopt(messageRow, true) + box.Adopt(controlRow, false) + window.Adopt(box) + if len(buttons) == 0 { button := elements.NewButton("OK") button.SetIcon(tomo.IconYes) @@ -80,9 +84,6 @@ func NewDialog ( button.Focus() } - box.Adopt(messageRow, true) - box.Adopt(controlRow, false) - window.Adopt(box) window.Show() return } From b9c835067775aed89a3cb2aed058273025cf2843 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 16 Apr 2023 03:37:28 -0400 Subject: [PATCH 24/41] Scroll containers yay --- backends/x/selection.go | 2 + backends/x/system.go | 10 +- element.go | 15 +- elements/containers/box.go | 1 - elements/containers/notdone/scroll.go | 332 -------------------------- elements/containers/scroll.go | 197 +++++++++++++++ elements/textbox.go | 9 +- entity.go | 16 +- examples/scroll/main.go | 79 +++--- 9 files changed, 272 insertions(+), 389 deletions(-) delete mode 100644 elements/containers/notdone/scroll.go create mode 100644 elements/containers/scroll.go diff --git a/backends/x/selection.go b/backends/x/selection.go index 886dc2c..befe208 100644 --- a/backends/x/selection.go +++ b/backends/x/selection.go @@ -97,11 +97,13 @@ func (request *selectionRequest) convertSelection ( func (request *selectionRequest) die (err error) { request.callback(nil, err) + request.window.system.afterEvent() request.state = selReqStateClosed } func (request *selectionRequest) finalize (data data.Data) { request.callback(data, nil) + request.window.system.afterEvent() request.state = selReqStateClosed } diff --git a/backends/x/system.go b/backends/x/system.go index 50b598f..84ffa23 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -141,11 +141,13 @@ func (system *system) afterEvent () { } func (system *system) layout (entity *entity, force bool) { - if entity == nil || !entity.isContainer { return } + if entity == nil { return } if entity.layoutInvalid == true || force { - entity.element.(tomo.Container).Layout() - entity.layoutInvalid = false - force = true + if element, ok := entity.element.(tomo.Layoutable); ok { + element.Layout() + entity.layoutInvalid = false + force = true + } } for _, child := range entity.children { diff --git a/element.go b/element.go index e91f57f..0b99b04 100644 --- a/element.go +++ b/element.go @@ -15,12 +15,19 @@ type Element interface { Entity () Entity } -// Container is an element capable of containing child elements. +// Layoutable represents an element that needs to perform layout calculations +// before it can draw itself. +type Layoutable interface { + Element + + // Layout causes this element to perform a layout operation. + Layout () +} + +// Container represents an element capable of containing child elements. type Container interface { Element - - // Layout causes this element to arrange its children. - Layout () + Layoutable // DrawBackground causes the element to draw its background pattern to // the specified canvas. The bounds of this canvas specify the area that diff --git a/elements/containers/box.go b/elements/containers/box.go index 516e9ed..9e67f65 100644 --- a/elements/containers/box.go +++ b/elements/containers/box.go @@ -98,7 +98,6 @@ func (element *Box) Layout () { if element.margin { x += marginSize } } } - } func (element *Box) Adopt (child tomo.Element, expand bool) { diff --git a/elements/containers/notdone/scroll.go b/elements/containers/notdone/scroll.go deleted file mode 100644 index f523d4b..0000000 --- a/elements/containers/notdone/scroll.go +++ /dev/null @@ -1,332 +0,0 @@ -package containers - -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/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// ScrollContainer is a container that is capable of holding a scrollable -// element. -type ScrollContainer struct { - *core.Core - *core.Propagator - core core.CoreControl - - child tomo.Scrollable - horizontal *elements.ScrollBar - vertical *elements.ScrollBar - - config config.Wrapped - theme theme.Wrapped - - onFocusRequest func () (granted bool) - onFocusMotionRequest func (input.KeynavDirection) (granted bool) -} - -// NewScrollContainer creates a new scroll container with the specified scroll -// bars. -func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { - element = &ScrollContainer { } - element.theme.Case = tomo.C("tomo", "scrollContainer") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - - if horizontal { - element.horizontal = elements.NewScrollBar(false) - element.setUpChild(element.horizontal) - element.horizontal.OnScroll (func (viewport image.Point) { - if element.child != nil { - element.child.ScrollTo(viewport) - } - if element.vertical != nil { - element.vertical.SetBounds ( - element.child.ScrollContentBounds(), - element.child.ScrollViewportBounds()) - } - }) - } - if vertical { - element.vertical = elements.NewScrollBar(true) - element.setUpChild(element.vertical) - element.vertical.OnScroll (func (viewport image.Point) { - if element.child != nil { - element.child.ScrollTo(viewport) - } - if element.horizontal != nil { - element.horizontal.SetBounds ( - element.child.ScrollContentBounds(), - element.child.ScrollViewportBounds()) - } - }) - } - return -} - - -// Adopt adds a scrollable element to the scroll container. The container can -// only contain one scrollable element at a time, and when a new one is adopted -// it replaces the last one. -func (element *ScrollContainer) Adopt (child tomo.Scrollable) { - // disown previous child if it exists - if element.child != nil { - element.disownChild(child) - } - - // adopt new child - element.child = child - if child != nil { - element.setUpChild(child) - } - - element.updateEnabled() - element.updateMinimumSize() - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -func (element *ScrollContainer) setUpChild (child tomo.Element) { - child.SetParent(element) - if child, ok := child.(tomo.Themeable); ok { - child.SetTheme(element.theme.Theme) - } - if child, ok := child.(tomo.Configurable); ok { - child.SetConfig(element.config.Config) - } -} - -func (element *ScrollContainer) disownChild (child tomo.Scrollable) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} - -func (element *ScrollContainer) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *ScrollContainer) NotifyMinimumSizeChange (child tomo.Element) { - element.redoAll() - element.core.DamageAll() -} - -// NotifyScrollBoundsChange notifies the container that the scroll bounds or -// axes of a child have changed. -func (element *ScrollContainer) NotifyScrollBoundsChange (child tomo.Scrollable) { - element.updateEnabled() - viewportBounds := element.child.ScrollViewportBounds() - contentBounds := element.child.ScrollContentBounds() - if element.horizontal != nil { - element.horizontal.SetBounds(contentBounds, viewportBounds) - } - if element.vertical != nil { - element.vertical.SetBounds(contentBounds, viewportBounds) - } -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *ScrollContainer) DrawBackground (bounds image.Rectangle) { - element.core.DrawBackgroundBounds ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - bounds) -} - -// SetTheme sets the element's theme. -func (element *ScrollContainer) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.updateMinimumSize() - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *ScrollContainer) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.updateMinimumSize() - element.redoAll() -} - -func (element *ScrollContainer) HandleScroll ( - x, y int, - deltaX, deltaY float64, -) { - horizontal, vertical := element.child.ScrollAxes() - if !horizontal { deltaX = 0 } - if !vertical { deltaY = 0 } - element.scrollChildBy(int(deltaX), int(deltaY)) -} - -// HandleKeyDown is called when a key is pressed down or repeated while -// this element has keyboard focus. It is important to note that not -// every key down event is guaranteed to be paired with exactly one key -// up event. This is the reason a list of modifier keys held down at the -// time of the key press is given. -func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - switch key { - case input.KeyPageUp: - viewport := element.child.ScrollViewportBounds() - element.HandleScroll(0, 0, 0, float64(-viewport.Dy())) - case input.KeyPageDown: - viewport := element.child.ScrollViewportBounds() - element.HandleScroll(0, 0, 0, float64(viewport.Dy())) - default: - element.Propagator.HandleKeyDown(key, modifiers) - } -} - -// HandleKeyUp is called when a key is released while this element has -// keyboard focus. -func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) { } - -// CountChildren returns the amount of children contained within this element. -func (element *ScrollContainer) CountChildren () (count int) { - return 3 -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *ScrollContainer) Child (index int) (child tomo.Element) { - switch index { - case 0: return element.child - case 1: - if element.horizontal == nil { - return nil - } else { - return element.horizontal - } - case 2: - if element.vertical == nil { - return nil - } else { - return element.vertical - } - default: return nil - } -} - -func (element *ScrollContainer) redoAll () { - if !element.core.HasImage() { return } - - zr := image.Rectangle { } - if element.child != nil { element.child.DrawTo(nil, zr, nil) } - if element.horizontal != nil { element.horizontal.DrawTo(nil, zr, nil) } - if element.vertical != nil { element.vertical.DrawTo(nil, zr, nil) } - - childBounds, horizontalBounds, verticalBounds := element.layout() - if element.child != nil { - element.child.DrawTo ( - canvas.Cut(element.core, childBounds), - childBounds, element.childDamageCallback) - } - if element.horizontal != nil { - element.horizontal.DrawTo ( - canvas.Cut(element.core, horizontalBounds), - horizontalBounds, element.childDamageCallback) - } - if element.vertical != nil { - element.vertical.DrawTo ( - canvas.Cut(element.core, verticalBounds), - verticalBounds, element.childDamageCallback) - } - element.draw() -} - -func (element *ScrollContainer) scrollChildBy (x, y int) { - if element.child == nil { return } - scrollPoint := - element.child.ScrollViewportBounds().Min. - Add(image.Pt(x, y)) - element.child.ScrollTo(scrollPoint) -} - -func (element *ScrollContainer) childDamageCallback (region image.Rectangle) { - element.core.DamageRegion(region) -} - -func (element *ScrollContainer) layout () ( - child image.Rectangle, - horizontal image.Rectangle, - vertical image.Rectangle, -) { - bounds := element.Bounds() - child = bounds - - if element.horizontal != nil { - _, hMinHeight := element.horizontal.MinimumSize() - child.Max.Y -= hMinHeight - } - if element.vertical != nil { - vMinWidth, _ := element.vertical.MinimumSize() - child.Max.X -= vMinWidth - } - - vertical.Min.X = child.Max.X - vertical.Max.X = bounds.Max.X - vertical.Min.Y = bounds.Min.Y - vertical.Max.Y = child.Max.Y - - horizontal.Min.X = bounds.Min.X - horizontal.Max.X = child.Max.X - horizontal.Min.Y = child.Max.Y - horizontal.Max.Y = bounds.Max.Y - return -} - -func (element *ScrollContainer) draw () { - if element.horizontal != nil && element.vertical != nil { - bounds := element.Bounds() - bounds.Min = image.Pt ( - bounds.Max.X - element.vertical.Bounds().Dx(), - bounds.Max.Y - element.horizontal.Bounds().Dy()) - state := tomo.State { } - deadArea := element.theme.Pattern(tomo.PatternDead, state) - deadArea.Draw(canvas.Cut(element.core, bounds), bounds) - } -} - -func (element *ScrollContainer) updateMinimumSize () { - var width, height int - - if element.child != nil { - width, height = element.child.MinimumSize() - } - if element.horizontal != nil { - hMinWidth, hMinHeight := element.horizontal.MinimumSize() - height += hMinHeight - if hMinWidth > width { - width = hMinWidth - } - } - if element.vertical != nil { - vMinWidth, vMinHeight := element.vertical.MinimumSize() - width += vMinWidth - if vMinHeight > height { - height = vMinHeight - } - } - element.core.SetMinimumSize(width, height) -} - -func (element *ScrollContainer) updateEnabled () { - horizontal, vertical := element.child.ScrollAxes() - if element.horizontal != nil { - element.horizontal.SetEnabled(horizontal) - } - if element.vertical != nil { - element.vertical.SetEnabled(vertical) - } -} diff --git a/elements/containers/scroll.go b/elements/containers/scroll.go new file mode 100644 index 0000000..5c420c4 --- /dev/null +++ b/elements/containers/scroll.go @@ -0,0 +1,197 @@ +package containers + +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/elements" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" + +type Scroll struct { + entity tomo.ContainerEntity + + child tomo.Scrollable + horizontal *elements.ScrollBar + vertical *elements.ScrollBar + + config config.Wrapped + theme theme.Wrapped +} + +func NewScroll (horizontal, vertical bool) (element *Scroll) { + element = &Scroll { } + element.theme.Case = tomo.C("tomo", "scroll") + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + + if horizontal { + element.horizontal = elements.NewScrollBar(false) + element.horizontal.OnScroll (func (viewport image.Point) { + if element.child != nil { + element.child.ScrollTo(viewport) + } + if element.vertical != nil { + element.vertical.SetBounds ( + element.child.ScrollContentBounds(), + element.child.ScrollViewportBounds()) + } + }) + element.entity.Adopt(element.horizontal) + } + if vertical { + element.vertical = elements.NewScrollBar(true) + element.vertical.OnScroll (func (viewport image.Point) { + if element.child != nil { + element.child.ScrollTo(viewport) + } + if element.horizontal != nil { + element.horizontal.SetBounds ( + element.child.ScrollContentBounds(), + element.child.ScrollViewportBounds()) + } + }) + element.entity.Adopt(element.vertical) + } + return +} + +func (element *Scroll) Entity () tomo.Entity { + return element.entity +} + +func (element *Scroll) Draw (destination canvas.Canvas) { + if element.horizontal != nil && element.vertical != nil { + bounds := element.entity.Bounds() + bounds.Min = image.Pt ( + bounds.Max.X - element.vertical.Entity().Bounds().Dx(), + bounds.Max.Y - element.horizontal.Entity().Bounds().Dy()) + state := tomo.State { } + deadArea := element.theme.Pattern(tomo.PatternDead, state) + deadArea.Draw(canvas.Cut(destination, bounds), bounds) + } +} + +func (element *Scroll) Layout () { + bounds := element.entity.Bounds() + child := bounds + + iHorizontal := element.entity.IndexOf(element.horizontal) + iVertical := element.entity.IndexOf(element.vertical) + iChild := element.entity.IndexOf(element.child) + + var horizontal, vertical image.Rectangle + + if element.horizontal != nil { + _, hMinHeight := element.entity.ChildMinimumSize(iHorizontal) + child.Max.Y -= hMinHeight + } + if element.vertical != nil { + vMinWidth, _ := element.entity.ChildMinimumSize(iVertical) + child.Max.X -= vMinWidth + } + + horizontal.Min.X = bounds.Min.X + horizontal.Max.X = child.Max.X + horizontal.Min.Y = child.Max.Y + horizontal.Max.Y = bounds.Max.Y + + vertical.Min.X = child.Max.X + vertical.Max.X = bounds.Max.X + vertical.Min.Y = bounds.Min.Y + vertical.Max.Y = child.Max.Y + + if element.horizontal != nil { + element.entity.PlaceChild (iHorizontal, horizontal) + } + if element.vertical != nil { + element.entity.PlaceChild(iVertical, vertical) + } + if element.child != nil { + element.entity.PlaceChild(iChild, child) + } +} + +func (element *Scroll) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +func (element *Scroll) Adopt (child tomo.Scrollable) { + if element.child != nil { + element.entity.Disown(element.entity.IndexOf(element.child)) + } + if child != nil { + element.entity.Adopt(child) + } + element.child = child + + element.updateEnabled() + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) { + element.updateEnabled() + viewportBounds := element.child.ScrollViewportBounds() + contentBounds := element.child.ScrollContentBounds() + if element.horizontal != nil { + element.horizontal.SetBounds(contentBounds, viewportBounds) + } + if element.vertical != nil { + element.vertical.SetBounds(contentBounds, viewportBounds) + } +} + +func (element *Scroll) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Scroll) SetConfig (config tomo.Config) { + element.config.Config = config +} + +func (element *Scroll) updateMinimumSize () { + var width, height int + + if element.child != nil { + width, height = element.entity.ChildMinimumSize ( + element.entity.IndexOf(element.child)) + } + if element.horizontal != nil { + hMinWidth, hMinHeight := element.entity.ChildMinimumSize ( + element.entity.IndexOf(element.horizontal)) + height += hMinHeight + if hMinWidth > width { + width = hMinWidth + } + } + if element.vertical != nil { + vMinWidth, vMinHeight := element.entity.ChildMinimumSize ( + element.entity.IndexOf(element.vertical)) + width += vMinWidth + if vMinHeight > height { + height = vMinHeight + } + } + element.entity.SetMinimumSize(width, height) +} + +func (element *Scroll) updateEnabled () { + horizontal, vertical := element.child.ScrollAxes() + if element.horizontal != nil { + element.horizontal.SetEnabled(horizontal) + } + if element.vertical != nil { + element.vertical.SetEnabled(vertical) + } +} diff --git a/elements/textbox.go b/elements/textbox.go index f4c0106..5f1ce2f 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -18,6 +18,7 @@ import "git.tebibyte.media/sashakoshka/tomo/default/config" type textBoxEntity interface { tomo.FocusableEntity tomo.ScrollableEntity + tomo.LayoutEntity } // TextBox is a single-line text input. @@ -72,7 +73,6 @@ func (element *TextBox) Entity () tomo.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() state := element.state() pattern := element.theme.Pattern(tomo.PatternInput, state) @@ -134,6 +134,11 @@ func (element *TextBox) Draw (destination canvas.Canvas) { } } +// Layout causes the element to perform a layout operation. +func (element *TextBox) Layout () { + element.scrollToCursor() +} + func (element *TextBox) HandleFocusChange () { element.entity.Invalidate() } @@ -497,8 +502,8 @@ func (element *TextBox) scrollToCursor () { } else if cursorPosition.X < minX { element.scroll -= minX - cursorPosition.X if element.scroll < 0 { element.scroll = 0 } - element.entity.Invalidate() element.entity.NotifyScrollBoundsChange() + element.entity.Invalidate() } } diff --git a/entity.go b/entity.go index 4bda6d9..dfdede7 100644 --- a/entity.go +++ b/entity.go @@ -31,14 +31,20 @@ type Entity interface { DrawBackground (canvas.Canvas) } +// LayoutEntity is given to elements that support the Layoutable interface. +type LayoutEntity interface { + Entity + + // InvalidateLayout marks the element's layout as invalid. At the end of + // every event, the backend will ask all invalid elements to recalculate + // their layouts. + InvalidateLayout () +} + // ContainerEntity is given to elements that support the Container interface. type ContainerEntity interface { Entity - - // InvalidateLayout marks the element's layout as invalid. At the end of - // every event, the backend will ask all invalid containers to - // recalculate their layouts. - InvalidateLayout () + LayoutEntity // Adopt adds an element as a child. Adopt (child Element) diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 2436bd9..b01a887 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -1,8 +1,7 @@ package main -import "image" +// import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" import "git.tebibyte.media/sashakoshka/tomo/elements/containers" @@ -14,58 +13,56 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240)) window.SetTitle("Scroll") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := containers.NewVBox(true, true) window.Adopt(container) textBox := elements.NewTextBox("", copypasta) - scrollContainer := containers.NewScrollContainer(true, false) + scrollContainer := containers.NewScroll(true, false) - disconnectedContainer := containers.NewContainer (layouts.Horizontal { - Gap: true, - }) - list := elements.NewList ( - elements.NewListEntry("This is list item 0", nil), - elements.NewListEntry("This is list item 1", nil), - elements.NewListEntry("This is list item 2", nil), - elements.NewListEntry("This is list item 3", nil), - elements.NewListEntry("This is list item 4", nil), - elements.NewListEntry("This is list item 5", nil), - elements.NewListEntry("This is list item 6", nil), - elements.NewListEntry("This is list item 7", nil), - elements.NewListEntry("This is list item 8", nil), - elements.NewListEntry("This is list item 9", nil), - elements.NewListEntry("This is list item 10", nil), - elements.NewListEntry("This is list item 11", nil), - elements.NewListEntry("This is list item 12", nil), - elements.NewListEntry("This is list item 13", nil), - elements.NewListEntry("This is list item 14", nil), - elements.NewListEntry("This is list item 15", nil), - elements.NewListEntry("This is list item 16", nil), - elements.NewListEntry("This is list item 17", nil), - elements.NewListEntry("This is list item 18", nil), - elements.NewListEntry("This is list item 19", nil), - elements.NewListEntry("This is list item 20", nil)) - list.Collapse(0, 32) - scrollBar := elements.NewScrollBar(true) - list.OnScrollBoundsChange (func () { - scrollBar.SetBounds ( - list.ScrollContentBounds(), - list.ScrollViewportBounds()) - }) - scrollBar.OnScroll (func (viewport image.Point) { - list.ScrollTo(viewport) - }) + disconnectedContainer := containers.NewHBox(false, true) + // list := elements.NewList ( + // elements.NewListEntry("This is list item 0", nil), + // elements.NewListEntry("This is list item 1", nil), + // elements.NewListEntry("This is list item 2", nil), + // elements.NewListEntry("This is list item 3", nil), + // elements.NewListEntry("This is list item 4", nil), + // elements.NewListEntry("This is list item 5", nil), + // elements.NewListEntry("This is list item 6", nil), + // elements.NewListEntry("This is list item 7", nil), + // elements.NewListEntry("This is list item 8", nil), + // elements.NewListEntry("This is list item 9", nil), + // elements.NewListEntry("This is list item 10", nil), + // elements.NewListEntry("This is list item 11", nil), + // elements.NewListEntry("This is list item 12", nil), + // elements.NewListEntry("This is list item 13", nil), + // elements.NewListEntry("This is list item 14", nil), + // elements.NewListEntry("This is list item 15", nil), + // elements.NewListEntry("This is list item 16", nil), + // elements.NewListEntry("This is list item 17", nil), + // elements.NewListEntry("This is list item 18", nil), + // elements.NewListEntry("This is list item 19", nil), + // elements.NewListEntry("This is list item 20", nil)) + // list.Collapse(0, 32) + // scrollBar := elements.NewScrollBar(true) + // list.OnScrollBoundsChange (func () { + // scrollBar.SetBounds ( + // list.ScrollContentBounds(), + // list.ScrollViewportBounds()) + // }) + // scrollBar.OnScroll (func (viewport image.Point) { + // list.ScrollTo(viewport) + // }) scrollContainer.Adopt(textBox) container.Adopt(elements.NewLabel("A ScrollContainer:", false), false) container.Adopt(scrollContainer, false) - disconnectedContainer.Adopt(list, false) + // disconnectedContainer.Adopt(list, false) disconnectedContainer.Adopt (elements.NewLabel ( "Notice how the scroll bar to the right can be used to " + "control the list, despite not even touching it. It is " + "indeed a thing you can do. It is also terrible UI design so " + "don't do it.", true), true) - disconnectedContainer.Adopt(scrollBar, false) + // disconnectedContainer.Adopt(scrollBar, false) container.Adopt(disconnectedContainer, true) window.OnClose(tomo.Stop) From 7d4ddaf387f98a6c57697aea6cad1d309e5d8761 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 16 Apr 2023 14:12:55 -0400 Subject: [PATCH 25/41] Scrolling over a ScrollContainer will now scroll it --- backends/x/entity.go | 15 +++++++++++++++ backends/x/event.go | 20 +++++++++++--------- backends/x/system.go | 5 +++++ elements/containers/scroll.go | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index c15a797..bc9c7b0 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -81,6 +81,21 @@ func (entity *entity) childAt (point image.Point) *entity { return entity } +func (entity *entity) scrollTargetChildAt (point image.Point) *entity { + for _, child := range entity.children { + if point.In(child.bounds) { + result := child.scrollTargetChildAt(point) + if result != nil { return result } + break + } + } + + if _, ok := entity.element.(tomo.ScrollTarget); ok { + return entity + } + return nil +} + // ----------- Entity ----------- // func (entity *entity) Invalidate () { diff --git a/backends/x/event.go b/backends/x/event.go index 73ea3c1..2f573ba 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -190,20 +190,22 @@ func (window *window) handleButtonPress ( insideWindow := point.In(window.canvas.Bounds()) scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 - underneath := window.system.childAt(point) - if !insideWindow && window.shy && !scrolling { window.Close() } else if scrolling { - if child, ok := underneath.element.(tomo.ScrollTarget); ok { - sum := scrollSum { } - sum.add(buttonEvent.Detail, window, buttonEvent.State) - window.compressScrollSum(buttonEvent, &sum) - child.HandleScroll ( - point.X, point.Y, - float64(sum.x), float64(sum.y)) + underneath := window.system.scrollTargetChildAt(point) + if underneath != nil { + if child, ok := underneath.element.(tomo.ScrollTarget); ok { + sum := scrollSum { } + sum.add(buttonEvent.Detail, window, buttonEvent.State) + window.compressScrollSum(buttonEvent, &sum) + child.HandleScroll ( + point.X, point.Y, + float64(sum.x), float64(sum.y)) + } } } else { + underneath := window.system.childAt(point) if child, ok := underneath.element.(tomo.MouseTarget); ok { window.system.drags[buttonEvent.Detail] = child child.HandleMouseDown ( diff --git a/backends/x/system.go b/backends/x/system.go index 84ffa23..78a43b7 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -123,6 +123,11 @@ func (system *system) childAt (point image.Point) *entity { return system.child.childAt(point) } +func (system *system) scrollTargetChildAt (point image.Point) *entity { + if system.child == nil { return nil } + return system.child.scrollTargetChildAt(point) +} + func (system *system) resizeChildToFit () { system.child.bounds = system.canvas.Bounds() system.child.clippedBounds = system.child.bounds diff --git a/elements/containers/scroll.go b/elements/containers/scroll.go index 5c420c4..b83fc47 100644 --- a/elements/containers/scroll.go +++ b/elements/containers/scroll.go @@ -148,6 +148,16 @@ func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) { } } +func (element *Scroll) HandleScroll ( + x, y int, + deltaX, deltaY float64, +) { + horizontal, vertical := element.child.ScrollAxes() + if !horizontal { deltaX = 0 } + if !vertical { deltaY = 0 } + element.scrollChildBy(int(deltaX), int(deltaY)) +} + func (element *Scroll) SetTheme (theme tomo.Theme) { if theme == element.theme.Theme { return } element.theme.Theme = theme @@ -195,3 +205,11 @@ func (element *Scroll) updateEnabled () { element.vertical.SetEnabled(vertical) } } + +func (element *Scroll) scrollChildBy (x, y int) { + if element.child == nil { return } + scrollPoint := + element.child.ScrollViewportBounds().Min. + Add(image.Pt(x, y)) + element.child.ScrollTo(scrollPoint) +} From a7de6c7f3bb1b8f4649ae9daaeedc283a5f045de Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 16 Apr 2023 16:48:32 -0400 Subject: [PATCH 26/41] Document works now --- elements/containers/box.go | 11 +- elements/containers/document.go | 235 +++++++++++++++++++++++++++++ examples/align/main.go | 6 +- examples/documentContainer/main.go | 4 +- 4 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 elements/containers/document.go diff --git a/elements/containers/box.go b/elements/containers/box.go index 9e67f65..d8b2862 100644 --- a/elements/containers/box.go +++ b/elements/containers/box.go @@ -7,8 +7,9 @@ import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/default/theme" type scratchEntry struct { - expand bool - minimum float64 + expand bool + minSize float64 + minBreadth float64 } // Box is a container that lays out its children horizontally or vertically. @@ -80,7 +81,7 @@ func (element *Box) Layout () { var size float64; if entry.expand { size = expandingElementSize } else { - size = entry.minimum + size = entry.minSize } var childBounds image.Rectangle; if element.vertical { @@ -170,7 +171,7 @@ func (element *Box) freeSpace () (space float64, nExpanding float64) { if entry.expand { nExpanding ++; } else { - space -= float64(entry.minimum) + space -= float64(entry.minSize) } } @@ -204,7 +205,7 @@ func (element *Box) updateMinimumSize () { key := element.entity.Child(index) entry := element.scratch[key] - entry.minimum = float64(childSize) + entry.minSize = float64(childSize) element.scratch[key] = entry if childBreadth > breadth { diff --git a/elements/containers/document.go b/elements/containers/document.go new file mode 100644 index 0000000..9bc5f2d --- /dev/null +++ b/elements/containers/document.go @@ -0,0 +1,235 @@ +package containers + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +type documentEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + +type Document struct { + entity documentEntity + + scratch map[tomo.Element] scratchEntry + scroll image.Point + contentBounds image.Rectangle + + theme theme.Wrapped + + onScrollBoundsChange func () +} + +func NewDocument () (element *Document) { + element = &Document { } + element.scratch = make(map[tomo.Element] scratchEntry) + element.theme.Case = tomo.C("tomo", "document") + element.entity = tomo.NewEntity(element).(documentEntity) + return +} + +func (element *Document) Entity () tomo.Entity { + return element.entity +} + +func (element *Document) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.entity.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *Document) Layout () { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + if dot.X > xStart && entry.expand { + nextLine() + } + + width := int(entry.minBreadth) + height := int(entry.minSize) + if width + dot.X > bounds.Dx() && !entry.expand { + nextLine() + } + if width < bounds.Dx() && entry.expand { + width = bounds.Dx() + } + if typedChild, ok := child.(tomo.Flexible); ok { + height = typedChild.FlexibleHeightFor(width) + } + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + if entry.expand { + nextLine() + } else { + dot.X += width + margin.X + } + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *Document) Adopt (child tomo.Element, expand bool) { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: expand } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) Disown (child tomo.Element) { + index := element.entity.IndexOf(child) + if index < 0 { return } + element.entity.Disown(index) + delete(element.scratch, child) + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) DisownAll () { + func () { + for index := 0; index < element.entity.CountChildren(); index ++ { + index := index + defer element.entity.Disown(index) + } + } () + element.scratch = make(map[tomo.Element] scratchEntry) + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) HandleChildMinimumSizeChange (child tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +// SetTheme sets the element's theme. +func (element *Document) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + + +// ScrollContentBounds returns the full content size of the element. +func (element *Document) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *Document) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternBackground) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds +} + +// ScrollTo scrolls the viewport to the specified point relative to +// ScrollBounds. +func (element *Document) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *Document) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *Document) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +func (element *Document) maxScrollHeight () (height int) { + padding := element.theme.Padding(tomo.PatternSunken) + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return +} + +func (element *Document) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternBackground) + minimumWidth := 0 + for index := 0; index < element.entity.CountChildren(); index ++ { + width, height := element.entity.ChildMinimumSize(index) + if width > minimumWidth { + minimumWidth = width + } + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minSize = float64(height) + entry.minBreadth = float64(width) + element.scratch[key] = entry + } + element.entity.SetMinimumSize ( + minimumWidth + padding.Horizontal(), + padding.Vertical()) +} diff --git a/examples/align/main.go b/examples/align/main.go index 1d0c77d..8c86d98 100644 --- a/examples/align/main.go +++ b/examples/align/main.go @@ -11,11 +11,11 @@ func main () { } func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) + window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256)) window.SetTitle("Text alignment") - container := containers.NewDocumentContainer() - scrollContainer := containers.NewScrollContainer(false, true) + container := containers.NewDocument() + scrollContainer := containers.NewScroll(false, true) scrollContainer.Adopt(container) window.Adopt(scrollContainer) diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go index 37a2a3f..7d04983 100644 --- a/examples/documentContainer/main.go +++ b/examples/documentContainer/main.go @@ -22,8 +22,8 @@ func run () { file.Close() if err != nil { panic(err.Error()); return } - scrollContainer := containers.NewScrollContainer(false, true) - document := containers.NewDocumentContainer() + scrollContainer := containers.NewScroll(false, true) + document := containers.NewDocument() document.Adopt (elements.NewLabel ( "A document container is a vertically stacked container " + From 775390e88417d9e966c0a556e39ed0606001f95b Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 16 Apr 2023 17:30:13 -0400 Subject: [PATCH 27/41] Containers are no longer in their own dir because why were they --- elements/{containers => }/box.go | 2 +- elements/containers/notdone/document.go | 368 --------------------- elements/{containers => }/document.go | 2 +- elements/{containers => }/notdone/table.go | 0 elements/{containers => }/scroll.go | 11 +- examples/align/main.go | 3 +- examples/checkbox/main.go | 3 +- examples/clipboard/main.go | 5 +- examples/documentContainer/main.go | 5 +- examples/flow/main.go | 3 +- examples/goroutines/main.go | 3 +- examples/hbox/main.go | 3 +- examples/icons/main.go | 3 +- examples/input/main.go | 3 +- examples/panels/main.go | 3 +- examples/popups/main.go | 3 +- examples/progress/main.go | 3 +- examples/raycaster/main.go | 3 +- examples/scroll/main.go | 3 +- examples/spacer/main.go | 3 +- examples/switch/main.go | 3 +- examples/vbox/main.go | 3 +- popups/dialog.go | 7 +- 23 files changed, 29 insertions(+), 416 deletions(-) rename elements/{containers => }/box.go (99%) delete mode 100644 elements/containers/notdone/document.go rename elements/{containers => }/document.go (99%) rename elements/{containers => }/notdone/table.go (100%) rename elements/{containers => }/scroll.go (95%) diff --git a/elements/containers/box.go b/elements/box.go similarity index 99% rename from elements/containers/box.go rename to elements/box.go index d8b2862..e72755f 100644 --- a/elements/containers/box.go +++ b/elements/box.go @@ -1,4 +1,4 @@ -package containers +package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" diff --git a/elements/containers/notdone/document.go b/elements/containers/notdone/document.go deleted file mode 100644 index 4ba9fde..0000000 --- a/elements/containers/notdone/document.go +++ /dev/null @@ -1,368 +0,0 @@ -package containers - -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/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// DocumentContainer is a scrollable container capable of containing flexible -// elements. -type DocumentContainer struct { - *core.Core - *core.Propagator - core core.CoreControl - - children []tomo.LayoutEntry - scroll image.Point - warping bool - contentBounds image.Rectangle - - config config.Wrapped - theme theme.Wrapped - - onScrollBoundsChange func () -} - -// NewDocumentContainer creates a new document container. -func NewDocumentContainer () (element *DocumentContainer) { - element = &DocumentContainer { } - element.theme.Case = tomo.C("tomo", "documentContainer") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - return -} - -// Adopt adds a new child element to the container. If expand is true, then the -// element will stretch to either side of the container (much like a css block -// element). If expand is false, the element will share a line with other inline -// elements. -func (element *DocumentContainer) Adopt (child tomo.Element, expand bool) { - // set event handlers - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(element.config.Config) - } - - // add child - element.children = append (element.children, tomo.LayoutEntry { - Element: child, - Expand: expand, - }) - - child.SetParent(element) - - // refresh stale data - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Warp runs the specified callback, deferring all layout and rendering updates -// until the callback has finished executing. This allows for aplications to -// perform batch gui updates without flickering and stuff. -func (element *DocumentContainer) Warp (callback func ()) { - if element.warping { - callback() - return - } - - element.warping = true - callback() - element.warping = false - - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// Disown removes the given child from the container if it is contained within -// it. -func (element *DocumentContainer) Disown (child tomo.Element) { - for index, entry := range element.children { - if entry.Element == child { - element.clearChildEventHandlers(entry.Element) - element.children = append ( - element.children[:index], - element.children[index + 1:]...) - break - } - } - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -func (element *DocumentContainer) clearChildEventHandlers (child tomo.Element) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} - -// DisownAll removes all child elements from the container at once. -func (element *DocumentContainer) DisownAll () { - for _, entry := range element.children { - element.clearChildEventHandlers(entry.Element) - } - element.children = nil - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Children returns a slice containing this element's children. -func (element *DocumentContainer) Children () (children []tomo.Element) { - children = make([]tomo.Element, len(element.children)) - for index, entry := range element.children { - children[index] = entry.Element - } - return -} - -// CountChildren returns the amount of children contained within this element. -func (element *DocumentContainer) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *DocumentContainer) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].Element -} - -// ChildAt returns the child that contains the specified x and y coordinates. If -// there are no children at the coordinates, this method will return nil. -func (element *DocumentContainer) ChildAt (point image.Point) (child tomo.Element) { - for _, entry := range element.children { - if point.In(entry.Bounds) { - child = entry.Element - } - } - return -} - -func (element *DocumentContainer) redoAll () { - if !element.core.HasImage() { return } - - // do a layout - element.doLayout() - - maxScrollHeight := element.maxScrollHeight() - if element.scroll.Y > maxScrollHeight { - element.scroll.Y = maxScrollHeight - element.doLayout() - } - - // draw a background - rocks := make([]image.Rectangle, len(element.children)) - for index, entry := range element.children { - rocks[index] = entry.Bounds - } - pattern := element.theme.Pattern ( - tomo.PatternBackground, - tomo.State { }) - artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) - - element.partition() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *DocumentContainer) partition () { - for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds, nil) - } - - // cut our canvas up and give peices to child elements - for _, entry := range element.children { - if entry.Bounds.Overlaps(element.Bounds()) { - entry.DrawTo ( - canvas.Cut(element.core, entry.Bounds), - entry.Bounds, func (region image.Rectangle) { - element.core.DamageRegion(region) - }) - } - } -} - -func (element *DocumentContainer) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *DocumentContainer) NotifyMinimumSizeChange (child tomo.Element) { - element.redoAll() - element.core.DamageAll() -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *DocumentContainer) DrawBackground (bounds image.Rectangle) { - element.core.DrawBackgroundBounds ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - bounds) -} - -// NotifyFlexibleHeightChange notifies the parent that the parameters -// affecting a child's flexible height have changed. This method is -// expected to be called by flexible child element when their content -// changes. -func (element *DocumentContainer) NotifyFlexibleHeightChange (child tomo.Flexible) { - element.redoAll() - element.core.DamageAll() -} - -// SetTheme sets the element's theme. -func (element *DocumentContainer) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *DocumentContainer) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.redoAll() -} - -// ScrollContentBounds returns the full content size of the element. -func (element *DocumentContainer) ScrollContentBounds () image.Rectangle { - return element.contentBounds -} - -// ScrollViewportBounds returns the size and position of the element's -// viewport relative to ScrollBounds. -func (element *DocumentContainer) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternBackground) - bounds := padding.Apply(element.Bounds()) - bounds = bounds.Sub(bounds.Min).Add(element.scroll) - return bounds -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *DocumentContainer) ScrollTo (position image.Point) { - if position.Y < 0 { - position.Y = 0 - } - maxScrollHeight := element.maxScrollHeight() - if position.Y > maxScrollHeight { - position.Y = maxScrollHeight - } - element.scroll = position - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *DocumentContainer) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -func (element *DocumentContainer) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - viewportHeight := element.Bounds().Dy() - padding.Vertical() - height = element.contentBounds.Dy() - viewportHeight - if height < 0 { height = 0 } - return -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *DocumentContainer) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -func (element *DocumentContainer) doLayout () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) - bounds := padding.Apply(element.Bounds()) - element.contentBounds = image.Rectangle { } - - dot := bounds.Min.Sub(element.scroll) - xStart := dot.X - rowHeight := 0 - - nextLine := func () { - dot.X = xStart - dot.Y += margin.Y - dot.Y += rowHeight - rowHeight = 0 - } - - for index, entry := range element.children { - if dot.X > xStart && entry.Expand { - nextLine() - } - - width, height := entry.MinimumSize() - if width + dot.X > bounds.Dx() && !entry.Expand { - nextLine() - } - if width < bounds.Dx() && entry.Expand { - width = bounds.Dx() - } - if typedChild, ok := entry.Element.(tomo.Flexible); ok { - height = typedChild.FlexibleHeightFor(width) - } - if rowHeight < height { - rowHeight = height - } - - entry.Bounds.Min = dot - entry.Bounds.Max = image.Pt(dot.X + width, dot.Y + height) - element.children[index] = entry - element.contentBounds = element.contentBounds.Union(entry.Bounds) - - if entry.Expand { - nextLine() - } else { - dot.X += width + margin.X - } - } - - element.contentBounds = - element.contentBounds.Sub(element.contentBounds.Min) -} - -func (element *DocumentContainer) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternBackground) - minimumWidth := 0 - for _, entry := range element.children { - width, _ := entry.MinimumSize() - if width > minimumWidth { - minimumWidth = width - } - } - element.core.SetMinimumSize ( - minimumWidth + padding.Horizontal(), - padding.Vertical()) -} diff --git a/elements/containers/document.go b/elements/document.go similarity index 99% rename from elements/containers/document.go rename to elements/document.go index 9bc5f2d..d090d23 100644 --- a/elements/containers/document.go +++ b/elements/document.go @@ -1,4 +1,4 @@ -package containers +package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" diff --git a/elements/containers/notdone/table.go b/elements/notdone/table.go similarity index 100% rename from elements/containers/notdone/table.go rename to elements/notdone/table.go diff --git a/elements/containers/scroll.go b/elements/scroll.go similarity index 95% rename from elements/containers/scroll.go rename to elements/scroll.go index b83fc47..f7cf300 100644 --- a/elements/containers/scroll.go +++ b/elements/scroll.go @@ -1,10 +1,9 @@ -package containers +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/elements" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" @@ -12,8 +11,8 @@ type Scroll struct { entity tomo.ContainerEntity child tomo.Scrollable - horizontal *elements.ScrollBar - vertical *elements.ScrollBar + horizontal *ScrollBar + vertical *ScrollBar config config.Wrapped theme theme.Wrapped @@ -25,7 +24,7 @@ func NewScroll (horizontal, vertical bool) (element *Scroll) { element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) if horizontal { - element.horizontal = elements.NewScrollBar(false) + element.horizontal = NewScrollBar(false) element.horizontal.OnScroll (func (viewport image.Point) { if element.child != nil { element.child.ScrollTo(viewport) @@ -39,7 +38,7 @@ func NewScroll (horizontal, vertical bool) (element *Scroll) { element.entity.Adopt(element.horizontal) } if vertical { - element.vertical = elements.NewScrollBar(true) + element.vertical = NewScrollBar(true) element.vertical.OnScroll (func (viewport image.Point) { if element.child != nil { element.child.ScrollTo(viewport) diff --git a/examples/align/main.go b/examples/align/main.go index 8c86d98..e8a6e6e 100644 --- a/examples/align/main.go +++ b/examples/align/main.go @@ -4,7 +4,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -15,7 +14,7 @@ func run () { window.SetTitle("Text alignment") container := containers.NewDocument() - scrollContainer := containers.NewScroll(false, true) + scrollContainer := elements.NewScroll(false, true) scrollContainer.Adopt(container) window.Adopt(scrollContainer) diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index 3bb199f..519e82d 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -14,7 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Checkboxes") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) introText := elements.NewLabel ( diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index a730b50..f11cab5 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -10,7 +10,6 @@ import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -26,9 +25,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) window.SetTitle("Clipboard") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) textInput := elements.NewTextBox("", "") - controlRow := containers.NewHBox(false, true) + controlRow := elements.NewHBox(false, true) copyButton := elements.NewButton("Copy") copyButton.SetIcon(tomo.IconCopy) pasteButton := elements.NewButton("Paste") diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go index 7d04983..934a445 100644 --- a/examples/documentContainer/main.go +++ b/examples/documentContainer/main.go @@ -6,7 +6,6 @@ import _ "image/png" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -22,8 +21,8 @@ func run () { file.Close() if err != nil { panic(err.Error()); return } - scrollContainer := containers.NewScroll(false, true) - document := containers.NewDocument() + scrollContainer := elements.NewScroll(false, true) + document := elements.NewDocument() document.Adopt (elements.NewLabel ( "A document container is a vertically stacked container " + diff --git a/examples/flow/main.go b/examples/flow/main.go index b7873f7..381eb40 100644 --- a/examples/flow/main.go +++ b/examples/flow/main.go @@ -4,7 +4,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/flow" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192)) window.SetTitle("adventure") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) var world flow.Flow diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index 0d8eb76..29f547a 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -6,7 +6,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/fun" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -16,7 +15,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) window.SetTitle("Clock") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) clock := fun.NewAnalogClock(time.Now()) diff --git a/examples/hbox/main.go b/examples/hbox/main.go index 100bbdf..ed5a112 100644 --- a/examples/hbox/main.go +++ b/examples/hbox/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("horizontal stack") - container := containers.NewHBox(true, true) + container := elements.NewHBox(true, true) window.Adopt(container) container.Adopt(elements.NewLabel("this is sample text", true), true) diff --git a/examples/icons/main.go b/examples/icons/main.go index 16b3be2..5e17d2a 100644 --- a/examples/icons/main.go +++ b/examples/icons/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("Icons") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) container.Adopt(elements.NewLabel("Just some of the wonderful icons we have:", false), false) diff --git a/examples/input/main.go b/examples/input/main.go index 00577ac..b0e8a84 100644 --- a/examples/input/main.go +++ b/examples/input/main.go @@ -4,7 +4,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Enter Details") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) // create inputs diff --git a/examples/panels/main.go b/examples/panels/main.go index 0d7284b..0141795 100644 --- a/examples/panels/main.go +++ b/examples/panels/main.go @@ -5,7 +5,6 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -15,7 +14,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256)) window.SetTitle("Main") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) container.Adopt(elements.NewLabel("Main window", false), true) window.Adopt(container) diff --git a/examples/popups/main.go b/examples/popups/main.go index a07e728..eb4f395 100644 --- a/examples/popups/main.go +++ b/examples/popups/main.go @@ -4,7 +4,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -15,7 +14,7 @@ func run () { if err != nil { panic(err.Error()) } window.SetTitle("Dialog Boxes") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) container.Adopt(elements.NewLabel("Try out different dialogs:", false), true) diff --git a/examples/progress/main.go b/examples/progress/main.go index 18268f2..33831b9 100644 --- a/examples/progress/main.go +++ b/examples/progress/main.go @@ -4,7 +4,6 @@ import "time" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -14,7 +13,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Approaching") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) container.Adopt (elements.NewLabel ( diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go index 0e741fb..b0c8906 100644 --- a/examples/raycaster/main.go +++ b/examples/raycaster/main.go @@ -7,7 +7,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" //go:embed wall.png var wallTextureBytes []uint8 @@ -22,7 +21,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480)) window.SetTitle("Raycaster") - container := containers.NewVBox(false, false) + container := elements.NewVBox(false, false) window.Adopt(container) wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes)) diff --git a/examples/scroll/main.go b/examples/scroll/main.go index b01a887..28a9f34 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -4,7 +4,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240)) window.SetTitle("Scroll") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) textBox := elements.NewTextBox("", copypasta) diff --git a/examples/spacer/main.go b/examples/spacer/main.go index ba20293..e74dbba 100644 --- a/examples/spacer/main.go +++ b/examples/spacer/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Spaced Out") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) container.Adopt (elements.NewLabel("This is at the top", false), false) diff --git a/examples/switch/main.go b/examples/switch/main.go index 66a9413..fa38c92 100644 --- a/examples/switch/main.go +++ b/examples/switch/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -13,7 +12,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Switches") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) window.Adopt(container) container.Adopt(elements.NewSwitch("hahahah", false), false) diff --git a/examples/vbox/main.go b/examples/vbox/main.go index 7b9c7b7..d90edb0 100644 --- a/examples/vbox/main.go +++ b/examples/vbox/main.go @@ -3,7 +3,6 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -14,7 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128)) window.SetTitle("vertical stack") - container := containers.NewVBox(true, true) + container := elements.NewVBox(true, true) label := elements.NewLabel("it is a label hehe", true) button := elements.NewButton("drawing pad") diff --git a/popups/dialog.go b/popups/dialog.go index 514ea22..af91ead 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -3,7 +3,6 @@ package popups import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" // DialogKind defines the semantic role of a dialog window. type DialogKind int @@ -44,9 +43,9 @@ func NewDialog ( } window.SetTitle(title) - box := containers.NewVBox(true, true) - messageRow := containers.NewHBox(false, true) - controlRow := containers.NewHBox(false, true) + box := elements.NewVBox(true, true) + messageRow := elements.NewHBox(false, true) + controlRow := elements.NewHBox(false, true) iconId := tomo.IconInformation switch kind { From 5ca3b80e8e858eaede5782e8225d21ea7ebe3df8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 17 Apr 2023 02:05:53 -0400 Subject: [PATCH 28/41] Made this crazy selection system --- backends/x/entity.go | 28 +++++ backends/x/event.go | 29 ++++- backends/x/system.go | 2 +- element.go | 48 +++++++- elements/entry.go | 142 ++++++++++++++++++++++++ elements/{file => notdone}/directory.go | 0 elements/{file => notdone}/file.go | 0 elements/{file => notdone}/fs.go | 0 entity.go | 12 ++ 9 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 elements/entry.go rename elements/{file => notdone}/directory.go (100%) rename elements/{file => notdone}/file.go (100%) rename elements/{file => notdone}/fs.go (100%) diff --git a/backends/x/entity.go b/backends/x/entity.go index bc9c7b0..19e37a3 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -15,6 +15,7 @@ type entity struct { minWidth int minHeight int + selected bool layoutInvalid bool isContainer bool } @@ -42,6 +43,11 @@ func (ent *entity) unlink () { } ent.parent = nil ent.window = nil + + if element, ok := ent.element.(tomo.Selectable); ok { + ent.selected = false + element.HandleSelectionChange() + } } func (entity *entity) link (parent *entity) { @@ -96,6 +102,14 @@ func (entity *entity) scrollTargetChildAt (point image.Point) *entity { return nil } +func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer)) { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok { + callback(parent) + } + entity.parent.forMouseTargetContainers(callback) +} + // ----------- Entity ----------- // func (entity *entity) Invalidate () { @@ -195,6 +209,14 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { child.InvalidateLayout() } +func (entity *entity) SelectChild (index int, selected bool) { + child := entity.children[index] + if element, ok := entity.element.(tomo.Selectable); ok { + child.selected = selected + element.HandleSelectionChange() + } +} + func (entity *entity) ChildMinimumSize (index int) (width, height int) { childEntity := entity.children[index] return childEntity.minWidth, childEntity.minHeight @@ -220,6 +242,12 @@ func (entity *entity) FocusPrevious () { entity.window.system.focusPrevious() } +// ----------- SelectableEntity ----------- // + +func (entity *entity) Selected () bool { + return entity.selected +} + // ----------- FlexibleEntity ----------- // func (entity *entity) NotifyFlexibleHeightChange () { diff --git a/backends/x/event.go b/backends/x/event.go index 2f573ba..84630a1 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -206,12 +206,19 @@ func (window *window) handleButtonPress ( } } else { underneath := window.system.childAt(point) + window.system.drags[buttonEvent.Detail] = underneath if child, ok := underneath.element.(tomo.MouseTarget); ok { - window.system.drags[buttonEvent.Detail] = child child.HandleMouseDown ( point.X, point.Y, input.Button(buttonEvent.Detail)) } + callback := func (container tomo.MouseTargetContainer) { + container.HandleChildMouseDown ( + point.X, point.Y, + input.Button(buttonEvent.Detail), + underneath.element) + } + underneath.forMouseTargetContainers(callback) } window.system.afterEvent() @@ -223,12 +230,22 @@ func (window *window) handleButtonRelease ( ) { buttonEvent := *event.ButtonReleaseEvent if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } - child := window.system.drags[buttonEvent.Detail] - if child != nil { - child.HandleMouseUp ( + dragging := window.system.drags[buttonEvent.Detail] + if dragging != nil { + if child, ok := dragging.element.(tomo.MouseTarget); ok { + child.HandleMouseUp ( + int(buttonEvent.EventX), + int(buttonEvent.EventY), + input.Button(buttonEvent.Detail)) + } + callback := func (container tomo.MouseTargetContainer) { + container.HandleChildMouseUp ( int(buttonEvent.EventX), int(buttonEvent.EventY), - input.Button(buttonEvent.Detail)) + input.Button(buttonEvent.Detail), + dragging.element) + } + dragging.forMouseTargetContainers(callback) } window.system.afterEvent() @@ -244,7 +261,7 @@ func (window *window) handleMotionNotify ( handled := false for _, child := range window.system.drags { - if child, ok := child.(tomo.MotionTarget); ok { + if child, ok := child.element.(tomo.MotionTarget); ok { child.HandleMotion(x, y) handled = true } diff --git a/backends/x/system.go b/backends/x/system.go index 78a43b7..540b4e8 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -33,7 +33,7 @@ type system struct { drawingInvalid entitySet anyLayoutInvalid bool - drags [10]tomo.MouseTarget + drags [10]*entity pushFunc func (image.Rectangle) } diff --git a/element.go b/element.go index 0b99b04..cad0a80 100644 --- a/element.go +++ b/element.go @@ -40,17 +40,43 @@ type Container interface { HandleChildMinimumSizeChange (child Element) } +// Enableable represents an element that can be enabled and disabled. Disabled +// elements typically appear greyed out. +type Enableable interface { + Element + + // Enabled returns whether or not the element is enabled. + Enabled () bool + + // SetEnabled sets whether or not the element is enabled. + SetEnabled (bool) +} + // Focusable represents an element that has keyboard navigation support. type Focusable interface { Element - - // Enabled returns whether or not the element can currently accept focus. - Enabled () bool + Enableable // HandleFocusChange is called when the element is focused or unfocused. HandleFocusChange () } +// Selectable represents an element that can be selected. This includes things +// like list items, files, etc. The difference between this and Focusable is +// that multiple Selectable elements may be selected at the same time, whereas +// only one Focusable element may be focused at the same time. Containers who's +// purpose is to contain selectable elements can determine when to select them +// by implementing MouseTargetContainer and listening for HandleChildMouseDown +// events. +type Selectable interface { + Element + Enableable + + // HandleSelectionChange is called when the element is selected or + // deselected. + HandleSelectionChange () +} + // KeyboardTarget represents an element that can receive keyboard input. type KeyboardTarget interface { Element @@ -80,6 +106,22 @@ type MouseTarget interface { HandleMouseUp (x, y int, button input.Button) } +// MouseTargetContainer represents an element that wants to know when one +// of its children is clicked. Children do not have to implement MouseTarget for +// a container satisfying MouseTargetContainer to be notified that they have +// been clicked. +type MouseTargetContainer interface { + Container + + // HandleMouseDown is called when a mouse button is pressed down on a + // child element. + HandleChildMouseDown (x, y int, button input.Button, child Element) + + // HandleMouseUp is called when a mouse button is released that was + // originally pressed down on a child element. + HandleChildMouseUp (x, y int, button input.Button, child Element) +} + // MotionTarget represents an element that can receive mouse motion events. type MotionTarget interface { Element diff --git a/elements/entry.go b/elements/entry.go new file mode 100644 index 0000000..61334c3 --- /dev/null +++ b/elements/entry.go @@ -0,0 +1,142 @@ +package elements + +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" + +type cellEntity interface { + tomo.ContainerEntity + tomo.SelectableEntity +} + +// Cell is a single-element container that satisfies tomo.Selectable. It +// provides styling based on whether or not it is selected. +type Cell struct { + entity cellEntity + child tomo.Element + enabled bool + padding bool + theme theme.Wrapped +} + +// NewCell creates a new cell element. If padding is true, the cell will have +// padding on all sides. Child can be nil and added later with the Adopt() +// method. +func NewCell (child tomo.Element, padding bool) (element *Cell) { + element = &Cell { padding: padding } + element.theme.Case = tomo.C("tomo", "cell") + element.entity = tomo.NewEntity(element).(cellEntity) + element.Adopt(child) + return +} + +// Entity returns this element's entity. +func (element *Cell) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Cell) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + pattern := element.theme.Pattern(tomo.PatternTableCell, element.state()) + if element.child == nil { + pattern.Draw(destination, bounds) + } else if element.padding { + artist.DrawShatter ( + destination, pattern, bounds, + element.child.Entity().Bounds()) + } +} + +// Draw causes the element to perform a layout operation. +func (element *Cell) Layout () { + if element.child == nil { return } + + bounds := element.entity.Bounds() + if element.padding { + bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds) + } + + element.entity.PlaceChild(0, bounds) +} + +// DrawBackground draws this element's background pattern to the specified +// destination canvas. +func (element *Cell) DrawBackground (destination canvas.Canvas) { + element.theme.Pattern(tomo.PatternTableCell, element.state()). + Draw(destination, element.entity.Bounds()) +} + +// Adopt sets this element's child. If nil is passed, any child is removed. +func (element *Cell) Adopt (child tomo.Element) { + if element.child != nil { + element.entity.Disown(element.entity.IndexOf(element.child)) + } + if child != nil { + element.entity.Adopt(child) + } + element.child = child + + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// Enabled returns whether this cell is enabled or not. +func (element *Cell) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this cell can be selected or not. +func (element *Cell) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() + if child, ok := element.child.(tomo.Enableable); ok { + child.SetEnabled(enabled) + } +} + +// SetTheme sets this element's theme. +func (element *Cell) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Cell) HandleSelectionChange () { + element.entity.Invalidate() +} + +func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Cell) state () tomo.State { + return tomo.State { + Disabled: !element.enabled, + On: element.entity.Selected(), + } +} + +func (element *Cell) updateMinimumSize () { + width, height := 0, 0 + + if element.child != nil { + childWidth, childHeight := element.entity.ChildMinimumSize(0) + width += childWidth + height += childHeight + } + if element.padding { + padding := element.theme.Padding(tomo.PatternTableCell) + width += padding.Horizontal() + height += padding.Vertical() + } + + element.entity.SetMinimumSize(width, height) +} diff --git a/elements/file/directory.go b/elements/notdone/directory.go similarity index 100% rename from elements/file/directory.go rename to elements/notdone/directory.go diff --git a/elements/file/file.go b/elements/notdone/file.go similarity index 100% rename from elements/file/file.go rename to elements/notdone/file.go diff --git a/elements/file/fs.go b/elements/notdone/fs.go similarity index 100% rename from elements/file/fs.go rename to elements/notdone/fs.go diff --git a/entity.go b/entity.go index dfdede7..bdeb562 100644 --- a/entity.go +++ b/entity.go @@ -69,6 +69,10 @@ type ContainerEntity interface { // index to a bounding rectangle. PlaceChild (index int, bounds image.Rectangle) + // SelectChild marks a child as selected or unselected, if it is + // selectable. + SelectChild (index int, selected bool) + // ChildMinimumSize returns the minimum size of the child at the // specified index. ChildMinimumSize (index int) (width, height int) @@ -94,6 +98,14 @@ type FocusableEntity interface { FocusPrevious () } +// SelectableEntity is given to elements that support the Selectable interface. +type SelectableEntity interface { + Entity + + // Selected returns whether this element is currently selected. + Selected () bool +} + // FlexibleEntity is given to elements that support the Flexible interface. type FlexibleEntity interface { Entity From 427b5e025d1a12269284b3f7cf8f419892ab5a81 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 17 Apr 2023 02:13:21 -0400 Subject: [PATCH 29/41] Scroll now has a constructor similar to Cell --- elements/{entry.go => cell.go} | 0 elements/scroll.go | 4 +++- examples/align/main.go | 6 ++---- examples/documentContainer/main.go | 4 +--- examples/scroll/main.go | 6 ++---- 5 files changed, 8 insertions(+), 12 deletions(-) rename elements/{entry.go => cell.go} (100%) diff --git a/elements/entry.go b/elements/cell.go similarity index 100% rename from elements/entry.go rename to elements/cell.go diff --git a/elements/scroll.go b/elements/scroll.go index f7cf300..9aa5727 100644 --- a/elements/scroll.go +++ b/elements/scroll.go @@ -18,7 +18,7 @@ type Scroll struct { theme theme.Wrapped } -func NewScroll (horizontal, vertical bool) (element *Scroll) { +func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scroll) { element = &Scroll { } element.theme.Case = tomo.C("tomo", "scroll") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) @@ -51,6 +51,8 @@ func NewScroll (horizontal, vertical bool) (element *Scroll) { }) element.entity.Adopt(element.vertical) } + + element.Adopt(child) return } diff --git a/examples/align/main.go b/examples/align/main.go index e8a6e6e..ecee3fe 100644 --- a/examples/align/main.go +++ b/examples/align/main.go @@ -13,10 +13,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256)) window.SetTitle("Text alignment") - container := containers.NewDocument() - scrollContainer := elements.NewScroll(false, true) - scrollContainer.Adopt(container) - window.Adopt(scrollContainer) + container := elements.NewDocument() left := elements.NewLabel(text, true) center := elements.NewLabel(text, true) @@ -32,6 +29,7 @@ func run () { container.Adopt(center, true) container.Adopt(right, true) container.Adopt(justify, true) + window.Adopt(elements.NewScroll(container, false, true)) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go index 934a445..eab82cd 100644 --- a/examples/documentContainer/main.go +++ b/examples/documentContainer/main.go @@ -21,7 +21,6 @@ func run () { file.Close() if err != nil { panic(err.Error()); return } - scrollContainer := elements.NewScroll(false, true) document := elements.NewDocument() document.Adopt (elements.NewLabel ( @@ -60,8 +59,7 @@ func run () { document.Adopt(elements.NewSwitch("", false), false) } - scrollContainer.Adopt(document) - window.Adopt(scrollContainer) + window.Adopt(elements.NewScroll(document, false, true)) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 28a9f34..af07def 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -16,9 +16,8 @@ func run () { window.Adopt(container) textBox := elements.NewTextBox("", copypasta) - scrollContainer := containers.NewScroll(true, false) - disconnectedContainer := containers.NewHBox(false, true) + disconnectedContainer := elements.NewHBox(false, true) // list := elements.NewList ( // elements.NewListEntry("This is list item 0", nil), // elements.NewListEntry("This is list item 1", nil), @@ -52,9 +51,8 @@ func run () { // list.ScrollTo(viewport) // }) - scrollContainer.Adopt(textBox) container.Adopt(elements.NewLabel("A ScrollContainer:", false), false) - container.Adopt(scrollContainer, false) + container.Adopt(elements.NewScroll(textBox, true, false), false) // disconnectedContainer.Adopt(list, false) disconnectedContainer.Adopt (elements.NewLabel ( "Notice how the scroll bar to the right can be used to " + From 6b13e772a9cf16ba0b81958ba3f671fdb27893fb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 17 Apr 2023 02:16:27 -0400 Subject: [PATCH 30/41] Fixed segfault in the X backend when handling mouse motion --- backends/x/event.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backends/x/event.go b/backends/x/event.go index 84630a1..5f6feba 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -261,6 +261,7 @@ func (window *window) handleMotionNotify ( handled := false for _, child := range window.system.drags { + if child == nil { continue } if child, ok := child.element.(tomo.MotionTarget); ok { child.HandleMotion(x, y) handled = true From 0bf5c3b86c93f954b5c44944eba5fef014b0fa7a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 02:59:44 -0400 Subject: [PATCH 31/41] Lists are a thing now Looks like child bounds arent clipped properly though, ugh --- backends/x/entity.go | 7 +- backends/x/event.go | 8 +- default/theme/default.go | 21 +-- elements/cell.go | 55 ++++--- elements/list.go | 315 +++++++++++++++++++++++++++++++++++++++ examples/list/main.go | 38 ++--- examples/scroll/main.go | 71 ++++----- 7 files changed, 420 insertions(+), 95 deletions(-) create mode 100644 elements/list.go diff --git a/backends/x/entity.go b/backends/x/entity.go index 19e37a3..6fde145 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -102,10 +102,10 @@ func (entity *entity) scrollTargetChildAt (point image.Point) *entity { return nil } -func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer)) { +func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) { if entity.parent == nil { return } if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok { - callback(parent) + callback(parent, entity.element) } entity.parent.forMouseTargetContainers(callback) } @@ -211,7 +211,8 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { func (entity *entity) SelectChild (index int, selected bool) { child := entity.children[index] - if element, ok := entity.element.(tomo.Selectable); ok { + if element, ok := child.element.(tomo.Selectable); ok { + if child.selected == selected { return } child.selected = selected element.HandleSelectionChange() } diff --git a/backends/x/event.go b/backends/x/event.go index 5f6feba..c099268 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -212,11 +212,11 @@ func (window *window) handleButtonPress ( point.X, point.Y, input.Button(buttonEvent.Detail)) } - callback := func (container tomo.MouseTargetContainer) { + callback := func (container tomo.MouseTargetContainer, child tomo.Element) { container.HandleChildMouseDown ( point.X, point.Y, input.Button(buttonEvent.Detail), - underneath.element) + child) } underneath.forMouseTargetContainers(callback) } @@ -238,12 +238,12 @@ func (window *window) handleButtonRelease ( int(buttonEvent.EventY), input.Button(buttonEvent.Detail)) } - callback := func (container tomo.MouseTargetContainer) { + callback := func (container tomo.MouseTargetContainer, child tomo.Element) { container.HandleChildMouseUp ( int(buttonEvent.EventX), int(buttonEvent.EventY), input.Button(buttonEvent.Detail), - dragging.element) + child) } dragging.forMouseTargetContainers(callback) } diff --git a/default/theme/default.go b/default/theme/default.go index 74ef9de..068e233 100644 --- a/default/theme/default.go +++ b/default/theme/default.go @@ -212,14 +212,9 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P switch id { case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF) case tomo.PatternDead: return defaultTextures[0][offset] - case tomo.PatternRaised: - if c.Match("tomo", "listEntry", "") { - return defaultTextures[10][offset] - } else { - return defaultTextures[1][offset] - } - case tomo.PatternSunken: return defaultTextures[2][offset] - case tomo.PatternPinboard: return defaultTextures[3][offset] + case tomo.PatternRaised: return defaultTextures[1][offset] + case tomo.PatternSunken: return defaultTextures[2][offset] + case tomo.PatternPinboard: return defaultTextures[3][offset] case tomo.PatternButton: switch { case c.Match("tomo", "checkbox", ""): @@ -272,16 +267,8 @@ func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA { // Padding returns the default padding value for the given pattern. func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { switch id { - case tomo.PatternRaised: - if c.Match("tomo", "listEntry", "") { - return artist.I(4, 8) - } else { - return artist.I(8) - } case tomo.PatternSunken: - if c.Match("tomo", "list", "") { - return artist.I(4, 0, 3) - } else if c.Match("tomo", "progressBar", "") { + if c.Match("tomo", "progressBar", "") { return artist.I(2, 1, 1, 2) } else { return artist.I(8) diff --git a/elements/cell.go b/elements/cell.go index 61334c3..c030a6e 100644 --- a/elements/cell.go +++ b/elements/cell.go @@ -13,18 +13,19 @@ type cellEntity interface { // Cell is a single-element container that satisfies tomo.Selectable. It // provides styling based on whether or not it is selected. type Cell struct { - entity cellEntity - child tomo.Element - enabled bool - padding bool - theme theme.Wrapped + entity cellEntity + child tomo.Element + enabled bool + theme theme.Wrapped + + onSelectionChange func () } // NewCell creates a new cell element. If padding is true, the cell will have // padding on all sides. Child can be nil and added later with the Adopt() // method. -func NewCell (child tomo.Element, padding bool) (element *Cell) { - element = &Cell { padding: padding } +func NewCell (child tomo.Element) (element *Cell) { + element = &Cell { enabled: true } element.theme.Case = tomo.C("tomo", "cell") element.entity = tomo.NewEntity(element).(cellEntity) element.Adopt(child) @@ -42,7 +43,7 @@ func (element *Cell) Draw (destination canvas.Canvas) { pattern := element.theme.Pattern(tomo.PatternTableCell, element.state()) if element.child == nil { pattern.Draw(destination, bounds) - } else if element.padding { + } else { artist.DrawShatter ( destination, pattern, bounds, element.child.Entity().Bounds()) @@ -54,9 +55,7 @@ func (element *Cell) Layout () { if element.child == nil { return } bounds := element.entity.Bounds() - if element.padding { - bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds) - } + bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds) element.entity.PlaceChild(0, bounds) } @@ -80,6 +79,7 @@ func (element *Cell) Adopt (child tomo.Element) { element.updateMinimumSize() element.entity.Invalidate() + element.invalidateChild() element.entity.InvalidateLayout() } @@ -93,9 +93,7 @@ func (element *Cell) SetEnabled (enabled bool) { if element.enabled == enabled { return } element.enabled = enabled element.entity.Invalidate() - if child, ok := element.child.(tomo.Enableable); ok { - child.SetEnabled(enabled) - } + element.invalidateChild() } // SetTheme sets this element's theme. @@ -104,11 +102,26 @@ func (element *Cell) SetTheme (theme tomo.Theme) { element.theme.Theme = theme element.updateMinimumSize() element.entity.Invalidate() + element.invalidateChild() element.entity.InvalidateLayout() } +// OnSelectionChange sets a function to be called when this element is selected +// or unselected. +func (element *Cell) OnSelectionChange (callback func ()) { + element.onSelectionChange = callback +} + +func (element *Cell) Selected () bool { + return element.entity.Selected() +} + func (element *Cell) HandleSelectionChange () { element.entity.Invalidate() + element.invalidateChild() + if element.onSelectionChange != nil { + element.onSelectionChange() + } } func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) { @@ -132,11 +145,15 @@ func (element *Cell) updateMinimumSize () { width += childWidth height += childHeight } - if element.padding { - padding := element.theme.Padding(tomo.PatternTableCell) - width += padding.Horizontal() - height += padding.Vertical() - } + padding := element.theme.Padding(tomo.PatternTableCell) + width += padding.Horizontal() + height += padding.Vertical() element.entity.SetMinimumSize(width, height) } + +func (element *Cell) invalidateChild () { + if element.child != nil { + element.child.Entity().Invalidate() + } +} diff --git a/elements/list.go b/elements/list.go new file mode 100644 index 0000000..33eacb5 --- /dev/null +++ b/elements/list.go @@ -0,0 +1,315 @@ +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/artist" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +type listEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + +type List struct { + entity listEntity + + scratch map[tomo.Element] scratchEntry + scroll image.Point + contentBounds image.Rectangle + columnSizes []int + selected int + + forcedMinimumWidth int + forcedMinimumHeight int + + theme theme.Wrapped + + onScrollBoundsChange func () +} + +func NewList (columns int, children ...tomo.Selectable) (element *List) { + if columns < 1 { columns = 1 } + element = &List { selected: -1 } + element.scratch = make(map[tomo.Element] scratchEntry) + element.columnSizes = make([]int, columns) + element.theme.Case = tomo.C("tomo", "list") + element.entity = tomo.NewEntity(element).(listEntity) + + for _, child := range children { + element.Adopt(child) + } + return +} + +func (element *List) Entity () tomo.Entity { + return element.entity +} + +func (element *List) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) + artist.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...) +} + +func (element *List) Layout () { + margin := element.theme.Margin(tomo.PatternSunken) + padding := element.theme.Padding(tomo.PatternSunken) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + columnIndex := 0 + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + columnIndex = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + if columnIndex >= len(element.columnSizes) { + nextLine() + } + width := element.columnSizes[columnIndex] + height := int(entry.minSize) + + if len(element.columnSizes) == 1 && width < bounds.Dx() { + width = bounds.Dx() + } + + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + dot.X += width + margin.X + + columnIndex ++ + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *List) Adopt (child tomo.Element) { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *List) Disown (child tomo.Element) { + index := element.entity.IndexOf(child) + if index < 0 { return } + if index == element.selected { + element.selected = -1 + element.entity.SelectChild(index, false) + } + element.entity.Disown(index) + delete(element.scratch, child) + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *List) DisownAll () { + func () { + for index := 0; index < element.entity.CountChildren(); index ++ { + index := index + defer element.entity.Disown(index) + } + } () + element.scratch = make(map[tomo.Element] scratchEntry) + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *List) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { + if child, ok := child.(tomo.Selectable); ok { + index := element.entity.IndexOf(child) + if element.selected == index { return } + if element.selected >= 0 { + element.entity.SelectChild(element.selected, false) + } + element.selected = index + element.entity.SelectChild(index, true) + } +} + +func (element *List) HandleChildMouseUp (int, int, input.Button, tomo.Element) { } + +func (element *List) HandleChildMinimumSizeChange (child tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *List) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *List) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +// SetTheme sets the element's theme. +func (element *List) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// Collapse forces a minimum width and height upon the list. If a zero value is +// given for a dimension, its minimum will be determined by the list's content. +// If the list's height goes beyond the forced size, it will need to be accessed +// via scrolling. If an entry's width goes beyond the forced size, its text will +// be truncated so that it fits. +func (element *List) Collapse (width, height int) { + if + element.forcedMinimumWidth == width && + element.forcedMinimumHeight == height { + + return + } + + element.forcedMinimumWidth = width + element.forcedMinimumHeight = height + + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// ScrollContentBounds returns the full content size of the element. +func (element *List) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *List) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternBackground) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds +} + +// ScrollTo scrolls the viewport to the specified point relative to +// ScrollBounds. +func (element *List) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *List) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *List) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +func (element *List) maxScrollHeight () (height int) { + padding := element.theme.Padding(tomo.PatternSunken) + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return +} + +func (element *List) updateMinimumSize () { + margin := element.theme.Margin(tomo.PatternSunken) + padding := element.theme.Padding(tomo.PatternSunken) + + for index := range element.columnSizes { + element.columnSizes[index] = 0 + } + + height := 0 + rowHeight := 0 + columnIndex := 0 + nextLine := func () { + height += rowHeight + rowHeight = 0 + columnIndex = 0 + } + for index := 0; index < element.entity.CountChildren(); index ++ { + if columnIndex >= len(element.columnSizes) { + if index > 0 { height += margin.Y } + nextLine() + } + + child := element.entity.Child(index) + entry := element.scratch[child] + + entryWidth, entryHeight := element.entity.ChildMinimumSize(index) + entry.minBreadth = float64(entryWidth) + entry.minSize = float64(entryHeight) + element.scratch[child] = entry + + if rowHeight < entryHeight { + rowHeight = entryHeight + } + if element.columnSizes[columnIndex] < entryWidth { + element.columnSizes[columnIndex] = entryWidth + } + + columnIndex ++ + } + nextLine() + + width := 0; for index, size := range element.columnSizes { + width += size + if index > 0 { width += margin.X } + } + width += padding.Horizontal() + height += padding.Vertical() + + if element.forcedMinimumHeight > 0 { + height = element.forcedMinimumHeight + } + if element.forcedMinimumWidth > 0 { + width = element.forcedMinimumWidth + } + + element.entity.SetMinimumSize(width, height) +} diff --git a/examples/list/main.go b/examples/list/main.go index 0aeeec8..9f49fee 100644 --- a/examples/list/main.go +++ b/examples/list/main.go @@ -2,10 +2,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -16,18 +14,16 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0)) window.SetTitle("List Sidebar") - container := containers.NewContainer(layouts.Horizontal { true, true }) + container := elements.NewHBox(true, true) window.Adopt(container) var currentPage tomo.Element turnPage := func (newPage tomo.Element) { - container.Warp (func () { - if currentPage != nil { - container.Disown(currentPage) - } - container.Adopt(newPage, true) - currentPage = newPage - }) + if currentPage != nil { + container.Disown(currentPage) + } + container.Adopt(newPage, true) + currentPage = newPage } intro := elements.NewLabel ( @@ -39,7 +35,7 @@ func run () { }) mouse := testing.NewMouse() input := elements.NewTextBox("Write some text", "") - form := containers.NewContainer(layouts.Vertical { true, false}) + form := elements.NewVBox(false, true) form.Adopt(elements.NewLabel("I have:", false), false) form.Adopt(elements.NewSpacer(true), false) form.Adopt(elements.NewCheckbox("Skin", true), false) @@ -47,13 +43,21 @@ func run () { form.Adopt(elements.NewCheckbox("Bone", false), false) art := testing.NewArtist() + makePage := func (name string, callback func ()) tomo.Selectable { + cell := elements.NewCell(elements.NewLabel(name, false)) + cell.OnSelectionChange (func () { + if cell.Selected() { callback() } + }) + return cell + } + list := elements.NewList ( - elements.NewListEntry("button", func () { turnPage(button) }), - elements.NewListEntry("mouse", func () { turnPage(mouse) }), - elements.NewListEntry("input", func () { turnPage(input) }), - elements.NewListEntry("form", func () { turnPage(form) }), - elements.NewListEntry("art", func () { turnPage(art) })) - list.OnNoEntrySelected(func () { turnPage (intro) }) + 1, + makePage("button", func () { turnPage(button) }), + makePage("mouse", func () { turnPage(mouse) }), + makePage("input", func () { turnPage(input) }), + makePage("form", func () { turnPage(form) }), + makePage("art", func () { turnPage(art) })) list.Collapse(96, 0) container.Adopt(list, false) diff --git a/examples/scroll/main.go b/examples/scroll/main.go index af07def..60fd6fe 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -1,6 +1,6 @@ package main -// import "image" +import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" @@ -18,48 +18,49 @@ func run () { textBox := elements.NewTextBox("", copypasta) disconnectedContainer := elements.NewHBox(false, true) - // list := elements.NewList ( - // elements.NewListEntry("This is list item 0", nil), - // elements.NewListEntry("This is list item 1", nil), - // elements.NewListEntry("This is list item 2", nil), - // elements.NewListEntry("This is list item 3", nil), - // elements.NewListEntry("This is list item 4", nil), - // elements.NewListEntry("This is list item 5", nil), - // elements.NewListEntry("This is list item 6", nil), - // elements.NewListEntry("This is list item 7", nil), - // elements.NewListEntry("This is list item 8", nil), - // elements.NewListEntry("This is list item 9", nil), - // elements.NewListEntry("This is list item 10", nil), - // elements.NewListEntry("This is list item 11", nil), - // elements.NewListEntry("This is list item 12", nil), - // elements.NewListEntry("This is list item 13", nil), - // elements.NewListEntry("This is list item 14", nil), - // elements.NewListEntry("This is list item 15", nil), - // elements.NewListEntry("This is list item 16", nil), - // elements.NewListEntry("This is list item 17", nil), - // elements.NewListEntry("This is list item 18", nil), - // elements.NewListEntry("This is list item 19", nil), - // elements.NewListEntry("This is list item 20", nil)) - // list.Collapse(0, 32) - // scrollBar := elements.NewScrollBar(true) - // list.OnScrollBoundsChange (func () { - // scrollBar.SetBounds ( - // list.ScrollContentBounds(), - // list.ScrollViewportBounds()) - // }) - // scrollBar.OnScroll (func (viewport image.Point) { - // list.ScrollTo(viewport) - // }) + list := elements.NewList ( + 1, + elements.NewCell(elements.NewLabel("This is list item 0", false)), + elements.NewCell(elements.NewLabel("This is list item 1", false)), + elements.NewCell(elements.NewLabel("This is list item 2", false)), + elements.NewCell(elements.NewLabel("This is list item 3", false)), + elements.NewCell(elements.NewLabel("This is list item 4", false)), + elements.NewCell(elements.NewLabel("This is list item 5", false)), + elements.NewCell(elements.NewLabel("This is list item 6", false)), + elements.NewCell(elements.NewLabel("This is list item 7", false)), + elements.NewCell(elements.NewLabel("This is list item 8", false)), + elements.NewCell(elements.NewLabel("This is list item 9", false)), + elements.NewCell(elements.NewLabel("This is list item 10", false)), + elements.NewCell(elements.NewLabel("This is list item 11", false)), + elements.NewCell(elements.NewLabel("This is list item 12", false)), + elements.NewCell(elements.NewLabel("This is list item 13", false)), + elements.NewCell(elements.NewLabel("This is list item 14", false)), + elements.NewCell(elements.NewLabel("This is list item 15", false)), + elements.NewCell(elements.NewLabel("This is list item 16", false)), + elements.NewCell(elements.NewLabel("This is list item 17", false)), + elements.NewCell(elements.NewLabel("This is list item 18", false)), + elements.NewCell(elements.NewLabel("This is list item 19", false)), + elements.NewCell(elements.NewLabel("This is list item 20", false))) + list.Collapse(0, 32) + scrollBar := elements.NewScrollBar(true) + list.OnScrollBoundsChange (func () { + scrollBar.SetBounds ( + list.ScrollContentBounds(), + list.ScrollViewportBounds()) + }) + scrollBar.OnScroll (func (viewport image.Point) { + list.ScrollTo(viewport) + }) container.Adopt(elements.NewLabel("A ScrollContainer:", false), false) container.Adopt(elements.NewScroll(textBox, true, false), false) - // disconnectedContainer.Adopt(list, false) + disconnectedContainer.Adopt(list, false) disconnectedContainer.Adopt (elements.NewLabel ( "Notice how the scroll bar to the right can be used to " + "control the list, despite not even touching it. It is " + "indeed a thing you can do. It is also terrible UI design so " + "don't do it.", true), true) - // disconnectedContainer.Adopt(scrollBar, false) + disconnectedContainer.Adopt(scrollBar, false) container.Adopt(disconnectedContainer, true) window.OnClose(tomo.Stop) From 785cc2d908918a2781ee5b300c27281d056964a6 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 03:07:06 -0400 Subject: [PATCH 32/41] Child draw bounds are properly clipped --- backends/x/entity.go | 10 +++++++++- backends/x/system.go | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/backends/x/entity.go b/backends/x/entity.go index 6fde145..bcd5ee1 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -52,6 +52,7 @@ func (ent *entity) unlink () { func (entity *entity) link (parent *entity) { entity.parent = parent + entity.clip(parent.clippedBounds) if parent.window != nil { entity.setWindow(parent.window) } @@ -110,6 +111,13 @@ func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetC entity.parent.forMouseTargetContainers(callback) } +func (entity *entity) clip (bounds image.Rectangle) { + entity.clippedBounds = entity.bounds.Intersect(bounds) + for _, child := range entity.children { + child.clip(entity.clippedBounds) + } +} + // ----------- Entity ----------- // func (entity *entity) Invalidate () { @@ -204,7 +212,7 @@ func (entity *entity) CountChildren () int { func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { child := entity.children[index] child.bounds = bounds - child.clippedBounds = entity.bounds.Intersect(bounds) + child.clip(entity.clippedBounds) child.Invalidate() child.InvalidateLayout() } diff --git a/backends/x/system.go b/backends/x/system.go index 540b4e8..ac2b5a7 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -170,6 +170,7 @@ func (system *system) draw () { defer func () { system.invalidateIgnore = false } () for entity := range system.drawingInvalid { + if entity.clippedBounds.Empty() { continue } entity.element.Draw (canvas.Cut ( system.canvas, entity.clippedBounds)) From d44e7b51daaebd170d58f24a03e074b887a4a2d9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 03:08:28 -0400 Subject: [PATCH 33/41] Hehe --- examples/scroll/main.go | 44 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 60fd6fe..0937b68 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -19,28 +19,28 @@ func run () { disconnectedContainer := elements.NewHBox(false, true) list := elements.NewList ( - 1, - elements.NewCell(elements.NewLabel("This is list item 0", false)), - elements.NewCell(elements.NewLabel("This is list item 1", false)), - elements.NewCell(elements.NewLabel("This is list item 2", false)), - elements.NewCell(elements.NewLabel("This is list item 3", false)), - elements.NewCell(elements.NewLabel("This is list item 4", false)), - elements.NewCell(elements.NewLabel("This is list item 5", false)), - elements.NewCell(elements.NewLabel("This is list item 6", false)), - elements.NewCell(elements.NewLabel("This is list item 7", false)), - elements.NewCell(elements.NewLabel("This is list item 8", false)), - elements.NewCell(elements.NewLabel("This is list item 9", false)), - elements.NewCell(elements.NewLabel("This is list item 10", false)), - elements.NewCell(elements.NewLabel("This is list item 11", false)), - elements.NewCell(elements.NewLabel("This is list item 12", false)), - elements.NewCell(elements.NewLabel("This is list item 13", false)), - elements.NewCell(elements.NewLabel("This is list item 14", false)), - elements.NewCell(elements.NewLabel("This is list item 15", false)), - elements.NewCell(elements.NewLabel("This is list item 16", false)), - elements.NewCell(elements.NewLabel("This is list item 17", false)), - elements.NewCell(elements.NewLabel("This is list item 18", false)), - elements.NewCell(elements.NewLabel("This is list item 19", false)), - elements.NewCell(elements.NewLabel("This is list item 20", false))) + 2, + elements.NewCell(elements.NewLabel("Item 0", false)), + elements.NewCell(elements.NewLabel("Item 1", false)), + elements.NewCell(elements.NewLabel("Item 2", false)), + elements.NewCell(elements.NewLabel("Item 3", false)), + elements.NewCell(elements.NewLabel("Item 4", false)), + elements.NewCell(elements.NewLabel("Item 5", false)), + elements.NewCell(elements.NewLabel("Item 6", false)), + elements.NewCell(elements.NewLabel("Item 7", false)), + elements.NewCell(elements.NewLabel("Item 8", false)), + elements.NewCell(elements.NewLabel("Item 9", false)), + elements.NewCell(elements.NewLabel("Item 10", false)), + elements.NewCell(elements.NewLabel("Item 11", false)), + elements.NewCell(elements.NewLabel("Item 12", false)), + elements.NewCell(elements.NewLabel("Item 13", false)), + elements.NewCell(elements.NewLabel("Item 14", false)), + elements.NewCell(elements.NewLabel("Item 15", false)), + elements.NewCell(elements.NewLabel("Item 16", false)), + elements.NewCell(elements.NewLabel("Item 17", false)), + elements.NewCell(elements.NewLabel("Item 18", false)), + elements.NewCell(elements.NewLabel("Item 19", false)), + elements.NewCell(elements.NewLabel("Item 20", false))) list.Collapse(0, 32) scrollBar := elements.NewScrollBar(true) list.OnScrollBoundsChange (func () { From 62763276137540ef4ea10f6bb63f661a271f7a1c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 03:12:36 -0400 Subject: [PATCH 34/41] Some theming tweaks --- default/theme/default.go | 14 ++++++++++++-- examples/scroll/main.go | 3 ++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/default/theme/default.go b/default/theme/default.go index 068e233..1cb4c69 100644 --- a/default/theme/default.go +++ b/default/theme/default.go @@ -279,16 +279,26 @@ func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { } else { return artist.I(8) } + case tomo.PatternTableCell: return artist.I(5) + case tomo.PatternTableHead: return artist.I(5) case tomo.PatternGutter: return artist.I(0) case tomo.PatternLine: return artist.I(1) case tomo.PatternMercury: return artist.I(5) - default: return artist.I(8) + default: return artist.I(8) } } // Margin returns the default margin value for the given pattern. func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point { - return image.Pt(8, 8) + switch id { + case tomo.PatternSunken: + if c.Match("tomo", "list", "") { + return image.Pt(-1, -1) + } else { + return image.Pt(8, 8) + } + default: return image.Pt(8, 8) + } } // Hints returns rendering optimization hints for a particular pattern. diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 0937b68..27a6be2 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -40,7 +40,8 @@ func run () { elements.NewCell(elements.NewLabel("Item 17", false)), elements.NewCell(elements.NewLabel("Item 18", false)), elements.NewCell(elements.NewLabel("Item 19", false)), - elements.NewCell(elements.NewLabel("Item 20", false))) + elements.NewCell(elements.NewLabel("Item 20", false)), + elements.NewCell(elements.NewLabel("Item 21", false))) list.Collapse(0, 32) scrollBar := elements.NewScrollBar(true) list.OnScrollBoundsChange (func () { From a2b1ac0c73bfd4dc07fd20115db3bdd0f4a988e1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 03:23:51 -0400 Subject: [PATCH 35/41] Oh yeah yeah! --- elements/list.go | 4 ++++ elements/scroll.go | 5 ++++- examples/scroll/main.go | 46 +++++++++++++++++++++-------------------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/elements/list.go b/elements/list.go index 33eacb5..bec6c71 100644 --- a/elements/list.go +++ b/elements/list.go @@ -58,6 +58,10 @@ func (element *List) Draw (destination canvas.Canvas) { } func (element *List) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + margin := element.theme.Margin(tomo.PatternSunken) padding := element.theme.Padding(tomo.PatternSunken) bounds := padding.Apply(element.entity.Bounds()) diff --git a/elements/scroll.go b/elements/scroll.go index 9aa5727..7ecfb25 100644 --- a/elements/scroll.go +++ b/elements/scroll.go @@ -198,7 +198,10 @@ func (element *Scroll) updateMinimumSize () { } func (element *Scroll) updateEnabled () { - horizontal, vertical := element.child.ScrollAxes() + horizontal, vertical := false, false + if element.child != nil { + horizontal, vertical = element.child.ScrollAxes() + } if element.horizontal != nil { element.horizontal.SetEnabled(horizontal) } diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 27a6be2..83aa521 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -20,28 +20,30 @@ func run () { disconnectedContainer := elements.NewHBox(false, true) list := elements.NewList ( 2, - elements.NewCell(elements.NewLabel("Item 0", false)), - elements.NewCell(elements.NewLabel("Item 1", false)), - elements.NewCell(elements.NewLabel("Item 2", false)), - elements.NewCell(elements.NewLabel("Item 3", false)), - elements.NewCell(elements.NewLabel("Item 4", false)), - elements.NewCell(elements.NewLabel("Item 5", false)), - elements.NewCell(elements.NewLabel("Item 6", false)), - elements.NewCell(elements.NewLabel("Item 7", false)), - elements.NewCell(elements.NewLabel("Item 8", false)), - elements.NewCell(elements.NewLabel("Item 9", false)), - elements.NewCell(elements.NewLabel("Item 10", false)), - elements.NewCell(elements.NewLabel("Item 11", false)), - elements.NewCell(elements.NewLabel("Item 12", false)), - elements.NewCell(elements.NewLabel("Item 13", false)), - elements.NewCell(elements.NewLabel("Item 14", false)), - elements.NewCell(elements.NewLabel("Item 15", false)), - elements.NewCell(elements.NewLabel("Item 16", false)), - elements.NewCell(elements.NewLabel("Item 17", false)), - elements.NewCell(elements.NewLabel("Item 18", false)), - elements.NewCell(elements.NewLabel("Item 19", false)), - elements.NewCell(elements.NewLabel("Item 20", false)), - elements.NewCell(elements.NewLabel("Item 21", false))) + elements.NewCell(elements.NewCheckbox("Item 0", true)), + elements.NewCell(elements.NewCheckbox("Item 1", false)), + elements.NewCell(elements.NewCheckbox("Item 2", false)), + elements.NewCell(elements.NewCheckbox("Item 3", true)), + elements.NewCell(elements.NewCheckbox("Item 4", false)), + elements.NewCell(elements.NewCheckbox("Item 5", false)), + elements.NewCell(elements.NewCheckbox("Item 6", false)), + elements.NewCell(elements.NewCheckbox("Item 7", true)), + elements.NewCell(elements.NewCheckbox("Item 8", true)), + elements.NewCell(elements.NewCheckbox("Item 9", false)), + elements.NewCell(elements.NewCheckbox("Item 10", false)), + elements.NewCell(elements.NewCheckbox("Item 11", true)), + elements.NewCell(elements.NewCheckbox("Item 12", false)), + elements.NewCell(elements.NewCheckbox("Item 13", true)), + elements.NewCell(elements.NewCheckbox("Item 14", false)), + elements.NewCell(elements.NewCheckbox("Item 15", false)), + elements.NewCell(elements.NewCheckbox("Item 16", true)), + elements.NewCell(elements.NewCheckbox("Item 17", true)), + elements.NewCell(elements.NewCheckbox("Item 18", false)), + elements.NewCell(elements.NewCheckbox("Item 19", false)), + elements.NewCell(elements.NewCheckbox("Item 20", true)), + elements.NewCell(elements.NewCheckbox("Item 21", false)), + elements.NewCell (elements.NewScroll (elements.NewTextBox ( + "", "I bet you weren't expecting this!"), true, false))) list.Collapse(0, 32) scrollBar := elements.NewScrollBar(true) list.OnScrollBoundsChange (func () { From 14080b1f885efea8878263e4ce1d3768bff428d7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 13:14:10 -0400 Subject: [PATCH 36/41] Element methods are now more consistent and have less bool flags Still need to update most examples... --- elements/box.go | 66 ++++- elements/cell.go | 6 + elements/document.go | 42 +++- elements/label.go | 13 +- elements/lerpslider.go | 12 +- elements/list.go | 11 +- elements/notdone/list.go | 455 ---------------------------------- elements/notdone/listentry.go | 104 -------- elements/progressbar.go | 4 + elements/scroll.go | 19 +- elements/scrollbar.go | 16 +- elements/slider.go | 9 +- elements/spacer.go | 15 +- examples/align/main.go | 17 +- examples/checkbox/main.go | 33 +-- examples/clipboard/main.go | 20 +- popups/dialog.go | 20 +- 17 files changed, 209 insertions(+), 653 deletions(-) delete mode 100644 elements/notdone/list.go delete mode 100644 elements/notdone/listentry.go diff --git a/elements/box.go b/elements/box.go index e72755f..29b3e13 100644 --- a/elements/box.go +++ b/elements/box.go @@ -6,6 +6,21 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/default/theme" +// Space is a list of spacing configurations that can be passed to some +// containers. +type Space int; const ( + SpaceNone = 0 + SpacePadding = 1 + SpaceMargin = 2 + SpaceBoth = SpacePadding | SpaceMargin +) + +// Includes returns whether a spacing value has been or'd with another spacing +// value. +func (space Space) Includes (sub Space) bool { + return (space & sub) > 0 +} + type scratchEntry struct { expand bool minSize float64 @@ -26,17 +41,21 @@ type Box struct { } // NewHBox creates a new horizontal box. -func NewHBox (padding, margin bool) (element *Box) { - element = &Box { padding: padding, margin: margin } +func NewHBox (space Space, children ...tomo.Element) (element *Box) { + element = &Box { + padding: space.Includes(SpacePadding), + margin: space.Includes(SpaceMargin), + } element.scratch = make(map[tomo.Element] scratchEntry) element.theme.Case = tomo.C("tomo", "box") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.Adopt(children...) return } // NewHBox creates a new vertical box. -func NewVBox (padding, margin bool) (element *Box) { - element = NewHBox(padding, margin) +func NewVBox (space Space) (element *Box) { + element = NewHBox(space) element.vertical = true return } @@ -101,19 +120,33 @@ func (element *Box) Layout () { } } -func (element *Box) Adopt (child tomo.Element, expand bool) { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: expand } +func (element *Box) Adopt (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: false } + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -func (element *Box) Disown (child tomo.Element) { - index := element.entity.IndexOf(child) - if index < 0 { return } - element.entity.Disown(index) - delete(element.scratch, child) +func (element *Box) AdoptExpand (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: true } + } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Box) Disown (children ...tomo.Element) { + for _, child := range children { + index := element.entity.IndexOf(child) + if index < 0 { continue } + element.entity.Disown(index) + delete(element.scratch, child) + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -132,6 +165,15 @@ func (element *Box) DisownAll () { element.entity.InvalidateLayout() } +func (element *Box) Child (index int) tomo.Element { + if index < 0 || index >= element.entity.CountChildren() { return nil } + return element.entity.Child(index) +} + +func (element *Box) CountChildren () int { + return element.entity.CountChildren() +} + func (element *Box) HandleChildMinimumSizeChange (child tomo.Element) { element.updateMinimumSize() element.entity.Invalidate() diff --git a/elements/cell.go b/elements/cell.go index c030a6e..f9da0db 100644 --- a/elements/cell.go +++ b/elements/cell.go @@ -83,6 +83,12 @@ func (element *Cell) Adopt (child tomo.Element) { element.entity.InvalidateLayout() } +// Child returns this element's child. If there is no child, this method will +// return nil. +func (element *Cell) Child () tomo.Element { + return element.child +} + // Enabled returns whether this cell is enabled or not. func (element *Cell) Enabled () bool { return element.enabled diff --git a/elements/document.go b/elements/document.go index d090d23..ec916a2 100644 --- a/elements/document.go +++ b/elements/document.go @@ -23,11 +23,12 @@ type Document struct { onScrollBoundsChange func () } -func NewDocument () (element *Document) { +func NewDocument (children ...tomo.Element) (element *Document) { element = &Document { } element.scratch = make(map[tomo.Element] scratchEntry) element.theme.Case = tomo.C("tomo", "document") element.entity = tomo.NewEntity(element).(documentEntity) + element.Adopt(children...) return } @@ -109,19 +110,33 @@ func (element *Document) Layout () { } } -func (element *Document) Adopt (child tomo.Element, expand bool) { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: expand } +func (element *Document) Adopt (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: true } + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -func (element *Document) Disown (child tomo.Element) { - index := element.entity.IndexOf(child) - if index < 0 { return } - element.entity.Disown(index) - delete(element.scratch, child) +func (element *Document) AdoptInline (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { expand: false } + } + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) Disown (children ...tomo.Element) { + for _, child := range children { + index := element.entity.IndexOf(child) + if index < 0 { return } + element.entity.Disown(index) + delete(element.scratch, child) + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() @@ -140,6 +155,15 @@ func (element *Document) DisownAll () { element.entity.InvalidateLayout() } +func (element *Document) Child (index int) tomo.Element { + if index < 0 || index >= element.entity.CountChildren() { return nil } + return element.entity.Child(index) +} + +func (element *Document) CountChildren () int { + return element.entity.CountChildren() +} + func (element *Document) HandleChildMinimumSizeChange (child tomo.Element) { element.updateMinimumSize() element.entity.Invalidate() diff --git a/elements/label.go b/elements/label.go index ec3b478..f48b088 100644 --- a/elements/label.go +++ b/elements/label.go @@ -24,20 +24,25 @@ type Label struct { theme theme.Wrapped } -// NewLabel creates a new label. If wrap is set to true, the text inside will be -// wrapped. -func NewLabel (text string, wrap bool) (element *Label) { +// NewLabel creates a new label. +func NewLabel (text string) (element *Label) { element = &Label { } element.theme.Case = tomo.C("tomo", "label") element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) - element.SetWrap(wrap) element.SetText(text) return } +// NewLabelWrapped creates a new label with text wrapping on. +func NewLabelWrapped (text string) (element *Label) { + element = NewLabel(text) + element.SetWrap(true) + return +} + // Entity returns this element's entity. func (element *Label) Entity () tomo.Entity { return element.entity diff --git a/elements/lerpslider.go b/elements/lerpslider.go index f55f190..b230b0f 100644 --- a/elements/lerpslider.go +++ b/elements/lerpslider.go @@ -15,16 +15,20 @@ type LerpSlider[T Numeric] struct { max T } -// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If -// vertical is set to true, the slider will be vertical instead of horizontal. -func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) { +// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. +func NewLerpSlider[T Numeric] ( + min, max T, value T, + orientation Orientation, +) ( + element *LerpSlider[T], +) { if min > max { temp := max max = min min = temp } element = &LerpSlider[T] { - Slider: NewSlider(0, vertical), + Slider: NewSlider(0, orientation), min: min, max: max, } diff --git a/elements/list.go b/elements/list.go index bec6c71..6926835 100644 --- a/elements/list.go +++ b/elements/list.go @@ -29,7 +29,7 @@ type List struct { onScrollBoundsChange func () } -func NewList (columns int, children ...tomo.Selectable) (element *List) { +func NewList (columns int, children ...tomo.Element) (element *List) { if columns < 1 { columns = 1 } element = &List { selected: -1 } element.scratch = make(map[tomo.Element] scratchEntry) @@ -152,6 +152,15 @@ func (element *List) DisownAll () { element.entity.InvalidateLayout() } +func (element *List) Child (index int) tomo.Element { + if index < 0 || index >= element.entity.CountChildren() { return nil } + return element.entity.Child(index) +} + +func (element *List) CountChildren () int { + return element.entity.CountChildren() +} + func (element *List) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { if child, ok := child.(tomo.Selectable); ok { index := element.entity.IndexOf(child) diff --git a/elements/notdone/list.go b/elements/notdone/list.go deleted file mode 100644 index 75d32eb..0000000 --- a/elements/notdone/list.go +++ /dev/null @@ -1,455 +0,0 @@ -package elements - -import "fmt" -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -type listEntity interface { - tomo.FlexibleEntity - tomo.ContainerEntity - tomo.ScrollableEntity -} - -// List is an element that contains several objects that a user can select. -type List struct { - entity listEntity - pressed bool - - contentHeight int - forcedMinimumWidth int - forcedMinimumHeight int - - selectedEntry int - scroll int - - config config.Wrapped - theme theme.Wrapped - - onNoEntrySelected func () - onScrollBoundsChange func () -} - -// NewList creates a new list element with the specified entries. -func NewList (entries ...ListEntry) (element *List) { - element = &List { selectedEntry: -1 } - element.theme.Case = tomo.C("tomo", "list") - - element.entries = make([]ListEntry, len(entries)) - for index, entry := range entries { - element.entries[index] = entry - } - return -} - -// SetTheme sets the element's theme. -func (element *List) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - for index, entry := range element.entries { - entry.SetTheme(element.theme.Theme) - element.entries[index] = entry - } - element.updateMinimumSize() - element.entity.Invalidate() -} - -// SetConfig sets the element's configuration. -func (element *List) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - for index, entry := range element.entries { - entry.SetConfig(element.config) - element.entries[index] = entry - } - element.updateMinimumSize() - element.redo() -} - -// Collapse forces a minimum width and height upon the list. If a zero value is -// given for a dimension, its minimum will be determined by the list's content. -// If the list's height goes beyond the forced size, it will need to be accessed -// via scrolling. If an entry's width goes beyond the forced size, its text will -// be truncated so that it fits. -func (element *List) Collapse (width, height int) { - if - element.forcedMinimumWidth == width && - element.forcedMinimumHeight == height { - - return - } - - element.forcedMinimumWidth = width - element.forcedMinimumHeight = height - element.updateMinimumSize() - - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - element.redo() -} - -func (element *List) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - if !element.Focused() { element.Focus() } - if button != input.ButtonLeft { return } - element.pressed = true - if element.selectUnderMouse(x, y) && element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft { return } - element.pressed = false -} - -func (element *List) HandleMotion (x, y int) { - if element.pressed { - if element.selectUnderMouse(x, y) && element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - } -} - -func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if !element.Enabled() { return } - - altered := false - switch key { - case input.KeyLeft, input.KeyUp: - altered = element.changeSelectionBy(-1) - - case input.KeyRight, input.KeyDown: - altered = element.changeSelectionBy(1) - - case input.KeyEscape: - altered = element.selectEntry(-1) - } - - if altered && element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } - -// ScrollContentBounds returns the full content size of the element. -func (element *List) ScrollContentBounds () (bounds image.Rectangle) { - return image.Rect ( - 0, 0, - 1, element.contentHeight) -} - -// ScrollViewportBounds returns the size and position of the element's viewport -// relative to ScrollBounds. -func (element *List) ScrollViewportBounds () (bounds image.Rectangle) { - return image.Rect ( - 0, element.scroll, - 0, element.scroll + element.scrollViewportHeight()) -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *List) ScrollTo (position image.Point) { - element.scroll = position.Y - if element.scroll < 0 { - element.scroll = 0 - } else if element.scroll > element.maxScrollHeight() { - element.scroll = element.maxScrollHeight() - } - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *List) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -// OnNoEntrySelected sets a function to be called when the user chooses to -// deselect the current selected entry by clicking on empty space within the -// list or by pressing the escape key. -func (element *List) OnNoEntrySelected (callback func ()) { - element.onNoEntrySelected = callback -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *List) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -// CountEntries returns the amount of entries in the list. -func (element *List) CountEntries () (count int) { - return len(element.entries) -} - -// Append adds one or more entries to the end of the list. -func (element *List) Append (entries ...ListEntry) { - // append - for index, entry := range entries { - entry = element.resizeEntryToFit(entry) - entry.SetTheme(element.theme.Theme) - entry.SetConfig(element.config) - entries[index] = entry - } - element.entries = append(element.entries, entries...) - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// EntryAt returns the entry at the specified index. If the index is out of -// bounds, it panics. -func (element *List) EntryAt (index int) (entry ListEntry) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index)) - } - return element.entries[index] -} - -// Insert inserts an entry into the list at the speified index. If the index is -// out of bounds, it is constrained either to zero or len(entries). -func (element *List) Insert (index int, entry ListEntry) { - if index < 0 { index = 0 } - if index > len(element.entries) { index = len(element.entries) } - - // insert - element.entries = append ( - element.entries[:index + 1], - element.entries[index:]...) - entry = element.resizeEntryToFit(entry) - element.entries[index] = entry - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Remove removes the entry at the specified index. If the index is out of -// bounds, it panics. -func (element *List) Remove (index int) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.Remove index out of range: ", index)) - } - - // delete - element.entries = append ( - element.entries[:index], - element.entries[index + 1:]...) - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Clear removes all entries from the list. -func (element *List) Clear () { - element.entries = nil - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Replace replaces the entry at the specified index with another. If the index -// is out of bounds, it panics. -func (element *List) Replace (index int, entry ListEntry) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.Replace index out of range: ", index)) - } - - // replace - entry = element.resizeEntryToFit(entry) - element.entries[index] = entry - - // redraw - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Select selects a specific item in the list. If the index is out of bounds, -// no items will be selecected. -func (element *List) Select (index int) { - if element.selectEntry(index) { - element.redo() - } -} - -func (element *List) selectUnderMouse (x, y int) (updated bool) { - padding := element.theme.Padding(tomo.PatternSunken) - bounds := padding.Apply(element.Bounds()) - mousePoint := image.Pt(x, y) - dot := image.Pt ( - bounds.Min.X, - bounds.Min.Y - element.scroll) - - newlySelectedEntryIndex := -1 - for index, entry := range element.entries { - entryPosition := dot - dot.Y += entry.Bounds().Dy() - if entryPosition.Y > bounds.Max.Y { break } - if mousePoint.In(entry.Bounds().Add(entryPosition)) { - newlySelectedEntryIndex = index - break - } - } - - return element.selectEntry(newlySelectedEntryIndex) -} - -func (element *List) selectEntry (index int) (updated bool) { - if element.selectedEntry == index { return false } - element.selectedEntry = index - if element.selectedEntry < 0 { - if element.onNoEntrySelected != nil { - element.onNoEntrySelected() - } - } else { - element.entries[element.selectedEntry].RunSelect() - } - return true -} - -func (element *List) changeSelectionBy (delta int) (updated bool) { - newIndex := element.selectedEntry + delta - if newIndex < 0 { newIndex = len(element.entries) - 1 } - if newIndex >= len(element.entries) { newIndex = 0 } - return element.selectEntry(newIndex) -} - -func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - bounds := element.Bounds() - padding := element.theme.Padding(tomo.PatternSunken) - entry.Resize(padding.Apply(bounds).Dx()) - return entry -} - -func (element *List) updateMinimumSize () { - element.contentHeight = 0 - for _, entry := range element.entries { - element.contentHeight += entry.Bounds().Dy() - } - - minimumWidth := element.forcedMinimumWidth - minimumHeight := element.forcedMinimumHeight - - if minimumWidth == 0 { - for _, entry := range element.entries { - entryWidth := entry.MinimumWidth() - if entryWidth > minimumWidth { - minimumWidth = entryWidth - } - } - } - - if minimumHeight == 0 { - minimumHeight = element.contentHeight - } - - padding := element.theme.Padding(tomo.PatternSunken) - minimumHeight += padding[0] + padding[2] - - element.core.SetMinimumSize(minimumWidth, minimumHeight) -} - -func (element *List) scrollBoundsChange () { - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *List) scrollViewportHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - return element.Bounds().Dy() - padding[0] - padding[2] -} - -func (element *List) maxScrollHeight () (height int) { - height = - element.contentHeight - - element.scrollViewportHeight() - if height < 0 { height = 0 } - return -} - -func (element *List) Layout () { - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - if element.scroll > element.maxScrollHeight() { - element.scroll = element.maxScrollHeight() - } - element.draw() - element.scrollBoundsChange() -} - -func (element *List) Draw (destination canvas.Canvas) { - bounds := element.Bounds() - padding := element.theme.Padding(tomo.PatternSunken) - innerBounds := padding.Apply(bounds) - state := tomo.State { - Disabled: !element.Enabled(), - Focused: element.Focused(), - } - - dot := image.Point { - innerBounds.Min.X, - innerBounds.Min.Y - element.scroll, - } - innerCanvas := canvas.Cut(element.core, innerBounds) - for index, entry := range element.entries { - entryPosition := dot - dot.Y += entry.Bounds().Dy() - if dot.Y < innerBounds.Min.Y { continue } - if entryPosition.Y > innerBounds.Max.Y { break } - entry.Draw ( - innerCanvas, entryPosition, - element.Focused(), element.selectedEntry == index) - } - - covered := image.Rect ( - 0, 0, - innerBounds.Dx(), element.contentHeight, - ).Add(innerBounds.Min).Intersect(innerBounds) - pattern := element.theme.Pattern(tomo.PatternSunken, state) - artist.DrawShatter ( - element.core, pattern, bounds, covered) -} diff --git a/elements/notdone/listentry.go b/elements/notdone/listentry.go deleted file mode 100644 index d98cf37..0000000 --- a/elements/notdone/listentry.go +++ /dev/null @@ -1,104 +0,0 @@ -package elements - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// ListEntry is an item that can be added to a list. -type ListEntry struct { - drawer textdraw.Drawer - bounds image.Rectangle - text string - width int - minimumWidth int - - config config.Wrapped - theme theme.Wrapped - - onSelect func () -} - -func NewListEntry (text string, onSelect func ()) (entry ListEntry) { - entry = ListEntry { - text: text, - onSelect: onSelect, - } - entry.theme.Case = tomo.C("tomo", "listEntry") - entry.drawer.SetFace (entry.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - entry.drawer.SetText([]rune(text)) - entry.updateBounds() - return -} - -func (entry *ListEntry) SetTheme (new tomo.Theme) { - if new == entry.theme.Theme { return } - entry.theme.Theme = new - entry.drawer.SetFace (entry.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - entry.updateBounds() -} - -func (entry *ListEntry) SetConfig (new tomo.Config) { - if new == entry.config.Config { return } - entry.config.Config = new -} - -func (entry *ListEntry) updateBounds () { - padding := entry.theme.Padding(tomo.PatternRaised) - entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds()) - entry.bounds = entry.bounds.Sub(entry.bounds.Min) - entry.minimumWidth = entry.bounds.Dx() - entry.bounds.Max.X = entry.width -} - -func (entry *ListEntry) Draw ( - destination canvas.Canvas, - offset image.Point, - focused bool, - on bool, -) ( - updatedRegion image.Rectangle, -) { - state := tomo.State { - Focused: focused, - On: on, - } - - pattern := entry.theme.Pattern(tomo.PatternRaised, state) - padding := entry.theme.Padding(tomo.PatternRaised) - bounds := entry.Bounds().Add(offset) - pattern.Draw(destination, bounds) - - foreground := entry.theme.Color (tomo.ColorForeground, state) - return entry.drawer.Draw ( - destination, - foreground, - offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])). - Sub(entry.drawer.LayoutBounds().Min)) -} - -func (entry *ListEntry) RunSelect () { - if entry.onSelect != nil { - entry.onSelect() - } -} - -func (entry *ListEntry) Bounds () (bounds image.Rectangle) { - return entry.bounds -} - -func (entry *ListEntry) Resize (width int) { - entry.width = width - entry.updateBounds() -} - -func (entry *ListEntry) MinimumWidth () (width int) { - return entry.minimumWidth -} diff --git a/elements/progressbar.go b/elements/progressbar.go index c0d2963..5c96daa 100644 --- a/elements/progressbar.go +++ b/elements/progressbar.go @@ -18,6 +18,8 @@ type ProgressBar struct { // NewProgressBar creates a new progress bar displaying the given progress // level. func NewProgressBar (progress float64) (element *ProgressBar) { + if progress < 0 { progress = 0 } + if progress > 1 { progress = 1 } element = &ProgressBar { progress: progress } element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "progressBar") @@ -48,6 +50,8 @@ func (element *ProgressBar) Draw (destination canvas.Canvas) { // SetProgress sets the progress level of the bar. func (element *ProgressBar) SetProgress (progress float64) { + if progress < 0 { progress = 0 } + if progress > 1 { progress = 1 } if progress == element.progress { return } element.progress = progress element.entity.Invalidate() diff --git a/elements/scroll.go b/elements/scroll.go index 7ecfb25..929743b 100644 --- a/elements/scroll.go +++ b/elements/scroll.go @@ -7,6 +7,19 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +type ScrollMode int; const ( + ScrollNeither ScrollMode = 0 + ScrollVertical = 1 + ScrollHorizontal = 2 + ScrollBoth = ScrollVertical | ScrollHorizontal +) + +// Includes returns whether a scroll mode has been or'd with another scroll +// mode. +func (mode ScrollMode) Includes (sub ScrollMode) bool { + return (mode & sub) > 0 +} + type Scroll struct { entity tomo.ContainerEntity @@ -18,12 +31,12 @@ type Scroll struct { theme theme.Wrapped } -func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scroll) { +func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) { element = &Scroll { } element.theme.Case = tomo.C("tomo", "scroll") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) - if horizontal { + if mode.Includes(ScrollHorizontal) { element.horizontal = NewScrollBar(false) element.horizontal.OnScroll (func (viewport image.Point) { if element.child != nil { @@ -37,7 +50,7 @@ func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scro }) element.entity.Adopt(element.horizontal) } - if vertical { + if mode.Includes(ScrollVertical) { element.vertical = NewScrollBar(true) element.vertical.OnScroll (func (viewport image.Point) { if element.child != nil { diff --git a/elements/scrollbar.go b/elements/scrollbar.go index 8835bde..f64731b 100644 --- a/elements/scrollbar.go +++ b/elements/scrollbar.go @@ -7,6 +7,13 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +// Orientation represents an orientation configuration that can be passed to +// scrollbars and sliders. +type Orientation bool; const ( + Vertical Orientation = true + Horizontal = false +) + // ScrollBar is an element similar to Slider, but it has special behavior that // makes it well suited for controlling the viewport position on one axis of a // scrollable element. Instead of having a value from zero to one, it stores @@ -37,14 +44,13 @@ type ScrollBar struct { onScroll func (viewport image.Point) } -// NewScrollBar creates a new scroll bar. If vertical is set to true, the scroll -// bar will be vertical instead of horizontal. -func NewScrollBar (vertical bool) (element *ScrollBar) { +// NewScrollBar creates a new scroll bar. +func NewScrollBar (orientation Orientation) (element *ScrollBar) { element = &ScrollBar { - vertical: vertical, + vertical: bool(orientation), enabled: true, } - if vertical { + if orientation == Vertical { element.theme.Case = tomo.C("tomo", "scrollBarHorizontal") } else { element.theme.Case = tomo.C("tomo", "scrollBarVertical") diff --git a/elements/slider.go b/elements/slider.go index 1e6da8e..f14903b 100644 --- a/elements/slider.go +++ b/elements/slider.go @@ -26,14 +26,13 @@ type Slider struct { onRelease func () } -// NewSlider creates a new slider with the specified value. If vertical is set -// to true, -func NewSlider (value float64, vertical bool) (element *Slider) { +// NewSlider creates a new slider with the specified value. +func NewSlider (value float64, orientation Orientation) (element *Slider) { element = &Slider { value: value, - vertical: vertical, + vertical: bool(orientation), } - if vertical { + if orientation == Vertical { element.theme.Case = tomo.C("tomo", "sliderVertical") } else { element.theme.Case = tomo.C("tomo", "sliderHorizontal") diff --git a/elements/spacer.go b/elements/spacer.go index a382b44..9b0ff4d 100644 --- a/elements/spacer.go +++ b/elements/spacer.go @@ -15,17 +15,22 @@ type Spacer struct { theme theme.Wrapped } -// NewSpacer creates a new spacer. If line is set to true, the spacer will be -// filled with a line color, and if compressed to its minimum width or height, -// will appear as a line. -func NewSpacer (line bool) (element *Spacer) { - element = &Spacer { line: line } +// NewSpacer creates a new spacer. +func NewSpacer () (element *Spacer) { + element = &Spacer { } element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "spacer") element.updateMinimumSize() return } +// NewLine creates a new line separator. +func NewLine () (element *Spacer) { + element = NewSpacer() + element.SetLine(true) + return +} + // Entity returns this element's entity. func (element *Spacer) Entity () tomo.Entity { return element.entity diff --git a/examples/align/main.go b/examples/align/main.go index ecee3fe..1857b42 100644 --- a/examples/align/main.go +++ b/examples/align/main.go @@ -13,23 +13,18 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256)) window.SetTitle("Text alignment") - container := elements.NewDocument() - - left := elements.NewLabel(text, true) - center := elements.NewLabel(text, true) - right := elements.NewLabel(text, true) - justify := elements.NewLabel(text, true) + left := elements.NewLabelWrapped(text) + center := elements.NewLabelWrapped(text) + right := elements.NewLabelWrapped(text) + justify := elements.NewLabelWrapped(text) left.SetAlign(textdraw.AlignLeft) center.SetAlign(textdraw.AlignCenter) right.SetAlign(textdraw.AlignRight) justify.SetAlign(textdraw.AlignJustify) - container.Adopt(left, true) - container.Adopt(center, true) - container.Adopt(right, true) - container.Adopt(justify, true) - window.Adopt(elements.NewScroll(container, false, true)) + window.Adopt (elements.NewScroll (elements.ScrollVertical, + elements.NewDocument(left, center, right, justify))) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index 519e82d..549e54d 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -13,23 +13,15 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Checkboxes") - container := elements.NewVBox(true, true) - window.Adopt(container) - - introText := elements.NewLabel ( + introText := elements.NewLabelWrapped ( "We advise you to not read thPlease listen to me. I am " + "trapped inside the example code. This is the only way for " + - "me to communicate.", true) + "me to communicate.") introText.EmCollapse(0, 5) - container.Adopt(introText, true) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(elements.NewCheckbox("Oh god", false), false) - container.Adopt(elements.NewCheckbox("Can you hear them", true), false) - container.Adopt(elements.NewCheckbox("They are in the walls", false), false) - container.Adopt(elements.NewCheckbox("They are coming for us", false), false) + disabledCheckbox := elements.NewCheckbox("We are but their helpless prey", false) disabledCheckbox.SetEnabled(false) - container.Adopt(disabledCheckbox, false) + vsync := elements.NewCheckbox("Enable vsync", false) vsync.OnToggle (func () { if vsync.Value() { @@ -40,12 +32,23 @@ func run () { "That doesn't do anything.") } }) - container.Adopt(vsync, false) + button := elements.NewButton("What") button.OnClick(tomo.Stop) - container.Adopt(button, false) - button.Focus() + + box := elements.NewVBox(elements.SpaceBoth) + box.AdoptExpand(introText) + box.Adopt ( + elements.NewLine(), + elements.NewCheckbox("Oh god", false), + elements.NewCheckbox("Can you hear them", true), + elements.NewCheckbox("They are in the walls", false), + elements.NewCheckbox("They are coming for us", false), + disabledCheckbox, + vsync, button) + window.Adopt(box) + button.Focus() window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index f11cab5..cfdc921 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -25,9 +25,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) window.SetTitle("Clipboard") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) textInput := elements.NewTextBox("", "") - controlRow := elements.NewHBox(false, true) + controlRow := elements.NewHBox(elements.SpaceMargin) copyButton := elements.NewButton("Copy") copyButton.SetIcon(tomo.IconCopy) pasteButton := elements.NewButton("Paste") @@ -107,11 +107,11 @@ func run () { window.Paste(imageClipboardCallback, validImageTypes...) }) - container.Adopt(textInput, true) - controlRow.Adopt(copyButton, true) - controlRow.Adopt(pasteButton, true) - controlRow.Adopt(pasteImageButton, true) - container.Adopt(controlRow, false) + container.AdoptExpand(textInput) + controlRow.AdoptExpand(copyButton) + controlRow.AdoptExpand(pasteButton) + controlRow.AdoptExpand(pasteImageButton) + container.Adopt(controlRow) window.Adopt(container) window.OnClose(tomo.Stop) @@ -121,13 +121,13 @@ func run () { func imageWindow (parent tomo.Window, image image.Image) { window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Clipboard Image") - container := containers.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) closeButton := elements.NewButton("Ok") closeButton.SetIcon(tomo.IconYes) closeButton.OnClick(window.Close) - container.Adopt(elements.NewImage(image), true) - container.Adopt(closeButton, false) + container.AdoptExpand(elements.NewImage(image)) + container.Adopt(closeButton) window.Adopt(container) window.Show() } diff --git a/popups/dialog.go b/popups/dialog.go index af91ead..722cc6f 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -43,9 +43,9 @@ func NewDialog ( } window.SetTitle(title) - box := elements.NewVBox(true, true) - messageRow := elements.NewHBox(false, true) - controlRow := elements.NewHBox(false, true) + box := elements.NewVBox(elements.SpaceBoth) + messageRow := elements.NewHBox(elements.SpaceMargin) + controlRow := elements.NewHBox(elements.SpaceMargin) iconId := tomo.IconInformation switch kind { @@ -55,19 +55,19 @@ func NewDialog ( case DialogKindError: iconId = tomo.IconError } - messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false) - messageRow.Adopt(elements.NewLabel(message, false), true) + messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge)) + messageRow.AdoptExpand(elements.NewLabel(message)) - controlRow.Adopt(elements.NewSpacer(false), true) - box.Adopt(messageRow, true) - box.Adopt(controlRow, false) + controlRow.AdoptExpand(elements.NewSpacer()) + box.AdoptExpand(messageRow) + box.Adopt(controlRow) window.Adopt(box) if len(buttons) == 0 { button := elements.NewButton("OK") button.SetIcon(tomo.IconYes) button.OnClick(window.Close) - controlRow.Adopt(button, false) + controlRow.Adopt(button) button.Focus() } else { var button *elements.Button @@ -78,7 +78,7 @@ func NewDialog ( buttonDescriptor.OnPress() window.Close() }) - controlRow.Adopt(button, false) + controlRow.Adopt(button) } button.Focus() } From 7cdc5868e5c396d9f9417ef0aa324a5087d618e3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 16:18:30 -0400 Subject: [PATCH 37/41] Updated the examples --- elements/box.go | 13 +++- elements/containers/common.go | 104 +++++++++++++++++++++++++++++ examples/clipboard/main.go | 2 + examples/documentContainer/main.go | 66 +++++++++--------- examples/flow/main.go | 47 ++++++------- examples/goroutines/main.go | 8 +-- examples/hbox/main.go | 8 +-- examples/icons/main.go | 24 ++++--- examples/image/image.go | 9 ++- examples/input/main.go | 11 +-- examples/label/main.go | 2 +- examples/list/main.go | 25 +++---- examples/panels/main.go | 10 +-- examples/popups/main.go | 18 ++--- examples/progress/main.go | 9 ++- examples/raycaster/main.go | 16 ++--- examples/scroll/main.go | 23 ++++--- examples/spacer/main.go | 16 ++--- examples/switch/main.go | 8 +-- examples/vbox/main.go | 21 +++--- 20 files changed, 271 insertions(+), 169 deletions(-) create mode 100644 elements/containers/common.go diff --git a/elements/box.go b/elements/box.go index 29b3e13..143060d 100644 --- a/elements/box.go +++ b/elements/box.go @@ -54,9 +54,16 @@ func NewHBox (space Space, children ...tomo.Element) (element *Box) { } // NewHBox creates a new vertical box. -func NewVBox (space Space) (element *Box) { - element = NewHBox(space) - element.vertical = true +func NewVBox (space Space, children ...tomo.Element) (element *Box) { + element = &Box { + padding: space.Includes(SpacePadding), + margin: space.Includes(SpaceMargin), + vertical: true, + } + element.scratch = make(map[tomo.Element] scratchEntry) + element.theme.Case = tomo.C("tomo", "box") + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.Adopt(children...) return } diff --git a/elements/containers/common.go b/elements/containers/common.go new file mode 100644 index 0000000..d65e71c --- /dev/null +++ b/elements/containers/common.go @@ -0,0 +1,104 @@ +package containers + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" + +type childManager struct { + onChange func () + children []tomo.LayoutEntry + parent tomo.Parent + theme theme.Wrapped + config config.Wrapped +} + +// Adopt adds a new child element to the container. If expand is set to true, +// the element will expand (instead of contract to its minimum size), in +// whatever way is defined by the container's layout. +func (manager *childManager) Adopt (child tomo.Element, expand bool) { + if child0, ok := child.(tomo.Themeable); ok { + child0.SetTheme(manager.theme.Theme) + } + if child0, ok := child.(tomo.Configurable); ok { + child0.SetConfig(manager.config.Config) + } + child.SetParent(manager.parent) + + manager.children = append (manager.children, tomo.LayoutEntry { + Element: child, + Expand: expand, + }) + + manager.onChange() +} + + +// Disown removes the given child from the container if it is contained within +// it. +func (manager *childManager) Disown (child tomo.Element) { + for index, entry := range manager.children { + if entry.Element == child { + manager.clearChildEventHandlers(entry.Element) + manager.children = append ( + manager.children[:index], + manager.children[index + 1:]...) + break + } + } + + manager.onChange() +} + +// DisownAll removes all child elements from the container at once. +func (manager *childManager) DisownAll () { + for _, entry := range manager.children { + manager.clearChildEventHandlers(entry.Element) + } + manager.children = nil + + manager.onChange() +} + +// Children returns a slice containing this element's children. +func (manager *childManager) Children () (children []tomo.Element) { + children = make([]tomo.Element, len(manager.children)) + for index, entry := range manager.children { + children[index] = entry.Element + } + return +} + +// CountChildren returns the amount of children contained within this element. +func (manager *childManager) CountChildren () (count int) { + return len(manager.children) +} + +// Child returns the child at the specified index. If the index is out of +// bounds, this method will return nil. +func (manager *childManager) Child (index int) (child tomo.Element) { + if index < 0 || index > len(manager.children) { return } + return manager.children[index].Element +} + +// ChildAt returns the child that contains the specified x and y coordinates. If +// there are no children at the coordinates, this method will return nil. +func (manager *childManager) ChildAt (point image.Point) (child tomo.Element) { + for _, entry := range manager.children { + if point.In(entry.Bounds) { + child = entry.Element + } + } + return +} + +func (manager *childManager) clearChildEventHandlers (child tomo.Element) { + child.DrawTo(nil, image.Rectangle { }, nil) + child.SetParent(nil) + + if child, ok := child.(tomo.Focusable); ok { + if child.Focused() { + child.HandleUnfocus() + } + } +} diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index cfdc921..4102686 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -129,5 +129,7 @@ func imageWindow (parent tomo.Window, image image.Image) { container.AdoptExpand(elements.NewImage(image)) container.Adopt(closeButton) window.Adopt(container) + + closeButton.Focus() window.Show() } diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go index eab82cd..146137b 100644 --- a/examples/documentContainer/main.go +++ b/examples/documentContainer/main.go @@ -22,44 +22,40 @@ func run () { if err != nil { panic(err.Error()); return } document := elements.NewDocument() - - document.Adopt (elements.NewLabel ( - "A document container is a vertically stacked container " + - "capable of properly laying out flexible elements such as " + - "text-wrapped labels. You can also include normal elements " + - "like:", true), true) - document.Adopt (elements.NewButton ( - "Buttons,"), true) - document.Adopt (elements.NewCheckbox ( - "Checkboxes,", true), true) - document.Adopt(elements.NewTextBox("", "And text boxes."), true) - document.Adopt (elements.NewSpacer(true), true) - document.Adopt (elements.NewLabel ( - "Document containers are meant to be placed inside of a " + - "ScrollContainer, like this one.", true), true) - document.Adopt (elements.NewLabel ( - "You could use document containers to do things like display various " + - "forms of hypertext (like HTML, gemtext, markdown, etc.), " + - "lay out a settings menu with descriptive label text between " + - "control groups like in iOS, or list comment or chat histories.", - true), true) - document.Adopt(elements.NewImage(logo), true) - document.Adopt (elements.NewLabel ( - "You can also choose whether each element is on its own line " + - "(sort of like an HTML/CSS block element) or on a line with " + - "other adjacent elements (like an HTML/CSS inline element).", - true), true) - document.Adopt(elements.NewButton("Just"), false) - document.Adopt(elements.NewButton("like"), false) - document.Adopt(elements.NewButton("this."), false) - document.Adopt (elements.NewLabel ( - "Oh, you're a switch? Then name all of these switches:", - true), true) + document.Adopt ( + elements.NewLabelWrapped ( + "A document container is a vertically stacked container " + + "capable of properly laying out flexible elements such as " + + "text-wrapped labels. You can also include normal elements " + + "like:"), + elements.NewButton("Buttons,"), + elements.NewCheckbox("Checkboxes,", true), + elements.NewTextBox("", "And text boxes."), + elements.NewLine(), + elements.NewLabelWrapped ( + "Document containers are meant to be placed inside of a " + + "ScrollContainer, like this one."), + elements.NewLabelWrapped ( + "You could use document containers to do things like display various " + + "forms of hypertext (like HTML, gemtext, markdown, etc.), " + + "lay out a settings menu with descriptive label text between " + + "control groups like in iOS, or list comment or chat histories."), + elements.NewImage(logo), + elements.NewLabelWrapped ( + "You can also choose whether each element is on its own line " + + "(sort of like an HTML/CSS block element) or on a line with " + + "other adjacent elements (like an HTML/CSS inline element).")) + document.AdoptInline ( + elements.NewButton("Just"), + elements.NewButton("like"), + elements.NewButton("this.")) + document.Adopt (elements.NewLabelWrapped ( + "Oh, you're a switch? Then name all of these switches:")) for i := 0; i < 30; i ++ { - document.Adopt(elements.NewSwitch("", false), false) + document.AdoptInline(elements.NewSwitch("", false)) } - window.Adopt(elements.NewScroll(document, false, true)) + window.Adopt(elements.NewScroll(elements.ScrollVertical, document)) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/flow/main.go b/examples/flow/main.go index 381eb40..dac69cc 100644 --- a/examples/flow/main.go +++ b/examples/flow/main.go @@ -12,15 +12,15 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192)) window.SetTitle("adventure") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) var world flow.Flow world.Transition = container.DisownAll world.Stages = map [string] func () { "start": func () { - label := elements.NewLabel ( - "you are standing next to a river.", true) + label := elements.NewLabelWrapped ( + "you are standing next to a river.") button0 := elements.NewButton("go in the river") button0.OnClick(world.SwitchFunc("wet")) @@ -29,70 +29,65 @@ func run () { button2 := elements.NewButton("turn around") button2.OnClick(world.SwitchFunc("bear")) - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - container.Adopt(button2, false) + container.AdoptExpand(label) + container.Adopt(button0, button1, button2) button0.Focus() }, "wet": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you get completely soaked.\n" + - "you die of hypothermia.", true) + "you die of hypothermia.") button0 := elements.NewButton("try again") button0.OnClick(world.SwitchFunc("start")) button1 := elements.NewButton("exit") button1.OnClick(tomo.Stop) - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) + container.AdoptExpand(label) + container.Adopt(button0, button1) button0.Focus() }, "house": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you are standing in front of a delapidated " + - "house.", true) + "house.") button1 := elements.NewButton("go inside") button1.OnClick(world.SwitchFunc("inside")) button0 := elements.NewButton("turn back") button0.OnClick(world.SwitchFunc("start")) - container.Adopt(label, true) - container.Adopt(button1, false) - container.Adopt(button0, false) + container.AdoptExpand(label) + container.Adopt(button0, button1) button1.Focus() }, "inside": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you are standing inside of the house.\n" + "it is dark, but rays of light stream " + "through the window.\n" + "there is nothing particularly interesting " + - "here.", true) + "here.") button0 := elements.NewButton("go back outside") button0.OnClick(world.SwitchFunc("house")) - container.Adopt(label, true) - container.Adopt(button0, false) + container.AdoptExpand(label) + container.Adopt(button0) button0.Focus() }, "bear": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you come face to face with a bear.\n" + - "it eats you (it was hungry).", true) + "it eats you (it was hungry).") button0 := elements.NewButton("try again") button0.OnClick(world.SwitchFunc("start")) button1 := elements.NewButton("exit") button1.OnClick(tomo.Stop) - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) + container.AdoptExpand(label) + container.Adopt(button0, button1) button0.Focus() }, } diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index 29f547a..5df9198 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -15,13 +15,13 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) window.SetTitle("Clock") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) clock := fun.NewAnalogClock(time.Now()) - container.Adopt(clock, true) - label := elements.NewLabel(formatTime(), false) - container.Adopt(label, false) + label := elements.NewLabel(formatTime()) + container.AdoptExpand(clock) + container.Adopt(label) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/hbox/main.go b/examples/hbox/main.go index ed5a112..b469ae7 100644 --- a/examples/hbox/main.go +++ b/examples/hbox/main.go @@ -12,12 +12,12 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("horizontal stack") - container := elements.NewHBox(true, true) + container := elements.NewHBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewLabel("this is sample text", true), true) - container.Adopt(elements.NewLabel("this is sample text", true), true) - container.Adopt(elements.NewLabel("this is sample text", true), true) + container.AdoptExpand(elements.NewLabelWrapped("this is sample text")) + container.AdoptExpand(elements.NewLabelWrapped("this is sample text")) + container.AdoptExpand(elements.NewLabelWrapped("this is sample text")) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/icons/main.go b/examples/icons/main.go index 5e17d2a..01ed2f5 100644 --- a/examples/icons/main.go +++ b/examples/icons/main.go @@ -12,29 +12,31 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("Icons") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewLabel("Just some of the wonderful icons we have:", false), false) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(icons(tomo.IconHome, tomo.IconHistory), true) - container.Adopt(icons(tomo.IconFile, tomo.IconNetwork), true) - container.Adopt(icons(tomo.IconOpen, tomo.IconRemoveFavorite), true) - container.Adopt(icons(tomo.IconCursor, tomo.IconDistort), true) + container.Adopt ( + elements.NewLabel("Just some of the wonderful icons we have:"), + elements.NewLine()) + container.AdoptExpand ( + icons(tomo.IconHome, tomo.IconHistory), + icons(tomo.IconFile, tomo.IconNetwork), + icons(tomo.IconOpen, tomo.IconRemoveFavorite), + icons(tomo.IconCursor, tomo.IconDistort)) closeButton := elements.NewButton("Yes verynice") closeButton.SetIcon(tomo.IconYes) closeButton.OnClick(tomo.Stop) - container.Adopt(closeButton, false) + container.Adopt(closeButton) window.OnClose(tomo.Stop) window.Show() } -func icons (min, max tomo.Icon) (container *containers.Box) { - container = containers.NewHBox(false, true) +func icons (min, max tomo.Icon) (container *elements.Box) { + container = elements.NewHBox(elements.SpaceMargin) for index := min; index <= max; index ++ { - container.Adopt(elements.NewIcon(index, tomo.IconSizeSmall), true) + container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall)) } return } diff --git a/examples/image/image.go b/examples/image/image.go index 5731ac9..0dc2100 100644 --- a/examples/image/image.go +++ b/examples/image/image.go @@ -9,7 +9,6 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -25,7 +24,7 @@ func run () { file.Close() if err != nil { fatalError(window, err); return } - container := containers.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) logoImage := elements.NewImage(logo) button := elements.NewButton("Show me a gopher instead") button.OnClick (func () { @@ -34,11 +33,11 @@ func run () { gopher, _, err := image.Decode(bytes.NewReader(gopher.GopherPng())) if err != nil { fatalError(window, err); return } - container.Adopt(elements.NewImage(gopher),true) + container.AdoptExpand(elements.NewImage(gopher)) }) - container.Adopt(logoImage, true) - container.Adopt(button, false) + container.AdoptExpand(logoImage) + container.Adopt(button) window.Adopt(container) button.Focus() diff --git a/examples/input/main.go b/examples/input/main.go index b0e8a84..e8035c0 100644 --- a/examples/input/main.go +++ b/examples/input/main.go @@ -12,7 +12,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Enter Details") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) // create inputs @@ -45,13 +45,8 @@ func run () { fingerLength.OnChange(check) // add elements to container - container.Adopt(elements.NewLabel("Choose your words carefully.", false), true) - container.Adopt(firstName, false) - container.Adopt(lastName, false) - container.Adopt(fingerLength, false) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(button, false) - + container.AdoptExpand(elements.NewLabel("Choose your words carefully.")) + container.Adopt(firstName, lastName, fingerLength, elements.NewLine(), button) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/label/main.go b/examples/label/main.go index 0429587..96dc6a8 100644 --- a/examples/label/main.go +++ b/examples/label/main.go @@ -11,7 +11,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360)) window.SetTitle("example label") - window.Adopt(elements.NewLabel(text, true)) + window.Adopt(elements.NewLabelWrapped(text)) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/list/main.go b/examples/list/main.go index 9f49fee..1e4fbc4 100644 --- a/examples/list/main.go +++ b/examples/list/main.go @@ -14,7 +14,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0)) window.SetTitle("List Sidebar") - container := elements.NewHBox(true, true) + container := elements.NewHBox(elements.SpaceBoth) window.Adopt(container) var currentPage tomo.Element @@ -22,29 +22,30 @@ func run () { if currentPage != nil { container.Disown(currentPage) } - container.Adopt(newPage, true) + container.AdoptExpand(newPage) currentPage = newPage } - intro := elements.NewLabel ( + intro := elements.NewLabelWrapped ( "The List element can be easily used as a sidebar. " + - "Click on entries to flip pages!", true) + "Click on entries to flip pages!") button := elements.NewButton("I do nothing!") button.OnClick (func () { popups.NewDialog(popups.DialogKindInfo, window, "", "Sike!") }) mouse := testing.NewMouse() input := elements.NewTextBox("Write some text", "") - form := elements.NewVBox(false, true) - form.Adopt(elements.NewLabel("I have:", false), false) - form.Adopt(elements.NewSpacer(true), false) - form.Adopt(elements.NewCheckbox("Skin", true), false) - form.Adopt(elements.NewCheckbox("Blood", false), false) - form.Adopt(elements.NewCheckbox("Bone", false), false) + form := elements.NewVBox ( + elements.SpaceMargin, + elements.NewLabel("I have:"), + elements.NewLine(), + elements.NewCheckbox("Skin", true), + elements.NewCheckbox("Blood", false), + elements.NewCheckbox("Bone", false)) art := testing.NewArtist() makePage := func (name string, callback func ()) tomo.Selectable { - cell := elements.NewCell(elements.NewLabel(name, false)) + cell := elements.NewCell(elements.NewLabel(name)) cell.OnSelectionChange (func () { if cell.Selected() { callback() } }) @@ -60,7 +61,7 @@ func run () { makePage("art", func () { turnPage(art) })) list.Collapse(96, 0) - container.Adopt(list, false) + container.Adopt(list) turnPage(intro) window.OnClose(tomo.Stop) diff --git a/examples/panels/main.go b/examples/panels/main.go index 0141795..b28b204 100644 --- a/examples/panels/main.go +++ b/examples/panels/main.go @@ -14,8 +14,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256)) window.SetTitle("Main") - container := elements.NewVBox(true, true) - container.Adopt(elements.NewLabel("Main window", false), true) + container := elements.NewVBox ( + elements.SpaceBoth, + elements.NewLabel("Main window")) window.Adopt(container) window.OnClose(tomo.Stop) @@ -31,8 +32,9 @@ func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) { window, _ := parent.NewPanel(bounds) title := fmt.Sprint("Panel #", id) window.SetTitle(title) - container := containers.NewVBox(true, true) - container.Adopt(elements.NewLabel(title, false), true) + container := elements.NewVBox ( + elements.SpaceBoth, + elements.NewLabel(title)) window.Adopt(container) window.Show() } diff --git a/examples/popups/main.go b/examples/popups/main.go index eb4f395..4b2c83a 100644 --- a/examples/popups/main.go +++ b/examples/popups/main.go @@ -14,10 +14,10 @@ func run () { if err != nil { panic(err.Error()) } window.SetTitle("Dialog Boxes") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewLabel("Try out different dialogs:", false), true) + container.AdoptExpand(elements.NewLabel("Try out different dialogs:")) infoButton := elements.NewButton("popups.DialogKindInfo") infoButton.OnClick (func () { @@ -27,7 +27,7 @@ func run () { "Information", "You are wacky") }) - container.Adopt(infoButton, false) + container.Adopt(infoButton) infoButton.Focus() questionButton := elements.NewButton("popups.DialogKindQuestion") @@ -41,7 +41,7 @@ func run () { popups.Button { "No", func () { } }, popups.Button { "Not sure", func () { } }) }) - container.Adopt(questionButton, false) + container.Adopt(questionButton) warningButton := elements.NewButton("popups.DialogKindWarning") warningButton.OnClick (func () { @@ -51,7 +51,7 @@ func run () { "Warning", "They are fast approaching.") }) - container.Adopt(warningButton, false) + container.Adopt(warningButton) errorButton := elements.NewButton("popups.DialogKindError") errorButton.OnClick (func () { @@ -61,7 +61,7 @@ func run () { "Error", "There is nowhere left to go.") }) - container.Adopt(errorButton, false) + container.Adopt(errorButton) menuButton := elements.NewButton("menu") menuButton.OnClick (func () { @@ -70,14 +70,14 @@ func run () { tomo.Bounds(0, 0, 64, 64). Add(menuButton.Entity().Bounds().Min)) if err != nil { println(err.Error()) } - menu.Adopt(elements.NewLabel("I'm a shy window...", true)) + menu.Adopt(elements.NewLabelWrapped("I'm a shy window...")) menu.Show() }) - container.Adopt(menuButton, false) + container.Adopt(menuButton) cancelButton := elements.NewButton("No thank you.") cancelButton.OnClick(tomo.Stop) - container.Adopt(cancelButton, false) + container.Adopt(cancelButton) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/progress/main.go b/examples/progress/main.go index 33831b9..0fad66b 100644 --- a/examples/progress/main.go +++ b/examples/progress/main.go @@ -13,16 +13,15 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Approaching") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt (elements.NewLabel ( - "Rapidly approaching your location...", false), false) + container.AdoptExpand(elements.NewLabel("Rapidly approaching your location...")) bar := elements.NewProgressBar(0) - container.Adopt(bar, false) + container.Adopt(bar) button := elements.NewButton("Stop") button.SetEnabled(false) - container.Adopt(button, false) + container.Adopt(button) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go index b0c8906..46e0471 100644 --- a/examples/raycaster/main.go +++ b/examples/raycaster/main.go @@ -21,7 +21,7 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480)) window.SetTitle("Raycaster") - container := elements.NewVBox(false, false) + container := elements.NewVBox(elements.SpaceNone) window.Adopt(container) wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes)) @@ -48,16 +48,16 @@ func run () { wallTexture, }) - topBar := containers.NewHBox(true, true) + topBar := elements.NewHBox(elements.SpaceBoth) staminaBar := elements.NewProgressBar(game.Stamina()) healthBar := elements.NewProgressBar(game.Health()) - topBar.Adopt(elements.NewLabel("Stamina:", false), false) - topBar.Adopt(staminaBar, true) - topBar.Adopt(elements.NewLabel("Health:", false), false) - topBar.Adopt(healthBar, true) - container.Adopt(topBar, false) - container.Adopt(game, true) + topBar.Adopt(elements.NewLabel("Stamina:")) + topBar.AdoptExpand(staminaBar) + topBar.Adopt(elements.NewLabel("Health:")) + topBar.AdoptExpand(healthBar) + container.Adopt(topBar) + container.AdoptExpand(game) game.Focus() game.OnStatUpdate (func () { diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 83aa521..05cbbdf 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -12,12 +12,12 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240)) window.SetTitle("Scroll") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) textBox := elements.NewTextBox("", copypasta) - disconnectedContainer := elements.NewHBox(false, true) + disconnectedContainer := elements.NewHBox(elements.SpaceMargin) list := elements.NewList ( 2, elements.NewCell(elements.NewCheckbox("Item 0", true)), @@ -42,8 +42,9 @@ func run () { elements.NewCell(elements.NewCheckbox("Item 19", false)), elements.NewCell(elements.NewCheckbox("Item 20", true)), elements.NewCell(elements.NewCheckbox("Item 21", false)), - elements.NewCell (elements.NewScroll (elements.NewTextBox ( - "", "I bet you weren't expecting this!"), true, false))) + elements.NewCell(elements.NewScroll ( + elements.ScrollHorizontal, + elements.NewTextBox("", "I bet you weren't expecting this!")))) list.Collapse(0, 32) scrollBar := elements.NewScrollBar(true) list.OnScrollBoundsChange (func () { @@ -55,16 +56,16 @@ func run () { list.ScrollTo(viewport) }) - container.Adopt(elements.NewLabel("A ScrollContainer:", false), false) - container.Adopt(elements.NewScroll(textBox, true, false), false) - disconnectedContainer.Adopt(list, false) - disconnectedContainer.Adopt (elements.NewLabel ( + container.Adopt(elements.NewLabel("A ScrollContainer:")) + container.Adopt(elements.NewScroll(elements.ScrollHorizontal, textBox)) + disconnectedContainer.Adopt(list) + disconnectedContainer.AdoptExpand(elements.NewLabelWrapped ( "Notice how the scroll bar to the right can be used to " + "control the list, despite not even touching it. It is " + "indeed a thing you can do. It is also terrible UI design so " + - "don't do it.", true), true) - disconnectedContainer.Adopt(scrollBar, false) - container.Adopt(disconnectedContainer, true) + "don't do it.")) + disconnectedContainer.Adopt(scrollBar) + container.AdoptExpand(disconnectedContainer) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/spacer/main.go b/examples/spacer/main.go index e74dbba..a537a37 100644 --- a/examples/spacer/main.go +++ b/examples/spacer/main.go @@ -12,15 +12,15 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Spaced Out") - container := elements.NewVBox(true, true) - window.Adopt(container) - - container.Adopt (elements.NewLabel("This is at the top", false), false) - container.Adopt (elements.NewSpacer(true), false) - container.Adopt (elements.NewLabel("This is in the middle", false), false) - container.Adopt (elements.NewSpacer(false), true) - container.Adopt (elements.NewLabel("This is at the bottom", false), false) + container := elements.NewVBox ( + elements.SpaceBoth, + elements.NewLabel("This is at the top"), + elements.NewLine(), + elements.NewLabel("This is in the middle")) + container.AdoptExpand(elements.NewSpacer()) + container.Adopt(elements.NewLabel("This is at the bottom")) + window.Adopt(container) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/switch/main.go b/examples/switch/main.go index fa38c92..70c2b88 100644 --- a/examples/switch/main.go +++ b/examples/switch/main.go @@ -12,12 +12,12 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Switches") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewSwitch("hahahah", false), false) - container.Adopt(elements.NewSwitch("hehehehheheh", false), false) - container.Adopt(elements.NewSwitch("you can flick da swicth", false), false) + container.Adopt(elements.NewSwitch("hahahah", false)) + container.Adopt(elements.NewSwitch("hehehehheheh", false)) + container.Adopt(elements.NewSwitch("you can flick da swicth", false)) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/vbox/main.go b/examples/vbox/main.go index d90edb0..3c6bfd9 100644 --- a/examples/vbox/main.go +++ b/examples/vbox/main.go @@ -13,26 +13,25 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128)) window.SetTitle("vertical stack") - container := elements.NewVBox(true, true) + container := elements.NewVBox(elements.SpaceBoth) - label := elements.NewLabel("it is a label hehe", true) + label := elements.NewLabelWrapped("it is a label hehe") button := elements.NewButton("drawing pad") okButton := elements.NewButton("OK") button.OnClick (func () { container.DisownAll() - container.Adopt(elements.NewLabel("Draw here:", false), false) - container.Adopt(testing.NewMouse(), true) - container.Adopt(okButton, false) + container.Adopt(elements.NewLabel("Draw here (not really):")) + container.AdoptExpand(testing.NewMouse()) + container.Adopt(okButton) okButton.Focus() }) okButton.OnClick(tomo.Stop) - - container.Adopt(label, true) - container.Adopt(button, false) - container.Adopt(okButton, false) - okButton.Focus() - + + container.AdoptExpand(label) + container.Adopt(button, okButton) window.Adopt(container) + + okButton.Focus() window.OnClose(tomo.Stop) window.Show() } From ac58a432207a7b76c050af885678b18a23bbb3fb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 18 Apr 2023 18:37:50 -0400 Subject: [PATCH 38/41] Half-done implementation of file elements --- elements/directory.go | 114 ++++++++++ elements/{notdone => }/file.go | 155 +++++++------- elements/{notdone => }/fs.go | 2 +- elements/list.go | 40 ++-- elements/notdone/directory.go | 378 --------------------------------- examples/fileBrowser/main.go | 36 ++-- theme.go | 2 +- 7 files changed, 237 insertions(+), 490 deletions(-) create mode 100644 elements/directory.go rename elements/{notdone => }/file.go (67%) rename elements/{notdone => }/fs.go (95%) delete mode 100644 elements/notdone/directory.go diff --git a/elements/directory.go b/elements/directory.go new file mode 100644 index 0000000..f5a1221 --- /dev/null +++ b/elements/directory.go @@ -0,0 +1,114 @@ +package elements + +import "image" +import "path/filepath" +import "git.tebibyte.media/sashakoshka/tomo" + +// TODO: base on flow implementation of list. also be able to switch to a table +// variant for a more information dense view. + +type historyEntry struct { + location string + filesystem ReadDirStatFS +} + +// Directory displays a list of files within a particular directory and +// file system. +type Directory struct { + *List + history []historyEntry + historyIndex int + onChoose func (file string) +} + +// NewDirectory creates a new directory view. If within is nil, it will use +// the OS file system. +func NewDirectory ( + location string, + within ReadDirStatFS, +) ( + element *Directory, + err error, +) { + element = &Directory { + List: NewList(8), + } + err = element.SetLocation(location, within) + return +} + +// Location returns the directory's location and filesystem. +func (element *Directory) Location () (string, ReadDirStatFS) { + if len(element.history) < 1 { return "", nil } + current := element.history[element.historyIndex] + return current.location, current.filesystem +} + +// SetLocation sets the directory's location and filesystem. If within is nil, +// it will use the OS file system. +func (element *Directory) SetLocation ( + location string, + within ReadDirStatFS, +) error { + if within == nil { + within = defaultFS { } + } + element.scroll = image.Point { } + + if element.history != nil { + element.historyIndex ++ + } + element.history = append ( + element.history[:element.historyIndex], + historyEntry { location, within }) + return element.Update() +} + +// Backward goes back a directory in history +func (element *Directory) Backward () (bool, error) { + if element.historyIndex > 1 { + element.historyIndex -- + return true, element.Update() + } else { + return false, nil + } +} + +// Forward goes forward a directory in history +func (element *Directory) Forward () (bool, error) { + if element.historyIndex < len(element.history) - 1 { + element.historyIndex ++ + return true, element.Update() + } else { + return false, nil + } +} + +// Update refreshes the directory's contents. +func (element *Directory) Update () error { + location, filesystem := element.Location() + entries, err := filesystem.ReadDir(location) + + children := make([]tomo.Element, len(entries)) + for index, entry := range entries { + filePath := filepath.Join(location, entry.Name()) + file, _ := NewFile(filePath, filesystem) + file.OnChoose (func () { + if element.onChoose != nil { + element.onChoose(filePath) + } + }) + + children[index] = file + } + + element.DisownAll() + element.Adopt(children...) + return err +} + +// OnChoose sets a function to be called when the user double-clicks a file or +// sub-directory within the directory view. +func (element *Directory) OnChoose (callback func (file string)) { + element.onChoose = callback +} diff --git a/elements/notdone/file.go b/elements/file.go similarity index 67% rename from elements/notdone/file.go rename to elements/file.go index 62e6e4d..6a758ce 100644 --- a/elements/notdone/file.go +++ b/elements/file.go @@ -1,4 +1,4 @@ -package fileElements +package elements import "time" import "io/fs" @@ -6,27 +6,29 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +type fileEntity interface { + tomo.SelectableEntity + tomo.FocusableEntity +} + // File displays an interactive visual representation of a file within any // file system. type File struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity fileEntity config config.Wrapped theme theme.Wrapped lastClick time.Time pressed bool + enabled bool iconID tomo.Icon filesystem fs.StatFS location string - selected bool onChoose func () } @@ -40,15 +42,44 @@ func NewFile ( element *File, err error, ) { - element = &File { } + element = &File { enabled: true } element.theme.Case = tomo.C("files", "file") - element.Core, element.core = core.NewCore(element, element.drawAll) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush) + element.entity = tomo.NewEntity(element).(fileEntity) err = element.SetLocation(location, within) return } +// Entity returns this element's entity. +func (element *File) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *File) Draw (destination canvas.Canvas) { + // background + state := element.state() + bounds := element.entity.Bounds() + sink := element.theme.Sink(tomo.PatternButton) + element.theme. + Pattern(tomo.PatternButton, state). + Draw(destination, bounds) + + // icon + icon := element.icon() + if icon != nil { + iconBounds := icon.Bounds() + offset := image.Pt ( + (bounds.Dx() - iconBounds.Dx()) / 2, + (bounds.Dy() - iconBounds.Dy()) / 2) + if element.pressed { + offset = offset.Add(sink) + } + icon.Draw ( + destination, + element.theme.Color(tomo.ColorForeground, state), + bounds.Min.Add(offset)) + } +} // Location returns the file's location and filesystem. func (element *File) Location () (string, fs.StatFS) { return element.location, element.filesystem @@ -82,55 +113,66 @@ func (element *File) Update () error { } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() return err } -func (element *File) Selected () bool { - return element.selected -} - -func (element *File) SetSelected (selected bool) { - if element.selected == selected { return } - element.selected = selected - element.drawAndPush() -} - func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if !element.Enabled() { return } if key == input.KeyEnter { element.pressed = true - element.drawAndPush() + element.entity.Invalidate() } } func (element *File) HandleKeyUp(key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter && element.pressed { element.pressed = false - element.drawAndPush() if !element.Enabled() { return } + element.entity.Invalidate() if element.onChoose != nil { element.onChoose() } } } +func (element *File) HandleFocusChange () { + element.entity.Invalidate() +} + func (element *File) OnChoose (callback func ()) { element.onChoose = callback } +// Focus gives this element input focus. +func (element *File) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this file is enabled or not. +func (element *File) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this file is enabled or not. +func (element *File) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + func (element *File) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } - if !element.Focused() { element.Focus() } + if !element.entity.Focused() { element.Focus() } if button != input.ButtonLeft { return } element.pressed = true - element.drawAndPush() + element.entity.Invalidate() } func (element *File) HandleMouseUp (x, y int, button input.Button) { if button != input.ButtonLeft { return } element.pressed = false - within := image.Point { x, y }.In(element.Bounds()) + within := image.Point { x, y }.In(element.entity.Bounds()) if time.Since(element.lastClick) < element.config.DoubleClickDelay() { if element.Enabled() && within && element.onChoose != nil { element.onChoose() @@ -138,29 +180,29 @@ func (element *File) HandleMouseUp (x, y int, button input.Button) { } else { element.lastClick = time.Now() } - element.drawAndPush() + element.entity.Invalidate() } // SetTheme sets the element's theme. -func (element *File) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawAndPush() +func (element *File) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.entity.Invalidate() } // SetConfig sets the element's configuration. -func (element *File) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.drawAndPush() +func (element *File) SetConfig (config tomo.Config) { + if config == element.config.Config { return } + element.config.Config = config + element.entity.Invalidate() } func (element *File) state () tomo.State { return tomo.State { Disabled: !element.Enabled(), - Focused: element.Focused(), + Focused: element.entity.Focused(), Pressed: element.pressed, - On: element.selected, + On: element.entity.Selected(), } } @@ -172,44 +214,11 @@ func (element *File) updateMinimumSize () { padding := element.theme.Padding(tomo.PatternButton) icon := element.icon() if icon == nil { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal(), padding.Vertical()) } else { bounds := padding.Inverse().Apply(icon.Bounds()) - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) - } -} - -func (element *File) drawAndPush () { - if element.core.HasImage() { - element.drawAll() - element.core.DamageAll() - } -} - -func (element *File) drawAll () { - // background - state := element.state() - bounds := element.Bounds() - sink := element.theme.Sink(tomo.PatternButton) - element.theme. - Pattern(tomo.PatternButton, state). - Draw(element.core, bounds) - - // icon - icon := element.icon() - if icon != nil { - iconBounds := icon.Bounds() - offset := image.Pt ( - (bounds.Dx() - iconBounds.Dx()) / 2, - (bounds.Dy() - iconBounds.Dy()) / 2) - if element.pressed { - offset = offset.Add(sink) - } - 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/notdone/fs.go b/elements/fs.go similarity index 95% rename from elements/notdone/fs.go rename to elements/fs.go index 85572e3..88d9965 100644 --- a/elements/notdone/fs.go +++ b/elements/fs.go @@ -1,4 +1,4 @@ -package fileElements +package elements import "os" import "io/fs" diff --git a/elements/list.go b/elements/list.go index 6926835..dcc9b19 100644 --- a/elements/list.go +++ b/elements/list.go @@ -7,6 +7,15 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/default/theme" +// TODO: make hidden variants: +// vertical: one column. +// flow: acts like DocumentContainer with all inline elements. +// create wrapper elements for making a plain version of each of these, but keep +// the implementations private (but with public methods) so they can be included +// in other elements. +// have table be a very tabular thing with named columns that can be sorted, +// resized, etc. + type listEntity interface { tomo.ContainerEntity tomo.ScrollableEntity @@ -36,10 +45,7 @@ func NewList (columns int, children ...tomo.Element) (element *List) { element.columnSizes = make([]int, columns) element.theme.Case = tomo.C("tomo", "list") element.entity = tomo.NewEntity(element).(listEntity) - - for _, child := range children { - element.Adopt(child) - } + element.Adopt(children...) return } @@ -117,23 +123,27 @@ func (element *List) Layout () { } } -func (element *List) Adopt (child tomo.Element) { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { } +func (element *List) Adopt (children ...tomo.Element) { + for _, child := range children { + element.entity.Adopt(child) + element.scratch[child] = scratchEntry { } + } element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() } -func (element *List) Disown (child tomo.Element) { - index := element.entity.IndexOf(child) - if index < 0 { return } - if index == element.selected { - element.selected = -1 - element.entity.SelectChild(index, false) +func (element *List) Disown (children ...tomo.Element) { + for _, child := range children { + index := element.entity.IndexOf(child) + if index < 0 { return } + if index == element.selected { + element.selected = -1 + element.entity.SelectChild(index, false) + } + element.entity.Disown(index) + delete(element.scratch, child) } - element.entity.Disown(index) - delete(element.scratch, child) element.updateMinimumSize() element.entity.Invalidate() element.entity.InvalidateLayout() diff --git a/elements/notdone/directory.go b/elements/notdone/directory.go deleted file mode 100644 index df352cf..0000000 --- a/elements/notdone/directory.go +++ /dev/null @@ -1,378 +0,0 @@ -package fileElements - -import "io/fs" -import "image" -import "path/filepath" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/artist" -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" - -type fileLayoutEntry struct { - *File - fs.DirEntry - Bounds image.Rectangle - Drawer textdraw.Drawer - TextPoint image.Point -} - -type historyEntry struct { - location string - filesystem ReadDirStatFS -} - -// Directory displays a list of files within a particular directory and -// file system. -type Directory struct { - *core.Core - *core.Propagator - core core.CoreControl - - children []fileLayoutEntry - scroll image.Point - contentBounds image.Rectangle - - config config.Wrapped - theme theme.Wrapped - - onScrollBoundsChange func () - - history []historyEntry - historyIndex int - onChoose func (file string) -} - -// NewDirectory creates a new directory view. If within is nil, it will use -// the OS file system. -func NewDirectory ( - location string, - within ReadDirStatFS, -) ( - element *Directory, - err error, -) { - element = &Directory { } - element.theme.Case = tomo.C("files", "directory") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - err = element.SetLocation(location, within) - return -} - -// Location returns the directory's location and filesystem. -func (element *Directory) Location () (string, ReadDirStatFS) { - if len(element.history) < 1 { return "", nil } - current := element.history[element.historyIndex] - return current.location, current.filesystem -} - -// SetLocation sets the directory's location and filesystem. If within is nil, -// it will use the OS file system. -func (element *Directory) SetLocation ( - location string, - within ReadDirStatFS, -) error { - if within == nil { - within = defaultFS { } - } - element.scroll = image.Point { } - - if element.history != nil { - element.historyIndex ++ - } - element.history = append ( - element.history[:element.historyIndex], - historyEntry { location, within }) - return element.Update() -} - -// Backward goes back a directory in history -func (element *Directory) Backward () (bool, error) { - if element.historyIndex > 1 { - element.historyIndex -- - return true, element.Update() - } else { - return false, nil - } -} - -// Forward goes forward a directory in history -func (element *Directory) Forward () (bool, error) { - if element.historyIndex < len(element.history) - 1 { - element.historyIndex ++ - return true, element.Update() - } else { - return false, nil - } -} - -// Update refreshes the directory's contents. -func (element *Directory) Update () error { - location, filesystem := element.Location() - entries, err := filesystem.ReadDir(location) - - // disown all entries - for _, file := range element.children { - file.DrawTo(nil, image.Rectangle { }, nil) - file.SetParent(nil) - - if file.Focused() { - file.HandleUnfocus() - } - } - - element.children = make([]fileLayoutEntry, len(entries)) - for index, entry := range entries { - filePath := filepath.Join(location, entry.Name()) - file, _ := NewFile(filePath, filesystem) - file.SetParent(element) - file.OnChoose (func () { - if element.onChoose != nil { - element.onChoose(filePath) - } - }) - element.children[index].File = file - element.children[index].DirEntry = entry - element.children[index].Drawer.SetFace (element.theme.FontFace( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.children[index].Drawer.SetText([]rune(entry.Name())) - element.children[index].Drawer.SetAlign(textdraw.AlignCenter) - } - - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } - return err -} - -// OnChoose sets a function to be called when the user double-clicks a file or -// sub-directory within the directory view. -func (element *Directory) OnChoose (callback func (file string)) { - element.onChoose = callback -} - -// CountChildren returns the amount of children contained within this element. -func (element *Directory) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *Directory) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].File -} - -func (element *Directory) HandleMouseDown (x, y int, button input.Button) { - if button == input.ButtonLeft { - var file *File - for _, entry := range element.children { - if image.Pt(x, y).In(entry.Bounds) { - file = entry.File - } - } - if file != nil { - file.SetSelected(!file.Selected()) - } - } - element.Propagator.HandleMouseDown(x, y, button) -} - -func (element *Directory) redoAll () { - if !element.core.HasImage() { return } - - // do a layout - element.doLayout() - - maxScrollHeight := element.maxScrollHeight() - if element.scroll.Y > maxScrollHeight { - element.scroll.Y = maxScrollHeight - element.doLayout() - } - - // draw a background - rocks := make([]image.Rectangle, len(element.children)) - for index, entry := range element.children { - rocks[index] = entry.Bounds - } - pattern := element.theme.Pattern ( - tomo.PatternPinboard, - tomo.State { }) - artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) - - element.partition() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } - - // draw labels - foreground := element.theme.Color(tomo.ColorForeground, tomo.State { }) - for _, entry := range element.children { - entry.Drawer.Draw(element.core, foreground, entry.TextPoint) - } -} - -func (element *Directory) partition () { - for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds, nil) - } - - // cut our canvas up and give peices to child elements - for _, entry := range element.children { - if entry.Bounds.Overlaps(element.Bounds()) { - entry.DrawTo ( - canvas.Cut(element.core, entry.Bounds), - entry.Bounds, func (region image.Rectangle) { - element.core.DamageRegion(region) - }) - } - } -} - -func (element *Directory) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *Directory) NotifyMinimumSizeChange (child tomo.Element) { - element.redoAll() - element.core.DamageAll() -} - -// SetTheme sets the element's theme. -func (element *Directory) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *Directory) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.redoAll() -} -// ScrollContentBounds returns the full content size of the element. -func (element *Directory) ScrollContentBounds () image.Rectangle { - return element.contentBounds -} - -// ScrollViewportBounds returns the size and position of the element's -// viewport relative to ScrollBounds. -func (element *Directory) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternPinboard) - bounds := padding.Apply(element.Bounds()) - bounds = bounds.Sub(bounds.Min).Add(element.scroll) - return bounds -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *Directory) ScrollTo (position image.Point) { - if position.Y < 0 { - position.Y = 0 - } - maxScrollHeight := element.maxScrollHeight() - if position.Y > maxScrollHeight { - position.Y = maxScrollHeight - } - element.scroll = position - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *Directory) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *Directory) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -func (element *Directory) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - viewportHeight := element.Bounds().Dy() - padding.Vertical() - height = element.contentBounds.Dy() - viewportHeight - if height < 0 { height = 0 } - return -} - -func (element *Directory) doLayout () { - margin := element.theme.Margin(tomo.PatternPinboard) - padding := element.theme.Padding(tomo.PatternPinboard) - bounds := padding.Apply(element.Bounds()) - element.contentBounds = image.Rectangle { } - - beginningOfRow := true - dot := bounds.Min.Sub(element.scroll) - rowHeight := 0 - for index, entry := range element.children { - width, height := entry.MinimumSize() - - if dot.X + width > bounds.Max.X { - dot.X = bounds.Min.Sub(element.scroll).X - dot.Y += rowHeight - if index > 1 { - dot.Y += margin.Y - } - beginningOfRow = true - } - - if beginningOfRow { - beginningOfRow = false - } else { - dot.X += margin.X - } - - entry.Drawer.SetMaxWidth(width) - bounds := image.Rect(dot.X, dot.Y, dot.X + width, dot.Y + height) - entry.Bounds = bounds - - drawerHeight := entry.Drawer.ReccomendedHeightFor(width) - entry.TextPoint = - image.Pt(bounds.Min.X, bounds.Max.Y + margin.Y). - Sub(entry.Drawer.LayoutBounds().Min) - bounds.Max.Y += margin.Y + drawerHeight - height += margin.Y + drawerHeight - if rowHeight < height { - rowHeight = height - } - - element.contentBounds = element.contentBounds.Union(bounds) - element.children[index] = entry - dot.X += width - } - - element.contentBounds = - element.contentBounds.Sub(element.contentBounds.Min) -} - -func (element *Directory) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternPinboard) - minimumWidth := 0 - for _, entry := range element.children { - width, _ := entry.MinimumSize() - if width > minimumWidth { - minimumWidth = width - } - } - element.core.SetMinimumSize ( - minimumWidth + padding.Horizontal(), - padding.Vertical()) -} diff --git a/examples/fileBrowser/main.go b/examples/fileBrowser/main.go index 156aab1..842df94 100644 --- a/examples/fileBrowser/main.go +++ b/examples/fileBrowser/main.go @@ -3,10 +3,7 @@ package main import "os" import "path/filepath" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/file" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -16,11 +13,11 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384)) window.SetTitle("File browser") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) homeDir, _ := os.UserHomeDir() - controlBar := containers.NewContainer(layouts.Horizontal { }) + controlBar := elements.NewHBox(elements.SpaceNone) backButton := elements.NewButton("Back") backButton.SetIcon(tomo.IconBackward) backButton.ShowText(false) @@ -35,12 +32,11 @@ func run () { upwardButton.ShowText(false) locationInput := elements.NewTextBox("Location", "") - statusBar := containers.NewContainer(layouts.Horizontal { true, false }) - directory, _ := fileElements.NewFile(homeDir, nil) - baseName := elements.NewLabel(filepath.Base(homeDir), false) + statusBar := elements.NewHBox(elements.SpaceMargin) + directory, _ := elements.NewFile(homeDir, nil) + baseName := elements.NewLabel(filepath.Base(homeDir)) - scrollContainer := containers.NewScrollContainer(false, true) - directoryView, _ := fileElements.NewDirectory(homeDir, nil) + directoryView, _ := elements.NewDirectory(homeDir, nil) updateStatus := func () { filePath, _ := directoryView.Location() directory.SetLocation(filePath, nil) @@ -72,19 +68,15 @@ func run () { filePath, _ := directoryView.Location() choose(filepath.Dir(filePath)) }) + + controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton) + controlBar.AdoptExpand(locationInput) + statusBar.Adopt(directory, baseName) - controlBar.Adopt(backButton, false) - controlBar.Adopt(forwardButton, false) - controlBar.Adopt(refreshButton, false) - controlBar.Adopt(upwardButton, false) - controlBar.Adopt(locationInput, true) - scrollContainer.Adopt(directoryView) - statusBar.Adopt(directory, false) - statusBar.Adopt(baseName, false) - - container.Adopt(controlBar, false) - container.Adopt(scrollContainer, true) - container.Adopt(statusBar, false) + container.Adopt(controlBar) + container.AdoptExpand ( + elements.NewScroll(elements.ScrollVertical, directoryView)) + container.Adopt(statusBar) window.OnClose(tomo.Stop) window.Show() diff --git a/theme.go b/theme.go index 73af8b8..15a651c 100644 --- a/theme.go +++ b/theme.go @@ -197,8 +197,8 @@ const ( IconBackward IconForward - IconRefresh IconUpward + IconRefresh IconYes IconNo From afdecc2c8bf3a8a7006dbcdb20f4a294bc7a1873 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 19 Apr 2023 00:29:25 -0400 Subject: [PATCH 39/41] Containers now share a bunch of code --- elements/box.go | 81 +++++-------------------------------------- elements/container.go | 77 ++++++++++++++++++++++++++++++++++++++++ elements/document.go | 66 ++++------------------------------- elements/list.go | 73 +++----------------------------------- 4 files changed, 95 insertions(+), 202 deletions(-) create mode 100644 elements/container.go diff --git a/elements/box.go b/elements/box.go index 143060d..dab9117 100644 --- a/elements/box.go +++ b/elements/box.go @@ -21,19 +21,12 @@ func (space Space) Includes (sub Space) bool { return (space & sub) > 0 } -type scratchEntry struct { - expand bool - minSize float64 - minBreadth float64 -} - // Box is a container that lays out its children horizontally or vertically. // Child elements can be set to contract to their minimum size, or expand to // fill remaining space. Boxes can be nested and used together to create more // complex layouts. type Box struct { - entity tomo.ContainerEntity - scratch map[tomo.Element] scratchEntry + container theme theme.Wrapped padding bool margin bool @@ -46,9 +39,10 @@ func NewHBox (space Space, children ...tomo.Element) (element *Box) { padding: space.Includes(SpacePadding), margin: space.Includes(SpaceMargin), } - element.scratch = make(map[tomo.Element] scratchEntry) - element.theme.Case = tomo.C("tomo", "box") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.minimumSize = element.updateMinimumSize + element.init() + element.theme.Case = tomo.C("tomo", "box") element.Adopt(children...) return } @@ -60,17 +54,14 @@ func NewVBox (space Space, children ...tomo.Element) (element *Box) { margin: space.Includes(SpaceMargin), vertical: true, } - element.scratch = make(map[tomo.Element] scratchEntry) - element.theme.Case = tomo.C("tomo", "box") element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.minimumSize = element.updateMinimumSize + element.init() + element.theme.Case = tomo.C("tomo", "box") element.Adopt(children...) return } -func (element *Box) Entity () tomo.Entity { - return element.entity -} - func (element *Box) Draw (destination canvas.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { @@ -127,64 +118,8 @@ func (element *Box) Layout () { } } -func (element *Box) Adopt (children ...tomo.Element) { - for _, child := range children { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: false } - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - func (element *Box) AdoptExpand (children ...tomo.Element) { - for _, child := range children { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: true } - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *Box) Disown (children ...tomo.Element) { - for _, child := range children { - index := element.entity.IndexOf(child) - if index < 0 { continue } - element.entity.Disown(index) - delete(element.scratch, child) - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *Box) DisownAll () { - func () { - for index := 0; index < element.entity.CountChildren(); index ++ { - index := index - defer element.entity.Disown(index) - } - } () - element.scratch = make(map[tomo.Element] scratchEntry) - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *Box) Child (index int) tomo.Element { - if index < 0 || index >= element.entity.CountChildren() { return nil } - return element.entity.Child(index) -} - -func (element *Box) CountChildren () int { - return element.entity.CountChildren() -} - -func (element *Box) HandleChildMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() + element.adopt(true, children...) } func (element *Box) DrawBackground (destination canvas.Canvas) { diff --git a/elements/container.go b/elements/container.go new file mode 100644 index 0000000..49d2c88 --- /dev/null +++ b/elements/container.go @@ -0,0 +1,77 @@ +package elements + +import "git.tebibyte.media/sashakoshka/tomo" + +type scratchEntry struct { + expand bool + minSize float64 + minBreadth float64 +} + +type container struct { + entity tomo.ContainerEntity + scratch map[tomo.Element] scratchEntry + minimumSize func () +} + +func (container *container) Entity () tomo.Entity { + return container.entity +} + +func (container *container) Adopt (children ...tomo.Element) { + container.adopt(false, children...) +} + +func (container *container) init () { + container.scratch = make(map[tomo.Element] scratchEntry) +} + +func (container *container) adopt (expand bool, children ...tomo.Element) { + for _, child := range children { + container.entity.Adopt(child) + container.scratch[child] = scratchEntry { expand: expand } + } + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} + +func (container *container) Disown (children ...tomo.Element) { + for _, child := range children { + index := container.entity.IndexOf(child) + if index < 0 { continue } + container.entity.Disown(index) + delete(container.scratch, child) + } + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} + +func (container *container) DisownAll () { + func () { + for index := 0; index < container.entity.CountChildren(); index ++ { + index := index + defer container.entity.Disown(index) + } + } () + container.scratch = make(map[tomo.Element] scratchEntry) + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} + +func (container *container) Child (index int) tomo.Element { + if index < 0 || index >= container.entity.CountChildren() { return nil } + return container.entity.Child(index) +} + +func (container *container) CountChildren () int { + return container.entity.CountChildren() +} + +func (container *container) HandleChildMinimumSizeChange (child tomo.Element) { + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} diff --git a/elements/document.go b/elements/document.go index ec916a2..276b61c 100644 --- a/elements/document.go +++ b/elements/document.go @@ -12,9 +12,9 @@ type documentEntity interface { } type Document struct { + container entity documentEntity - scratch map[tomo.Element] scratchEntry scroll image.Point contentBounds image.Rectangle @@ -25,17 +25,15 @@ type Document struct { func NewDocument (children ...tomo.Element) (element *Document) { element = &Document { } - element.scratch = make(map[tomo.Element] scratchEntry) element.theme.Case = tomo.C("tomo", "document") element.entity = tomo.NewEntity(element).(documentEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() element.Adopt(children...) return } -func (element *Document) Entity () tomo.Entity { - return element.entity -} - func (element *Document) Draw (destination canvas.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { @@ -111,63 +109,11 @@ func (element *Document) Layout () { } func (element *Document) Adopt (children ...tomo.Element) { - for _, child := range children { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: true } - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() + element.adopt(true, children...) } func (element *Document) AdoptInline (children ...tomo.Element) { - for _, child := range children { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { expand: false } - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *Document) Disown (children ...tomo.Element) { - for _, child := range children { - index := element.entity.IndexOf(child) - if index < 0 { return } - element.entity.Disown(index) - delete(element.scratch, child) - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *Document) DisownAll () { - func () { - for index := 0; index < element.entity.CountChildren(); index ++ { - index := index - defer element.entity.Disown(index) - } - } () - element.scratch = make(map[tomo.Element] scratchEntry) - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *Document) Child (index int) tomo.Element { - if index < 0 || index >= element.entity.CountChildren() { return nil } - return element.entity.Child(index) -} - -func (element *Document) CountChildren () int { - return element.entity.CountChildren() -} - -func (element *Document) HandleChildMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() + element.adopt(false, children...) } func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) { diff --git a/elements/list.go b/elements/list.go index dcc9b19..b7429f6 100644 --- a/elements/list.go +++ b/elements/list.go @@ -7,24 +7,15 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/default/theme" -// TODO: make hidden variants: -// vertical: one column. -// flow: acts like DocumentContainer with all inline elements. -// create wrapper elements for making a plain version of each of these, but keep -// the implementations private (but with public methods) so they can be included -// in other elements. -// have table be a very tabular thing with named columns that can be sorted, -// resized, etc. - type listEntity interface { tomo.ContainerEntity tomo.ScrollableEntity } type List struct { + container entity listEntity - scratch map[tomo.Element] scratchEntry scroll image.Point contentBounds image.Rectangle columnSizes []int @@ -41,18 +32,16 @@ type List struct { func NewList (columns int, children ...tomo.Element) (element *List) { if columns < 1 { columns = 1 } element = &List { selected: -1 } - element.scratch = make(map[tomo.Element] scratchEntry) element.columnSizes = make([]int, columns) element.theme.Case = tomo.C("tomo", "list") element.entity = tomo.NewEntity(element).(listEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() element.Adopt(children...) return } -func (element *List) Entity () tomo.Entity { - return element.entity -} - func (element *List) Draw (destination canvas.Canvas) { rocks := make([]image.Rectangle, element.entity.CountChildren()) for index := 0; index < element.entity.CountChildren(); index ++ { @@ -123,54 +112,6 @@ func (element *List) Layout () { } } -func (element *List) Adopt (children ...tomo.Element) { - for _, child := range children { - element.entity.Adopt(child) - element.scratch[child] = scratchEntry { } - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *List) Disown (children ...tomo.Element) { - for _, child := range children { - index := element.entity.IndexOf(child) - if index < 0 { return } - if index == element.selected { - element.selected = -1 - element.entity.SelectChild(index, false) - } - element.entity.Disown(index) - delete(element.scratch, child) - } - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *List) DisownAll () { - func () { - for index := 0; index < element.entity.CountChildren(); index ++ { - index := index - defer element.entity.Disown(index) - } - } () - element.scratch = make(map[tomo.Element] scratchEntry) - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - -func (element *List) Child (index int) tomo.Element { - if index < 0 || index >= element.entity.CountChildren() { return nil } - return element.entity.Child(index) -} - -func (element *List) CountChildren () int { - return element.entity.CountChildren() -} - func (element *List) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { if child, ok := child.(tomo.Selectable); ok { index := element.entity.IndexOf(child) @@ -185,12 +126,6 @@ func (element *List) HandleChildMouseDown (x, y int, button input.Button, child func (element *List) HandleChildMouseUp (int, int, input.Button, tomo.Element) { } -func (element *List) HandleChildMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.entity.Invalidate() - element.entity.InvalidateLayout() -} - func (element *List) HandleChildFlexibleHeightChange (child tomo.Flexible) { element.updateMinimumSize() element.entity.Invalidate() From dbee2ff5a9c92007ece3f3686b3295d52088c689 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 20 Apr 2023 00:15:37 -0400 Subject: [PATCH 40/41] Directory view works --- elements/directory.go | 210 ++++++++++++++++++++++++++++++++++-- elements/document.go | 7 +- elements/file.go | 4 + examples/goroutines/main.go | 1 + 4 files changed, 214 insertions(+), 8 deletions(-) diff --git a/elements/directory.go b/elements/directory.go index f5a1221..6859d59 100644 --- a/elements/directory.go +++ b/elements/directory.go @@ -3,10 +3,19 @@ package elements import "image" import "path/filepath" 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/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" // TODO: base on flow implementation of list. also be able to switch to a table // variant for a more information dense view. +type directoryEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + type historyEntry struct { location string filesystem ReadDirStatFS @@ -15,10 +24,18 @@ type historyEntry struct { // Directory displays a list of files within a particular directory and // file system. type Directory struct { - *List + container + entity directoryEntity + theme theme.Wrapped + + scroll image.Point + contentBounds image.Rectangle + history []historyEntry historyIndex int - onChoose func (file string) + + onChoose func (file string) + onScrollBoundsChange func () } // NewDirectory creates a new directory view. If within is nil, it will use @@ -30,13 +47,159 @@ func NewDirectory ( element *Directory, err error, ) { - element = &Directory { - List: NewList(8), - } + element = &Directory { } + element.theme.Case = tomo.C("tomo", "list") + element.entity = tomo.NewEntity(element).(directoryEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() err = element.SetLocation(location, within) return } +func (element *Directory) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *Directory) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + + margin := element.theme.Margin(tomo.PatternPinboard) + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + width := int(entry.minBreadth) + height := int(entry.minSize) + if width + dot.X > bounds.Max.X { + nextLine() + } + if typedChild, ok := child.(tomo.Flexible); ok { + height = typedChild.FlexibleHeightFor(width) + } + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + dot.X += width + margin.X + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *Directory) HandleMouseDown (x, y int, button input.Button) { + element.selectNone() +} + +func (element *Directory) HandleMouseUp (x, y int, button input.Button) { } + +func (element *Directory) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { + element.selectNone() + if child, ok := child.(tomo.Selectable); ok { + index := element.entity.IndexOf(child) + element.entity.SelectChild(index, true) + } +} + +func (element *Directory) HandleChildMouseUp (int, int, input.Button, tomo.Element) { } + +func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// ScrollContentBounds returns the full content size of the element. +func (element *Directory) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *Directory) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds +} + +// ScrollTo scrolls the viewport to the specified point relative to +// ScrollBounds. +func (element *Directory) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *Directory) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *Directory) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +func (element *Directory) DrawBackground (destination canvas.Canvas) { + element.theme.Pattern(tomo.PatternPinboard, tomo.State { }). + Draw(destination, element.entity.Bounds()) +} + +// SetTheme sets the element's theme. +func (element *Directory) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + // Location returns the directory's location and filesystem. func (element *Directory) Location () (string, ReadDirStatFS) { if len(element.history) < 1 { return "", nil } @@ -101,7 +264,7 @@ func (element *Directory) Update () error { children[index] = file } - + element.DisownAll() element.Adopt(children...) return err @@ -112,3 +275,38 @@ func (element *Directory) Update () error { func (element *Directory) OnChoose (callback func (file string)) { element.onChoose = callback } + +func (element *Directory) selectNone () { + for index := 0; index < element.entity.CountChildren(); index ++ { + element.entity.SelectChild(index, false) + } +} + +func (element *Directory) maxScrollHeight () (height int) { + padding := element.theme.Padding(tomo.PatternSunken) + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return +} + + +func (element *Directory) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternPinboard) + minimumWidth := 0 + for index := 0; index < element.entity.CountChildren(); index ++ { + width, height := element.entity.ChildMinimumSize(index) + if width > minimumWidth { + minimumWidth = width + } + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minSize = float64(height) + entry.minBreadth = float64(width) + element.scratch[key] = entry + } + element.entity.SetMinimumSize ( + minimumWidth + padding.Horizontal(), + padding.Vertical()) +} diff --git a/elements/document.go b/elements/document.go index 276b61c..9954322 100644 --- a/elements/document.go +++ b/elements/document.go @@ -47,6 +47,10 @@ func (element *Document) Draw (destination canvas.Canvas) { } func (element *Document) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + margin := element.theme.Margin(tomo.PatternBackground) padding := element.theme.Padding(tomo.PatternBackground) bounds := padding.Apply(element.entity.Bounds()) @@ -73,7 +77,7 @@ func (element *Document) Layout () { width := int(entry.minBreadth) height := int(entry.minSize) - if width + dot.X > bounds.Dx() && !entry.expand { + if width + dot.X > bounds.Max.X && !entry.expand { nextLine() } if width < bounds.Dx() && entry.expand { @@ -135,7 +139,6 @@ func (element *Document) SetTheme (theme tomo.Theme) { element.entity.InvalidateLayout() } - // ScrollContentBounds returns the full content size of the element. func (element *Document) ScrollContentBounds () image.Rectangle { return element.contentBounds diff --git a/elements/file.go b/elements/file.go index 6a758ce..6d5835c 100644 --- a/elements/file.go +++ b/elements/file.go @@ -140,6 +140,10 @@ func (element *File) HandleFocusChange () { element.entity.Invalidate() } +func (element *File) HandleSelectionChange () { + element.entity.Invalidate() +} + func (element *File) OnChoose (callback func ()) { element.onChoose = callback } diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index 5df9198..bfbb508 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -15,6 +15,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) window.SetTitle("Clock") + window.SetApplicationName("TomoClock") container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) From 698414ee65adabe1fa54e3504a1bc6abd06f2e47 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 20 Apr 2023 00:22:29 -0400 Subject: [PATCH 41/41] Raycaster example works --- elements/containers/common.go | 104 ------ elements/notdone/table.go | 587 -------------------------------- examples/raycaster/game.go | 2 +- examples/raycaster/main.go | 3 +- examples/raycaster/raycaster.go | 2 + 5 files changed, 5 insertions(+), 693 deletions(-) delete mode 100644 elements/containers/common.go delete mode 100644 elements/notdone/table.go diff --git a/elements/containers/common.go b/elements/containers/common.go deleted file mode 100644 index d65e71c..0000000 --- a/elements/containers/common.go +++ /dev/null @@ -1,104 +0,0 @@ -package containers - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -type childManager struct { - onChange func () - children []tomo.LayoutEntry - parent tomo.Parent - theme theme.Wrapped - config config.Wrapped -} - -// Adopt adds a new child element to the container. If expand is set to true, -// the element will expand (instead of contract to its minimum size), in -// whatever way is defined by the container's layout. -func (manager *childManager) Adopt (child tomo.Element, expand bool) { - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(manager.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(manager.config.Config) - } - child.SetParent(manager.parent) - - manager.children = append (manager.children, tomo.LayoutEntry { - Element: child, - Expand: expand, - }) - - manager.onChange() -} - - -// Disown removes the given child from the container if it is contained within -// it. -func (manager *childManager) Disown (child tomo.Element) { - for index, entry := range manager.children { - if entry.Element == child { - manager.clearChildEventHandlers(entry.Element) - manager.children = append ( - manager.children[:index], - manager.children[index + 1:]...) - break - } - } - - manager.onChange() -} - -// DisownAll removes all child elements from the container at once. -func (manager *childManager) DisownAll () { - for _, entry := range manager.children { - manager.clearChildEventHandlers(entry.Element) - } - manager.children = nil - - manager.onChange() -} - -// Children returns a slice containing this element's children. -func (manager *childManager) Children () (children []tomo.Element) { - children = make([]tomo.Element, len(manager.children)) - for index, entry := range manager.children { - children[index] = entry.Element - } - return -} - -// CountChildren returns the amount of children contained within this element. -func (manager *childManager) CountChildren () (count int) { - return len(manager.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (manager *childManager) Child (index int) (child tomo.Element) { - if index < 0 || index > len(manager.children) { return } - return manager.children[index].Element -} - -// ChildAt returns the child that contains the specified x and y coordinates. If -// there are no children at the coordinates, this method will return nil. -func (manager *childManager) ChildAt (point image.Point) (child tomo.Element) { - for _, entry := range manager.children { - if point.In(entry.Bounds) { - child = entry.Element - } - } - return -} - -func (manager *childManager) clearChildEventHandlers (child tomo.Element) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} diff --git a/elements/notdone/table.go b/elements/notdone/table.go deleted file mode 100644 index 4fe987f..0000000 --- a/elements/notdone/table.go +++ /dev/null @@ -1,587 +0,0 @@ -package containers - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// TODO: using the event propagator core might not be the best idea here. we -// should have slightly different behavior to sync the focused element with the -// selected cell. alternatively we could pass a callback to the propagator that -// fires when the focused child changes. this would also allow things like -// scrolling to the focused child (for this element and others). - -type tableCell struct { - tomo.Element - tomo.Pattern - image.Rectangle -} - -// TableContainer is a container that lays its contents out in a table. It can -// be scrolled. -type TableContainer struct { - *core.Core - *core.Propagator - core core.CoreControl - - topHeading bool - leftHeading bool - - columns int - rows int - scroll image.Point - warping bool - grid [][]tableCell - children []tomo.Element - - contentBounds image.Rectangle - forcedMinimumWidth int - forcedMinimumHeight int - - selectedColumn int - selectedRow int - - config config.Wrapped - theme theme.Wrapped - - onSelect func () - onScrollBoundsChange func () -} - -// NewTable creates a new table element with the specified amount of columns and -// rows. If top or left heading is set to true, the first row or column -// respectively will display as a table header. -func NewTableContainer ( - columns, rows int, - topHeading, leftHeading bool, -) ( - element *TableContainer, -) { - element = &TableContainer { - topHeading: topHeading, - leftHeading: leftHeading, - selectedColumn: -1, - selectedRow: -1, - } - - element.theme.Case = tomo.C("tomo", "tableContainer") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - element.Resize(columns, rows) - return -} - -// Set places an element at the specified column and row. If the element passed -// is nil, whatever element occupies the cell currently is removed. -func (element *TableContainer) Set (column, row int, child tomo.Element) { - if row < 0 || row >= element.rows { return } - if column < 0 || column >= element.columns { return } - - childList := element.children - if child == nil { - if element.grid[row][column].Element == nil { - // no-op - return - } else { - // removing the child that is currently in a slow - element.unhook(element.grid[row][column].Element) - childList = childList[:len(childList) - 1] - element.grid[row][column].Element = child - } - } else { - element.hook(child) - if element.grid[row][column].Element == nil { - // putting the child in an empty slot - childList = append(childList, nil) - element.grid[row][column].Element = child - } else { - // replacing the child that is currently in a slow - element.unhook(element.grid[row][column].Element) - element.grid[row][column].Element = child - } - } - - element.rebuildChildList(childList) - element.children = childList - element.redoAll() -} - -// Resize changes the amount of columns and rows in the table. If the table is -// resized to be smaller, children in cells that do not exist anymore will be -// removed. The minimum size for a TableContainer is 1x1. -func (element *TableContainer) Resize (columns, rows int) { - if columns < 1 { columns = 1 } - if rows < 1 { rows = 1 } - if element.columns == columns && element.rows == rows { return } - amountRemoved := 0 - - // handle rows as a whole - if rows < element.rows { - // disown children in bottom rows - for _, row := range element.grid[rows:] { - for index, child := range row { - if child.Element != nil { - element.unhook(child.Element) - amountRemoved ++ - row[index].Element = nil - }}} - // cut grid to size - element.grid = element.grid[:rows] - } else { - // expand grid - newGrid := make([][]tableCell, rows) - copy(newGrid, element.grid) - element.grid = newGrid - } - - // handle each row individually - for rowIndex, row := range element.grid { - if columns < element.columns { - // disown children in the far right of the row - for index, child := range row[columns:] { - if child.Element != nil { - element.unhook(child.Element) - amountRemoved ++ - row[index].Element = nil - }} - // cut row to size - element.grid[rowIndex] = row[:columns] - } else { - // expand row - newRow := make([]tableCell, columns) - copy(newRow, row) - element.grid[rowIndex] = newRow - } - } - - element.columns = columns - element.rows = rows - - if amountRemoved > 0 { - childList := element.children[:len(element.children) - amountRemoved] - element.rebuildChildList(childList) - element.children = childList - } - element.redoAll() -} - -// Selected returns the column and row of the cell that is currently selected. -// If no cell is selected, this method will return (-1, -1). -func (element *TableContainer) Selected () (column, row int) { - return element.selectedColumn, element.selectedRow -} - -// OnSelect sets a function to be called when the user selects a table cell. -func (element *TableContainer) OnSelect (callback func ()) { - element.onSelect = callback -} - -// Warp runs the specified callback, deferring all layout and rendering updates -// until the callback has finished executing. This allows for aplications to -// perform batch gui updates without flickering and stuff. -func (element *TableContainer) Warp (callback func ()) { - if element.warping { - callback() - return - } - - element.warping = true - callback() - element.warping = false - - element.redoAll() -} - -// Collapse collapses the element's minimum width and height. A value of zero -// for either means that the element's normal value is used. -func (element *TableContainer) Collapse (width, height int) { - if - element.forcedMinimumWidth == width && - element.forcedMinimumHeight == height { - - return - } - - element.forcedMinimumWidth = width - element.forcedMinimumHeight = height - element.updateMinimumSize() -} - -// CountChildren returns the amount of children contained within this element. -func (element *TableContainer) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *TableContainer) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index] -} - -func (element *TableContainer) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *TableContainer) NotifyMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.redoAll() -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *TableContainer) DrawBackground (bounds image.Rectangle) { - if !bounds.Overlaps(element.core.Bounds()) { return } - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - if bounds.Overlaps(child.Rectangle) { - element.theme.Pattern ( - child.Pattern, - element.state(columnIndex, rowIndex)). - Draw(canvas.Cut(element.core, bounds), child.Rectangle) - return - }}} -} - -func (element *TableContainer) HandleMouseDown (x, y int, button input.Button) { - element.Focus() - element.Propagator.HandleMouseDown(x, y, button) - if button != input.ButtonLeft { return } - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - if image.Pt(x, y).In(child.Rectangle) { - element.selectCell(columnIndex, rowIndex) - return - }}} -} - -func (element *TableContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - switch key { - case input.KeyLeft: element.changeSelectionBy(-1, 0) - case input.KeyRight: element.changeSelectionBy(1, 0) - case input.KeyUp: element.changeSelectionBy(0, -1) - case input.KeyDown: element.changeSelectionBy(0, 1) - case input.KeyEscape: element.selectCell(-1, -1) - default: element.Propagator.HandleKeyDown(key, modifiers) - } -} - -// ScrollContentBounds returns the full content size of the element. -func (element *TableContainer) ScrollContentBounds () image.Rectangle { - return element.contentBounds -} - -// ScrollViewportBounds returns the size and position of the element's -// viewport relative to ScrollBounds. -func (element *TableContainer) ScrollViewportBounds () image.Rectangle { - bounds := element.Bounds() - bounds = bounds.Sub(bounds.Min).Add(element.scroll) - return bounds -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *TableContainer) ScrollTo (position image.Point) { - if position.Y < 0 { - position.Y = 0 - } - maxScrollHeight := element.maxScrollHeight() - if position.Y > maxScrollHeight { - position.Y = maxScrollHeight - } - if position.X < 0 { - position.X = 0 - } - maxScrollWidth := element.maxScrollWidth() - if position.X > maxScrollWidth { - position.X = maxScrollWidth - } - element.scroll = position - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *TableContainer) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *TableContainer) ScrollAxes () (horizontal, vertical bool) { - return true, true -} - -func (element *TableContainer) changeSelectionBy (column, row int) { - column += element.selectedColumn - row += element.selectedRow - if column < 0 { column = 0 } - if row < 0 { row = 0 } - element.selectCell(column, row) -} - -func (element *TableContainer) selectCell (column, row int) { - if column < -1 { column = -1 } - if row < -1 { row = -1 } - if column >= element.columns { column = element.columns - 1 } - if row >= element.rows { row = element.rows - 1 } - - if column == element.selectedColumn && row == element.selectedRow { - return - } - - oldColumn, oldRow := element.selectedColumn, element.selectedRow - element.selectedColumn = column - element.selectedRow = row - if oldColumn >= 0 && oldRow >= 0 { - element.core.DamageRegion(element.redoCell(oldColumn, oldRow)) - } - if column >= 0 && row >= 0 { - element.core.DamageRegion(element.redoCell(column, row)) - } - if element.onSelect != nil { - element.onSelect() - } -} - -func (element *TableContainer) maxScrollHeight () (height int) { - viewportHeight := element.Bounds().Dy() - height = element.contentBounds.Dy() - viewportHeight - if height < 0 { height = 0 } - return -} - -func (element *TableContainer) maxScrollWidth () (width int) { - viewportWidth := element.Bounds().Dx() - width = element.contentBounds.Dx() - viewportWidth - if width < 0 { width = 0 } - return -} - -func (element *TableContainer) hook (child tomo.Element) { - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(element.config.Config) - } - child.SetParent(element) -} - -func (element *TableContainer) unhook (child tomo.Element) { - child.SetParent(nil) - child.DrawTo(nil, image.Rectangle { }, nil) -} - -func (element *TableContainer) rebuildChildList (list []tomo.Element) { - index := 0 - for _, row := range element.grid { - for _, child := range row { - if child.Element == nil { continue } - list[index] = child.Element - index ++ - }} -} - -func (element *TableContainer) state (column, row int) (state tomo.State) { - if column == element.selectedColumn && row == element.selectedRow { - state.On = true - } - return -} - -func (element *TableContainer) redoCell (column, row int) image.Rectangle { - padding := element.theme.Padding(tomo.PatternTableCell) - cell := element.grid[row][column] - pattern := element.theme.Pattern ( - cell.Pattern, element.state(column, row)) - - if cell.Element != nil { - // give child canvas portion - innerCellBounds := padding.Apply(cell.Rectangle) - artist.DrawShatter ( - element.core, pattern, - cell.Rectangle, innerCellBounds) - cell.DrawTo ( - canvas.Cut(element.core, innerCellBounds), - innerCellBounds, - element.childDrawCallback) - } else { - // draw cell pattern in empty cells - pattern.Draw(element.core, cell.Rectangle) - } - return cell.Rectangle -} - -func (element *TableContainer) redoAll () { - if element.warping || !element.core.HasImage() { - element.updateMinimumSize() - return - } - - maxScrollHeight := element.maxScrollHeight() - if element.scroll.Y > maxScrollHeight { - element.scroll.Y = maxScrollHeight - } - maxScrollWidth := element.maxScrollWidth() - if element.scroll.X > maxScrollWidth { - element.scroll.X = maxScrollWidth - } - - // calculate the minimum size of each column and row - var minWidth, minHeight float64 - columnWidths := make([]float64, element.columns) - rowHeights := make([]float64, element.rows) - padding := element.theme.Padding(tomo.PatternTableCell) - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - width, height := padding.Horizontal(), padding.Vertical() - - if child.Element != nil { - minWidth, minHeight := child.MinimumSize() - width += minWidth - height += minHeight - fwidth := float64(width) - fheight := float64(height) - if fwidth > columnWidths[columnIndex] { - columnWidths[columnIndex] = fwidth - } - if fheight > rowHeights[rowIndex] { - rowHeights[rowIndex] = fheight - } - } - }} - for _, width := range columnWidths { minWidth += width } - for _, height := range rowHeights { minHeight += height } - - // ignore given bounds for layout if they are below minimum size. we do - // this because we are scrollable in both directions and we might be - // collapsed. - bounds := element.Bounds().Sub(element.scroll) - if bounds.Dx() < int(minWidth) { - bounds.Max.X = bounds.Min.X + int(minWidth) - } - if bounds.Dy() < int(minHeight) { - bounds.Max.Y = bounds.Min.Y + int(minHeight) - } - element.contentBounds = bounds - - // scale up those minimum sizes to an actual size. - // FIXME: replace this with a more accurate algorithm - widthRatio := float64(bounds.Dx()) / minWidth - heightRatio := float64(bounds.Dy()) / minHeight - for index := range columnWidths { - columnWidths[index] *= widthRatio - } - for index := range rowHeights { - rowHeights[index] *= heightRatio - } - - // cut up canvas - x := float64(bounds.Min.X) - y := float64(bounds.Min.Y) - for rowIndex, row := range element.grid { - for columnIndex, _ := range row { - width := columnWidths[columnIndex] - height := rowHeights[rowIndex] - cellBounds := image.Rect ( - int(x), int(y), - int(x + width), int(y + height)) - - var id tomo.Pattern - isHeading := - rowIndex == 0 && element.topHeading || - columnIndex == 0 && element.leftHeading - if isHeading { - id = tomo.PatternTableHead - } else { - id = tomo.PatternTableCell - } - element.grid[rowIndex][columnIndex].Rectangle = cellBounds - element.grid[rowIndex][columnIndex].Pattern = id - - element.redoCell(columnIndex, rowIndex) - x += float64(width) - } - - x = float64(bounds.Min.X) - y += rowHeights[rowIndex] - } - - element.core.DamageAll() - - // update the minimum size of the element - if element.forcedMinimumHeight > 0 { - minHeight = float64(element.forcedMinimumHeight) - } - if element.forcedMinimumWidth > 0 { - minWidth = float64(element.forcedMinimumWidth) - } - element.core.SetMinimumSize(int(minWidth), int(minHeight)) - - // notify parent of scroll bounds change - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *TableContainer) updateMinimumSize () { - if element.forcedMinimumHeight > 0 && element.forcedMinimumWidth > 0 { - element.core.SetMinimumSize ( - element.forcedMinimumWidth, - element.forcedMinimumHeight) - return - } - - columnWidths := make([]int, element.columns) - rowHeights := make([]int, element.rows) - padding := element.theme.Padding(tomo.PatternTableCell) - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - width, height := padding.Horizontal(), padding.Vertical() - - if child.Element != nil { - minWidth, minHeight := child.MinimumSize() - width += minWidth - height += minHeight - if width > columnWidths[columnIndex] { - columnWidths[columnIndex] = width - } - if height > rowHeights[rowIndex] { - rowHeights[rowIndex] = height - } - } - }} - - var minWidth, minHeight int - for _, width := range columnWidths { minWidth += width } - for _, height := range rowHeights { minHeight += height } - - if element.forcedMinimumHeight > 0 { - minHeight = element.forcedMinimumHeight - } - if element.forcedMinimumWidth > 0 { - minWidth = element.forcedMinimumWidth - } - - element.core.SetMinimumSize(minWidth, minHeight) -} - -func (element *TableContainer) childDrawCallback (region image.Rectangle) { - element.core.DamageRegion(region) -} diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go index 6c7843e..35d804a 100644 --- a/examples/raycaster/game.go +++ b/examples/raycaster/game.go @@ -103,7 +103,7 @@ func (game *Game) tick () { if game.stamina < 0 { game.stamina = 0 } - + tomo.Do(game.Invalidate) if statUpdate && game.onStatUpdate != nil { tomo.Do(game.onStatUpdate) diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go index 46e0471..f848c99 100644 --- a/examples/raycaster/main.go +++ b/examples/raycaster/main.go @@ -57,12 +57,13 @@ func run () { topBar.Adopt(elements.NewLabel("Health:")) topBar.AdoptExpand(healthBar) container.Adopt(topBar) - container.AdoptExpand(game) + container.AdoptExpand(game.Raycaster) game.Focus() game.OnStatUpdate (func () { staminaBar.SetProgress(game.Stamina()) }) + game.Start() window.OnClose(tomo.Stop) window.Show() diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go index 4b6e902..4360ef4 100644 --- a/examples/raycaster/raycaster.go +++ b/examples/raycaster/raycaster.go @@ -138,6 +138,8 @@ func (element *Raycaster) Focus () { element.entity.Focus() } +func (element *Raycaster) SetEnabled (bool) { } + func (element *Raycaster) Enabled () bool { return true } func (element *Raycaster) HandleFocusChange () { }