From 4722656c7d37778dbfd823ff1c707c5494ceac0a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 2 Feb 2023 18:20:02 -0500 Subject: [PATCH 01/15] Config stub --- config/config.go | 53 +++++++++++++++++++++++++++++++++++++----------- config/parse.go | 9 ++++++++ 2 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 config/parse.go diff --git a/config/config.go b/config/config.go index 8828639..ab962be 100644 --- a/config/config.go +++ b/config/config.go @@ -1,22 +1,51 @@ package config -// Padding returns the amount of internal padding elements should have. An -// element's inner content (such as text) should be inset by this amount, -// in addition to the inset returned by the pattern of its background. When -// using the aforementioned inset values to calculate the element's minimum size -// or the position and alignment of its content, all parameters in the -// PatternState should be unset except for Case. -func Padding () int { +type Config interface { + // Padding returns the amount of internal padding elements should have. + // An element's inner content (such as text) should be inset by this + // amount, in addition to the inset returned by the pattern of its + // background. + Padding () int + + // Margin returns how much space should be put in between elements. + Margin () int + + // HandleWidth returns how large grab handles should typically be. This + // is important for accessibility reasons. + HandleWidth () int + + // ScrollVelocity returns how many pixels should be scrolled every time + // a scroll button is pressed. + ScrollVelocity () int + + // ThemePath returns the directory path to the theme. + ThemePath () string +} + +// Default specifies default configuration values. +type Default struct { } + +// Padding returns the default padding value. +func (Default) Padding () int { return 7 } -// Margin returns how much space should be put in between elements. -func Margin () int { +// Margin returns the default margin value. +func (Default) Margin () int { return 8 } -// HandleWidth returns how large grab handles should typically be. This is -// important for accessibility reasons. -func HandleWidth () int { +// HandleWidth returns the default handle width value. +func (Default) HandleWidth () int { return 16 } + +// ScrollVelocity returns the default scroll velocity value. +func (Default) ScrollVelocity () int { + return 16 +} + +// ThemePath returns the default theme path. +func (Default) ThemePath () (string) { + return "" +} diff --git a/config/parse.go b/config/parse.go new file mode 100644 index 0000000..258d48f --- /dev/null +++ b/config/parse.go @@ -0,0 +1,9 @@ +package config + +import "io" + +// Parse parses a configuration file and returns it as a Config. +func Parse (source io.Reader) (config Config) { + // TODO + return Default { } +} From 83b8040520890c01a5dfdf934d3e1ea33d4eb7e4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 00:57:18 -0500 Subject: [PATCH 02/15] Theme stub --- config/config.go | 1 + theme/button.go | 48 -------- theme/default.go | 167 +++++++++++++++++++++++++++ theme/defaultpatterns.go | 237 ++++++++++++++++++++++++++++++++++++++ theme/input.go | 21 ---- theme/inset.go | 42 +++++++ theme/list.go | 43 ------- theme/parse.go | 9 ++ theme/patterns.go | 239 --------------------------------------- theme/scroll.go | 63 ----------- theme/state.go | 48 ++++++++ theme/theme.go | 199 +++++++++++++++----------------- theme/util.go | 16 +++ 13 files changed, 613 insertions(+), 520 deletions(-) delete mode 100644 theme/button.go create mode 100644 theme/default.go create mode 100644 theme/defaultpatterns.go delete mode 100644 theme/input.go create mode 100644 theme/inset.go delete mode 100644 theme/list.go create mode 100644 theme/parse.go delete mode 100644 theme/patterns.go delete mode 100644 theme/scroll.go create mode 100644 theme/state.go create mode 100644 theme/util.go diff --git a/config/config.go b/config/config.go index ab962be..cdc4868 100644 --- a/config/config.go +++ b/config/config.go @@ -1,5 +1,6 @@ package config +// Config can return global configuration parameters. type Config interface { // Padding returns the amount of internal padding elements should have. // An element's inner content (such as text) should be inset by this diff --git a/theme/button.go b/theme/button.go deleted file mode 100644 index 213d341..0000000 --- a/theme/button.go +++ /dev/null @@ -1,48 +0,0 @@ -package theme - -import "git.tebibyte.media/sashakoshka/tomo/artist" - -var buttonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var selectedButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x4B5B59FF)), - artist.NewUniform(hex(0x8D9894FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedSelectedButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x4B5B59FF)), - artist.NewUniform(hex(0x8D9894FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var disabledButtonPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) diff --git a/theme/default.go b/theme/default.go new file mode 100644 index 0000000..5d2eec6 --- /dev/null +++ b/theme/default.go @@ -0,0 +1,167 @@ +package theme + +import "image" +import "golang.org/x/image/font" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/defaultfont" + +// Default is the default theme. +type Default struct { } + +// FontFace returns the default font face. +func (Default) FontFace (style FontStyle, size FontSize, c Case) font.Face { + switch style { + case FontStyleBold: + return defaultfont.FaceBold + case FontStyleItalic: + return defaultfont.FaceItalic + case FontStyleBoldItalic: + return defaultfont.FaceBoldItalic + default: + return defaultfont.FaceRegular + } +} + +// Icon returns an icon from the default set corresponding to the given name. +func (Default) Icon (string, Case) artist.Pattern { + // TODO + return uhex(0) +} + +// Pattern returns a pattern from the default theme corresponding to the given +// pattern ID. +func (Default) Pattern ( + pattern Pattern, + c Case, + state PatternState, +) artist.Pattern { + switch pattern { + case PatternAccent: + return accentPattern + case PatternBackground: + return backgroundPattern + case PatternForeground: + if state.Disabled { + return weakForegroundPattern + } else { + return foregroundPattern + } + case PatternDead: + return deadPattern + case PatternRaised: + if c == C("basic", "listEntry") { + if state.Focused { + if state.On { + return focusedOnListEntryPattern + } else { + return focusedListEntryPattern + } + } else { + if state.On { + return onListEntryPattern + } else { + return listEntryPattern + } + } + } else { + if state.Focused { + return selectedRaisedPattern + } else { + return raisedPattern + } + } + case PatternSunken: + if c == C("basic", "list") { + if state.Focused { + return focusedListPattern + } else { + return listPattern + } + } else { + return sunkenPattern + } + case PatternPinboard: + return texturedSunkenPattern + case PatternButton: + if state.Disabled { + return disabledButtonPattern + } else { + if state.Pressed { + if state.Focused { + return pressedSelectedButtonPattern + } else { + return pressedButtonPattern + } + } else { + if state.Focused { + return selectedButtonPattern + } else { + return buttonPattern + } + } + } + case PatternInput: + if state.Disabled { + return disabledInputPattern + } else { + if state.Focused { + return selectedInputPattern + } else { + return inputPattern + } + } + case PatternGutter: + if state.Disabled { + return disabledScrollGutterPattern + } else { + return scrollGutterPattern + } + case PatternHandle: + if state.Disabled { + return disabledScrollBarPattern + } else { + if state.Focused { + if state.Pressed { + return pressedSelectedScrollBarPattern + } else { + return selectedScrollBarPattern + } + } else { + if state.Pressed { + return pressedScrollBarPattern + } else { + return scrollBarPattern + } + } + } + default: + return uhex(0) + } +} + +// Inset returns the default inset value for the given pattern. +func (Default) Inset (pattern Pattern, c Case) Inset { + switch pattern { + case PatternRaised: + if c == C("basic", "listEntry") { + return Inset { 2, 1, 2, 1 } + } else { + return Inset { 1, 1, 1, 1 } + } + case PatternSunken: + if c == C("basic", "list") { + return Inset { 4, 6, 4, 6 } + } else { + return Inset { 1, 1, 1, 1 } + } + + case PatternInput, PatternButton, PatternHandle, PatternPinboard: + return Inset { 1, 1, 1, 1} + default: return Inset { } + } +} + +// Sink returns the default sink vector for the given pattern. +func (Default) Sink (pattern Pattern, c Case) image.Point { + return image.Point { 1, 1 } +} diff --git a/theme/defaultpatterns.go b/theme/defaultpatterns.go new file mode 100644 index 0000000..6d32a42 --- /dev/null +++ b/theme/defaultpatterns.go @@ -0,0 +1,237 @@ +package theme + +import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/artist" + +var accentPattern = artist.NewUniform(hex(0x408090FF)) +var backgroundPattern = artist.NewUniform(color.Gray16 { 0xAAAA }) +var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 }) +var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 }) +var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 }) + +var sunkenPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0x3b534eFF)), + artist.NewUniform(hex(0x97a09cFF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) + +var texturedSunkenPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0x3b534eFF)), + artist.NewUniform(hex(0x97a09cFF)), + }, + }, + // artist.Stroke { Pattern: artist.Striped { + // First: artist.Stroke { + // Weight: 2, + // Pattern: artist.NewUniform(hex(0x97a09cFF)), + // }, + // Second: artist.Stroke { + // Weight: 1, + // Pattern: artist.NewUniform(hex(0x6e8079FF)), + // }, + // }}) + + artist.Stroke { Pattern: artist.Noisy { + Low: artist.NewUniform(hex(0x97a09cFF)), + High: artist.NewUniform(hex(0x6e8079FF)), + }}) + +var raisedPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xDBDBDBFF)), + artist.NewUniform(hex(0x383C3AFF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) }) + +var selectedRaisedPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xDBDBDBFF)), + artist.NewUniform(hex(0x383C3AFF)), + }, + }, + artist.Stroke { Weight: 1, Pattern: accentPattern }, + artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) }) + +var deadPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) + +var buttonPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xCCD5D2FF)), + artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) +var selectedButtonPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xCCD5D2FF)), + artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Stroke { Weight: 1, Pattern: accentPattern }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) + +var pressedButtonPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0x4B5B59FF)), + artist.NewUniform(hex(0x8D9894FF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) +var pressedSelectedButtonPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0x4B5B59FF)), + artist.NewUniform(hex(0x8D9894FF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) +var disabledButtonPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, + artist.Stroke { Pattern: backgroundPattern }) + +var inputPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0x89925AFF)), + artist.NewUniform(hex(0xD2CB9AFF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) }) +var selectedInputPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { Weight: 1, Pattern: accentPattern }, + artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) }) +var disabledInputPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, + artist.Stroke { Pattern: backgroundPattern }) + +var listPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + uhex(0x383C3AFF), + uhex(0x999C99FF), + }, + }, + artist.Stroke { Pattern: uhex(0x999C99FF) }) + +var focusedListPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { Weight: 1, Pattern: accentPattern }, + artist.Stroke { Pattern: uhex(0x999C99FF) }) + +var listEntryPattern = artist.Padded { + Stroke: uhex(0x383C3AFF), + Fill: uhex(0x999C99FF), + Sides: []int { 0, 0, 0, 1 }, +} + +var onListEntryPattern = artist.Padded { + Stroke: uhex(0x383C3AFF), + Fill: uhex(0x6e8079FF), + Sides: []int { 0, 0, 0, 1 }, +} + +var focusedListEntryPattern = artist.Padded { + Stroke: accentPattern, + Fill: uhex(0x999C99FF), + Sides: []int { 0, 1, 0, 1 }, +} + +var focusedOnListEntryPattern = artist.Padded { + Stroke: accentPattern, + Fill: uhex(0x6e8079FF), + Sides: []int { 0, 1, 0, 1 }, +} + +var scrollGutterPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0x3b534eFF)), + artist.NewUniform(hex(0x6e8079FF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) }) +var disabledScrollGutterPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, + artist.Stroke { Pattern: backgroundPattern }) +var scrollBarPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xCCD5D2FF)), + artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) +var selectedScrollBarPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xCCD5D2FF)), + artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Stroke { Weight: 1, Pattern: accentPattern }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) +var pressedScrollBarPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xCCD5D2FF)), + artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Stroke { Weight: 1, Pattern: artist.NewUniform(hex(0x8D9894FF)) }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) }) +var pressedSelectedScrollBarPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: strokePattern }, + artist.Stroke { + Weight: 1, + Pattern: artist.Beveled { + artist.NewUniform(hex(0xCCD5D2FF)), + artist.NewUniform(hex(0x4B5B59FF)), + }, + }, + artist.Stroke { Weight: 1, Pattern: accentPattern }, + artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) }) +var disabledScrollBarPattern = artist.NewMultiBordered ( + artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, + artist.Stroke { Pattern: backgroundPattern }) diff --git a/theme/input.go b/theme/input.go deleted file mode 100644 index e201beb..0000000 --- a/theme/input.go +++ /dev/null @@ -1,21 +0,0 @@ -package theme - -import "git.tebibyte.media/sashakoshka/tomo/artist" - -var inputPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x89925AFF)), - artist.NewUniform(hex(0xD2CB9AFF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) }) -var selectedInputPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xD2CB9AFF)) }) -var disabledInputPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) diff --git a/theme/inset.go b/theme/inset.go new file mode 100644 index 0000000..1cace51 --- /dev/null +++ b/theme/inset.go @@ -0,0 +1,42 @@ +package theme + +import "image" + +// Inset represents an inset amount for all four sides of a rectangle. The top +// side is at index zero, the right at index one, the bottom at index two, and +// the left at index three. These values may be negative. +type Inset [4]int + +// Apply returns the given rectangle, shrunk on all four sides by the given +// inset. If a measurment of the inset is negative, that side will instead be +// expanded outward. If the rectangle's dimensions cannot be reduced any +// further, an empty rectangle near its center will be returned. +func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) { + smaller = bigger + if smaller.Dx() < inset[3] + inset[1] { + smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2 + smaller.Max.X = smaller.Min.X + } else { + smaller.Min.X += inset[3] + smaller.Max.X -= inset[1] + } + + if smaller.Dy() < inset[0] + inset[2] { + smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2 + smaller.Max.Y = smaller.Min.Y + } else { + smaller.Min.Y += inset[0] + smaller.Max.Y -= inset[2] + } + return +} + +// Inverse returns a negated version of the inset. +func (inset Inset) Inverse () (prime Inset) { + return Inset { + inset[0] * -1, + inset[1] * -1, + inset[2] * -1, + inset[3] * -1, + } +} diff --git a/theme/list.go b/theme/list.go deleted file mode 100644 index faf12e3..0000000 --- a/theme/list.go +++ /dev/null @@ -1,43 +0,0 @@ -package theme - -import "git.tebibyte.media/sashakoshka/tomo/artist" - -var listPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - uhex(0x383C3AFF), - uhex(0x999C99FF), - }, - }, - artist.Stroke { Pattern: uhex(0x999C99FF) }) - -var focusedListPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: uhex(0x999C99FF) }) - -var listEntryPattern = artist.Padded { - Stroke: uhex(0x383C3AFF), - Fill: uhex(0x999C99FF), - Sides: []int { 0, 0, 0, 1 }, -} - -var onListEntryPattern = artist.Padded { - Stroke: uhex(0x383C3AFF), - Fill: uhex(0x6e8079FF), - Sides: []int { 0, 0, 0, 1 }, -} - -var focusedListEntryPattern = artist.Padded { - Stroke: accentPattern, - Fill: uhex(0x999C99FF), - Sides: []int { 0, 1, 0, 1 }, -} - -var focusedOnListEntryPattern = artist.Padded { - Stroke: accentPattern, - Fill: uhex(0x6e8079FF), - Sides: []int { 0, 1, 0, 1 }, -} diff --git a/theme/parse.go b/theme/parse.go new file mode 100644 index 0000000..db96c86 --- /dev/null +++ b/theme/parse.go @@ -0,0 +1,9 @@ +package theme + +import "io" + +// Parse parses a theme file and returns it as a Theme. +func Parse (io.Reader) (Theme) { + // TODO + return Default { } +} diff --git a/theme/patterns.go b/theme/patterns.go deleted file mode 100644 index aab2b18..0000000 --- a/theme/patterns.go +++ /dev/null @@ -1,239 +0,0 @@ -package theme - -import "image" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -// Case sepecifies what kind of element is using a pattern. It contains a -// namespace parameter and an element parameter. The element parameter does not -// necissarily need to match an element name, but if it can, it should. Both -// parameters should be written in camel case. Themes can change their styling -// based on this parameter for fine-grained control over the look and feel of -// specific elements. -type Case struct { Namespace, Element string } - -// C can be used as shorthand to generate a case struct as used in PatternState. -func C (namespace, element string) (c Case) { - return Case { - Namespace: namespace, - Element: element, - } -} - -// PatternState lists parameters which can change the appearance of some -// patterns. For example, passing a PatternState with Selected set to true may -// result in a pattern that has a colored border within it. -type PatternState struct { - Case - - // On should be set to true if the element that is using this pattern is - // in some sort of "on" state, such as if a checkbox is checked or a - // switch is toggled on. This is only necessary if the element in - // question is capable of being toggled. - On bool - - // Focused should be set to true if the element that is using this - // pattern is currently focused. - Focused bool - - // Pressed should be set to true if the element that is using this - // pattern is being pressed down by the mouse. This is only necessary if - // the element in question processes mouse button events. - Pressed bool - - // Disabled should be set to true if the element that is using this - // pattern is locked and cannot be interacted with. Disabled variations - // of patterns are typically flattened and greyed-out. - Disabled bool - - // Invalid should be set to true if th element that is using this - // pattern wants to warn the user of an invalid interaction or data - // entry. Invalid variations typically have some sort of reddish tint - // or outline. - Invalid bool -} - -// Inset represents an inset amount for all four sides of a rectangle. The top -// side is at index zero, the right at index one, the bottom at index two, and -// the left at index three. These values may be negative. -type Inset [4]int - -// Apply returns the given rectangle, shrunk on all four sides by the given -// inset. If a measurment of the inset is negative, that side will instead be -// expanded outward. If the rectangle's dimensions cannot be reduced any -// further, an empty rectangle near its center will be returned. -func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) { - smaller = bigger - if smaller.Dx() < inset[3] + inset[1] { - smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2 - smaller.Max.X = smaller.Min.X - } else { - smaller.Min.X += inset[3] - smaller.Max.X -= inset[1] - } - - if smaller.Dy() < inset[0] + inset[2] { - smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2 - smaller.Max.Y = smaller.Min.Y - } else { - smaller.Min.Y += inset[0] - smaller.Max.Y -= inset[2] - } - return -} - -// Inverse returns a negated version of the inset. -func (inset Inset) Inverse () (prime Inset) { - return Inset { - inset[0] * -1, - inset[1] * -1, - inset[2] * -1, - inset[3] * -1, - } -} - -// AccentPattern returns the accent pattern, which is usually just a solid -// color. -func AccentPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - return accentPattern, Inset { } -} - -// BackgroundPattern returns the main background pattern. -func BackgroundPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - return backgroundPattern, Inset { } -} - -// DeadPattern returns a pattern that can be used to mark an area or gap that -// serves no purpose, but still needs aesthetic structure. -func DeadPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - return deadPattern, Inset { } -} - -// ForegroundPattern returns the color text should be. -func ForegroundPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Disabled { - return weakForegroundPattern, Inset { } - } else { - return foregroundPattern, Inset { } - } -} - -// InputPattern returns a background pattern for any input field that can be -// edited by typing with the keyboard. -func InputPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Disabled { - return disabledInputPattern, Inset { 1, 1, 1, 1 } - } else { - if state.Focused { - return selectedInputPattern, Inset { 1, 1, 1, 1 } - } else { - return inputPattern, Inset { 1, 1, 1, 1 } - } - } -} - -// ListPattern returns a background pattern for a list of things. -func ListPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Focused { - pattern = focusedListPattern - inset = Inset { 2, 1, 2, 1 } - } else { - pattern = listPattern - inset = Inset { 2, 1, 1, 1 } - } - return -} - -// ItemPattern returns a background pattern for a list item. -func ItemPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Focused { - if state.On { - pattern = focusedOnListEntryPattern - } else { - pattern = focusedListEntryPattern - } - } else { - if state.On { - pattern = onListEntryPattern - } else { - pattern = listEntryPattern - } - } - inset = Inset { 4, 6, 4, 6 } - return -} - -// ButtonPattern returns a pattern to be displayed on buttons. -func ButtonPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Disabled { - return disabledButtonPattern, Inset { 1, 1, 1, 1 } - } else { - if state.Pressed { - if state.Focused { - return pressedSelectedButtonPattern, Inset { - 2, 0, 0, 2 } - } else { - return pressedButtonPattern, Inset { 2, 0, 0, 2 } - } - } else { - if state.Focused { - return selectedButtonPattern, Inset { 1, 1, 1, 1 } - } else { - return buttonPattern, Inset { 1, 1, 1, 1 } - } - } - } -} - -// GutterPattern returns a pattern to be used to mark a track along which -// something slides. -func GutterPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Disabled { - return disabledScrollGutterPattern, Inset { 0, 0, 0, 0 } - } else { - return scrollGutterPattern, Inset { 0, 0, 0, 0 } - } -} - -// HandlePattern returns a pattern to be displayed on a grab handle that slides -// along a gutter. -func HandlePattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Disabled { - return disabledScrollBarPattern, Inset { 1, 1, 1, 1 } - } else { - if state.Focused { - if state.Pressed { - return pressedSelectedScrollBarPattern, Inset { 1, 1, 1, 1 } - } else { - return selectedScrollBarPattern, Inset { 1, 1, 1, 1 } - } - } else { - if state.Pressed { - return pressedScrollBarPattern, Inset { 1, 1, 1, 1 } - } else { - return scrollBarPattern, Inset { 1, 1, 1, 1 } - } - } - } -} - -// SunkenPattern returns a general purpose pattern that is sunken/engraved into -// the background. -func SunkenPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - return sunkenPattern, Inset { 1, 1, 1, 1 } -} - -// RaisedPattern returns a general purpose pattern that is raised up out of the -// background. -func RaisedPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - if state.Focused { - return selectedRaisedPattern, Inset { 1, 1, 1, 1 } - } else { - return raisedPattern, Inset { 1, 1, 1, 1 } - } -} - -// PinboardPattern returns a textured backdrop pattern. Anything drawn within it -// should have its own background pattern. -func PinboardPattern (state PatternState) (pattern artist.Pattern, inset Inset) { - return texturedSunkenPattern, Inset { 1, 1, 1, 1 } -} diff --git a/theme/scroll.go b/theme/scroll.go deleted file mode 100644 index a12cdd3..0000000 --- a/theme/scroll.go +++ /dev/null @@ -1,63 +0,0 @@ -package theme - -import "git.tebibyte.media/sashakoshka/tomo/artist" - -var scrollGutterPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b534eFF)), - artist.NewUniform(hex(0x6e8079FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) }) -var disabledScrollGutterPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) -var scrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var selectedScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x8D9894FF)) }) -var pressedScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: artist.NewUniform(hex(0x8D9894FF)) }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) }) -var pressedSelectedScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xCCD5D2FF)), - artist.NewUniform(hex(0x4B5B59FF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x7f8c89FF)) }) -var disabledScrollBarPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: weakForegroundPattern }, - artist.Stroke { Pattern: backgroundPattern }) diff --git a/theme/state.go b/theme/state.go new file mode 100644 index 0000000..51e0ab2 --- /dev/null +++ b/theme/state.go @@ -0,0 +1,48 @@ +package theme + +// Case sepecifies what kind of element is using a pattern. It contains a +// namespace parameter and an element parameter. The element parameter does not +// necissarily need to match an element name, but if it can, it should. Both +// parameters should be written in camel case. Themes can change their styling +// based on this parameter for fine-grained control over the look and feel of +// specific elements. +type Case struct { Namespace, Element string } + +// C can be used as shorthand to generate a case struct as used in PatternState. +func C (namespace, element string) (c Case) { + return Case { + Namespace: namespace, + Element: element, + } +} + +// PatternState lists parameters which can change the appearance of some +// patterns. For example, passing a PatternState with Selected set to true may +// result in a pattern that has a colored border within it. +type PatternState struct { + // On should be set to true if the element that is using this pattern is + // in some sort of "on" state, such as if a checkbox is checked or a + // switch is toggled on. This is only necessary if the element in + // question is capable of being toggled. + On bool + + // Focused should be set to true if the element that is using this + // pattern is currently focused. + Focused bool + + // Pressed should be set to true if the element that is using this + // pattern is being pressed down by the mouse. This is only necessary if + // the element in question processes mouse button events. + Pressed bool + + // Disabled should be set to true if the element that is using this + // pattern is locked and cannot be interacted with. Disabled variations + // of patterns are typically flattened and greyed-out. + Disabled bool + + // Invalid should be set to true if th element that is using this + // pattern wants to warn the user of an invalid interaction or data + // entry. Invalid variations typically have some sort of reddish tint + // or outline. + Invalid bool +} diff --git a/theme/theme.go b/theme/theme.go index 3f59fe6..bc0f370 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -1,113 +1,100 @@ package theme -import "image/color" +import "image" import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/defaultfont" -// none of these colors are final! TODO: generate these values from a theme -// file at startup. +// FontStyle specifies stylistic alterations to a font face. +type FontStyle int; const ( + FontStyleRegular FontStyle = 0 + FontStyleBold FontStyle = 1 + FontStyleItalic FontStyle = 2 + FontStyleBoldItalic FontStyle = 1 | 2 +) -func hex (color uint32) (c color.RGBA) { - c.A = uint8(color) - c.B = uint8(color >> 8) - c.G = uint8(color >> 16) - c.R = uint8(color >> 24) - return -} - -func uhex (color uint32) (pattern artist.Pattern) { - return artist.NewUniform(hex(color)) -} - -var accentPattern = artist.NewUniform(hex(0x408090FF)) -var backgroundPattern = artist.NewUniform(color.Gray16 { 0xAAAA }) -var foregroundPattern = artist.NewUniform(color.Gray16 { 0x0000 }) -var weakForegroundPattern = artist.NewUniform(color.Gray16 { 0x4444 }) -var strokePattern = artist.NewUniform(color.Gray16 { 0x0000 }) - -var sunkenPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b534eFF)), - artist.NewUniform(hex(0x97a09cFF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) - -var texturedSunkenPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0x3b534eFF)), - artist.NewUniform(hex(0x97a09cFF)), - }, - }, - // artist.Stroke { Pattern: artist.Striped { - // First: artist.Stroke { - // Weight: 2, - // Pattern: artist.NewUniform(hex(0x97a09cFF)), - // }, - // Second: artist.Stroke { - // Weight: 1, - // Pattern: artist.NewUniform(hex(0x6e8079FF)), - // }, - // }}) - - artist.Stroke { Pattern: artist.Noisy { - Low: artist.NewUniform(hex(0x97a09cFF)), - High: artist.NewUniform(hex(0x6e8079FF)), - }}) - -var raisedPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xDBDBDBFF)), - artist.NewUniform(hex(0x383C3AFF)), - }, - }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) }) - -var selectedRaisedPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { - Weight: 1, - Pattern: artist.Beveled { - artist.NewUniform(hex(0xDBDBDBFF)), - artist.NewUniform(hex(0x383C3AFF)), - }, - }, - artist.Stroke { Weight: 1, Pattern: accentPattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0xAAAAAAFF)) }) - -var deadPattern = artist.NewMultiBordered ( - artist.Stroke { Weight: 1, Pattern: strokePattern }, - artist.Stroke { Pattern: artist.NewUniform(hex(0x97a09cFF)) }) - -// TODO: load fonts from an actual source instead of using defaultfont - -// FontFaceRegular returns the font face to be used for normal text. -func FontFaceRegular () font.Face { - return defaultfont.FaceRegular -} - -// FontFaceBold returns the font face to be used for bolded text. -func FontFaceBold () font.Face { - return defaultfont.FaceBold -} - -// FontFaceItalic returns the font face to be used for italicized text. -func FontFaceItalic () font.Face { - return defaultfont.FaceItalic -} - -// FontFaceBoldItalic returns the font face to be used for text that is both -// bolded and italicized. -func FontFaceBoldItalic () font.Face { - return defaultfont.FaceBoldItalic +// FontSize specifies the general size of a font face in a semantic way. +type FontSize int; const ( + // FontSizeNormal is the default font size that should be used for most + // things. + FontSizeNormal FontSize = iota + + // FontSizeLarge is a larger font size suitable for things like section + // headings. + FontSizeLarge + + // FontSizeHuge is a very large font size suitable for things like + // titles, wizard step names, digital clocks, etc. + FontSizeHuge + + // FontSizeSmall is a smaller font size. Try not to use this unless it + // makes a lot of sense to do so, because it can negatively impact + // accessibility. It is useful for things like copyright notices at the + // bottom of some window that the average user doesn't actually care + // about. + FontSizeSmall +) + +// Pattern lists a number of cannonical pattern types, each with its own ID. +// This allows custom elements to follow themes, even those that do not +// explicitly support them. +type Pattern int; const ( + // PatternAccent is the accent color of the theme. It is safe to assume + // that this is, by default, a solid color. + PatternAccent Pattern = iota + + // PatternBackground is the background color of the theme. It is safe to + // assume that this is, by default, a solid color. + PatternBackground + + // PatternForeground is the foreground text color of the theme. It is + // safe to assume that this is, by default, a solid color. + PatternForeground + + // PatternDead is a pattern that is displayed on a "dead area" where no + // controls exist, but there still must be some indication of visual + // structure (such as in the corner between two scroll bars). + PatternDead + + // PatternRaised is a generic raised pattern. + PatternRaised + + // PatternSunken is a generic sunken pattern. + PatternSunken + + // PatternPinboard is similar to PatternSunken, but it is textured. + PatternPinboard + + // PatternButton is a button pattern. + PatternButton + + // PatternInput is a pattern for input fields, editable text areas, etc. + PatternInput + + // PatternGutter is a track for things to slide on. + PatternGutter + + // PatternHandle is a handle that slides along a gutter. + PatternHandle +) + +// Theme represents a visual style configuration, +type Theme interface { + // FontFace returns the proper font for a given style, size, and case. + FontFace (FontStyle, FontSize, Case) font.Face + + // Icon returns an appropriate icon given an icon name and case. + Icon (string, Case) artist.Pattern + + // Pattern returns an appropriate pattern given a pattern name, case, + // and state. + Pattern (Pattern, Case, PatternState) artist.Pattern + + // Inset returns the area on all sides of a given pattern that is not + // meant to be drawn on. + Inset (Pattern, Case) Inset + + // Sink returns a vector that should be added to an element's inner + // content when it is pressed down (if applicable) to simulate a 3D + // sinking effect. + Sink (Pattern, Case) image.Point } diff --git a/theme/util.go b/theme/util.go new file mode 100644 index 0000000..2e9723e --- /dev/null +++ b/theme/util.go @@ -0,0 +1,16 @@ +package theme + +import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/artist" + +func hex (color uint32) (c color.RGBA) { + c.A = uint8(color) + c.B = uint8(color >> 8) + c.G = uint8(color >> 16) + c.R = uint8(color >> 24) + return +} + +func uhex (color uint32) (pattern artist.Pattern) { + return artist.NewUniform(hex(color)) +} From 8ccaa0faba703e2a100210136bfee9cdb3967dbc Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 01:14:03 -0500 Subject: [PATCH 03/15] Added Themeable and Configurable element interfaces --- elements/element.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/elements/element.go b/elements/element.go index f97a48c..3743aa4 100644 --- a/elements/element.go +++ b/elements/element.go @@ -2,7 +2,9 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/config" // Element represents a basic on-screen object. type Element interface { @@ -157,3 +159,19 @@ type Scrollable interface { // ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed. OnScrollBoundsChange (callback func ()) } + +// Themeable represents an element that can modify its appearance to fit within +// a theme. +type Themeable interface { + // SetTheme sets the element's theme to something fulfilling the + // theme.Theme interface. + SetTheme (theme.Theme) +} + +// Configurable represents an element that can modify its behavior to fit within +// a set of configuration parameters. +type Configurable interface { + // SetConfig sets the element's configuration to something fulfilling + // the config.Config interface. + SetConfig (config.Config) +} From bdf599f93cbf37ca6ec572458bdaa361ee0e5eda Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 01:25:45 -0500 Subject: [PATCH 04/15] Backends must now accept Config and Theme --- backend.go | 8 ++++++++ elements/element.go | 3 ++- tomo.go | 28 ++++++++++++++++++++-------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/backend.go b/backend.go index 3b1ff31..a22e1ec 100644 --- a/backend.go +++ b/backend.go @@ -2,6 +2,8 @@ package tomo import "errors" import "git.tebibyte.media/sashakoshka/tomo/data" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/elements" // Backend represents a connection to a display server, or something similar. @@ -28,6 +30,12 @@ type Backend interface { // Paste returns the data currently in the clipboard. Paste (accept []data.Mime) (data.Data) + + // SetTheme sets the theme of all open windows. + SetTheme (theme.Theme) + + // SetConfig sets the configuration of all open windows. + SetConfig (config.Config) } // BackendFactory represents a function capable of constructing a backend diff --git a/elements/element.go b/elements/element.go index 3743aa4..30022ee 100644 --- a/elements/element.go +++ b/elements/element.go @@ -39,7 +39,8 @@ type Element interface { type Focusable interface { Element - // Focused returns whether or not this element is currently focused. + // Focused returns whether or not this element or any of its children + // are currently focused. Focused () (selected bool) // Focus focuses this element, if its parent element grants the diff --git a/tomo.go b/tomo.go index b1416b0..ac0a87e 100644 --- a/tomo.go +++ b/tomo.go @@ -1,7 +1,8 @@ package tomo -import "errors" import "git.tebibyte.media/sashakoshka/tomo/data" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/elements" var backend Backend @@ -26,7 +27,7 @@ func Stop () { // Do executes the specified callback within the main thread as soon as // possible. This function can be safely called from other threads. func Do (callback func ()) { - if backend == nil { panic("no backend is running") } + assertBackend() backend.Do(callback) } @@ -35,22 +36,33 @@ func Do (callback func ()) { // why. If this function is called without a running backend, an error is // returned as well. func NewWindow (width, height int) (window elements.Window, err error) { - if backend == nil { - err = errors.New("no backend is running.") - return - } + assertBackend() return backend.NewWindow(width, height) } // Copy puts data into the clipboard. func Copy (data data.Data) { - if backend == nil { panic("no backend is running") } + assertBackend() backend.Copy(data) } // Paste returns the data currently in the clipboard. This method may // return nil. func Paste (accept []data.Mime) (data.Data) { - if backend == nil { panic("no backend is running") } + assertBackend() return backend.Paste(accept) } + +// SetTheme sets the theme of all open windows. +func SetTheme (theme theme.Theme) { + backend.SetTheme(theme) +} + +// SetConfig sets the configuration of all open windows. +func SetConfig (config config.Config) { + backend.SetConfig(config) +} + +func assertBackend () { + if backend == nil { panic("no backend is running") } +} From d79701d01b47273e84c5407b9e9ebe95f78147d9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 01:35:59 -0500 Subject: [PATCH 05/15] X backend conforms to new API --- backends/x/window.go | 28 ++++++++++++++++++++++++++++ backends/x/x.go | 24 ++++++++++++++++++++++++ elements/element.go | 4 ++++ 3 files changed, 56 insertions(+) diff --git a/backends/x/window.go b/backends/x/window.go index d51d948..5509bf2 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -8,6 +8,8 @@ import "github.com/jezek/xgbutil/xevent" import "github.com/jezek/xgbutil/xwindow" import "github.com/jezek/xgbutil/xgraphics" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/elements" @@ -20,6 +22,9 @@ type Window struct { onClose func () skipChildDrawCallback bool + theme theme.Theme + config config.Config + metrics struct { width int height int @@ -69,6 +74,9 @@ func (backend *Backend) NewWindow ( Connect(backend.connection, window.xWindow.Id) xevent.MotionNotifyFun(window.handleMotionNotify). Connect(backend.connection, window.xWindow.Id) + + window.SetTheme(backend.theme) + window.SetConfig(backend.config) window.metrics.width = width window.metrics.height = height @@ -100,6 +108,12 @@ func (window *Window) Adopt (child elements.Element) { // adopt new child window.child = child + if newChild, ok := child.(elements.Themeable); ok { + newChild.SetTheme(window.theme) + } + if newChild, ok := child.(elements.Configurable); ok { + newChild.SetConfig(window.config) + } if newChild, ok := child.(elements.Flexible); ok { newChild.OnFlexibleHeightChange(window.resizeChildToFit) } @@ -196,6 +210,20 @@ func (window *Window) OnClose (callback func ()) { window.onClose = callback } +func (window *Window) SetTheme (theme theme.Theme) { + window.theme = theme + if child, ok := window.child.(elements.Themeable); ok { + child.SetTheme(theme) + } +} + +func (window *Window) SetConfig (config config.Config) { + window.config = config + if child, ok := window.child.(elements.Configurable); ok { + child.SetConfig(config) + } +} + func (window *Window) reallocateCanvas () { window.canvas.Reallocate(window.metrics.width, window.metrics.height) diff --git a/backends/x/x.go b/backends/x/x.go index c7a514b..d3c08f5 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -2,6 +2,8 @@ package x import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "github.com/jezek/xgbutil" import "github.com/jezek/xgb/xproto" @@ -25,6 +27,9 @@ type Backend struct { hyper uint16 } + theme theme.Theme + config config.Config + windows map[xproto.Window] *Window } @@ -95,6 +100,25 @@ func (backend *Backend) Paste (accept []data.Mime) (data data.Data) { return } + +// SetTheme sets the theme of all open windows. +func (backend *Backend) SetTheme (theme theme.Theme) { + backend.assert() + backend.theme = theme + for _, window := range backend.windows { + window.SetTheme(theme) + } +} + +// SetConfig sets the configuration of all open windows. +func (backend *Backend) SetConfig (config config.Config) { + backend.assert() + backend.config = config + for _, window := range backend.windows { + window.SetConfig(config) + } +} + func (backend *Backend) assert () { if backend == nil { panic("nil backend") } } diff --git a/elements/element.go b/elements/element.go index 30022ee..8aaa63f 100644 --- a/elements/element.go +++ b/elements/element.go @@ -164,6 +164,8 @@ type Scrollable interface { // Themeable represents an element that can modify its appearance to fit within // a theme. type Themeable interface { + Element + // SetTheme sets the element's theme to something fulfilling the // theme.Theme interface. SetTheme (theme.Theme) @@ -172,6 +174,8 @@ type Themeable interface { // Configurable represents an element that can modify its behavior to fit within // a set of configuration parameters. type Configurable interface { + Element + // SetConfig sets the element's configuration to something fulfilling // the config.Config interface. SetConfig (config.Config) From 2ff32ca8eafa6adc5adc2b0a331d7c47f5cfd7d4 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 17:06:51 -0500 Subject: [PATCH 06/15] Added thing to get standard directories --- dirs/dirs.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 dirs/dirs.go diff --git a/dirs/dirs.go b/dirs/dirs.go new file mode 100644 index 0000000..966bd5b --- /dev/null +++ b/dirs/dirs.go @@ -0,0 +1,82 @@ +package dirs + +import "os" +import "strings" +import "path/filepath" + +var homeDirectory string +var configHome string +var configDirs []string +var dataHome string +var dataDirs []string +var cacheHome string + +func init () { + var err error + homeDirectory, err = os.UserHomeDir() + if err != nil { + panic("could not get user home directory: " + err.Error()) + } + + configHome = os.Getenv("XDG_CONFIG_HOME") + if configHome == "" { + configHome = filepath.Join(homeDirectory, "/.config/") + } + + configDirsString := os.Getenv("XDG_CONFIG_DIRS") + if configDirsString == "" { + configDirsString = "/etc/xdg/" + } + configDirs = append(strings.Split(configDirsString, ":"), configHome) + + dataHome = os.Getenv("XDG_DATA_HOME") + if dataHome == "" { + dataHome = filepath.Join(homeDirectory, "/.local/share/") + } + + dataDirsString := os.Getenv("XDG_CONFIG_DIRS") + if dataDirsString == "" { + dataDirsString = "/usr/local/share/:/usr/share/" + } + configDirs = append(strings.Split(configDirsString, ":"), configHome) + + cacheHome = os.Getenv("XDG_CACHE_HOME") + if cacheHome == "" { + cacheHome = filepath.Join(homeDirectory, "/.cache/") + } +} + +// ConfigHome returns the path to the directory where user configuration files +// should be stored. +func ConfigHome (name string) (home string) { + return filepath.Join(configHome, name) +} + +// ConfigDirs returns all paths where configuration files might exist. +func ConfigDirs (name string) (dirs []string) { + dirs = make([]string, len(configDirs)) + for index, dir := range configDirs { + dirs[index] = filepath.Join(dir, name) + } + return +} + +// DataHome returns the path to the directory where user data should be stored. +func DataHome (name string) (home string) { + return filepath.Join(dataHome, name) +} + +// DataDirs returns all paths where data files might exist. +func DataDirs (name string) (dirs []string) { + dirs = make([]string, len(dataDirs)) + for index, dir := range dataDirs { + dirs[index] = filepath.Join(dir, name) + } + return +} + +// CacheHome returns the path to the directory where user cache files should be +// stored. +func CacheHome (name string) (home string) { + return filepath.Join(cacheHome, name) +} From 43fea5c8ba9f1cc8e2d794f0f1098980a0e9ed7e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 17:50:45 -0500 Subject: [PATCH 07/15] Tomo will call the parse functions in Theme and Config --- backends/x/x.go | 6 +++++ config/parse.go | 4 ++-- theme/parse.go | 4 ++-- tomo.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/backends/x/x.go b/backends/x/x.go index d3c08f5..9b3c10a 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -38,6 +38,8 @@ func NewBackend () (output tomo.Backend, err error) { backend := &Backend { windows: map[xproto.Window] *Window { }, doChannel: make(chan func (), 0), + theme: theme.Default { }, + config: config.Default { }, } // connect to X @@ -71,7 +73,11 @@ func (backend *Backend) Run () (err error) { // Stop gracefully closes the connection and stops the event loop. func (backend *Backend) Stop () { backend.assert() + toClose := []*Window { } for _, window := range backend.windows { + toClose = append(toClose, window) + } + for _, window := range toClose { window.Close() } xevent.Quit(backend.connection) diff --git a/config/parse.go b/config/parse.go index 258d48f..428848a 100644 --- a/config/parse.go +++ b/config/parse.go @@ -2,8 +2,8 @@ package config import "io" -// Parse parses a configuration file and returns it as a Config. -func Parse (source io.Reader) (config Config) { +// Parse parses one or more configuration files and returns them as a Config. +func Parse (sources ...io.Reader) (config Config) { // TODO return Default { } } diff --git a/theme/parse.go b/theme/parse.go index db96c86..153eb89 100644 --- a/theme/parse.go +++ b/theme/parse.go @@ -2,8 +2,8 @@ package theme import "io" -// Parse parses a theme file and returns it as a Theme. -func Parse (io.Reader) (Theme) { +// Parse parses one or more theme files and returns them as a Theme. +func Parse (sources ...io.Reader) (Theme) { // TODO return Default { } } diff --git a/tomo.go b/tomo.go index ac0a87e..8f38825 100644 --- a/tomo.go +++ b/tomo.go @@ -1,5 +1,9 @@ package tomo +import "os" +import "io" +import "path/filepath" +import "git.tebibyte.media/sashakoshka/tomo/dirs" import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" @@ -12,6 +16,9 @@ var backend Backend // the backend experiences a fatal error. func Run (callback func ()) (err error) { backend, err = instantiateBackend() + config := parseConfig() + backend.SetConfig(config) + backend.SetTheme(parseTheme(config.ThemePath())) if callback != nil { callback() } err = backend.Run() backend = nil @@ -63,6 +70,60 @@ func SetConfig (config config.Config) { backend.SetConfig(config) } +func parseConfig () (config.Config) { + return parseMany [config.Config] ( + dirs.ConfigDirs("tomo/tomo.conf"), + config.Parse, + config.Default { }) +} + +func parseTheme (path string) (theme.Theme) { + if path == "" { return theme.Default { } } + path = filepath.Join(path, "tomo") + + // find all tomo pattern graph files in the directory + directory, err := os.Open(path) + if err != nil { return theme.Default { } } + names, _ := directory.Readdirnames(0) + paths := []string { } + for _, name := range names { + if filepath.Ext(name) == ".tpg" { + paths = append(paths, filepath.Join(path, name)) + } + } + + // parse them + return parseMany [theme.Theme] ( + paths, + theme.Parse, + theme.Default { }) +} + +func parseMany [OBJECT any] ( + paths []string, + parser func (...io.Reader) OBJECT, + fallback OBJECT, +) ( + object OBJECT, +) { + // convert all paths into readers + sources := []io.Reader { } + for _, path := range paths { + file, err := os.Open(path) + if err != nil { continue } + sources = append(sources, file) + defer file.Close() + } + + if sources == nil { + // if there are no readers, return the fallback object + return fallback + } else { + // if there are readers, parse them + return parser(sources...) + } +} + func assertBackend () { if backend == nil { panic("no backend is running") } } From 8d90dbdc92aaad061884921104f64d7fc521d0a1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 18:07:10 -0500 Subject: [PATCH 08/15] Element core now deals with Config and Theme objects --- elements/core/core.go | 44 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/elements/core/core.go b/elements/core/core.go index bb9357e..827b87c 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -2,6 +2,8 @@ package core import "image" import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" // Core is a struct that implements some core functionality common to most @@ -14,13 +16,25 @@ type Core struct { minimumHeight int } + config config.Config + theme theme.Theme + drawSizeChange func () + onConfigChange func () + onThemeChange func () onMinimumSizeChange func () onDamage func (region canvas.Canvas) } // NewCore creates a new element core and its corresponding control. -func NewCore (drawSizeChange func ()) (core *Core, control CoreControl) { +func NewCore ( + drawSizeChange func (), + onConfigChange func (), + onThemeChange func (), +) ( + core *Core, + control CoreControl, +) { core = &Core { drawSizeChange: drawSizeChange } control = CoreControl { core: core } return @@ -82,6 +96,24 @@ func (core *Core) OnMinimumSizeChange (callback func ()) { core.onMinimumSizeChange = callback } +// SetConfig fulfills the elements.Configurable interface. This should not need +// to be overridden. +func (core *Core) SetConfig (config config.Config) { + core.config = config + if core.onConfigChange != nil { + core.onConfigChange() + } +} + +// SetTheme fulfills the elements.Themeable interface. This should not need +// to be overridden. +func (core *Core) SetTheme (theme theme.Theme) { + core.theme = theme + if core.onThemeChange != nil { + core.onThemeChange() + } +} + // 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 @@ -147,3 +179,13 @@ func (control CoreControl) ConstrainSize ( } return } + +// Config returns the current configuration. +func (control CoreControl) Config () (config.Config) { + return control.core.config +} + +// Theme returns the current theme. +func (control CoreControl) Theme () (theme.Theme) { + return control.core.theme +} From f8ebe5b1e4c1ae9542bb3040ec6ceba55b89f91e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 18:28:01 -0500 Subject: [PATCH 09/15] Core provides convenience methods for easy theme access --- elements/core/core.go | 47 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/elements/core/core.go b/elements/core/core.go index 827b87c..85999e0 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -2,9 +2,11 @@ package core import "image" import "image/color" +import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" // Core is a struct that implements some core functionality common to most // widgets. It is meant to be embedded directly into a struct. @@ -18,6 +20,7 @@ type Core struct { config config.Config theme theme.Theme + c theme.Case drawSizeChange func () onConfigChange func () @@ -31,11 +34,17 @@ func NewCore ( drawSizeChange func (), onConfigChange func (), onThemeChange func (), + c theme.Case, ) ( core *Core, control CoreControl, ) { - core = &Core { drawSizeChange: drawSizeChange } + core = &Core { + drawSizeChange: drawSizeChange, + onConfigChange: onConfigChange, + onThemeChange: onThemeChange, + c: c, + } control = CoreControl { core: core } return } @@ -189,3 +198,39 @@ func (control CoreControl) Config () (config.Config) { func (control CoreControl) Theme () (theme.Theme) { return control.core.theme } + +// FontFace is like Theme.FontFace, but it automatically applies the correct +// case. +func (control CoreControl) FontFace ( + style theme.FontStyle, + size theme.FontSize, +) ( + face font.Face, +) { + return control.core.theme.FontFace(style, size, control.core.c) +} + +// Icon is like Theme.Icon, but it automatically applies the correct case. +func (control CoreControl) Icon (name string) (artist.Pattern) { + return control.core.theme.Icon(name, control.core.c) +} + +// Pattern is like Theme.Pattern, but it automatically applies the correct case. +func (control CoreControl) Pattern ( + id theme.Pattern, + state theme.PatternState, +) ( + pattern artist.Pattern, +) { + return control.core.theme.Pattern(id, control.core.c, state) +} + +// Inset is like Theme.Inset, but it automatically applies the correct case. +func (control CoreControl) Inset (id theme.Pattern) (inset theme.Inset) { + return control.core.theme.Inset(id, control.core.c) +} + +// Sink is like Theme.Sink, but it automatically applies the correct case. +func (control CoreControl) Sink (id theme.Pattern) (offset image.Point) { + return control.core.theme.Sink(id, control.core.c) +} From 0bdbaa39cac15fa3a70a4d06706e7e60ec5b66ea Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Feb 2023 18:32:22 -0500 Subject: [PATCH 10/15] Artist and test examples work --- elements/testing/artist.go | 6 +++++- elements/testing/mouse.go | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/elements/testing/artist.go b/elements/testing/artist.go index f9a2290..968143e 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -4,6 +4,7 @@ import "fmt" import "time" import "image" import "image/color" +import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/defaultfont" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -19,13 +20,16 @@ type Artist struct { // NewArtist creates a new artist test element. func NewArtist () (element *Artist) { element = &Artist { } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore ( + element.draw, nil, nil, theme.C("testing", "artist")) element.core.SetMinimumSize(480, 600) return } func (element *Artist) draw () { bounds := element.Bounds() + artist.FillRectangle(element, artist.NewUniform(hex(0)), bounds) + element.cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5 element.cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 8 diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index baf6044..d027284 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -20,15 +20,25 @@ type Mouse struct { // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { element = &Mouse { } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore ( + element.draw, + element.redo, + element.redo, + theme.C("testing", "mouse")) element.core.SetMinimumSize(32, 32) element.color = artist.NewUniform(color.Black) return } +func (element *Mouse) redo () { + if !element.core.HasImage() { return } + element.draw() + element.core.DamageAll() +} + func (element *Mouse) draw () { bounds := element.Bounds() - pattern, _ := theme.AccentPattern(theme.PatternState { }) + pattern := element.core.Pattern(theme.PatternAccent, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) artist.StrokeRectangle ( element, From 3998d842b108faa169332554ea2be2701acc7eed Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 7 Feb 2023 11:27:59 -0500 Subject: [PATCH 11/15] Half-done migration of basic elements --- elements/basic/button.go | 59 ++++++++++++++++------------------- elements/basic/checkbox.go | 56 ++++++++++++++++----------------- elements/basic/container.go | 38 ++++++++++++++++------ elements/basic/label.go | 41 +++++++++++++++++------- elements/basic/list.go | 58 +++++++++++++++++++++++++--------- elements/basic/listentry.go | 32 ++++++++++++------- elements/basic/progressbar.go | 26 ++++++++++++--- elements/core/core.go | 32 +++++++++---------- 8 files changed, 214 insertions(+), 128 deletions(-) diff --git a/elements/basic/button.go b/elements/basic/button.go index c768d23..d2c5899 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -6,8 +6,6 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var buttonCase = theme.C("basic", "button") - // Button is a clickable button. type Button struct { *core.Core @@ -25,19 +23,27 @@ type Button struct { // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { element = &Button { } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore ( + element.draw, + element.redo, + element.redo, + theme.C("basic", "button")) element.FocusableCore, - element.focusableControl = core.NewFocusableCore (func () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - }) - element.drawer.SetFace(theme.FontFaceRegular()) + element.focusableControl = core.NewFocusableCore(element.redo) element.SetText(text) return } +func (element *Button) redo () { + element.drawer.SetFace ( + element.core.FontFace(theme.FontStyleRegular, + theme.FontSizeNormal)) + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + func (element *Button) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } if !element.Focused() { element.Focus() } @@ -83,10 +89,7 @@ func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter && element.pressed { element.pressed = false - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.redo() if !element.Enabled() { return } if element.onClick != nil { element.onClick() @@ -111,33 +114,28 @@ func (element *Button) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) textBounds := element.drawer.LayoutBounds() - _, inset := theme.ButtonPattern(theme.PatternState { Case: buttonCase }) - minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding()) + minimumSize := textBounds.Inset(-element.core.Config().Padding()) element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.redo() } func (element *Button) draw () { bounds := element.Bounds() - pattern, inset := theme.ButtonPattern(theme.PatternState { - Case: buttonCase, + state := theme.PatternState { Disabled: !element.Enabled(), - Focused: element.Focused(), + Focused: element.Focused(), Pressed: element.pressed, - }) + } + + pattern := element.core.Pattern(theme.PatternButton, state) artist.FillRectangle(element, pattern, bounds) - - innerBounds := inset.Apply(bounds) textBounds := element.drawer.LayoutBounds() offset := image.Point { - X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2, - Y: innerBounds.Min.Y + (innerBounds.Dy() - textBounds.Dy()) / 2, + X: bounds.Min.X + (bounds.Dx() - textBounds.Dx()) / 2, + Y: bounds.Min.Y + (bounds.Dy() - textBounds.Dy()) / 2, } // account for the fact that the bounding rectangle will be shifted over @@ -145,9 +143,6 @@ func (element *Button) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground, _ := theme.ForegroundPattern (theme.PatternState { - Case: buttonCase, - Disabled: !element.Enabled(), - }) + foreground := element.core.Pattern(theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index ba0a869..a7ba73e 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -6,8 +6,6 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var checkboxCase = theme.C("basic", "checkbox") - // Checkbox is a toggle-able checkbox with a label. type Checkbox struct { *core.Core @@ -26,19 +24,27 @@ 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.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore ( + element.draw, + element.redo, + element.redo, + theme.C("basic", "checkbox")) element.FocusableCore, - element.focusableControl = core.NewFocusableCore (func () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - }) - element.drawer.SetFace(theme.FontFaceRegular()) + element.focusableControl = core.NewFocusableCore(element.redo) element.SetText(text) return } +func (element *Checkbox) redo () { + element.drawer.SetFace ( + element.core.FontFace(theme.FontStyleRegular, + theme.FontSizeNormal)) + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() @@ -122,7 +128,7 @@ func (element *Checkbox) SetText (text string) { element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) } else { element.core.SetMinimumSize ( - textBounds.Dy() + theme.Padding() + textBounds.Dx(), + textBounds.Dy() + element.core.Config().Padding() + textBounds.Dx(), textBounds.Dy()) } @@ -136,35 +142,27 @@ func (element *Checkbox) draw () { bounds := element.Bounds() boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) - backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { - Case: checkboxCase, - }) - artist.FillRectangle(element, backgroundPattern, bounds) - - pattern, inset := theme.ButtonPattern(theme.PatternState { - Case: checkboxCase, + state := theme.PatternState { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, - }) + On: element.checked, + } + + backgroundPattern := element.core.Pattern(theme.PatternBackground, state) + artist.FillRectangle(element, backgroundPattern, bounds) + + pattern := element.core.Pattern (theme.PatternButton, state) artist.FillRectangle(element, pattern, boxBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { - X: bounds.Dy() + theme.Padding(), + X: bounds.Dy() + element.core.Config().Padding(), }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground, _ := theme.ForegroundPattern (theme.PatternState { - Case: checkboxCase, - Disabled: !element.Enabled(), - }) + foreground := element.core.Pattern(theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) - - if element.checked { - checkBounds := inset.Apply(boxBounds).Inset(2) - artist.FillRectangle(element, foreground, checkBounds) - } } diff --git a/elements/basic/container.go b/elements/basic/container.go index 328e18a..2719208 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -9,8 +9,6 @@ import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var containerCase = theme.C("basic", "container") - // Container is an element capable of containg other elements, and arranging // them in a layout. type Container struct { @@ -33,7 +31,11 @@ type Container struct { // NewContainer creates a new container. func NewContainer (layout layouts.Layout) (element *Container) { element = &Container { } - element.Core, element.core = core.NewCore(element.redoAll) + element.Core, element.core = core.NewCore ( + element.redoAll, + element.handleConfigChange, + element.handleThemeChange, + theme.C("basic", "container")) element.SetLayout(layout) return } @@ -205,9 +207,7 @@ func (element *Container) redoAll () { // draw a background bounds := element.Bounds() - pattern, _ := theme.BackgroundPattern (theme.PatternState { - Case: containerCase, - }) + pattern := element.core.Pattern (theme.PatternBackground, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) // cut our canvas up and give peices to child elements @@ -216,6 +216,24 @@ func (element *Container) redoAll () { } } +func (element *Container) handleConfigChange () { + for _, child := range element.children { + if child0, ok := child.Element.(elements.Configurable); ok { + child0.SetConfig(element.core.Config()) + } + } + element.redoAll() +} + +func (element *Container) handleThemeChange () { + for _, child := range element.children { + if child0, ok := child.Element.(elements.Themeable); ok { + child0.SetTheme(element.core.Theme()) + } + } + element.redoAll() +} + func (element *Container) HandleMouseDown (x, y int, button input.Button) { child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget) if !handlesMouse { return } @@ -266,7 +284,7 @@ func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) func (element *Container) FlexibleHeightFor (width int) (height int) { return element.layout.FlexibleHeightFor ( element.children, - theme.Margin(), width) + element.core.Config().Margin(), width) } func (element *Container) OnFlexibleHeightChange (callback func ()) { @@ -469,15 +487,15 @@ func (element *Container) childFocusRequestCallback ( func (element *Container) updateMinimumSize () { width, height := element.layout.MinimumSize ( - element.children, theme.Margin()) + element.children, element.core.Config().Margin()) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, theme.Margin(), width) + element.children, element.core.Config().Margin(), width) } element.core.SetMinimumSize(width, height) } func (element *Container) recalculate () { element.layout.Arrange ( - element.children, theme.Margin(), element.Bounds()) + element.children, element.core.Config().Margin(), element.Bounds()) } diff --git a/elements/basic/label.go b/elements/basic/label.go index b105580..2803798 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -4,8 +4,6 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var labelCase = theme.C("basic", "label") - // Label is a simple text box. type Label struct { *core.Core @@ -22,14 +20,35 @@ type Label struct { // wrapped. func NewLabel (text string, wrap bool) (element *Label) { element = &Label { } - element.Core, element.core = core.NewCore(element.handleResize) - face := theme.FontFaceRegular() + element.Core, element.core = core.NewCore ( + element.handleResize, + element.redo, + element.redo, + theme.C("basic", "label")) + face := element.core.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal) element.drawer.SetFace(face) element.SetWrap(wrap) element.SetText(text) return } +func (element *Label) redo () { + face := element.core.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal) + element.drawer.SetFace(face) + 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 { @@ -93,7 +112,7 @@ func (element *Label) SetWrap (wrap bool) { func (element *Label) updateMinimumSize () { if element.wrap { em := element.drawer.Em().Round() - if em < 1 { em = theme.Padding() } + if em < 1 { em = element.core.Config().Padding() } element.core.SetMinimumSize ( em, element.drawer.LineHeight().Round()) if element.onFlexibleHeightChange != nil { @@ -108,15 +127,15 @@ func (element *Label) updateMinimumSize () { func (element *Label) draw () { bounds := element.Bounds() - pattern, _ := theme.BackgroundPattern(theme.PatternState { - Case: labelCase, - }) + pattern := element.core.Pattern ( + theme.PatternBackground, + theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) textBounds := element.drawer.LayoutBounds() - foreground, _ := theme.ForegroundPattern (theme.PatternState { - Case: labelCase, - }) + foreground := element.core.Pattern ( + theme.PatternForeground, + theme.PatternState { }) element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min)) } diff --git a/elements/basic/list.go b/elements/basic/list.go index e8a60b9..d76ff0b 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -8,8 +8,6 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var listCase = theme.C("basic", "list") - // List is an element that contains several objects that a user can select. type List struct { *core.Core @@ -34,7 +32,11 @@ type List struct { // NewList creates a new list element with the specified entries. func NewList (entries ...ListEntry) (element *List) { element = &List { selectedEntry: -1 } - element.Core, element.core = core.NewCore(element.handleResize) + element.Core, element.core = core.NewCore ( + element.handleResize, + element.redo, + element.redo, + theme.C("basic", "list")) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { if element.core.HasImage () { @@ -63,6 +65,36 @@ func (element *List) handleResize () { } } +func (element *List) handleConfigChange () { + for index, entry := range element.entries { + entry.SetConfig(element.core.Config()) + element.entries[index] = entry + } + element.redo() +} + +func (element *List) handleThemeChange () { + for index, entry := range element.entries { + entry.SetConfig(element.core.Config()) + element.entries[index] = entry + } + 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() + } + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + // 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 @@ -164,9 +196,7 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - _, inset := theme.ListPattern(theme.PatternState { - Case: listCase, - }) + inset := element.core.Inset(theme.PatternSunken) return element.Bounds().Dy() - inset[0] - inset[2] } @@ -198,6 +228,8 @@ func (element *List) CountEntries () (count int) { func (element *List) Append (entry ListEntry) { // append entry.Collapse(element.forcedMinimumWidth) + entry.SetTheme(element.core.Theme()) + entry.SetConfig(element.core.Config()) element.entries = append(element.entries, entry) // recalculate, redraw, notify @@ -290,7 +322,7 @@ func (element *List) Replace (index int, entry ListEntry) { } func (element *List) selectUnderMouse (x, y int) (updated bool) { - _, inset := theme.ListPattern(theme.PatternState { }) + inset := element.core.Inset(theme.PatternSunken) bounds := inset.Apply(element.Bounds()) mousePoint := image.Pt(x, y) dot := image.Pt ( @@ -332,9 +364,7 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { } func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - _, inset := theme.ListPattern(theme.PatternState { - Case: listCase, - }) + inset := element.core.Inset(theme.PatternSunken) entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1]) return entry } @@ -361,9 +391,7 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - _, inset := theme.ListPattern(theme.PatternState { - Case: listCase, - }) + inset := element.core.Inset(theme.PatternSunken) minimumHeight += inset[0] + inset[2] element.core.SetMinimumSize(minimumWidth, minimumHeight) @@ -372,8 +400,8 @@ func (element *List) updateMinimumSize () { func (element *List) draw () { bounds := element.Bounds() - pattern, inset := theme.ListPattern(theme.PatternState { - Case: listCase, + inset := element.core.Inset(theme.PatternSunken) + pattern := element.core.Pattern (theme.PatternSunken, theme.PatternState { Disabled: !element.Enabled(), Focused: element.Focused(), }) diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index fb45ba2..60b9618 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -2,6 +2,7 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" @@ -15,6 +16,8 @@ type ListEntry struct { text string forcedMinimumWidth int onSelect func () + theme theme.Theme + config config.Config } func NewListEntry (text string, onSelect func ()) (entry ListEntry) { @@ -23,7 +26,6 @@ func NewListEntry (text string, onSelect func ()) (entry ListEntry) { onSelect: onSelect, } entry.drawer.SetText([]rune(text)) - entry.drawer.SetFace(theme.FontFaceRegular()) entry.updateBounds() return } @@ -34,6 +36,19 @@ func (entry *ListEntry) Collapse (width int) { entry.updateBounds() } +func (entry *ListEntry) SetTheme (new theme.Theme) { + entry.theme = new + entry.drawer.SetFace (entry.theme.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal, + listEntryCase)) + entry.updateBounds() +} + +func (entry *ListEntry) SetConfig (config config.Config) { + entry.config = config +} + func (entry *ListEntry) updateBounds () { entry.bounds = image.Rectangle { } entry.bounds.Max.Y = entry.drawer.LineHeight().Round() @@ -43,8 +58,7 @@ func (entry *ListEntry) updateBounds () { entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx() } - _, inset := theme.ItemPattern(theme.PatternState { - }) + inset := entry.theme.Inset(theme.PatternRaised, listEntryCase) entry.bounds.Max.Y += inset[0] + inset[2] entry.textPoint = @@ -60,20 +74,16 @@ func (entry *ListEntry) Draw ( ) ( updatedRegion image.Rectangle, ) { - pattern, _ := theme.ItemPattern(theme.PatternState { - Case: listEntryCase, + state := theme.PatternState { Focused: focused, On: on, - }) + } + pattern := entry.theme.Pattern (theme.PatternRaised, listEntryCase, state) artist.FillRectangle ( destination, pattern, entry.Bounds().Add(offset)) - foreground, _ := theme.ForegroundPattern (theme.PatternState { - Case: listEntryCase, - Focused: focused, - On: on, - }) + foreground := entry.theme.Pattern (theme.PatternForeground, listEntryCase, state) return entry.drawer.Draw ( destination, foreground, diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index e97665c..3545e60 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -16,11 +16,24 @@ type ProgressBar struct { // level. func NewProgressBar (progress float64) (element *ProgressBar) { element = &ProgressBar { progress: progress } - element.Core, element.core = core.NewCore(element.draw) - element.core.SetMinimumSize(theme.Padding() * 2, theme.Padding() * 2) + element.Core, element.core = core.NewCore ( + element.draw, + element.redo, + element.redo, + theme.C("basic", "progressBar")) return } +func (element *ProgressBar) redo () { + element.core.SetMinimumSize ( + element.core.Config().Padding() * 2, + element.core.Config().Padding() * 2) + if element.core.HasImage() { + element.draw() + element.core.DamageAll() + } +} + // SetProgress sets the progress level of the bar. func (element *ProgressBar) SetProgress (progress float64) { if progress == element.progress { return } @@ -34,13 +47,18 @@ func (element *ProgressBar) SetProgress (progress float64) { func (element *ProgressBar) draw () { bounds := element.Bounds() - pattern, inset := theme.SunkenPattern(theme.PatternState { }) + pattern := element.core.Pattern ( + theme.PatternSunken, + theme.PatternState { }) + inset := element.core.Inset(theme.PatternSunken) artist.FillRectangle(element, pattern, bounds) bounds = inset.Apply(bounds) meterBounds := image.Rect ( bounds.Min.X, bounds.Min.Y, bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) - accent, _ := theme.AccentPattern(theme.PatternState { }) + accent := element.core.Pattern ( + theme.PatternSunken, + theme.PatternState { }) artist.FillRectangle(element, accent, meterBounds) } diff --git a/elements/core/core.go b/elements/core/core.go index 85999e0..3635c08 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -22,27 +22,27 @@ type Core struct { theme theme.Theme c theme.Case - drawSizeChange func () - onConfigChange func () - onThemeChange func () + handleSizeChange func () + handleConfigChange func () + handleThemeChange func () onMinimumSizeChange func () onDamage func (region canvas.Canvas) } // NewCore creates a new element core and its corresponding control. func NewCore ( - drawSizeChange func (), - onConfigChange func (), - onThemeChange func (), + handleSizeChange func (), + handleConfigChange func (), + handleThemeChange func (), c theme.Case, ) ( core *Core, control CoreControl, ) { - core = &Core { - drawSizeChange: drawSizeChange, - onConfigChange: onConfigChange, - onThemeChange: onThemeChange, + core = &Core { + handleSizeChange: handleSizeChange, + handleConfigChange: handleConfigChange, + handleThemeChange: handleThemeChange, c: c, } control = CoreControl { core: core } @@ -88,8 +88,8 @@ func (core *Core) MinimumSize () (width, height int) { // overridden. func (core *Core) DrawTo (canvas canvas.Canvas) { core.canvas = canvas - if core.drawSizeChange != nil { - core.drawSizeChange() + if core.handleSizeChange != nil { + core.handleSizeChange() } } @@ -109,8 +109,8 @@ func (core *Core) OnMinimumSizeChange (callback func ()) { // to be overridden. func (core *Core) SetConfig (config config.Config) { core.config = config - if core.onConfigChange != nil { - core.onConfigChange() + if core.handleConfigChange != nil { + core.handleConfigChange() } } @@ -118,8 +118,8 @@ func (core *Core) SetConfig (config config.Config) { // to be overridden. func (core *Core) SetTheme (theme theme.Theme) { core.theme = theme - if core.onThemeChange != nil { - core.onThemeChange() + if core.handleThemeChange != nil { + core.handleThemeChange() } } From 6936353516e412b33669df023a6efb3aa7b5c8d8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 8 Feb 2023 00:22:40 -0500 Subject: [PATCH 12/15] asuhfdjkshlk --- elements/basic/button.go | 79 +++++++++++--------- elements/basic/checkbox.go | 72 ++++++++++++------- elements/basic/container.go | 47 +++++++----- elements/basic/label.go | 58 ++++++++++----- elements/basic/list.go | 46 +++++++----- elements/basic/listentry.go | 16 +++-- elements/basic/progressbar.go | 61 +++++++++++----- elements/basic/scrollcontainer.go | 95 ++++++++++++------------ elements/basic/spacer.go | 44 +++++++++--- elements/basic/switch.go | 104 +++++++++++++++------------ elements/basic/textbox.go | 115 ++++++++++++++++-------------- elements/core/core.go | 90 ++--------------------- 12 files changed, 448 insertions(+), 379 deletions(-) diff --git a/elements/basic/button.go b/elements/basic/button.go index d2c5899..28e8566 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -3,6 +3,7 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -17,51 +18,37 @@ type Button struct { pressed bool text string + config config.Config + theme theme.Theme + c theme.Case + onClick func () } // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { - element = &Button { } - element.Core, element.core = core.NewCore ( - element.draw, - element.redo, - element.redo, - theme.C("basic", "button")) + element = &Button { + c: theme.C("basic", "button"), + } + element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) element.SetText(text) return } -func (element *Button) redo () { - element.drawer.SetFace ( - element.core.FontFace(theme.FontStyleRegular, - theme.FontSizeNormal)) - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - 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 - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.redo() } func (element *Button) HandleMouseUp (x, y int, button input.Button) { if button != input.ButtonLeft { return } element.pressed = false - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.redo() within := image.Point { x, y }. In(element.Bounds()) @@ -79,10 +66,7 @@ func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) if !element.Enabled() { return } if key == input.KeyEnter { element.pressed = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.redo() } } @@ -113,12 +97,41 @@ func (element *Button) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) - textBounds := element.drawer.LayoutBounds() - minimumSize := textBounds.Inset(-element.core.Config().Padding()) - element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) + element.updateMinimumSize() element.redo() } +// SetTheme sets the element's theme. +func (element *Button) SetTheme (new theme.Theme) { + element.theme = new + element.drawer.SetFace (element.theme.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal, + element.c)) + element.updateMinimumSize() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Button) SetConfig (new config.Config) { + element.config = new + element.updateMinimumSize() + element.redo() +} + +func (element *Button) updateMinimumSize () { + textBounds := element.drawer.LayoutBounds() + minimumSize := textBounds.Inset(-element.config.Padding()) + element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) +} + +func (element *Button) redo () { + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + func (element *Button) draw () { bounds := element.Bounds() @@ -128,7 +141,7 @@ func (element *Button) draw () { Pressed: element.pressed, } - pattern := element.core.Pattern(theme.PatternButton, state) + pattern := element.theme.Pattern(theme.PatternButton, element.c, state) artist.FillRectangle(element, pattern, bounds) @@ -143,6 +156,6 @@ func (element *Button) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.core.Pattern(theme.PatternForeground, state) + foreground := element.theme.Pattern(theme.PatternForeground, element.c, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index a7ba73e..d6a7680 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -3,6 +3,7 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -18,33 +19,26 @@ type Checkbox struct { checked bool text string + config config.Config + theme theme.Theme + c theme.Case + onToggle func () } // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { - element = &Checkbox { checked: checked } - element.Core, element.core = core.NewCore ( - element.draw, - element.redo, - element.redo, - theme.C("basic", "checkbox")) + element = &Checkbox { + checked: checked, + c: theme.C("basic", "checkbox"), + } + element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) element.SetText(text) return } -func (element *Checkbox) redo () { - element.drawer.SetFace ( - element.core.FontFace(theme.FontStyleRegular, - theme.FontSizeNormal)) - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() @@ -122,16 +116,44 @@ func (element *Checkbox) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) - textBounds := element.drawer.LayoutBounds() + element.updateMinimumSize() - if text == "" { + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + +// SetTheme sets the element's theme. +func (element *Checkbox) SetTheme (new theme.Theme) { + element.theme = new + element.drawer.SetFace (element.theme.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal, + element.c)) + element.updateMinimumSize() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Checkbox) SetConfig (new config.Config) { + element.config = new + element.updateMinimumSize() + element.redo() +} + +func (element *Checkbox) updateMinimumSize () { + textBounds := element.drawer.LayoutBounds() + if element.text == "" { element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) } else { element.core.SetMinimumSize ( - textBounds.Dy() + element.core.Config().Padding() + textBounds.Dx(), + textBounds.Dy() + element.config.Padding() + textBounds.Dx(), textBounds.Dy()) } - +} + +func (element *Checkbox) redo () { if element.core.HasImage () { element.draw() element.core.DamageAll() @@ -149,20 +171,22 @@ func (element *Checkbox) draw () { On: element.checked, } - backgroundPattern := element.core.Pattern(theme.PatternBackground, state) + backgroundPattern := element.theme.Pattern ( + theme.PatternBackground, element.c, state) artist.FillRectangle(element, backgroundPattern, bounds) - pattern := element.core.Pattern (theme.PatternButton, state) + pattern := element.theme.Pattern(theme.PatternButton, element.c, state) artist.FillRectangle(element, pattern, boxBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { - X: bounds.Dy() + element.core.Config().Padding(), + X: bounds.Dy() + element.config.Padding(), }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.core.Pattern(theme.PatternForeground, state) + foreground := element.theme.Pattern ( + theme.PatternForeground, element.c, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/container.go b/elements/basic/container.go index 2719208..75bf24c 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -3,6 +3,7 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" @@ -23,6 +24,10 @@ type Container struct { focusable bool flexible bool + config config.Config + theme theme.Theme + c theme.Case + onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) onFlexibleHeightChange func () @@ -30,12 +35,10 @@ type Container struct { // NewContainer creates a new container. func NewContainer (layout layouts.Layout) (element *Container) { - element = &Container { } - element.Core, element.core = core.NewCore ( - element.redoAll, - element.handleConfigChange, - element.handleThemeChange, - theme.C("basic", "container")) + element = &Container { + c: theme.C("basic", "container"), + } + element.Core, element.core = core.NewCore(element.redoAll) element.SetLayout(layout) return } @@ -207,7 +210,10 @@ func (element *Container) redoAll () { // draw a background bounds := element.Bounds() - pattern := element.core.Pattern (theme.PatternBackground, theme.PatternState { }) + pattern := element.theme.Pattern ( + theme.PatternBackground, + element.c, + theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) // cut our canvas up and give peices to child elements @@ -216,21 +222,28 @@ func (element *Container) redoAll () { } } -func (element *Container) handleConfigChange () { + +// SetTheme sets the element's theme. +func (element *Container) SetTheme (new theme.Theme) { + element.theme = new for _, child := range element.children { - if child0, ok := child.Element.(elements.Configurable); ok { - child0.SetConfig(element.core.Config()) + if child0, ok := child.Element.(elements.Themeable); ok { + child0.SetTheme(element.theme) } } + element.updateMinimumSize() element.redoAll() } -func (element *Container) handleThemeChange () { +// SetConfig sets the element's configuration. +func (element *Container) SetConfig (new config.Config) { + element.config = new for _, child := range element.children { - if child0, ok := child.Element.(elements.Themeable); ok { - child0.SetTheme(element.core.Theme()) + if child0, ok := child.Element.(elements.Configurable); ok { + child0.SetConfig(element.config) } } + element.updateMinimumSize() element.redoAll() } @@ -284,7 +297,7 @@ func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) func (element *Container) FlexibleHeightFor (width int) (height int) { return element.layout.FlexibleHeightFor ( element.children, - element.core.Config().Margin(), width) + element.config.Margin(), width) } func (element *Container) OnFlexibleHeightChange (callback func ()) { @@ -487,15 +500,15 @@ func (element *Container) childFocusRequestCallback ( func (element *Container) updateMinimumSize () { width, height := element.layout.MinimumSize ( - element.children, element.core.Config().Margin()) + element.children, element.config.Margin()) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, element.core.Config().Margin(), width) + element.children, element.config.Margin(), width) } element.core.SetMinimumSize(width, height) } func (element *Container) recalculate () { element.layout.Arrange ( - element.children, element.core.Config().Margin(), element.Bounds()) + element.children, element.config.Margin(), element.Bounds()) } diff --git a/elements/basic/label.go b/elements/basic/label.go index 2803798..1448309 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -1,6 +1,7 @@ package basicElements import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -13,31 +14,28 @@ type Label struct { text string drawer artist.TextDrawer + config config.Config + theme theme.Theme + c theme.Case + onFlexibleHeightChange func () } // 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.Core, element.core = core.NewCore ( - element.handleResize, - element.redo, - element.redo, - theme.C("basic", "label")) - face := element.core.FontFace ( - theme.FontStyleRegular, - theme.FontSizeNormal) - element.drawer.SetFace(face) + element = &Label { c: theme.C("basic", "label") } + element.Core, element.core = core.NewCore(element.handleResize) element.SetWrap(wrap) element.SetText(text) return } func (element *Label) redo () { - face := element.core.FontFace ( + face := element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal) + theme.FontSizeNormal, + element.c) element.drawer.SetFace(face) element.updateMinimumSize() bounds := element.Bounds() @@ -109,10 +107,36 @@ func (element *Label) SetWrap (wrap bool) { } } +// SetTheme sets the element's theme. +func (element *Label) SetTheme (new theme.Theme) { + element.theme = new + element.drawer.SetFace (element.theme.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal, + element.c)) + element.updateMinimumSize() + + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + +// SetConfig sets the element's configuration. +func (element *Label) SetConfig (new config.Config) { + element.config = new + element.updateMinimumSize() + + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + func (element *Label) updateMinimumSize () { if element.wrap { em := element.drawer.Em().Round() - if em < 1 { em = element.core.Config().Padding() } + if em < 1 { em = element.config.Padding() } element.core.SetMinimumSize ( em, element.drawer.LineHeight().Round()) if element.onFlexibleHeightChange != nil { @@ -127,15 +151,17 @@ func (element *Label) updateMinimumSize () { func (element *Label) draw () { bounds := element.Bounds() - pattern := element.core.Pattern ( + pattern := element.theme.Pattern ( theme.PatternBackground, + element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) textBounds := element.drawer.LayoutBounds() - foreground := element.core.Pattern ( + foreground := element.theme.Pattern ( theme.PatternForeground, + element.c, theme.PatternState { }) - element.drawer.Draw (element, foreground, bounds.Min.Sub(textBounds.Min)) + element.drawer.Draw(element, foreground, bounds.Min.Sub(textBounds.Min)) } diff --git a/elements/basic/list.go b/elements/basic/list.go index d76ff0b..6da55b3 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -4,6 +4,7 @@ import "fmt" import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -25,18 +26,21 @@ type List struct { scroll int entries []ListEntry + config config.Config + theme theme.Theme + c theme.Case + onScrollBoundsChange func () onNoEntrySelected func () } // NewList creates a new list element with the specified entries. func NewList (entries ...ListEntry) (element *List) { - element = &List { selectedEntry: -1 } - element.Core, element.core = core.NewCore ( - element.handleResize, - element.redo, - element.redo, - theme.C("basic", "list")) + element = &List { + selectedEntry: -1, + c: theme.C("basic", "list"), + } + element.Core, element.core = core.NewCore(element.handleResize) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { if element.core.HasImage () { @@ -65,19 +69,25 @@ func (element *List) handleResize () { } } -func (element *List) handleConfigChange () { +// SetTheme sets the element's theme. +func (element *List) SetTheme (new theme.Theme) { + element.theme = new for index, entry := range element.entries { - entry.SetConfig(element.core.Config()) + entry.SetConfig(element.config) element.entries[index] = entry } + element.updateMinimumSize() element.redo() } -func (element *List) handleThemeChange () { +// SetConfig sets the element's configuration. +func (element *List) SetConfig (new config.Config) { + element.config = new for index, entry := range element.entries { - entry.SetConfig(element.core.Config()) + entry.SetConfig(element.config) element.entries[index] = entry } + element.updateMinimumSize() element.redo() } @@ -196,7 +206,7 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - inset := element.core.Inset(theme.PatternSunken) + inset := element.theme.Inset(theme.PatternSunken, element.c) return element.Bounds().Dy() - inset[0] - inset[2] } @@ -228,8 +238,8 @@ func (element *List) CountEntries () (count int) { func (element *List) Append (entry ListEntry) { // append entry.Collapse(element.forcedMinimumWidth) - entry.SetTheme(element.core.Theme()) - entry.SetConfig(element.core.Config()) + entry.SetTheme(element.theme) + entry.SetConfig(element.config) element.entries = append(element.entries, entry) // recalculate, redraw, notify @@ -322,7 +332,7 @@ func (element *List) Replace (index int, entry ListEntry) { } func (element *List) selectUnderMouse (x, y int) (updated bool) { - inset := element.core.Inset(theme.PatternSunken) + inset := element.theme.Inset(theme.PatternSunken, element.c) bounds := inset.Apply(element.Bounds()) mousePoint := image.Pt(x, y) dot := image.Pt ( @@ -364,7 +374,7 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { } func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - inset := element.core.Inset(theme.PatternSunken) + inset := element.theme.Inset(theme.PatternSunken, element.c) entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1]) return entry } @@ -391,7 +401,7 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - inset := element.core.Inset(theme.PatternSunken) + inset := element.theme.Inset(theme.PatternSunken, element.c) minimumHeight += inset[0] + inset[2] element.core.SetMinimumSize(minimumWidth, minimumHeight) @@ -400,8 +410,8 @@ func (element *List) updateMinimumSize () { func (element *List) draw () { bounds := element.Bounds() - inset := element.core.Inset(theme.PatternSunken) - pattern := element.core.Pattern (theme.PatternSunken, theme.PatternState { + inset := element.theme.Inset(theme.PatternSunken, element.c) + pattern := element.theme.Pattern (theme.PatternSunken, element.c, theme.PatternState { Disabled: !element.Enabled(), Focused: element.Focused(), }) diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 60b9618..79d91d8 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -6,8 +6,6 @@ import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -var listEntryCase = theme.C("basic", "listEntry") - // ListEntry is an item that can be added to a list. type ListEntry struct { drawer artist.TextDrawer @@ -15,15 +13,19 @@ type ListEntry struct { textPoint image.Point text string forcedMinimumWidth int - onSelect func () + theme theme.Theme config config.Config + c theme.Case + + onSelect func () } func NewListEntry (text string, onSelect func ()) (entry ListEntry) { entry = ListEntry { text: text, onSelect: onSelect, + c: theme.C("basic", "listEntry"), } entry.drawer.SetText([]rune(text)) entry.updateBounds() @@ -41,7 +43,7 @@ func (entry *ListEntry) SetTheme (new theme.Theme) { entry.drawer.SetFace (entry.theme.FontFace ( theme.FontStyleRegular, theme.FontSizeNormal, - listEntryCase)) + entry.c)) entry.updateBounds() } @@ -58,7 +60,7 @@ func (entry *ListEntry) updateBounds () { entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx() } - inset := entry.theme.Inset(theme.PatternRaised, listEntryCase) + inset := entry.theme.Inset(theme.PatternRaised, entry.c) entry.bounds.Max.Y += inset[0] + inset[2] entry.textPoint = @@ -78,12 +80,12 @@ func (entry *ListEntry) Draw ( Focused: focused, On: on, } - pattern := entry.theme.Pattern (theme.PatternRaised, listEntryCase, state) + pattern := entry.theme.Pattern (theme.PatternRaised, entry.c, state) artist.FillRectangle ( destination, pattern, entry.Bounds().Add(offset)) - foreground := entry.theme.Pattern (theme.PatternForeground, listEntryCase, state) + foreground := entry.theme.Pattern (theme.PatternForeground, entry.c, state) return entry.drawer.Draw ( destination, foreground, diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 3545e60..80806ca 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -2,6 +2,7 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -10,28 +11,21 @@ type ProgressBar struct { *core.Core core core.CoreControl progress float64 + + theme theme.Theme + config config.Config + c theme.Case } // NewProgressBar creates a new progress bar displaying the given progress // level. func NewProgressBar (progress float64) (element *ProgressBar) { - element = &ProgressBar { progress: progress } - element.Core, element.core = core.NewCore ( - element.draw, - element.redo, - element.redo, - theme.C("basic", "progressBar")) - return -} - -func (element *ProgressBar) redo () { - element.core.SetMinimumSize ( - element.core.Config().Padding() * 2, - element.core.Config().Padding() * 2) - if element.core.HasImage() { - element.draw() - element.core.DamageAll() + element = &ProgressBar { + progress: progress, + c: theme.C("basic", "progressBar"), } + element.Core, element.core = core.NewCore(element.draw) + return } // SetProgress sets the progress level of the bar. @@ -44,21 +38,50 @@ func (element *ProgressBar) SetProgress (progress float64) { } } +// SetTheme sets the element's theme. +func (element *ProgressBar) SetTheme (new theme.Theme) { + element.theme = new + element.updateMinimumSize() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *ProgressBar) SetConfig (new config.Config) { + element.config = new + element.updateMinimumSize() + element.redo() +} + +func (element (ProgressBar)) updateMinimumSize() { + element.core.SetMinimumSize ( + element.config.Padding() * 2, + element.config.Padding() * 2) +} + +func (element *ProgressBar) redo () { + if element.core.HasImage() { + element.draw() + element.core.DamageAll() + } +} + func (element *ProgressBar) draw () { bounds := element.Bounds() - pattern := element.core.Pattern ( + pattern := element.theme.Pattern ( theme.PatternSunken, + element.c, theme.PatternState { }) - inset := element.core.Inset(theme.PatternSunken) + inset := element.theme.Inset(theme.PatternSunken, element.c) artist.FillRectangle(element, pattern, bounds) bounds = inset.Apply(bounds) meterBounds := image.Rect ( bounds.Min.X, bounds.Min.Y, bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) - accent := element.core.Pattern ( + accent := element.theme.Pattern ( theme.PatternSunken, + element.c, theme.PatternState { }) artist.FillRectangle(element, accent, meterBounds) } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index a3ccbbb..553cc5e 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -3,15 +3,12 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var scrollContainerCase = theme.C("basic", "scrollContainer") -var scrollBarHorizontalCase = theme.C("basic", "scrollBarHorizontal") -var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical") - // ScrollContainer is a container that is capable of holding a scrollable // element. type ScrollContainer struct { @@ -23,6 +20,7 @@ type ScrollContainer struct { childWidth, childHeight int horizontal struct { + c theme.Case exists bool enabled bool dragging bool @@ -33,6 +31,7 @@ type ScrollContainer struct { } vertical struct { + c theme.Case exists bool enabled bool dragging bool @@ -41,6 +40,10 @@ type ScrollContainer struct { track image.Rectangle bar image.Rectangle } + + theme theme.Theme + config config.Config + c theme.Case onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) @@ -49,7 +52,10 @@ type ScrollContainer struct { // NewScrollContainer creates a new scroll container with the specified scroll // bars. func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { - element = &ScrollContainer { } + element = &ScrollContainer { c: theme.C("basic", "scrollContainer") } + element.horizontal.c = theme.C("basic", "scrollBarHorizontal") + element.vertical.c = theme.C("basic", "scrollBarVertical") + element.Core, element.core = core.NewCore(element.handleResize) element.updateMinimumSize() element.horizontal.exists = horizontal @@ -85,8 +91,6 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { element.childFocusMotionRequestCallback) } - // TODO: somehow inform the core that we do not in fact want to - // redraw the element. element.updateMinimumSize() element.horizontal.enabled, @@ -111,6 +115,7 @@ func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modi } func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) { + velocity := element.config.ScrollVelocity() point := image.Pt(x, y) if point.In(element.horizontal.bar) { element.horizontal.dragging = true @@ -123,9 +128,9 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) // FIXME: x backend and scroll container should pull these // values from the same place if x > element.horizontal.bar.Min.X { - element.scrollChildBy(16, 0) + element.scrollChildBy(velocity, 0) } else { - element.scrollChildBy(-16, 0) + element.scrollChildBy(-velocity, 0) } } else if point.In(element.vertical.bar) { @@ -137,9 +142,9 @@ func (element *ScrollContainer) HandleMouseDown (x, y int, button input.Button) } else if point.In(element.vertical.gutter) { if y > element.vertical.bar.Min.Y { - element.scrollChildBy(0, 16) + element.scrollChildBy(0, velocity) } else { - element.scrollChildBy(0, -16) + element.scrollChildBy(0, -velocity) } } else if child, ok := element.child.(elements.MouseTarget); ok { @@ -281,22 +286,19 @@ func (element *ScrollContainer) resizeChildToFit () { } func (element *ScrollContainer) recalculate () { - _, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState { - Case: scrollBarHorizontalCase, - }) - _, gutterInsetVertical := theme.GutterPattern(theme.PatternState { - Case: scrollBarHorizontalCase, - }) - horizontal := &element.horizontal vertical := &element.vertical + + gutterInsetHorizontal := element.theme.Inset(theme.PatternGutter, horizontal.c) + gutterInsetVertical := element.theme.Inset(theme.PatternGutter, vertical.c) + bounds := element.Bounds() thicknessHorizontal := - theme.HandleWidth() + + element.config.HandleWidth() + gutterInsetHorizontal[3] + gutterInsetHorizontal[1] thicknessVertical := - theme.HandleWidth() + + element.config.HandleWidth() + gutterInsetVertical[3] + gutterInsetVertical[1] @@ -373,9 +375,8 @@ func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) draw () { artist.Paste(element, element.child, image.Point { }) - deadPattern, _ := theme.DeadPattern(theme.PatternState { - Case: scrollContainerCase, - }) + deadPattern := element.theme.Pattern ( + theme.PatternDead, element.c, theme.PatternState { }) artist.FillRectangle ( element, deadPattern, image.Rect ( @@ -388,32 +389,30 @@ func (element *ScrollContainer) draw () { } func (element *ScrollContainer) drawHorizontalBar () { - gutterPattern, _ := theme.GutterPattern (theme.PatternState { - Case: scrollBarHorizontalCase, - Disabled: !element.horizontal.enabled, - }) - artist.FillRectangle(element, gutterPattern, element.horizontal.gutter) - - handlePattern, _ := theme.HandlePattern (theme.PatternState { - Case: scrollBarHorizontalCase, + state := theme.PatternState { Disabled: !element.horizontal.enabled, Pressed: element.horizontal.dragging, - }) + } + gutterPattern := element.theme.Pattern ( + theme.PatternGutter, element.horizontal.c, state) + artist.FillRectangle(element, gutterPattern, element.horizontal.gutter) + + handlePattern := element.theme.Pattern ( + theme.PatternHandle, element.horizontal.c, state) artist.FillRectangle(element, handlePattern, element.horizontal.bar) } func (element *ScrollContainer) drawVerticalBar () { - gutterPattern, _ := theme.GutterPattern (theme.PatternState { - Case: scrollBarVerticalCase, - Disabled: !element.vertical.enabled, - }) - artist.FillRectangle(element, gutterPattern, element.vertical.gutter) - - handlePattern, _ := theme.HandlePattern (theme.PatternState { - Case: scrollBarVerticalCase, + state := theme.PatternState { Disabled: !element.vertical.enabled, Pressed: element.vertical.dragging, - }) + } + gutterPattern := element.theme.Pattern ( + theme.PatternGutter, element.vertical.c, state) + artist.FillRectangle(element, gutterPattern, element.vertical.gutter) + + handlePattern := element.theme.Pattern ( + theme.PatternHandle, element.vertical.c, state) artist.FillRectangle(element, handlePattern, element.vertical.bar) } @@ -436,19 +435,17 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) { } func (element *ScrollContainer) updateMinimumSize () { - _, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState { - Case: scrollBarHorizontalCase, - }) - _, gutterInsetVertical := theme.GutterPattern(theme.PatternState { - Case: scrollBarHorizontalCase, - }) + gutterInsetHorizontal := element.theme.Inset ( + theme.PatternGutter, element.horizontal.c) + gutterInsetVertical := element.theme.Inset ( + theme.PatternGutter, element.vertical.c) thicknessHorizontal := - theme.HandleWidth() + + element.config.HandleWidth() + gutterInsetHorizontal[3] + gutterInsetHorizontal[1] thicknessVertical := - theme.HandleWidth() + + element.config.HandleWidth() + gutterInsetVertical[3] + gutterInsetVertical[1] diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index 931087e..5588e5f 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -1,23 +1,26 @@ package basicElements import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var spacerCase = theme.C("basic", "spacer") - // Spacer can be used to put space between two elements.. type Spacer struct { *core.Core core core.CoreControl line bool + + theme theme.Theme + config config.Config + c theme.Case } // 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 } + element = &Spacer { line: line, c: theme.C("basic", "spacer") } element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(1, 1) return @@ -33,20 +36,39 @@ func (element *Spacer) SetLine (line bool) { } } +// SetTheme sets the element's theme. +func (element *Spacer) SetTheme (new theme.Theme) { + element.theme = new + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Spacer) SetConfig (new config.Config) { + element.config = new + element.redo() +} + +func (element *Spacer) redo () { + if !element.core.HasImage() { + element.draw() + element.core.DamageAll() + } +} + func (element *Spacer) draw () { bounds := element.Bounds() if element.line { - pattern, _ := theme.ForegroundPattern(theme.PatternState { - Case: spacerCase, - Disabled: true, - }) + pattern := element.theme.Pattern ( + theme.PatternForeground, + element.c, + theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) } else { - pattern, _ := theme.BackgroundPattern(theme.PatternState { - Case: spacerCase, - Disabled: true, - }) + pattern := element.theme.Pattern ( + theme.PatternBackground, + element.c, + theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) } } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 8a92e4e..994b77c 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -3,11 +3,10 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var switchCase = theme.C("basic", "switch") - // 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 { @@ -21,23 +20,25 @@ type Switch struct { checked bool text string + theme theme.Theme + config config.Config + c theme.Case + onToggle func () } // NewSwitch creates a new switch with the specified label text. func NewSwitch (text string, on bool) (element *Switch) { - element = &Switch { checked: on, text: text } + element = &Switch { + checked: on, + text: text, + c: theme.C("basic", "switch"), + } element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, - element.focusableControl = core.NewFocusableCore (func () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - }) - element.drawer.SetFace(theme.FontFaceRegular()) + element.focusableControl = core.NewFocusableCore(element.redo) element.drawer.SetText([]rune(text)) - element.calculateMinimumSize() + element.updateMinimumSize() return } @@ -45,10 +46,7 @@ func (element *Switch) 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() - } + element.redo() } func (element *Switch) HandleMouseUp (x, y int, button input.Button) { @@ -76,10 +74,7 @@ func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter { element.pressed = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.redo() } } @@ -87,10 +82,7 @@ func (element *Switch) 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() - } + element.redo() if element.onToggle != nil { element.onToggle() } @@ -118,15 +110,36 @@ func (element *Switch) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) - element.calculateMinimumSize() - + element.updateMinimumSize() + element.redo() +} + +// SetTheme sets the element's theme. +func (element *Switch) SetTheme (new theme.Theme) { + element.theme = new + element.drawer.SetFace (element.theme.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal, + element.c)) + element.updateMinimumSize() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Switch) SetConfig (new config.Config) { + element.config = new + element.updateMinimumSize() + element.redo() +} + +func (element *Switch) redo () { if element.core.HasImage () { element.draw() element.core.DamageAll() } } -func (element *Switch) calculateMinimumSize () { +func (element *Switch) updateMinimumSize () { textBounds := element.drawer.LayoutBounds() lineHeight := element.drawer.LineHeight().Round() @@ -134,7 +147,9 @@ func (element *Switch) calculateMinimumSize () { element.core.SetMinimumSize(lineHeight * 2, lineHeight) } else { element.core.SetMinimumSize ( - lineHeight * 2 + theme.Padding() + textBounds.Dx(), + lineHeight * 2 + + element.config.Padding() + + textBounds.Dx(), lineHeight) } } @@ -143,9 +158,14 @@ func (element *Switch) draw () { bounds := element.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) - backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { - Case: switchCase, - }) + + state := theme.PatternState { + Disabled: !element.Enabled(), + Focused: element.Focused(), + Pressed: element.pressed, + } + backgroundPattern := element.theme.Pattern ( + theme.PatternBackground, element.c, state) artist.FillRectangle (element, backgroundPattern, bounds) if element.checked { @@ -162,33 +182,23 @@ func (element *Switch) draw () { } } - gutterPattern, _ := theme.GutterPattern(theme.PatternState { - Case: switchCase, - Disabled: !element.Enabled(), - Focused: element.Focused(), - Pressed: element.pressed, - }) + gutterPattern := element.theme.Pattern ( + theme.PatternGutter, element.c, state) artist.FillRectangle(element, gutterPattern, gutterBounds) - handlePattern, _ := theme.HandlePattern(theme.PatternState { - Case: switchCase, - Disabled: !element.Enabled(), - Focused: element.Focused(), - Pressed: element.pressed, - }) + handlePattern := element.theme.Pattern ( + theme.PatternHandle, element.c, state) artist.FillRectangle(element, handlePattern, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { - X: bounds.Dy() * 2 + theme.Padding(), + X: bounds.Dy() * 2 + element.config.Padding(), }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground, _ := theme.ForegroundPattern (theme.PatternState { - Case: switchCase, - Disabled: !element.Enabled(), - }) + foreground := element.theme.Pattern ( + theme.PatternForeground, element.c, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index 397d84f..fbd859b 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -3,12 +3,11 @@ package basicElements import "image" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var textBoxCase = theme.C("basic", "textBox") - // TextBox is a single-line text input. type TextBox struct { *core.Core @@ -24,6 +23,10 @@ type TextBox struct { placeholderDrawer artist.TextDrawer valueDrawer artist.TextDrawer + theme theme.Theme + config config.Config + c theme.Case + onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onChange func () onScrollBoundsChange func () @@ -33,7 +36,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 { c: theme.C("basic", "textBox") } element.Core, element.core = core.NewCore(element.handleResize) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { @@ -42,8 +45,6 @@ func NewTextBox (placeholder, value string) (element *TextBox) { element.core.DamageAll() } }) - element.placeholderDrawer.SetFace(theme.FontFaceRegular()) - element.valueDrawer.SetFace(theme.FontFaceRegular()) element.placeholder = placeholder element.placeholderDrawer.SetText([]rune(placeholder)) element.updateMinimumSize() @@ -130,9 +131,8 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.onScrollBoundsChange() } - if altered && element.core.HasImage () { - element.draw() - element.core.DamageAll() + if altered { + element.redo() } } @@ -145,10 +145,7 @@ func (element *TextBox) SetPlaceholder (placeholder string) { element.placeholderDrawer.SetText([]rune(placeholder)) element.updateMinimumSize() - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.redo() } func (element *TextBox) SetValue (text string) { @@ -161,11 +158,7 @@ func (element *TextBox) SetValue (text string) { element.cursor = element.valueDrawer.Length() } element.scrollToCursor() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.redo() } func (element *TextBox) Value () (value string) { @@ -203,7 +196,7 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) { } func (element *TextBox) scrollViewportWidth () (width int) { - return element.Bounds().Inset(theme.Padding()).Dx() + return element.Bounds().Inset(element.config.Padding()).Dx() } // ScrollTo scrolls the viewport to the specified point relative to @@ -218,10 +211,7 @@ func (element *TextBox) ScrollTo (position image.Point) { maxPosition := contentBounds.Max.X - element.scrollViewportWidth() if element.scroll > maxPosition { element.scroll = maxPosition } - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.redo() if element.onScrollBoundsChange != nil { element.onScrollBoundsChange() } @@ -236,18 +226,6 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) { element.onScrollBoundsChange = callback } -func (element *TextBox) updateMinimumSize () { - textBounds := element.placeholderDrawer.LayoutBounds() - _, inset := theme.InputPattern(theme.PatternState { - Case: textBoxCase, - }) - element.core.SetMinimumSize ( - textBounds.Dx() + - theme.Padding() * 2 + inset[3] + inset[1], - element.placeholderDrawer.LineHeight().Round() + - theme.Padding() * 2 + inset[0] + inset[2]) -} - func (element *TextBox) runOnChange () { if element.onChange != nil { element.onChange() @@ -257,7 +235,7 @@ func (element *TextBox) runOnChange () { func (element *TextBox) scrollToCursor () { if !element.core.HasImage() { return } - bounds := element.Bounds().Inset(theme.Padding()) + bounds := element.Bounds().Inset(element.config.Padding()) bounds = bounds.Sub(bounds.Min) bounds.Max.X -= element.valueDrawer.Em().Round() cursorPosition := element.valueDrawer.PositionOf(element.cursor) @@ -272,28 +250,64 @@ func (element *TextBox) scrollToCursor () { } } +// SetTheme sets the element's theme. +func (element *TextBox) SetTheme (new theme.Theme) { + element.theme = new + face := element.theme.FontFace ( + theme.FontStyleRegular, + theme.FontSizeNormal, + element.c) + element.placeholderDrawer.SetFace(face) + element.valueDrawer.SetFace(face) + element.updateMinimumSize() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *TextBox) SetConfig (new config.Config) { + element.config = new + element.updateMinimumSize() + element.redo() +} + +func (element *TextBox) updateMinimumSize () { + textBounds := element.placeholderDrawer.LayoutBounds() + inset := element.theme.Inset(theme.PatternInput, element.c) + element.core.SetMinimumSize ( + textBounds.Dx() + + element.config.Padding() * 2 + inset[3] + inset[1], + element.placeholderDrawer.LineHeight().Round() + + element.config.Padding() * 2 + inset[0] + inset[2]) +} + +func (element *TextBox) redo () { + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } +} + func (element *TextBox) draw () { bounds := element.Bounds() // FIXME: take index into account - pattern, inset := theme.InputPattern(theme.PatternState { - Case: textBoxCase, + state := theme.PatternState { Disabled: !element.Enabled(), Focused: element.Focused(), - }) + } + pattern := element.theme.Pattern(theme.PatternSunken, element.c, state) artist.FillRectangle(element, pattern, bounds) if len(element.text) == 0 && !element.Focused() { // draw placeholder textBounds := element.placeholderDrawer.LayoutBounds() offset := bounds.Min.Add (image.Point { - X: theme.Padding() + inset[3], - Y: theme.Padding() + inset[0], - }) - foreground, _ := theme.ForegroundPattern(theme.PatternState { - Case: textBoxCase, - Disabled: true, + X: element.config.Padding(), + Y: element.config.Padding(), }) + foreground := element.theme.Pattern ( + theme.PatternForeground, element.c, + theme.PatternState { Disabled: true }) element.placeholderDrawer.Draw ( element, foreground, @@ -302,13 +316,11 @@ func (element *TextBox) draw () { // draw input value textBounds := element.valueDrawer.LayoutBounds() offset := bounds.Min.Add (image.Point { - X: theme.Padding() + inset[3] - element.scroll, - Y: theme.Padding() + inset[0], - }) - foreground, _ := theme.ForegroundPattern(theme.PatternState { - Case: textBoxCase, - Disabled: !element.Enabled(), + X: element.config.Padding() - element.scroll, + Y: element.config.Padding(), }) + foreground := element.theme.Pattern ( + theme.PatternForeground, element.c, state) element.valueDrawer.Draw ( element, foreground, @@ -318,9 +330,6 @@ func (element *TextBox) draw () { // cursor cursorPosition := element.valueDrawer.PositionOf ( element.cursor) - foreground, _ := theme.ForegroundPattern(theme.PatternState { - Case: textBoxCase, - }) artist.Line ( element, foreground, 1, diff --git a/elements/core/core.go b/elements/core/core.go index 3635c08..88fb0b5 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -2,11 +2,7 @@ package core import "image" import "image/color" -import "golang.org/x/image/font" -import "git.tebibyte.media/sashakoshka/tomo/theme" -import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" // Core is a struct that implements some core functionality common to most // widgets. It is meant to be embedded directly into a struct. @@ -18,32 +14,20 @@ type Core struct { minimumHeight int } - config config.Config - theme theme.Theme - c theme.Case - - handleSizeChange func () - handleConfigChange func () - handleThemeChange func () + drawSizeChange func () onMinimumSizeChange func () onDamage func (region canvas.Canvas) } // NewCore creates a new element core and its corresponding control. func NewCore ( - handleSizeChange func (), - handleConfigChange func (), - handleThemeChange func (), - c theme.Case, + drawSizeChange func (), ) ( core *Core, control CoreControl, ) { core = &Core { - handleSizeChange: handleSizeChange, - handleConfigChange: handleConfigChange, - handleThemeChange: handleThemeChange, - c: c, + drawSizeChange: drawSizeChange, } control = CoreControl { core: core } return @@ -88,8 +72,8 @@ func (core *Core) MinimumSize () (width, height int) { // overridden. func (core *Core) DrawTo (canvas canvas.Canvas) { core.canvas = canvas - if core.handleSizeChange != nil { - core.handleSizeChange() + if core.drawSizeChange != nil { + core.drawSizeChange() } } @@ -105,24 +89,6 @@ func (core *Core) OnMinimumSizeChange (callback func ()) { core.onMinimumSizeChange = callback } -// SetConfig fulfills the elements.Configurable interface. This should not need -// to be overridden. -func (core *Core) SetConfig (config config.Config) { - core.config = config - if core.handleConfigChange != nil { - core.handleConfigChange() - } -} - -// SetTheme fulfills the elements.Themeable interface. This should not need -// to be overridden. -func (core *Core) SetTheme (theme theme.Theme) { - core.theme = theme - if core.handleThemeChange != nil { - core.handleThemeChange() - } -} - // 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 @@ -188,49 +154,3 @@ func (control CoreControl) ConstrainSize ( } return } - -// Config returns the current configuration. -func (control CoreControl) Config () (config.Config) { - return control.core.config -} - -// Theme returns the current theme. -func (control CoreControl) Theme () (theme.Theme) { - return control.core.theme -} - -// FontFace is like Theme.FontFace, but it automatically applies the correct -// case. -func (control CoreControl) FontFace ( - style theme.FontStyle, - size theme.FontSize, -) ( - face font.Face, -) { - return control.core.theme.FontFace(style, size, control.core.c) -} - -// Icon is like Theme.Icon, but it automatically applies the correct case. -func (control CoreControl) Icon (name string) (artist.Pattern) { - return control.core.theme.Icon(name, control.core.c) -} - -// Pattern is like Theme.Pattern, but it automatically applies the correct case. -func (control CoreControl) Pattern ( - id theme.Pattern, - state theme.PatternState, -) ( - pattern artist.Pattern, -) { - return control.core.theme.Pattern(id, control.core.c, state) -} - -// Inset is like Theme.Inset, but it automatically applies the correct case. -func (control CoreControl) Inset (id theme.Pattern) (inset theme.Inset) { - return control.core.theme.Inset(id, control.core.c) -} - -// Sink is like Theme.Sink, but it automatically applies the correct case. -func (control CoreControl) Sink (id theme.Pattern) (offset image.Point) { - return control.core.theme.Sink(id, control.core.c) -} From a0e57921a4ce45832f5104b3b0f63e1e940134f7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 8 Feb 2023 14:36:14 -0500 Subject: [PATCH 13/15] Oh my jod --- artist/text.go | 1 + config/config.go | 41 +++++++++++++++++ elements/basic/button.go | 27 ++++++----- elements/basic/checkbox.go | 27 +++++------ elements/basic/container.go | 25 ++++++---- elements/basic/label.go | 22 ++++----- elements/basic/list.go | 33 +++++++------- elements/basic/listentry.go | 24 +++++----- elements/basic/progressbar.go | 23 ++++------ elements/basic/scrollcontainer.go | 75 ++++++++++++++++++++---------- elements/basic/spacer.go | 16 +++---- elements/basic/switch.go | 24 +++++----- elements/basic/textbox.go | 28 ++++++------ elements/testing/artist.go | 4 +- elements/testing/mouse.go | 30 +++++++++--- theme/default.go | 20 ++++++-- theme/state.go | 30 ++++++++++++ theme/theme.go | 76 +++++++++++++++++++------------ 18 files changed, 333 insertions(+), 193 deletions(-) diff --git a/artist/text.go b/artist/text.go index 88616f1..bc8c8cd 100644 --- a/artist/text.go +++ b/artist/text.go @@ -165,6 +165,7 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) { // have its maximum width set to the given width. This does not alter the // drawer's state. func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) { + if drawer.face == nil { return } if !drawer.layoutClean { drawer.recalculate() } metrics := drawer.face.Metrics() dot := fixed.Point26_6 { 0, metrics.Height } diff --git a/config/config.go b/config/config.go index cdc4868..2937e0c 100644 --- a/config/config.go +++ b/config/config.go @@ -50,3 +50,44 @@ func (Default) ScrollVelocity () int { func (Default) ThemePath () (string) { return "" } + +// Wrapped wraps a configuration and uses Default if it is nil. +type Wrapped struct { + Config +} + +// Padding returns the amount of internal padding elements should have. +// An element's inner content (such as text) should be inset by this +// amount, in addition to the inset returned by the pattern of its +// background. +func (wrapped Wrapped) Padding () int { + return wrapped.ensure().Padding() +} + +// Margin returns how much space should be put in between elements. +func (wrapped Wrapped) Margin () int { + return wrapped.ensure().Margin() +} + +// HandleWidth returns how large grab handles should typically be. This +// is important for accessibility reasons. +func (wrapped Wrapped) HandleWidth () int { + return wrapped.ensure().HandleWidth() +} + +// ScrollVelocity returns how many pixels should be scrolled every time +// a scroll button is pressed. +func (wrapped Wrapped) ScrollVelocity () int { + return wrapped.ensure().ScrollVelocity() +} + +// ThemePath returns the directory path to the theme. +func (wrapped Wrapped) ThemePath () string { + return wrapped.ensure().ThemePath() +} + +func (wrapped Wrapped) ensure () (real Config) { + real = wrapped.Config + if real == nil { real = Default { } } + return +} diff --git a/elements/basic/button.go b/elements/basic/button.go index 28e8566..cf8677e 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -18,18 +18,16 @@ type Button struct { pressed bool text string - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onClick func () } // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { - element = &Button { - c: theme.C("basic", "button"), - } + element = &Button { } + element.theme.Case = theme.C("basic", "button") element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) @@ -103,18 +101,19 @@ func (element *Button) SetText (text string) { // SetTheme sets the element's theme. func (element *Button) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *Button) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -141,7 +140,7 @@ func (element *Button) draw () { Pressed: element.pressed, } - pattern := element.theme.Pattern(theme.PatternButton, element.c, state) + pattern := element.theme.Pattern(theme.PatternButton, state) artist.FillRectangle(element, pattern, bounds) @@ -156,6 +155,10 @@ func (element *Button) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern(theme.PatternForeground, element.c, state) + if element.pressed { + offset = offset.Add(element.theme.Sink(theme.PatternButton)) + } + + foreground := element.theme.Pattern(theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index d6a7680..9c330b6 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -19,19 +19,16 @@ type Checkbox struct { checked bool text string - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onToggle func () } // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { - element = &Checkbox { - checked: checked, - c: theme.C("basic", "checkbox"), - } + element = &Checkbox { checked: checked } + element.theme.Case = theme.C("basic", "checkbox") element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) @@ -126,18 +123,19 @@ func (element *Checkbox) SetText (text string) { // SetTheme sets the element's theme. func (element *Checkbox) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *Checkbox) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -172,10 +170,10 @@ func (element *Checkbox) draw () { } backgroundPattern := element.theme.Pattern ( - theme.PatternBackground, element.c, state) + theme.PatternBackground, state) artist.FillRectangle(element, backgroundPattern, bounds) - pattern := element.theme.Pattern(theme.PatternButton, element.c, state) + pattern := element.theme.Pattern(theme.PatternButton, state) artist.FillRectangle(element, pattern, boxBounds) textBounds := element.drawer.LayoutBounds() @@ -186,7 +184,6 @@ func (element *Checkbox) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, state) + foreground := element.theme.Pattern(theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/container.go b/elements/basic/container.go index 75bf24c..e6f2637 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -24,9 +24,8 @@ type Container struct { focusable bool flexible bool - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) @@ -35,9 +34,8 @@ type Container struct { // NewContainer creates a new container. func NewContainer (layout layouts.Layout) (element *Container) { - element = &Container { - c: theme.C("basic", "container"), - } + element = &Container { } + element.theme.Case = theme.C("basic", "container") element.Core, element.core = core.NewCore(element.redoAll) element.SetLayout(layout) return @@ -57,6 +55,12 @@ func (element *Container) SetLayout (layout layouts.Layout) { // whatever way is defined by the current layout. func (element *Container) Adopt (child elements.Element, expand bool) { // set event handlers + if child0, ok := child.(elements.Themeable); ok { + child0.SetTheme(element.theme.Theme) + } + if child0, ok := child.(elements.Configurable); ok { + child0.SetConfig(element.config.Config) + } child.OnDamage (func (region canvas.Canvas) { element.core.DamageRegion(region.Bounds()) }) @@ -212,7 +216,6 @@ func (element *Container) redoAll () { bounds := element.Bounds() pattern := element.theme.Pattern ( theme.PatternBackground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) @@ -225,10 +228,11 @@ func (element *Container) redoAll () { // SetTheme sets the element's theme. func (element *Container) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new for _, child := range element.children { if child0, ok := child.Element.(elements.Themeable); ok { - child0.SetTheme(element.theme) + child0.SetTheme(element.theme.Theme) } } element.updateMinimumSize() @@ -237,7 +241,8 @@ func (element *Container) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *Container) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new for _, child := range element.children { if child0, ok := child.Element.(elements.Configurable); ok { child0.SetConfig(element.config) diff --git a/elements/basic/label.go b/elements/basic/label.go index 1448309..7d75ae3 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -14,9 +14,8 @@ type Label struct { text string drawer artist.TextDrawer - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onFlexibleHeightChange func () } @@ -24,7 +23,8 @@ type Label struct { // 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 { c: theme.C("basic", "label") } + element = &Label { } + element.theme.Case = theme.C("basic", "label") element.Core, element.core = core.NewCore(element.handleResize) element.SetWrap(wrap) element.SetText(text) @@ -34,8 +34,7 @@ func NewLabel (text string, wrap bool) (element *Label) { func (element *Label) redo () { face := element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c) + theme.FontSizeNormal) element.drawer.SetFace(face) element.updateMinimumSize() bounds := element.Bounds() @@ -109,11 +108,11 @@ func (element *Label) SetWrap (wrap bool) { // SetTheme sets the element's theme. func (element *Label) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() if element.core.HasImage () { @@ -124,7 +123,8 @@ func (element *Label) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *Label) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() if element.core.HasImage () { @@ -153,7 +153,6 @@ func (element *Label) draw () { pattern := element.theme.Pattern ( theme.PatternBackground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) @@ -161,7 +160,6 @@ func (element *Label) draw () { foreground := element.theme.Pattern ( theme.PatternForeground, - element.c, theme.PatternState { }) element.drawer.Draw(element, foreground, bounds.Min.Sub(textBounds.Min)) } diff --git a/elements/basic/list.go b/elements/basic/list.go index 6da55b3..8a19584 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -26,9 +26,8 @@ type List struct { scroll int entries []ListEntry - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onScrollBoundsChange func () onNoEntrySelected func () @@ -36,10 +35,8 @@ type List struct { // NewList creates a new list element with the specified entries. func NewList (entries ...ListEntry) (element *List) { - element = &List { - selectedEntry: -1, - c: theme.C("basic", "list"), - } + element = &List { selectedEntry: -1 } + element.theme.Case = theme.C("basic", "list") element.Core, element.core = core.NewCore(element.handleResize) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { @@ -71,9 +68,10 @@ func (element *List) handleResize () { // SetTheme sets the element's theme. func (element *List) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new for index, entry := range element.entries { - entry.SetConfig(element.config) + entry.SetTheme(element.theme.Theme) element.entries[index] = entry } element.updateMinimumSize() @@ -82,7 +80,8 @@ func (element *List) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *List) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new for index, entry := range element.entries { entry.SetConfig(element.config) element.entries[index] = entry @@ -206,7 +205,7 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) return element.Bounds().Dy() - inset[0] - inset[2] } @@ -238,7 +237,7 @@ func (element *List) CountEntries () (count int) { func (element *List) Append (entry ListEntry) { // append entry.Collapse(element.forcedMinimumWidth) - entry.SetTheme(element.theme) + entry.SetTheme(element.theme.Theme) entry.SetConfig(element.config) element.entries = append(element.entries, entry) @@ -332,7 +331,7 @@ func (element *List) Replace (index int, entry ListEntry) { } func (element *List) selectUnderMouse (x, y int) (updated bool) { - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) bounds := inset.Apply(element.Bounds()) mousePoint := image.Pt(x, y) dot := image.Pt ( @@ -374,7 +373,7 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { } func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1]) return entry } @@ -401,7 +400,7 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) minimumHeight += inset[0] + inset[2] element.core.SetMinimumSize(minimumWidth, minimumHeight) @@ -410,8 +409,8 @@ func (element *List) updateMinimumSize () { func (element *List) draw () { bounds := element.Bounds() - inset := element.theme.Inset(theme.PatternSunken, element.c) - pattern := element.theme.Pattern (theme.PatternSunken, element.c, theme.PatternState { + inset := element.theme.Inset(theme.PatternSunken) + pattern := element.theme.Pattern (theme.PatternSunken, theme.PatternState { Disabled: !element.Enabled(), Focused: element.Focused(), }) diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 79d91d8..5248d1b 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -14,9 +14,8 @@ type ListEntry struct { text string forcedMinimumWidth int - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onSelect func () } @@ -25,8 +24,8 @@ func NewListEntry (text string, onSelect func ()) (entry ListEntry) { entry = ListEntry { text: text, onSelect: onSelect, - c: theme.C("basic", "listEntry"), } + entry.theme.Case = theme.C("basic", "listEntry") entry.drawer.SetText([]rune(text)) entry.updateBounds() return @@ -39,16 +38,17 @@ func (entry *ListEntry) Collapse (width int) { } func (entry *ListEntry) SetTheme (new theme.Theme) { - entry.theme = new + if new == entry.theme.Theme { return } + entry.theme.Theme = new entry.drawer.SetFace (entry.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - entry.c)) + theme.FontSizeNormal)) entry.updateBounds() } -func (entry *ListEntry) SetConfig (config config.Config) { - entry.config = config +func (entry *ListEntry) SetConfig (new config.Config) { + if new == entry.config.Config { return } + entry.config.Config = new } func (entry *ListEntry) updateBounds () { @@ -60,7 +60,7 @@ func (entry *ListEntry) updateBounds () { entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx() } - inset := entry.theme.Inset(theme.PatternRaised, entry.c) + inset := entry.theme.Inset(theme.PatternRaised) entry.bounds.Max.Y += inset[0] + inset[2] entry.textPoint = @@ -80,12 +80,12 @@ func (entry *ListEntry) Draw ( Focused: focused, On: on, } - pattern := entry.theme.Pattern (theme.PatternRaised, entry.c, state) + pattern := entry.theme.Pattern (theme.PatternRaised, state) artist.FillRectangle ( destination, pattern, entry.Bounds().Add(offset)) - foreground := entry.theme.Pattern (theme.PatternForeground, entry.c, state) + foreground := entry.theme.Pattern (theme.PatternForeground, state) return entry.drawer.Draw ( destination, foreground, diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 80806ca..f1f2faf 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -12,18 +12,15 @@ type ProgressBar struct { core core.CoreControl progress float64 - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped } // NewProgressBar creates a new progress bar displaying the given progress // level. func NewProgressBar (progress float64) (element *ProgressBar) { - element = &ProgressBar { - progress: progress, - c: theme.C("basic", "progressBar"), - } + element = &ProgressBar { progress: progress } + element.theme.Case = theme.C("basic", "progressBar") element.Core, element.core = core.NewCore(element.draw) return } @@ -40,14 +37,16 @@ func (element *ProgressBar) SetProgress (progress float64) { // SetTheme sets the element's theme. func (element *ProgressBar) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *ProgressBar) SetConfig (new config.Config) { - element.config = new + if new == nil || new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -70,9 +69,8 @@ func (element *ProgressBar) draw () { pattern := element.theme.Pattern ( theme.PatternSunken, - element.c, theme.PatternState { }) - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) artist.FillRectangle(element, pattern, bounds) bounds = inset.Apply(bounds) meterBounds := image.Rect ( @@ -80,8 +78,7 @@ func (element *ProgressBar) draw () { bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) accent := element.theme.Pattern ( - theme.PatternSunken, - element.c, + theme.PatternAccent, theme.PatternState { }) artist.FillRectangle(element, accent, meterBounds) } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index 553cc5e..cfc540a 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -20,7 +20,7 @@ type ScrollContainer struct { childWidth, childHeight int horizontal struct { - c theme.Case + theme theme.Wrapped exists bool enabled bool dragging bool @@ -31,7 +31,7 @@ type ScrollContainer struct { } vertical struct { - c theme.Case + theme theme.Wrapped exists bool enabled bool dragging bool @@ -41,9 +41,8 @@ type ScrollContainer struct { bar image.Rectangle } - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) @@ -52,12 +51,12 @@ type ScrollContainer struct { // NewScrollContainer creates a new scroll container with the specified scroll // bars. func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { - element = &ScrollContainer { c: theme.C("basic", "scrollContainer") } - element.horizontal.c = theme.C("basic", "scrollBarHorizontal") - element.vertical.c = theme.C("basic", "scrollBarVertical") + element = &ScrollContainer { } + element.theme.Case = theme.C("basic", "scrollContainer") + element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal") + element.vertical.theme.Case = theme.C("basic", "scrollBarVertical") element.Core, element.core = core.NewCore(element.handleResize) - element.updateMinimumSize() element.horizontal.exists = horizontal element.vertical.exists = vertical return @@ -81,6 +80,12 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { // adopt new child element.child = child if child != nil { + if child0, ok := child.(elements.Themeable); ok { + child0.SetTheme(element.theme.Theme) + } + if child0, ok := child.(elements.Configurable); ok { + child0.SetConfig(element.config.Config) + } child.OnDamage(element.childDamageCallback) child.OnMinimumSizeChange(element.updateMinimumSize) child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback) @@ -102,6 +107,34 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { } } +// SetTheme sets the element's theme. +func (element *ScrollContainer) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + if child, ok := element.child.(elements.Themeable); ok { + child.SetTheme(element.theme.Theme) + } + if element.core.HasImage() { + element.recalculate() + element.resizeChildToFit() + element.draw() + } +} + +// SetConfig sets the element's configuration. +func (element *ScrollContainer) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.config.Config = new + if child, ok := element.child.(elements.Configurable); ok { + child.SetConfig(element.config.Config) + } + if element.core.HasImage() { + element.recalculate() + element.resizeChildToFit() + element.draw() + } +} + func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if child, ok := element.child.(elements.KeyboardTarget); ok { child.HandleKeyDown(key, modifiers) @@ -289,8 +322,8 @@ func (element *ScrollContainer) recalculate () { horizontal := &element.horizontal vertical := &element.vertical - gutterInsetHorizontal := element.theme.Inset(theme.PatternGutter, horizontal.c) - gutterInsetVertical := element.theme.Inset(theme.PatternGutter, vertical.c) + gutterInsetHorizontal := horizontal.theme.Inset(theme.PatternGutter) + gutterInsetVertical := vertical.theme.Inset(theme.PatternGutter) bounds := element.Bounds() thicknessHorizontal := @@ -376,7 +409,7 @@ func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) draw () { artist.Paste(element, element.child, image.Point { }) deadPattern := element.theme.Pattern ( - theme.PatternDead, element.c, theme.PatternState { }) + theme.PatternDead, theme.PatternState { }) artist.FillRectangle ( element, deadPattern, image.Rect ( @@ -393,12 +426,10 @@ func (element *ScrollContainer) drawHorizontalBar () { Disabled: !element.horizontal.enabled, Pressed: element.horizontal.dragging, } - gutterPattern := element.theme.Pattern ( - theme.PatternGutter, element.horizontal.c, state) + gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state) artist.FillRectangle(element, gutterPattern, element.horizontal.gutter) - handlePattern := element.theme.Pattern ( - theme.PatternHandle, element.horizontal.c, state) + handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state) artist.FillRectangle(element, handlePattern, element.horizontal.bar) } @@ -407,12 +438,10 @@ func (element *ScrollContainer) drawVerticalBar () { Disabled: !element.vertical.enabled, Pressed: element.vertical.dragging, } - gutterPattern := element.theme.Pattern ( - theme.PatternGutter, element.vertical.c, state) + gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state) artist.FillRectangle(element, gutterPattern, element.vertical.gutter) - handlePattern := element.theme.Pattern ( - theme.PatternHandle, element.vertical.c, state) + handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state) artist.FillRectangle(element, handlePattern, element.vertical.bar) } @@ -435,10 +464,8 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) { } func (element *ScrollContainer) updateMinimumSize () { - gutterInsetHorizontal := element.theme.Inset ( - theme.PatternGutter, element.horizontal.c) - gutterInsetVertical := element.theme.Inset ( - theme.PatternGutter, element.vertical.c) + gutterInsetHorizontal := element.horizontal.theme.Inset(theme.PatternGutter) + gutterInsetVertical := element.vertical.theme.Inset(theme.PatternGutter) thicknessHorizontal := element.config.HandleWidth() + diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index 5588e5f..1b14067 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -11,16 +11,16 @@ type Spacer struct { core core.CoreControl line bool - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + 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, c: theme.C("basic", "spacer") } + element = &Spacer { line: line } + element.theme.Case = theme.C("basic", "spacer") element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(1, 1) return @@ -38,13 +38,15 @@ func (element *Spacer) SetLine (line bool) { // SetTheme sets the element's theme. func (element *Spacer) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.redo() } // SetConfig sets the element's configuration. func (element *Spacer) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.redo() } @@ -61,13 +63,11 @@ func (element *Spacer) draw () { if element.line { pattern := element.theme.Pattern ( theme.PatternForeground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) } else { pattern := element.theme.Pattern ( theme.PatternBackground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 994b77c..fa7c7d3 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -20,9 +20,8 @@ type Switch struct { checked bool text string - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onToggle func () } @@ -32,8 +31,8 @@ func NewSwitch (text string, on bool) (element *Switch) { element = &Switch { checked: on, text: text, - c: theme.C("basic", "switch"), } + element.theme.Case = theme.C("basic", "switch") element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) @@ -116,18 +115,19 @@ func (element *Switch) SetText (text string) { // SetTheme sets the element's theme. func (element *Switch) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *Switch) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -165,7 +165,7 @@ func (element *Switch) draw () { Pressed: element.pressed, } backgroundPattern := element.theme.Pattern ( - theme.PatternBackground, element.c, state) + theme.PatternBackground, state) artist.FillRectangle (element, backgroundPattern, bounds) if element.checked { @@ -183,11 +183,11 @@ func (element *Switch) draw () { } gutterPattern := element.theme.Pattern ( - theme.PatternGutter, element.c, state) + theme.PatternGutter, state) artist.FillRectangle(element, gutterPattern, gutterBounds) handlePattern := element.theme.Pattern ( - theme.PatternHandle, element.c, state) + theme.PatternHandle, state) artist.FillRectangle(element, handlePattern, handleBounds) textBounds := element.drawer.LayoutBounds() @@ -199,6 +199,6 @@ func (element *Switch) draw () { offset.X -= textBounds.Min.X foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, state) + theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index fbd859b..babefdc 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -23,9 +23,8 @@ type TextBox struct { placeholderDrawer artist.TextDrawer valueDrawer artist.TextDrawer - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onChange func () @@ -36,7 +35,8 @@ 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 { c: theme.C("basic", "textBox") } + element = &TextBox { } + element.theme.Case = theme.C("basic", "textBox") element.Core, element.core = core.NewCore(element.handleResize) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { @@ -252,11 +252,11 @@ func (element *TextBox) scrollToCursor () { // SetTheme sets the element's theme. func (element *TextBox) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new face := element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c) + theme.FontSizeNormal) element.placeholderDrawer.SetFace(face) element.valueDrawer.SetFace(face) element.updateMinimumSize() @@ -265,19 +265,19 @@ func (element *TextBox) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *TextBox) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() - inset := element.theme.Inset(theme.PatternInput, element.c) element.core.SetMinimumSize ( textBounds.Dx() + - element.config.Padding() * 2 + inset[3] + inset[1], + element.config.Padding() * 2, element.placeholderDrawer.LineHeight().Round() + - element.config.Padding() * 2 + inset[0] + inset[2]) + element.config.Padding() * 2) } func (element *TextBox) redo () { @@ -295,7 +295,7 @@ func (element *TextBox) draw () { Disabled: !element.Enabled(), Focused: element.Focused(), } - pattern := element.theme.Pattern(theme.PatternSunken, element.c, state) + pattern := element.theme.Pattern(theme.PatternSunken, state) artist.FillRectangle(element, pattern, bounds) if len(element.text) == 0 && !element.Focused() { @@ -306,7 +306,7 @@ func (element *TextBox) draw () { Y: element.config.Padding(), }) foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, + theme.PatternForeground, theme.PatternState { Disabled: true }) element.placeholderDrawer.Draw ( element, @@ -320,7 +320,7 @@ func (element *TextBox) draw () { Y: element.config.Padding(), }) foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, state) + theme.PatternForeground, state) element.valueDrawer.Draw ( element, foreground, diff --git a/elements/testing/artist.go b/elements/testing/artist.go index 968143e..6e4a015 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -4,7 +4,6 @@ import "fmt" import "time" import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/defaultfont" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -20,8 +19,7 @@ type Artist struct { // NewArtist creates a new artist test element. func NewArtist () (element *Artist) { element = &Artist { } - element.Core, element.core = core.NewCore ( - element.draw, nil, nil, theme.C("testing", "artist")) + element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(480, 600) return } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index d027284..810f103 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -3,6 +3,7 @@ package testing import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -15,21 +16,33 @@ type Mouse struct { drawing bool color artist.Pattern lastMousePos image.Point + + config config.Config + theme theme.Theme + c theme.Case } // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { - element = &Mouse { } - element.Core, element.core = core.NewCore ( - element.draw, - element.redo, - element.redo, - theme.C("testing", "mouse")) + element = &Mouse { c: theme.C("testing", "mouse") } + element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(32, 32) element.color = artist.NewUniform(color.Black) return } +// SetTheme sets the element's theme. +func (element *Mouse) SetTheme (new theme.Theme) { + element.theme = new + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Mouse) SetConfig (new config.Config) { + element.config = new + element.redo() +} + func (element *Mouse) redo () { if !element.core.HasImage() { return } element.draw() @@ -38,7 +51,10 @@ func (element *Mouse) redo () { func (element *Mouse) draw () { bounds := element.Bounds() - pattern := element.core.Pattern(theme.PatternAccent, theme.PatternState { }) + pattern := element.theme.Pattern ( + theme.PatternAccent, + element.c, + theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) artist.StrokeRectangle ( element, diff --git a/theme/default.go b/theme/default.go index 5d2eec6..0521dea 100644 --- a/theme/default.go +++ b/theme/default.go @@ -41,7 +41,7 @@ func (Default) Pattern ( case PatternBackground: return backgroundPattern case PatternForeground: - if state.Disabled { + if state.Disabled || c == C("basic", "spacer") { return weakForegroundPattern } else { return foregroundPattern @@ -77,6 +77,16 @@ func (Default) Pattern ( } else { return listPattern } + } else if c == C("basic", "textBox") { + if state.Disabled { + return disabledInputPattern + } else { + if state.Focused { + return selectedInputPattern + } else { + return inputPattern + } + } } else { return sunkenPattern } @@ -86,7 +96,7 @@ func (Default) Pattern ( if state.Disabled { return disabledButtonPattern } else { - if state.Pressed { + if state.Pressed || state.On && c == C("basic", "checkbox") { if state.Focused { return pressedSelectedButtonPattern } else { @@ -144,13 +154,15 @@ func (Default) Inset (pattern Pattern, c Case) Inset { switch pattern { case PatternRaised: if c == C("basic", "listEntry") { - return Inset { 2, 1, 2, 1 } + return Inset { 4, 6, 4, 6 } } else { return Inset { 1, 1, 1, 1 } } case PatternSunken: if c == C("basic", "list") { - return Inset { 4, 6, 4, 6 } + return Inset { 2, 1, 2, 1 } + } else if c == C("basic", "progressBar") { + return Inset { 2, 1, 1, 2 } } else { return Inset { 1, 1, 1, 1 } } diff --git a/theme/state.go b/theme/state.go index 51e0ab2..cd46e75 100644 --- a/theme/state.go +++ b/theme/state.go @@ -46,3 +46,33 @@ type PatternState struct { // or outline. Invalid bool } + +// FontStyle specifies stylistic alterations to a font face. +type FontStyle int; const ( + FontStyleRegular FontStyle = 0 + FontStyleBold FontStyle = 1 + FontStyleItalic FontStyle = 2 + FontStyleBoldItalic FontStyle = 1 | 2 +) + +// FontSize specifies the general size of a font face in a semantic way. +type FontSize int; const ( + // FontSizeNormal is the default font size that should be used for most + // things. + FontSizeNormal FontSize = iota + + // FontSizeLarge is a larger font size suitable for things like section + // headings. + FontSizeLarge + + // FontSizeHuge is a very large font size suitable for things like + // titles, wizard step names, digital clocks, etc. + FontSizeHuge + + // FontSizeSmall is a smaller font size. Try not to use this unless it + // makes a lot of sense to do so, because it can negatively impact + // accessibility. It is useful for things like copyright notices at the + // bottom of some window that the average user doesn't actually care + // about. + FontSizeSmall +) diff --git a/theme/theme.go b/theme/theme.go index bc0f370..5f9f8ce 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -4,36 +4,6 @@ import "image" import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/artist" -// FontStyle specifies stylistic alterations to a font face. -type FontStyle int; const ( - FontStyleRegular FontStyle = 0 - FontStyleBold FontStyle = 1 - FontStyleItalic FontStyle = 2 - FontStyleBoldItalic FontStyle = 1 | 2 -) - -// FontSize specifies the general size of a font face in a semantic way. -type FontSize int; const ( - // FontSizeNormal is the default font size that should be used for most - // things. - FontSizeNormal FontSize = iota - - // FontSizeLarge is a larger font size suitable for things like section - // headings. - FontSizeLarge - - // FontSizeHuge is a very large font size suitable for things like - // titles, wizard step names, digital clocks, etc. - FontSizeHuge - - // FontSizeSmall is a smaller font size. Try not to use this unless it - // makes a lot of sense to do so, because it can negatively impact - // accessibility. It is useful for things like copyright notices at the - // bottom of some window that the average user doesn't actually care - // about. - FontSizeSmall -) - // Pattern lists a number of cannonical pattern types, each with its own ID. // This allows custom elements to follow themes, even those that do not // explicitly support them. @@ -98,3 +68,49 @@ type Theme interface { // sinking effect. Sink (Pattern, Case) image.Point } + +// Wrapped wraps any theme and injects a case into it automatically so that it +// doesn't need to be specified for each query. Additionally, if the underlying +// theme is nil, it just uses the default theme instead. +type Wrapped struct { + Theme + Case +} + +// FontFace returns the proper font for a given style and size. +func (wrapped Wrapped) FontFace (style FontStyle, size FontSize) font.Face { + real := wrapped.ensure() + return real.FontFace(style, size, wrapped.Case) +} + +// Icon returns an appropriate icon given an icon name. +func (wrapped Wrapped) Icon (name string) artist.Pattern { + real := wrapped.ensure() + return real.Icon(name, wrapped.Case) +} + +// Pattern returns an appropriate pattern given a pattern name and state. +func (wrapped Wrapped) Pattern (id Pattern, state PatternState) artist.Pattern { + real := wrapped.ensure() + return real.Pattern(id, wrapped.Case, state) +} + +// Inset returns the area on all sides of a given pattern that is not meant to +// be drawn on. +func (wrapped Wrapped) Inset (id Pattern) Inset { + real := wrapped.ensure() + return real.Inset(id, wrapped.Case) +} + +// Sink returns a vector that should be added to an element's inner content when +// it is pressed down (if applicable) to simulate a 3D sinking effect. +func (wrapped Wrapped) Sink (id Pattern) image.Point { + real := wrapped.ensure() + return real.Sink(id, wrapped.Case) +} + +func (wrapped Wrapped) ensure () (real Theme) { + real = wrapped.Theme + if real == nil { real = Default { } } + return +} From 6cc0f360000fae304658936775553687b37e95ba Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 8 Feb 2023 15:12:18 -0500 Subject: [PATCH 14/15] Migrated the clock --- elements/fun/clock.go | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/elements/fun/clock.go b/elements/fun/clock.go index facbc8f..2007231 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -4,21 +4,24 @@ import "time" import "math" import "image" import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" -var clockCase = theme.C("fun", "clock") - // 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 + theme theme.Wrapped } // NewAnalogClock creates a new analog clock that displays the specified time. func NewAnalogClock (newTime time.Time) (element *AnalogClock) { element = &AnalogClock { } + element.theme.Case = theme.C("fun", "clock") element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(64, 64) return @@ -28,6 +31,24 @@ func NewAnalogClock (newTime time.Time) (element *AnalogClock) { func (element *AnalogClock) SetTime (newTime time.Time) { if newTime == element.time { return } element.time = newTime + element.redo() +} + +// SetTheme sets the element's theme. +func (element *AnalogClock) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *AnalogClock) SetConfig (new config.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() @@ -37,19 +58,15 @@ func (element *AnalogClock) SetTime (newTime time.Time) { func (element *AnalogClock) draw () { bounds := element.Bounds() - pattern, inset := theme.SunkenPattern(theme.PatternState { - Case: clockCase, - }) + state := theme.PatternState { } + pattern := element.theme.Pattern(theme.PatternSunken, state) + inset := element.theme.Inset(theme.PatternSunken) artist.FillRectangle(element, pattern, bounds) bounds = inset.Apply(bounds) - foreground, _ := theme.ForegroundPattern(theme.PatternState { - Case: clockCase, - }) - accent, _ := theme.AccentPattern(theme.PatternState { - Case: clockCase, - }) + foreground := element.theme.Pattern(theme.PatternForeground, state) + accent := element.theme.Pattern(theme.PatternAccent, state) for hour := 0; hour < 12; hour ++ { element.radialLine ( @@ -71,7 +88,7 @@ func (element *AnalogClock) FlexibleHeightFor (width int) (height int) { return width } -// OnFlexibleHeightChange sets a function to be calle dwhen the parameters +// OnFlexibleHeightChange sets a function to be called when the parameters // affecting the clock's flexible height change. func (element *AnalogClock) OnFlexibleHeightChange (func ()) { } From bec8b817c89ed54388166a2499fa23c99ca1417e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 8 Feb 2023 21:05:36 -0500 Subject: [PATCH 15/15] Added a piano widget because why not really --- elements/fun/piano.go | 244 ++++++++++++++++++++++++++++++++++++ examples/goroutines/main.go | 2 +- examples/piano/main.go | 26 ++++ 3 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 elements/fun/piano.go create mode 100644 examples/piano/main.go diff --git a/elements/fun/piano.go b/elements/fun/piano.go new file mode 100644 index 0000000..900c6a3 --- /dev/null +++ b/elements/fun/piano.go @@ -0,0 +1,244 @@ +package fun + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/theme" +import "git.tebibyte.media/sashakoshka/tomo/config" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/elements/core" + +// Octave represents a MIDI octave. +type Octave int + +// Note returns the note at the specified scale degree in the chromatic scale. +func (octave Octave) Note (degree int) Note { + return Note(int(octave + 1) * 12 + degree) +} + +// Note represents a MIDI note. +type Note int + +// Octave returns the octave of the note +func (note Note) Octave () int { + return int(note / 12 - 1) +} + +// Degree returns the scale degree of the note in the chromatic scale. +func (note Note) Degree () int { + mod := note % 12 + if mod < 0 { mod += 12 } + return int(mod) +} + +// IsSharp returns whether or not the note is a sharp. +func (note Note) IsSharp () bool { + degree := note.Degree() + return degree == 1 || + degree == 3 || + degree == 6 || + degree == 8 || + degree == 10 +} + +const pianoKeyWidth = 18 + +type pianoKey struct { + image.Rectangle + Note +} + +type Piano struct { + *core.Core + core core.CoreControl + low, high Octave + + config config.Wrapped + theme theme.Wrapped + + flatKeys []pianoKey + sharpKeys []pianoKey + + pressed *pianoKey + + onPress func (Note) + onRelease func (Note) +} + +func NewPiano (low, high Octave) (element *Piano) { + element = &Piano { + low: low, + high: high, + } + element.theme.Case = theme.C("fun", "piano") + element.Core, element.core = core.NewCore (func () { + element.recalculate() + element.draw() + }) + element.updateMinimumSize() + return +} + +// OnPress sets a function to be called when a key is pressed. +func (element *Piano) OnPress (callback func (note Note)) { + element.onPress = callback +} + +// OnRelease sets a function to be called when a key is released. +func (element *Piano) OnRelease (callback func (note Note)) { + element.onRelease = callback +} + +func (element *Piano) HandleMouseDown (x, y int, button input.Button) { + if button != input.ButtonLeft { return } + element.pressUnderMouseCursor(image.Pt(x, y)) +} + +func (element *Piano) HandleMouseUp (x, y int, button input.Button) { + if button != input.ButtonLeft { return } + if element.onRelease != nil { + element.onRelease((*element.pressed).Note) + } + element.pressed = nil + element.redo() +} + +func (element *Piano) HandleMouseMove (x, y int) { + if element.pressed == nil { return } + element.pressUnderMouseCursor(image.Pt(x, y)) +} + +func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } + +func (element *Piano) pressUnderMouseCursor (point image.Point) { + // release previous note + if element.pressed != nil && element.onRelease != nil { + element.onRelease((*element.pressed).Note) + } + + // find out which note is being pressed + newKey := (*pianoKey)(nil) + for index, key := range element.flatKeys { + if point.In(key.Rectangle) { + newKey = &element.flatKeys[index] + break + } + } + for index, key := range element.sharpKeys { + if point.In(key.Rectangle) { + newKey = &element.sharpKeys[index] + break + } + } + if newKey == nil { return } + + if newKey != element.pressed { + // press new note + element.pressed = newKey + if element.onPress != nil { + element.onPress((*element.pressed).Note) + } + element.redo() + } +} + +// SetTheme sets the element's theme. +func (element *Piano) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.updateMinimumSize() + element.recalculate() + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Piano) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.recalculate() + element.redo() +} + +func (element *Piano) updateMinimumSize () { + element.core.SetMinimumSize ( + pianoKeyWidth * 7 * element.countOctaves(), 64) +} + +func (element *Piano) countOctaves () int { + return int(element.high - element.low + 1) +} + +func (element *Piano) countFlats () int { + return element.countOctaves() * 8 +} + +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()) + + bounds := element.Bounds() + dot := bounds.Min + note := element.low.Note(0) + limit := element.high.Note(12) + flatIndex := 0 + sharpIndex := 0 + for note < limit { + if note.IsSharp() { + element.sharpKeys[sharpIndex].Rectangle = image.Rect ( + -(pianoKeyWidth * 3) / 7, 0, + (pianoKeyWidth * 3) / 7, + bounds.Dy() / 2).Add(dot) + element.sharpKeys[sharpIndex].Note = note + sharpIndex ++ + } else { + element.flatKeys[flatIndex].Rectangle = image.Rect ( + 0, 0, pianoKeyWidth, bounds.Dy()).Add(dot) + dot.X += pianoKeyWidth + element.flatKeys[flatIndex].Note = note + flatIndex ++ + } + note ++ + } +} + +func (element *Piano) draw () { + for _, key := range element.flatKeys { + element.drawFlat ( + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note) + } + for _, key := range element.sharpKeys { + element.drawSharp ( + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note) + } +} + +func (element *Piano) drawFlat (bounds image.Rectangle, pressed bool) { + state := theme.PatternState { + Pressed: pressed, + } + pattern := element.theme.Pattern(theme.PatternButton, state) + artist.FillRectangle(element, pattern, bounds) +} + +func (element *Piano) drawSharp (bounds image.Rectangle, pressed bool) { + state := theme.PatternState { + Pressed: pressed, + } + pattern := element.theme.Pattern(theme.PatternButton, state) + artist.FillRectangle(element, pattern, bounds) +} diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index 86840be..0a383d1 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -15,7 +15,7 @@ func main () { func run () { window, _ := tomo.NewWindow(2, 2) - window.SetTitle("clock") + window.SetTitle("Clock") container := basicElements.NewContainer(basicLayouts.Vertical { true, true }) window.Adopt(container) diff --git a/examples/piano/main.go b/examples/piano/main.go new file mode 100644 index 0000000..bc74a2c --- /dev/null +++ b/examples/piano/main.go @@ -0,0 +1,26 @@ +package main + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/layouts/basic" +import "git.tebibyte.media/sashakoshka/tomo/elements/fun" +import "git.tebibyte.media/sashakoshka/tomo/elements/basic" +import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" + +func main () { + tomo.Run(run) +} + +func run () { + window, _ := tomo.NewWindow(2, 2) + window.SetTitle("Piano") + container := basicElements.NewContainer(basicLayouts.Vertical { true, true }) + window.Adopt(container) + + label := basicElements.NewLabel("Play a song!", false) + container.Adopt(label, false) + piano := fun.NewPiano(3, 5) + container.Adopt(piano, true) + + window.OnClose(tomo.Stop) + window.Show() +}