Compare commits
6 Commits
bcd32f8372
...
12a9f5ee39
Author | SHA1 | Date | |
---|---|---|---|
12a9f5ee39 | |||
de8ce3e7bc | |||
5dcecb8cc1 | |||
c0586c1d8f | |||
764f2a0dd2 | |||
5a4dcd52b0 |
24
addons/embedres/embedres.go
Normal file
24
addons/embedres/embedres.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package embedres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ ui.Resources = &resources{}
|
||||||
|
|
||||||
|
type resources struct {
|
||||||
|
fs embed.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(fs embed.FS) ui.Resources {
|
||||||
|
return &resources{fs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resources) Destroy() error { return nil }
|
||||||
|
|
||||||
|
func (r resources) OpenResource(name string) (io.ReadCloser, error) {
|
||||||
|
return r.fs.Open(name)
|
||||||
|
}
|
20
ui/button.go
20
ui/button.go
@ -40,30 +40,30 @@ func BuildIconButton(icon, text string, fn func(b *Button)) *Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Button) desiredSize(ctx Context) geom.PointF32 {
|
func (b *Button) desiredSize(ctx Context) geom.PointF32 {
|
||||||
var pad = ctx.Style().Dimensions.TextPadding
|
var pad = b.ActualTextPadding(ctx)
|
||||||
var font = b.Font_(ctx)
|
var font = b.ActualFont(ctx)
|
||||||
var w, h float32 = 0, font.Height()
|
var w, h float32 = 0, font.Height()
|
||||||
|
|
||||||
icon, iconW, iconH := b.icon(ctx)
|
icon, iconW, iconH := b.icon(ctx)
|
||||||
if len(b.Text) == 0 {
|
if len(b.Text) == 0 {
|
||||||
if icon != nil && iconH > 0 {
|
if icon != nil && iconH > 0 {
|
||||||
w = pad + iconW + pad
|
w = pad.Left + iconW + pad.Right
|
||||||
h = iconH
|
h = iconH
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
w += pad + font.WidthOf(b.Text) + pad
|
w += pad.Left + font.WidthOf(b.Text) + pad.Right
|
||||||
if icon != nil && iconH > 0 {
|
if icon != nil && iconH > 0 {
|
||||||
if b.IconHeight == 0 {
|
if b.IconHeight == 0 {
|
||||||
iconW = iconW * h / iconH
|
iconW = iconW * h / iconH
|
||||||
// iconH = h
|
// iconH = h
|
||||||
}
|
}
|
||||||
w += iconW + pad
|
w += iconW + pad.Right
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w == 0 {
|
if w == 0 {
|
||||||
return geom.ZeroPtF32
|
return geom.ZeroPtF32
|
||||||
}
|
}
|
||||||
return geom.PtF32(w, pad+h+pad)
|
return geom.PtF32(w, pad.Top+h+pad.Bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Button) icon(ctx Context) (Texture, float32, float32) {
|
func (b *Button) icon(ctx Context) (Texture, float32, float32) {
|
||||||
@ -203,8 +203,8 @@ func (b *Button) Render(ctx Context) {
|
|||||||
bounds.Min.X += .5 * deltaX
|
bounds.Min.X += .5 * deltaX
|
||||||
bounds.Min.Y += .5 * deltaY
|
bounds.Min.Y += .5 * deltaY
|
||||||
|
|
||||||
var pad = style.Dimensions.TextPadding
|
pad := b.ActualTextPadding(ctx)
|
||||||
bounds = bounds.Inset(pad)
|
bounds = pad.InsetRect(bounds)
|
||||||
boundsH := bounds.Dy()
|
boundsH := bounds.Dy()
|
||||||
pos := bounds.Min
|
pos := bounds.Min
|
||||||
icon, iconW, iconH := b.icon(ctx)
|
icon, iconW, iconH := b.icon(ctx)
|
||||||
@ -222,10 +222,10 @@ func (b *Button) Render(ctx Context) {
|
|||||||
iconOffsetY = .5 * (boundsH - iconH)
|
iconOffsetY = .5 * (boundsH - iconH)
|
||||||
}
|
}
|
||||||
ctx.Renderer().DrawTextureOptions(icon, geom.RectRelF32(pos.X, pos.Y+iconOffsetY, iconW, iconH), DrawOptions{Tint: textColor})
|
ctx.Renderer().DrawTextureOptions(icon, geom.RectRelF32(pos.X, pos.Y+iconOffsetY, iconW, iconH), DrawOptions{Tint: textColor})
|
||||||
pos.X += iconW + pad
|
pos.X += iconW + pad.Right
|
||||||
}
|
}
|
||||||
if len(b.Text) != 0 {
|
if len(b.Text) != 0 {
|
||||||
font := b.Font_(ctx)
|
font := b.ActualFont(ctx)
|
||||||
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), textColor, b.Text)
|
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), textColor, b.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,16 +22,16 @@ func BuildCheckbox(text string, fn func(c *Checkbox)) *Checkbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 {
|
func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 {
|
||||||
var pad = ctx.Style().Dimensions.TextPadding
|
pad := c.ActualTextPadding(ctx)
|
||||||
font := c.Font_(ctx)
|
font := c.ActualFont(ctx)
|
||||||
var w, h float32 = 0, font.Height()
|
var w, h float32 = 0, font.Height()
|
||||||
if len(c.Text) != 0 {
|
if len(c.Text) != 0 {
|
||||||
w += pad + font.WidthOf(c.Text)
|
w += pad.Left + font.WidthOf(c.Text)
|
||||||
}
|
}
|
||||||
icon := c.getOrCreateNormalIcon(ctx)
|
icon := c.getOrCreateNormalIcon(ctx)
|
||||||
_, iconWidth := ScaleToHeight(SizeOfTexture(icon).ToF32(), h)
|
_, iconWidth := ScaleToHeight(SizeOfTexture(icon).ToF32(), h)
|
||||||
w += pad + iconWidth
|
w += pad.Left + iconWidth
|
||||||
return geom.PtF32(w+pad, pad+h+pad)
|
return geom.PtF32(w+pad.Right, pad.Top+h+pad.Bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checkbox) icon(ctx Context) Texture {
|
func (c *Checkbox) icon(ctx Context) Texture {
|
||||||
@ -108,8 +108,8 @@ func (c *Checkbox) Render(ctx Context) {
|
|||||||
fore := c.TextColor(ctx)
|
fore := c.TextColor(ctx)
|
||||||
bounds := c.bounds
|
bounds := c.bounds
|
||||||
|
|
||||||
var pad = style.Dimensions.TextPadding
|
pad := c.ActualTextPadding(ctx)
|
||||||
bounds = bounds.Inset(pad)
|
bounds = pad.InsetRect(bounds)
|
||||||
boundsH := bounds.Dy()
|
boundsH := bounds.Dy()
|
||||||
pos := bounds.Min
|
pos := bounds.Min
|
||||||
icon := c.icon(ctx)
|
icon := c.icon(ctx)
|
||||||
@ -125,10 +125,10 @@ func (c *Checkbox) Render(ctx Context) {
|
|||||||
_, iconWidth := ScaleToHeight(SizeOfTexture(scaledIcon).ToF32(), boundsH)
|
_, iconWidth := ScaleToHeight(SizeOfTexture(scaledIcon).ToF32(), boundsH)
|
||||||
rect := geom.RectRelF32(pos.X, pos.Y, iconWidth, boundsH)
|
rect := geom.RectRelF32(pos.X, pos.Y, iconWidth, boundsH)
|
||||||
ctx.Renderer().DrawTextureOptions(scaledIcon, rect, DrawOptions{Tint: iconColor})
|
ctx.Renderer().DrawTextureOptions(scaledIcon, rect, DrawOptions{Tint: iconColor})
|
||||||
pos.X += iconWidth + pad
|
pos.X += iconWidth + pad.Right
|
||||||
}
|
}
|
||||||
if len(c.Text) != 0 {
|
if len(c.Text) != 0 {
|
||||||
font := c.Font_(ctx)
|
font := c.ActualFont(ctx)
|
||||||
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), fore, c.Text)
|
ctx.Renderer().Text(font, geom.PtF32(pos.X, pos.Y+.5*(boundsH-font.Height())), fore, c.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,7 @@ type ControlBase struct {
|
|||||||
Background color.Color
|
Background color.Color
|
||||||
Font FontStyle
|
Font FontStyle
|
||||||
TextAlignment HorizontalAlignment
|
TextAlignment HorizontalAlignment
|
||||||
|
TextPadding SideLengths
|
||||||
|
|
||||||
Disabled bool
|
Disabled bool
|
||||||
|
|
||||||
@ -188,11 +189,15 @@ func (c *ControlBase) HandleNotify(ctx Context, e Event, notifier Notifier) bool
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ControlBase) Font_(ctx Context) Font {
|
func (c *ControlBase) ActualFont(ctx Context) Font {
|
||||||
name := c.FontName(ctx)
|
name := c.FontName(ctx)
|
||||||
return ctx.Fonts().Font(name)
|
return ctx.Fonts().Font(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ControlBase) ActualTextPadding(ctx Context) Sides {
|
||||||
|
return c.TextPadding.Zero(ctx.Style().Dimensions.TextPadding)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ControlBase) FontColor(ctx Context, color color.Color) color.Color {
|
func (c *ControlBase) FontColor(ctx Context, color color.Color) color.Color {
|
||||||
if c.Disabled {
|
if c.Disabled {
|
||||||
return ctx.Style().Palette.TextOnDisabled
|
return ctx.Style().Palette.TextOnDisabled
|
||||||
|
22
ui/debug.go
22
ui/debug.go
@ -77,8 +77,9 @@ func NewDebugOverlay(root Control) *debugOverlay {
|
|||||||
|
|
||||||
func (o *debugOverlay) renderControl(ctx Context, control Control) {
|
func (o *debugOverlay) renderControl(ctx Context, control Control) {
|
||||||
renderer := ctx.Renderer()
|
renderer := ctx.Renderer()
|
||||||
// bounds := control.Bounds()
|
|
||||||
// renderer.Rectangle(bounds, o.boundsColor, 1)
|
currentColor := zntg.MustHexColor("#FF0000")
|
||||||
|
parentColor := zntg.MustHexColor("#0000FF")
|
||||||
|
|
||||||
var maxY float32
|
var maxY float32
|
||||||
var renderHoverNode func(pos geom.PointF32, node *controlNode)
|
var renderHoverNode func(pos geom.PointF32, node *controlNode)
|
||||||
@ -96,6 +97,10 @@ func (o *debugOverlay) renderControl(ctx Context, control Control) {
|
|||||||
renderer.FillRectangle(pos.RectRel2D(nameTextureWidth, nameTextureHeight), color.Black)
|
renderer.FillRectangle(pos.RectRel2D(nameTextureWidth, nameTextureHeight), color.Black)
|
||||||
renderer.DrawTexturePoint(nameTexture, pos)
|
renderer.DrawTexturePoint(nameTexture, pos)
|
||||||
childPos := pos.Add2D(nameTextureWidth+ctx.Style().Dimensions.Margin, 0)
|
childPos := pos.Add2D(nameTextureWidth+ctx.Style().Dimensions.Margin, 0)
|
||||||
|
if len(node.Children) == 0 {
|
||||||
|
renderer.Rectangle(node.Parent.Bounds, parentColor, 1)
|
||||||
|
renderer.Rectangle(node.Bounds, currentColor, 1)
|
||||||
|
}
|
||||||
for _, child := range node.Children {
|
for _, child := range node.Children {
|
||||||
if childPos.Y == maxY {
|
if childPos.Y == maxY {
|
||||||
childPos.Y = maxY + nameTextureHeight
|
childPos.Y = maxY + nameTextureHeight
|
||||||
@ -114,15 +119,18 @@ func (o *debugOverlay) renderControl(ctx Context, control Control) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createHoverNodes(hover geom.PointF32, control Control) *controlNode {
|
func createHoverNodes(hover geom.PointF32, control Control) *controlNode {
|
||||||
if !hover.In(control.Bounds()) {
|
bounds := control.Bounds()
|
||||||
|
if !hover.In(bounds) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
node := &controlNode{Name: controlName(control)}
|
node := &controlNode{Name: controlName(control), Bounds: bounds}
|
||||||
for _, child := range controlChildren(control) {
|
for _, child := range controlChildren(control) {
|
||||||
childNode := createHoverNodes(hover, child)
|
childNode := createHoverNodes(hover, child)
|
||||||
if childNode != nil {
|
if childNode == nil {
|
||||||
node.Children = append(node.Children, childNode)
|
continue
|
||||||
}
|
}
|
||||||
|
childNode.Parent = node
|
||||||
|
node.Children = append(node.Children, childNode)
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@ -147,5 +155,7 @@ func (o *debugOverlay) Shown() {}
|
|||||||
|
|
||||||
type controlNode struct {
|
type controlNode struct {
|
||||||
Name string
|
Name string
|
||||||
|
Bounds geom.RectangleF32
|
||||||
|
Parent *controlNode
|
||||||
Children []*controlNode
|
Children []*controlNode
|
||||||
}
|
}
|
||||||
|
29
ui/label.go
29
ui/label.go
@ -6,10 +6,18 @@ import (
|
|||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TextOverflow int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TextOverflowClip TextOverflow = iota
|
||||||
|
TextOverflowEllipsis
|
||||||
|
)
|
||||||
|
|
||||||
type Label struct {
|
type Label struct {
|
||||||
ControlBase
|
ControlBase
|
||||||
|
|
||||||
Text string
|
Text string
|
||||||
|
TextOverflow TextOverflow
|
||||||
DropShadow color.Color
|
DropShadow color.Color
|
||||||
|
|
||||||
desired CachedValue
|
desired CachedValue
|
||||||
@ -28,11 +36,11 @@ func (l *Label) hashDesiredSize(ctx Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Label) desiredSize(ctx Context) interface{} {
|
func (l *Label) desiredSize(ctx Context) interface{} {
|
||||||
font := l.Font_(ctx)
|
font := l.ActualFont(ctx)
|
||||||
width := font.WidthOf(l.Text)
|
width := font.WidthOf(l.Text)
|
||||||
height := font.Height()
|
height := font.Height()
|
||||||
pad := ctx.Style().Dimensions.TextPadding
|
pad := l.ActualTextPadding(ctx)
|
||||||
return geom.PtF32(width+pad*2, height+pad*2)
|
return geom.PtF32(pad.Left+width+pad.Right, pad.Top+height+pad.Bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Label) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
|
func (l *Label) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
|
||||||
@ -40,8 +48,8 @@ func (l *Label) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Label) getLabelTopLeft(ctx Context) geom.PointF32 {
|
func (l *Label) getLabelTopLeft(ctx Context) geom.PointF32 {
|
||||||
pad := ctx.Style().Dimensions.TextPadding
|
pad := l.ActualTextPadding(ctx)
|
||||||
bounds := l.bounds.Inset(pad)
|
bounds := pad.InsetRect(l.bounds)
|
||||||
switch l.TextAlignment {
|
switch l.TextAlignment {
|
||||||
case AlignRight:
|
case AlignRight:
|
||||||
return geom.PtF32(bounds.Max.X, bounds.Min.Y)
|
return geom.PtF32(bounds.Max.X, bounds.Min.Y)
|
||||||
@ -55,10 +63,15 @@ func (l *Label) getLabelTopLeft(ctx Context) geom.PointF32 {
|
|||||||
func (l *Label) Render(ctx Context) {
|
func (l *Label) Render(ctx Context) {
|
||||||
l.RenderBackground(ctx)
|
l.RenderBackground(ctx)
|
||||||
fontColor := l.TextColor(ctx)
|
fontColor := l.TextColor(ctx)
|
||||||
fontName := l.FontName(ctx)
|
font := l.ActualFont(ctx)
|
||||||
topLeft := l.getLabelTopLeft(ctx)
|
topLeft := l.getLabelTopLeft(ctx)
|
||||||
|
text := l.Text
|
||||||
|
availableWidth := l.bounds.Dx()
|
||||||
|
if l.TextOverflow == TextOverflowEllipsis {
|
||||||
|
text = fitTextEllipsis(font, text, availableWidth)
|
||||||
|
}
|
||||||
if l.DropShadow != nil {
|
if l.DropShadow != nil {
|
||||||
ctx.Fonts().TextAlign(fontName, topLeft.Add2D(1, 1), l.DropShadow, l.Text, l.TextAlignment)
|
ctx.Renderer().TextAlign(font, topLeft.Add2D(1, 1), l.DropShadow, text, l.TextAlignment)
|
||||||
}
|
}
|
||||||
ctx.Fonts().TextAlign(fontName, topLeft, fontColor, l.Text, l.TextAlignment)
|
ctx.Renderer().TextAlign(font, topLeft, fontColor, text, l.TextAlignment)
|
||||||
}
|
}
|
||||||
|
45
ui/length.go
45
ui/length.go
@ -24,9 +24,54 @@ func Fixed(l float32) *Length { return &Length{l} }
|
|||||||
|
|
||||||
func Infinite() *Length { return &Length{geom.NaN32()} }
|
func Infinite() *Length { return &Length{geom.NaN32()} }
|
||||||
|
|
||||||
|
func ZL() *Length { return &Length{0} }
|
||||||
|
|
||||||
type SideLengths struct {
|
type SideLengths struct {
|
||||||
Left *Length
|
Left *Length
|
||||||
Top *Length
|
Top *Length
|
||||||
Right *Length
|
Right *Length
|
||||||
Bottom *Length
|
Bottom *Length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l SideLengths) InsetRect(r geom.RectangleF32) geom.RectangleF32 {
|
||||||
|
return Sides{
|
||||||
|
Left: l.Left.Value(),
|
||||||
|
Top: l.Top.Value(),
|
||||||
|
Right: l.Right.Value(),
|
||||||
|
Bottom: l.Bottom.Value(),
|
||||||
|
}.InsetRect(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l SideLengths) Zero(value float32) Sides {
|
||||||
|
return Sides{
|
||||||
|
Left: l.Left.Zero(value),
|
||||||
|
Top: l.Top.Zero(value),
|
||||||
|
Right: l.Right.Zero(value),
|
||||||
|
Bottom: l.Bottom.Zero(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sides struct {
|
||||||
|
Left float32
|
||||||
|
Top float32
|
||||||
|
Right float32
|
||||||
|
Bottom float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Sides) InsetRect(r geom.RectangleF32) geom.RectangleF32 {
|
||||||
|
if r.Dx() < (s.Left + s.Right) {
|
||||||
|
r.Min.X += r.Dx() * s.Left / (s.Left + s.Right)
|
||||||
|
r.Max.X = r.Min.X
|
||||||
|
} else {
|
||||||
|
r.Min.X += s.Left
|
||||||
|
r.Max.X -= s.Right
|
||||||
|
}
|
||||||
|
if r.Dy() < (s.Top + s.Bottom) {
|
||||||
|
r.Min.Y += r.Dy() * s.Top / (s.Top + s.Bottom)
|
||||||
|
r.Max.Y = r.Min.Y
|
||||||
|
} else {
|
||||||
|
r.Min.Y += s.Top
|
||||||
|
r.Max.Y -= s.Bottom
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
@ -11,6 +11,9 @@ type overflow struct {
|
|||||||
|
|
||||||
Background color.Color
|
Background color.Color
|
||||||
|
|
||||||
|
ClipHorizontal bool
|
||||||
|
ClipVertical bool
|
||||||
|
|
||||||
barWidth float32
|
barWidth float32
|
||||||
desired geom.PointF32
|
desired geom.PointF32
|
||||||
bounds geom.RectangleF32
|
bounds geom.RectangleF32
|
||||||
@ -26,18 +29,30 @@ type overflow struct {
|
|||||||
type ScrollControl interface {
|
type ScrollControl interface {
|
||||||
Control
|
Control
|
||||||
|
|
||||||
|
SetBackgroundColor(color.Color)
|
||||||
|
SetClipHorizontal(bool)
|
||||||
|
SetClipVertical(bool)
|
||||||
SetScrollbarColor(bar color.Color, hover color.Color)
|
SetScrollbarColor(bar color.Color, hover color.Color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildOverflow(content Control, build func(c ScrollControl)) ScrollControl {
|
||||||
|
o := &overflow{Proxy: Proxy{Content: content}}
|
||||||
|
o.hor = BuildScrollbar(OrientationHorizontal, func(*Scrollbar) {})
|
||||||
|
o.ver = BuildScrollbar(OrientationVertical, func(*Scrollbar) {})
|
||||||
|
if build != nil {
|
||||||
|
build(o)
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
func Overflow(content Control) ScrollControl {
|
func Overflow(content Control) ScrollControl {
|
||||||
return OverflowBackground(content, nil)
|
return OverflowBackground(content, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OverflowBackground(content Control, back color.Color) ScrollControl {
|
func OverflowBackground(content Control, back color.Color) ScrollControl {
|
||||||
var o = &overflow{Proxy: Proxy{Content: content}, Background: back}
|
return BuildOverflow(content, func(c ScrollControl) {
|
||||||
o.hor = BuildScrollbar(OrientationHorizontal, func(*Scrollbar) {})
|
c.SetBackgroundColor(back)
|
||||||
o.ver = BuildScrollbar(OrientationVertical, func(*Scrollbar) {})
|
})
|
||||||
return o
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
|
func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
|
||||||
@ -53,6 +68,8 @@ func (o *overflow) shouldScroll(bounds geom.RectangleF32) (hor bool, ver bool) {
|
|||||||
if hor && !ver {
|
if hor && !ver {
|
||||||
ver = scroll(size.Y+o.barWidth, bounds.Dy())
|
ver = scroll(size.Y+o.barWidth, bounds.Dy())
|
||||||
}
|
}
|
||||||
|
hor = hor && !o.ClipHorizontal
|
||||||
|
ver = ver && !o.ClipVertical
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +175,17 @@ func (o *overflow) Render(ctx Context) {
|
|||||||
bar.Render(ctx)
|
bar.Render(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
func (o *overflow) SetBackgroundColor(c color.Color) {
|
||||||
|
o.Background = c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *overflow) SetClipHorizontal(clip bool) {
|
||||||
|
o.ClipHorizontal = clip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *overflow) SetClipVertical(clip bool) {
|
||||||
|
o.ClipVertical = clip
|
||||||
|
}
|
||||||
|
|
||||||
func (o *overflow) SetScrollbarColor(bar color.Color, hover color.Color) {
|
func (o *overflow) SetScrollbarColor(bar color.Color, hover color.Color) {
|
||||||
o.hor.BarColor = bar
|
o.hor.BarColor = bar
|
||||||
|
@ -26,11 +26,11 @@ func BuildParagraph(text string, fn func(*Paragraph)) *Paragraph {
|
|||||||
func fastFormatFloat32(f float32) string { return strconv.FormatFloat(float64(f), 'b', 32, 32) }
|
func fastFormatFloat32(f float32) string { return strconv.FormatFloat(float64(f), 'b', 32, 32) }
|
||||||
|
|
||||||
func (p *Paragraph) desiredSize(ctx Context) interface{} {
|
func (p *Paragraph) desiredSize(ctx Context) interface{} {
|
||||||
font := p.Font_(ctx)
|
font := p.ActualFont(ctx)
|
||||||
|
|
||||||
pad := ctx.Style().Dimensions.TextPadding
|
pad := p.ActualTextPadding(ctx)
|
||||||
lines := p.splitInLines(ctx, p.width-2*pad)
|
lines := p.splitInLines(ctx, p.width-pad.Left-pad.Right)
|
||||||
return geom.PtF32(p.width, float32(len(lines))*font.Height()+2*pad)
|
return geom.PtF32(p.width, float32(len(lines))*font.Height()+pad.Top+pad.Bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Paragraph) hashTextArranged(ctx Context) string {
|
func (p *Paragraph) hashTextArranged(ctx Context) string {
|
||||||
@ -42,38 +42,7 @@ func (p *Paragraph) hashTextDesired(ctx Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
|
func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
|
||||||
font := p.Font_(ctx)
|
font := p.ActualFont(ctx)
|
||||||
|
|
||||||
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
|
var lines []string
|
||||||
for _, line := range strings.Split(p.Text, "\n") {
|
for _, line := range strings.Split(p.Text, "\n") {
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
@ -82,7 +51,7 @@ func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for len(line) > 0 {
|
for len(line) > 0 {
|
||||||
clipped := fit(line)
|
clipped := fitTextWord(font, line, width)
|
||||||
lines = append(lines, clipped)
|
lines = append(lines, clipped)
|
||||||
line = strings.TrimLeft(line[len(clipped):], " ")
|
line = strings.TrimLeft(line[len(clipped):], " ")
|
||||||
}
|
}
|
||||||
@ -91,8 +60,8 @@ func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Paragraph) updateLines(ctx Context) interface{} {
|
func (p *Paragraph) updateLines(ctx Context) interface{} {
|
||||||
pad := ctx.Style().Dimensions.TextPadding
|
pad := p.ActualTextPadding(ctx)
|
||||||
return p.splitInLines(ctx, p.Bounds().Dx()-2*pad)
|
return p.splitInLines(ctx, p.Bounds().Dx()-pad.Left-pad.Right)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Paragraph) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
|
func (p *Paragraph) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
|
||||||
@ -104,14 +73,14 @@ func (p *Paragraph) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
|
|||||||
func (p *Paragraph) Render(ctx Context) {
|
func (p *Paragraph) Render(ctx Context) {
|
||||||
p.RenderBackground(ctx)
|
p.RenderBackground(ctx)
|
||||||
|
|
||||||
pad := ctx.Style().Dimensions.TextPadding
|
pad := p.ActualTextPadding(ctx)
|
||||||
width := p.Bounds().Dx() - 2*pad
|
width := p.Bounds().Dx() - pad.Left - pad.Right
|
||||||
lines := p.lines.GetContext(ctx, p.updateLines, p.hashTextArranged).([]string)
|
lines := p.lines.GetContext(ctx, p.updateLines, p.hashTextArranged).([]string)
|
||||||
|
|
||||||
fontColor := p.TextColor(ctx)
|
fontColor := p.TextColor(ctx)
|
||||||
fontName := p.FontName(ctx)
|
fontName := p.FontName(ctx)
|
||||||
fontHeight := ctx.Fonts().Font(fontName).Height()
|
fontHeight := ctx.Fonts().Font(fontName).Height()
|
||||||
bounds := p.bounds.Inset(pad)
|
bounds := pad.InsetRect(p.bounds)
|
||||||
|
|
||||||
left := bounds.Min.X
|
left := bounds.Min.X
|
||||||
switch p.TextAlignment {
|
switch p.TextAlignment {
|
||||||
|
@ -42,7 +42,9 @@ func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.
|
|||||||
height = remainder / float32(stretch)
|
height = remainder / float32(stretch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var child = geom.RectF32(bounds.Min.X, bounds.Min.Y+childOffset, bounds.Max.X, bounds.Min.Y+childOffset+height)
|
minY := geom.Min32(bounds.Max.Y, bounds.Min.Y+childOffset)
|
||||||
|
maxY := geom.Min32(bounds.Max.Y, bounds.Min.Y+childOffset+height)
|
||||||
|
var child = geom.RectF32(bounds.Min.X, minY, bounds.Max.X, maxY)
|
||||||
p.Children[i].Arrange(ctx, p.Orientation.FlipRect(child), offset, p)
|
p.Children[i].Arrange(ctx, p.Orientation.FlipRect(child), offset, p)
|
||||||
childOffset += height
|
childOffset += height
|
||||||
}
|
}
|
||||||
|
68
ui/text.go
Normal file
68
ui/text.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// findOptimalFit tries to find the optimal (largest) fit for the interval [0..n] by doing a binary search.
|
||||||
|
func findOptimalFit(n int, fits func(i int) bool) int {
|
||||||
|
if n < 0 || fits(n) {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
min, max := 0, n
|
||||||
|
for {
|
||||||
|
if min == max {
|
||||||
|
if min == 0 && !fits(min) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
middle := (max + min + 1) / 2
|
||||||
|
if fits(middle) {
|
||||||
|
min = middle
|
||||||
|
} else {
|
||||||
|
max = middle - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSpaces creates a slice with indices where spaces can be found in string s
|
||||||
|
func findSpaces(s string) []int {
|
||||||
|
var spaces []int
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
space := strings.Index(s[offset:], " ")
|
||||||
|
if space == -1 {
|
||||||
|
return spaces
|
||||||
|
}
|
||||||
|
offset += space
|
||||||
|
spaces = append(spaces, offset)
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fitTextEllipsis(font Font, text string, availableWidth float32) string {
|
||||||
|
if font.WidthOf(text) <= availableWidth {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
ellipsis := "..."
|
||||||
|
availableWidth -= font.WidthOf(ellipsis)
|
||||||
|
cut := findOptimalFit(len(text), func(i int) bool { return font.WidthOf(text[:i]) <= availableWidth })
|
||||||
|
if cut == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return text[:cut] + ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
// fitTextWord tries to fit as much of string s in width space.
|
||||||
|
func fitTextWord(font Font, s string, availableWidth float32) string {
|
||||||
|
if font.WidthOf(s) < availableWidth {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
spaces := findSpaces(s)
|
||||||
|
split := findOptimalFit(len(spaces)-1, func(i int) bool {
|
||||||
|
return font.WidthOf(s[:spaces[i]]) <= availableWidth
|
||||||
|
})
|
||||||
|
if split == -1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:spaces[split]]
|
||||||
|
}
|
@ -49,21 +49,18 @@ func BuildTextBox(fn func(*TextBox)) *TextBox {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TextBox) pad(ctx Context) float32 {
|
|
||||||
return ctx.Style().Dimensions.TextPadding
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *TextBox) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
|
func (b *TextBox) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) {
|
||||||
b.ControlBase.Arrange(ctx, bounds, offset, parent)
|
b.ControlBase.Arrange(ctx, bounds, offset, parent)
|
||||||
b.box.Arrange(ctx, bounds.Inset(b.pad(ctx)), offset, b)
|
pad := b.ActualTextPadding(ctx)
|
||||||
|
b.box.Arrange(ctx, pad.InsetRect(bounds), offset, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TextBox) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
|
func (b *TextBox) DesiredSize(ctx Context, _ geom.PointF32) geom.PointF32 {
|
||||||
font := b.Font_(ctx)
|
font := b.ActualFont(ctx)
|
||||||
var width = font.WidthOf(b.Text)
|
var width = font.WidthOf(b.Text)
|
||||||
var height = font.Height()
|
var height = font.Height()
|
||||||
var pad = b.pad(ctx)
|
pad := b.ActualTextPadding(ctx)
|
||||||
return geom.PtF32(width+pad*2, height+pad*2)
|
return geom.PtF32(pad.Left+width+pad.Right, pad.Top+height+pad.Bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *TextBox) TextChanged() *Events { return &b.textChanged }
|
func (b *TextBox) TextChanged() *Events { return &b.textChanged }
|
||||||
@ -71,7 +68,7 @@ func (b *TextBox) TextChanged() *Events { return &b.textChanged }
|
|||||||
func (b *TextBox) mousePosToCaretPos(ctx Context, e MouseEvent) int {
|
func (b *TextBox) mousePosToCaretPos(ctx Context, e MouseEvent) int {
|
||||||
p := b.ToControlPosition(e.Pos())
|
p := b.ToControlPosition(e.Pos())
|
||||||
offset := p.X - b.box.bounds.Min.X
|
offset := p.X - b.box.bounds.Min.X
|
||||||
f := b.Font_(ctx)
|
f := b.ActualFont(ctx)
|
||||||
var carets = [3]int{0, 0, len(b.Text)}
|
var carets = [3]int{0, 0, len(b.Text)}
|
||||||
var offsets = [3]float32{0, 0, f.WidthOf(b.Text)}
|
var offsets = [3]float32{0, 0, f.WidthOf(b.Text)}
|
||||||
var updateCenter = func() {
|
var updateCenter = func() {
|
||||||
@ -262,7 +259,7 @@ func (b *TextBox) Render(ctx Context) {
|
|||||||
back = ctx.Style().Palette.Background
|
back = ctx.Style().Palette.Background
|
||||||
}
|
}
|
||||||
renderer.Clear(back)
|
renderer.Clear(back)
|
||||||
font := b.Font_(ctx)
|
font := b.ActualFont(ctx)
|
||||||
if b.Selection.Start != b.Selection.End {
|
if b.Selection.Start != b.Selection.End {
|
||||||
left, right := font.WidthOf(b.Text[:b.Selection.Start]), font.WidthOf(b.Text[:b.Selection.End])
|
left, right := font.WidthOf(b.Text[:b.Selection.Start]), font.WidthOf(b.Text[:b.Selection.End])
|
||||||
renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight)
|
renderer.FillRectangle(geom.RectF32(left, 0, right, size.Y), style.Palette.PrimaryHighlight)
|
||||||
|
Loading…
Reference in New Issue
Block a user