114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import GLib from "gi://GLib"
|
|
import Pango from "gi://Pango"
|
|
import Gtk from "gi://Gtk?version=3.0"
|
|
import { bind, type Binding, type Variable } from "astal"
|
|
|
|
// rotating arrow button: icon turns 90deg in small steps while the section
|
|
// opens, ported from AlfheimOS; a single self-clearing chained timer drives
|
|
// the animation so cancellation only ever touches provably live sources
|
|
export function Arrow({
|
|
open,
|
|
onToggle,
|
|
}: {
|
|
open: Variable<boolean>
|
|
onToggle: () => void
|
|
}) {
|
|
let deg = 0
|
|
let timer: number | undefined
|
|
const icon = <icon icon="pan-end-symbolic" />
|
|
|
|
const animate = (target: number) => {
|
|
if (timer !== undefined) {
|
|
GLib.source_remove(timer)
|
|
timer = undefined
|
|
}
|
|
const tick = () => {
|
|
deg += Math.sign(target - deg) * 10
|
|
icon.css = `-gtk-icon-transform: rotate(${deg}deg);`
|
|
if (deg !== target) {
|
|
timer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 15, tick)
|
|
} else {
|
|
timer = undefined
|
|
}
|
|
return GLib.SOURCE_REMOVE
|
|
}
|
|
if (deg !== target) tick()
|
|
}
|
|
|
|
open.subscribe((v) => animate(v ? 90 : 0))
|
|
|
|
return (
|
|
<button className="arrow" onClicked={onToggle}>
|
|
{icon}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
type ArrowToggleButtonProps = {
|
|
open: Variable<boolean>
|
|
icon: string | Binding<string>
|
|
label: string | Binding<string>
|
|
active: Binding<boolean>
|
|
onMainClick: () => void
|
|
onArrowClick: () => void
|
|
}
|
|
|
|
// icon + label pill with a detached arrow: the pill toggles the feature,
|
|
// the arrow rotates open its device menu
|
|
export function ArrowToggleButton({
|
|
open,
|
|
icon,
|
|
label,
|
|
active,
|
|
onMainClick,
|
|
onArrowClick,
|
|
}: ArrowToggleButtonProps) {
|
|
return (
|
|
<box
|
|
className={active.as((v) => (v ? "toggle-button active" : "toggle-button"))}
|
|
>
|
|
<button className="toggle-pill" hexpand onClicked={onMainClick}>
|
|
<box hexpand>
|
|
<icon className="icon" icon={icon} />
|
|
<label
|
|
className="label"
|
|
label={label}
|
|
maxWidthChars={10}
|
|
ellipsize={Pango.EllipsizeMode.END}
|
|
/>
|
|
</box>
|
|
</button>
|
|
<Arrow open={open} onToggle={onArrowClick} />
|
|
</box>
|
|
)
|
|
}
|
|
|
|
type MenuProps = {
|
|
open: Variable<boolean>
|
|
icon: string
|
|
title: string
|
|
children: Array<JSX.Element>
|
|
}
|
|
|
|
// revealer with a titled card header, shown while the section is open
|
|
export function Menu({ open, icon, title, children }: MenuProps) {
|
|
return (
|
|
<revealer
|
|
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
|
transitionDuration={180}
|
|
revealChild={bind(open)}
|
|
>
|
|
<box className="menu" vertical>
|
|
<box className="title-box">
|
|
<icon className="icon" icon={icon} />
|
|
<label className="title" label={title} ellipsize={Pango.EllipsizeMode.END} />
|
|
</box>
|
|
<box className="menu-separator" />
|
|
<box className="content" vertical>
|
|
{children}
|
|
</box>
|
|
</box>
|
|
</revealer>
|
|
)
|
|
}
|