diff --git a/app/os.go b/app/os.go index 3d2e044c..d6ef8099 100644 --- a/app/os.go +++ b/app/os.go @@ -45,6 +45,8 @@ type Config struct { CustomRenderer bool // Decorated reports whether window decorations are provided automatically. Decorated bool + // Borderless reports whether the window has no decorations or borders. + Borderless bool // Focused reports whether has the keyboard focus. Focused bool // decoHeight is the height of the fallback decoration for platforms such diff --git a/app/os_x11.go b/app/os_x11.go index 72a73bfe..e6053116 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -40,6 +40,8 @@ import ( "time" "unsafe" + "lol.mleku.dev/log" + "github.com/mleku/fromage/f32" "github.com/mleku/fromage/io/event" "github.com/mleku/fromage/io/key" @@ -110,8 +112,14 @@ type x11Window struct { clipboard struct { content []byte } - cursor pointer.Cursor - config Config + cursor pointer.Cursor + config Config + position image.Point // Current window position + + // Drag state + dragging bool + dragStart image.Point + dragButton pointer.Buttons wakeups chan struct{} handler x11EventHandler @@ -289,6 +297,34 @@ func (w *x11Window) center() { y := (int(height) - sz.Y) / 2 C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y)) + w.position = image.Point{X: x, Y: y} +} + +func (w *x11Window) move(delta image.Point) { + newPos := w.position.Add(delta) + C.XMoveResizeWindow(w.x, w.xw, C.int(newPos.X), C.int(newPos.Y), C.uint(w.config.Size.X), C.uint(w.config.Size.Y)) + w.position = newPos +} + +func (w *x11Window) getMousePosition() image.Point { + var root C.Window + var child C.Window + var rootX, rootY C.int + var winX, winY C.int + var mask C.uint + + C.XQueryPointer(w.x, w.xw, &root, &child, &rootX, &rootY, &winX, &winY, &mask) + return image.Point{X: int(rootX), Y: int(rootY)} +} + +func (w *x11Window) checkDragStart(pos f32.Point) { + // Check if the position is over an ActionMove area + action, hasAction := w.w.ActionAt(pos) + if hasAction && action == system.ActionMove { + w.dragging = true + w.dragStart = w.getMousePosition() // Store absolute mouse position + w.dragButton = pointer.ButtonPrimary + } } func (w *x11Window) raise() { @@ -653,22 +689,42 @@ func (h *x11EventHandler) handleEvents() bool { case C.ButtonPress: w.pointerBtns |= btn ev.Buttons = w.pointerBtns + // Check if this is a drag gesture on ActionMove area + if btn == pointer.ButtonPrimary { + w.checkDragStart(ev.Position) + } case C.ButtonRelease: ev.Buttons = w.pointerBtns w.pointerBtns &^= btn + // Stop dragging if this was the drag button + if btn == w.dragButton { + w.dragging = false + } } w.ProcessEvent(ev) } case C.MotionNotify: mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) + pos := f32.Point{ + X: float32(mevt.x), + Y: float32(mevt.y), + } + + // Handle drag gesture using absolute mouse position + if w.dragging && w.pointerBtns&w.dragButton != 0 { + currentMousePos := w.getMousePosition() + delta := currentMousePos.Sub(w.dragStart) + if delta.X != 0 || delta.Y != 0 { + w.move(delta) + w.dragStart = currentMousePos + } + } + w.ProcessEvent(pointer.Event{ - Kind: pointer.Move, - Source: pointer.Mouse, - Buttons: w.pointerBtns, - Position: f32.Point{ - X: float32(mevt.x), - Y: float32(mevt.y), - }, + Kind: pointer.Move, + Source: pointer.Mouse, + Buttons: w.pointerBtns, + Position: pos, Time: time.Duration(mevt.time) * time.Millisecond, Modifiers: w.xkb.Modifiers(), }) @@ -683,10 +739,10 @@ func (h *x11EventHandler) handleEvents() bool { w.ProcessEvent(ConfigEvent{Config: w.config}) case C.EnterNotify: // mouse enters window eevt := (*C.XCrossingEvent)(unsafe.Pointer(xev)) - fmt.Printf("Window Mouse Enter: Position=(%.1f,%.1f)\n", float32(eevt.x), float32(eevt.y)) + log.T.F("Window Mouse Enter: Position=(%.1f,%.1f)", float32(eevt.x), float32(eevt.y)) case C.LeaveNotify: // mouse leaves window levt := (*C.XCrossingEvent)(unsafe.Pointer(xev)) - fmt.Printf("Window Mouse Leave: Position=(%.1f,%.1f)\n", float32(levt.x), float32(levt.y)) + log.T.F("Window Mouse Leave: Position=(%.1f,%.1f)", float32(levt.x), float32(levt.y)) case C.ConfigureNotify: // window configuration change cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size { @@ -830,6 +886,11 @@ func newX11Window(gioWin *callbacks, options []Option) error { var cnf Config cnf.apply(cfg, options) + overrideRedirect := C.int(0) + if cnf.Borderless { + overrideRedirect = C.int(1) + } + swa := C.XSetWindowAttributes{ event_mask: C.ExposureMask | C.FocusChangeMask | // update C.KeyPressMask | C.KeyReleaseMask | // keyboard @@ -838,7 +899,7 @@ func newX11Window(gioWin *callbacks, options []Option) error { C.EnterWindowMask | C.LeaveWindowMask | // mouse enter/leave window C.StructureNotifyMask, // resize background_pixmap: C.None, - override_redirect: C.False, + override_redirect: overrideRedirect, } win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy), 0, 0, C.uint(cnf.Size.X), C.uint(cnf.Size.Y), @@ -852,6 +913,7 @@ func newX11Window(gioWin *callbacks, options []Option) error { xkbEventBase: xkbEventBase, wakeups: make(chan struct{}, 1), config: Config{Size: cnf.Size}, + position: image.Point{X: 0, Y: 0}, // Initialize at origin } w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} w.notify.read = pipe[0] diff --git a/app/window.go b/app/window.go index a0ff300f..eaffe873 100644 --- a/app/window.go +++ b/app/window.go @@ -960,6 +960,14 @@ func Decorated(enabled bool) Option { } } +// Borderless creates a window without any borders or decorations. +// The window will have no title bar and no window manager controls. +func Borderless(enabled bool) Option { + return func(_ unit.Metric, cnf *Config) { + cnf.Borderless = enabled + } +} + // flushEvent is sent to detect when the user program // has completed processing of all prior events. Its an // [io/event.Event] but only for internal use. diff --git a/examples/borderless/README.md b/examples/borderless/README.md new file mode 100644 index 00000000..818ea72d --- /dev/null +++ b/examples/borderless/README.md @@ -0,0 +1,98 @@ +# Borderless Draggable Window Example + +This example demonstrates how to create a borderless window that can be dragged around by the user. + +## Features + +- **Borderless Window**: A window without any decorations, title bar, or window manager controls +- **Drag Functionality**: The entire window surface can be clicked and dragged to move the window +- **Real-time Movement**: Window position updates in real-time as you drag +- **Material Design**: Uses the widgets theme system for consistent styling +- **Centered Content**: Label is centered both horizontally and vertically + +## Window Configuration + +The window is created with: +- **Size**: 200x200 pixels +- **Borderless**: No decorations or borders +- **Draggable**: Entire surface responds to drag gestures + +## Implementation Details + +### Borderless Window Support + +The borderless functionality is implemented by: + +1. **Config Field**: Added `Borderless bool` to the `app.Config` struct +2. **Option Function**: Added `app.Borderless(enabled bool)` option +3. **X11 Backend**: Modified window creation to set `override_redirect` based on borderless config + +### Drag Functionality + +The drag functionality uses Gio's built-in system with custom X11 implementation: + +1. **Action Detection**: `widget.Decorations.LayoutMove()` marks areas as draggable using `system.ActionMove` +2. **Drag Initiation**: On left mouse button press, checks if position is over ActionMove area +3. **Position Tracking**: Uses `XQueryPointer` to get absolute screen coordinates for accurate tracking +4. **Window Movement**: Uses `XMoveResizeWindow` to update window position in real-time +5. **Drag Termination**: Stops dragging on button release + +The implementation uses absolute screen coordinates to ensure the window moves exactly with the mouse cursor, avoiding scaling issues that can occur with relative coordinates. + +```go +// Wrap entire content in LayoutMove for dragging +deco.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions { + // Window content here +}) +``` + +## Usage + +Run the example with: + +```bash +cd examples/borderless +go run main.go +``` + +## Code Structure + +```go +// Create borderless window +w.Option( + app.Size(200, 200), + app.Borderless(true), +) + +// Make entire window draggable +deco.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions { + // Fill background + paint.FillShape(gtx.Ops, theme.ColorScheme.Surface, ...) + + // Center content + return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + label := theme.NewLabel("Drag Me"). + Size(widgets.TitleLarge). + OnSurfaceColor(). + Alignment(text.Middle) + return label.Layout(gtx) + }) +}) +``` + +## Technical Notes + +- **X11 Implementation**: Uses `override_redirect` to bypass window manager decorations +- **Drag Detection**: Custom implementation tracks mouse button press/release and motion events +- **Position Tracking**: Uses `XQueryPointer` to get absolute screen coordinates for accurate movement +- **Real-time Updates**: Uses `XMoveResizeWindow` for immediate window position changes +- **Coordinate System**: Uses absolute screen coordinates to avoid scaling issues +- **Theme Integration**: Uses Material Design 3 colors for consistent appearance +- **Layout**: `layout.Center` centers content, `text.Middle` centers text within the label + +## Platform Support + +Currently implemented for: +- **Linux X11**: Full support with `override_redirect` + +Other platforms would need similar modifications to their window creation code. diff --git a/examples/borderless/main.go b/examples/borderless/main.go new file mode 100644 index 00000000..1da0772c --- /dev/null +++ b/examples/borderless/main.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "image" + "image/color" + "os" + + "github.com/mleku/fromage/app" + "github.com/mleku/fromage/font/gofont" + "github.com/mleku/fromage/io/event" + "github.com/mleku/fromage/io/pointer" + "github.com/mleku/fromage/io/system" + "github.com/mleku/fromage/layout" + "github.com/mleku/fromage/op" + "github.com/mleku/fromage/op/clip" + "github.com/mleku/fromage/op/paint" + "github.com/mleku/fromage/text" + "github.com/mleku/fromage/widget" + "github.com/mleku/fromage/widgets" +) + +func main() { + go func() { + w := &app.Window{} + w.Option( + app.Size(200, 200), + app.Borderless(true), + ) + if err := run(w); err != nil { + fmt.Printf("Error: %v\n", err) + } + os.Exit(0) + }() + app.Main() +} + +func run(w *app.Window) error { + var ops op.Ops + var theme = widgets.NewM3Theme(widgets.ThemeModeLight) + var shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) + theme.Shaper = shaper + var deco widget.Decorations + var closeTag = &closeTag{} + + for { + switch e := w.Event().(type) { + case app.DestroyEvent: + return e.Err + case app.FrameEvent: + gtx := app.NewContext(&ops, e) + + // Fill background with theme surface color + paint.FillShape(gtx.Ops, theme.ColorScheme.Surface, + clip.Rect{Max: gtx.Constraints.Max}.Op()) + + // Create 3x3 grid layout + layout.Flex{Axis: layout.Vertical}.Layout(gtx, + // Top row + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove|system.ActionMaximize) // Top-left: move + maximize + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove) // Top-center: move + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove|system.ActionMaximize) // Top-right: move + maximize + }), + ) + }), + // Middle row + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove) // Middle-left: move + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return dragArea(gtx, theme, &deco) // Middle-center-left: drag + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return closeArea(gtx, theme, closeTag) // Middle-center-right: close + }), + ) + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove) // Middle-right: move + }), + ) + }), + // Bottom row + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove|system.ActionMaximize) // Bottom-left: move + maximize + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove) // Bottom-center: move + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return resizeArea(gtx, theme, system.ActionMove|system.ActionMaximize) // Bottom-right: move + maximize + }), + ) + }), + ) + + e.Frame(&ops) + } + } +} + +// resizeArea creates a resize area with red border and system action handling +func resizeArea(gtx layout.Context, theme *widgets.M3Theme, action system.Action) layout.Dimensions { + // Create red color + redColor := color.NRGBA{R: 255, A: 255} + + // Draw red border + border := clip.Rect{Max: gtx.Constraints.Max}.Op() + paint.FillShape(gtx.Ops, redColor, border) + + // Fill interior with surface color + inner := clip.Rect{Min: image.Pt(1, 1), Max: gtx.Constraints.Max.Sub(image.Pt(1, 1))}.Op() + paint.FillShape(gtx.Ops, theme.ColorScheme.Surface, inner) + + // Add system action input + system.ActionInputOp(action).Add(gtx.Ops) + + // Handle pointer events + pointerFilter := pointer.Filter{ + Target: &action, + Kinds: pointer.Press | pointer.Drag, + } + + // Process pointer events + for { + ev, ok := gtx.Event(pointerFilter) + if !ok { + break + } + switch ev := ev.(type) { + case pointer.Event: + if ev.Kind == pointer.Press { + // Handle press events for resizing + } + } + } + + return layout.Dimensions{Size: gtx.Constraints.Max} +} + +// dragArea creates the center drag area with label +func dragArea(gtx layout.Context, theme *widgets.M3Theme, deco *widget.Decorations) layout.Dimensions { + // Create red color + redColor := color.NRGBA{R: 255, A: 255} + + // Draw red border + border := clip.Rect{Max: gtx.Constraints.Max}.Op() + paint.FillShape(gtx.Ops, redColor, border) + + // Fill interior with surface color + inner := clip.Rect{Min: image.Pt(1, 1), Max: gtx.Constraints.Max.Sub(image.Pt(1, 1))}.Op() + paint.FillShape(gtx.Ops, theme.ColorScheme.Surface, inner) + + // Use decorations for proper drag handling + return deco.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions { + // Center the "Drag" label + return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + label := theme.NewLabel("Drag"). + Size(widgets.TitleLarge). + OnSurfaceColor(). + Alignment(text.Middle) + return label.Layout(gtx) + }) + }) +} + +// closeArea creates a close button area with X and red border +func closeArea(gtx layout.Context, theme *widgets.M3Theme, tag *closeTag) layout.Dimensions { + // Create red color + redColor := color.NRGBA{R: 255, A: 255} + + // Draw red border + border := clip.Rect{Max: gtx.Constraints.Max}.Op() + paint.FillShape(gtx.Ops, redColor, border) + + // Fill interior with surface color + inner := clip.Rect{Min: image.Pt(1, 1), Max: gtx.Constraints.Max.Sub(image.Pt(1, 1))}.Op() + paint.FillShape(gtx.Ops, theme.ColorScheme.Surface, inner) + + // Set up pointer event area + area := clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops) + event.Op(gtx.Ops, tag) + area.Pop() + + // Handle pointer events for closing + pointerFilter := pointer.Filter{ + Target: tag, + Kinds: pointer.Press, + } + + // Process pointer events + for { + ev, ok := gtx.Event(pointerFilter) + if !ok { + break + } + switch ev := ev.(type) { + case pointer.Event: + if ev.Kind == pointer.Press { + // Close the program + os.Exit(0) + } + } + } + + // Center the "X" label + return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + label := theme.NewLabel("X"). + Size(widgets.TitleLarge). + OnSurfaceColor(). + Alignment(text.Middle) + return label.Layout(gtx) + }) +} + +// closeTag is a simple tag for the close button +type closeTag struct{} diff --git a/examples/label/README.md b/examples/label/README.md new file mode 100644 index 00000000..ef792b1e --- /dev/null +++ b/examples/label/README.md @@ -0,0 +1,108 @@ +# Label Widget Demo + +This demo showcases the new `widgets.Label` widget and displays all standard Material Design 3 font sizes in a vertical column layout. + +## Features + +- **Label Widget**: A new widget in the `/widgets/` directory that places text in the top-left corner of its widget area +- **Theme Integration**: The Label widget embeds the M3Theme to provide Material Design 3 theming capabilities +- **Fluent API**: Chainable methods for easy customization +- **Standard Font Sizes**: Displays all Material Design 3 typography scale sizes: + - Display sizes (Large, Medium, Small) + - Headline sizes (Large, Medium, Small) + - Title sizes (Large, Medium, Small) + - Label sizes (Large, Medium, Small) + - Body sizes (Large, Medium, Small) +- **Material Colors**: Uses theme colors for consistent Material Design 3 appearance + +## Window Size + +The demo uses a 600x1200 pixel window to accommodate all font sizes in a vertical column layout. + +## Usage + +Run the demo with: + +```bash +cd examples/label +go run main.go +``` + +## Widget API + +The `widgets.Label` provides a fluent API for creating text labels with theme integration: + +```go +// Create a theme +theme := widgets.NewM3Theme(widgets.ThemeModeLight) +theme.Shaper = shaper + +// Create a basic label using the theme +label := theme.NewLabel("Hello World") + +// Create a header label using the theme +header := theme.NewHeaderLabel("Title") + +// Customize the label using fluent chaining +customLabel := theme.NewLabel("Custom Text"). + Size(widgets.BodyLarge). + SetPrimaryColor(). + Alignment(text.Middle). + MaxLines(2) + +// Use theme colors +primaryLabel := theme.NewLabel("Primary Text").SetPrimaryColor() +errorLabel := theme.NewLabel("Error Text").SetErrorColor() +surfaceLabel := theme.NewLabel("Surface Text").SetOnSurfaceColor() + +// Layout the label (no need to pass shaper - it's embedded in the theme) +dims := label.Layout(gtx) +``` + +## Theme Color Methods + +The widget provides convenient methods for using Material Design 3 theme colors: + +- `SetPrimaryColor()` - Primary color +- `SetOnPrimaryColor()` - On-primary color +- `SetSecondaryColor()` - Secondary color +- `SetOnSecondaryColor()` - On-secondary color +- `SetErrorColor()` - Error color +- `SetOnErrorColor()` - On-error color +- `SetSurfaceColor()` - Surface color +- `SetOnSurfaceColor()` - On-surface color (default) +- `SetOnSurfaceVariantColor()` - On-surface-variant color +- `SetBackgroundColor()` - Background color +- `SetOnBackgroundColor()` - On-background color + +## Fluent API Methods + +All setters return the label for chaining: + +- `Text(string) *Label` - Set text content +- `Size(unit.Sp) *Label` - Set font size +- `Color(color.NRGBA) *Label` - Set custom color +- `Font(font.Font) *Label` - Set font face +- `Alignment(text.Alignment) *Label` - Set text alignment +- `MaxLines(int) *Label` - Set maximum lines + +## Getters + +Access current values with getter methods: + +- `GetText() string` - Get text content +- `GetSize() unit.Sp` - Get font size +- `GetColor() color.NRGBA` - Get text color +- `GetFont() font.Font` - Get font face +- `GetAlignment() text.Alignment` - Get text alignment +- `GetMaxLines() int` - Get maximum lines + +## Font Size Constants + +The widget provides standard Material Design 3 font size constants: + +- `widgets.DisplayLarge`, `widgets.DisplayMedium`, `widgets.DisplaySmall` +- `widgets.HeadlineLarge`, `widgets.HeadlineMedium`, `widgets.HeadlineSmall` +- `widgets.TitleLarge`, `widgets.TitleMedium`, `widgets.TitleSmall` +- `widgets.LabelLarge`, `widgets.LabelMedium`, `widgets.LabelSmall` +- `widgets.BodyLarge`, `widgets.BodyMedium`, `widgets.BodySmall` diff --git a/examples/label/main.go b/examples/label/main.go new file mode 100644 index 00000000..96d0e391 --- /dev/null +++ b/examples/label/main.go @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "image" + "os" + + "github.com/mleku/fromage/app" + "github.com/mleku/fromage/font/gofont" + "github.com/mleku/fromage/layout" + "github.com/mleku/fromage/op" + "github.com/mleku/fromage/op/clip" + "github.com/mleku/fromage/op/paint" + "github.com/mleku/fromage/text" + "github.com/mleku/fromage/unit" + "github.com/mleku/fromage/widgets" +) + +func main() { + go func() { + w := &app.Window{} + w.Option(app.Size(600, 1200)) + if err := run(w); err != nil { + fmt.Printf("Error: %v\n", err) + } + os.Exit(0) + }() + app.Main() +} + +func run(w *app.Window) error { + var ops op.Ops + var theme = widgets.NewM3Theme(widgets.ThemeModeLight) + var shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) + + // Set the shaper on the theme + theme.Shaper = shaper + + for { + switch e := w.Event().(type) { + case app.DestroyEvent: + return e.Err + case app.FrameEvent: + gtx := app.NewContext(&ops, e) + + // Layout the font size demo + layoutFontSizes(gtx, theme) + + e.Frame(&ops) + } + } +} + +func layoutFontSizes(gtx layout.Context, theme *widgets.M3Theme) layout.Dimensions { + // Create padding around the entire layout + padding := unit.Dp(16) + inset := layout.Inset{ + Top: padding, + Bottom: padding, + Left: padding, + Right: padding, + } + + return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + // Fill background with theme's background color + paint.FillShape(gtx.Ops, theme.ColorScheme.Background, + clip.Rect{Max: gtx.Constraints.Max}.Op()) + + // Get all standard font sizes + fontSizes := widgets.GetStandardFontSizes() + + // Create vertical flex layout for all font sizes + var children []layout.FlexChild + + for _, fontSizeInfo := range fontSizes { + // Capture the font size info in closure to avoid variable capture issues + info := fontSizeInfo + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layoutFontSizeItem(gtx, theme, info) + })) + } + + return layout.Flex{ + Axis: layout.Vertical, + }.Layout(gtx, children...) + }) +} + +func layoutFontSizeItem(gtx layout.Context, theme *widgets.M3Theme, info widgets.FontSizeInfo) layout.Dimensions { + // Create a container with border for each font size item + return layout.Flex{ + Axis: layout.Vertical, + }.Layout(gtx, + // Font size name label + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + // Create label for the font size name using fluent API + nameLabel := theme.NewLabel(info.Name). + Size(unit.Sp(12)). + OnSurfaceVariantColor() // Use theme's surface variant color for names + + return nameLabel.Layout(gtx) + }), + + // Sample text with the actual font size + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + // Create label for the sample text using fluent API + sampleLabel := theme.NewLabel("The quick brown fox jumps over the lazy dog"). + Size(info.Size). + OnSurfaceColor() // Use theme's standard text color + + return sampleLabel.Layout(gtx) + }), + + // Separator line + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + // Draw a subtle separator line using theme colors + separatorHeight := unit.Dp(1) + separatorColor := theme.ColorScheme.OutlineVariant + + paint.FillShape(gtx.Ops, separatorColor, + clip.Rect{ + Min: image.Point{X: 0, Y: 0}, + Max: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Dp(separatorHeight)}, + }.Op()) + + return layout.Dimensions{ + Size: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Dp(separatorHeight)}, + } + }), + ) +} diff --git a/go.mod b/go.mod index ac8b8d45..abfadb84 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mleku/fromage -go 1.25 +go 1.25.0 require ( eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d @@ -9,6 +9,9 @@ require ( golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/image v0.26.0 - golang.org/x/sys v0.34.0 + golang.org/x/sys v0.35.0 golang.org/x/text v0.24.0 + lol.mleku.dev v1.0.3 ) + +require github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fc37e7a9..0d33a4a2 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8v gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= @@ -13,7 +15,9 @@ golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfa golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +lol.mleku.dev v1.0.3 h1:IrqLd/wFRghu6MX7mgyKh//3VQiId2AM4RdCbFqSLnY= +lol.mleku.dev v1.0.3/go.mod h1:DQ0WnmkntA9dPLCXgvtIgYt5G0HSqx3wSTLolHgWeLA= diff --git a/widgets/label.go b/widgets/label.go new file mode 100644 index 00000000..25bd23b3 --- /dev/null +++ b/widgets/label.go @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widgets + +import ( + "image/color" + + "github.com/mleku/fromage/font" + "github.com/mleku/fromage/layout" + "github.com/mleku/fromage/op" + "github.com/mleku/fromage/op/paint" + "github.com/mleku/fromage/text" + "github.com/mleku/fromage/unit" + "github.com/mleku/fromage/widget" +) + +// Label represents a text label widget that places text in the top left corner +// of its widget area with standard Header/standard text sizes. +// It embeds M3Theme to provide Material Design 3 theming capabilities. +type Label struct { + *M3Theme + text string + size unit.Sp + color color.NRGBA + font font.Font + alignment text.Alignment + maxLines int +} + +// NewLabel creates a new label widget with default settings using the provided theme +func (t *M3Theme) NewLabel(textContent string) *Label { + return &Label{ + M3Theme: t, + text: textContent, + size: t.TextSize, // Use theme's default text size + color: t.ColorScheme.OnSurface, // Use theme's standard text color + font: font.Font{}, + alignment: text.Start, // Top-left alignment + maxLines: 0, // No limit + } +} + +// NewHeaderLabel creates a new label widget with header size using the provided theme +func (t *M3Theme) NewHeaderLabel(textContent string) *Label { + return &Label{ + M3Theme: t, + text: textContent, + size: HeadlineSmall, // Header size + color: t.ColorScheme.OnSurface, // Use theme's standard text color + font: font.Font{}, + alignment: text.Start, // Top-left alignment + maxLines: 0, // No limit + } +} + +// Text returns the text content +func (l *Label) GetText() string { + return l.text +} + +// Text sets the text content and returns the label for chaining +func (l *Label) Text(textContent string) *Label { + l.text = textContent + return l +} + +// GetSize returns the font size +func (l *Label) GetSize() unit.Sp { + return l.size +} + +// Size sets the font size and returns the label for chaining +func (l *Label) Size(size unit.Sp) *Label { + l.size = size + return l +} + +// GetColor returns the text color +func (l *Label) GetColor() color.NRGBA { + return l.color +} + +// Color sets the text color and returns the label for chaining +func (l *Label) Color(color color.NRGBA) *Label { + l.color = color + return l +} + +// GetFont returns the font face +func (l *Label) GetFont() font.Font { + return l.font +} + +// Font sets the font face and returns the label for chaining +func (l *Label) Font(font font.Font) *Label { + l.font = font + return l +} + +// GetAlignment returns the text alignment +func (l *Label) GetAlignment() text.Alignment { + return l.alignment +} + +// Alignment sets the text alignment and returns the label for chaining +func (l *Label) Alignment(alignment text.Alignment) *Label { + l.alignment = alignment + return l +} + +// GetMaxLines returns the maximum number of lines +func (l *Label) GetMaxLines() int { + return l.maxLines +} + +// MaxLines sets the maximum number of lines and returns the label for chaining +func (l *Label) MaxLines(maxLines int) *Label { + l.maxLines = maxLines + return l +} + +// SetPrimaryColor sets the text color to the theme's primary color and returns the label for chaining +func (l *Label) SetPrimaryColor() *Label { + l.color = l.ColorScheme.Primary + return l +} + +// OnPrimaryColor sets the text color to the theme's on-primary color and returns the label for chaining +func (l *Label) OnPrimaryColor() *Label { + l.color = l.ColorScheme.OnPrimary + return l +} + +// SecondaryColor sets the text color to the theme's secondary color and returns the label for chaining +func (l *Label) SecondaryColor() *Label { + l.color = l.ColorScheme.Secondary + return l +} + +// OnSecondaryColor sets the text color to the theme's on-secondary color and returns the label for chaining +func (l *Label) OnSecondaryColor() *Label { + l.color = l.ColorScheme.OnSecondary + return l +} + +// ErrorColor sets the text color to the theme's error color and returns the label for chaining +func (l *Label) ErrorColor() *Label { + l.color = l.ColorScheme.Error + return l +} + +// OnErrorColor sets the text color to the theme's on-error color and returns the label for chaining +func (l *Label) OnErrorColor() *Label { + l.color = l.ColorScheme.OnError + return l +} + +// SurfaceColor sets the text color to the theme's surface color and returns the label for chaining +func (l *Label) SurfaceColor() *Label { + l.color = l.ColorScheme.Surface + return l +} + +// OnSurfaceColor sets the text color to the theme's on-surface color and returns the label for chaining +func (l *Label) OnSurfaceColor() *Label { + l.color = l.ColorScheme.OnSurface + return l +} + +// BackgroundColor sets the text color to the theme's background color and returns the label for chaining +func (l *Label) BackgroundColor() *Label { + l.color = l.ColorScheme.Background + return l +} + +// OnSurfaceVariantColor sets the text color to the theme's on-surface-variant color and returns the label for chaining +func (l *Label) OnSurfaceVariantColor() *Label { + l.color = l.ColorScheme.OnSurfaceVariant + return l +} + +// OnBackgroundColor sets the text color to the theme's on-background color and returns the label for chaining +func (l *Label) OnBackgroundColor() *Label { + l.color = l.ColorScheme.OnBackground + return l +} + +// Layout renders the label widget +func (l *Label) Layout(gtx layout.Context) layout.Dimensions { + // Create the label widget + label := widget.Label{ + Alignment: l.alignment, + MaxLines: l.maxLines, + } + + // Create text material with the specified color + colMacro := op.Record(gtx.Ops) + paint.ColorOp{Color: l.color}.Add(gtx.Ops) + textMaterial := colMacro.Stop() + + // Layout the text using the theme's shaper + return label.Layout(gtx, l.Shaper, l.font, l.size, l.text, textMaterial) +} + +// Standard font sizes following Material Design 3 typography scale +const ( + // Display sizes + DisplayLarge unit.Sp = 57 + DisplayMedium unit.Sp = 45 + DisplaySmall unit.Sp = 36 + + // Headline sizes + HeadlineLarge unit.Sp = 32 + HeadlineMedium unit.Sp = 28 + HeadlineSmall unit.Sp = 24 + + // Title sizes + TitleLarge unit.Sp = 22 + TitleMedium unit.Sp = 16 + TitleSmall unit.Sp = 14 + + // Label sizes + LabelLarge unit.Sp = 14 + LabelMedium unit.Sp = 12 + LabelSmall unit.Sp = 11 + + // Body sizes + BodyLarge unit.Sp = 16 + BodyMedium unit.Sp = 14 + BodySmall unit.Sp = 12 +) + +// FontSizeInfo represents information about a font size +type FontSizeInfo struct { + Name string + Size unit.Sp +} + +// GetStandardFontSizes returns all standard font sizes with their names +func GetStandardFontSizes() []FontSizeInfo { + return []FontSizeInfo{ + // Display sizes + {"Display Large", DisplayLarge}, + {"Display Medium", DisplayMedium}, + {"Display Small", DisplaySmall}, + + // Headline sizes + {"Headline Large", HeadlineLarge}, + {"Headline Medium", HeadlineMedium}, + {"Headline Small", HeadlineSmall}, + + // Title sizes + {"Title Large", TitleLarge}, + {"Title Medium", TitleMedium}, + {"Title Small", TitleSmall}, + + // Label sizes + {"Label Large", LabelLarge}, + {"Label Medium", LabelMedium}, + {"Label Small", LabelSmall}, + + // Body sizes + {"Body Large", BodyLarge}, + {"Body Medium", BodyMedium}, + {"Body Small", BodySmall}, + } +}