diff --git a/pkg/x/m3table/cmd/demo/main.go b/pkg/x/m3table/cmd/demo/main.go new file mode 100644 index 0000000..9a2da59 --- /dev/null +++ b/pkg/x/m3table/cmd/demo/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "log" + "os" + + "github.com/zodimo/go-compose/compose" + "github.com/zodimo/go-compose/pkg/api" + "github.com/zodimo/go-compose/runtime" + "github.com/zodimo/go-compose/store" + "github.com/zodimo/go-compose/theme" + + "gioui.org/app" + "gioui.org/io/system" + "gioui.org/op" + "gioui.org/unit" +) + +func main() { + go func() { + w := new(app.Window) + w.Option(app.Title("Table Demo")) + w.Option(app.Size(unit.Dp(600), unit.Dp(800))) + + if err := Run(w); err != nil { + log.Fatal(err) + } + os.Exit(0) + }() + app.Main() +} + +func Run(window *app.Window) error { + enLocale := system.Locale{Language: "en", Direction: system.LTR} + var ops op.Ops + + store := store.NewPersistentState() + store.Subscribe(func() { + window.Invalidate() + }) + + runtime := runtime.NewRuntime() + themeManager := theme.GetThemeManager() + + for { + switch frameEvent := window.Event().(type) { + case app.DestroyEvent: + return frameEvent.Err + case app.FrameEvent: + gtx := app.NewContext(&ops, frameEvent) + gtx.Locale = enLocale + gtx = themeManager.Material3ThemeInit(gtx) + + composer := compose.NewComposer(api.ComposerWithStore(store)) + + callOp := runtime.Run(gtx, composer, UI()) + callOp.Add(gtx.Ops) + frameEvent.Frame(gtx.Ops) + } + } +} diff --git a/pkg/x/m3table/cmd/demo/ui.go b/pkg/x/m3table/cmd/demo/ui.go new file mode 100644 index 0000000..e472c40 --- /dev/null +++ b/pkg/x/m3table/cmd/demo/ui.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "image/color" + + "github.com/zodimo/go-compose/compose/foundation/layout/box" + "github.com/zodimo/go-compose/compose/foundation/layout/column" + ftext "github.com/zodimo/go-compose/compose/foundation/text" + "github.com/zodimo/go-compose/compose/material3/text" + "github.com/zodimo/go-compose/compose/ui/graphics" + "github.com/zodimo/go-compose/modifiers/background" + "github.com/zodimo/go-compose/modifiers/padding" + "github.com/zodimo/go-compose/modifiers/size" + "github.com/zodimo/go-compose/pkg/api" + "github.com/zodimo/go-compose/pkg/x/m3table" +) + +func UI() api.Composable { + return func(c api.Composer) api.Composer { + + return column.Column( + c.Sequence( + // Title for Fixed Grid + text.HeadlineMedium("Table", + ftext.WithModifier(padding.All(16)), + ), + + m3table.Table( + []m3table.Column{ + { + Header: text.BodyLarge("Header 1"), + Width: 100, + }, + { + Header: text.BodyLarge("Header 2"), + Width: 200, + }, + { + Header: text.BodyLarge("Header 3"), + Width: 300, + }, + }, + 10, + func(row, col int) api.Composable { + return text.BodyLarge(fmt.Sprintf("Cell %d,%d", row, col)) + }, + ), + ), + column.WithModifier(size.FillMax()), + )(c) + } +} + +// GridItem creates a single grid item with colored background +func GridItem(index int) api.Composable { + // Simple alternating colors for visual distinction + colors := []color.NRGBA{ + {R: 234, G: 221, B: 255, A: 255}, // Primary container + {R: 232, G: 222, B: 248, A: 255}, // Secondary container + {R: 255, G: 216, B: 228, A: 255}, // Tertiary container + } + bgColor := colors[index%len(colors)] + + return box.Box( + text.TitleLarge(fmt.Sprintf("%d", index)), + box.WithModifier( + size.Height(80). + Then(size.FillMaxWidth()). + Then(background.Background(graphics.FromNRGBA(bgColor))). + Then(padding.All(8)), + ), + box.WithAlignment(box.Center), + ) +} diff --git a/pkg/x/m3table/options.go b/pkg/x/m3table/options.go new file mode 100644 index 0000000..4bf6d7e --- /dev/null +++ b/pkg/x/m3table/options.go @@ -0,0 +1,54 @@ +package m3table + +import ( + "github.com/zodimo/go-compose/compose/ui" + "github.com/zodimo/go-compose/compose/ui/unit" + "github.com/zodimo/go-compose/pkg/api" +) + +// Column defines the configuration for a single column in the table. +type Column struct { + // Header is an optional composable that defines the column header. + Header api.Composable + // Weight is the flex weight of the column. If greater than 0, the column + // scales proportionally based on its weight. + Weight int + // Width is the fixed width of the column. This is used if Weight is 0. + Width unit.Dp +} + +type TableOptions struct { + Modifier ui.Modifier + MinHeaderRowHeight unit.Dp + MinRowHeight unit.Dp +} + +type TableOption func(*TableOptions) + +func DefaultTableOptions() TableOptions { + return TableOptions{ + Modifier: ui.EmptyModifier, + MinHeaderRowHeight: 56, // Material 3 standard data table header row height + MinRowHeight: 52, // Material 3 standard data table row height + } +} + +func WithModifier(modifier ui.Modifier) TableOption { + return func(o *TableOptions) { + o.Modifier = modifier + } +} + +// WithMinHeaderRowHeight sets the minimum height for the header row. +func WithMinHeaderRowHeight(height unit.Dp) TableOption { + return func(o *TableOptions) { + o.MinHeaderRowHeight = height + } +} + +// WithMinRowHeight sets the minimum height for data rows. +func WithMinRowHeight(height unit.Dp) TableOption { + return func(o *TableOptions) { + o.MinRowHeight = height + } +} diff --git a/pkg/x/m3table/table.go b/pkg/x/m3table/table.go new file mode 100644 index 0000000..ecab3bb --- /dev/null +++ b/pkg/x/m3table/table.go @@ -0,0 +1,84 @@ +package m3table + +import ( + "github.com/zodimo/go-compose/compose" + "github.com/zodimo/go-compose/compose/foundation/layout/box" + "github.com/zodimo/go-compose/compose/foundation/layout/column" + "github.com/zodimo/go-compose/compose/foundation/layout/row" + "github.com/zodimo/go-compose/compose/material3/divider" + "github.com/zodimo/go-compose/compose/ui" + "github.com/zodimo/go-compose/modifiers/size" + "github.com/zodimo/go-compose/modifiers/weight" + "github.com/zodimo/go-compose/pkg/api" +) + +// Table constructs a basic material3 table layout given a list of columns, +// the number of data rows, and a factory function to create each cell's content. +func Table( + columns []Column, + rowCount int, + cellContent func(row, col int) api.Composable, + options ...TableOption, +) api.Composable { + + opts := DefaultTableOptions() + for _, option := range options { + if option != nil { + option(&opts) + } + } + + return func(c api.Composer) api.Composer { + + hasHeaders := false + for _, col := range columns { + if col.Header != nil { + hasHeaders = true + break + } + } + + return column.Column( + c.Sequence( + c.When(hasHeaders, func(c api.Composer) api.Composer { + return c.Sequence( + row.Row( + c.Range(len(columns), func(i int) api.Composable { + return wrapCell(columns[i], columns[i].Header) + }), + row.WithAlignment(row.Middle), + row.WithModifier(size.MinHeight(int(opts.MinHeaderRowHeight))), + ), + divider.Divider(), + )(c) + }), + c.Range(rowCount, func(r int) api.Composable { + return row.Row( + c.Range(len(columns), func(cIdx int) api.Composable { + return wrapCell(columns[cIdx], cellContent(r, cIdx)) + }), + row.WithAlignment(row.Middle), + row.WithModifier(size.MinHeight(int(opts.MinRowHeight))), + ) + }), + ), + column.WithModifier(opts.Modifier), + )(c) + } +} + +// wrapCell wraps the provided content into a box with proper width modifiers. +func wrapCell(col Column, content api.Composable) api.Composable { + var mod ui.Modifier = ui.EmptyModifier + if col.Weight > 0 { + mod = mod.Then(weight.Weight(col.Weight)) + } else if col.Width > 0 { + mod = mod.Then(size.Width(int(col.Width))) + } + + if content == nil { + content = compose.Id() + } + + return box.Box(content, box.WithModifier(mod)) +} diff --git a/test-plan.txt b/test-plan.txt new file mode 100644 index 0000000..ed5adb1 --- /dev/null +++ b/test-plan.txt @@ -0,0 +1,15 @@ +Ah, the user asked "What about row height constraints ?" +In `Table`, we currently don't constrain the row height. +Usually a table row in Material 3 has a standard minimum height, or we can add `MinHeight` to `TableOptions` or `RowOptions`. +But `pkg/x/m3table/table.go` uses `row.Row`. We can apply a `MinHeight` modifier to the `row.Row` for both headers and cells. +Let's see if there's a standard Material 3 table row height. The Material 3 spec for Data tables says: +Row height: 52dp +Header row height: 56dp + +We can add `MinHeight` modifier to the row. +Wait, let's look at `modifiers/size/constructor.go` to see if we have `MinHeight`. +Yes, `MinHeight(minHeight int) ui.Modifier`. + +Let's modify `pkg/x/m3table/table.go` to add a default `MinHeight` to rows, or maybe pass it via `TableOptions`. +Since it's a basic table, maybe we can just hardcode or provide options. +Let's provide `MinRowHeight unit.Dp` and `MinHeaderRowHeight unit.Dp` in `TableOptions`.