From 75fce53716a7b0db3a19091d9dc5705b871d4e86 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Sat, 16 May 2020 15:37:53 +0200 Subject: [PATCH] Added Paragraph control. Extended DesiredSize method with extra arguments that tells the control how much space is available for the parent control (note: this might not be the actual given size when Arrange is called on the control). --- ui/button.go | 2 +- ui/cache.go | 48 ++++++++++--- ui/checkbox.go | 2 +- ui/control.go | 2 +- ui/controlbase.go | 2 +- ui/examples/01_basic/basic.go | 6 +- ui/label.go | 18 ++--- ui/overflow.go | 4 +- ui/paragraph.go | 130 ++++++++++++++++++++++++++++++++++ ui/proxy.go | 4 +- ui/scrollbar.go | 2 +- ui/slider.go | 2 +- ui/spacing.go | 8 +-- ui/spacing_test.go | 4 +- ui/stackpanel.go | 6 +- ui/textbox.go | 2 +- 16 files changed, 196 insertions(+), 46 deletions(-) create mode 100644 ui/paragraph.go diff --git a/ui/button.go b/ui/button.go index a5b06e2..053adc1 100644 --- a/ui/button.go +++ b/ui/button.go @@ -63,7 +63,7 @@ func (b *Button) icon(ctx Context) Texture { func (b *Button) ButtonClicked() ControlClickedEventHandler { return &b.clicked } -func (b *Button) DesiredSize(ctx Context) geom.PointF32 { +func (b *Button) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { return b.desiredSize(ctx) } diff --git a/ui/cache.go b/ui/cache.go index 9f6ad98..5e635ed 100644 --- a/ui/cache.go +++ b/ui/cache.go @@ -15,24 +15,50 @@ func (c CacheUpdateContextFn) Fn() CacheUpdateFn { } type Cache struct { - value interface{} - hash string + value CachedValue - updateFn CacheUpdateFn - hashFn CacheHashFn + update CacheUpdateFn + hash CacheHashFn } func NewCache(update CacheUpdateFn, hash CacheHashFn) *Cache { - return &Cache{updateFn: update, hashFn: hash} -} - -func NewCacheContext(update CacheUpdateContextFn, hash CacheHashContextFn) *Cache { - return NewCache(update.Fn(), hash.Fn()) + return &Cache{update: update, hash: hash} } func (c *Cache) Get(state interface{}) interface{} { - if c.hashFn(state) != c.hash { - c.value = c.updateFn(state) + return c.value.Get(state, c.update, c.hash) +} + +type CacheContext struct { + value CachedValue + + update CacheUpdateContextFn + hash CacheHashContextFn +} + +func NewCacheContext(update CacheUpdateContextFn, hash CacheHashContextFn) *CacheContext { + return &CacheContext{update: update, hash: hash} +} + +func (c *CacheContext) Get(ctx Context) interface{} { + return c.value.GetContext(ctx, c.update, c.hash) +} + +type CachedValue struct { + value interface{} + hash string +} + +func (c *CachedValue) Get(state interface{}, update CacheUpdateFn, hash CacheHashFn) interface{} { + if hash(state) != c.hash { + c.value = update(state) + } + return c.value +} + +func (c *CachedValue) GetContext(ctx Context, update CacheUpdateContextFn, hash CacheHashContextFn) interface{} { + if hash(ctx) != c.hash { + c.value = update(ctx) } return c.value } diff --git a/ui/checkbox.go b/ui/checkbox.go index 45fd095..b4eed2e 100644 --- a/ui/checkbox.go +++ b/ui/checkbox.go @@ -75,7 +75,7 @@ func (c *Checkbox) selectedIcon(pt geom.PointF32) bool { return false } -func (c *Checkbox) DesiredSize(ctx Context) geom.PointF32 { return c.desiredSize(ctx) } +func (c *Checkbox) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { return c.desiredSize(ctx) } func (c *Checkbox) Handle(ctx Context, e Event) bool { result := c.ControlBase.Handle(ctx, e) diff --git a/ui/control.go b/ui/control.go index cc5ce84..22f6762 100644 --- a/ui/control.go +++ b/ui/control.go @@ -6,7 +6,7 @@ import ( type Control interface { Arrange(Context, geom.RectangleF32, geom.PointF32, Control) - DesiredSize(Context) geom.PointF32 + DesiredSize(Context, geom.PointF32) geom.PointF32 Handle(Context, Event) bool Render(Context) diff --git a/ui/controlbase.go b/ui/controlbase.go index 8df78af..45eb423 100644 --- a/ui/controlbase.go +++ b/ui/controlbase.go @@ -119,7 +119,7 @@ func (c *ControlBase) Bounds() geom.RectangleF32 { return c.bounds } func (c *ControlBase) ControlClicked() ControlClickedEventHandler { return &c.clicked } -func (c *ControlBase) DesiredSize(Context) geom.PointF32 { return geom.ZeroPtF32 } +func (c *ControlBase) DesiredSize(Context, geom.PointF32) geom.PointF32 { return geom.ZeroPtF32 } func (c *ControlBase) Disable() { c.Disabled = true } diff --git a/ui/examples/01_basic/basic.go b/ui/examples/01_basic/basic.go index 11a0442..542ee10 100644 --- a/ui/examples/01_basic/basic.go +++ b/ui/examples/01_basic/basic.go @@ -82,7 +82,11 @@ func (b *basic) Init(ctx ui.Context) error { }), } }), - ui.Stretch(&ui.Label{Text: "Content"}), + ui.Stretch(ui.BuildParagraph( + "Content"+ + "\n\n"+ + "Could be on multiple lines...\n"+ + "And if the line is long enough (and without line breaks in the string) it will wrap around to the next line on the screen. You can test this behaviour by resizing the window. Go ahead!", nil)), ui.Margin(ui.StretchWidth(ui.BuildTextBox(func(b *ui.TextBox) { b.Text = "Type here..." })), 8), diff --git a/ui/label.go b/ui/label.go index 44ef7b5..ee6ccce 100644 --- a/ui/label.go +++ b/ui/label.go @@ -9,8 +9,7 @@ type Label struct { Text string - init bool - size *Cache + desired CachedValue } func BuildLabel(text string, fn func(*Label)) *Label { @@ -21,15 +20,7 @@ func BuildLabel(text string, fn func(*Label)) *Label { return l } -func (l *Label) initialize() { - if l.init { - return - } - l.size = NewCacheContext(l.desiredSize, l.hashContent) - l.init = true -} - -func (l *Label) hashContent(ctx Context) string { +func (l *Label) hashDesiredSize(ctx Context) string { return l.FontName(ctx) + l.Text } @@ -42,9 +33,8 @@ func (l *Label) desiredSize(ctx Context) interface{} { return geom.PtF32(width+pad*2, height+pad*2) } -func (l *Label) DesiredSize(ctx Context) geom.PointF32 { - l.initialize() - return l.size.Get(ctx).(geom.PointF32) +func (l *Label) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { + return l.desired.GetContext(ctx, l.desiredSize, l.hashDesiredSize).(geom.PointF32) } func (l *Label) Render(ctx Context) { diff --git a/ui/overflow.go b/ui/overflow.go index 7fb82ad..ff465f3 100644 --- a/ui/overflow.go +++ b/ui/overflow.go @@ -62,7 +62,7 @@ func (o *overflow) doOnVisibleBars(fn func(bar *Scrollbar)) { func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) { o.barWidth = ctx.Style().Dimensions.ScrollbarWidth - o.desired = o.Content.DesiredSize(ctx) + o.desired = o.Content.DesiredSize(ctx, bounds.Size()) o.bounds = bounds o.offset = offset o.parent = parent @@ -91,7 +91,7 @@ func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Po func (o *overflow) Bounds() geom.RectangleF32 { return o.bounds } -func (o *overflow) DesiredSize(ctx Context) geom.PointF32 { +func (o *overflow) DesiredSize(Context, geom.PointF32) geom.PointF32 { return geom.PtF32(geom.NaN32(), geom.NaN32()) } diff --git a/ui/paragraph.go b/ui/paragraph.go new file mode 100644 index 0000000..a4cee92 --- /dev/null +++ b/ui/paragraph.go @@ -0,0 +1,130 @@ +package ui + +import ( + "strconv" + "strings" + + "opslag.de/schobers/geom" +) + +type Paragraph struct { + Label + + width float32 + lines CachedValue +} + +func BuildParagraph(text string, fn func(*Paragraph)) *Paragraph { + var p = &Paragraph{} + p.Text = text + if fn != nil { + fn(p) + } + return p +} + +func fastFormatFloat32(f float32) string { return strconv.FormatFloat(float64(f), 'b', 32, 32) } + +func (p *Paragraph) desiredSize(ctx Context) interface{} { + fontName := p.FontName(ctx) + font := ctx.Fonts().Font(fontName) + + pad := ctx.Style().Dimensions.TextPadding + lines := p.splitInLines(ctx, p.width-2*pad) + return geom.PtF32(p.width, float32(len(lines))*font.Height()+2*pad) +} + +func (p *Paragraph) hashTextArranged(ctx Context) string { + return p.FontName(ctx) + p.Text + fastFormatFloat32(p.Bounds().Dx()) +} + +func (p *Paragraph) hashTextDesired(ctx Context) string { + return p.FontName(ctx) + p.Text + fastFormatFloat32(p.width) +} + +func (p *Paragraph) splitInLines(ctx Context, width float32) []string { + fontName := p.FontName(ctx) + font := ctx.Fonts().Font(fontName) + + spaces := func(s string) []int { // creates a slice with indices where spaces can be found in string s + var spaces []int + offset := 0 + for { + space := strings.Index(s[offset:], " ") + if space == -1 { + return spaces + } + offset += space + spaces = append(spaces, offset) + offset++ + } + } + + fit := func(s string) string { // tries to fit as much of string s in width space. + if font.WidthOf(s) < width { + return s + } + spaces := spaces(s) + // removes one word (delimited by spaces) at a time and tries until the result fits. + for split := len(spaces) - 1; split >= 0; split-- { + clipped := s[:spaces[split]] + if font.WidthOf(clipped) < width { + return clipped + } + } + // nothing fits (returns the whole string)... + return s + } + + var lines []string + for _, line := range strings.Split(p.Text, "\n") { + if len(line) == 0 { + lines = append(lines, line) + continue + } + + for len(line) > 0 { + clipped := fit(line) + lines = append(lines, clipped) + line = strings.TrimLeft(line[len(clipped):], " ") + } + } + return lines +} + +func (p *Paragraph) updateLines(ctx Context) interface{} { + pad := ctx.Style().Dimensions.TextPadding + return p.splitInLines(ctx, p.Bounds().Dx()-2*pad) +} + +func (p *Paragraph) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { + // stores the given width, is used when calculating the new desired size (and thus used in the hash method as well) + p.width = size.X + return p.desired.GetContext(ctx, p.desiredSize, p.hashTextDesired).(geom.PointF32) +} + +func (p *Paragraph) Render(ctx Context) { + p.RenderBackground(ctx) + + pad := ctx.Style().Dimensions.TextPadding + width := p.Bounds().Dx() - 2*pad + lines := p.lines.GetContext(ctx, p.updateLines, p.hashTextArranged).([]string) + + fontColor := p.TextColor(ctx) + fontName := p.FontName(ctx) + fontHeight := ctx.Fonts().Font(fontName).Height() + bounds := p.bounds.Inset(pad) + + left := bounds.Min.X + switch p.TextAlignment { + case AlignRight: + left = bounds.Max.X + case AlignCenter: + left += .5 * width + } + offset := bounds.Min.Y + for _, line := range lines { + ctx.Fonts().TextAlign(fontName, geom.PtF32(left, offset), fontColor, line, p.TextAlignment) + offset += fontHeight + } +} diff --git a/ui/proxy.go b/ui/proxy.go index be6874b..d4a6273 100644 --- a/ui/proxy.go +++ b/ui/proxy.go @@ -12,8 +12,8 @@ func (p *Proxy) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Point p.Content.Arrange(ctx, bounds, offset, parent) } -func (p *Proxy) DesiredSize(ctx Context) geom.PointF32 { - return p.Content.DesiredSize(ctx) +func (p *Proxy) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { + return p.Content.DesiredSize(ctx, size) } func (p *Proxy) Handle(ctx Context, e Event) bool { diff --git a/ui/scrollbar.go b/ui/scrollbar.go index 0ced53b..db5ee5a 100644 --- a/ui/scrollbar.go +++ b/ui/scrollbar.go @@ -40,7 +40,7 @@ func (s *Scrollbar) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.P s.updateBar(ctx) } -func (s *Scrollbar) DesiredSize(ctx Context) geom.PointF32 { +func (s *Scrollbar) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { return s.Orientation.Pt(geom.NaN32(), ctx.Style().Dimensions.ScrollbarWidth) } diff --git a/ui/slider.go b/ui/slider.go index 70c5dfc..ef389b5 100644 --- a/ui/slider.go +++ b/ui/slider.go @@ -101,7 +101,7 @@ func (s *Slider) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Poin } } -func (s *Slider) DesiredSize(ctx Context) geom.PointF32 { +func (s *Slider) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { w := ctx.Style().Dimensions.ScrollbarWidth if s.Orientation == OrientationHorizontal { return geom.PtF32(geom.NaN32(), w) diff --git a/ui/spacing.go b/ui/spacing.go index 8750869..6e4961c 100644 --- a/ui/spacing.go +++ b/ui/spacing.go @@ -65,7 +65,7 @@ func insetMargins(bounds geom.RectangleF32, margin SideLengths, width, height fl } func (s *Spacing) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) { - size := s.DesiredSize(ctx) + size := s.DesiredSize(ctx, bounds.Size()) content := insetMargins(bounds, s.Margin, s.Width.Zero(size.X), s.Height.Zero(size.Y)) s.bounds = bounds s.Proxy.Arrange(ctx, content, offset, parent) @@ -80,9 +80,9 @@ func (s *Spacing) Center() { s.Margin.Bottom = Infinite() } -func (s *Spacing) DesiredSize(ctx Context) geom.PointF32 { - var size = s.Proxy.DesiredSize(ctx) - var w, h = s.Width.Zero(size.X), s.Height.Zero(size.Y) +func (s *Spacing) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { + var content = s.Proxy.DesiredSize(ctx, size) + var w, h = s.Width.Zero(content.X), s.Height.Zero(content.Y) var margin = func(l *Length) float32 { var v = l.Value() if geom.IsNaN32(v) { diff --git a/ui/spacing_test.go b/ui/spacing_test.go index 2f2cf04..2e35077 100644 --- a/ui/spacing_test.go +++ b/ui/spacing_test.go @@ -15,11 +15,11 @@ type Mock struct { Size *geom.PointF32 } -func (m *Mock) DesiredSize(ctx Context) geom.PointF32 { +func (m *Mock) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { if m.Size != nil { return *m.Size } - return m.ControlBase.DesiredSize(ctx) + return m.ControlBase.DesiredSize(ctx, size) } func TestNoStretchFillsAvailableSpace(t *testing.T) { diff --git a/ui/stackpanel.go b/ui/stackpanel.go index 662753f..6ae42a9 100644 --- a/ui/stackpanel.go +++ b/ui/stackpanel.go @@ -23,7 +23,7 @@ func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom. var stretch int var desired = make([]geom.PointF32, len(p.Children)) for i, child := range p.Children { - var size = p.Orientation.FlipPt(child.DesiredSize(ctx)) + var size = p.Orientation.FlipPt(child.DesiredSize(ctx, bounds.Size())) if geom.IsNaN32(size.Y) { stretch++ } else { @@ -49,11 +49,11 @@ func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom. p.ControlBase.Arrange(ctx, p.Orientation.FlipRect(bounds), offset, parent) } -func (p *StackPanel) DesiredSize(ctx Context) geom.PointF32 { +func (p *StackPanel) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { var length float32 var width float32 for _, child := range p.Children { - var size = child.DesiredSize(ctx) + var size = child.DesiredSize(ctx, size) var l, w = p.Orientation.LengthParallel(size), p.Orientation.LengthPerpendicular(size) if geom.IsNaN32(l) { length = l diff --git a/ui/textbox.go b/ui/textbox.go index 4942a54..f87763e 100644 --- a/ui/textbox.go +++ b/ui/textbox.go @@ -56,7 +56,7 @@ func (b *TextBox) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Poi b.box.Arrange(ctx, bounds.Inset(b.pad(ctx)), offset, b) } -func (b *TextBox) DesiredSize(ctx Context) geom.PointF32 { +func (b *TextBox) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 { var fontName = b.FontName(ctx) var font = ctx.Fonts().Font(fontName) var width = font.WidthOf(b.Text)