diff --git a/artist/inset.go b/artist/inset.go index d552eda..196b29b 100644 --- a/artist/inset.go +++ b/artist/inset.go @@ -48,3 +48,13 @@ func (inset Inset) Inverse () (prime Inset) { inset[3] * -1, } } + +// Horizontal returns the sum of SideRight and SideLeft. +func (inset Inset) Horizontal () int { + return inset[SideRight] + inset[SideLeft] +} + +// Vertical returns the sum of SideTop and SideBottom. +func (inset Inset) Vertical () int { + return inset[SideTop] + inset[SideBottom] +} diff --git a/artist/pattern.go b/artist/pattern.go index 2a41d41..ce8a683 100644 --- a/artist/pattern.go +++ b/artist/pattern.go @@ -2,6 +2,7 @@ package artist import "image" import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" // Pattern is capable of drawing to a canvas within the bounds of a given // clipping rectangle. @@ -20,24 +21,38 @@ func Draw ( destination canvas.Canvas, source Pattern, clips ...image.Rectangle, +) ( + updatedRegion image.Rectangle, ) { for _, clip := range clips { source.Draw(destination, clip) + updatedRegion = updatedRegion.Union(clip) } + return } -// DrawBounds is like Draw, but lets you specify an overall bounding rectangle -// for the pattern. The destination is cut to this rectangle. +// DrawBounds lets you specify an overall bounding rectangle for drawing a +// pattern. The destination is cut to this rectangle. func DrawBounds ( destination canvas.Canvas, - bounds image.Rectangle, source Pattern, - clips ...image.Rectangle, + bounds image.Rectangle, +) ( + updatedRegion image.Rectangle, ) { - cut := canvas.Cut(destination, bounds) - for _, clip := range clips { - source.Draw(cut, clip) - } + return Draw(canvas.Cut(destination, bounds), source, bounds) +} + +// DrawShatter is like an inverse of Draw, drawing nothing in the areas +// specified in "rocks". +func DrawShatter ( + destination canvas.Canvas, + source Pattern, + rocks ...image.Rectangle, +) ( + updatedRegion image.Rectangle, +) { + return Draw(destination, source, shatter.Shatter(destination.Bounds(), rocks...)...) } // AllocateSample returns a new canvas containing the result of a pattern. The diff --git a/canvas/canvas.go b/canvas/canvas.go index 436dcd1..1661b9c 100644 --- a/canvas/canvas.go +++ b/canvas/canvas.go @@ -34,6 +34,21 @@ func NewBasicCanvas (width, height int) (canvas BasicCanvas) { return } +// FromImage creates a new BasicCanvas from an image.Image. +func FromImage (img image.Image) (canvas BasicCanvas) { + bounds := img.Bounds() + canvas = NewBasicCanvas(bounds.Dx(), bounds.Dy()) + point := image.Point { } + for point.Y = bounds.Min.Y; point.Y < bounds.Max.Y; point.Y ++ { + for point.X = bounds.Min.X; point.X < bounds.Max.X; point.X ++ { + canvasPoint := point.Sub(bounds.Min) + canvas.Set ( + canvasPoint.X, canvasPoint.Y, + img.At(point.X, point.Y)) + }} + return +} + // you know what it do func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) { return canvas.rect diff --git a/elements/basic/button.go b/elements/basic/button.go index 69ed715..e11a827 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -6,6 +6,7 @@ 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/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -121,7 +122,8 @@ func (element *Button) SetConfig (new config.Config) { func (element *Button) updateMinimumSize () { textBounds := element.drawer.LayoutBounds() - minimumSize := textBounds.Inset(-element.config.Padding()) + padding := element.theme.Padding(theme.PatternButton) + minimumSize := padding.Inverse().Apply(textBounds) element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) } @@ -138,8 +140,8 @@ func (element *Button) drawAndPush (partial bool) { } } -func (element *Button) state () theme.PatternState { - return theme.PatternState { +func (element *Button) state () theme.State { + return theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, @@ -152,20 +154,20 @@ func (element *Button) drawBackground (partial bool) []image.Rectangle { pattern := element.theme.Pattern(theme.PatternButton, state) static := element.theme.Hints(theme.PatternButton).StaticInset - if partial && static != (theme.Inset { }) { - return artist.FillRectangleShatter ( - element.core, pattern, bounds, static.Apply(bounds)) + if partial && static != (artist.Inset { }) { + tiles := shatter.Shatter(bounds, static.Apply(bounds)) + artist.Draw(element.core, pattern, tiles...) + return tiles } else { - return []image.Rectangle { - artist.FillRectangle(element.core, pattern, bounds), - } + pattern.Draw(element.core, bounds) + return []image.Rectangle { bounds } } } func (element *Button) drawText (partial bool) image.Rectangle { state := element.state() bounds := element.Bounds() - foreground := element.theme.Pattern(theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) sink := element.theme.Sink(theme.PatternButton) textBounds := element.drawer.LayoutBounds() @@ -183,8 +185,7 @@ func (element *Button) drawText (partial bool) image.Rectangle { if partial { pattern := element.theme.Pattern(theme.PatternButton, state) - artist.FillRectangleClip ( - element.core, pattern, bounds, region) + pattern.Draw(element.core, region) } element.drawer.Draw(element.core, foreground, offset) diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index 89eaabc..14690bb 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -4,7 +4,6 @@ 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/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -146,8 +145,9 @@ func (element *Checkbox) updateMinimumSize () { if element.text == "" { element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) } else { + margin := element.theme.Margin(theme.PatternBackground) element.core.SetMinimumSize ( - textBounds.Dy() + element.config.Padding() + textBounds.Dx(), + textBounds.Dy() + margin.X + textBounds.Dx(), textBounds.Dy()) } } @@ -163,7 +163,7 @@ func (element *Checkbox) draw () { bounds := element.Bounds() boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) - state := theme.PatternState { + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, @@ -172,19 +172,20 @@ func (element *Checkbox) draw () { backgroundPattern := element.theme.Pattern ( theme.PatternBackground, state) - artist.FillRectangle(element.core, backgroundPattern, bounds) + backgroundPattern.Draw(element.core, bounds) pattern := element.theme.Pattern(theme.PatternButton, state) - artist.FillRectangle(element.core, pattern, boxBounds) + pattern.Draw(element.core, boxBounds) textBounds := element.drawer.LayoutBounds() + margin := element.theme.Margin(theme.PatternBackground) offset := bounds.Min.Add(image.Point { - X: bounds.Dy() + element.config.Padding(), + X: bounds.Dy() + margin.X, }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern(theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) element.drawer.Draw(element.core, foreground, offset) } diff --git a/elements/basic/container.go b/elements/basic/container.go index b4bb5b0..0347def 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -226,9 +226,9 @@ func (element *Container) redoAll () { } pattern := element.theme.Pattern ( theme.PatternBackground, - theme.PatternState { }) - artist.FillRectangleShatter ( - element.core, pattern, element.Bounds(), rocks...) + theme.State { }) + artist.DrawShatter ( + element.core, pattern, rocks...) // cut our canvas up and give peices to child elements for _, entry := range element.children { @@ -311,9 +311,11 @@ func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) } func (element *Container) FlexibleHeightFor (width int) (height int) { + margin := element.theme.Margin(theme.PatternBackground) + // TODO: have layouts take in x and y margins return element.layout.FlexibleHeightFor ( element.children, - element.config.Margin(), width) + margin.X, width) } func (element *Container) OnFlexibleHeightChange (callback func ()) { @@ -515,16 +517,20 @@ func (element *Container) childFocusRequestCallback ( } func (element *Container) updateMinimumSize () { - width, height := element.layout.MinimumSize ( - element.children, element.config.Margin()) + margin := element.theme.Margin(theme.PatternBackground) + // TODO: have layouts take in x and y margins + width, height := element.layout.MinimumSize(element.children, margin.X) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, element.config.Margin(), width) + element.children, + margin.X, width) } element.core.SetMinimumSize(width, height) } func (element *Container) doLayout () { + margin := element.theme.Margin(theme.PatternBackground) + // TODO: have layouts take in x and y margins element.layout.Arrange ( - element.children, element.config.Margin(), element.Bounds()) + element.children, margin.X, element.Bounds()) } diff --git a/elements/basic/image.go b/elements/basic/image.go index 46482a3..2e5d4fd 100644 --- a/elements/basic/image.go +++ b/elements/basic/image.go @@ -1,17 +1,18 @@ package basicElements import "image" -import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" type Image struct { *core.Core core core.CoreControl - buffer artist.Pattern + buffer canvas.Canvas } func NewImage (image image.Image) (element *Image) { - element = &Image { buffer: artist.NewTexture(image) } + element = &Image { buffer: canvas.FromImage(image) } element.Core, element.core = core.NewCore(element.draw) bounds := image.Bounds() element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) @@ -19,5 +20,6 @@ func NewImage (image image.Image) (element *Image) { } func (element *Image) draw () { - artist.FillRectangle(element.core, element.buffer, element.Bounds()) + (patterns.Texture { Canvas: element.buffer }). + Draw(element.core, element.Bounds()) } diff --git a/elements/basic/label.go b/elements/basic/label.go index e82fea9..2674715 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -2,7 +2,6 @@ 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/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -137,7 +136,9 @@ func (element *Label) SetConfig (new config.Config) { func (element *Label) updateMinimumSize () { if element.wrap { em := element.drawer.Em().Round() - if em < 1 { em = element.config.Padding() } + if em < 1 { + em = element.theme.Padding(theme.PatternBackground)[0] + } element.core.SetMinimumSize ( em, element.drawer.LineHeight().Round()) if element.onFlexibleHeightChange != nil { @@ -154,13 +155,13 @@ func (element *Label) draw () { pattern := element.theme.Pattern ( theme.PatternBackground, - theme.PatternState { }) - artist.FillRectangle(element.core, pattern, bounds) + theme.State { }) + pattern.Draw(element.core, bounds) textBounds := element.drawer.LayoutBounds() - foreground := element.theme.Pattern ( - theme.PatternForeground, - theme.PatternState { }) + foreground := element.theme.Color ( + theme.ColorForeground, + theme.State { }) element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min)) } diff --git a/elements/basic/list.go b/elements/basic/list.go index 7fdaa87..0f4c009 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -221,8 +221,8 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - inset := element.theme.Inset(theme.PatternSunken) - return element.Bounds().Dy() - inset[0] - inset[2] + padding := element.theme.Padding(theme.PatternSunken) + return element.Bounds().Dy() - padding[0] - padding[2] } func (element *List) maxScrollHeight () (height int) { @@ -355,8 +355,8 @@ func (element *List) Select (index int) { } func (element *List) selectUnderMouse (x, y int) (updated bool) { - inset := element.theme.Inset(theme.PatternSunken) - bounds := inset.Apply(element.Bounds()) + padding := element.theme.Padding(theme.PatternSunken) + bounds := padding.Apply(element.Bounds()) mousePoint := image.Pt(x, y) dot := image.Pt ( bounds.Min.X, @@ -398,8 +398,8 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { bounds := element.Bounds() - inset := element.theme.Inset(theme.PatternSunken) - entry.Resize(inset.Apply(bounds).Dx()) + padding := element.theme.Padding(theme.PatternSunken) + entry.Resize(padding.Apply(bounds).Dx()) return entry } @@ -425,17 +425,17 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - inset := element.theme.Inset(theme.PatternSunken) - minimumHeight += inset[0] + inset[2] + padding := element.theme.Padding(theme.PatternSunken) + minimumHeight += padding[0] + padding[2] element.core.SetMinimumSize(minimumWidth, minimumHeight) } func (element *List) draw () { bounds := element.Bounds() - inset := element.theme.Inset(theme.PatternSunken) - innerBounds := inset.Apply(bounds) - state := theme.PatternState { + padding := element.theme.Padding(theme.PatternSunken) + innerBounds := padding.Apply(bounds) + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), } @@ -460,6 +460,6 @@ func (element *List) draw () { innerBounds.Dx(), element.contentHeight, ).Add(innerBounds.Min).Intersect(innerBounds) pattern := element.theme.Pattern(theme.PatternSunken, state) - artist.FillRectangleShatter ( + artist.DrawShatter ( element.core, pattern, bounds, covered) } diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 439ce90..1a690cd 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -47,8 +47,8 @@ func (entry *ListEntry) SetConfig (new config.Config) { } func (entry *ListEntry) updateBounds () { - inset := entry.theme.Inset(theme.PatternRaised) - entry.bounds = inset.Inverse().Apply(entry.drawer.LayoutBounds()) + padding := entry.theme.Padding(theme.PatternRaised) + entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds()) entry.bounds = entry.bounds.Sub(entry.bounds.Min) entry.minimumWidth = entry.bounds.Dx() entry.bounds.Max.X = entry.width @@ -62,23 +62,21 @@ func (entry *ListEntry) Draw ( ) ( updatedRegion image.Rectangle, ) { - state := theme.PatternState { + state := theme.State { Focused: focused, On: on, } pattern := entry.theme.Pattern (theme.PatternRaised, state) - inset := entry.theme.Inset(theme.PatternRaised) - artist.FillRectangle ( - destination, - pattern, - entry.Bounds().Add(offset)) + padding := entry.theme.Padding(theme.PatternRaised) + bounds := entry.Bounds().Add(offset) + artist.DrawBounds(destination, pattern, bounds) - foreground := entry.theme.Pattern (theme.PatternForeground, state) + foreground := entry.theme.Color (theme.ColorForeground, state) return entry.drawer.Draw ( destination, foreground, - offset.Add(image.Pt(inset[3], inset[0])). + offset.Add(image.Pt(padding[3], padding[0])). Sub(entry.drawer.LayoutBounds().Min)) } diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 626b215..3b197c7 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -3,7 +3,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/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // ProgressBar displays a visual indication of how far along a task is. @@ -52,9 +52,10 @@ func (element *ProgressBar) SetConfig (new config.Config) { } func (element (ProgressBar)) updateMinimumSize() { + padding := element.theme.Padding(theme.PatternSunken) element.core.SetMinimumSize ( - element.config.Padding() * 2, - element.config.Padding() * 2) + padding[3] + padding[1], + padding[0] + padding[2]) } func (element *ProgressBar) redo () { @@ -67,18 +68,15 @@ func (element *ProgressBar) redo () { func (element *ProgressBar) draw () { bounds := element.Bounds() - pattern := element.theme.Pattern ( - theme.PatternSunken, - theme.PatternState { }) - inset := element.theme.Inset(theme.PatternSunken) - artist.FillRectangle(element.core, pattern, bounds) - bounds = inset.Apply(bounds) + pattern := element.theme.Pattern(theme.PatternSunken, theme.State { }) + padding := element.theme.Padding(theme.PatternSunken) + pattern.Draw(element.core, bounds) + bounds = padding.Apply(bounds) meterBounds := image.Rect ( bounds.Min.X, bounds.Min.Y, bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) - accent := element.theme.Pattern ( - theme.PatternAccent, - theme.PatternState { }) - artist.FillRectangle(element.core, accent, meterBounds) + // TODO: maybe dont use the accent color here... + accent := element.theme.Color(theme.ColorAccent, theme.State { }) + shapes.FillColorRectangle(element.core, accent, meterBounds) } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index 719eff2..c686878 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -302,7 +302,7 @@ func (element *ScrollContainer) OnFocusMotionRequest ( } func (element *ScrollContainer) childDamageCallback (region canvas.Canvas) { - element.core.DamageRegion(artist.Paste(element.core, region, image.Point { })) + element.core.DamageRegion(region.Bounds()) } func (element *ScrollContainer) childFocusRequestCallback () (granted bool) { @@ -352,8 +352,8 @@ func (element *ScrollContainer) recalculate () { horizontal := &element.horizontal vertical := &element.vertical - gutterInsetHorizontal := horizontal.theme.Inset(theme.PatternGutter) - gutterInsetVertical := vertical.theme.Inset(theme.PatternGutter) + gutterInsetHorizontal := horizontal.theme.Padding(theme.PatternGutter) + gutterInsetVertical := vertical.theme.Padding(theme.PatternGutter) bounds := element.Bounds() thicknessHorizontal := @@ -438,8 +438,8 @@ func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) draw () { deadPattern := element.theme.Pattern ( - theme.PatternDead, theme.PatternState { }) - artist.FillRectangle ( + theme.PatternDead, theme.State { }) + artist.DrawBounds ( element.core, deadPattern, image.Rect ( element.vertical.gutter.Min.X, @@ -451,27 +451,27 @@ func (element *ScrollContainer) draw () { } func (element *ScrollContainer) drawHorizontalBar () { - state := theme.PatternState { + state := theme.State { Disabled: !element.horizontal.enabled, Pressed: element.horizontal.dragging, } gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state) - artist.FillRectangle(element.core, gutterPattern, element.horizontal.gutter) + artist.DrawBounds(element.core, gutterPattern, element.horizontal.gutter) handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state) - artist.FillRectangle(element.core, handlePattern, element.horizontal.bar) + artist.DrawBounds(element.core, handlePattern, element.horizontal.bar) } func (element *ScrollContainer) drawVerticalBar () { - state := theme.PatternState { + state := theme.State { Disabled: !element.vertical.enabled, Pressed: element.vertical.dragging, } gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state) - artist.FillRectangle(element.core, gutterPattern, element.vertical.gutter) + artist.DrawBounds(element.core, gutterPattern, element.vertical.gutter) handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state) - artist.FillRectangle(element.core, handlePattern, element.vertical.bar) + artist.DrawBounds(element.core, handlePattern, element.vertical.bar) } func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) { @@ -493,8 +493,8 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) { } func (element *ScrollContainer) updateMinimumSize () { - gutterInsetHorizontal := element.horizontal.theme.Inset(theme.PatternGutter) - gutterInsetVertical := element.vertical.theme.Inset(theme.PatternGutter) + gutterInsetHorizontal := element.horizontal.theme.Padding(theme.PatternGutter) + gutterInsetVertical := element.vertical.theme.Padding(theme.PatternGutter) thicknessHorizontal := element.config.HandleWidth() + diff --git a/elements/basic/slider.go b/elements/basic/slider.go index 63121bb..bf6efaa 100644 --- a/elements/basic/slider.go +++ b/elements/basic/slider.go @@ -173,7 +173,7 @@ func (element *Slider) redo () { func (element *Slider) draw () { bounds := element.Bounds() - element.track = element.theme.Inset(theme.PatternGutter).Apply(bounds) + element.track = element.theme.Padding(theme.PatternGutter).Apply(bounds) if element.vertical { barSize := element.track.Dx() element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) @@ -190,16 +190,16 @@ func (element *Slider) draw () { element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) } - state := theme.PatternState { + state := theme.State { Focused: element.Focused(), Disabled: !element.Enabled(), Pressed: element.dragging, } - artist.FillRectangle ( + artist.DrawBounds ( element.core, element.theme.Pattern(theme.PatternGutter, state), bounds) - artist.FillRectangle ( + artist.DrawBounds ( element.core, element.theme.Pattern(theme.PatternHandle, state), element.bar) diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index cb77206..297ad71 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -2,7 +2,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/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // Spacer can be used to put space between two elements.. @@ -61,14 +61,14 @@ func (element *Spacer) draw () { bounds := element.Bounds() if element.line { - pattern := element.theme.Pattern ( - theme.PatternForeground, - theme.PatternState { }) - artist.FillRectangle(element.core, pattern, bounds) + color := element.theme.Color ( + theme.ColorForeground, + theme.State { }) + shapes.FillColorRectangle(element.core, color, bounds) } else { pattern := element.theme.Pattern ( theme.PatternBackground, - theme.PatternState { }) - artist.FillRectangle(element.core, pattern, bounds) + theme.State { }) + pattern.Draw(element.core, bounds) } } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 1c06b80..4eeb9ad 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -149,7 +149,7 @@ func (element *Switch) updateMinimumSize () { } else { element.core.SetMinimumSize ( lineHeight * 2 + - element.config.Padding() + + element.theme.Margin(theme.PatternBackground).X + textBounds.Dx(), lineHeight) } @@ -160,14 +160,14 @@ func (element *Switch) draw () { handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min) - state := theme.PatternState { + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), Pressed: element.pressed, } backgroundPattern := element.theme.Pattern ( theme.PatternBackground, state) - artist.FillRectangle (element.core, backgroundPattern, bounds) + backgroundPattern.Draw(element.core, bounds) if element.checked { handleBounds.Min.X += bounds.Dy() @@ -185,21 +185,22 @@ func (element *Switch) draw () { gutterPattern := element.theme.Pattern ( theme.PatternGutter, state) - artist.FillRectangle(element.core, gutterPattern, gutterBounds) + artist.DrawBounds(element.core, gutterPattern, gutterBounds) handlePattern := element.theme.Pattern ( theme.PatternHandle, state) - artist.FillRectangle(element.core, handlePattern, handleBounds) + artist.DrawBounds(element.core, handlePattern, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { - X: bounds.Dy() * 2 + element.config.Padding(), + X: bounds.Dy() * 2 + + element.theme.Margin(theme.PatternBackground).X, }) offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern ( - theme.PatternForeground, state) + foreground := element.theme.Color ( + theme.ColorForeground, state) element.drawer.Draw(element.core, foreground, offset) } diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index bcc004b..7d7649a 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -9,6 +9,7 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/fixedutil" +import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // TextBox is a single-line text input. @@ -92,9 +93,10 @@ func (element *TextBox) HandleMouseMove (x, y int) { } func (element *TextBox) atPosition (position image.Point) int { + padding := element.theme.Padding(theme.PatternSunken) offset := element.Bounds().Min.Add (image.Pt ( - element.config.Padding() - element.scroll, - element.config.Padding())) + padding[artist.SideLeft] - element.scroll, + padding[artist.SideTop])) textBoundsMin := element.valueDrawer.LayoutBounds().Min return element.valueDrawer.AtPosition ( fixedutil.Pt(position.Sub(offset).Add(textBoundsMin))) @@ -251,7 +253,8 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) { } func (element *TextBox) scrollViewportWidth () (width int) { - return element.Bounds().Inset(element.config.Padding()).Dx() + padding := element.theme.Padding(theme.PatternSunken) + return padding.Apply(element.Bounds()).Dx() } // ScrollTo scrolls the viewport to the specified point relative to @@ -290,7 +293,8 @@ func (element *TextBox) runOnChange () { func (element *TextBox) scrollToCursor () { if !element.core.HasImage() { return } - bounds := element.Bounds().Inset(element.config.Padding()) + padding := element.theme.Padding(theme.PatternSunken) + bounds := padding.Apply(element.Bounds()) bounds = bounds.Sub(bounds.Min) bounds.Max.X -= element.valueDrawer.Em().Round() cursorPosition := fixedutil.RoundPt ( @@ -329,11 +333,11 @@ func (element *TextBox) SetConfig (new config.Config) { func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() + padding := element.theme.Padding(theme.PatternSunken) element.core.SetMinimumSize ( - textBounds.Dx() + - element.config.Padding() * 2, - element.placeholderDrawer.LineHeight().Round() + - element.config.Padding() * 2) + padding.Horizontal() + textBounds.Dx(), + padding.Vertical() + + element.placeholderDrawer.LineHeight().Round()) } func (element *TextBox) redo () { @@ -346,30 +350,29 @@ func (element *TextBox) redo () { func (element *TextBox) draw () { bounds := element.Bounds() - state := theme.PatternState { + state := theme.State { Disabled: !element.Enabled(), Focused: element.Focused(), } pattern := element.theme.Pattern(theme.PatternSunken, state) - inset := element.theme.Inset(theme.PatternSunken) - innerCanvas := canvas.Cut(element.core, inset.Apply(bounds)) - artist.FillRectangle(element.core, pattern, bounds) + padding := element.theme.Padding(theme.PatternSunken) + innerCanvas := canvas.Cut(element.core, padding.Apply(bounds)) + pattern.Draw(element.core, bounds) offset := bounds.Min.Add (image.Point { - X: element.config.Padding() - element.scroll, - Y: element.config.Padding(), + X: -element.scroll, + Y: 0, }) if element.Focused() && !element.dot.Empty() { // draw selection bounds - accent := element.theme.Pattern ( - theme.PatternAccent, state) + accent := element.theme.Color(theme.ColorAccent, state) canon := element.dot.Canon() foff := fixedutil.Pt(offset) start := element.valueDrawer.PositionAt(canon.Start).Add(foff) end := element.valueDrawer.PositionAt(canon.End).Add(foff) end.Y += element.valueDrawer.LineHeight() - artist.FillRectangle ( + shapes.FillColorRectangle ( innerCanvas, accent, image.Rectangle { @@ -381,9 +384,9 @@ func (element *TextBox) draw () { if len(element.text) == 0 { // draw placeholder textBounds := element.placeholderDrawer.LayoutBounds() - foreground := element.theme.Pattern ( - theme.PatternForeground, - theme.PatternState { Disabled: true }) + foreground := element.theme.Color ( + theme.ColorForeground, + theme.State { Disabled: true }) element.placeholderDrawer.Draw ( innerCanvas, foreground, @@ -391,8 +394,7 @@ func (element *TextBox) draw () { } else { // draw input value textBounds := element.valueDrawer.LayoutBounds() - foreground := element.theme.Pattern ( - theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) element.valueDrawer.Draw ( innerCanvas, foreground, @@ -401,11 +403,10 @@ func (element *TextBox) draw () { if element.Focused() && element.dot.Empty() { // draw cursor - foreground := element.theme.Pattern ( - theme.PatternForeground, state) + foreground := element.theme.Color(theme.ColorForeground, state) cursorPosition := fixedutil.RoundPt ( element.valueDrawer.PositionAt(element.dot.End)) - artist.Line ( + shapes.ColorLine ( innerCanvas, foreground, 1, cursorPosition.Add(offset), diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 066af12..00861ab 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -3,9 +3,10 @@ package fun import "time" import "math" 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/artist" +import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // AnalogClock can display the time of day in an analog format. @@ -58,15 +59,15 @@ func (element *AnalogClock) redo () { func (element *AnalogClock) draw () { bounds := element.Bounds() - state := theme.PatternState { } + state := theme.State { } pattern := element.theme.Pattern(theme.PatternSunken, state) - inset := element.theme.Inset(theme.PatternSunken) - artist.FillRectangle(element.core, pattern, bounds) + padding := element.theme.Padding(theme.PatternSunken) + pattern.Draw(element.core, bounds) - bounds = inset.Apply(bounds) + bounds = padding.Apply(bounds) - foreground := element.theme.Pattern(theme.PatternForeground, state) - accent := element.theme.Pattern(theme.PatternAccent, state) + foreground := element.theme.Color(theme.ColorForeground, state) + accent := element.theme.Color(theme.ColorAccent, state) for hour := 0; hour < 12; hour ++ { element.radialLine ( @@ -93,7 +94,7 @@ func (element *AnalogClock) FlexibleHeightFor (width int) (height int) { func (element *AnalogClock) OnFlexibleHeightChange (func ()) { } func (element *AnalogClock) radialLine ( - source artist.Pattern, + source color.RGBA, inner float64, outer float64, radian float64, @@ -107,5 +108,5 @@ func (element *AnalogClock) radialLine ( max := element.Bounds().Min.Add(image.Pt ( int(math.Cos(radian) * outer * width + width), int(math.Sin(radian) * outer * height + height))) - artist.Line(element.core, source, 1, min, max) + shapes.ColorLine(element.core, source, 1, min, max) } diff --git a/elements/fun/piano.go b/elements/fun/piano.go index e116c1d..a11a3c2 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -218,10 +218,11 @@ func (element *Piano) SetConfig (new config.Config) { } func (element *Piano) updateMinimumSize () { - inset := element.theme.Inset(theme.PatternSunken) + padding := element.theme.Padding(theme.PatternSunken) element.core.SetMinimumSize ( - pianoKeyWidth * 7 * element.countOctaves() + inset[1] + inset[3], - 64 + inset[0] + inset[2]) + pianoKeyWidth * 7 * element.countOctaves() + + padding[1] + padding[3], + 64 + padding[0] + padding[2]) } func (element *Piano) countOctaves () int { @@ -247,8 +248,8 @@ func (element *Piano) recalculate () { element.flatKeys = make([]pianoKey, element.countFlats()) element.sharpKeys = make([]pianoKey, element.countSharps()) - inset := element.theme.Inset(theme.PatternPinboard) - bounds := inset.Apply(element.Bounds()) + padding := element.theme.Padding(theme.PatternPinboard) + bounds := padding.Apply(element.Bounds()) dot := bounds.Min note := element.low.Note(0) @@ -280,7 +281,7 @@ func (element *Piano) recalculate () { } func (element *Piano) draw () { - state := theme.PatternState { + state := theme.State { Focused: element.Focused(), Disabled: !element.Enabled(), } @@ -303,28 +304,28 @@ func (element *Piano) draw () { } pattern := element.theme.Pattern(theme.PatternPinboard, state) - artist.FillRectangleShatter ( - element.core, pattern, element.Bounds(), element.contentBounds) + artist.DrawShatter ( + element.core, pattern, element.contentBounds) } func (element *Piano) drawFlat ( bounds image.Rectangle, pressed bool, - state theme.PatternState, + state theme.State, ) { state.Pressed = pressed pattern := element.theme.Theme.Pattern ( theme.PatternButton, state, theme.C("fun", "flatKey")) - artist.FillRectangle(element.core, pattern, bounds) + artist.DrawBounds(element.core, pattern, bounds) } func (element *Piano) drawSharp ( bounds image.Rectangle, pressed bool, - state theme.PatternState, + state theme.State, ) { state.Pressed = pressed pattern := element.theme.Theme.Pattern ( theme.PatternButton, state, theme.C("fun", "sharpKey")) - artist.FillRectangle(element.core, pattern, bounds) + artist.DrawBounds(element.core, pattern, bounds) } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index d3b5475..57022d1 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -80,7 +80,7 @@ func (element *Artist) draw () { tiles := shatter.Shatter(c41.Bounds(), rocks...) for index, tile := range tiles { artist.DrawBounds ( - element.core, tile, + element.core, []artist.Pattern { patterns.Uhex(0xFF0000FF), patterns.Uhex(0x00FF00FF), diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 57eeb67..194518a 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -49,11 +49,11 @@ func (element *Mouse) redo () { func (element *Mouse) draw () { bounds := element.Bounds() - pattern := element.theme.Pattern ( - theme.PatternAccent, - theme.PatternState { }, + accent := element.theme.Color ( + theme.ColorAccent, + theme.State { }, element.c) - pattern.Draw(element.core, bounds) + shapes.FillColorRectangle(element.core, accent, bounds) shapes.StrokeColorRectangle ( element.core, artist.Hex(0x000000FF), diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go index f4bfa7b..3ccd703 100644 --- a/examples/raycaster/raycaster.go +++ b/examples/raycaster/raycaster.go @@ -7,6 +7,7 @@ import "image/color" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/elements/core" type ControlState struct { @@ -202,9 +203,9 @@ func (element *Raycaster) drawMinimap () { if cell > 0 { cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF } } - artist.FillRectangle ( + shapes.FillColorRectangle ( element.core, - artist.NewUniform(cellColor), + cellColor, cellBounds.Inset(1)) }} @@ -217,18 +218,18 @@ func (element *Raycaster) drawMinimap () { hitPt := hit.Mul(float64(scale)).Point().Add(bounds.Min) playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8) - artist.FillEllipse ( + shapes.FillColorEllipse ( element.core, - artist.Uhex(0xFFFFFFFF), + artist.Hex(0xFFFFFFFF), playerBounds) - artist.Line ( + shapes.ColorLine ( element.core, - artist.Uhex(0xFFFFFFFF), 1, + artist.Hex(0xFFFFFFFF), 1, playerPt, playerAnglePt) - artist.Line ( + shapes.ColorLine ( element.core, - artist.Uhex(0x00FF00FF), 1, + artist.Hex(0x00FF00FF), 1, playerPt, hitPt) } diff --git a/theme/default.go b/theme/default.go index 4cdebcb..da0e868 100644 --- a/theme/default.go +++ b/theme/default.go @@ -1,6 +1,7 @@ package theme import "image" +import "image/color" import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/canvas" @@ -32,18 +33,9 @@ func (Default) Icon (string, IconSize, Case) canvas.Image { // Pattern returns a pattern from the default theme corresponding to the given // pattern ID. -func (Default) Pattern ( - pattern Pattern, - state PatternState, - c Case, -) artist.Pattern { - switch pattern { - case PatternAccent: - return patterns.Uhex(0xFF8800FF) - case PatternBackground: - return patterns.Uhex(0x000000FF) - case PatternForeground: - return patterns.Uhex(0xFFFFFFFF) +func (Default) Pattern (id Pattern, state State, c Case) artist.Pattern { + switch id { + case PatternBackground: return patterns.Uhex(0x000000FF) // case PatternDead: // case PatternRaised: // case PatternSunken: @@ -56,6 +48,14 @@ func (Default) Pattern ( } } +func (Default) Color (id Color, state State, c Case) color.RGBA { + switch id { + case ColorAccent: return artist.Hex(0xFF8800FF) + case ColorForeground: return artist.Hex(0xFFFFFFFF) + default: return artist.Hex(0x888888FF) + } +} + // Padding returns the default padding value for the given pattern. func (Default) Padding (pattern Pattern, c Case) artist.Inset { return artist.Inset { 4, 4, 4, 4} diff --git a/theme/state.go b/theme/state.go index cd46e75..57f96a4 100644 --- a/theme/state.go +++ b/theme/state.go @@ -8,7 +8,7 @@ package theme // specific elements. type Case struct { Namespace, Element string } -// C can be used as shorthand to generate a case struct as used in PatternState. +// C can be used as shorthand to generate a case struct as used in State. func C (namespace, element string) (c Case) { return Case { Namespace: namespace, @@ -16,14 +16,14 @@ func C (namespace, element string) (c Case) { } } -// 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 { +// State lists parameters which can change the appearance of some patterns and +// colors. For example, passing a State with Selected set to true may result in +// a pattern that has a colored border within it. +type State 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. + // in some sort of selected or "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 or selected. On bool // Focused should be set to true if the element that is using this diff --git a/theme/theme.go b/theme/theme.go index f506035..b5ca38b 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -18,17 +18,9 @@ const ( // 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 + // PatternBackground is the window background of the theme. It appears + // in things like containers and behind text. + PatternBackground Pattern = iota // PatternDead is a pattern that is displayed on a "dead area" where no // controls exist, but there still must be some indication of visual @@ -57,6 +49,14 @@ type Pattern int; const ( PatternHandle ) +type Color int; const ( + // ColorAccent is the accent color of the theme. + ColorAccent Color = iota + + // ColorForeground is the text/icon color of the theme. + ColorForeground +) + // Hints specifies rendering hints for a particular pattern. Elements can take // these into account in order to gain extra performance. type Hints struct { @@ -80,7 +80,11 @@ type Theme interface { // Pattern returns an appropriate pattern given a pattern name, case, // and state. - Pattern (Pattern, PatternState, Case) artist.Pattern + Pattern (Pattern, State, Case) artist.Pattern + + // Color returns an appropriate pattern given a color name, case, and + // state. + Color (Color, State, Case) color.RGBA // Padding returns how much space should be between the bounds of a // pattern whatever an element draws inside of it. diff --git a/theme/wrapped.go b/theme/wrapped.go index 947481b..731c6fc 100644 --- a/theme/wrapped.go +++ b/theme/wrapped.go @@ -1,6 +1,7 @@ package theme import "image" +import "image/color" import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/canvas" @@ -26,11 +27,17 @@ func (wrapped Wrapped) Icon (name string, size IconSize) canvas.Image { } // Pattern returns an appropriate pattern given a pattern name and state. -func (wrapped Wrapped) Pattern (id Pattern, state PatternState) artist.Pattern { +func (wrapped Wrapped) Pattern (id Pattern, state State) artist.Pattern { real := wrapped.ensure() return real.Pattern(id, state, wrapped.Case) } +// Color returns an appropriate color given a color name and state. +func (wrapped Wrapped) Color (id Color, state State) color.RGBA { + real := wrapped.ensure() + return real.Color(id, state, wrapped.Case) +} + // Padding returns how much space should be between the bounds of a // pattern whatever an element draws inside of it. func (wrapped Wrapped) Padding (id Pattern) artist.Inset {