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:
parent
add33c6e7e
commit
75fce53716
@ -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)
|
||||
}
|
||||
|
||||
|
48
ui/cache.go
48
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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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),
|
||||
|
18
ui/label.go
18
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) {
|
||||
|
@ -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
130
ui/paragraph.go
Normal 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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user