1888 lines
74 KiB
Rust
1888 lines
74 KiB
Rust
use crate::{
|
|
animator::{Animate, Animator, AnimatorAction, AnimatorImpl, Play},
|
|
makepad_derive_widget::*,
|
|
makepad_draw::*,
|
|
makepad_script::ScriptFnRef,
|
|
widget::*,
|
|
widget_async::{CxWidgetToScriptCallExt, ScriptAsyncResult},
|
|
};
|
|
|
|
use crate::makepad_draw::DrawSvg;
|
|
|
|
script_mod! {
|
|
use mod.prelude.widgets_internal.*
|
|
use mod.widgets.*
|
|
|
|
mod.widgets.ButtonBase = #(Button::register_widget(vm))
|
|
|
|
/** The flat button: the standard face with no outset gradient; the
|
|
* other button variants inherit from it. */
|
|
mod.widgets.ButtonFlat = set_type_default() do mod.widgets.ButtonBase{
|
|
/** the label text */
|
|
text: "Button"
|
|
width: Fit
|
|
height: Fit
|
|
/** gap between icon and label 0..24 step 1 */
|
|
spacing: theme.space_2
|
|
align: Center
|
|
/** inner face padding around icon and label */
|
|
padding: theme.mspace_1{left: theme.space_2, right: theme.space_2}
|
|
/** outer gap to neighbouring widgets */
|
|
margin: theme.mspace_v_1
|
|
/** the label's box inside the face */
|
|
label_walk: Walk{width: Fit, height: Fit}
|
|
|
|
/** The button label ink, state-mixed with the face. */
|
|
draw_text +: {
|
|
/** pointer-hover mix 0..1 step 0.01 */
|
|
hover: 0.0
|
|
/** pressed mix 0..1 step 0.01 */
|
|
down: instance(0.0)
|
|
/** keyboard-focus mix 0..1 step 0.01 */
|
|
focus: instance(0.0)
|
|
/** disabled mix 0..1 step 0.01 */
|
|
disabled: instance(0.0)
|
|
/** loading mix: fades the label out under the spinner 0..1 step 0.01 */
|
|
loading: instance(0.0)
|
|
|
|
// A button face is a box with one line of text in it: center the
|
|
// ink, not the line box, or the label reads as sitting high.
|
|
/** center the ink, not the line box */
|
|
ink_centered: true
|
|
|
|
/** label ink at rest */
|
|
color: theme.color_label_inner
|
|
/** label ink under the pointer */
|
|
color_hover: theme.color_label_inner_hover
|
|
/** label ink while held */
|
|
color_down: uniform(theme.color_label_inner_down)
|
|
/** label ink when key-focused */
|
|
color_focus: uniform(theme.color_label_inner_focus)
|
|
/** label ink when disabled */
|
|
color_disabled: uniform(theme.color_label_inner_disabled)
|
|
|
|
/** the label typeface */
|
|
text_style: theme.font_regular{
|
|
/** label type size in points 6..32 step 0.5 */
|
|
font_size: theme.font_size_p
|
|
}
|
|
/** ink mix order: focus, hover, down, disabled; then faded out while loading */
|
|
get_color: fn() {
|
|
let ink = self.color
|
|
.mix(self.color_focus, self.focus)
|
|
.mix(self.color_hover, self.hover)
|
|
.mix(self.color_down, self.down)
|
|
.mix(self.color_disabled, self.disabled)
|
|
return vec4(ink.rgb, ink.a * (1.0 - self.loading))
|
|
}
|
|
}
|
|
|
|
/** the icon's box inside the face; draw_icon paints the SVG into it */
|
|
icon_walk: Walk{/** icon width in pixels 8..64 step 1 */ width: 22.0, height: Fit}
|
|
/** the trailing icon's box after the label; draw_icon_end paints the SVG into it */
|
|
icon_end_walk: Walk{/** icon width in pixels 8..64 step 1 */ width: 22.0, height: Fit}
|
|
|
|
/** The button face material: an SDF box with a bevel stroke and an
|
|
* optional two-stop gradient fill, state-mixed by the animator's
|
|
* hover/down/focus/disabled instances. */
|
|
draw_bg +: {
|
|
/** pointer-hover mix 0..1 step 0.01 */
|
|
hover: instance(0.0)
|
|
/** keyboard-focus mix 0..1 step 0.01 */
|
|
focus: instance(0.0)
|
|
/** pressed mix 0..1 step 0.01 */
|
|
down: instance(0.0)
|
|
/** disabled mix 0..1 step 0.01 */
|
|
disabled: instance(0.0)
|
|
/** loading mix: fades the spinner arc in over the face 0..1 step 0.01 */
|
|
loading: instance(0.0)
|
|
/** the spinner's clock, ramped 0..1 once a second by the time track 0..1 step 0.01 */
|
|
anim_time: instance(0.0)
|
|
|
|
/** spinner arc ink while loading */
|
|
loading_color: uniform(theme.color_label_inner)
|
|
/** spinner arc thickness in pixels 0.5..6 step 0.25 */
|
|
loading_stroke: uniform(2.0)
|
|
/** spinner arc inset from the face edge in pixels 0..12 step 0.5 */
|
|
loading_inset: uniform(3.0)
|
|
|
|
/** bevel border thickness in pixels 0..4 step 0.5 */
|
|
border_size: uniform(theme.beveling)
|
|
/** corner rounding radius 0..24 step 0.5 */
|
|
border_radius: uniform(theme.corner_radius)
|
|
/** top-left corner; -1 follows border_radius -1..24 step 0.5 */
|
|
border_radius_tl: uniform(-1.0)
|
|
/** top-right corner; -1 follows border_radius -1..24 step 0.5 */
|
|
border_radius_tr: uniform(-1.0)
|
|
/** bottom-right corner; -1 follows border_radius -1..24 step 0.5 */
|
|
border_radius_br: uniform(-1.0)
|
|
/** bottom-left corner; -1 follows border_radius -1..24 step 0.5 */
|
|
border_radius_bl: uniform(-1.0)
|
|
|
|
/** dither the gradient fill to hide banding 0..1 step 1 */
|
|
color_dither: uniform(1.0)
|
|
/** bevel gradient axis: 0 vertical, 1 horizontal 0..1 step 1 */
|
|
gradient_border_horizontal: uniform(0.0)
|
|
/** fill gradient axis: 0 vertical, 1 horizontal 0..1 step 1 */
|
|
gradient_fill_horizontal: uniform(0.0)
|
|
|
|
/** face fill at rest */
|
|
color: uniform(theme.color_outset)
|
|
/** face fill under the pointer */
|
|
color_hover: uniform(theme.color_outset_hover)
|
|
/** face fill while held */
|
|
color_down: uniform(theme.color_outset_down)
|
|
/** face fill when key-focused */
|
|
color_focus: uniform(theme.color_outset_focus)
|
|
/** face fill when disabled */
|
|
color_disabled: uniform(theme.color_outset_disabled)
|
|
|
|
/** fill gradient end stop; negative alpha means flat fill */
|
|
color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0))
|
|
/** fill end stop under the pointer */
|
|
color_2_hover: uniform(theme.color_outset_2_hover)
|
|
/** fill end stop while held */
|
|
color_2_down: uniform(theme.color_outset_2_down)
|
|
/** fill end stop when key-focused */
|
|
color_2_focus: uniform(theme.color_outset_2_focus)
|
|
/** fill end stop when disabled */
|
|
color_2_disabled: uniform(theme.color_outset_2_disabled)
|
|
|
|
/** bevel stroke at rest */
|
|
border_color: uniform(theme.color_bevel)
|
|
/** bevel stroke under the pointer */
|
|
border_color_hover: uniform(theme.color_bevel_hover)
|
|
/** bevel stroke while held */
|
|
border_color_down: uniform(theme.color_bevel_down)
|
|
/** bevel stroke when key-focused */
|
|
border_color_focus: uniform(theme.color_bevel_focus)
|
|
/** bevel stroke when disabled */
|
|
border_color_disabled: uniform(theme.color_bevel_disabled)
|
|
|
|
/** bevel gradient end stop; negative alpha means flat stroke */
|
|
border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0))
|
|
/** bevel end stop under the pointer */
|
|
border_color_2_hover: uniform(theme.color_bevel_outset_2_hover)
|
|
/** bevel end stop while held */
|
|
border_color_2_down: uniform(theme.color_bevel_outset_2_down)
|
|
/** bevel end stop when key-focused */
|
|
border_color_2_focus: uniform(theme.color_bevel_outset_2_focus)
|
|
/** bevel end stop when disabled */
|
|
border_color_2_disabled: uniform(theme.color_bevel_outset_2_disabled)
|
|
|
|
/** state layer ink laid over the face by hover and press; negative alpha means none */
|
|
layer_color: uniform(vec4(-1.0, -1.0, -1.0, -1.0))
|
|
/** state layer opacity under the pointer 0..1 step 0.01 */
|
|
layer_hover: uniform(theme.state_hover_opacity)
|
|
/** state layer opacity added while held 0..1 step 0.01 */
|
|
layer_down: uniform(theme.state_press_opacity)
|
|
/** dash length of the bevel stroke in pixels; 0 draws it solid 0..16 step 0.5 */
|
|
border_dash: uniform(0.0)
|
|
|
|
/** the face fill for the current state: gradient resolved at
|
|
* this pixel, state-mixed, then the state layer laid over it */
|
|
face_fill: fn() -> vec4 {
|
|
let border_sz_uv = vec2(
|
|
self.border_size / self.rect_size.x
|
|
self.border_size / self.rect_size.y
|
|
)
|
|
|
|
let sz_inner_px = vec2(
|
|
self.rect_size.x - self.border_size * 2.
|
|
self.rect_size.y - self.border_size * 2.
|
|
)
|
|
|
|
let scale_factor_fill = vec2(
|
|
self.rect_size.x / sz_inner_px.x
|
|
self.rect_size.y / sz_inner_px.y
|
|
)
|
|
|
|
let mut color_fill = self.color
|
|
let mut color_fill_hover = self.color_hover
|
|
let mut color_fill_down = self.color_down
|
|
let mut color_fill_focus = self.color_focus
|
|
let mut color_fill_disabled = self.color_disabled
|
|
|
|
if self.color_2.x > -0.5 {
|
|
let dither = Math.random_2d(self.pos.xy) * /** dither grain 0..0.5 step 0.01 */ 0.04 * self.color_dither
|
|
let gradient_fill = vec2(
|
|
self.pos.x * scale_factor_fill.x - border_sz_uv.x * 2. + dither
|
|
self.pos.y * scale_factor_fill.y - border_sz_uv.y * 2. + dither
|
|
)
|
|
let dir = if self.gradient_fill_horizontal > 0.5 gradient_fill.x else gradient_fill.y
|
|
color_fill = mix(self.color, self.color_2, dir)
|
|
color_fill_hover = mix(self.color_hover, self.color_2_hover, dir)
|
|
color_fill_down = mix(self.color_down, self.color_2_down, dir)
|
|
color_fill_focus = mix(self.color_focus, self.color_2_focus, dir)
|
|
color_fill_disabled = mix(self.color_disabled, self.color_2_disabled, dir)
|
|
}
|
|
|
|
let mut fill = color_fill
|
|
.mix(color_fill_focus, self.focus)
|
|
.mix(color_fill_hover, self.hover)
|
|
.mix(color_fill_down, self.down)
|
|
.mix(color_fill_disabled, self.disabled)
|
|
|
|
if self.layer_color.x > -0.5 {
|
|
// The state layer composites over the fill with straight
|
|
// alpha, so a transparent face still shows the tint.
|
|
let layer_a = clamp(self.hover * self.layer_hover + self.down * self.layer_down, 0.0, 1.0) * (1.0 - self.disabled)
|
|
let a = fill.a + layer_a * (1.0 - fill.a)
|
|
let rgb = (fill.rgb * fill.a * (1.0 - layer_a) + self.layer_color.rgb * layer_a) / max(a, 0.0001)
|
|
fill = vec4(rgb, a)
|
|
}
|
|
return fill
|
|
}
|
|
|
|
/** the bevel stroke for the current state, dashed when border_dash is set */
|
|
face_stroke: fn() -> vec4 {
|
|
let mut color_stroke = self.border_color
|
|
let mut color_stroke_hover = self.border_color_hover
|
|
let mut color_stroke_down = self.border_color_down
|
|
let mut color_stroke_focus = self.border_color_focus
|
|
let mut color_stroke_disabled = self.border_color_disabled
|
|
|
|
if self.border_color_2.x > -0.5 {
|
|
let dither = Math.random_2d(self.pos.xy) * /** dither grain 0..0.5 step 0.01 */ 0.04 * self.color_dither
|
|
let gradient_border = vec2(
|
|
self.pos.x + dither
|
|
self.pos.y + dither
|
|
)
|
|
let dir = if self.gradient_border_horizontal > 0.5 gradient_border.x else gradient_border.y
|
|
color_stroke = mix(self.border_color, self.border_color_2, dir)
|
|
color_stroke_hover = mix(self.border_color_hover, self.border_color_2_hover, dir)
|
|
color_stroke_down = mix(self.border_color_down, self.border_color_2_down, dir)
|
|
color_stroke_focus = mix(self.border_color_focus, self.border_color_2_focus, dir)
|
|
color_stroke_disabled = mix(self.border_color_disabled, self.border_color_2_disabled, dir)
|
|
}
|
|
|
|
let mut stroke = color_stroke
|
|
.mix(color_stroke_focus, self.focus)
|
|
.mix(color_stroke_hover, self.hover)
|
|
.mix(color_stroke_down, self.down)
|
|
.mix(color_stroke_disabled, self.disabled)
|
|
|
|
if self.border_dash > 0.0 {
|
|
// Dashes along x + y: regular on every straight edge.
|
|
let p = self.pos * self.rect_size
|
|
let on = step(0.5, fract((p.x + p.y) / (self.border_dash * 2.0)))
|
|
stroke = vec4(stroke.rgb, stroke.a * on)
|
|
}
|
|
return stroke
|
|
}
|
|
|
|
/** the face: rounded SDF box, gradient fill, bevel stroke */
|
|
pixel: fn() {
|
|
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
|
|
|
|
// Each corner follows border_radius unless it has been given
|
|
// one of its own. A button group squares the two corners at
|
|
// each joint this way, so a connected row reads as one
|
|
// object; every other button leaves all four at -1 and draws
|
|
// exactly the shape it always did.
|
|
let r_tl = mix(self.border_radius, self.border_radius_tl, step(0., self.border_radius_tl))
|
|
let r_tr = mix(self.border_radius, self.border_radius_tr, step(0., self.border_radius_tr))
|
|
let r_br = mix(self.border_radius, self.border_radius_br, step(0., self.border_radius_br))
|
|
let r_bl = mix(self.border_radius, self.border_radius_bl, step(0., self.border_radius_bl))
|
|
|
|
sdf.box_all(
|
|
self.border_size
|
|
self.border_size
|
|
self.rect_size.x - self.border_size * 2.
|
|
self.rect_size.y - self.border_size * 2.
|
|
r_tl
|
|
r_tr
|
|
r_br
|
|
r_bl
|
|
)
|
|
|
|
sdf.fill_keep(self.face_fill())
|
|
sdf.stroke(self.face_stroke(), self.border_size)
|
|
|
|
// The wait spinner: a three-quarter arc turning once a second
|
|
// in the middle of the face, where the label was.
|
|
if self.loading > 0.0 {
|
|
let c = self.rect_size * 0.5
|
|
let r = max(min(self.rect_size.x, self.rect_size.y) * 0.5 - self.border_size - self.loading_inset, 1.0)
|
|
let a0 = self.anim_time * 2.0 * PI
|
|
sdf.arc_round_caps(c.x, c.y, r, a0, a0 + 1.5 * PI, self.loading_stroke)
|
|
sdf.fill(vec4(self.loading_color.rgb, self.loading_color.a * self.loading))
|
|
}
|
|
return sdf.result
|
|
}
|
|
}
|
|
|
|
/** The description ink for a compound button: a second, quieter
|
|
* line under the label. */
|
|
draw_description +: {
|
|
/** center the ink, not the line box */
|
|
ink_centered: true
|
|
/** description ink */
|
|
color: theme.color_on_surface_variant
|
|
/** the description typeface */
|
|
text_style: theme.font_regular{
|
|
/** description type size in points 6..32 step 0.5 */
|
|
font_size: theme.type_label_m_size
|
|
}
|
|
}
|
|
/** the description's box under the label */
|
|
description_walk: Walk{width: Fit, height: Fit, margin: Inset{top: 1.}}
|
|
|
|
/** the state machine driving the face and label mixes */
|
|
animator: Animator{
|
|
/** enabled/disabled track: drives the disabled mix on face and label */
|
|
disabled: {
|
|
default: @off
|
|
/** enabled: face and label at full strength, cut instantly */
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.}}
|
|
apply: {
|
|
draw_bg: {disabled: 0.0}
|
|
draw_text: {disabled: 0.0}
|
|
}
|
|
}
|
|
/** disabled: 0.2s fade of face and label to the disabled colors */
|
|
on: AnimatorState{
|
|
from: {all: Forward {duration: 0.2}}
|
|
apply: {
|
|
draw_bg: {disabled: 1.0}
|
|
draw_text: {disabled: 1.0}
|
|
}
|
|
}
|
|
}
|
|
/** free-running clock track for animated faces */
|
|
time: {
|
|
default: @off
|
|
/** clock stopped: nothing applied */
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.}}
|
|
apply: {
|
|
}
|
|
}
|
|
/** clock running: ramps draw_bg.anim_time 0..1 once per second, forever */
|
|
on: AnimatorState{
|
|
from: {all: Loop {duration: 1.0, end: 1000000000.0}}
|
|
apply: {
|
|
draw_bg: {anim_time: [{time: 0.0, value: 0.0}, {time: 1.0, value: 1.0}]}
|
|
}
|
|
}
|
|
}
|
|
/** pointer track: drives both the hover and down mixes */
|
|
hover: {
|
|
default: @off
|
|
/** pointer away: 0.1s fade of hover and down back to 0 */
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.1}}
|
|
apply: {
|
|
draw_bg: {down: 0.0, hover: 0.0}
|
|
draw_text: {down: 0.0, hover: 0.0}
|
|
}
|
|
}
|
|
|
|
/** pointer over: hover snaps to 1, down releases in 0.01s */
|
|
on: AnimatorState{
|
|
from: {
|
|
all: Forward {duration: 0.1}
|
|
down: Forward {duration: 0.01}
|
|
}
|
|
apply: {
|
|
draw_bg: {down: 0.0, hover: snap(1.0)}
|
|
draw_text: {down: 0.0, hover: snap(1.0)}
|
|
}
|
|
}
|
|
|
|
/** held down: down snaps to 1 with hover held on under it */
|
|
down: AnimatorState{
|
|
from: {all: Forward {duration: 0.2}}
|
|
apply: {
|
|
draw_bg: {down: snap(1.0), hover: 1.0}
|
|
draw_text: {down: snap(1.0), hover: 1.0}
|
|
}
|
|
}
|
|
}
|
|
/** keyboard-focus track: drives the focus mix on face and label */
|
|
focus: {
|
|
default: @off
|
|
/** focus lost: 0.2s fade of the focus mix back to 0 */
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.2}}
|
|
apply: {
|
|
draw_bg: {focus: 0.0}
|
|
draw_text: {focus: 0.0}
|
|
}
|
|
}
|
|
/** focused: focus mix on immediately, arrow cursor */
|
|
on: AnimatorState{
|
|
cursor: MouseCursor.Arrow
|
|
from: {all: Forward {duration: 0.0}}
|
|
apply: {
|
|
draw_bg: {focus: 1.0}
|
|
draw_text: {focus: 1.0}
|
|
}
|
|
}
|
|
}
|
|
/** loading track: swaps the label for the spinner arc */
|
|
loading: {
|
|
default: @off
|
|
/** not loading: 0.15s fade of the arc out and the label back */
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.15}}
|
|
apply: {
|
|
draw_bg: {loading: 0.0}
|
|
draw_text: {loading: 0.0}
|
|
}
|
|
}
|
|
/** loading: 0.15s fade of the label out and the arc in */
|
|
on: AnimatorState{
|
|
from: {all: Forward {duration: 0.15}}
|
|
apply: {
|
|
draw_bg: {loading: 1.0}
|
|
draw_text: {loading: 1.0}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The borderless button: face and bevel hidden until disabled. */
|
|
mod.widgets.ButtonFlatter = mod.widgets.ButtonFlat{
|
|
draw_bg +: {
|
|
color: theme.color_u_hidden
|
|
color_hover: theme.color_u_hidden
|
|
color_down: theme.color_u_hidden
|
|
/** the one state that paints a face: disabled */
|
|
color_disabled: theme.color_outset_disabled
|
|
|
|
border_color: theme.color_u_hidden
|
|
border_color_hover: theme.color_u_hidden
|
|
border_color_down: theme.color_u_hidden
|
|
border_color_focus: theme.color_u_hidden
|
|
border_color_disabled: theme.color_u_hidden
|
|
}
|
|
}
|
|
|
|
/** The standard button: the flat face plus the outset gradient fill
|
|
* and bevel colors from the theme. */
|
|
mod.widgets.Button = mod.widgets.ButtonFlat{
|
|
draw_bg +: {
|
|
border_color: theme.color_bevel_outset_1
|
|
border_color_hover: theme.color_bevel_outset_1_hover
|
|
border_color_down: theme.color_bevel_outset_1_down
|
|
border_color_focus: theme.color_bevel_outset_1_focus
|
|
border_color_disabled: theme.color_bevel_outset_1_disabled
|
|
|
|
/** second bevel stop: turns the flat stroke into a gradient */
|
|
border_color_2: theme.color_bevel_outset_2
|
|
border_color_2_hover: theme.color_bevel_outset_2_hover
|
|
border_color_2_down: theme.color_bevel_outset_2_down
|
|
border_color_2_focus: theme.color_bevel_outset_2_focus
|
|
border_color_2_disabled: theme.color_bevel_outset_2_disabled
|
|
}
|
|
}
|
|
|
|
/** The gradient button: the standard face filled with a two-stop
|
|
* vertical gradient instead of a flat color. */
|
|
mod.widgets.ButtonGradientX = mod.widgets.Button{
|
|
draw_bg +: {
|
|
/** gradient start stop at rest */
|
|
color: theme.color_outset_1
|
|
/** gradient start stop under the pointer */
|
|
color_hover: theme.color_outset_1_hover
|
|
/** gradient start stop while held */
|
|
color_down: theme.color_outset_1_down
|
|
/** gradient start stop when key-focused */
|
|
color_focus: theme.color_outset_1_focus
|
|
/** gradient start stop when disabled */
|
|
color_disabled: theme.color_outset_1_disabled
|
|
|
|
/** second fill stop: a positive alpha here switches the fill to a gradient */
|
|
color_2: theme.color_outset_2
|
|
}
|
|
}
|
|
|
|
/** The gradient button turned sideways: the same fill run left to right. */
|
|
mod.widgets.ButtonGradientY = mod.widgets.ButtonGradientX{
|
|
draw_bg.gradient_fill_horizontal: /** fill gradient axis: 1 horizontal 0..1 step 1 */ 1.0
|
|
}
|
|
|
|
/** The standard button carrying only an icon: no label, no gap. */
|
|
mod.widgets.ButtonIcon = mod.widgets.Button{
|
|
/** no gap: there is no label to sit beside the icon 0..24 step 1 */
|
|
spacing: 0.
|
|
/** icon only: the label is empty */
|
|
text: ""
|
|
}
|
|
|
|
/** The vertical-gradient button carrying only an icon. */
|
|
mod.widgets.ButtonGradientXIcon = mod.widgets.ButtonGradientX{
|
|
/** no gap: there is no label to sit beside the icon 0..24 step 1 */
|
|
spacing: 0.
|
|
/** icon only: the label is empty */
|
|
text: ""
|
|
}
|
|
|
|
/** The horizontal-gradient button carrying only an icon. */
|
|
mod.widgets.ButtonGradientYIcon = mod.widgets.ButtonGradientY{
|
|
/** no gap: there is no label to sit beside the icon 0..24 step 1 */
|
|
spacing: 0.
|
|
/** icon only: the label is empty */
|
|
text: ""
|
|
}
|
|
|
|
/** The flat button carrying only an icon. */
|
|
mod.widgets.ButtonFlatIcon = mod.widgets.ButtonFlat{
|
|
/** no gap: there is no label to sit beside the icon 0..24 step 1 */
|
|
spacing: 0.
|
|
/** icon only: the label is empty */
|
|
text: ""
|
|
}
|
|
|
|
/** The borderless button carrying only an icon: a bare clickable mark. */
|
|
mod.widgets.ButtonFlatterIcon = mod.widgets.ButtonFlatter{
|
|
draw_bg.color_focus: theme.color_u_hidden
|
|
/** no gap: there is no label to sit beside the icon 0..24 step 1 */
|
|
spacing: 0.
|
|
/** icon only: the label is empty */
|
|
text: ""
|
|
}
|
|
|
|
/** The filled button for the page's one main action: the primary role
|
|
* as the face, its on-colour as the ink, a state layer for hover and
|
|
* press, and a focus ring in the ink. */
|
|
mod.widgets.ButtonPrimary = mod.widgets.ButtonFlat{
|
|
draw_bg +: {
|
|
color: theme.color_primary
|
|
color_hover: theme.color_primary
|
|
color_down: theme.color_primary
|
|
color_focus: theme.color_primary
|
|
color_disabled: theme.color_outset_disabled
|
|
border_color: theme.color_primary
|
|
border_color_hover: theme.color_primary
|
|
border_color_down: theme.color_primary
|
|
border_color_focus: theme.color_on_primary
|
|
border_color_disabled: theme.color_outset_disabled
|
|
layer_color: theme.color_on_primary
|
|
}
|
|
draw_text +: {
|
|
color: theme.color_on_primary
|
|
color_hover: theme.color_on_primary
|
|
color_down: theme.color_on_primary
|
|
color_focus: theme.color_on_primary
|
|
color_disabled: theme.color_label_inner_disabled
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_on_primary
|
|
}
|
|
}
|
|
|
|
/** The filled button for the second action: the secondary role. */
|
|
mod.widgets.ButtonSecondary = mod.widgets.ButtonPrimary{
|
|
draw_bg +: {
|
|
color: theme.color_secondary
|
|
color_hover: theme.color_secondary
|
|
color_down: theme.color_secondary
|
|
color_focus: theme.color_secondary
|
|
border_color: theme.color_secondary
|
|
border_color_hover: theme.color_secondary
|
|
border_color_down: theme.color_secondary
|
|
border_color_focus: theme.color_on_secondary
|
|
layer_color: theme.color_on_secondary
|
|
}
|
|
draw_text +: {
|
|
color: theme.color_on_secondary
|
|
color_hover: theme.color_on_secondary
|
|
color_down: theme.color_on_secondary
|
|
color_focus: theme.color_on_secondary
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_on_secondary
|
|
}
|
|
}
|
|
|
|
/** The tonal button: a quieter fill from the tertiary container, for
|
|
* actions that matter but must not compete with the primary. */
|
|
mod.widgets.ButtonTertiary = mod.widgets.ButtonPrimary{
|
|
draw_bg +: {
|
|
color: theme.color_tertiary_container
|
|
color_hover: theme.color_tertiary_container
|
|
color_down: theme.color_tertiary_container
|
|
color_focus: theme.color_tertiary_container
|
|
border_color: theme.color_tertiary_container
|
|
border_color_hover: theme.color_tertiary_container
|
|
border_color_down: theme.color_tertiary_container
|
|
border_color_focus: theme.color_on_tertiary_container
|
|
layer_color: theme.color_on_tertiary_container
|
|
}
|
|
draw_text +: {
|
|
color: theme.color_on_tertiary_container
|
|
color_hover: theme.color_on_tertiary_container
|
|
color_down: theme.color_on_tertiary_container
|
|
color_focus: theme.color_on_tertiary_container
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_on_tertiary_container
|
|
}
|
|
}
|
|
|
|
/** The outlined button: no face, a one-pixel outline, primary ink,
|
|
* and the state layer tinting the inside on hover and press. */
|
|
mod.widgets.ButtonOutline = mod.widgets.ButtonFlat{
|
|
draw_bg +: {
|
|
/** outline thickness in pixels 0..4 step 0.5 */
|
|
border_size: 1.0
|
|
color: theme.color_u_hidden
|
|
color_hover: theme.color_u_hidden
|
|
color_down: theme.color_u_hidden
|
|
color_focus: theme.color_u_hidden
|
|
color_disabled: theme.color_u_hidden
|
|
border_color: theme.color_outline
|
|
border_color_hover: theme.color_outline
|
|
border_color_down: theme.color_outline
|
|
border_color_focus: theme.color_primary
|
|
border_color_disabled: theme.color_outline_variant
|
|
layer_color: theme.color_primary
|
|
}
|
|
draw_text +: {
|
|
color: theme.color_primary
|
|
color_hover: theme.color_primary
|
|
color_down: theme.color_primary
|
|
color_focus: theme.color_primary
|
|
color_disabled: theme.color_label_inner_disabled
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_primary
|
|
}
|
|
}
|
|
|
|
/** The outlined button with a dashed outline: an add-here or drop-here
|
|
* affordance rather than a firm action. */
|
|
mod.widgets.ButtonDashed = mod.widgets.ButtonOutline{
|
|
/** dash length in pixels 0..16 step 0.5 */
|
|
draw_bg.border_dash: 4.0
|
|
}
|
|
|
|
/** The destructive button: the error role as the face. */
|
|
mod.widgets.ButtonDanger = mod.widgets.ButtonPrimary{
|
|
draw_bg +: {
|
|
color: theme.color_error
|
|
color_hover: theme.color_error
|
|
color_down: theme.color_error
|
|
color_focus: theme.color_error
|
|
border_color: theme.color_error
|
|
border_color_hover: theme.color_error
|
|
border_color_down: theme.color_error
|
|
border_color_focus: theme.color_on_error
|
|
layer_color: theme.color_on_error
|
|
}
|
|
draw_text +: {
|
|
color: theme.color_on_error
|
|
color_hover: theme.color_on_error
|
|
color_down: theme.color_on_error
|
|
color_focus: theme.color_on_error
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_on_error
|
|
}
|
|
}
|
|
|
|
/** The extra-small button: the shortest rung of the control ladder,
|
|
* for dense toolbars and table rows. */
|
|
mod.widgets.ButtonXs = mod.widgets.ButtonFlat{
|
|
/** fixed face height in pixels 12..64 step 1 */
|
|
height: theme.size_control_s - theme.space_1
|
|
padding: theme.mspace_1{top: 0., bottom: 0., left: theme.space_1, right: theme.space_1}
|
|
draw_text.text_style.font_size: theme.type_label_s_size
|
|
icon_walk: Walk{width: 12.0, height: Fit}
|
|
}
|
|
|
|
/** The small button: the control ladder's small rung. */
|
|
mod.widgets.ButtonSm = mod.widgets.ButtonFlat{
|
|
/** fixed face height in pixels 12..64 step 1 */
|
|
height: theme.size_control_s
|
|
padding: theme.mspace_1{top: 0., bottom: 0., left: theme.space_2, right: theme.space_2}
|
|
draw_text.text_style.font_size: theme.type_label_m_size
|
|
icon_walk: Walk{width: 16.0, height: Fit}
|
|
}
|
|
|
|
/** The large button: the control ladder's large rung. */
|
|
mod.widgets.ButtonLg = mod.widgets.ButtonFlat{
|
|
/** fixed face height in pixels 12..64 step 1 */
|
|
height: theme.size_control_l
|
|
padding: theme.mspace_1{top: 0., bottom: 0., left: theme.space_3, right: theme.space_3}
|
|
draw_text.text_style.font_size: theme.type_title_s_size
|
|
icon_walk: Walk{width: 24.0, height: Fit}
|
|
}
|
|
|
|
/** The extra-large button: above the ladder, for hero actions. */
|
|
mod.widgets.ButtonXl = mod.widgets.ButtonFlat{
|
|
/** fixed face height in pixels 12..64 step 1 */
|
|
height: theme.size_control_l + theme.space_2
|
|
padding: theme.mspace_1{top: 0., bottom: 0., left: theme.space_4, right: theme.space_4}
|
|
draw_text.text_style.font_size: theme.type_title_m_size
|
|
icon_walk: Walk{width: 28.0, height: Fit}
|
|
}
|
|
|
|
/** The compound button: a label with a quieter description line
|
|
* under it, the icon beside both. */
|
|
mod.widgets.ButtonCompound = mod.widgets.ButtonFlat{
|
|
/** the second line under the label */
|
|
description: "Description"
|
|
padding: theme.mspace_1{left: theme.space_3, right: theme.space_3}
|
|
icon_walk: Walk{width: 28.0, height: Fit}
|
|
}
|
|
|
|
/** The tonal icon button: an icon alone on the tertiary container. */
|
|
mod.widgets.ButtonTonalIcon = mod.widgets.ButtonTertiary{
|
|
/** no gap: there is no label to sit beside the icon 0..24 step 1 */
|
|
spacing: 0.
|
|
/** icon only: the label is empty */
|
|
text: ""
|
|
padding: theme.mspace_1
|
|
}
|
|
|
|
/** The subtle icon button: an icon alone with no face until the
|
|
* pointer arrives, when the state layer tints it. */
|
|
mod.widgets.ButtonSubtleIcon = mod.widgets.ButtonFlatterIcon{
|
|
padding: theme.mspace_1
|
|
draw_bg +: {
|
|
layer_color: theme.color_on_surface
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_on_surface_variant
|
|
}
|
|
}
|
|
|
|
/** The close button: a standalone cross drawn in the face shader, with
|
|
* a round state layer under the pointer. Dialogs, toasts and drawers
|
|
* take one as their close control. */
|
|
mod.widgets.CloseButton = mod.widgets.ButtonFlatterIcon{
|
|
/** the square the cross sits in, in pixels 10..48 step 1 */
|
|
width: 20.
|
|
height: 20.
|
|
padding: 0.
|
|
margin: 0.
|
|
draw_bg +: {
|
|
border_radius: theme.radius_full
|
|
layer_color: theme.color_on_surface
|
|
/** cross span as a fraction of the shorter side 0.1..0.9 step 0.05 */
|
|
cross_size: uniform(0.42)
|
|
/** cross stroke in pixels 0.5..4 step 0.25 */
|
|
cross_stroke: uniform(1.25)
|
|
/** cross ink at rest */
|
|
cross_color: uniform(theme.color_label_inner)
|
|
/** cross ink under the pointer */
|
|
cross_color_hover: uniform(theme.color_label_inner_hover)
|
|
/** cross ink when disabled */
|
|
cross_color_disabled: uniform(theme.color_label_inner_disabled)
|
|
|
|
pixel: fn() {
|
|
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
|
|
sdf.box(
|
|
self.border_size
|
|
self.border_size
|
|
self.rect_size.x - self.border_size * 2.
|
|
self.rect_size.y - self.border_size * 2.
|
|
self.border_radius
|
|
)
|
|
sdf.fill_keep(self.face_fill())
|
|
sdf.stroke(self.face_stroke(), self.border_size)
|
|
|
|
let mid = self.rect_size * 0.5
|
|
let half = min(self.rect_size.x, self.rect_size.y) * self.cross_size * 0.5
|
|
sdf.move_to(mid.x - half, mid.y - half)
|
|
sdf.line_to(mid.x + half, mid.y + half)
|
|
sdf.move_to(mid.x - half, mid.y + half)
|
|
sdf.line_to(mid.x + half, mid.y - half)
|
|
let ink = self.cross_color
|
|
.mix(self.cross_color_hover, self.hover)
|
|
.mix(self.cross_color_disabled, self.disabled)
|
|
sdf.stroke(ink, self.cross_stroke)
|
|
return sdf.result
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The small close button, for chips and dense rows. */
|
|
mod.widgets.CloseButtonSm = mod.widgets.CloseButton{
|
|
width: 16.
|
|
height: 16.
|
|
draw_bg.cross_stroke: 1.0
|
|
}
|
|
|
|
/** The large close button, for dialogs and full-screen panels. */
|
|
mod.widgets.CloseButtonLg = mod.widgets.CloseButton{
|
|
width: 28.
|
|
height: 28.
|
|
draw_bg.cross_stroke: 1.5
|
|
}
|
|
|
|
/** The copy button: a click copies text_to_copy to the clipboard, the
|
|
* two sheets at the start of the face give way to a check, and the
|
|
* label reads copied_text for copied_secs. */
|
|
mod.widgets.CopyButton = mod.widgets.ButtonFlat{
|
|
text: "Copy"
|
|
/** the label shown after a copy */
|
|
copied_text: "Copied"
|
|
/** the face's left padding leaves room for the glyph */
|
|
padding: theme.mspace_1{left: theme.space_2 + 16., right: theme.space_2}
|
|
draw_bg +: {
|
|
/** copied mix: sheets out, check in 0..1 step 0.01 */
|
|
copied: instance(0.0)
|
|
/** glyph side in pixels 6..24 step 0.5 */
|
|
glyph_size: uniform(11.0)
|
|
/** glyph inset from the face's left edge in pixels 0..24 step 0.5 */
|
|
glyph_inset: uniform(theme.space_2)
|
|
/** glyph ink at rest */
|
|
glyph_color: uniform(theme.color_label_inner)
|
|
/** glyph ink under the pointer */
|
|
glyph_color_hover: uniform(theme.color_label_inner_hover)
|
|
/** glyph ink when disabled */
|
|
glyph_color_disabled: uniform(theme.color_label_inner_disabled)
|
|
|
|
pixel: fn() {
|
|
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
|
|
sdf.box(
|
|
self.border_size
|
|
self.border_size
|
|
self.rect_size.x - self.border_size * 2.
|
|
self.rect_size.y - self.border_size * 2.
|
|
self.border_radius
|
|
)
|
|
let face = self.face_fill()
|
|
sdf.fill_keep(face)
|
|
sdf.stroke(self.face_stroke(), self.border_size)
|
|
|
|
// The glyph zone: two stacked sheets at rest, the front one
|
|
// filled with the face so it hides the back one's edges.
|
|
let g = self.glyph_size
|
|
let gx = self.glyph_inset
|
|
let gy = (self.rect_size.y - g) * 0.5
|
|
let ink = self.glyph_color
|
|
.mix(self.glyph_color_hover, self.hover)
|
|
.mix(self.glyph_color_disabled, self.disabled)
|
|
let sheet = g * 0.66
|
|
let sheet_ink = vec4(ink.rgb, ink.a * (1.0 - self.copied))
|
|
sdf.box(gx + g - sheet, gy, sheet, sheet, 1.0)
|
|
sdf.stroke(sheet_ink, 1.0)
|
|
sdf.box(gx, gy + g - sheet, sheet, sheet, 1.0)
|
|
sdf.fill_keep(vec4(face.rgb, face.a * (1.0 - self.copied)))
|
|
sdf.stroke(sheet_ink, 1.0)
|
|
|
|
// The check, faded in once copied.
|
|
sdf.move_to(gx + g * 0.12, gy + g * 0.55)
|
|
sdf.line_to(gx + g * 0.4, gy + g * 0.82)
|
|
sdf.line_to(gx + g * 0.9, gy + g * 0.2)
|
|
sdf.stroke(vec4(ink.rgb, ink.a * self.copied), 1.5)
|
|
return sdf.result
|
|
}
|
|
}
|
|
animator +: {
|
|
/** copied track: sheets out and the check in, and back */
|
|
copied: {
|
|
default: @off
|
|
off: AnimatorState{
|
|
from: {all: Forward {duration: 0.2}}
|
|
apply: {
|
|
draw_bg: {copied: 0.0}
|
|
}
|
|
}
|
|
on: AnimatorState{
|
|
from: {all: Forward {duration: 0.15}}
|
|
apply: {
|
|
draw_bg: {copied: 1.0}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The burger button: three bars that turn into a cross as open goes
|
|
* to 1, for a drawer or a collapsed navigation. */
|
|
mod.widgets.BurgerButton = mod.widgets.ButtonFlatterIcon{
|
|
/** the square the bars sit in, in pixels 16..48 step 1 */
|
|
width: 28.
|
|
height: 28.
|
|
padding: 0.
|
|
draw_bg +: {
|
|
border_radius: theme.radius_s
|
|
layer_color: theme.color_on_surface
|
|
/** 0 draws three bars, 1 a cross; the open track drives it 0..1 step 0.01 */
|
|
open: instance(0.0)
|
|
/** bar span as a fraction of the shorter side 0.1..0.9 step 0.05 */
|
|
bar_size: uniform(0.55)
|
|
/** bar stroke in pixels 0.5..4 step 0.25 */
|
|
bar_stroke: uniform(1.5)
|
|
/** gap between the bars as a fraction of half the span 0.1..1 step 0.05 */
|
|
bar_gap: uniform(0.6)
|
|
/** bar ink at rest */
|
|
bar_color: uniform(theme.color_label_inner)
|
|
/** bar ink under the pointer */
|
|
bar_color_hover: uniform(theme.color_label_inner_hover)
|
|
/** bar ink when disabled */
|
|
bar_color_disabled: uniform(theme.color_label_inner_disabled)
|
|
|
|
pixel: fn() {
|
|
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
|
|
sdf.box(
|
|
self.border_size
|
|
self.border_size
|
|
self.rect_size.x - self.border_size * 2.
|
|
self.rect_size.y - self.border_size * 2.
|
|
self.border_radius
|
|
)
|
|
sdf.fill_keep(self.face_fill())
|
|
sdf.stroke(self.face_stroke(), self.border_size)
|
|
|
|
let mid = self.rect_size * 0.5
|
|
let half = min(self.rect_size.x, self.rect_size.y) * self.bar_size * 0.5
|
|
let gap = half * self.bar_gap * (1.0 - self.open)
|
|
let ink = self.bar_color
|
|
.mix(self.bar_color_hover, self.hover)
|
|
.mix(self.bar_color_disabled, self.disabled)
|
|
|
|
// The outer bars close on the middle and turn to the
|
|
// diagonals; the middle bar shrinks away.
|
|
let angle = self.open * PI * 0.25
|
|
sdf.rotate(angle, mid.x, mid.y)
|
|
sdf.move_to(mid.x - half, mid.y - gap)
|
|
sdf.line_to(mid.x + half, mid.y - gap)
|
|
sdf.stroke(ink, self.bar_stroke)
|
|
sdf.rotate(-2.0 * angle, mid.x, mid.y)
|
|
sdf.move_to(mid.x - half, mid.y + gap)
|
|
sdf.line_to(mid.x + half, mid.y + gap)
|
|
sdf.stroke(ink, self.bar_stroke)
|
|
sdf.rotate(angle, mid.x, mid.y)
|
|
let middle = half * (1.0 - self.open)
|
|
sdf.move_to(mid.x - middle, mid.y)
|
|
sdf.line_to(mid.x + middle, mid.y)
|
|
sdf.stroke(vec4(ink.rgb, ink.a * (1.0 - self.open)), self.bar_stroke)
|
|
return sdf.result
|
|
}
|
|
}
|
|
animator +: {
|
|
/** open track: bars to cross and back */
|
|
open: {
|
|
default: @off
|
|
off: AnimatorState{
|
|
ease: OutQuad
|
|
from: {all: Forward {duration: 0.2}}
|
|
apply: {
|
|
draw_bg: {open: 0.0}
|
|
}
|
|
}
|
|
on: AnimatorState{
|
|
ease: OutQuad
|
|
from: {all: Forward {duration: 0.2}}
|
|
apply: {
|
|
draw_bg: {open: 1.0}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The round face of a floating action: a plus drawn in the face shader
|
|
* that turns to a cross as open goes to 1, so the button that brings a
|
|
* set of actions out is also the one that puts them away. */
|
|
mod.widgets.ButtonFloating = mod.widgets.ButtonFlatIcon{
|
|
/** the disc's diameter in pixels 40..96 step 4 */
|
|
width: 56.
|
|
height: 56.
|
|
padding: 0.
|
|
margin: 0.
|
|
align: Center
|
|
draw_bg +: {
|
|
// The base declares these inputs, so they take plain values here.
|
|
color: theme.color_primary_container
|
|
color_hover: theme.color_primary_container
|
|
color_down: theme.color_primary_container
|
|
color_focus: theme.color_primary_container
|
|
color_disabled: theme.color_outset_disabled
|
|
/** the stroke the focus ring is drawn with, in pixels 0..4 step 0.5 */
|
|
border_size: 2.
|
|
border_radius: theme.radius_full
|
|
// No outline at rest: the ring appears only with key focus.
|
|
border_color: #0000
|
|
border_color_hover: #0000
|
|
border_color_down: #0000
|
|
border_color_focus: theme.color_primary
|
|
border_color_disabled: #0000
|
|
layer_color: theme.color_on_primary_container
|
|
|
|
/** 0 draws the plus, 1 the turned glyph; the open track drives it 0..1 step 0.01 */
|
|
open: instance(0.0)
|
|
/** how far the glyph turns when open, in radians 0..3.1416 step 0.01 */
|
|
turn: uniform(0.7853982)
|
|
/** plus span as a fraction of the shorter side 0.1..0.8 step 0.02 */
|
|
glyph_size: uniform(0.36)
|
|
/** glyph stroke in pixels 0.5..4 step 0.25 */
|
|
glyph_stroke: uniform(2.0)
|
|
/** glyph ink */
|
|
glyph_color: uniform(theme.color_on_primary_container)
|
|
/** glyph ink when disabled */
|
|
glyph_color_disabled: uniform(theme.color_label_inner_disabled)
|
|
|
|
pixel: fn() {
|
|
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
|
|
sdf.box(
|
|
self.border_size
|
|
self.border_size
|
|
self.rect_size.x - self.border_size * 2.
|
|
self.rect_size.y - self.border_size * 2.
|
|
self.border_radius
|
|
)
|
|
sdf.fill_keep(self.face_fill())
|
|
sdf.stroke(self.face_stroke(), self.border_size)
|
|
|
|
// One plus turned as a whole: a quarter turn of a plus is
|
|
// the same plus, so an eighth reads as the cross.
|
|
let mid = self.rect_size * 0.5
|
|
let half = min(self.rect_size.x, self.rect_size.y) * self.glyph_size * 0.5
|
|
let ink = self.glyph_color.mix(self.glyph_color_disabled, self.disabled)
|
|
sdf.rotate(self.open * self.turn, mid.x, mid.y)
|
|
sdf.move_to(mid.x - half, mid.y)
|
|
sdf.line_to(mid.x + half, mid.y)
|
|
sdf.move_to(mid.x, mid.y - half)
|
|
sdf.line_to(mid.x, mid.y + half)
|
|
sdf.stroke(ink, self.glyph_stroke)
|
|
return sdf.result
|
|
}
|
|
}
|
|
animator +: {
|
|
/** open track: the plus turns to the cross and back on the theme's motion eases, the same a floating action's set moves on; a floating action cuts this track and plays the turn on its own clock */
|
|
open: {
|
|
default: @off
|
|
off: AnimatorState{
|
|
ease: theme.motion_ease_standard_accelerate
|
|
from: {all: Forward {duration: theme.motion_short_3}}
|
|
apply: {
|
|
draw_bg: {open: 0.0}
|
|
}
|
|
}
|
|
on: AnimatorState{
|
|
ease: theme.motion_ease_emphasized_decelerate
|
|
from: {all: Forward {duration: theme.motion_short_4}}
|
|
apply: {
|
|
draw_bg: {open: 1.0}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** The small round face of a sub-action: an icon, centred on the disc. */
|
|
mod.widgets.ButtonFloatingSm = mod.widgets.ButtonFlatIcon{
|
|
/** the disc's diameter in pixels 24..64 step 2 */
|
|
width: 40.
|
|
height: 40.
|
|
padding: 0.
|
|
margin: 0.
|
|
align: Center
|
|
icon_walk: Walk{/** icon width in pixels 8..48 step 1 */ width: 20., height: Fit}
|
|
draw_bg +: {
|
|
color: theme.color_surface_container_high
|
|
color_hover: theme.color_surface_container_high
|
|
color_down: theme.color_surface_container_high
|
|
color_focus: theme.color_surface_container_high
|
|
/** the stroke the focus ring is drawn with, in pixels 0..4 step 0.5 */
|
|
border_size: 2.
|
|
border_radius: theme.radius_full
|
|
border_color: #0000
|
|
border_color_hover: #0000
|
|
border_color_down: #0000
|
|
border_color_focus: theme.color_primary
|
|
border_color_disabled: #0000
|
|
layer_color: theme.color_on_surface
|
|
}
|
|
draw_icon +: {
|
|
color: theme.color_on_surface
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Raised by a copy button once the text is on the clipboard.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub enum CopyButtonAction {
|
|
#[default]
|
|
None,
|
|
Copied,
|
|
}
|
|
|
|
/// Actions emitted by a button widget, including the key modifiers
|
|
/// that were active when the action occurred.
|
|
///
|
|
/// The sequence of actions emitted by a button is as follows:
|
|
/// 1. `ButtonAction::Pressed` when the button is pressed.
|
|
/// 2. `ButtonAction::LongPressed` when the button has been pressed for a long time.
|
|
/// * This only occurs on platforms that support a *native* long press, e.g., mobile.
|
|
/// 3. Then, either one of the following, but not both:
|
|
/// * `ButtonAction::Clicked` when the mouse/finger is lifted up while over the button area.
|
|
/// * `ButtonAction::Released` when the mouse/finger is lifted up while *not* over the button area.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub enum ButtonAction {
|
|
#[default]
|
|
None,
|
|
/// The button was pressed (a "down" event).
|
|
Pressed(KeyModifiers),
|
|
/// The button was pressed for a long time (only occurs on mobile platforms).
|
|
LongPressed,
|
|
/// The button was clicked (an "up" event).
|
|
Clicked(KeyModifiers),
|
|
/// The button was released (an "up" event), but should not be considered clicked
|
|
/// because the mouse/finger was not over the button area when released.
|
|
Released(KeyModifiers),
|
|
}
|
|
|
|
/// A clickable button widget that emits actions when pressed, and when either released or clicked.
|
|
#[derive(Script, Widget, Animator)]
|
|
pub struct Button {
|
|
#[uid]
|
|
uid: WidgetUid,
|
|
#[source]
|
|
source: ScriptObjectRef,
|
|
#[apply_default]
|
|
animator: Animator,
|
|
|
|
#[redraw]
|
|
#[live]
|
|
draw_bg: DrawQuad,
|
|
#[live]
|
|
draw_text: DrawText,
|
|
#[live]
|
|
/// Public so a host can recolour the mark directly. Going through a
|
|
/// script apply instead re-applies the whole object and drops the loaded
|
|
/// document, which renders the icon as a white silhouette.
|
|
pub draw_icon: DrawSvg,
|
|
#[live]
|
|
icon_walk: Walk,
|
|
/// The trailing icon, drawn after the label; leave the svg unset for none.
|
|
#[live]
|
|
pub draw_icon_end: DrawSvg,
|
|
#[live]
|
|
icon_end_walk: Walk,
|
|
#[live]
|
|
label_walk: Walk,
|
|
/// Per-row horizontal alignment of the label, which only shows once the text
|
|
/// wraps onto more rows; give `label_walk` a `Fill` width so the rows have
|
|
/// room to move within.
|
|
#[live]
|
|
label_align: Align,
|
|
#[walk]
|
|
walk: Walk,
|
|
|
|
/// The button is waiting: the label gives way to a spinner arc and
|
|
/// clicks are ignored until it is cleared.
|
|
#[live]
|
|
pub loading: bool,
|
|
|
|
/// Text a click puts on the clipboard; set, it makes this a copy button
|
|
/// that shows `copied_text` for `copied_secs` and raises
|
|
/// `CopyButtonAction::Copied`.
|
|
#[live]
|
|
pub text_to_copy: String,
|
|
/// The label shown after a copy; empty keeps `text`.
|
|
#[live]
|
|
pub copied_text: String,
|
|
/// How long the copied label and check stay, in seconds.
|
|
#[live(1.5)]
|
|
pub copied_secs: f64,
|
|
#[rust]
|
|
copied: bool,
|
|
#[rust]
|
|
copied_timer: Timer,
|
|
|
|
/// The burger button's state: bars while closed, a cross while open.
|
|
#[live]
|
|
pub open: bool,
|
|
|
|
#[layout]
|
|
layout: Layout,
|
|
|
|
/// The quieter second line under the label; empty draws none.
|
|
#[live]
|
|
pub description: String,
|
|
/// The description's ink.
|
|
#[live]
|
|
draw_description: DrawText,
|
|
/// The description's box under the label.
|
|
#[live]
|
|
description_walk: Walk,
|
|
|
|
#[live(true)]
|
|
grab_key_focus: bool,
|
|
|
|
#[live(true)]
|
|
enabled: bool,
|
|
|
|
#[live(true)]
|
|
#[visible]
|
|
visible: bool,
|
|
|
|
/// Set the long-press handling behavior of this button.
|
|
/// * If `false` (default), the button will ignore long-press events
|
|
/// and will never emit [`ButtonAction::LongPressed`].
|
|
/// * Also, the button logic will *not* call [`FingerUpEvent::was_tap()`]
|
|
/// to check if the button press was a short tap.
|
|
/// This means that this button will consider itself to be clicked
|
|
/// (and thus emit a [`ButtonAction::Clicked`] event)
|
|
/// if the finger-up/release event occurs within the button area,
|
|
/// *regardless* of how long the button was pressed down before it was released.
|
|
/// * If `true`, the button will respond to a long-press event
|
|
/// by emitting [`ButtonAction::LongPressed`], which can only occur on
|
|
/// mobile platforms that support a *native* long press event.
|
|
/// * Also, the button will only consider itself to be clicked
|
|
/// (and thus emit [`ButtonAction::Clicked`]) if [`FingerUpEvent::was_tap()`] returns `true`,
|
|
/// meaning that a long press did *not* occur and that the button was released over the button area
|
|
/// within a short time frame (~0.5 seconds) after the initial down press.
|
|
#[live]
|
|
pub enable_long_press: bool,
|
|
|
|
/// It indicates if the hover state will be reset when the button is clicked.
|
|
/// This could be useful for buttons that disappear when clicked, where the hover state
|
|
/// should not be preserved.
|
|
#[live]
|
|
reset_hover_on_click: bool,
|
|
|
|
#[live]
|
|
pub text: ArcStringMut,
|
|
|
|
#[live]
|
|
on_click: ScriptFnRef,
|
|
|
|
#[live]
|
|
on_press: ScriptFnRef,
|
|
|
|
/// Legacy compatibility flag that fires `on_click` on press instead of click.
|
|
#[live]
|
|
trigger_on_press: bool,
|
|
|
|
#[action_data]
|
|
#[rust]
|
|
action_data: WidgetActionData,
|
|
}
|
|
|
|
impl ScriptHook for Button {
|
|
/// Every apply that is not an animation frame may have set `loading`
|
|
/// or `open` from the script side; bring the animator in line with
|
|
/// them. A new or reloaded button cuts to the state, an edit animates
|
|
/// into it.
|
|
fn on_after_apply(
|
|
&mut self,
|
|
vm: &mut ScriptVm,
|
|
apply: &Apply,
|
|
_scope: &mut Scope,
|
|
_value: ScriptValue,
|
|
) {
|
|
if apply.is_animate() {
|
|
return;
|
|
}
|
|
let animate = if apply.is_eval() { Animate::Yes } else { Animate::No };
|
|
vm.with_cx_mut(|cx| {
|
|
self.sync_loading(cx, animate);
|
|
self.sync_open(cx, animate);
|
|
});
|
|
}
|
|
}
|
|
|
|
impl Widget for Button {
|
|
fn script_call(
|
|
&mut self,
|
|
vm: &mut ScriptVm,
|
|
method: LiveId,
|
|
args: ScriptValue,
|
|
) -> ScriptAsyncResult {
|
|
if method == live_id!(text) {
|
|
let str_val = vm.bx.heap.new_string_from_str(self.text.as_ref());
|
|
return ScriptAsyncResult::Return(str_val.into());
|
|
}
|
|
if method == live_id!(set_text) {
|
|
if let Some(args_obj) = args.as_object() {
|
|
let trap = vm.bx.threads.cur().trap.pass();
|
|
let value = vm.bx.heap.vec_value(args_obj, 0, trap);
|
|
if !value.is_err() {
|
|
if let Some(new_text) = vm
|
|
.bx
|
|
.heap
|
|
.cast_to_owned_string(value, "copying button text")
|
|
{
|
|
vm.with_cx_mut(|cx| {
|
|
self.set_text(cx, &new_text);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return ScriptAsyncResult::Return(NIL);
|
|
}
|
|
if method == live_id!(on_click) {
|
|
let uid = self.widget_uid();
|
|
vm.with_cx_mut(|cx| {
|
|
cx.widget_to_script_call(uid, NIL, self.source.clone(), self.on_click.clone(), &[]);
|
|
});
|
|
return ScriptAsyncResult::Return(TRUE);
|
|
}
|
|
if method == live_id!(on_press) {
|
|
let uid = self.widget_uid();
|
|
vm.with_cx_mut(|cx| {
|
|
cx.widget_to_script_call(uid, NIL, self.source.clone(), self.on_press.clone(), &[]);
|
|
});
|
|
return ScriptAsyncResult::Return(TRUE);
|
|
}
|
|
ScriptAsyncResult::MethodNotFound
|
|
}
|
|
|
|
fn set_disabled(&mut self, cx: &mut Cx, disabled: bool) {
|
|
self.animator_toggle(
|
|
cx,
|
|
disabled,
|
|
Animate::Yes,
|
|
ids!(disabled.on),
|
|
ids!(disabled.off),
|
|
);
|
|
}
|
|
|
|
fn disabled(&self, cx: &Cx) -> bool {
|
|
self.animator_in_state(cx, ids!(disabled.on))
|
|
}
|
|
|
|
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
|
|
let uid = self.widget_uid();
|
|
if self.animator_handle_event(cx, event).must_redraw() {
|
|
self.draw_bg.redraw(cx);
|
|
}
|
|
|
|
if let Event::ClearHover = event {
|
|
self.animator_cut(cx, ids!(hover.off));
|
|
}
|
|
|
|
if self.copied_timer.is_event(event).is_some() {
|
|
self.copied = false;
|
|
self.animator_play(cx, ids!(copied.off));
|
|
self.draw_bg.redraw(cx);
|
|
}
|
|
|
|
// The button only handles hits when it's visible and enabled.
|
|
// If it's not enabled, we still show the button, but we set
|
|
// the NotAllowed mouse cursor upon hover instead of the Hand cursor.
|
|
// While loading it takes no presses either, and shows the wait cursor.
|
|
let takes_input = self.enabled && !self.loading;
|
|
match event.hits(cx, self.draw_bg.area()) {
|
|
Hit::KeyFocus(_) => {
|
|
self.animator_play(cx, ids!(focus.on));
|
|
}
|
|
Hit::KeyFocusLost(_) => {
|
|
self.animator_play(cx, ids!(focus.off));
|
|
self.draw_bg.redraw(cx);
|
|
}
|
|
Hit::FingerDown(fe) if takes_input && fe.is_primary_hit() => {
|
|
if self.grab_key_focus {
|
|
cx.set_key_focus(self.draw_bg.area());
|
|
}
|
|
cx.widget_action_with_data(
|
|
&self.action_data,
|
|
uid,
|
|
ButtonAction::Pressed(fe.modifiers),
|
|
);
|
|
cx.widget_to_script_call(uid, NIL, self.source.clone(), self.on_press.clone(), &[]);
|
|
if self.trigger_on_press {
|
|
cx.widget_to_script_call(
|
|
uid,
|
|
NIL,
|
|
self.source.clone(),
|
|
self.on_click.clone(),
|
|
&[],
|
|
);
|
|
}
|
|
self.animator_play(cx, ids!(hover.down));
|
|
}
|
|
Hit::FingerHoverIn(_) => {
|
|
if takes_input {
|
|
cx.set_cursor(MouseCursor::Hand);
|
|
self.animator_play(cx, ids!(hover.on));
|
|
} else if self.loading {
|
|
cx.set_cursor(MouseCursor::Wait);
|
|
} else {
|
|
cx.set_cursor(MouseCursor::NotAllowed);
|
|
}
|
|
}
|
|
Hit::FingerHoverOut(_) => {
|
|
self.animator_play(cx, ids!(hover.off));
|
|
}
|
|
Hit::FingerLongPress(_lp) if takes_input && self.enable_long_press => {
|
|
cx.widget_action_with_data(&self.action_data, uid, ButtonAction::LongPressed);
|
|
}
|
|
Hit::FingerUp(fe) if takes_input && fe.is_primary_hit() => {
|
|
let was_clicked = fe.is_over
|
|
&& if self.enable_long_press {
|
|
fe.was_tap()
|
|
} else {
|
|
true
|
|
};
|
|
if was_clicked {
|
|
cx.widget_action_with_data(
|
|
&self.action_data,
|
|
uid,
|
|
ButtonAction::Clicked(fe.modifiers),
|
|
);
|
|
if !self.trigger_on_press {
|
|
cx.widget_to_script_call(
|
|
uid,
|
|
NIL,
|
|
self.source.clone(),
|
|
self.on_click.clone(),
|
|
&[],
|
|
);
|
|
}
|
|
if !self.text_to_copy.is_empty() {
|
|
self.copy_now(cx);
|
|
}
|
|
if self.reset_hover_on_click {
|
|
self.animator_cut(cx, ids!(hover.off));
|
|
} else if fe.has_hovers() {
|
|
self.animator_play(cx, ids!(hover.on));
|
|
} else {
|
|
self.animator_play(cx, ids!(hover.off));
|
|
}
|
|
} else {
|
|
cx.widget_action_with_data(
|
|
&self.action_data,
|
|
uid,
|
|
ButtonAction::Released(fe.modifiers),
|
|
);
|
|
self.animator_play(cx, ids!(hover.off));
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
|
|
if !self.visible {
|
|
return DrawStep::done();
|
|
}
|
|
|
|
self.draw_bg.begin(cx, walk, self.layout);
|
|
self.draw_icon.draw_walk(cx, self.icon_walk);
|
|
self.draw_label(cx);
|
|
self.draw_icon_end.draw_walk(cx, self.icon_end_walk);
|
|
self.draw_bg.end(cx);
|
|
cx.add_nav_stop(self.draw_bg.area(), NavRole::TextInput, Inset::default());
|
|
DrawStep::done()
|
|
}
|
|
|
|
/// The label as shown: the copied text while a copy is fresh, else `text`.
|
|
fn text(&self) -> String {
|
|
self.shown_label().to_string()
|
|
}
|
|
|
|
fn set_text(&mut self, cx: &mut Cx, v: &str) {
|
|
if self.text.as_ref() == v { return }
|
|
self.text.as_mut_empty().push_str(v);
|
|
self.redraw(cx);
|
|
}
|
|
}
|
|
|
|
impl Button {
|
|
/// The label the face shows right now.
|
|
fn shown_label(&self) -> &str {
|
|
if self.copied && !self.copied_text.is_empty() {
|
|
&self.copied_text
|
|
} else {
|
|
self.text.as_ref()
|
|
}
|
|
}
|
|
|
|
/// Puts `text_to_copy` on the clipboard and starts the copied moment.
|
|
fn copy_now(&mut self, cx: &mut Cx) {
|
|
let uid = self.widget_uid();
|
|
cx.copy_to_clipboard(&self.text_to_copy);
|
|
self.copied = true;
|
|
cx.stop_timer(self.copied_timer);
|
|
self.copied_timer = cx.start_timeout(self.copied_secs.max(0.1));
|
|
self.animator_play(cx, ids!(copied.on));
|
|
cx.widget_action_with_data(&self.action_data, uid, CopyButtonAction::Copied);
|
|
self.draw_bg.redraw(cx);
|
|
}
|
|
|
|
/// Returns `true` if this copy button put its text on the clipboard.
|
|
pub fn copied(&self, actions: &Actions) -> bool {
|
|
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
|
|
matches!(item.cast(), CopyButtonAction::Copied)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Whether a burger button shows the cross.
|
|
pub fn open(&self) -> bool {
|
|
self.open
|
|
}
|
|
|
|
/// Turns a burger button's bars into the cross, or back.
|
|
pub fn set_open(&mut self, cx: &mut Cx, open: bool) {
|
|
self.set_open_with(cx, open, Animate::Yes);
|
|
}
|
|
|
|
/// As [`Self::set_open`], choosing whether the turn animates. A host
|
|
/// with its motion switched off passes `Animate::No` to land on the end
|
|
/// state at once instead of playing the track.
|
|
pub fn set_open_with(&mut self, cx: &mut Cx, open: bool, animate: Animate) {
|
|
if self.open == open {
|
|
return;
|
|
}
|
|
self.open = open;
|
|
self.sync_open(cx, animate);
|
|
self.draw_bg.redraw(cx);
|
|
}
|
|
|
|
/// Brings the open track in line with the `open` prop; a button whose
|
|
/// animator has no such track, or already agrees, is left alone.
|
|
fn sync_open(&mut self, cx: &mut Cx, animate: Animate) {
|
|
let open = self.open;
|
|
if open == self.animator_in_state(cx, ids!(open.on)) {
|
|
return;
|
|
}
|
|
self.animator_toggle(cx, open, animate, ids!(open.on), ids!(open.off));
|
|
}
|
|
|
|
pub fn draw_button(&mut self, cx: &mut Cx2d, label: &str) {
|
|
self.draw_bg.begin(cx, self.walk, self.layout);
|
|
self.draw_icon.draw_walk(cx, self.icon_walk);
|
|
self.draw_text
|
|
.draw_walk(cx, self.label_walk, self.label_align, label);
|
|
self.draw_bg.end(cx);
|
|
}
|
|
|
|
/// The label, alone or stacked over the description in its own column
|
|
/// when there is one.
|
|
fn draw_label(&mut self, cx: &mut Cx2d) {
|
|
let label: &str = if self.copied && !self.copied_text.is_empty() {
|
|
&self.copied_text
|
|
} else {
|
|
self.text.as_ref()
|
|
};
|
|
if self.description.is_empty() {
|
|
self.draw_text
|
|
.draw_walk(cx, self.label_walk, self.label_align, label);
|
|
return;
|
|
}
|
|
cx.begin_turtle(
|
|
Walk::fit(),
|
|
Layout {
|
|
flow: Flow::Down,
|
|
clip_x: false,
|
|
clip_y: false,
|
|
..Default::default()
|
|
},
|
|
);
|
|
self.draw_text
|
|
.draw_walk(cx, self.label_walk, self.label_align, label);
|
|
self.draw_description
|
|
.draw_walk(cx, self.description_walk, Align::default(), &self.description);
|
|
cx.end_turtle();
|
|
}
|
|
|
|
pub fn enabled(&self) -> bool {
|
|
self.enabled
|
|
}
|
|
|
|
/// Whether the button is waiting: spinner shown, clicks ignored.
|
|
pub fn loading(&self) -> bool {
|
|
self.loading
|
|
}
|
|
|
|
/// Starts or ends the wait: the label fades under a turning arc and
|
|
/// presses are ignored while it lasts.
|
|
pub fn set_loading(&mut self, cx: &mut Cx, loading: bool) {
|
|
if self.loading == loading {
|
|
return;
|
|
}
|
|
self.loading = loading;
|
|
self.sync_loading(cx, Animate::Yes);
|
|
self.draw_bg.redraw(cx);
|
|
}
|
|
|
|
/// Brings the loading and clock tracks in line with the `loading` prop.
|
|
/// A button whose animator already agrees is left alone, so the common
|
|
/// case costs nothing.
|
|
fn sync_loading(&mut self, cx: &mut Cx, animate: Animate) {
|
|
let loading = self.loading;
|
|
if loading == self.animator_in_state(cx, ids!(loading.on)) {
|
|
return;
|
|
}
|
|
self.animator_toggle(cx, loading, animate, ids!(loading.on), ids!(loading.off));
|
|
// The clock only ever plays: cutting a loop would park it at its end.
|
|
if loading {
|
|
self.animator_play(cx, ids!(time.on));
|
|
} else {
|
|
self.animator_cut(cx, ids!(time.off));
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if this button was clicked.
|
|
///
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn clicked(&self, actions: &Actions) -> bool {
|
|
self.clicked_modifiers(actions).is_some()
|
|
}
|
|
|
|
/// Returns `true` if this button was pressed down.
|
|
///
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn pressed(&self, actions: &Actions) -> bool {
|
|
self.pressed_modifiers(actions).is_some()
|
|
}
|
|
|
|
/// Returns `true` if this button was long-pressed on.
|
|
///
|
|
/// Note that this does not mean the button has been released yet.
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn long_pressed(&self, actions: &Actions) -> bool {
|
|
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
|
|
matches!(item.cast(), ButtonAction::LongPressed)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if this button was released, which is *not* considered to be clicked.
|
|
///
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn released(&self, actions: &Actions) -> bool {
|
|
self.released_modifiers(actions).is_some()
|
|
}
|
|
|
|
/// Returns `Some` (with active keyboard modifiers) if this button was clicked.
|
|
///
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn clicked_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
|
|
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
|
|
if let ButtonAction::Clicked(m) = item.cast() {
|
|
return Some(m);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Returns `Some` (with active keyboard modifiers) if this button was pressed down.
|
|
///
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn pressed_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
|
|
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
|
|
if let ButtonAction::Pressed(m) = item.cast() {
|
|
return Some(m);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Returns `Some` (with active keyboard modifiers) if this button was released,
|
|
/// which is *not* considered to be clicked.
|
|
///
|
|
/// See [`ButtonAction`] for more details.
|
|
pub fn released_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
|
|
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
|
|
if let ButtonAction::Released(m) = item.cast() {
|
|
return Some(m);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
impl ButtonRef {
|
|
/// See [`Button::clicked()`].
|
|
pub fn clicked(&self, actions: &Actions) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.clicked(actions))
|
|
}
|
|
|
|
/// See [`Button::pressed()`].
|
|
pub fn pressed(&self, actions: &Actions) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.pressed(actions))
|
|
}
|
|
|
|
/// See [`Button::long_pressed()`].
|
|
pub fn long_pressed(&self, actions: &Actions) -> bool {
|
|
self.borrow()
|
|
.is_some_and(|inner| inner.long_pressed(actions))
|
|
}
|
|
|
|
/// See [`Button::released()`].
|
|
pub fn released(&self, actions: &Actions) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.released(actions))
|
|
}
|
|
|
|
/// See [`Button::clicked_modifiers()`].
|
|
pub fn clicked_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
|
|
self.borrow()
|
|
.and_then(|inner| inner.clicked_modifiers(actions))
|
|
}
|
|
|
|
/// See [`Button::pressed_modifiers()`].
|
|
pub fn pressed_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
|
|
self.borrow()
|
|
.and_then(|inner| inner.pressed_modifiers(actions))
|
|
}
|
|
|
|
/// See [`Button::released_modifiers()`].
|
|
pub fn released_modifiers(&self, actions: &Actions) -> Option<KeyModifiers> {
|
|
self.borrow()
|
|
.and_then(|inner| inner.released_modifiers(actions))
|
|
}
|
|
|
|
pub fn set_visible(&self, cx: &mut Cx, visible: bool) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.visible = visible;
|
|
inner.redraw(cx);
|
|
}
|
|
}
|
|
|
|
pub fn set_enabled(&self, cx: &mut Cx, enabled: bool) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.enabled = enabled;
|
|
inner.redraw(cx);
|
|
}
|
|
}
|
|
|
|
/// See [`Button::loading()`].
|
|
pub fn loading(&self) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.loading())
|
|
}
|
|
|
|
/// See [`Button::set_loading()`].
|
|
pub fn set_loading(&self, cx: &mut Cx, loading: bool) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.set_loading(cx, loading);
|
|
}
|
|
}
|
|
|
|
/// See [`Button::copied()`].
|
|
pub fn copied(&self, actions: &Actions) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.copied(actions))
|
|
}
|
|
|
|
/// See [`Button::open()`].
|
|
pub fn open(&self) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.open())
|
|
}
|
|
|
|
/// See [`Button::set_open()`].
|
|
pub fn set_open(&self, cx: &mut Cx, open: bool) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.set_open(cx, open);
|
|
}
|
|
}
|
|
|
|
/// See [`Button::set_open_with()`].
|
|
pub fn set_open_with(&self, cx: &mut Cx, open: bool, animate: Animate) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.set_open_with(cx, open, animate);
|
|
}
|
|
}
|
|
|
|
/// Resets the hover state of this button.
|
|
///
|
|
/// This is useful in certain cases where the hover state should be reset
|
|
/// (cleared) regardelss of whether the mouse is over it.
|
|
pub fn reset_hover(&self, cx: &mut Cx) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.animator_cut(cx, ids!(hover.off));
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ButtonSet {
|
|
pub fn clicked(&self, actions: &Actions) -> bool {
|
|
self.iter().any(|v| v.clicked(actions))
|
|
}
|
|
pub fn pressed(&self, actions: &Actions) -> bool {
|
|
self.iter().any(|v| v.pressed(actions))
|
|
}
|
|
pub fn released(&self, actions: &Actions) -> bool {
|
|
self.iter().any(|v| v.released(actions))
|
|
}
|
|
|
|
pub fn reset_hover(&self, cx: &mut Cx) {
|
|
for item in self.iter() {
|
|
item.reset_hover(cx)
|
|
}
|
|
}
|
|
|
|
pub fn which_clicked_modifiers(&self, actions: &Actions) -> Option<(usize, KeyModifiers)> {
|
|
for (index, btn) in self.iter().enumerate() {
|
|
if let Some(km) = btn.clicked_modifiers(actions) {
|
|
return Some((index, km));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn set_visible(&self, cx: &mut Cx, visible: bool) {
|
|
for item in self.iter() {
|
|
item.set_visible(cx, visible)
|
|
}
|
|
}
|
|
pub fn set_enabled(&self, cx: &mut Cx, enabled: bool) {
|
|
for item in self.iter() {
|
|
item.set_enabled(cx, enabled)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod open_tests {
|
|
use super::*;
|
|
|
|
/// The floating face carries the burger's open track. Without motion the
|
|
/// turn is cut to its end state, so nothing is left animating; with it,
|
|
/// the track plays.
|
|
#[test]
|
|
fn set_open_with_lands_at_once_without_motion() {
|
|
let mut cx = Cx::new(Box::new(|_, _| {}));
|
|
cx.with_vm(|vm| {
|
|
crate::script_mod(vm);
|
|
let value = crate::script_eval!(vm, {use mod.widgets.* ButtonFloating{}});
|
|
let mut button = Button::script_from_value(vm, value);
|
|
vm.with_cx_mut(|cx| {
|
|
assert!(!button.open());
|
|
button.set_open_with(cx, true, Animate::No);
|
|
assert!(button.open());
|
|
assert!(button.animator_in_state(cx, ids!(open.on)));
|
|
assert!(!button.animator.is_track_animating(live_id!(open)), "cut, not played");
|
|
button.set_open_with(cx, false, Animate::Yes);
|
|
assert!(!button.open());
|
|
assert!(button.animator_in_state(cx, ids!(open.off)));
|
|
assert!(button.animator.is_track_animating(live_id!(open)), "played");
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Both round faces compile: the floating face draws its glyph in a
|
|
/// pixel function of its own, and a shader error there would only show
|
|
/// as a blank disc at run time.
|
|
#[test]
|
|
fn the_round_faces_compile() {
|
|
let mut cx = Cx::new(Box::new(|_, _| {}));
|
|
cx.with_vm(|vm| {
|
|
crate::script_mod(vm);
|
|
for (name, value) in [
|
|
("ButtonFloating", crate::script_eval!(vm, {
|
|
mod.shader.test_compile_draw_source(mod.widgets.ButtonFloating.draw_bg, "glsl", false)
|
|
})),
|
|
("ButtonFloatingSm", crate::script_eval!(vm, {
|
|
mod.shader.test_compile_draw_source(mod.widgets.ButtonFloatingSm.draw_bg, "glsl", false)
|
|
})),
|
|
] {
|
|
let text = vm
|
|
.bx
|
|
.heap
|
|
.string_with(value, |_heap, text| text.to_string())
|
|
.expect("the compiler answers with source");
|
|
assert!(!text.starts_with("ERRORS:"), "{name} did not compile: {text}");
|
|
assert!(!text.is_empty(), "{name} compiled to nothing");
|
|
if name == "ButtonFloating" {
|
|
assert!(text.contains("glyph_stroke"), "{name} draws its glyph");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|