Layout: review r2 fixes
This commit is contained in:
7 files changed
+105
-30
No files matched your search
+7
-6
@@ -1,6 +1,10 @@
|
|||||||
package mujica
|
package mujica
|
||||||
|
|
||||||
import "github.com/egoist/mygo/ui"
|
import (
|
||||||
|
"github.com/egoist/mygo/ui"
|
||||||
|
|
||||||
|
"mujicaui/internal"
|
||||||
|
)
|
||||||
|
|
||||||
// AspectRatioOptions configure an AspectRatio.
|
// AspectRatioOptions configure an AspectRatio.
|
||||||
type AspectRatioOptions struct {
|
type AspectRatioOptions struct {
|
||||||
@@ -22,6 +26,7 @@ func AspectRatio(c *ui.Context, opts AspectRatioOptions, child func()) *ui.Eleme
|
|||||||
}
|
}
|
||||||
outer := ui.Box(c).Fill().Center()
|
outer := ui.Box(c).Fill().Center()
|
||||||
room := outer.Bounds()
|
room := outer.Bounds()
|
||||||
|
internal.Remeasure(c, outer, room.W, room.H)
|
||||||
return outer.Children(func() {
|
return outer.Children(func() {
|
||||||
frame := ui.Box(c).AspectRatio(r).Shrink(0).Center().Clip()
|
frame := ui.Box(c).AspectRatio(r).Shrink(0).Center().Clip()
|
||||||
was := frame.Bounds()
|
was := frame.Bounds()
|
||||||
@@ -31,12 +36,8 @@ func AspectRatio(c *ui.Context, opts AspectRatioOptions, child func()) *ui.Eleme
|
|||||||
w = min(w, room.H*r)
|
w = min(w, room.H*r)
|
||||||
}
|
}
|
||||||
if w <= 0 {
|
if w <= 0 {
|
||||||
// Not laid out yet: lay out once, then size from the room.
|
// Not laid out yet: Remeasure rebuilds once the room is known.
|
||||||
frame.WidthPercent(100)
|
frame.WidthPercent(100)
|
||||||
if first := ui.Local(outer, "aspect_ratio.first", func() bool { return true }); *first {
|
|
||||||
*first = false
|
|
||||||
c.Invalidate()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
frame.Width(w)
|
frame.Width(w)
|
||||||
if w-was.W > 0.5 || was.W-w > 0.5 {
|
if w-was.W > 0.5 || was.W-w > 0.5 {
|
||||||
|
|||||||
@@ -62,12 +62,10 @@ func TestAspectRatioFollowsWindowResize(t *testing.T) {
|
|||||||
t.Fatalf("short window: %vx%v, want 200x100", box.W, box.H)
|
t.Fatalf("short window: %vx%v, want 200x100", box.W, box.H)
|
||||||
}
|
}
|
||||||
tt.SetSize(400, 400)
|
tt.SetSize(400, 400)
|
||||||
tt.Frame()
|
|
||||||
if box.W != 400 || box.H != 200 {
|
if box.W != 400 || box.H != 200 {
|
||||||
t.Fatalf("tall window: %vx%v, want 400x200", box.W, box.H)
|
t.Fatalf("tall window: %vx%v, want 400x200", box.W, box.H)
|
||||||
}
|
}
|
||||||
tt.SetSize(300, 400)
|
tt.SetSize(300, 400)
|
||||||
tt.Frame()
|
|
||||||
if box.W != 300 || box.H != 150 {
|
if box.W != 300 || box.H != 150 {
|
||||||
t.Fatalf("narrow window: %vx%v, want 300x150", box.W, box.H)
|
t.Fatalf("narrow window: %vx%v, want 300x150", box.W, box.H)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "github.com/egoist/mygo/ui"
|
||||||
|
|
||||||
|
// Remeasure rebuilds the view when layout gives e another size than the w×h it was built for.
|
||||||
|
func Remeasure(c *ui.Context, e *ui.Element, w, h float32) *ui.Element {
|
||||||
|
return e.DrawOver(func(_ *ui.Painter, r ui.Rect) {
|
||||||
|
if abs(r.W-w) > 0.5 || abs(r.H-h) > 0.5 {
|
||||||
|
c.Invalidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(v float32) float32 { return max(v, -v) }
|
||||||
+4
-6
@@ -96,9 +96,11 @@ func SplitPane(c *ui.Context, state *SplitPaneState, opts SplitPaneOptions, firs
|
|||||||
func splitPaneBuild(c *ui.Context, in *ui.Element, state *SplitPaneState, opts SplitPaneOptions, lo, minSecond float32, res *SplitPaneResult, first, second func()) {
|
func splitPaneBuild(c *ui.Context, in *ui.Element, state *SplitPaneState, opts SplitPaneOptions, lo, minSecond float32, res *SplitPaneResult, first, second func()) {
|
||||||
k := Tokens(c)
|
k := Tokens(c)
|
||||||
u := Density(c).Unit()
|
u := Density(c).Unit()
|
||||||
total := in.Bounds().W
|
was := in.Bounds()
|
||||||
|
internal.Remeasure(c, in, was.W, was.H)
|
||||||
|
total := was.W
|
||||||
if opts.Vertical {
|
if opts.Vertical {
|
||||||
total = in.Bounds().H
|
total = was.H
|
||||||
}
|
}
|
||||||
hi := max(lo, total-theme.BorderWidth-minSecond)
|
hi := max(lo, total-theme.BorderWidth-minSecond)
|
||||||
clamp := func(v float32) float32 { return max(lo, min(v, hi)) }
|
clamp := func(v float32) float32 { return max(lo, min(v, hi)) }
|
||||||
@@ -106,10 +108,6 @@ func splitPaneBuild(c *ui.Context, in *ui.Element, state *SplitPaneState, opts S
|
|||||||
state.Size = clamp((total - theme.BorderWidth) / 2)
|
state.Size = clamp((total - theme.BorderWidth) / 2)
|
||||||
} else if total > 0 {
|
} else if total > 0 {
|
||||||
state.Size = clamp(state.Size)
|
state.Size = clamp(state.Size)
|
||||||
} else if once := ui.Local(in, "split_pane.first", func() bool { return true }); *once {
|
|
||||||
// Not laid out yet: measure, then size in the next frame.
|
|
||||||
*once = false
|
|
||||||
c.Invalidate()
|
|
||||||
}
|
}
|
||||||
resize := func(v float32) {
|
resize := func(v float32) {
|
||||||
if v = clamp(v); v != state.Size {
|
if v = clamp(v); v != state.Size {
|
||||||
|
|||||||
@@ -211,3 +211,16 @@ func TestSplitPaneSizesInsideBorder(t *testing.T) {
|
|||||||
t.Fatalf("drag to x=201 gave %v, want 200", st.Size)
|
t.Fatalf("drag to x=201 gave %v, want 200", st.Size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSplitPaneClampsOnResizeAlone(t *testing.T) {
|
||||||
|
st := &SplitPaneState{Size: 299}
|
||||||
|
tt := ui.NewTester(func(c *ui.Context) {
|
||||||
|
Use(c, Settings{})
|
||||||
|
SplitPane(c, st, SplitPaneOptions{}, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }).Fill()
|
||||||
|
}, 400, 200)
|
||||||
|
tt.SetSize(250, 200)
|
||||||
|
// 250 less the divider and the 40 DIP second minimum.
|
||||||
|
if r, _ := tt.Find("Editor"); st.Size != 209 || r.X != 210 {
|
||||||
|
t.Fatalf("after resize: size %v, second pane at %v; want 209 and 210", st.Size, r.X)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-12
@@ -65,7 +65,9 @@ func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult {
|
|||||||
e := ui.Row(c).MinHeight(max(theme.MinHitSize, d.ControlHeight()-8)).Shrink(0).PaddingX(d.Unit()*2).Gap(gap).
|
e := ui.Row(c).MinHeight(max(theme.MinHitSize, d.ControlHeight()-8)).Shrink(0).PaddingX(d.Unit()*2).Gap(gap).
|
||||||
Background(k.Surface).BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border).
|
Background(k.Surface).BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border).
|
||||||
Label(msg(c, "status_bar.label"))
|
Label(msg(c, "status_bar.label"))
|
||||||
hidden := statusBarHide(c, e, opts, e.Bounds().W-d.Unit()*4, gap)
|
was := e.Bounds()
|
||||||
|
internal.Remeasure(c, e, was.W, was.H)
|
||||||
|
hidden := statusBarHide(c, opts, was.W-d.Unit()*4, gap)
|
||||||
open := ui.Local(e, "status_bar.open", func() bool { return false })
|
open := ui.Local(e, "status_bar.open", func() bool { return false })
|
||||||
*open = *open && len(hidden) > 0
|
*open = *open && len(hidden) > 0
|
||||||
var res StatusBarResult
|
var res StatusBarResult
|
||||||
@@ -76,8 +78,8 @@ func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult {
|
|||||||
slog.Debug("mujica: status_bar clicked", "id", it.ID)
|
slog.Debug("mujica: status_bar clicked", "id", it.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
group := func(items []StatusItem) {
|
group := func(key string, items []StatusItem) {
|
||||||
ui.Row(c).Gap(gap).Shrink(0).Dividers(theme.BorderWidth, k.Border).Children(func() {
|
ui.Row(c).Key(key).Gap(gap).Shrink(0).Dividers(theme.BorderWidth, k.Border).Children(func() {
|
||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
if !hidden[it.ID] {
|
if !hidden[it.ID] {
|
||||||
item(it)
|
item(it)
|
||||||
@@ -86,8 +88,8 @@ func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
e.Children(func() {
|
e.Children(func() {
|
||||||
group(opts.Left)
|
group("status_bar.left", opts.Left)
|
||||||
ui.Spacer(c)
|
ui.Spacer(c).Key("status_bar.spacer")
|
||||||
if len(hidden) > 0 {
|
if len(hidden) > 0 {
|
||||||
more := statusBarMore(c, len(hidden))
|
more := statusBarMore(c, len(hidden))
|
||||||
if more.Clicked() {
|
if more.Clicked() {
|
||||||
@@ -107,7 +109,7 @@ func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
group(opts.Right)
|
group("status_bar.right", opts.Right)
|
||||||
})
|
})
|
||||||
res.Element = e
|
res.Element = e
|
||||||
return res
|
return res
|
||||||
@@ -152,14 +154,10 @@ func statusBarCheck(opts StatusBarOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// statusBarHide returns the secondary items to hide, last first, so the rest fit in room DIPs.
|
// statusBarHide returns the secondary items to hide, last first, so the rest fit in room DIPs.
|
||||||
func statusBarHide(c *ui.Context, bar *ui.Element, opts StatusBarOptions, room, gap float32) map[string]bool {
|
func statusBarHide(c *ui.Context, opts StatusBarOptions, room, gap float32) map[string]bool {
|
||||||
hidden := map[string]bool{}
|
hidden := map[string]bool{}
|
||||||
if room <= 0 {
|
if room <= 0 {
|
||||||
// Not laid out yet: show all, and measure in the next frame.
|
// Not laid out yet: show all; Remeasure rebuilds once measured.
|
||||||
if once := ui.Local(bar, "status_bar.first", func() bool { return true }); *once {
|
|
||||||
*once = false
|
|
||||||
c.Invalidate()
|
|
||||||
}
|
|
||||||
return hidden
|
return hidden
|
||||||
}
|
}
|
||||||
u := Density(c).Unit()
|
u := Density(c).Unit()
|
||||||
|
|||||||
+57
-4
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/egoist/mygo/ui"
|
"github.com/egoist/mygo/ui"
|
||||||
|
|
||||||
"mujicaui/icons"
|
"mujicaui/icons"
|
||||||
|
"mujicaui/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
func statusBarSample() StatusBarOptions {
|
func statusBarSample() StatusBarOptions {
|
||||||
@@ -71,7 +72,6 @@ func TestStatusBarCollapsesSecondary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tt.SetSize(330, 100)
|
tt.SetSize(330, 100)
|
||||||
tt.Frame()
|
|
||||||
for _, s := range []string{"Ln 12, Col 4", "UTF-8", "2 warnings"} {
|
for _, s := range []string{"Ln 12, Col 4", "UTF-8", "2 warnings"} {
|
||||||
if tt.HasText(s) {
|
if tt.HasText(s) {
|
||||||
t.Fatalf("narrow bar still shows secondary %q; texts %q", s, tt.Texts())
|
t.Fatalf("narrow bar still shows secondary %q; texts %q", s, tt.Texts())
|
||||||
@@ -85,21 +85,18 @@ func TestStatusBarCollapsesSecondary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Medium width drops only the last secondary items.
|
// Medium width drops only the last secondary items.
|
||||||
tt.SetSize(400, 100)
|
tt.SetSize(400, 100)
|
||||||
tt.Frame()
|
|
||||||
if !tt.HasText("2 warnings") || tt.HasText("Ln 12, Col 4") {
|
if !tt.HasText("2 warnings") || tt.HasText("Ln 12, Col 4") {
|
||||||
t.Fatalf("medium bar hid the wrong items: %q", tt.Texts())
|
t.Fatalf("medium bar hid the wrong items: %q", tt.Texts())
|
||||||
}
|
}
|
||||||
// At every width the visible items stay inside the bar.
|
// At every width the visible items stay inside the bar.
|
||||||
for w := 320; w <= 800; w += 10 {
|
for w := 320; w <= 800; w += 10 {
|
||||||
tt.SetSize(w, 100)
|
tt.SetSize(w, 100)
|
||||||
tt.Frame()
|
|
||||||
bell, ok := tt.Find("Notifications")
|
bell, ok := tt.Find("Notifications")
|
||||||
if !ok || bell.X+bell.W > float32(w)-8+0.5 || bell.W < 28 {
|
if !ok || bell.X+bell.W > float32(w)-8+0.5 || bell.W < 28 {
|
||||||
t.Fatalf("width %d: action at %+v", w, bell)
|
t.Fatalf("width %d: action at %+v", w, bell)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tt.SetSize(800, 100)
|
tt.SetSize(800, 100)
|
||||||
tt.Frame()
|
|
||||||
if !tt.HasText("UTF-8") {
|
if !tt.HasText("UTF-8") {
|
||||||
t.Fatal("secondary items did not come back when wide")
|
t.Fatal("secondary items did not come back when wide")
|
||||||
}
|
}
|
||||||
@@ -233,3 +230,59 @@ func TestStatusBarSnapshot(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStatusBarFoldsOnResizeAlone(t *testing.T) {
|
||||||
|
tt := ui.NewTester(func(c *ui.Context) {
|
||||||
|
Use(c, Settings{})
|
||||||
|
StatusBar(c, statusBarSample())
|
||||||
|
}, 400, 100)
|
||||||
|
tt.SetSize(160, 100)
|
||||||
|
for _, s := range []string{"2 warnings", "UTF-8", "Ln 12, Col 4"} {
|
||||||
|
if tt.HasText(s) {
|
||||||
|
t.Fatalf("160 DIP bar still shows %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !tt.HasText("3 more items") {
|
||||||
|
t.Fatalf("no hidden count: %q", tt.Texts())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusBarKeepsFocusAcrossDensity(t *testing.T) {
|
||||||
|
density := theme.Default
|
||||||
|
n := 0
|
||||||
|
// At 155 DIP the secondary item fits at Default and folds into "+1" at Comfortable.
|
||||||
|
o := StatusBarOptions{
|
||||||
|
Left: []StatusItem{{ID: "main", Text: "main"}, {ID: "enc", Text: "UTF-8", Secondary: true}},
|
||||||
|
Right: []StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}},
|
||||||
|
}
|
||||||
|
tt := ui.NewTester(func(c *ui.Context) {
|
||||||
|
Use(c, Settings{Density: density})
|
||||||
|
if StatusBar(c, o).Clicked == "bell" {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}, 155, 100)
|
||||||
|
if !tt.HasText("UTF-8") {
|
||||||
|
t.Fatal("secondary item folded at Default density")
|
||||||
|
}
|
||||||
|
for range 4 {
|
||||||
|
if tt.Focused("Notifications") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tt.Key(0, ui.KeyTab)
|
||||||
|
}
|
||||||
|
if !tt.Focused("Notifications") {
|
||||||
|
t.Fatal("Tab never reached Notifications")
|
||||||
|
}
|
||||||
|
density = theme.Comfortable
|
||||||
|
tt.Frame()
|
||||||
|
if tt.HasText("UTF-8") || !tt.HasText("1 more items") {
|
||||||
|
t.Fatalf("Comfortable did not fold the secondary item: %q", tt.Texts())
|
||||||
|
}
|
||||||
|
if !tt.Focused("Notifications") {
|
||||||
|
t.Fatal("density switch lost the focus")
|
||||||
|
}
|
||||||
|
tt.Key(0, ui.KeyEnter)
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("Enter after the switch activated %d times, want 1", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user