WIP feedback
This commit is contained in:
25 files changed
+2141
-2
No files matched your search
@@ -0,0 +1,189 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// Notification is one in-app message. The caller owns and persists the list.
|
||||
type Notification struct {
|
||||
// ID is required and unique within the list.
|
||||
ID string
|
||||
Title string
|
||||
Body string
|
||||
// Time shows as given, as "5 min ago".
|
||||
Time string
|
||||
Read bool
|
||||
// Action labels a button; empty shows none.
|
||||
Action string
|
||||
}
|
||||
|
||||
// NotificationCenterOptions configure a NotificationCenter.
|
||||
type NotificationCenterOptions struct {
|
||||
// Title uses the built-in "Notifications" when empty.
|
||||
Title string
|
||||
}
|
||||
|
||||
// NotificationCenterResult reports the frame's events by notification ID.
|
||||
type NotificationCenterResult struct {
|
||||
Element *ui.Element
|
||||
Action string
|
||||
// Toggled changed its Read state; Opened was clicked and is now read.
|
||||
Toggled string
|
||||
Opened string
|
||||
Removed string
|
||||
// MarkedAllRead and ClearedAll report the header buttons.
|
||||
MarkedAllRead bool
|
||||
ClearedAll bool
|
||||
}
|
||||
|
||||
// NotificationCenter lists *items and edits them in place: read, unread,
|
||||
// remove, clear. The list is one Tab stop; arrows move among its buttons
|
||||
// and Delete removes the focused notification.
|
||||
func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationCenterOptions) NotificationCenterResult {
|
||||
if items == nil {
|
||||
panic("mujica: NotificationCenter needs items")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
unread := 0
|
||||
for _, n := range *items {
|
||||
if n.ID == "" || seen[n.ID] {
|
||||
panic(fmt.Sprintf("mujica: notification ID %q empty or repeated", n.ID))
|
||||
}
|
||||
seen[n.ID] = true
|
||||
if !n.Read {
|
||||
unread++
|
||||
}
|
||||
}
|
||||
k := Tokens(c)
|
||||
u := Density(c).Unit()
|
||||
title := cmp.Or(opts.Title, msg(c, "notification_center.title"))
|
||||
var res NotificationCenterResult
|
||||
panel := ui.Column(c).FillWidth().Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
|
||||
Role(ui.RoleGroup).Label(title).Clip()
|
||||
panel.Children(func() {
|
||||
ui.Row(c).AlignItems(ui.Center).Gap(u*2).Padding(u*3, u*3, u*3, u*4).Children(func() {
|
||||
ui.Text(c, title).Font(theme.SerifFont).FontSize(theme.H3Size).TextColor(k.Text)
|
||||
if unread > 0 {
|
||||
ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(theme.CaptionSize).TextColor(k.AccentText)
|
||||
}
|
||||
ui.Spacer(c)
|
||||
if Button(c, msg(c, "notification_center.mark_all_read"), ButtonOptions{Variant: Ghost, Disabled: unread == 0}).Clicked() {
|
||||
for i := range *items {
|
||||
(*items)[i].Read = true
|
||||
}
|
||||
res.MarkedAllRead = true
|
||||
slog.Debug("mujica: notification_center mark all read", "count", unread)
|
||||
}
|
||||
if Button(c, msg(c, "notification_center.clear_all"), ButtonOptions{Variant: Ghost, Disabled: len(*items) == 0}).Clicked() {
|
||||
slog.Debug("mujica: notification_center clear all", "count", len(*items))
|
||||
*items = (*items)[:0]
|
||||
res.ClearedAll = true
|
||||
}
|
||||
})
|
||||
// Ornament hairline under the panel title.
|
||||
ui.Box(c).FillWidth().Height(1).Background(k.Ornament).Role(ui.RoleNone)
|
||||
if len(*items) == 0 && !res.ClearedAll {
|
||||
EmptyState(c, EmptyStateOptions{
|
||||
Title: msg(c, "notification_center.empty"),
|
||||
Description: msg(c, "notification_center.empty_hint"),
|
||||
Icon: icons.Must("bell"),
|
||||
})
|
||||
return
|
||||
}
|
||||
list := ui.Scroll(c).Grow(1).Shrink(1).FillWidth().Role(ui.RoleList).FocusGroup(ui.Vertical | ui.Horizontal)
|
||||
list.Dividers(theme.BorderWidth, k.Border)
|
||||
list.Children(func() {
|
||||
for i := range *items {
|
||||
notificationRow(c, k, u, &(*items)[i], &res)
|
||||
}
|
||||
})
|
||||
})
|
||||
if res.Removed != "" {
|
||||
for i, n := range *items {
|
||||
if n.ID == res.Removed {
|
||||
*items = append((*items)[:i], (*items)[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
res.Element = panel
|
||||
return res
|
||||
}
|
||||
|
||||
func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, res *NotificationCenterResult) {
|
||||
name := n.Title
|
||||
if !n.Read {
|
||||
name = msg(c, "notification_center.unread_mark") + ": " + n.Title
|
||||
}
|
||||
row := ui.Row(c).Key(n.ID).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2).
|
||||
Role(ui.RoleListItem).Label(name)
|
||||
if row.Hovered() {
|
||||
row.Background(k.SurfaceHover)
|
||||
}
|
||||
row.Children(func() {
|
||||
// Unread mark: a diamond and bold title, not color alone.
|
||||
ui.Box(c).Size(12, 20).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
if !n.Read {
|
||||
p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, 8), k.AccentText)
|
||||
}
|
||||
})
|
||||
open := ui.ButtonBase(c).Grow(1).Shrink(1).Column().AlignItems(ui.Start).Justify(ui.Start).Gap(u).
|
||||
Radius(theme.ControlRadius).Label(name)
|
||||
internal.Ring(open, k.Focus, theme.ControlRadius)
|
||||
open.Children(func() {
|
||||
ui.Row(c).FillWidth().Gap(u * 2).Children(func() {
|
||||
t := ui.Text(c, n.Title).TextColor(k.Text).Grow(1).Shrink(1)
|
||||
if !n.Read {
|
||||
t.Bold()
|
||||
}
|
||||
if n.Time != "" {
|
||||
ui.Text(c, n.Time).FontSize(theme.CaptionSize).TextColor(k.TextMuted).SingleLine()
|
||||
}
|
||||
})
|
||||
if n.Body != "" {
|
||||
ui.Text(c, n.Body).TextColor(k.TextMuted).LineHeight(theme.LineHeight)
|
||||
}
|
||||
})
|
||||
if open.Clicked() && !n.Read {
|
||||
n.Read, res.Opened = true, n.ID
|
||||
slog.Debug("mujica: notification_center opened", "id", n.ID)
|
||||
}
|
||||
ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() {
|
||||
if n.Action != "" && Button(c, n.Action, ButtonOptions{Variant: Outline}).Clicked() {
|
||||
res.Action = n.ID
|
||||
slog.Debug("mujica: notification_center action", "id", n.ID)
|
||||
}
|
||||
toggle, icon := "notification_center.mark_read", icons.Must("check")
|
||||
if n.Read {
|
||||
toggle, icon = "notification_center.mark_unread", icons.Must("circle")
|
||||
}
|
||||
if notificationIconButton(c, k, u, icon, msg(c, toggle)).Clicked() {
|
||||
n.Read, res.Toggled = !n.Read, n.ID
|
||||
slog.Debug("mujica: notification_center toggled", "id", n.ID, "read", n.Read)
|
||||
}
|
||||
if notificationIconButton(c, k, u, icons.Must("x"), msg(c, "notification_center.clear")).Clicked() {
|
||||
res.Removed = n.ID
|
||||
}
|
||||
})
|
||||
})
|
||||
if row.Shortcut(0, ui.KeyDelete) || row.Shortcut(0, ui.KeyBackspace) {
|
||||
res.Removed = n.ID
|
||||
}
|
||||
if res.Removed == n.ID {
|
||||
slog.Debug("mujica: notification_center removed", "id", n.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func notificationIconButton(c *ui.Context, k theme.Tokens, u float32, icon *ui.SVG, label string) *ui.Element {
|
||||
b := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icon, Label: label})
|
||||
b.TextColor(k.TextMuted).Padding(0, u).Tooltip(label)
|
||||
return b
|
||||
}
|
||||
Reference in new issue
Block a user