Files
MujicaUI/overlay.go
T

186 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mujica
import (
"log/slog"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/theme"
)
// OverlayPlace is where an Overlay sits in the window. The zero value is OverlayCenter.
type OverlayPlace int
const (
OverlayCenter OverlayPlace = iota
OverlayTop
OverlayBottom
OverlayLeft
OverlayRight
OverlayTopLeft
OverlayTopRight
OverlayBottomLeft
OverlayBottomRight
)
// OverlayOptions configure an Overlay.
type OverlayOptions struct {
Place OverlayPlace
// Inset keeps the panel from the window edges; 16 DIPs when zero.
Inset float32
// Modal traps the focus, makes the window inert and closes on a backdrop click.
Modal bool
// Dim darkens the window behind a modal overlay. It requires Modal.
Dim bool
// Label names the layer for assistive technology.
Label string
}
// Overlay shows content in a layer above the window while *open is true; Escape closes it.
func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui.Element {
if opts.Dim && !opts.Modal {
panic("mujica: Overlay Dim requires Modal")
}
if opts.Place < OverlayCenter || opts.Place > OverlayBottomRight {
panic("mujica: Overlay Place out of range")
}
site, focus := overlaySite(c)
if !*open {
focus.closed(c, site)
return nil
}
k := Tokens(c)
inset := opts.Inset
if inset == 0 {
inset = 16
}
fx, fy := overlayFractions(opts.Place)
if opts.Modal {
overlayLayer(c, open)
var backClicked bool
shown := true
panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) {
backClicked = back.Clicked()
p := overlayEnter(c, panel)
back.Padding(inset).Justify(overlayAlign(fy)).AlignItems(overlayAlign(fx))
if opts.Dim {
back.Background(overlayScrim(k).Alpha(p))
}
overlayStyle(c, panel, p)
panel.Label(opts.Label).Padding(Density(c).Unit() * 4)
content()
})
focus.shown(panel)
if !shown {
slog.Debug("mujica: overlay close", "backdrop", backClicked)
*open = false
}
return panel
}
var panel *ui.Element
ui.Overlay(c, func() {
w, h := c.Size()
panel = ui.Column(c).Absolute().Role(ui.RoleDialog).Label(opts.Label).Padding(Density(c).Unit() * 4)
b := panel.Bounds()
panel.Left(inset + (w-2*inset-b.W)*fx).Top(inset + (h-2*inset-b.H)*fy)
p := overlayEnter(c, panel)
if b.W == 0 {
p = 0 // not measured yet
c.AnimationFrame()
}
overlayStyle(c, panel, p)
panel.Children(content)
})
focus.shown(panel)
if overlayEscape(c, open) {
slog.Debug("mujica: overlay close", "escape", true)
*open = false
}
return panel
}
func overlayFractions(p OverlayPlace) (fx, fy float32) {
switch p {
case OverlayTop:
return 0.5, 0
case OverlayBottom:
return 0.5, 1
case OverlayLeft:
return 0, 0.5
case OverlayRight:
return 1, 0.5
case OverlayTopLeft:
return 0, 0
case OverlayTopRight:
return 1, 0
case OverlayBottomLeft:
return 0, 1
case OverlayBottomRight:
return 1, 1
}
return 0.5, 0.5
}
func overlayAlign(f float32) ui.Align {
switch f {
case 0:
return ui.Start
case 1:
return ui.End
}
return ui.Center
}
// overlayEnter returns how far, 0 to 1, the popup born with e has entered.
func overlayEnter(c *ui.Context, e *ui.Element) float32 {
born := ui.Local(e, "mujica.overlay.born", func() time.Time { return c.Now() })
d := Motion(c, theme.PopupDuration)
if d == 0 {
return 1
}
t := float32(c.Now().Sub(*born)) / float32(d)
if t >= 1 {
return 1
}
c.AnimationFrame()
return ui.EaseOut(t)
}
// overlayScrim is the color dimming the window behind a modal layer.
func overlayScrim(k theme.Tokens) ui.Color {
if k.Dark {
return ui.RGBA(0, 0, 0, 0.6)
}
return k.Text.Alpha(0.42)
}
// overlayStyle gives a popup panel the surface, edge and shadow of the theme.
func overlayStyle(c *ui.Context, panel *ui.Element, p float32) {
k := Tokens(c)
panel.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.PopupRadius).
TextColor(k.Text).Opacity(p).Shadow(0, 6, 18, 0, ui.RGBA(0, 0, 0, 0.16*p))
}
// overlayPlace puts a w×h box below anchor a, above when there is no room, inside the window.
func overlayPlace(c *ui.Context, a ui.Rect, w, h, gap float32) (x, y float32) {
ww, wh := c.Size()
const edge = 4
x, y = a.X, a.Y+a.H+gap
if y+h > wh-edge && a.Y-gap-h >= edge {
y = a.Y - gap - h
}
x = max(edge, min(x, ww-edge-w))
y = max(edge, min(y, wh-edge-h))
return x, y
}
// overlayAnchored is overlayEnter, held at 0 until the anchor b has been laid out.
func overlayAnchored(c *ui.Context, panel *ui.Element, b ui.Rect) float32 {
if b.W == 0 && b.H == 0 {
c.AnimationFrame()
return 0
}
return overlayEnter(c, panel)
}