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) DesiredSize(ctx Context) geom.PointF32 {
func (b *Button) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
return b.desiredSize(ctx)
}

View File

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

View File

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

View File

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

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

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) {
b.Text = "Type here..."
})), 8),

View File

@ -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) {

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) {
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())
}

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)
}
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 {

View File

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

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
if s.Orientation == OrientationHorizontal {
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) {
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) {

View File

@ -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) {

View File

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

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