From cfc0bb003bb215809710e64e41f6d7aded1ea22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:32:23 -0400 Subject: [PATCH] WIP essentials 13 --- dock_panel.go | 193 ++++++++++++++++++ dock_panel_test.go | 102 +++++++++ messages_essentials.go | 6 + testdata/snapshots/darwin/dock_panel-dark.png | Bin 0 -> 13872 bytes .../snapshots/darwin/dock_panel-light.png | Bin 0 -> 13699 bytes 5 files changed, 301 insertions(+) create mode 100644 dock_panel.go create mode 100644 dock_panel_test.go create mode 100644 testdata/snapshots/darwin/dock_panel-dark.png create mode 100644 testdata/snapshots/darwin/dock_panel-light.png diff --git a/dock_panel.go b/dock_panel.go new file mode 100644 index 0000000..8d34e49 --- /dev/null +++ b/dock_panel.go @@ -0,0 +1,193 @@ +package mujica + +import ( + "fmt" + "log/slog" + "slices" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// DockZone is an area of a DockPanel. +type DockZone int + +const ( + DockCenter DockZone = iota + DockLeft + DockRight + DockBottom +) + +// DockLayout is the caller-held arrangement: panel IDs per zone in tab order, and each zone's shown tab. +type DockLayout struct { + Zones [4][]string + Active [4]string +} + +// DockItem is one dockable panel. +type DockItem struct { + ID, Title string + Build func() +} + +// DockPanelOptions configure a DockPanel. +type DockPanelOptions struct { + Disabled bool +} + +// dockPanelDrag is a tab being dragged. +type dockPanelDrag struct{ id string } + +// DockPanel arranges panels in left, center, right and bottom zones; tabs drag to an edge or zone to dock there. +func DockPanel(c *ui.Context, layout *DockLayout, items []DockItem, opts DockPanelOptions) ChoiceResult { + if layout == nil || len(items) == 0 { + panic("mujica: DockPanel needs a layout and items") + } + byID := map[string]DockItem{} + for _, it := range items { + if it.ID == "" || it.Title == "" || it.Build == nil { + panic("mujica: DockPanel item needs an ID, Title and Build") + } + if _, dup := byID[it.ID]; dup { + panic("mujica: DockPanel item ID repeated: " + it.ID) + } + byID[it.ID] = it + } + placed := map[string]bool{} + for z, ids := range layout.Zones { + for _, id := range ids { + if _, ok := byID[id]; !ok || placed[id] { + panic(fmt.Sprintf("mujica: DockPanel zone %d holds unknown or repeated %q", z, id)) + } + placed[id] = true + } + } + changed := false + for _, it := range items { + if !placed[it.ID] { + layout.Zones[DockCenter] = append(layout.Zones[DockCenter], it.ID) + changed = true + } + } + for z, ids := range layout.Zones { + if len(ids) > 0 && !slices.Contains(ids, layout.Active[z]) { + layout.Active[z] = ids[0] + } + } + k := Tokens(c) + u := Density(c).Unit() + root := ui.Box(c).Disabled(opts.Disabled).MinHeight(0) + disabled := root.IsDisabled() + // follow is the tab that moved; it keeps the focus in its new zone. + follow := ui.Local(root, "dock_panel.follow", func() string { return "" }) + move := func(id string, to DockZone) { + *follow = id + for z := range layout.Zones { + layout.Zones[z] = slices.DeleteFunc(slices.Clone(layout.Zones[z]), func(s string) bool { return s == id }) + } + layout.Zones[to] = append(layout.Zones[to], id) + layout.Active[to] = id + changed = true + slog.Debug("mujica: dock_panel docked", "panel", id, "zone", to) + } + dragging := false + root.Children(func() { + ui.Column(c).Fill().Children(func() { + ui.Row(c).Grow(1).MinHeight(0).AlignItems(ui.Stretch).Children(func() { + for _, z := range []DockZone{DockLeft, DockCenter, DockRight} { + if dockPanelZone(c, k, u, layout, byID, z, disabled, move, &changed, follow) { + dragging = true + } + } + }) + if dockPanelZone(c, k, u, layout, byID, DockBottom, disabled, move, &changed, follow) { + dragging = true + } + }) + // Edges stay one frame after the drag, when the drop arrives. + was := ui.Local(root, "dock_panel.drag", func() bool { return false }) + if (dragging || *was) && !disabled { + dockPanelEdges(c, k, u, move) + } + *was = dragging + }) + return ChoiceResult{root, changed} +} + +// dockPanelZone builds zone z's tabs and shown panel; it reports a tab of it being dragged. +func dockPanelZone(c *ui.Context, k theme.Tokens, u float32, layout *DockLayout, byID map[string]DockItem, z DockZone, disabled bool, move func(string, DockZone), changed *bool, follow *string) bool { + ids := layout.Zones[z] + if len(ids) == 0 { + return false + } + box := ui.Column(c).Key(z).MinWidth(0).MinHeight(0).Background(k.Surface).Clip().Role(ui.RoleGroup). + Label(msg(c, fmt.Sprintf("dock_panel.zone%d", z))) + switch z { + case DockLeft, DockRight: + box.Width(220).Shrink(0).Border(theme.BorderWidth, k.Border) + case DockBottom: + box.Height(160).Shrink(0).Border(theme.BorderWidth, k.Border) + default: + box.Grow(1) + } + if d, ok := ui.DragOver[dockPanelDrag](box); ok && !slices.Contains(ids, d.id) { + box.Border(2, k.AccentText) + } + if d, ok := ui.Drop[dockPanelDrag](box); ok && !disabled { + move(d.id, z) + } + dragging := false + box.Children(func() { + at := slices.Index(ids, layout.Active[z]) + base := ui.TabsBase(c, &at, len(ids)) + base.List.Gap(u).Padding(theme.FocusGap+theme.FocusWidth, u*2, 0, u*2).Children(func() { + for i, id := range ids { + tab := base.Tab(i).Drag(dockPanelDrag{id}) + tabsStyle(c, tab, Tab{Label: byID[id].Title}, i == at, false) + tab.Description(msg(c, "dock_panel.hint")) + if *follow == id { + tab.Focus() + *follow = "" + } + dragging = dragging || tab.Dragging() + for key, to := range map[ui.Key]DockZone{ui.KeyLeft: DockLeft, ui.KeyRight: DockRight, ui.KeyDown: DockBottom, ui.KeyUp: DockCenter} { + if tab.Shortcut(ui.Shift|ui.Alt, key) && !disabled && to != z { + move(id, to) + } + } + } + }) + if base.List.Changed() && !disabled && ids[at] != layout.Active[z] { + layout.Active[z], *changed = ids[at], true + } + ui.Box(c).FillWidth().Height(theme.BorderWidth).Background(k.Border).Role(ui.RoleNone) + ui.Scroll(c).Grow(1).MinHeight(0).Padding(u * 3).Children(byID[layout.Active[z]].Build) + }) + return dragging +} + +// dockPanelEdges shows drop strips on the four edges while a tab is dragged. +func dockPanelEdges(c *ui.Context, k theme.Tokens, u float32, move func(string, DockZone)) { + for _, z := range []DockZone{DockLeft, DockRight, DockBottom, DockCenter} { + e := ui.Box(c).Key(fmt.Sprint("edge", z)).Absolute().Role(ui.RoleNone) + switch z { + case DockLeft: + e.Left(0).Top(0).Bottom(0).Width(12 * u) + case DockRight: + e.Right(0).Top(0).Bottom(0).Width(12 * u) + case DockBottom: + e.Left(12 * u).Right(12 * u).Bottom(0).Height(10 * u) + default: + e.Left(0).Right(0).Top(0).Height(4 * u) + } + _, over := ui.DragOver[dockPanelDrag](e) + if over { + e.Background(k.Selection).Border(2, k.AccentText) + } + if d, ok := ui.Drop[dockPanelDrag](e); ok { + move(d.id, z) + } + } +} diff --git a/dock_panel_test.go b/dock_panel_test.go new file mode 100644 index 0000000..20b5aa0 --- /dev/null +++ b/dock_panel_test.go @@ -0,0 +1,102 @@ +package mujica + +import ( + "slices" + "testing" + + "github.com/egoist/mygo/ui" +) + +func dockItems(c *ui.Context) []DockItem { + var out []DockItem + for _, id := range []string{"files", "editor", "terminal"} { + out = append(out, DockItem{ID: id, Title: "Panel " + id, Build: func() { ui.Text(c, "Body of "+id) }}) + } + return out +} + +func dockTester(l *DockLayout, dis *bool, n *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := DockPanel(c, l, dockItems(c), DockPanelOptions{Disabled: *dis}) + r.Element.Size(800, 500) + if r.Changed() { + *n++ + } + }, 820, 520) +} + +func TestDockPanelTabsAndKeyboardDock(t *testing.T) { + var l DockLayout + dis, n := false, 0 + tt := dockTester(&l, &dis, &n) + if !slices.Equal(l.Zones[DockCenter], []string{"files", "editor", "terminal"}) || l.Active[DockCenter] != "files" || !tt.HasText("Body of files") { + t.Fatalf("default layout %+v", l) + } + if err := tt.Click("Panel editor"); err != nil { + t.Fatal(err) + } + if l.Active[DockCenter] != "editor" || !tt.HasText("Body of editor") { + t.Fatalf("tab click: %+v", l) + } + tt.Key(ui.Shift|ui.Alt, ui.KeyLeft) + if !slices.Equal(l.Zones[DockLeft], []string{"editor"}) || slices.Contains(l.Zones[DockCenter], "editor") { + t.Fatalf("Shift+Alt+Left: %+v", l) + } + if _, ok := tt.Find("Left dock"); !ok { + t.Fatal("left zone not shown") + } + tt.Key(ui.Shift|ui.Alt, ui.KeyUp) + if len(l.Zones[DockLeft]) != 0 || l.Active[DockCenter] != "editor" { + t.Fatalf("back to center: %+v", l) + } + before := n + dis = true + tt.Frame() + tt.Key(ui.Shift|ui.Alt, ui.KeyDown) + if len(l.Zones[DockBottom]) != 0 || n != before { + t.Fatalf("disabled docked: %+v", l) + } +} + +func TestDockPanelDragToEdge(t *testing.T) { + var l DockLayout + dis, n := false, 0 + tt := dockTester(&l, &dis, &n) + r, _ := tt.Find("Panel terminal") + x, y := r.X+r.W/2, r.Y+r.H/2 + tt.Press(x, y) + for _, px := range []float32{x + 10, x + 200, 600, 790} { + tt.Move(px, 250) + } + tt.Release(790, 250) + tt.Frame() + if !slices.Equal(l.Zones[DockRight], []string{"terminal"}) || n == 0 { + t.Fatalf("drag to the right edge: %+v", l) + } +} + +func TestDockPanelRejects(t *testing.T) { + for name, l := range map[string]DockLayout{ + "unknown": {Zones: [4][]string{{"nope"}}}, + "repeated": {Zones: [4][]string{{"files"}, {"files"}}}, + } { + func() { + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); DockPanel(c, &l, dockItems(c), DockPanelOptions{}) }, 300, 200) + }() + } +} + +func TestDockPanelSnapshot(t *testing.T) { + l := DockLayout{Zones: [4][]string{{"editor"}, {"files"}, nil, {"terminal"}}} + snapshot(t, "dock_panel", 640, 360, func(c *ui.Context) { + ui.Column(c).Padding(12).Children(func() { + DockPanel(c, &l, dockItems(c), DockPanelOptions{}).Element.Size(616, 336) + }) + }) +} diff --git a/messages_essentials.go b/messages_essentials.go index 618816a..5c9e9d2 100644 --- a/messages_essentials.go +++ b/messages_essentials.go @@ -30,6 +30,12 @@ func init() { "resizable_panel_group.divider": {"Resize %s and %s", "调整 %s 与 %s 的大小"}, + "dock_panel.zone0": {"Center dock", "中央停靠区"}, + "dock_panel.zone1": {"Left dock", "左侧停靠区"}, + "dock_panel.zone2": {"Right dock", "右侧停靠区"}, + "dock_panel.zone3": {"Bottom dock", "底部停靠区"}, + "dock_panel.hint": {"Drag to an edge, or Shift+Alt+arrow, to dock", "拖到边缘,或按 Shift+Alt+方向键停靠"}, + "qr_code.label": {"QR code", "二维码"}, "qr_code.too_long": {"Text too long for a QR code", "文字过长,无法生成二维码"}, }) diff --git a/testdata/snapshots/darwin/dock_panel-dark.png b/testdata/snapshots/darwin/dock_panel-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..cf54f97fda3b2de38553bb2ba999cbccd4812625 GIT binary patch literal 13872 zcmeHtbyQrOT`gi! z+^3S*}nQh7blA1`iq^|t1LZq-K$zX z+g7@31Qr0I0|sJ}7oK!;c3N<8n$`tUvxvct-yLr_(CfYRIMGeF;GqG-@3ofgh23ZP zf}L=+dE)r`I>Y71S$<$xi6jppb0^!_xZ4vljhE?V4XN!qPanOJRe3YnWfJ1cHa~Z~ z!l9dG)F_r*IJOfG6!G%q7;;H6OMzovPYFP4`=>OFXNK>U&}cs0cMalu5Fj!eubHC7 z91Y^jB1SRJ+V?5s#PqDoj_p0x1GRM)`Oz~BS>9j>2B5K^X{uFFjFEa=7hi*0y!(DR z?y+yejzNSY#R3RLJE1IcKW1>3Z_g1H&1z-7`vD7h zWX5Z)GIs~p3@`4qve}EpHXl~nkEZcbj#B$lAAnl3A$f_JVmj7~BgLlPZW;q5*F?TT z&DN1JYHIbHD{LmaU#eIw)7`_B#+9_wxh9CogZk(PT%+U`VVk287{qZqPySfj9rYx; z!2RoX-$raaVbu0M*i7xSq)blMn*`x)o$II3-_Y`Ye#qocyr@?_Dm+`h$b*e?dhD*@ zt!R3*Zx-|DQes?vv(Z%g5Y6R1;u!=C%a3-P-yE3%0siFd`S00JuRNl}o%cp%WXzdW zJ{71J!rz;m@(?Da_2q4BEwQk&QN}Ud+=0wv*<`b`Tg6Om)0q;P0Zhh44gG1Uz6d8;LG(qliA6OaZRR>y_~)T^Szay zb)8n}_xD=3>E8_Hv57J#^q<#oz2mwF3rkipmm^6qJm>Hj0eRT1hA`>3SZD^?%IwiX zE6QP8Ek?oz0|>Fwtq#Oh0Utr@XMmRfUY`sF-Tn zm#bBe*~YCTiL1!J#m*ONjb*m6KbVI-Gm|dp^XJGLhdX4x+5`e*!ieqRo69`JRog}C zQ7kjy8=v(v=me&m;1&T4=q3ZnajD4MAX3NnOt%Bkmu#P*4^x?6w>$0-0s$>OMP@nFfh01JcaQWmguyix8 zkDHVz(Wxkb?sGyfVFMTDXPMrY^Y31-2|%i87fI+|s;SWrUeh$j8t6327EadO!N;O= zs;HoN6f}0frp-80BCRAs^Rjy^>qpV*; zKegMrb$C8c$@}j4yrEX$*|HWS2v~ct>>t{z*W@s66E-lCT6Q!1o+Q*x?F*d(ILakz z%Bq@MP{6bMhgB0mR^urUX zGZalv24&Q@KZw4qYrPTsZtO_AkgitPC~sZ8$*mWDpxU+5;8tAbLT1=~$y$7Dp|jMG zo{p)zY&#fT)4`y(yYXB(>`MRz^ZVD3cTVoHg9zA>Kbg)fS|CF#w4m!W7TImsrv6JF zj0>Aft6vu^GbCpOWM35=EXrUij~6)B-d$;_*JiZlROm^vZ-b?Y$w!6xO3fTM`sUNs7eK%ZB{c+&`PlBijr*K-*nBRSUx4+Qa#<<4^Kh! zZ14{diUC;)2~;`Sd-lE@W3sfiEWByTYSlQRJd^s?&1^th+1>L5xSo zm&wQVo)5Y0h8(*WQJ3K|c+8i}5P#|eu&MP{WU$g(RbK($mmm!^q-@bpJ?Y}veZECr zTVzRg%a)%FauRc<9wZ+mbZE;NzZ};sx3hE&58H+u;Wf5@L|8mGaWMSy(uvRw#n@=* z(>AiisfTE3*5qEO{IfOyJ^*nIwZz@nCMKzJx7a731GVuvDt2S7B0!)XgpaA+nncIKj8O((n7 zwb409uOxE2xrN#cF49D!M1;pZS%aXrW$UqZb{4B2Q9VEN{;y z>D3ZjV@(ILuyLDpoW4Lt%pr_1@-ixgm#rWDE2vED`t03d(dmrVI|l@3Rb@~kU8dS~ ztIp+-#y>@X1(5r9vdssF6I|yHg4mypGRxRoT0OK+&NO}n5vA;Mn-KQeaGj%!e&AXp zq2BtGO=i0cGkbS(s8wnH(3r(b&Y2<5YjbDCis9=ubRwMHo|G7u*-UE<7b~|Ym-)=m zL|>l_y()emXOFt{w}Qi9r?`nsxf;FAndvDK-yLr9oQAC2ts4TUmFK$?8`sv{{WFcQ z2<)LxGdcr@RA!4Z2GAlALQ1`pPN*icz6^WVI*g~0h!=Q@AxVs z0I3Zy_Y)zb$g7u>z)w$maIVzGmB@vQ*4XIfF<%|4dA7+9u?oek*?pMPSzO8hAuot6 z>U@6!y~-LdUgR<&(-sru8AHhh3y+4fS4qw{K?;c%(PWI=BZB+KyDPQOSs(Y7yf&5P zJTsvUxx$%84qHY6Un<-P@IwbSZMgjHXP*n+=1!S!8FbiQsMUmw=WRkSkDyTf1?pyj z=4X6GnYR7w4s&?CN*-q> zf96-kCrF?NY2kc%X{Vb~$rG$E@-j5r?JP5WbpfY=bm2XzkkZC8?5hx$(-8N=x+dds z-7-s8!i98cjkcI*`MY#?2^Io5n(Qz^!c6W|mBN_`rW(pFx1hOb;3DP+J`o#<&qpZb zB;+-eE+3Cj;Gr%YK)CAt7U%!=7upwV1S%JIB42t3ycgXknAl!Qd0X=xJ^4L(9)8;| z_tpHBA8EfYfqEyT-_{5r@^xJ7o6{!?)q~rUkVz)L5rvbT$dHh$-K)}uQp;=DBZI}l z`!95u=9$l;UZb$RV#|Pcyh6P|uKOfRgAQ6DgB87v44rQ+iUNE74TF&~_R!TVwmnq*3B+ zu-kyvt$y;r4y3Ww~eMnN&n z?-Lkm&L5AhgMbmHZpRmxuAHxYke+<>X(Ml0-p_s&moN20@kR#4k(*?Cno~eP;AV@+ zG;wt1K5*ke$?>KwGYm^^+%_HilQ6~u9oxC^ilj=>yctJI#}&!yfo$r2lh=It_e$48 zB%k+vnvy4BzHAf9HH&iBQxz*Lh zWo1vvzs21tN=O`J*RzAcrl~0@y-{&X`(QhMJp*OsthM!Q_8q49Z>q0@EI5LT^^a+K zkUD5v@I%OhWaZGC-FYd%M%L; z@oEknC*LBgrjk`c!CNM-F2BZSMegqrD=J=`OhZk-3)vp+(4(Qb>=O~j#X>T8yiv)6 zjFNi2%yo!l-oJ-7HBnHM!{VJ~66$BSnfXC}jqQ{1-E6eLvR!L(ka3)RdJFgAo}{#N zkO6;ks>ZM4VpgsA^}315+$%ailRrePm$;2$5z<3X_cB?V1xES)eekgU%FW{qr(IeO ze9obvT0CisbD z_d_mt@bNM?n!GGY8p$~?^?gOLTXdrfg-s)u za`G$Z-b>ZT+M4L^h*A7TjGKfxRF;mOo-QVD{rsK+ScbR?DJx@O+K9kQx7e&c0%uh+d z*KHmP&LV%@YLxy)-q?mTL{jgBBKf+$WB8Du$@3 zyZH3ov~?;ogzWq4GC#o)BV#U$jV0WvH)=eo`Sj_|eL=I5ikd3WAI(*>rmX4S@g(NQ zO~V7UYeZaJ+p2gm;Xd#KTQUx=mrq!;o}|SVmNy?)qhs+R?#sN*yogpYU7le3@yuN! zRPm686vF)uUn|*U{=>bcNGTB%@D5&mJvZTwQG zC}XMj0pI?h6nY&rci7`Fb(X3yr&bhOU(2|C%xCCTI%2fIL0!Xo_V@axxK7;@Php z;#CxiiF{*_*s3IuA#3Z~#YtJ*xy_EM+5<8jbvXNB1U%8F4Y)t80gDy*RERm!|*gMwSLZ{QOe0xhd+Om9~zE= z;;9V0J%t)6YhQVU+df+NF_|oNKV2BhKZ5J&@u|ZHIQg^quLNZ zOPNkjZ<#XvWedfV-;VLd0HN&7b)%9BjYVR;=ldO|6A1iZpO~7iq|F%D1uk5RMe}k z+T}O9>v|d#S-UROp+8?lki`pmcJ%Coh8betVGBRPf5^$95^~pj+_G%`w0Yar#gP{u z5kq<<*P3Wr+a@h9FMi{6u{Jd&*v(>d2O^@Rw2yS*y=nN<0rCNG!G0jC0s4Z1LTmn| zbZV-ugE{mKYo(M z%@q~58$S&yE6y-Qh`5gC8qHHD2dKreG}T?SZibl`Ca60dbPmY7{qAo9g1$Uz6qbW*idU5IJipJslZ4vz)dfnZ5 zWoE;gEcQ$>vz4<#4s(-V6G&%gXF*#F=r}mvKI4dMDEA~vMAjc)bQGMe@939y-Jp1S z&VN)`2VOOH<#0SU2J}o`9na^Mg5>5P(a+MGQy&M=)cm@3Zp)j_7a~d%T!|V z*w+jo(@U=L`579;d9e1FE%NOq$EV z+yVRT#6g0TNj9{eE)|J%U*zl`SpJ@{Xr za{eW_z`%eP2#?Hnt}FlmAmH85YJ&tmC=!ze0sG(WUxI*xZX4doT($91kkK2oHaF|S z0afsGl7`DijoqoAvS_*pWFfPiKQ_iP>$pqYIaNvnk^B@%j<#v)FGw1q0yRUdL0Am1ZR1+wv~BY}X7gR# zCldKgzIC&*Q{n^o6TDKiH5_YyVVYj>s0XwMUxlX{w&u3l(^xua&d4gm)Zk6X0&Xa<>zm@D84?8`XUY#6>*CFl2(bm z+4+QPL;Y8SUu!Sk&KdDYXJO1Jgg*`Wclzx9`;$Ymwb2`v3zP^}Lsle)o`7!rGJHf_ zWBmSZd_R$mxJ@4)JAp(~Ti&SXO>Qqod@LY-Sxe3s*d-p`AEIP)| zZocwJV*k5)>=!dJlDn4M;|L)7G6)nb|8>lx&(+W<{6-~poGH~8l* zz+d;&auLk+@!iQ)OK~`N_om7OQHyn7st}*OpT3D|P``!xbpDmSP@DE#GE`xhzR#X} z@r{ZYO%6>Jho!vFSqwZQEAm;ALA&XL(t^oPZpkH=lD#<5Os>MI3sl6jtx29%ulfKW z)+>7EQjhB~-lJFO?O16?MS{Ywrtn7v#uI+_@IK)+sI$x`MZ@14j_*^uJsr2s` zC`@yedBw9@3t+c5Apg5Tys8+u8-edGJUM#Ohj=*ekNmseNN8 zgD!Zzc%mVE#>TcmK&XeOBQzDQGbu86Ja)V?GkJPqYC^Lxmd>Oom^#SgfW_L>VEIJ) zDo(2?xOv4<;4O{S{DBPm5t2R3vW>xFD6r?R8|kCg*vBkDKfcs4Y{yJKQx-+ZKyguX z(Og2%zqYQ)Vehs=ZV{DS3Omlid189L`V6|-QezlAKec&qkeVUD%EZb=El__a?!F%D zT4PKsEi>f<{dYFl$nuQ5RmUYjB%cN~S3Pm0Mvp-7#&Qh(p*I znIDrU9+kLiBkv_E(CQ72>h_d5P?&9uL9Neit?_9e*m#wSLXOsX=xGiaJU0+faSZ`t z#o>!)iXM*{iAc(*48jRz_Oje82)35@^Bj2K%cVwd!KO5f3}AiIgM$CNBVqScx8g3w zC9z7+j_(po7dKAKfIc$PpQgbAl4M&DsO%q&;UbSRCu+1$LLpzBHRv1Yj}4LhNv=Ff z2D@)LL|1T41Y#xPNI>u@PWz7&P-Cc@oKI{=Rkt2%XERG?b*Cn3j*H9HO{+DlhqV(Y zyKh*kS7$Iqn0!}bI3~)arpMjE8v^s_7up>i?~~?z8Lv4WHmk%~IF%){ya~`J*mORJ2Y6t zXfKrd2j-8W_-U?M?V$uiOBUOge^mfr3;@VNL*tPXk${tZGW$~XuAYv&M*6RTWOhnw zfn8oC#4Fn zdV%ds7fqK(Gjs)ugp7m`bKj{UfmyR(dp>Idoh^9C+WH>|Pyt#5NN%GYv~JFs1wocp zJvo*H7ge*r)T|z+$tb{>I@dPi1v<2H!zd_d+)a0%_4D%Nkha3bd(V9&HT!Cse9-^(_{T_$Q89&@ozyEmFkCB6dgO9ebB)6p$-PZrWTFaOAh|9abfl+QJwS%HF zcQ|E$WvD#6H|7D#-RsCd+L-pq->(;gq&J!r!r>y`Ef(D0q2^kUPThFH>#J?1yJp9b6uXB*!j5_yskK)y3U|0xN>QZUS4hcXX9m5y#?~YZXk(ND$(9 zxoJR!3%$8bilp)SU;_G~_=c3a;zBbNjF@I+JsV*BR2aDBCqa;^IH@TULmGlrWnQUW zh-LL$-Te#dCh5CJFX zbsvym#9r82ClWyzj@p+;r5>6wz5hVGIO;6|z3`v5{hwF`yH`ZGx4$~(XDD!rLAT*DnR37&0WP<23U;+VjskdcycIPy0)pLiq$6v4o!q@rYFO~hLGxGNh zy-@(njgPNPPitNGj*KMqZkB71f);CvG6e=B1iU+vQj_iO4Q+SxgpCb|yJsGkB!3!r zzC0qZvhKzu1+7HvG}W%e2#Ed`-M8zN$jxzM3ZEj;?#j)l78Ytu7fWt}WNg-J+2>UX zl=I~?!;D@i#4@HLE>UkpMjqZV7TXlFr*hrm;S4DFk@e$x(Tds@iAOn+8d@o9yL~Da zQ17y%Bqlj@KY~Uw{zC{`$qXq5`i6LbSSu2*@%PR;!oe##pwh-K-v(IFydNx0Zq7T| z*eWzyE-`9OSo9}p2=PCAk*pch%BRP)dK4CfF#aJzr9ZC`>};A6Q|T$QhKh)3^VYFx zBiUJ5OG;ZOYfIa21L8k`@DLG|+(s@tje1QhK2Wf+6O{=Xf-O!Zm)jCM*MZ|ntR<z^9$ zu0d{26GB9eCK78c7m+q{8d;AIQPDxqUIgDxB=%-RaaTC1DC+zDxX$94%u}eS@nT|b zJ=raf%ePDxtMXm+Mnt-R?Ct^^>w|**_4HJ(uXF~nm~v!KFMtFn+1c32OnJyn=iA>u zn!S1ja({{h2Us8{761d?udIxXjd^TX^ns@$@raK?(JS*?eQ7T$nOz|r)6?_*!)HHg z)ESFywY2JCXbZ4G1m2l|a->L3!y*J@bbRYJg$g8DF?jyS*f^V96lpY#by|;tiHWJO zw6zoLbalAEYrW0kDdnl`v>#!ti}4gf%+Z(DyLoi$aXv-2>Fi_*XF#xY6#MyQ*st&J z?Ck7r_vtehbS838nYFaFac~pLFvdBEtOp;wg$Vb~M0`4a7U*!E>w9y)aS%O!NJyF! z3(m?eXyBsZD$lLSIFh%uJz^~nlcrYDQ^CVd^|%{Zu6Ne=bboj&n2Gm_#NOV#@h*|$p`ENOzz6SDHyxtFzF#*J z6XARM*blQU?T;4!!HV67O0+5O%>_BHT4KPBF8XWI z&@;XH=FFW|iD^(*$6c}fm-Sy0LMxM1zd!OHJ@ep?5Z-n+Q^0t>RJJkfUR;zxnRUL9 ztVto=u|X_s!Az;>SgDVWa?RXDz7iu z)_yfVMM3f8lCJve*_mGj1?sPN*H>BC*(HkA52$8jD^pXpz_0pDx+im^uXXk&N6Sjf z%fE_1RcE{Z&Wz9|KtpXdTM{*ljy5`2Hl3}dsw(g^FE0**D_SSZ6TwX8HTPnA z9O-!JOG8f_9#^R8dy^9172R4C@3e;W!kpdNGax*Px6HeIp~O@+MgocSuo;GoEvO z8uHVg9_rx?&f%O^CHPFPTA1FmBRDt%y)E1WD=Rg1n$Fl^Vny^7vt$Hc6(z-@u2Zb6 z_O-RO-JT*9s=j?0%0A-jQmbUoT$9)n1?Q=($6sXt>_QrY_pr%o1=;;U20;4T5%t~# zprfO6d&{uq!~@@vCF&2=+PB0*xrNRyTHP~!4s*z{?W&J=Pe+FjT)odq z@s^vkhV>ylpttt*VR{L-OrJl0j*e6^Se~#RAt>+dr?kL zSBq77jN7nRBEX$TQc4QPs=X2OAAfWCW8?kN48{BB&?`?3Ds``a(AGB{?w&#>CrdHM z_c@8&?fh50Pjv+|HMHG~B@D}=8v-tM+&rMJ3R8m`sJ^-!FcNG>Tshc z*6ot%Pm|Tu6ey=7c%=hwJWdC_dj5VPzGzrS$Vg%|@(gp8|8aj;sx@A)@F8Kqq#IeJ z<2}XrRQ{#_2Zmb4y&&6RgniW~``iPsN;}?L;b0^DpA~oIB{8u^s&B=M)!mPZ>Jj1K z5ig+zpe^OZuNR-e?Qz#bfq-0!T=6nkJScwZnf8x>;;$sY zKQU1Jdwlix{KDU-UNa|NgOwW~BLV;bl& z{Blujz&jR{Y?P7L+UQ)@2BR0|pM<}M|Gzm0dNBVka{mKRw7~;_&)$rff45b literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/dock_panel-light.png b/testdata/snapshots/darwin/dock_panel-light.png new file mode 100644 index 0000000000000000000000000000000000000000..0cab0c0a651f434b56beabfc3ec0d34014bcae04 GIT binary patch literal 13699 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�sWG+o)g|uOyWy9-Mk*n52tjCrHk!83h^LN?(AC2Jy5dsxCIeDQf6Y;k@ zfa}Fh&G~kFajghhqk)l{TAh`bmb5fu<$dGs&JMhcPaM_r&AGW$z`yR>x6BcEiS|!a zTM{pt1ny(}H&hEvl zw_0kzBcv=v-Li7quge2AHjQ#v^%SC#n`l1NOWP}(UA}_r`HC5vxisj zJWjE(4I1rc^j#Ybn3H{7d=wRjNl20nl-r%y4U2&hVtFTPXA=3bFT$`Ivq6W4bYe2H zvU7VU9!jxCqmJ{7v$KWsGHU7*#v4un3GGWRH9!{^8xM~d(o}67osE5por8^G#%&w}JychA zbT>E{xx4^wCV$GyqxH4>D;rKIky*9-!@|O45n=Rw=^CfiZqz@WlR>^^fG?0*6EFn#l^tjtUb<9tS3VK zNRWeXW^KK)Vv-Y&Z)R@py{F-^hPALY45Ue*o>^F^f5p|*+#H5KF?ntV4-BxGiGktA zv}c}39*Y|t2`vPtcH~G}O${wQ0c6N52Q)xYv(h_h<4-tN=5&vL(c-~Z0 zv_?eKz~2%$ICzeQ<(B=()#ODFKNM=N9U4-cl5a#iTjU3qIpHS%>T#Scsj#$k?z6?v zc6(tu0S?Xb+ZKBz-wWG${y1JVC- zqj}e1TC+7;kMGB9@7VV9>U?zBf95QASFni^;h*^5W)`TpHRR&vrfNtCx6g|43RASG zH0>3wguZ^%5glAOAdJvpPQK3vSKdiah1Av5fblip3g`Ze(}}nIZc2j8%ZXF=pVMka z@`Y-8pXo6}OikY9SCv>|i#Si<^?jVV9rSf5;2U`%BR-E_5^I;{bNMg2_<_xJ6bNZE8uENrgvX*c$( z_NOjKM!-i$jyJZ1#2Y!r&JuJ9O)dsMvldQk>grh8NyB^Ao^e}#NpFfFCs&Xk9EfZG z`0?YObeGn2%GI(+>S&z0mS!9X0)aTqz4Z_NH?_ClI5_xxcN06R4ARw}!EX+!s;b)F+j|3q zwc6Jip6vtLe%<}Np2u>j@k9M6x*YKf?H8mzLioCnpQ%4*x_9k#hmavD0lytmvzlMM zCZVT?Iy2~Yb0Leu!wsz7`?-4#Aw@+)L%vIeH5utVR`5TgeN;~OreX=$Z2HUhUt z>uG|`+wQlpFc$I)*3G|)+Sv`Z-a;*FboOpiK9kbD$kAq!k&y58xxb?cw1U}N=Y$f zIBaiT?kLv$s)tg;DNf^lcLUTv#OToy%sZnG5R|j>`qbGenx^16Ds(m0B?E_ho;St2 zI|?OFHp!?`(9kLkOw6)UQWSdG7t<;DZmfpJp83!F0>l}TY;0nLM2>c^$2&USiJR@+ zA`;Oh%b}s7S`MdQ?hlgv{scUp4mTzhYt^~Kw6g*kV~UyGc~jE3ttJK&&_i-Isrv2g zhz@=1wu6f>#;3Bb_p#cAdiTp8xVT&I+zKi&vsRyRWFxNswNhT^fz_$()=9o5s2~^!2F#Q^FKqK9)O7dncM%QJ^$y~ z*}wYvgJS-o#;qn_d941Is`3Adl<-^Ch@%9&K}10L$Pu~nz%3BCw1L9Q$ZQX^o*5l#=_uY;KlOaQ?3p=$8?_aS&{F WEvlYGQYj