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).
This commit is contained in:
Sander Schobers 2020-05-16 15:37:53 +02:00
parent add33c6e7e
commit 75fce53716
16 changed files with 196 additions and 46 deletions

View File

@ -63,7 +63,7 @@ func (b *Button) icon(ctx Context) Texture {
func (b *Button) ButtonClicked() ControlClickedEventHandler { return &b.clicked } 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) return b.desiredSize(ctx)
} }

View File

@ -15,24 +15,50 @@ func (c CacheUpdateContextFn) Fn() CacheUpdateFn {
} }
type Cache struct { type Cache struct {
value interface{} value CachedValue
hash string
updateFn CacheUpdateFn update CacheUpdateFn
hashFn CacheHashFn hash CacheHashFn
} }
func NewCache(update CacheUpdateFn, hash CacheHashFn) *Cache { func NewCache(update CacheUpdateFn, hash CacheHashFn) *Cache {
return &Cache{updateFn: update, hashFn: hash} return &Cache{update: update, hash: hash}
}
func NewCacheContext(update CacheUpdateContextFn, hash CacheHashContextFn) *Cache {
return NewCache(update.Fn(), hash.Fn())
} }
func (c *Cache) Get(state interface{}) interface{} { func (c *Cache) Get(state interface{}) interface{} {
if c.hashFn(state) != c.hash { return c.value.Get(state, c.update, c.hash)
c.value = c.updateFn(state) }
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 return c.value
} }

View File

@ -75,7 +75,7 @@ func (c *Checkbox) selectedIcon(pt geom.PointF32) bool {
return false 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 { func (c *Checkbox) Handle(ctx Context, e Event) bool {
result := c.ControlBase.Handle(ctx, e) result := c.ControlBase.Handle(ctx, e)

View File

@ -6,7 +6,7 @@ import (
type Control interface { type Control interface {
Arrange(Context, geom.RectangleF32, geom.PointF32, Control) Arrange(Context, geom.RectangleF32, geom.PointF32, Control)
DesiredSize(Context) geom.PointF32 DesiredSize(Context, geom.PointF32) geom.PointF32
Handle(Context, Event) bool Handle(Context, Event) bool
Render(Context) Render(Context)

View File

@ -119,7 +119,7 @@ func (c *ControlBase) Bounds() geom.RectangleF32 { return c.bounds }
func (c *ControlBase) ControlClicked() ControlClickedEventHandler { return &c.clicked } 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 } func (c *ControlBase) Disable() { c.Disabled = true }

View File

@ -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) { ui.Margin(ui.StretchWidth(ui.BuildTextBox(func(b *ui.TextBox) {
b.Text = "Type here..." b.Text = "Type here..."
})), 8), })), 8),

View File

@ -9,8 +9,7 @@ type Label struct {
Text string Text string
init bool desired CachedValue
size *Cache
} }
func BuildLabel(text string, fn func(*Label)) *Label { func BuildLabel(text string, fn func(*Label)) *Label {
@ -21,15 +20,7 @@ func BuildLabel(text string, fn func(*Label)) *Label {
return l return l
} }
func (l *Label) initialize() { func (l *Label) hashDesiredSize(ctx Context) string {
if l.init {
return
}
l.size = NewCacheContext(l.desiredSize, l.hashContent)
l.init = true
}
func (l *Label) hashContent(ctx Context) string {
return l.FontName(ctx) + l.Text 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) return geom.PtF32(width+pad*2, height+pad*2)
} }
func (l *Label) DesiredSize(ctx Context) geom.PointF32 { func (l *Label) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
l.initialize() return l.desired.GetContext(ctx, l.desiredSize, l.hashDesiredSize).(geom.PointF32)
return l.size.Get(ctx).(geom.PointF32)
} }
func (l *Label) Render(ctx Context) { func (l *Label) Render(ctx Context) {

View File

@ -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) { func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
o.barWidth = ctx.Style().Dimensions.ScrollbarWidth o.barWidth = ctx.Style().Dimensions.ScrollbarWidth
o.desired = o.Content.DesiredSize(ctx) o.desired = o.Content.DesiredSize(ctx, bounds.Size())
o.bounds = bounds o.bounds = bounds
o.offset = offset o.offset = offset
o.parent = parent 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) 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()) return geom.PtF32(geom.NaN32(), geom.NaN32())
} }

