app: implement borderless window functionality with drag support

- Added a new configuration option for borderless windows in the app.Config struct.
- Implemented drag functionality for borderless windows, allowing users to move the window by clicking and dragging anywhere on the surface.
- Updated the X11 window creation to support borderless mode by setting the override_redirect attribute.
- Introduced a new example demonstrating the usage of borderless windows with real-time drag capabilities.
- Enhanced the label widget to showcase standard Material Design 3 font sizes in a new demo.

These changes improve the user experience by providing a modern, flexible windowing option and enhancing the visual presentation of text elements.
This commit is contained in:
2025-10-20 10:51:47 +01:00
parent 9b360bca6a
commit 68c30b51dc
10 changed files with 933 additions and 16 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -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.

View File

@@ -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.

232
examples/borderless/main.go Normal file
View File

@@ -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{}

108
examples/label/README.md Normal file
View File

@@ -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`

133
examples/label/main.go Normal file
View File

@@ -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)},
}
}),
)
}

7
go.mod
View File

@@ -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

8
go.sum
View File

@@ -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=

267
widgets/label.go Normal file
View File

@@ -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},
}
}