Refactor: results expose Element as a named field

Every component result type now names its element Element instead of
embedding *ui.Element. The eleven element-plus-changed results
(AccordionView, CarouselView, CollapsibleView, ImageViewerView,
PaginationResult, StepsResult, SidebarResult, TabsResult, RatingParts,
FileDropZoneParts, TransferParts) fold into ChoiceResult.

Fixes four Gallery demos where .Width(...).Changed() read the element's
flag instead of the component's: Accordion, MultiSelect, Cascader,
TreeSelect. TestDataAccordionDemoLogsChange covers the Accordion.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 13:35:53 -04:00
1 parent 078dcab8b4
commit 8880865021
73 files changed
+163 -245

No files matched your search

+3 -12
View File
@@ -28,17 +28,8 @@ type AccordionOptions struct {
KeepOne bool
}
// AccordionView is a built Accordion and what happened to it in this frame.
type AccordionView struct {
*ui.Element
changed bool
}
// Changed reports that a section opened or closed.
func (v AccordionView) Changed() bool { return v.changed }
// Accordion shows sections whose open IDs live in *open, which the caller controls.
func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) AccordionView {
func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) ChoiceResult {
seen := map[string]bool{}
for _, it := range items {
if it.ID == "" || seen[it.ID] || it.Content == nil {
@@ -49,10 +40,10 @@ func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts Accord
k := Tokens(c)
d := Density(c)
u := d.Unit()
var v AccordionView
var v ChoiceResult
v.Element = ui.Column(c).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius)
focusTo := ui.Local(v.Element, "focus", func() int { return -1 })
v.Children(func() {
v.Element.Children(func() {
for i, it := range items {
ui.Column(c).Key(it.ID).Children(func() {
if i > 0 {
+1 -1
View File
@@ -28,7 +28,7 @@ type AppShellOptions struct {
// AppShellResult is an AppShell and its event in this frame.
type AppShellResult struct {
*ui.Element
Element *ui.Element
// Toggled reports the sidebar opened or collapsed.
Toggled bool
}
+2 -2
View File
@@ -28,8 +28,8 @@ type BreadcrumbOptions struct {
// BreadcrumbResult is the built Breadcrumb.
type BreadcrumbResult struct {
*ui.Element
chosen int
Element *ui.Element
chosen int
}
// Chosen returns the index of the item the user activated in this frame.
+1 -1
View File
@@ -28,7 +28,7 @@ type ButtonGroupOptions struct {
// ButtonGroupResult is the group element and the item clicked this frame.
type ButtonGroupResult struct {
*ui.Element
Element *ui.Element
clicked int
}
+8 -17
View File
@@ -24,15 +24,6 @@ type CarouselOptions struct {
Loop bool
}
// CarouselView is a built Carousel and what happened to it in this frame.
type CarouselView struct {
*ui.Element
changed bool
}
// Changed reports a new slide.
func (v CarouselView) Changed() bool { return v.changed }
type carouselState struct {
paused bool
next time.Time
@@ -41,7 +32,7 @@ type carouselState struct {
}
// Carousel shows slide *index of n with previous, next and position controls.
func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) CarouselView {
func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) ChoiceResult {
if n < 0 || opts.Interval < 0 {
panic("mujica: Carousel with negative count or Interval")
}
@@ -51,7 +42,7 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func
}
k := Tokens(c)
u := Density(c).Unit()
var v CarouselView
var v ChoiceResult
v.Element = ui.Column(c).Gap(u * 2).Label(opts.Label)
st := ui.Local(v.Element, "state", func() carouselState { return carouselState{} })
if n > 0 && (*index < 0 || *index >= n) {
@@ -75,7 +66,7 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func
slog.Debug("mujica: carousel slide", "index", i)
}
now := c.Now()
playing := opts.Autoplay && n > 1 && !st.paused && !v.Hovered() && !v.FocusWithin()
playing := opts.Autoplay && n > 1 && !st.paused && !v.Element.Hovered() && !v.Element.FocusWithin()
if !playing {
st.next = time.Time{}
} else if st.next.IsZero() {
@@ -89,16 +80,16 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func
}
switch {
case st.dotFocus:
case v.Shortcut(0, ui.KeyLeft):
case v.Element.Shortcut(0, ui.KeyLeft):
goTo(*index - 1)
case v.Shortcut(0, ui.KeyRight):
case v.Element.Shortcut(0, ui.KeyRight):
goTo(*index + 1)
case v.Shortcut(0, ui.KeyHome):
case v.Element.Shortcut(0, ui.KeyHome):
goTo(0)
case v.Shortcut(0, ui.KeyEnd):
case v.Element.Shortcut(0, ui.KeyEnd):
goTo(n - 1)
}
v.Children(func() {
v.Element.Children(func() {
ui.Row(c).Grow(1).MinHeight(120).AlignItems(ui.Center).Gap(u * 2).Children(func() {
prev := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("chevron-left"), Label: msg(c, "carousel.prev"), Disabled: n < 2 || !wrap && *index == 0})
ui.Column(c).Grow(1).MinWidth(0).FillHeight().Clip().Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius).
+3 -3
View File
@@ -20,7 +20,7 @@ func (h *carouselHarness) view(c *ui.Context) {
v := Carousel(c, &h.index, 4, h.opts, func(i int) {
ui.Text(c, fmt.Sprintf("Window %d", i+1)).Padding(20)
})
v.Grow(1)
v.Element.Grow(1)
if v.Changed() {
h.changes++
}
@@ -126,7 +126,7 @@ func TestCarouselSnapshot(t *testing.T) {
ui.Column(c).Grow(1).Center().Children(func() {
ui.Text(c, fmt.Sprintf("Rose window %d", n+1)).FontSize(FontSize(c, 18))
})
}).Grow(1)
}).Element.Grow(1)
})
})
}
@@ -135,7 +135,7 @@ func TestCarouselEmptyAutoplayKeys(t *testing.T) {
index := 0
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
Carousel(c, &index, 0, CarouselOptions{Autoplay: true}, func(int) {}).Size(300, 200)
Carousel(c, &index, 0, CarouselOptions{Autoplay: true}, func(int) {}).Element.Size(300, 200)
}, 320, 240)
for range 4 {
tt.Key(0, ui.KeyTab)
+3 -12
View File
@@ -19,25 +19,16 @@ type CollapsibleOptions struct {
Disabled bool
}
// CollapsibleView is a built Collapsible and what happened to it in this frame.
type CollapsibleView struct {
*ui.Element
changed bool
}
// Changed reports that the trigger opened or closed the content.
func (v CollapsibleView) Changed() bool { return v.changed }
// Collapsible shows content below a trigger while *open; closed content is not built.
func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) CollapsibleView {
func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) ChoiceResult {
if opts.Title == "" && opts.Trigger == nil {
panic("mujica: Collapsible needs Title or Trigger")
}
k := Tokens(c)
u := Density(c).Unit()
var v CollapsibleView
var v ChoiceResult
v.Element = ui.Column(c).Shrink(0)
v.Children(func() {
v.Element.Children(func() {
was := *open
p := ui.CollapsibleBase(c, open)
tr := p.Trigger
+2 -2
View File
@@ -32,8 +32,8 @@ type CommandPaletteOptions struct {
// CommandPaletteResult is the built CommandPalette; Element is nil while closed.
type CommandPaletteResult struct {
*ui.Element
chosen string
Element *ui.Element
chosen string
}
// Chosen returns the ID of the command run in this frame; running closes the palette.
+2 -2
View File
@@ -85,7 +85,7 @@ func (s *DataTableState[K]) Rows() int { return len(s.cache.view) }
// DataTableView is a built DataTable and what happened to it in this frame.
type DataTableView struct {
*ui.Element
Element *ui.Element
changed, submitted, sorted, retry bool
}
@@ -114,7 +114,7 @@ func DataTable[R any, K comparable](c *ui.Context, s *DataTableState[K], rows []
before := s.Sort
var v DataTableView
v.Element = ui.Column(c).MinHeight(0).Gap(d.Unit() * 2)
v.Children(func() {
v.Element.Children(func() {
if len(rows) > 0 {
ui.Row(c).Justify(ui.End).Children(func() {
tableColumnMenu(c, dataTableMenu(opts), &s.Sort, &s.Layout, &s.Hidden)
+2 -2
View File
@@ -54,7 +54,7 @@ func (h *tableHarness) view(c *ui.Context) {
cols.Columns[i].Text = func(r relic) string { h.seen[r.Name]++; return text(r) }
}
v := DataTable(c, &h.st, h.rows, cols)
v.Size(360, 300)
v.Element.Size(360, 300)
if v.Changed() {
h.changes++
}
@@ -347,7 +347,7 @@ func TestDataTableSnapshot(t *testing.T) {
return h
})
ui.Column(c).Padding(16).Children(func() {
DataTable(c, &(*h).st, (*h).rows, (*h).opts).Size(600, 260)
DataTable(c, &(*h).st, (*h).rows, (*h).opts).Element.Size(600, 260)
})
})
}
+1 -1
View File
@@ -68,7 +68,7 @@ func (d Date) within(lo, hi Date) bool {
// DateTimeResult is a date or time component and what happened to it this frame.
type DateTimeResult struct {
*ui.Element
Element *ui.Element
// Input is the text field inside, for labels and focus; nil without one.
Input *ui.Element
changed bool
+5 -5
View File
@@ -31,8 +31,8 @@ type DescriptionListOptions struct {
// DescriptionListView is a built DescriptionList and what happened to it in this frame.
type DescriptionListView struct {
*ui.Element
copied int
Element *ui.Element
copied int
}
// Copied returns the index of the item whose value was copied in this frame.
@@ -69,15 +69,15 @@ func DescriptionList(c *ui.Context, items []DescriptionItem, opts DescriptionLis
}
return max(1, min(most, int(math.Floor(float64((w+gap)/(minW+gap))))))
}
cols := fit(v.Bounds().W)
v.Draw(func(p *ui.Painter, r ui.Rect) {
cols := fit(v.Element.Bounds().W)
v.Element.Draw(func(p *ui.Painter, r ui.Rect) {
if fit(r.W) != cols {
// The new width needs other columns: build again.
c.Invalidate()
}
})
now := c.Now()
v.Columns(cols).Children(func() {
v.Element.Columns(cols).Children(func() {
for i, it := range items {
cell := ui.Column(c).Gap(u).MinWidth(0)
if it.Span < 0 || it.Span > cols {
+1 -1
View File
@@ -29,7 +29,7 @@ mujica.AppShell(c, &collapsed, mujica.AppShellOptions{
| `SidebarWidth` | `float32` | 240 | 展开时侧栏宽度(DIP);负数 panic。 |
| `ContentScroll` | `*ui.ScrollState` | nil | 保存主内容滚动位置;nil 时按位置保留。 |
返回 `AppShellResult`,内嵌 `*ui.Element`。
返回 `AppShellResult`,`Element` 字段是组件元素。
## 状态
+2 -2
View File
@@ -11,7 +11,7 @@ split := mujica.SplitPaneState{Size: 160}
mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible: mujica.SplitPaneFirst},
func() { ui.Text(c, "Files") },
func() { ui.Text(c, "Editor") },
).Fill()
).Element.Fill()
```
签名:`func SplitPane(c *ui.Context, state *SplitPaneState, opts SplitPaneOptions, first, second func()) SplitPaneResult`
@@ -33,7 +33,7 @@ mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible:
| `MinFirst`, `MinSecond` | `float32` | 10 单位(默认 40 DIP) | 两栏最小尺寸;负数 panic。 |
| `Collapsible` | `SplitPaneSide` | `SplitPaneNeither` | 可折叠的栏:`SplitPaneFirst` 或 `SplitPaneSecond`。 |
返回 `SplitPaneResult`,内嵌 `*ui.Element`。
返回 `SplitPaneResult`,`Element` 字段是组件元素。
## 状态
+1 -1
View File
@@ -15,7 +15,7 @@ if r.Submitted() {
签名:`func TextInput(c *ui.Context, value *string, opts TextInputOptions) InputResult`
`InputResult` 内嵌外层 `*ui.Element`,`Input` 字段是内部编辑元素,`Changed()`、`Submitted()` 报告事件。
`InputResult` 的 `Element` 字段是外层元素,`Input` 字段是内部编辑元素,`Changed()`、`Submitted()` 报告事件。
## 参数
+1 -1
View File
@@ -35,7 +35,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`RatingParts.Changed()` 用户设置了新值;同值点击不触发。
`ChoiceResult.Changed()` 用户设置了新值;同值点击不触发。
## 键盘操作
+1 -1
View File
@@ -36,7 +36,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`FileDropZoneParts.Changed()` 添加或移除了文件。被拒绝的条目只显示在组件内,可单独关闭。
`ChoiceResult.Changed()` 添加或移除了文件。被拒绝的条目只显示在组件内,可单独关闭。
## 键盘操作
+1 -1
View File
@@ -34,7 +34,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`TransferParts.Changed()` 有条目移动。
`ChoiceResult.Changed()` 有条目移动。
## 键盘操作
+1 -1
View File
@@ -19,7 +19,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func Calendar(c *ui.Context, value *Date, opts CalendarOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func Calendar(c *ui.Context, value *Date, opts CalendarOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -21,7 +21,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func DatePicker(c *ui.Context, value *Date, opts DatePickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func DatePicker(c *ui.Context, value *Date, opts DatePickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -20,7 +20,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -20,7 +20,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func TimePicker(c *ui.Context, value *TimeOfDay, opts TimePickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func TimePicker(c *ui.Context, value *TimeOfDay, opts TimePickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -20,7 +20,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -24,7 +24,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func DateTimePicker(c *ui.Context, value *time.Time, opts DateTimePickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func DateTimePicker(c *ui.Context, value *time.Time, opts DateTimePickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -20,7 +20,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func MonthPicker(c *ui.Context, value *YearMonth, opts MonthPickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func MonthPicker(c *ui.Context, value *YearMonth, opts MonthPickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -20,7 +20,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func YearPicker(c *ui.Context, value *int, opts YearPickerOptions) DateTimeResult`,返回值嵌入 `*ui.Element`。
`func YearPicker(c *ui.Context, value *int, opts YearPickerOptions) DateTimeResult`,返回值的 `Element` 字段是组件元素。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+2 -2
View File
@@ -14,7 +14,7 @@ mujica.Tabs(c, &tab, []mujica.Tab{
}, mujica.TabsOptions{Label: "Settings"})
```
签名:`func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) TabsResult`
签名:`func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResult`
## 参数
@@ -36,7 +36,7 @@ mujica.Tabs(c, &tab, []mujica.Tab{
| `Vertical` | `bool` | false | 标签竖排在左侧,面板在右侧。 |
| `Label` | `string` | "" | 标签列表的无障碍名称。 |
返回 `TabsResult`,内嵌包含标签与面板的 `*ui.Element`。
返回 `ChoiceResult`,`Element` 字段是包含标签与面板的元素。
## 状态
+1 -1
View File
@@ -33,7 +33,7 @@ if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{}).Chosen(); ok
| `MaxItemWidth` | `float32` | 0(即 160 DIP) | 单项文字最大宽度,超出以省略号截断。 |
| `Label` | `string` | 内置“路径导航” | 整组的无障碍名称。 |
返回 `BreadcrumbResult`,内嵌 `*ui.Element`。
返回 `BreadcrumbResult`,`Element` 字段是组件元素。
## 状态
+2 -2
View File
@@ -12,7 +12,7 @@ if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageS
}
```
签名:`func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) PaginationResult`
签名:`func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) ChoiceResult`
## 参数
@@ -28,7 +28,7 @@ if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageS
| `Siblings` | `int` | 0(即 1) | 当前页两侧显示的页码数;为负 panic。 |
| `Disabled` | `bool` | false | 整体禁用。 |
返回 `PaginationResult`,内嵌 `*ui.Element`。
返回 `ChoiceResult`,`Element` 字段是组件元素。
## 状态
+2 -2
View File
@@ -14,7 +14,7 @@ mujica.Steps(c, &current, []mujica.Step{
}, mujica.StepsOptions{Label: "Sign-up"})
```
签名:`func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) StepsResult`
签名:`func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceResult`
## 参数
@@ -36,7 +36,7 @@ mujica.Steps(c, &current, []mujica.Step{
| `Vertical` | `bool` | false | 纵向排列,连接线在标记下方。 |
| `Label` | `string` | "" | 整组的无障碍名称。 |
返回 `StepsResult`,内嵌 `*ui.Element`。
返回 `ChoiceResult`,`Element` 字段是组件元素。
## 状态
+2 -2
View File
@@ -15,7 +15,7 @@ mujica.Sidebar(c, &selected, []mujica.SidebarSection{
}, mujica.SidebarOptions{})
```
签名:`func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) SidebarResult`
签名:`func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) ChoiceResult`
## 参数
@@ -45,7 +45,7 @@ mujica.Sidebar(c, &selected, []mujica.SidebarSection{
| `Compact` | `bool` | false | 图标栏:隐藏文字与组标题,组之间以旧金细线分隔,名称作为悬停提示。 |
| `Width` | `float32` | 0(240,紧凑时 56) | 宽度,DIP。高度填满父容器。 |
返回 `SidebarResult`,内嵌 `*ui.Element`。展开折叠状态由组件保存。
返回 `ChoiceResult`,`Element` 字段是组件元素。展开折叠状态由组件保存。
## 状态
+1 -1
View File
@@ -45,7 +45,7 @@ if id, ok := res.Chosen(); ok && id == "wrap" {
| --- | --- | --- | --- |
| `Label` | `string` | 内置“菜单栏” | 菜单栏的无障碍名称。 |
返回 `MenubarResult`,内嵌菜单栏 `*ui.Element`。
返回 `MenubarResult`,`Element` 字段是菜单栏元素。
## 状态
+2 -2
View File
@@ -15,7 +15,7 @@ view := func(c *ui.Context) {
Key: func(i int) string { return names[i] },
Label: func(i int) string { return names[i] },
}, func(i int) { ui.Text(c, names[i]) })
v.Size(240, 200)
v.Element.Size(240, 200)
if v.Changed() {
key, _ := state.Selected()
fmt.Println("chose", key)
@@ -26,7 +26,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func List[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) ListView`。`ListView` 嵌入 `*ui.Element`;`ListState` 由调用方持有,零值表示未选择。`state.Selected()` 返回所选键,多选时 `state.Selection` 保存全部键;`state.ScrollTo(i)` 滚动到第 i 项。
`func List[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) ListView`。`ListView` 的 `Element` 字段是列表元素;`ListState` 由调用方持有,零值表示未选择。`state.Selected()` 返回所选键,多选时 `state.Selection` 保存全部键;`state.ScrollTo(i)` 滚动到第 i 项。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
+1 -1
View File
@@ -27,7 +27,7 @@ opts := mujica.DataTableOptions[relic, int]{
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DataTable(c, &state, rows, opts).Size(360, 240)
mujica.DataTable(c, &state, rows, opts).Element.Size(360, 240)
}
ui.Render(view, 400, 300, 1)
```
+1 -1
View File
@@ -15,7 +15,7 @@ view := func(c *ui.Context) {
Roots: []string{"Nave", "Sacristy"},
Children: func(n string) []string { return kids[n] },
Label: func(n string) string { return n },
}).Size(280, 240)
}).Element.Size(280, 240)
}
ui.Render(view, 400, 300, 1)
```
+1 -1
View File
@@ -22,7 +22,7 @@ view := func(c *ui.Context) {
{ID: "size", Title: "Size", Width: 80, Align: ui.End,
Cell: func(n string) { ui.Text(c, strconv.Itoa(size[n])) }},
},
}).Size(360, 240)
}).Element.Size(360, 240)
}
ui.Render(view, 400, 300, 1)
```
+2 -2
View File
@@ -20,7 +20,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) AccordionView`。`*open` 是展开区段的 ID 列表;`AccordionItem` 字段:`ID`(必填、唯一)、`Title`、`Content`(必填)、`Disabled`。
`func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) ChoiceResult`。`*open` 是展开区段的 ID 列表;`AccordionItem` 字段:`ID`(必填、唯一)、`Title`、`Content`(必填)、`Disabled`。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
@@ -33,7 +33,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`AccordionView.Changed()`:某区段因用户操作而展开或关闭,`*open` 已更新。外部直接修改 `*open` 不触发事件。
`ChoiceResult.Changed()`:某区段因用户操作而展开或关闭,`*open` 已更新。外部直接修改 `*open` 不触发事件。
## 键盘操作
+2 -2
View File
@@ -19,7 +19,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) CollapsibleView`。
`func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) ChoiceResult`。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
@@ -33,7 +33,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`CollapsibleView.Changed()`:用户点击或按键切换了展开状态。
`ChoiceResult.Changed()`:用户点击或按键切换了展开状态。
## 键盘操作
+2 -2
View File
@@ -18,7 +18,7 @@ ui.Render(view, 400, 300, 1)
## 参数
`func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ImageViewerView`。`ViewerImage` 字段:`Source`(`*ui.Bitmap` 或 `*ui.SVG`,必填)、`Alt`(说明文字与无障碍名称)。关闭时返回的 `Element` 为 nil。
`func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ChoiceResult`。`ViewerImage` 字段:`Source`(`*ui.Bitmap` 或 `*ui.SVG`,必填)、`Alt`(说明文字与无障碍名称)。关闭时返回的 `Element` 为 nil。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
@@ -32,7 +32,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`ImageViewerView.Changed()`:切换了图片,`*index` 已更新。关闭通过 `*open` 变为 false 观察。
`ChoiceResult.Changed()`:切换了图片,`*index` 已更新。关闭通过 `*open` 变为 false 观察。
## 键盘操作
+3 -3
View File
@@ -12,14 +12,14 @@ view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) {
ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24)
}).Size(360, 220)
}).Element.Size(360, 220)
}
ui.Render(view, 400, 300, 1)
```
## 参数
`func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) CarouselView`。
`func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) ChoiceResult`。
| 字段 | 类型 | 默认值 | 含义 |
| --- | --- | --- | --- |
@@ -34,7 +34,7 @@ ui.Render(view, 400, 300, 1)
## 事件
`CarouselView.Changed()`:当前项变化,`*index` 已更新;同时向读屏软件播报“第 N 项,共 M 项”。
`ChoiceResult.Changed()`:当前项变化,`*index` 已更新;同时向读屏软件播报“第 N 项,共 M 项”。
## 键盘操作
+5 -5
View File
@@ -31,7 +31,7 @@ func ExampleList() {
Key: func(i int) string { return names[i] },
Label: func(i int) string { return names[i] },
}, func(i int) { ui.Text(c, names[i]) })
v.Size(240, 200)
v.Element.Size(240, 200)
if v.Changed() {
key, _ := state.Selected()
fmt.Println("chose", key)
@@ -61,7 +61,7 @@ func ExampleDataTable() {
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DataTable(c, &state, rows, opts).Size(360, 240)
mujica.DataTable(c, &state, rows, opts).Element.Size(360, 240)
}
ui.Render(view, 400, 300, 1)
}
@@ -75,7 +75,7 @@ func ExampleTree() {
Roots: []string{"Nave", "Sacristy"},
Children: func(n string) []string { return kids[n] },
Label: func(n string) string { return n },
}).Size(280, 240)
}).Element.Size(280, 240)
}
ui.Render(view, 400, 300, 1)
}
@@ -96,7 +96,7 @@ func ExampleTreeTable() {
{ID: "size", Title: "Size", Width: 80, Align: ui.End,
Cell: func(n string) { ui.Text(c, strconv.Itoa(size[n])) }},
},
}).Size(360, 240)
}).Element.Size(360, 240)
}
ui.Render(view, 400, 300, 1)
}
@@ -176,7 +176,7 @@ func ExampleCarousel() {
mujica.Use(c, mujica.Settings{})
mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) {
ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24)
}).Size(360, 220)
}).Element.Size(360, 220)
}
ui.Render(view, 400, 300, 1)
}
+1 -1
View File
@@ -100,7 +100,7 @@ func ExampleSplitPane() {
mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible: mujica.SplitPaneFirst},
func() { ui.Text(c, "Files") },
func() { ui.Text(c, "Editor") },
).Fill()
).Element.Fill()
}
ui.Render(view, 400, 300, 1)
}
+2 -2
View File
@@ -152,7 +152,7 @@ func (g *gallery) filesView(c *ui.Context, log func(string)) {
v := mujica.DataTable(c, &s.table, s.entries, mujica.DataTableOptions[fileEntry, int]{
Columns: cols, Key: func(f fileEntry) int { return f.ID }, Multiple: true, Version: s.version,
})
v.FillWidth().Height(430)
v.Element.FillWidth().Height(430)
mujica.ContextMenu(c, v.Element, mujica.ContextMenuOptions{}, func(m *mujica.PopupMenu) { g.fileMenu(m, log, targets) })
if v.Changed() {
log(fmt.Sprintf("selection %d checked", s.table.Selection.Len()))
@@ -174,7 +174,7 @@ func (g *gallery) filesView(c *ui.Context, log func(string)) {
{ID: "shown", Text: fmt.Sprintf(g.t("%d shown", "显示 %d 个"), shown), Secondary: true},
},
Right: []mujica.StatusItem{{ID: "selected", Text: fmt.Sprintf(g.t("%d selected", "已选 %d 个"), s.table.Selection.Len())}},
}).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border)
}).Element.Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border)
g.fileAdders(c, log)
g.renameDialog(c, log)
g.deleteDialog(c, log)
+8 -3
View File
@@ -330,7 +330,8 @@ func multiSelectDemo(c *ui.Context, log func(string)) {
}
choiceLabeled(c, "Fruits", func() {
r := mujica.MultiSelect(c, &s.values, choiceFruits, mujica.MultiSelectOptions{Max: limit, MaxTags: 2, Disabled: s.disable, Label: "Fruits"})
if r.Width(320).Changed() {
r.Element.Width(320)
if r.Changed() {
log(fmt.Sprintf("Fruits: %v", s.values))
}
})
@@ -435,7 +436,9 @@ func cascaderDemo(c *ui.Context, log func(string)) {
}, func() {
choiceLabeled(c, "City (children load on demand)", func() {
opts := mujica.CascaderOptions{Level: level, Load: load, AnyLevel: s.anyLevel, Disabled: s.ds, Label: "City"}
if mujica.Cascader(c, &s.path, opts).Width(320).Changed() {
r := mujica.Cascader(c, &s.path, opts)
r.Element.Width(320)
if r.Changed() {
log(fmt.Sprintf("path: %v", s.path))
}
})
@@ -471,7 +474,9 @@ func treeSelectDemo(c *ui.Context, log func(string)) {
}
choiceLabeled(c, "Roles", func() {
opts := mujica.TreeSelectOptions{Multiple: s.multiple, Rule: rule, MaxTags: 2, Disabled: s.disabled, Label: "Roles"}
if mujica.TreeSelect(c, &s.sel, choiceRoles, opts).Width(320).Changed() {
r := mujica.TreeSelect(c, &s.sel, choiceRoles, opts)
r.Element.Width(320)
if r.Changed() {
log(fmt.Sprintf("Roles: %v", s.sel))
}
})
+4 -4
View File
@@ -58,7 +58,7 @@ func dataListDemo(c *ui.Context, log func(string)) {
ui.Text(c, "Office").TextColor(mujica.Tokens(c).TextMuted)
}
})
v.Size(480, 320)
v.Element.Size(480, 320)
if v.Changed() {
k, _ := s.list.Selected()
log(fmt.Sprintf("selected %d, %d chosen", k, s.list.Selection.Len()))
@@ -138,7 +138,7 @@ func dataTableDemo(c *ui.Context, log func(string)) {
v := mujica.DataTable(c, &s.table, rows, mujica.DataTableOptions[dataRelic, int]{
Columns: cols, Key: func(r dataRelic) int { return r.ID }, Multiple: s.multiple, Status: st,
})
v.Size(620, 360)
v.Element.Size(620, 360)
ui.Text(c, fmt.Sprintf("%d of %d rows shown", s.table.Rows(), len(rows))).TextColor(k.TextMuted)
if v.SortChanged() {
log(fmt.Sprintf("sort %s descending=%v", s.table.Sort.Column, s.table.Sort.Descending))
@@ -224,7 +224,7 @@ func dataTreeDemo(c *ui.Context, log func(string)) {
ui.Checkbox(c, &s.multiple, "Multiple")
})
v := mujica.Tree(c, ts, opts)
v.Size(420, 340)
v.Element.Size(420, 340)
if v.Changed() {
n, _ := ts.Selected()
log(fmt.Sprintf("selected %s, %d chosen", n, ts.Selection.Len()))
@@ -290,7 +290,7 @@ func dataTreeTableDemo(c *ui.Context, log func(string)) {
{ID: "kids", Title: "Rooms", Width: 80, Align: ui.End, Cell: func(n string) { ui.Text(c, strconv.Itoa(len(s.lazy.kids[n]))) }},
},
})
v.Size(520, 320)
v.Element.Size(520, 320)
if v.SortChanged() {
log(fmt.Sprintf("sort %s descending=%v", s.table.Sort.Column, s.table.Sort.Descending))
}
+5 -3
View File
@@ -58,7 +58,7 @@ func dataAccordionDemo(c *ui.Context, log func(string)) {
log("closed all from outside")
}
})
if mujica.Accordion(c, &s.open, []mujica.AccordionItem{
r := mujica.Accordion(c, &s.open, []mujica.AccordionItem{
{ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices, two at the altar.") }},
{ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Pews of black oak, polished for the feast.") }},
{ID: "crypt", Title: "Crypt (disabled)", Disabled: true, Content: func() {}},
@@ -67,7 +67,9 @@ func dataAccordionDemo(c *ui.Context, log func(string)) {
log("ring the bells")
}
}},
}, mujica.AccordionOptions{Multiple: s.multiple, KeepOne: s.keepOne}).Width(420).Changed() {
}, mujica.AccordionOptions{Multiple: s.multiple, KeepOne: s.keepOne})
r.Element.Width(420)
if r.Changed() {
log(fmt.Sprintf("open %v", s.open))
}
})
@@ -235,7 +237,7 @@ func dataCarouselDemo(c *ui.Context, log func(string)) {
v := mujica.Carousel(c, &s.index, len(dataPictures), mujica.CarouselOptions{Label: "Windows", Autoplay: s.autoplay, Loop: s.loop, Interval: d}, func(i int) {
ui.Image(c, dataPictures[i].Source).Grow(1).Label(dataPictures[i].Alt)
})
v.Size(520, 300)
v.Element.Size(520, 300)
if v.Changed() {
log(fmt.Sprintf("slide %d", s.index+1))
}
+10
View File
@@ -47,3 +47,13 @@ func TestDataDemoButtonsLog(t *testing.T) {
t.Fatalf("Light candles not logged: %q", *events)
}
}
func TestDataAccordionDemoLogsChange(t *testing.T) {
tt, events := dataDemo(t, "074")
if err := tt.Click("Bells"); err != nil {
t.Fatal(err)
}
if !slices.Contains(*events, "open [bells]") {
t.Fatalf("Accordion change not logged: %q", *events)
}
}
+5 -5
View File
@@ -180,9 +180,9 @@ if r.Submitted() {
inputSection(c, "States", func() {
ui.Row(c).Gap(12).Children(func() {
e, ro, off := "1789-13-01", "Read only", "Disabled"
mujica.TextInput(c, &e, mujica.TextInputOptions{Label: "Error state", Error: "No such date"}).Grow(1)
mujica.TextInput(c, &ro, mujica.TextInputOptions{Label: "Read-only state", ReadOnly: true}).Grow(1)
mujica.TextInput(c, &off, mujica.TextInputOptions{Label: "Disabled state", Disabled: true}).Grow(1)
mujica.TextInput(c, &e, mujica.TextInputOptions{Label: "Error state", Error: "No such date"}).Element.Grow(1)
mujica.TextInput(c, &ro, mujica.TextInputOptions{Label: "Read-only state", ReadOnly: true}).Element.Grow(1)
mujica.TextInput(c, &off, mujica.TextInputOptions{Label: "Disabled state", Disabled: true}).Element.Grow(1)
})
})
}},
@@ -244,8 +244,8 @@ if r.Submitted() {
ui.Row(c).Gap(12).AlignItems(ui.Start).Children(func() {
a, b, e := 4.0, 2.0, 7.5
mujica.NumberInput(c, &a, mujica.NumberInputOptions{Min: 1, Max: 8, StepperOnly: true, Label: "Stepper state"})
mujica.NumberInput(c, &b, mujica.NumberInputOptions{Label: "Disabled state", Disabled: true}).Width(160)
mujica.NumberInput(c, &e, mujica.NumberInputOptions{Step: 0.5, Label: "Error state", Error: "Must be whole"}).Width(160)
mujica.NumberInput(c, &b, mujica.NumberInputOptions{Label: "Disabled state", Disabled: true}).Element.Width(160)
mujica.NumberInput(c, &e, mujica.NumberInputOptions{Step: 0.5, Label: "Error state", Error: "Must be whole"}).Element.Width(160)
})
})
}},
+1 -1
View File
@@ -417,7 +417,7 @@ func layoutSplitPaneDemo(c *ui.Context, log func(string)) {
Collapsible: sides[s.collapsible]},
pane("Files", "Drag the divider, or focus it and use the arrows.", k.Surface),
pane("Editor", "Enter on the divider folds the collapsible pane.", k.Background))
r.Fill()
r.Element.Fill()
if r.Resized {
log(fmt.Sprintf("resized to %.0f", s.split.Size))
}
+3 -12
View File
@@ -37,15 +37,6 @@ type FileDropZoneOptions struct {
Disabled bool
}
// FileDropZoneParts are the parts of a FileDropZone.
type FileDropZoneParts struct {
Element *ui.Element
changed bool
}
// Changed reports whether files were added or removed this frame.
func (p FileDropZoneParts) Changed() bool { return p.changed }
// fileDropZoneReject is a file that failed a check, shown until dismissed.
type fileDropZoneReject struct {
path, reason string
@@ -66,7 +57,7 @@ type fileDropZoneState struct {
}
// FileDropZone checks dropped or browsed files and lists them; it never reads or uploads them.
func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) FileDropZoneParts {
func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) ChoiceResult {
if opts.MaxFiles < 0 || opts.MaxSize < 0 {
panic("mujica: FileDropZone limits must not be negative")
}
@@ -77,7 +68,7 @@ func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions)
}
k := Tokens(c)
d := Density(c)
parts := FileDropZoneParts{}
parts := ChoiceResult{}
col := ui.Column(c).Gap(d.Unit() * 2)
parts.Element = col
st := ui.Local(col, "zone", func() fileDropZoneState { return fileDropZoneState{} })
@@ -213,7 +204,7 @@ func fileDropZoneCheck(c *ui.Context, st *fileDropZoneState) {
}
// fileDropZoneList shows accepted, checking and rejected files.
func fileDropZoneList(c *ui.Context, st *fileDropZoneState, files *[]DroppedFile, parts *FileDropZoneParts, disabled bool) {
func fileDropZoneList(c *ui.Context, st *fileDropZoneState, files *[]DroppedFile, parts *ChoiceResult, disabled bool) {
k := Tokens(c)
d := Density(c)
row := func(key string, icon string, color ui.Color, name, detail string, remove func()) {
+3 -12
View File
@@ -20,15 +20,6 @@ type ViewerImage struct {
Alt string
}
// ImageViewerView is a built ImageViewer; Element is nil while closed.
type ImageViewerView struct {
*ui.Element
changed bool
}
// Changed reports that another image was chosen.
func (v ImageViewerView) Changed() bool { return v.changed }
// Zoom limits, as multiples of the fitted size.
const (
viewerMinZoom = 0.25
@@ -42,9 +33,9 @@ type viewerState struct {
zoom, x, y float32
}
// ImageViewer shows images[*index] over the window while *open, with zoom, pan and switching.
func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ImageViewerView {
var v ImageViewerView
// ImageViewer shows images[*index] over the window while *open, with zoom, pan and switching; Element is nil while closed.
func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ChoiceResult {
var v ChoiceResult
if !*open {
return v
}
+2 -2
View File
@@ -65,7 +65,7 @@ func (s *ListState[K]) ScrollTo(i int) {
// ListView is a built List and what happened to it in this frame.
type ListView struct {
*ui.Element
Element *ui.Element
changed, submitted, retry bool
}
@@ -93,7 +93,7 @@ func List[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[
k := Tokens(c)
var v ListView
v.Element = ui.Column(c).MinHeight(0)
v.Children(func() {
v.Element.Children(func() {
if n == 0 {
v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty"))
return
+3 -3
View File
@@ -40,7 +40,7 @@ func (h *listHarness) view(c *ui.Context) {
h.built++
ui.Text(c, h.opts.Label(i)).SingleLine()
})
v.Size(h.listSize[0], h.listSize[1])
v.Element.Size(h.listSize[0], h.listSize[1])
if v.Changed() {
h.changes++
}
@@ -207,13 +207,13 @@ func TestListSnapshot(t *testing.T) {
g := ui.Local(c.Root(), "g", func() *ListState[int] { return &ListState[int]{started: true, cursor: 4} })
opts := ListOptions[int]{Key: func(i int) int { return i }, Label: func(i int) string { return names[i] }, Multiple: true}
ui.Row(c).Padding(16).Gap(16).Children(func() {
List(c, *st, len(names), opts, func(i int) { ui.Text(c, names[i]) }).Size(220, 228)
List(c, *st, len(names), opts, func(i int) { ui.Text(c, names[i]) }).Element.Size(220, 228)
gopts := opts
gopts.Grid, gopts.Multiple, gopts.ItemWidth, gopts.ItemHeight = true, false, 100, 64
List(c, *g, len(names), gopts, func(i int) {
ui.Text(c, names[i]).FontWeight(600)
ui.Text(c, "Office").TextColor(Tokens(c).TextMuted)
}).Size(360, 228)
}).Element.Size(360, 228)
})
})
}
+2 -2
View File
@@ -56,8 +56,8 @@ type MenubarOptions struct {
// MenubarResult is the built Menubar.
type MenubarResult struct {
*ui.Element
chosen string
Element *ui.Element
chosen string
}
// Chosen returns the ID of the item the user chose in this frame.
+3 -12
View File
@@ -27,17 +27,8 @@ type PaginationOptions struct {
Disabled bool
}
// PaginationResult is the built Pagination.
type PaginationResult struct {
*ui.Element
changed bool
}
// Changed reports a new page or page size in this frame, from the user or a correction.
func (r PaginationResult) Changed() bool { return r.changed }
// Pagination pages *page (from 1) through Total records, clamping it when Total shrinks.
func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) PaginationResult {
func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) ChoiceResult {
if opts.Total < 0 {
panic("mujica: Pagination Total is negative")
}
@@ -58,7 +49,7 @@ func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) Pagi
d := Density(c)
u := d.Unit()
row := ui.Row(c).AlignItems(ui.Center).Gap(u * 3).Wrap().Role(ui.RoleGroup).Label(msg(c, "pagination.label")).Disabled(opts.Disabled)
res := PaginationResult{Element: row}
res := ChoiceResult{Element: row}
pages := paginationPages(opts.Total, *pageSize)
if p := max(1, min(*page, pages)); p != *page {
slog.Debug("mujica: pagination correct", "from", *page, "to", p, "pages", pages)
@@ -166,7 +157,7 @@ func paginationButton(c *ui.Context, text string, icon *ui.SVG, name string, on,
return b
}
func paginationSizes(c *ui.Context, page, pageSize *int, sizes []int, res *PaginationResult) {
func paginationSizes(c *ui.Context, page, pageSize *int, sizes []int, res *ChoiceResult) {
k := Tokens(c)
d := Density(c)
u := d.Unit()
+2 -11
View File
@@ -25,17 +25,8 @@ type RatingOptions struct {
ShowValue bool
}
// RatingParts are the parts of a Rating.
type RatingParts struct {
Element *ui.Element
changed bool
}
// Changed reports whether the user set a new value this frame.
func (p RatingParts) Changed() bool { return p.changed }
// Rating shows *value as stars and lets the pointer and arrows set it in steps.
func Rating(c *ui.Context, value *float64, opts RatingOptions) RatingParts {
func Rating(c *ui.Context, value *float64, opts RatingOptions) ChoiceResult {
n, step := opts.Max, opts.Step
if n == 0 {
n = 5
@@ -54,7 +45,7 @@ func Rating(c *ui.Context, value *float64, opts RatingOptions) RatingParts {
if label == "" {
label = msg(c, "rating.label")
}
parts := RatingParts{}
parts := ChoiceResult{}
set := func(v float64) {
v = math.Max(0, math.Min(float64(n), math.Round(v/step)*step))
if v != *value {
+1 -1
View File
@@ -105,7 +105,7 @@ func TestRatingSnapshot(t *testing.T) {
func TestRatingHitAreaCompact(t *testing.T) {
v := 2.0
var r RatingParts
var r ChoiceResult
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{Density: theme.Compact})
ui.Column(c).AlignItems(ui.Start).Children(func() {
+2 -11
View File
@@ -37,17 +37,8 @@ type SidebarOptions struct {
Width float32
}
// SidebarResult is the built Sidebar.
type SidebarResult struct {
*ui.Element
changed bool
}
// Changed reports a new choice in this frame.
func (r SidebarResult) Changed() bool { return r.changed }
// Sidebar is a source list choosing an item ID into *selected, built on MyGo's Sidebar.
func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) SidebarResult {
func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) ChoiceResult {
k := Tokens(c)
width := opts.Width
if width == 0 {
@@ -58,7 +49,7 @@ func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts Si
}
wrap := ui.Column(c).Width(width).FillHeight().AlignItems(ui.Stretch).Shrink(0)
closed := ui.Local(wrap, "closed", func() map[string]*bool { return map[string]*bool{} })
res := SidebarResult{Element: wrap}
res := ChoiceResult{Element: wrap}
prev := *selected
wrap.Children(func() {
// MyGo draws icons in Accent: swap in readable state colors for the sidebar.
+1 -1
View File
@@ -82,7 +82,7 @@ func TestSidebarCompact(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
ui.Row(c).FillHeight().Children(func() {
w = Sidebar(c, &sel, sidebarSample(), SidebarOptions{Compact: true}).Bounds().W
w = Sidebar(c, &sel, sidebarSample(), SidebarOptions{Compact: true}).Element.Bounds().W
})
}, 400, 500)
tt.Frame()
+1 -1
View File
@@ -90,7 +90,7 @@ func TestSliderSnapshot(t *testing.T) {
ui.Column(c).Padding(20).Gap(8).Width(380).Children(func() {
Slider(c, &a, SliderOptions{Step: 5})
s := Slider(c, &b, SliderOptions{Step: 25, Marks: marks, ShowValue: true, Format: func(v float64) string { return fmt.Sprintf("%.0f%%", v) }})
s.Focus()
s.Element.Focus()
Slider(c, &d, SliderOptions{Disabled: true})
})
})
+1 -1
View File
@@ -39,7 +39,7 @@ type SplitPaneOptions struct {
// SplitPaneResult is a SplitPane and its events in this frame.
type SplitPaneResult struct {
*ui.Element
Element *ui.Element
Resized bool
Toggled bool
}
+5 -5
View File
@@ -14,7 +14,7 @@ func splitPaneTester(st *SplitPaneState, opts SplitPaneOptions, p *splitPaneProb
return ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
r := SplitPane(c, st, opts, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") })
r.Fill()
r.Element.Fill()
if r.Resized {
p.resized++
}
@@ -136,7 +136,7 @@ func TestSplitPaneCollapseSecondVertical(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
SplitPane(c, st, SplitPaneOptions{Vertical: true, MinSecond: 60, Collapsible: SplitPaneSecond},
func() { ui.Text(c, "Editor") }, func() { ui.Text(c, "Console") }).Fill()
func() { ui.Text(c, "Editor") }, func() { ui.Text(c, "Console") }).Element.Fill()
}, 300, 201)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyDown)
@@ -184,7 +184,7 @@ func TestSplitPaneSnapshot(t *testing.T) {
}
ui.Column(c).Padding(16).Children(func() {
SplitPane(c, st, SplitPaneOptions{}, pane("Files", k.Surface), pane("Editor", k.Background)).
Height(128).Border(1, k.Border).Radius(4).Clip()
Element.Height(128).Border(1, k.Border).Radius(4).Clip()
})
})
}
@@ -196,7 +196,7 @@ func TestSplitPaneSizesInsideBorder(t *testing.T) {
Use(c, Settings{})
SplitPane(c, st, SplitPaneOptions{MinSecond: 120}, func() { ui.Text(c, "Files") }, func() {
second = ui.Box(c).Fill().Absolute().Bounds()
}).Fill().Border(1, ui.RGB(0, 0, 0))
}).Element.Fill().Border(1, ui.RGB(0, 0, 0))
}, 401, 200)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnd)
@@ -216,7 +216,7 @@ 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()
SplitPane(c, st, SplitPaneOptions{}, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }).Element.Fill()
}, 400, 200)
tt.SetSize(250, 200)
// 250 less the divider and the 40 DIP second minimum.
+1 -1
View File
@@ -46,7 +46,7 @@ type StatusBarOptions struct {
// StatusBarResult is a StatusBar and its event in this frame.
type StatusBarResult struct {
*ui.Element
Element *ui.Element
// Clicked is the ID of the action item clicked, or empty.
Clicked string
}
+2 -11
View File
@@ -29,15 +29,6 @@ type StepsOptions struct {
Label string
}
// StepsResult is the built Steps.
type StepsResult struct {
*ui.Element
changed bool
}
// Changed reports that the user chose a clickable step in this frame.
func (r StepsResult) Changed() bool { return r.changed }
type stepsState int
const (
@@ -48,7 +39,7 @@ const (
)
// Steps shows a process whose step *current is under way: earlier steps are done.
func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) StepsResult {
func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceResult {
if len(steps) == 0 {
panic("mujica: Steps needs at least one step")
}
@@ -63,7 +54,7 @@ func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) StepsRe
if opts.Label != "" {
root.Label(opts.Label)
}
res := StepsResult{Element: root}
res := ChoiceResult{Element: root}
root.Children(func() {
for i, s := range steps {
if s.Title == "" {
+2 -11
View File
@@ -27,17 +27,8 @@ type TabsOptions struct {
Label string
}
// TabsResult is the built Tabs: the element holding tabs and panel.
type TabsResult struct {
*ui.Element
changed bool
}
// Changed reports a new choice made by the user in this frame.
func (r TabsResult) Changed() bool { return r.changed }
// Tabs shows tabs choosing *selected and the chosen tab's panel. Disabled tabs are skipped by the arrows.
func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) TabsResult {
func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResult {
if len(tabs) == 0 {
panic("mujica: Tabs needs at least one tab")
}
@@ -66,7 +57,7 @@ func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) TabsResult
if opts.Vertical {
root.Row()
}
res := TabsResult{Element: root}
res := ChoiceResult{Element: root}
root.Children(func() {
var strip *ui.Element
if opts.Vertical {
+1 -1
View File
@@ -87,7 +87,7 @@ func TestTabsOverflowScrolls(t *testing.T) {
tt := ui.NewTester(func(c *ui.Context) {
Use(c, Settings{})
ui.Column(c).Width(240).Children(func() {
w = Tabs(c, &sel, tabs, TabsOptions{}).Bounds().W
w = Tabs(c, &sel, tabs, TabsOptions{}).Element.Bounds().W
})
}, 400, 200)
tt.Key(0, ui.KeyTab)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// InputResult is a value input's outer element and its events.
type InputResult struct {
*ui.Element
Element *ui.Element
// Input is the text field inside, for focus and keys.
Input *ui.Element
changed bool
+1 -1
View File
@@ -12,7 +12,7 @@ import (
// ChoiceResult is a choice control's element and whether its value changed this frame.
type ChoiceResult struct {
*ui.Element
Element *ui.Element
changed bool
}
+2 -11
View File
@@ -29,22 +29,13 @@ type TransferOptions struct {
Disabled bool
}
// TransferParts are the parts of a Transfer.
type TransferParts struct {
Element *ui.Element
changed bool
}
// Changed reports whether items moved this frame.
func (p TransferParts) Changed() bool { return p.changed }
type transferState struct {
checked map[string]bool
query [2]string
}
// Transfer moves items between a source list and *target, the chosen IDs in order.
func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts TransferOptions) TransferParts {
func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts TransferOptions) ChoiceResult {
byID := make(map[string]TransferItem, len(items))
for _, it := range items {
if _, dup := byID[it.ID]; dup || it.ID == "" {
@@ -58,7 +49,7 @@ func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts Transf
}
}
d := Density(c)
parts := TransferParts{}
parts := ChoiceResult{}
row := ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Disabled(opts.Disabled)
parts.Element = row
st := ui.Local(row, "transfer", func() transferState { return transferState{checked: map[string]bool{}} })
+2 -2
View File
@@ -160,7 +160,7 @@ func (t *treeCore[K]) itemStatus(c *ui.Context, item K, status func(K) DataStatu
// TreeView is a built Tree and what happened to it in this frame.
type TreeView struct {
*ui.Element
Element *ui.Element
changed, submitted, retry bool
}
@@ -185,7 +185,7 @@ func Tree[K comparable](c *ui.Context, s *TreeState[K], opts TreeOptions[K]) Tre
t.begin(opts.Label, &s.Selection, opts.Multiple)
var v TreeView
v.Element = ui.Column(c).MinHeight(0)
v.Children(func() {
v.Element.Children(func() {
if len(opts.Roots) == 0 {
t.clear(&s.Selection)
v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty"))
+1 -1
View File
@@ -122,7 +122,7 @@ func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTable
before := s.Sort
var v DataTableView
v.Element = ui.Column(c).MinHeight(0).Gap(d.Unit() * 2)
v.Children(func() {
v.Element.Children(func() {
if len(opts.Roots) == 0 {
t.clear(&s.Selection)
v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty"))
+4 -4
View File
@@ -50,7 +50,7 @@ func (h *ttHarness) view(c *ui.Context) {
h.c = c
ui.Column(c).Padding(10).Children(func() {
v := TreeTable(c, &h.st, h.opts)
v.Size(360, 320)
v.Element.Size(360, 320)
if v.SortChanged() {
h.sorts++
}
@@ -154,7 +154,7 @@ func TestTreeTableLargeIsVirtualized(t *testing.T) {
Use(c, Settings{})
clear(rows)
TreeTable(c, &st, TreeTableOptions[int]{Roots: roots, Children: kids, Label: strconv.Itoa,
Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: cell(c), Compare: cmp.Compare[int]}, {ID: "m", Title: "M", Cell: cell(c)}}}).Size(320, 360)
Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: cell(c), Compare: cmp.Compare[int]}, {ID: "m", Title: "M", Cell: cell(c)}}}).Element.Size(320, 360)
}
tt := ui.NewTester(view, 400, 400)
const bound = 26 // see TestTreeLargeIsVirtualized
@@ -187,7 +187,7 @@ func TestTreeTableSnapshot(t *testing.T) {
})
(*h).c = c
ui.Column(c).Padding(16).Children(func() {
TreeTable(c, &(*h).st, (*h).opts).Size(388, 208)
TreeTable(c, &(*h).st, (*h).opts).Element.Size(388, 208)
})
})
}
@@ -208,7 +208,7 @@ func TestTreeTableSelectionFollowsLargeSort(t *testing.T) {
return nil
},
Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: func(n int) { ui.Text(c, "#"+strconv.Itoa(n)) }, Compare: cmp.Compare[int]}},
}).Size(320, 360)
}).Element.Size(320, 360)
}
st.SetOpen(0, true)
tt := ui.NewTester(view, 400, 400)
+4 -4
View File
@@ -53,7 +53,7 @@ func (h *treeHarness) view(c *ui.Context) {
opts.Label = func(n string) string { h.built[n] = true; return label(n) }
ui.Column(c).Padding(10).Children(func() {
v := Tree(c, &h.st, opts)
v.Size(320, 360)
v.Element.Size(320, 360)
if v.Changed() {
h.changes++
}
@@ -186,7 +186,7 @@ func TestTreeLargeIsVirtualized(t *testing.T) {
Tree(c, &st, TreeOptions[int]{Roots: roots, Children: kids, Label: func(n int) string {
built++
return fmt.Sprintf("Node %d", n)
}}).Size(320, 360)
}}).Element.Size(320, 360)
}
tt := ui.NewTester(view, 400, 400)
// Two Label calls per row; a jump builds 2×(10 rows + 2 overscan) + 2 edges.
@@ -220,7 +220,7 @@ func TestTreeSnapshot(t *testing.T) {
return h
})
ui.Column(c).Padding(16).Children(func() {
Tree(c, &(*h).st, (*h).opts).Size(360, 228)
Tree(c, &(*h).st, (*h).opts).Element.Size(360, 228)
})
})
}
@@ -234,7 +234,7 @@ func TestTreeSelectionFollowsLargeReorder(t *testing.T) {
view := func(c *ui.Context) {
Use(c, Settings{Mode: Light})
Tree(c, &st, TreeOptions[int]{Roots: roots, Children: func(int) []int { return nil },
Label: func(n int) string { return fmt.Sprintf("Node %d", n) }}).Size(320, 360)
Label: func(n int) string { return fmt.Sprintf("Node %d", n) }}).Element.Size(320, 360)
}
tt := ui.NewTester(view, 400, 400)
if err := tt.Click("Node 2"); err != nil {