182 lines
4.4 KiB
Go
182 lines
4.4 KiB
Go
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; a modal one also closes on a backdrop click.
|
||
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")
|
||
}
|
||
if !*open {
|
||
return nil
|
||
}
|
||
k := Tokens(c)
|
||
inset := opts.Inset
|
||
if inset == 0 {
|
||
inset = 16
|
||
}
|
||
fx, fy := overlayFractions(opts.Place)
|
||
if opts.Modal {
|
||
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()
|
||
})
|
||
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)
|
||
})
|
||
if c.Shortcut(0, ui.KeyEscape) {
|
||
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)
|
||
}
|