130
ui/paragraph.go Normal file
View File

@ -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
}
}

View File

@ -12,8 +12,8 @@ func (p *Proxy) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Point
p.Content.Arrange(ctx, bounds, offset, parent) p.Content.Arrange(ctx, bounds, offset, parent)
} }
func (p *Proxy) DesiredSize(ctx Context) geom.PointF32 { func (p *Proxy) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
return p.Content.DesiredSize(ctx) return p.Content.DesiredSize(ctx, size)
} }
func (p *Proxy) Handle(ctx Context, e Event) bool { func (p *Proxy) Handle(ctx Context, e Event) bool {

View File

@ -40,7 +40,7 @@ func (s *Scrollbar) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.P
s.updateBar(ctx) 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) return s.Orientation.Pt(geom.NaN32(), ctx.Style().Dimensions.ScrollbarWidth)
} }

View File

@ -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 w := ctx.Style().Dimensions.ScrollbarWidth
if s.Orientation == OrientationHorizontal { if s.Orientation == OrientationHorizontal {
return geom.PtF32(geom.NaN32(), w) return geom.PtF32(geom.NaN32(), w)

View File

@ -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) { 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)) content := insetMargins(bounds, s.Margin, s.Width.Zero(size.X), s.Height.Zero(size.Y))
s.bounds = bounds s.bounds = bounds
s.Proxy.Arrange(ctx, content, offset, parent) s.Proxy.Arrange(ctx, content, offset, parent)
@ -80,9 +80,9 @@ func (s *Spacing) Center() {
s.Margin.Bottom = Infinite() s.Margin.Bottom = Infinite()
} }
func (s *Spacing) DesiredSize(ctx Context) geom.PointF32 { func (s *Spacing) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
var size = s.Proxy.DesiredSize(ctx) var content = s.Proxy.DesiredSize(ctx, size)
var w, h = s.Width.Zero(size.X), s.Height.Zero(size.Y) var w, h = s.Width.Zero(content.X), s.Height.Zero(content.Y)
var margin = func(l *Length) float32 { var margin = func(l *Length) float32 {
var v = l.Value() var v = l.Value()
if geom.IsNaN32(v) { if geom.IsNaN32(v) {

View File

@ -15,11 +15,11 @@ type Mock struct {
Size *geom.PointF32 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 { if m.Size != nil {
return *m.Size return *m.Size
} }
return m.ControlBase.DesiredSize(ctx) return m.ControlBase.DesiredSize(ctx, size)
} }
func TestNoStretchFillsAvailableSpace(t *testing.T) { func TestNoStretchFillsAvailableSpace(t *testing.T) {

View File

@ -23,7 +23,7 @@ func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.
var stretch int var stretch int
var desired = make([]geom.PointF32, len(p.Children)) var desired = make([]geom.PointF32, len(p.Children))
for i, child := range 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) { if geom.IsNaN32(size.Y) {
stretch++ stretch++
} else { } 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) 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 length float32
var width float32 var width float32
for _, child := range p.Children { 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) var l, w = p.Orientation.LengthParallel(size), p.Orientation.LengthPerpendicular(size)
if geom.IsNaN32(l) { if geom.IsNaN32(l) {
length = l length = l

View File

@ -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) 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 fontName = b.FontName(ctx)
var font = ctx.Fonts().Font(fontName) var font = ctx.Fonts().Font(fontName)
var width = font.WidthOf(b.Text) var width = font.WidthOf(b.Text)