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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
98
examples/borderless/README.md
Normal file
98
examples/borderless/README.md
Normal 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
232
examples/borderless/main.go
Normal 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
108
examples/label/README.md
Normal 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
133
examples/label/main.go
Normal 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
7
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
|
||||
|
||||
8
go.sum
8
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=
|
||||
|
||||
267
widgets/label.go
Normal file
267
widgets/label.go
Normal 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},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user