//! FloatingAction — the one action a screen exists for, as a round button //! pinned to a corner or an edge of its container or of the window. //! //! When that action is really a small set, pressing the button fans the set //! out toward the middle of the screen, along an arc, a row or a column, as //! small round buttons with optional label chips. The plus on the button //! turns into a cross, so the place that brought the set out also puts it //! away again. //! //! The name is spelled out rather than abbreviated because the short prefix //! already belongs to the property-panel controls, and a floating action //! beside them would read as one of that family. //! //! # Toward the interior, always //! //! The anchor is the only thing that decides direction. A button pinned to //! a corner can only grow into the quarter of the screen in front of it, and //! one pinned to the middle of an edge only into the half in front of that //! edge, so the anchor names both where the button sits and which way its //! set goes. A set that could open outward would put its actions under the //! window's edge on exactly the screens that need them most: the small ones. //! Every piece of geometry below is a pure function of the anchor, the //! layout and a handful of sizes, which is what lets the tests pin every //! combination without a window. //! //! # Drawn on its own overlay //! //! The button and its set float over whatever the container holds, and a //! list scrolling underneath must not carry them away or clip them. So both //! are drawn on an overlay list of the widget's own. Pinned, the widget //! claims no room at all in its parent; laid out `inline` (a toolbar's //! floating slot), the button takes its room in the parent's flow like any //! control and only the set floats. //! //! Drawn on top is not heard first. A closed button takes a press only //! where nothing that hears events before it lies underneath, which is why //! it is declared last in its container, and why one pinned to the window //! belongs last in the window's body: pinned from deep inside a page, it //! floats over the panels beside the page but they still hear the press. //! //! While the set is out the widget holds the sweep lock, so a press that //! misses the set closes it and reaches nothing underneath, the pairing //! every popup in this crate uses. The button and the actions hit-test with //! plain `hits`, which the lock would turn away too, so the lock is lifted //! around their own dispatch and taken again after, as the popover does. //! A lock outlives the widget that took it when the widget is dropped while //! its set is out (a live edit rebuilds the tree), and a lock nobody holds //! turns every press in the window away for good; so a dropped widget //! leaves its lock behind for the window to release on the next event. //! //! # Any number of actions //! //! A set is not always three. An arc grows its radius before two actions //! would touch, their round faces or the squares a press is aimed at, and //! when the grown arc would leave the window it is already as wide as its //! anchor lets it open (a quarter from a corner, a half from the middle of //! an edge), so the actions go on round a second arc further out, the inner //! one filled first. The arcs also stay inside a furthest radius, //! `radial_max_radius`, however much window is left: an action a long reach //! from the button is a slow one to hit, so a set that would reach past it //! goes on round further arcs inside it just as it does in a small window. //! A set no arcs inside it can hold rests on the arcs of the least room //! past it that does, as near the button as the spacing allows. A row or a //! column that would leave the window folds //! into a second line one step further in. Nothing scrolls and nothing //! shrinks: a scrolling set hides the actions it exists to show, and a //! smaller target is a worse target. Only a window too small for the set at //! all is overrun, the choice every popup in the crate makes. //! //! # Motion //! //! Durations and easings are the theme's motion tokens, so a set moves the //! way the rest of the theme does. Each action runs its own clock, out in //! the order the actions were declared and back the other way round, and //! the stagger between them shrinks for a large set so twelve open about as //! quickly as three. The plus turns on the same eases and the same clock. A //! set reopened on its way back turns round from wherever each action is. //! Whatever an ease does on the way, and a spring goes past the place and //! back, presses and arrow keys aim at where each action comes to rest. use crate::{ animator::{Animate, Ease}, button::*, event::TouchState, makepad_derive_widget::*, makepad_draw::*, family_api::{orphan_sweep_locks, release_orphaned_sweep_locks}, overlay_place::{claim_escape, span_inboard}, popover::FocusTrap, widget::*, widget_tree::CxWidgetExt, }; script_mod! { use mod.prelude.widgets_internal.* use mod.widgets.* // Not splatted: TopLeft is an alignment preset, and TopCenter and // BottomCenter are already exported bare by the popover's placements. // Written out as FloatingAnchor.BottomRight they cannot shadow anything. mod.widgets.FloatingAnchor = set_type_default() do #(FloatingAnchor::script_api(vm)) mod.widgets.FloatingPin = set_type_default() do #(FloatingPin::script_api(vm)) mod.widgets.SpeedDialLayout = set_type_default() do #(SpeedDialLayout::script_api(vm)) mod.widgets.SpeedDialLabels = set_type_default() do #(SpeedDialLabels::script_api(vm)) mod.widgets.DrawFloatingShadowBase = #(DrawFloatingShadow::script_component(vm)) set_type_default() do #(DrawFloatingShadow::script_shader(vm)){ ..mod.draw.DrawQuad // Rust writes both per disc, so they are plain literals. radius: 28.0 weight: 1.0 /** shadow ink */ color: uniform(theme.color_elevation_3) /** how soft the shadow's edge is, in points 0..48 step 1 */ blur: uniform(theme.elevation_3_radius) /** how far the shadow falls below the disc, in points 0..24 step 1 */ offset_y: uniform(theme.elevation_3_offset_y) pixel: fn() { // The quad is centred on the disc and reaches past it by the // blur and the drop, so the middle of the quad is the middle of // the disc. let p = self.pos * self.rect_size - self.rect_size * 0.5 - vec2(0.0, self.offset_y) let d = length(p) - self.radius // The logistic curve standing in for the normal one, half way // down at the disc's edge: exact for a circle, where the // distance to the edge is the whole story. let sigma = max(self.blur * 0.5, 0.001) let v = 1.0 / (1.0 + exp(clamp(1.702 * d / sigma, -30.0, 30.0))) return Pal.premul(vec4(self.color.xyz, self.color.w * v * self.weight)) } } mod.widgets.DrawFloatingChipBase = #(DrawFloatingChip::script_component(vm)) set_type_default() do #(DrawFloatingChip::script_shader(vm)){ ..mod.draw.DrawQuad /** the chip's fill */ color: uniform(theme.color_inverse_surface) /** the chip's corner rounding 0..12 step 0.5 */ radius: uniform(theme.radius_s) pixel: fn() { let sdf = Sdf2d.viewport(self.pos * self.rect_size) sdf.box(0.5, 0.5, self.rect_size.x - 1.0, self.rect_size.y - 1.0, self.radius) sdf.fill(self.color) return sdf.result } } mod.widgets.FloatingActionItemBase = #(FloatingActionItem::register_widget(vm)) /** One action in a floating action's set: a small round button with an * optional label chip. The set places it; its own width and height are * not read. */ mod.widgets.FloatingActionItem = set_type_default() do mod.widgets.FloatingActionItemBase{ width: Fit height: Fit /** the words on the chip, and the item's text in the test tree */ label: "" /** the action answers presses; a disabled one is drawn dimmed 0..1 step 1 */ enabled: true /** the action is in the set at all; a hidden one leaves no gap 0..1 step 1 */ visible: true /** space between the button's edge and the chip, in points 0..24 step 1 */ chip_gap: 8. /** room between the chip's edge and its words */ chip_padding: Inset{left: 10. right: 10. top: 4. bottom: 4.} // The shader declares these inputs, so they take plain values here. draw_shadow +: { color: theme.color_elevation_2 blur: theme.elevation_2_radius offset_y: theme.elevation_2_offset_y } /** The label's background. The inverse pair contrasts with the page * in either scheme, which a label floating over arbitrary content * needs. */ draw_chip +: { color: theme.color_inverse_surface radius: theme.radius_s } draw_chip_text +: { color: theme.color_inverse_on_surface text_style: theme.font_regular{ /** chip type size 6..24 step 0.5 */ font_size: theme.type_label_m_size line_spacing: 1.0 } } /** the small round face; set its icon with `button +: {draw_icon +: {svg: ...}}` */ button := ButtonFloatingSm{} } mod.widgets.FloatingActionBase = #(FloatingAction::register_widget(vm)) /** The one action a screen exists for, pinned to a corner or an edge; * a set of actions behind it fans out toward the middle. Declare it as * the last child of its container, so it hears a press before the * content under it does. */ mod.widgets.FloatingAction = set_type_default() do mod.widgets.FloatingActionBase{ width: Fit height: Fit /** where it is pinned, and so which way its set opens */ anchor: mod.widgets.FloatingAnchor.BottomRight /** the rect it is pinned inside: its container, or the window */ pin_to: mod.widgets.FloatingPin.Container /** laid out in its parent's flow, as in a toolbar's floating slot; anchor still picks the direction 0..1 step 1 */ inline: false /** distance from the edges the anchor touches; the other edges are not read */ edge_margin: Inset{left: 16. top: 16. right: 16. bottom: 16.} /** how the set is laid out: along an arc, a row or a column */ dial: mod.widgets.SpeedDialLayout.Radial /** which label chips show: Auto is Always for a column and Hot otherwise */ labels: mod.widgets.SpeedDialLabels.Auto /** the main button's diameter, in points 40..96 step 4 */ size: 56. /** each action's diameter, in points 24..64 step 2 */ item_size: 40. /** space between neighbours, and between the button and the first action 0..32 step 1 */ item_gap: 12. /** the least radius of the arc; it grows before neighbours would touch 48..200 step 2 */ radial_radius: 88. /** the furthest radius of the arcs: a set that would reach past it goes on round more arcs inside it; 0 is no limit 0..400 step 2 */ radial_max_radius: 200. /** picking an action puts the set away 0..1 step 1 */ close_on_pick: true /** shows the set and keeps it out: no pointer grab, no outside or Escape close 0..1 step 1 */ pinned: false /** each action's travel out, and the plus turning to the cross, in seconds 0..0.6 step 0.01 */ enter_secs: theme.motion_short_4 /** how the travel out and the turn ease: one of the theme's motion easings */ enter_ease: theme.motion_ease_emphasized_decelerate /** each action's travel back under the button, and the turn back, in seconds 0..0.6 step 0.01 */ exit_secs: theme.motion_short_3 /** how the travel back and the turn back ease: one of the theme's motion easings */ exit_ease: theme.motion_ease_standard_accelerate /** delay added per action, in seconds; shrunk for a large set so the last sets off within 0.15 of the first 0..0.1 step 0.005 */ stagger_secs: 0.03 /** no travel and no turn: everything lands at once 0..1 step 1 */ reduced_motion: false /** drawn at all */ visible: true draw_shadow +: { color: theme.color_elevation_3 blur: theme.elevation_3_radius offset_y: theme.elevation_3_offset_y } /** the main face; its walk is rewritten from `size` every draw */ main := ButtonFloating{} } /** A smaller floating action, for dense screens. */ mod.widgets.FloatingActionSm = mod.widgets.FloatingAction{ size: 40. item_size: 32. item_gap: 8. radial_radius: 64. radial_max_radius: 156. main +: {width: 40. height: 40.} } /** A larger floating action, for the one screen that is mostly this. */ mod.widgets.FloatingActionLg = mod.widgets.FloatingAction{ size: 72. item_size: 48. item_gap: 14. radial_radius: 108. radial_max_radius: 238. main +: {width: 72. height: 72.} } } /// Where a floating action is pinned. The same choice decides which way its /// set opens: always into the container, away from the edges it touches. #[derive(Copy, Clone, Debug, PartialEq, Script, ScriptHook, Default)] pub enum FloatingAnchor { TopLeft, TopCenter, TopRight, CenterLeft, CenterRight, BottomLeft, BottomCenter, #[pick] #[default] BottomRight, } impl FloatingAnchor { pub const ALL: [FloatingAnchor; 8] = [ FloatingAnchor::TopLeft, FloatingAnchor::TopCenter, FloatingAnchor::TopRight, FloatingAnchor::CenterLeft, FloatingAnchor::CenterRight, FloatingAnchor::BottomLeft, FloatingAnchor::BottomCenter, FloatingAnchor::BottomRight, ]; /// Which edges the anchor touches: -1 for the left or top edge, 1 for the /// right or bottom one, and 0 on an axis where it is centred instead. pub fn sides(self) -> (f64, f64) { match self { FloatingAnchor::TopLeft => (-1.0, -1.0), FloatingAnchor::TopCenter => (0.0, -1.0), FloatingAnchor::TopRight => (1.0, -1.0), FloatingAnchor::CenterLeft => (-1.0, 0.0), FloatingAnchor::CenterRight => (1.0, 0.0), FloatingAnchor::BottomLeft => (-1.0, 1.0), FloatingAnchor::BottomCenter => (0.0, 1.0), FloatingAnchor::BottomRight => (1.0, 1.0), } } /// The anchor as the test tree spells it. pub fn slug(self) -> &'static str { match self { FloatingAnchor::TopLeft => "top-left", FloatingAnchor::TopCenter => "top-center", FloatingAnchor::TopRight => "top-right", FloatingAnchor::CenterLeft => "center-left", FloatingAnchor::CenterRight => "center-right", FloatingAnchor::BottomLeft => "bottom-left", FloatingAnchor::BottomCenter => "bottom-center", FloatingAnchor::BottomRight => "bottom-right", } } /// A corner, rather than the middle of an edge. pub fn is_corner(self) -> bool { let (sx, sy) = self.sides(); sx != 0.0 && sy != 0.0 } } /// The rect a floating action is pinned inside. #[derive(Copy, Clone, Debug, PartialEq, Script, ScriptHook, Default)] pub enum FloatingPin { /// The container it is declared in. #[pick] #[default] Container, /// The whole window, whatever it is declared in. Window, } /// How a floating action's set is laid out. #[derive(Copy, Clone, Debug, PartialEq, Script, ScriptHook, Default)] pub enum SpeedDialLayout { /// Along an arc: a quarter circle from a corner, a half from an edge. #[pick] #[default] Radial, /// Along a row. Horizontal, /// Along a column. Vertical, } impl SpeedDialLayout { pub fn slug(self) -> &'static str { match self { SpeedDialLayout::Radial => "radial", SpeedDialLayout::Horizontal => "horizontal", SpeedDialLayout::Vertical => "vertical", } } } /// Which label chips a floating action's set shows. #[derive(Copy, Clone, Debug, PartialEq, Script, ScriptHook, Default)] pub enum SpeedDialLabels { /// Always for a column, whose chips line up beside it; Hot for an arc /// or a row, whose chips would overlap their neighbours. #[pick] #[default] Auto, /// Every chip. Always, /// The chip of the action under the pointer or with key focus. Hot, /// None: the icons carry the meaning. Never, } /// Which side of its action a label chip goes on. #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] pub enum ChipSide { Left, Right, Above, Below, /// Straight out from the main button, past the action. #[default] Outward, } /// What a floating action reports. #[derive(Clone, Debug, PartialEq, Default)] pub enum FloatingActionAction { /// The button was clicked and there is no set behind it: the action /// itself. Pressed, /// The set came out. Opened, /// The set was put away, however: the button, a pick, Escape, a press /// outside, or the window losing focus. Closed, /// An action was picked, by the child id it was declared under. Picked(LiveId), #[default] None, } /// Inset kept between a chip and the window's edges. const EDGE: f64 = 6.0; /// An arrow key moves to an action within this many degrees either side of /// the arrow's direction. Wider than 45 so that the diagonal step of a /// quarter arc is reachable with either of its two arrows. const ARROW_CONE_DEGREES: f64 = 67.5; /// Two actions this close in distance are a tie, settled by alignment. The /// ends of an arc are the same distance from its middle to within floating /// point, and the better aligned one is the one the arrow means. const ARROW_TIE: f64 = 0.5; /// The share of its size an action starts its travel at. const START_SCALE: f64 = 0.4; /// How dark an action's shadow is against the main button's. Smaller things /// sit lower. const ITEM_SHADOW_WEIGHT: f32 = 0.6; /// A rect read on the event side counts as moved past this many points. const HONEST_SLACK: f64 = 0.5; /// The most the stagger adds up to across a whole set, in seconds. Past a /// handful of actions a fixed stagger makes the last one arrive long after /// the hand has moved on, so a large set shares this out instead. const STAGGER_BUDGET: f64 = 0.15; /// A window with no edges anywhere near, for the layout nothing bounds. const UNBOUNDED: Rect = Rect { pos: DVec2 { x: -1.0e9, y: -1.0e9 }, size: DVec2 { x: 2.0e9, y: 2.0e9 }, }; /// The unit vector from the anchor into its container. pub fn interior(anchor: FloatingAnchor) -> DVec2 { let (sx, sy) = anchor.sides(); dvec2(-sx, -sy).normalize() } /// The main button's rect inside `bounds`, `margin` in from the edges the /// anchor touches and centred on an axis it does not touch. /// /// Each axis is then pulled inside the bounds, so a container smaller than /// the button keeps the button's low edge in it rather than hanging the /// button off the container's top or left. pub fn main_rect(anchor: FloatingAnchor, bounds: Rect, size: f64, margin: Inset) -> Rect { let (sx, sy) = anchor.sides(); let place = |side: f64, lo: f64, extent: f64, near: f64, far: f64| -> f64 { let start = if side < 0.0 { lo + near } else if side > 0.0 { lo + extent - far - size } else { lo + (extent - size) * 0.5 }; span_inboard(start, size, lo, extent) }; Rect { pos: dvec2( place(sx, bounds.pos.x, bounds.size.x, margin.left, margin.right), place(sy, bounds.pos.y, bounds.size.y, margin.top, margin.bottom), ), size: dvec2(size, size), } } /// The part of the window a pass of `pass` leaves usable after the safe-area /// insets. fn safe_rect(pass: DVec2, insets: SafeAreaInsets) -> Rect { Rect { pos: dvec2(insets.left, insets.top), size: dvec2( (pass.x - insets.left - insets.right).max(0.0), (pass.y - insets.top - insets.bottom).max(0.0), ), } } /// The rect a floating action pins inside: its host, less whatever of the /// host the safe-area insets cover. A button under a notch or a home /// indicator is a button that cannot be pressed. pub fn pin_bounds(host: Rect, pass: DVec2, insets: SafeAreaInsets) -> Rect { let safe = safe_rect(pass, insets); let x0 = host.pos.x.max(safe.pos.x); let y0 = host.pos.y.max(safe.pos.y); let x1 = (host.pos.x + host.size.x).min(safe.pos.x + safe.size.x); let y1 = (host.pos.y + host.size.y).min(safe.pos.y + safe.size.y); Rect { pos: dvec2(x0, y0), size: dvec2((x1 - x0).max(0.0), (y1 - y0).max(0.0)), } } /// The rect a chip must stay inside: the safe part of the window, a little /// further in, as every popup in the crate keeps. fn window_bounds(pass: DVec2, insets: SafeAreaInsets) -> Rect { let safe = safe_rect(pass, insets); Rect { pos: safe.pos + dvec2(EDGE, EDGE), size: dvec2((safe.size.x - EDGE * 2.0).max(0.0), (safe.size.y - EDGE * 2.0).max(0.0)), } } /// The arc a radial set is laid along: where it starts, which way it runs /// (+1 clockwise, -1 anticlockwise) and how far. Radians, clockwise from /// straight up, the measure the pie menu's ring uses. /// /// The two ends lie along the two edges the arc runs between, so a corner /// set reads as a quarter circle and an edge set as a half, both opening /// into the container. pub fn radial_arc(anchor: FloatingAnchor) -> (f64, f64, f64) { let (start, dir, span): (f64, f64, f64) = match anchor { FloatingAnchor::BottomRight => (0.0, -1.0, 90.0), FloatingAnchor::BottomLeft => (0.0, 1.0, 90.0), FloatingAnchor::TopRight => (180.0, 1.0, 90.0), FloatingAnchor::TopLeft => (180.0, -1.0, 90.0), FloatingAnchor::CenterRight => (0.0, -1.0, 180.0), FloatingAnchor::CenterLeft => (0.0, 1.0, 180.0), FloatingAnchor::BottomCenter => (270.0, 1.0, 180.0), FloatingAnchor::TopCenter => (270.0, -1.0, 180.0), }; (start.to_radians(), dir, span.to_radians()) } /// How far out an arc of `n` actions spanning `span` radians is laid. /// /// Never less than `min_r`, never so close that the first action touches /// the main button, and never so tight that two neighbours' discs, `gap` /// apart, would overlap along the chord between them. The arc grows rather /// than the actions shrinking: a smaller target is a worse target. pub fn radial_radius(n: usize, span: f64, item: f64, gap: f64, main: f64, min_r: f64) -> f64 { let clear_of_main = main * 0.5 + gap + item * 0.5; let mut r = min_r.max(clear_of_main); if n >= 2 { let step = span / (n - 1) as f64; let half_sin = (step * 0.5).sin(); if half_sin > 1e-9 { r = r.max((item + gap) / (2.0 * half_sin)); } } r } /// How far out an arc of `n` actions spanning `span` radians is laid so /// that, on top of [`radial_radius`], no two actions' squares overlap and /// none overlaps the main button's square. /// /// The chord rule keeps round faces a gap apart, but a press is aimed at an /// action's square, and two neighbours a diagonal step apart have squares /// that reach toward each other past their faces: at a gap under (√2 - 1) /// times the diameter they share a corner. Two squares are clear when their /// middles are a diameter apart along one axis or the other, and the longer /// axis of a step between two places on the arc is that step's length /// times the larger of the sine and cosine of the angle half way between /// them. Every arc starts along an axis ([`radial_arc`]), so that share /// depends only on the span and the count, never on the anchor. pub fn arc_radius(n: usize, span: f64, item: f64, gap: f64, main: f64, min_r: f64) -> f64 { let mut r = radial_radius(n, span, item, gap, main, min_r); let angle = |k: usize| { if n >= 2 { span * k as f64 / (n - 1) as f64 } else { span * 0.5 } }; let longer_axis = |a: f64| a.sin().abs().max(a.cos().abs()); for k in 0..n { let a = angle(k); r = r.max((main + item) * 0.5 / longer_axis(a)); for j in k + 1..n { let b = angle(j); let apart = 2.0 * ((b - a) * 0.5).sin() * longer_axis((a + b) * 0.5); if apart > 1e-9 { r = r.max(item / apart); } } } r } /// How far apart two arcs are laid: far enough that a face on one is a gap /// clear of a face on the other whatever their angles, and a square on one /// clear of a square on the other, which along a diagonal takes √2 times /// the diameter. pub fn arc_step(item: f64, gap: f64) -> f64 { (item + gap).max(item * std::f64::consts::SQRT_2) } /// Where each of `n` actions sits, as an offset from the main button's /// centre, in the order they were declared, when nothing is in the way: the /// layout a window large enough for the whole set gets. An arc still keeps /// inside `max_r` when that is above 0, as [`arc_counts`] lays it out. /// /// A row or a column runs from the button toward the interior along its own /// axis when that axis leads inward. When it does not (a row from the /// middle of the top or bottom edge, a column from the middle of a side) it /// is centred on the button instead, one step toward the interior: running /// along the edge in one direction would pick a side for no reason. pub fn dial_offsets( anchor: FloatingAnchor, dial: SpeedDialLayout, n: usize, main: f64, item: f64, gap: f64, min_r: f64, max_r: f64, ) -> Vec { dial_plan(anchor, dial, n, main, item, gap, min_r, max_r, dvec2(0.0, 0.0), UNBOUNDED).offsets } /// A set laid out inside a window: where each action rests, and which arc /// or line it rests on. #[derive(Clone, Debug, Default, PartialEq)] pub struct DialPlan { /// Each action's resting offset from the main button's centre, in /// declaration order. pub offsets: Vec, /// The arc or line each action is on, 0 nearest the button. It never /// goes down along the order: the inner arc or the first line fills /// first. pub rows: Vec, /// Each arc's radius, innermost first; empty for a row or a column. pub radii: Vec, } impl DialPlan { /// How many arcs or lines the set is laid on. pub fn row_count(&self) -> usize { self.rows.last().map_or(0, |row| row + 1) } } /// The set laid out around a main button centred at `centre`, every action /// inside `window` whenever the window can hold the set at all. /// /// An arc grows with its count by the chord rule, and further so no two /// squares meet, and once it would leave the window, or reach past `max_r` /// when that is above 0, goes on round further arcs a step apart, the inner /// ones filled first. A row or a column folds /// into parallel lines a step further in. A window that cannot hold the set /// is overrun rather than the set being cut: an action that cannot be /// reached is worse than one partly under the window's edge. /// /// `max_r` is measured from `centre`, the button's middle, even when a /// small button close to the window's edge moves its set in. pub fn dial_plan( anchor: FloatingAnchor, dial: SpeedDialLayout, n: usize, main: f64, item: f64, gap: f64, min_r: f64, max_r: f64, centre: DVec2, window: Rect, ) -> DialPlan { let lay_out = |centre: DVec2, max_r: f64| match dial { SpeedDialLayout::Radial => radial_plan(anchor, n, main, item, gap, min_r, max_r, centre, window), _ => line_plan(anchor, dial, n, main, item, gap, centre, window), }; let plan = lay_out(centre, max_r); // The actions level with the button, the ends of an arc and the first // line of a row or a column, reach half an action past its middle // toward the edges it is pinned to. A small button close to the window's // edge has less room than that, and the whole set moves in by what they // overrun, which keeps its shape and moves no action nearer the button. let shift = pinned_overrun(anchor, &plan.offsets, centre, item, window); if shift.x == 0.0 && shift.y == 0.0 { return plan; } // The arcs are laid around the moved centre, so they are held that much // nearer for no action to rest past the furthest radius from the // button's own middle. let held = if max_r > 0.0 { (max_r - shift.length()).max(f64::MIN_POSITIVE) } else { max_r }; let mut plan = lay_out(centre + shift, held); for offset in &mut plan.offsets { *offset += shift; } plan } /// How far a set laid out around `centre` has to move away from the edges /// its anchor touches for none of its actions to lie past them in /// `window`. Never toward an edge, and never more than half an action, /// which is all an action level with the button reaches past its middle. fn pinned_overrun(anchor: FloatingAnchor, offsets: &[DVec2], centre: DVec2, item: f64, window: Rect) -> DVec2 { let (sx, sy) = anchor.sides(); let half = item * 0.5; let axis = |side: f64, middle: f64, lo: f64, extent: f64, along: fn(DVec2) -> f64| -> f64 { if side == 0.0 { return 0.0; } let over = offsets .iter() .map(|offset| { let at = middle + along(*offset); if side > 0.0 { at + half - (lo + extent) } else { lo - (at - half) } }) .fold(0.0, f64::max); if over > 1e-9 { -side * over.min(half) } else { 0.0 } }; dvec2( axis(sx, centre.x, window.pos.x, window.size.x, |offset| offset.x), axis(sy, centre.y, window.pos.y, window.size.y, |offset| offset.y), ) } fn radial_plan( anchor: FloatingAnchor, n: usize, main: f64, item: f64, gap: f64, min_r: f64, max_r: f64, centre: DVec2, window: Rect, ) -> DialPlan { let (start, dir, span) = radial_arc(anchor); let counts = arc_counts(anchor, n, main, item, gap, min_r, max_r, centre, window); let mut plan = DialPlan { radii: arc_radii(&counts, span, item, gap, main, min_r), ..DialPlan::default() }; for (row, &count) in counts.iter().enumerate() { let r = plan.radii[row]; for i in 0..count { let a = if count >= 2 { start + dir * span * i as f64 / (count - 1) as f64 } else { start + dir * span * 0.5 }; plan.offsets.push(dvec2(a.sin() * r, -a.cos() * r)); plan.rows.push(row); } } plan } /// Each arc's radius, innermost first, for arcs holding `counts` actions: /// each as tight as its own count lets it be, and a whole arc step clear of /// the arc inside it, so neither a face nor a square on one touches one on /// the other whatever their angles. fn arc_radii(counts: &[usize], span: f64, item: f64, gap: f64, main: f64, min_r: f64) -> Vec { let mut radii: Vec = Vec::with_capacity(counts.len()); for &count in counts { let least = arc_radius(count, span, item, gap, main, min_r); radii.push(match radii.last() { Some(inner) => least.max(inner + arc_step(item, gap)), None => least, }); } radii } /// How many of `n` actions each arc holds, innermost first. /// /// One arc whenever one fits. Otherwise the outermost arc goes as far out /// as the room allows, as few arcs as the set needs go inside it a step /// apart, and each takes all the spacing lets it hold before the next one /// out takes any: the actions nearest the button are the ones reached /// first, and a half-empty inner arc spends its room on nothing. The room /// is what the window leaves, and no more than `max_r` when that is above /// 0: left to the window alone one arc grows until the far actions are a /// long reach from the button. /// /// A set no number of arcs inside the room can hold is laid out as it /// would be in the least room further out that does hold it: no action is /// made smaller, none is left out, and none rests further from the button /// than the set needs. Past `max_r` that room still keeps inside the /// window whenever the window can hold the set. A window that cannot gets /// arcs from the least radius out, as tight as they go, and they overrun /// it. pub fn arc_counts( anchor: FloatingAnchor, n: usize, main: f64, item: f64, gap: f64, min_r: f64, max_r: f64, centre: DVec2, window: Rect, ) -> Vec { if n == 0 { return Vec::new(); } let (_, _, span) = radial_arc(anchor); let window_room = arc_room(anchor, centre, item, window); let room = window_room.min(furthest_radius(max_r)); if let Some(counts) = arcs_within(n, span, main, item, gap, min_r, room) { return counts; } let least = least_room(n, span, main, item, gap, min_r, room); if least <= window_room + 1e-9 { if let Some(counts) = arcs_within(n, span, main, item, gap, min_r, least) { return counts; } } let step = arc_step(item, gap); let mut counts = Vec::new(); let mut left = n; let mut r = arc_radius(1, span, item, gap, main, min_r); while left > 0 { let held = arc_capacity(r, span, item, gap, main, min_r, left); counts.push(held); left -= held; r += step; } counts } /// The least room past `room` inside which some number of arcs holds all /// `n` actions. /// /// An arc's capacity only changes where its radius reaches the least /// radius of an arc of some count, and the arcs inside a room lie whole /// steps in from it, so the least room is one of those radii a whole number /// of steps further out. A set that fits in a room fits in any larger one, /// so the least is the first of them, in order, that holds it. `n` arcs of /// one a step apart always do. fn least_room(n: usize, span: f64, main: f64, item: f64, gap: f64, min_r: f64, room: f64) -> f64 { let step = arc_step(item, gap); let mut rooms: Vec = (1..=n) .map(|count| arc_radius(count, span, item, gap, main, min_r)) .flat_map(|r| (0..n).map(move |k| r + k as f64 * step)) .filter(|r| *r > room) .collect(); rooms.sort_by(f64::total_cmp); let first = rooms.partition_point(|r| arcs_within(n, span, main, item, gap, min_r, *r).is_none()); rooms.get(first).copied().unwrap_or(f64::INFINITY) } /// How many of `n` actions each arc holds with every arc inside `room`: /// one arc when it fits, and otherwise the fewest arcs a step apart with /// the outermost at `room`, each filled before the next one out takes any. /// `None` when no number of arcs inside `room` holds the set. fn arcs_within(n: usize, span: f64, main: f64, item: f64, gap: f64, min_r: f64, room: f64) -> Option> { if n == 1 || arc_radius(n, span, item, gap, main, min_r) <= room + 1e-9 { return Some(vec![n]); } let step = arc_step(item, gap); let least = arc_radius(1, span, item, gap, main, min_r); let mut arcs = 2; loop { let first = room - (arcs - 1) as f64 * step; if first < least - 1e-9 { return None; } let mut left = n; let mut counts = Vec::new(); for k in 0..arcs { if left == 0 { break; } let held = arc_capacity(first + k as f64 * step, span, item, gap, main, min_r, left); counts.push(held); left -= held; } if left == 0 { return Some(counts); } arcs += 1; } } /// The furthest an arc may be laid for a furthest radius of `max_r`: that /// radius, or no limit at all when it is 0. fn furthest_radius(max_r: f64) -> f64 { if max_r > 0.0 { max_r } else { f64::INFINITY } } /// How many actions an arc of radius `r` spanning `span` holds end to end /// with no two faces closer than `gap` and no two squares overlapping: at /// least one, and at most `most`. pub fn arc_capacity(r: f64, span: f64, item: f64, gap: f64, main: f64, min_r: f64, most: usize) -> usize { let mut held = 1; while held < most && arc_radius(held + 1, span, item, gap, main, min_r) <= r + 1e-9 { held += 1; } held } /// The largest radius an arc from `anchor` around `centre` can have with /// every action on it inside `window`; infinite when nothing bounds it. /// /// Read off the directions the arc covers rather than off the actions on /// it, so the room an arc has does not change as actions are added to it. pub fn arc_room(anchor: FloatingAnchor, centre: DVec2, item: f64, window: Rect) -> f64 { let (start, dir, span) = radial_arc(anchor); let covers = |a: f64| ((a - start) * dir).rem_euclid(std::f64::consts::TAU) <= span + 1e-9; // The arc reaches furthest toward an edge at one of its ends or where // it passes straight toward that edge. let mut angles = vec![start, start + dir * span]; angles.extend( (0..4) .map(|quarter| quarter as f64 * std::f64::consts::FRAC_PI_2) .filter(|a| covers(*a)), ); let half = item * 0.5; let sides = [ (dvec2(1.0, 0.0), window.pos.x + window.size.x - centre.x), (dvec2(-1.0, 0.0), centre.x - window.pos.x), (dvec2(0.0, 1.0), window.pos.y + window.size.y - centre.y), (dvec2(0.0, -1.0), centre.y - window.pos.y), ]; sides.iter().fold(f64::INFINITY, |room, (toward, space)| { let reach = angles .iter() .map(|a| a.sin() * toward.x - a.cos() * toward.y) .fold(f64::NEG_INFINITY, f64::max); if reach > 1e-9 { room.min((space - half) / reach) } else { room } }) } /// A row or a column, folded into parallel lines when one line would leave /// the window. fn line_plan( anchor: FloatingAnchor, dial: SpeedDialLayout, n: usize, main: f64, item: f64, gap: f64, centre: DVec2, window: Rect, ) -> DialPlan { let d0 = main * 0.5 + gap + item * 0.5; let step = item + gap; let along_x = dial == SpeedDialLayout::Horizontal; let (run, across) = line_axes(anchor, dial); let per_line = line_capacity(run, along_x, centre, item, gap, d0, window) .unwrap_or(n) .max(1); let lines = n.div_ceil(per_line).max(1); let centred = |k: usize, count: usize| (k as f64 - (count as f64 - 1.0) * 0.5) * step; let mut plan = DialPlan::default(); for i in 0..n { let line = i / per_line; let in_line = if line + 1 == lines { n - line * per_line } else { per_line }; let along = if run != 0.0 { run * (d0 + (i % per_line) as f64 * step) } else { centred(i % per_line, in_line) }; let off = if run == 0.0 { across * (d0 + line as f64 * step) } else if across != 0.0 { across * line as f64 * step } else { // From the middle of a side the lines have no edge to step away // from, so they are centred on the button like the line itself. centred(line, lines) }; plan.offsets.push(if along_x { dvec2(along, off) } else { dvec2(off, along) }); plan.rows.push(line); } plan } /// How many actions one line holds before it would leave `window`, or None /// when not even one fits and the line is left to overrun. fn line_capacity(run: f64, along_x: bool, centre: DVec2, item: f64, gap: f64, d0: f64, window: Rect) -> Option { let step = item + gap; let half = item * 0.5; let (c, lo, hi) = if along_x { (centre.x, window.pos.x, window.pos.x + window.size.x) } else { (centre.y, window.pos.y, window.pos.y + window.size.y) }; // How many steps past the first action still fit. let further = if run != 0.0 { let space = if run < 0.0 { c - lo } else { hi - c }; (space - d0 - half) / step } else { // Centred on the button: half the line goes either way. ((c - lo).min(hi - c) - half) * 2.0 / step }; (further >= -1e-9).then(|| (further + 1e-9).floor() as usize + 1) } /// `run`, the way a row or a column runs from the button along its own /// axis, 0 when it is centred on the button instead; and `across`, the way /// further lines go, 0 when there is no edge to move away from across the /// line. fn line_axes(anchor: FloatingAnchor, dial: SpeedDialLayout) -> (f64, f64) { let inward = interior(anchor); if dial == SpeedDialLayout::Horizontal { (sign(inward.x), sign(inward.y)) } else { (sign(inward.y), sign(inward.x)) } } fn sign(v: f64) -> f64 { if v > 1e-9 { 1.0 } else if v < -1e-9 { -1.0 } else { 0.0 } } /// Which side of its action a label chip goes on: always the side facing /// the interior, so a chip never pokes out past the edge the set is pinned /// to. pub fn chip_side(anchor: FloatingAnchor, dial: SpeedDialLayout) -> ChipSide { let (sx, sy) = anchor.sides(); match dial { SpeedDialLayout::Vertical => { if sx < 0.0 { ChipSide::Right } else { // Right anchors, and the centre of the top or bottom edge, // where either side is inside and the reading side wins. ChipSide::Left } } SpeedDialLayout::Horizontal => { if sy < 0.0 { ChipSide::Below } else { ChipSide::Above } } SpeedDialLayout::Radial => ChipSide::Outward, } } /// The labels setting with Auto resolved. A column's chips line up beside /// it and never touch each other, so they can all show; an arc's or a /// row's would run into their neighbours, so only the one being aimed at /// shows. pub fn labels_shown(labels: SpeedDialLabels, dial: SpeedDialLayout) -> SpeedDialLabels { match labels { SpeedDialLabels::Auto => match dial { SpeedDialLayout::Vertical => SpeedDialLabels::Always, _ => SpeedDialLabels::Hot, }, other => other, } } /// The labels setting resolved for a set laid out on `rows` arcs or lines. /// A row or a column folded into lines puts the next line a step away, where /// a chip beside one line lies over the other, so a folded set shows only /// the chip being aimed at even when every chip was asked for. pub fn labels_shown_in(labels: SpeedDialLabels, dial: SpeedDialLayout, rows: usize) -> SpeedDialLabels { match labels_shown(labels, dial) { SpeedDialLabels::Always if rows > 1 && dial != SpeedDialLayout::Radial => SpeedDialLabels::Hot, other => other, } } /// Where a chip of `size` goes beside an action of diameter `item` centred /// at `centre`, `gap` clear of the disc on `side`, then pulled inside /// `window` on both axes. /// /// `outward` is the action's offset from the main button, which is the /// direction an Outward chip goes in: past the action, away from the /// button, where nothing else in the set is. `beyond` carries the chip /// that much further out on its side, past the arcs or lines beyond the /// action's own, so it does not lie over them. pub fn chip_rect( centre: DVec2, item: f64, gap: f64, size: DVec2, side: ChipSide, outward: DVec2, beyond: f64, window: Rect, ) -> Rect { let out = item * 0.5 + gap + beyond.max(0.0); let pos = match side { ChipSide::Left => dvec2(centre.x - out - size.x, centre.y - size.y * 0.5), ChipSide::Right => dvec2(centre.x + out, centre.y - size.y * 0.5), ChipSide::Above => dvec2(centre.x - size.x * 0.5, centre.y - out - size.y), ChipSide::Below => dvec2(centre.x - size.x * 0.5, centre.y + out), ChipSide::Outward => { let u = if outward.length() > 1e-9 { outward.normalize() } else { dvec2(0.0, -1.0) }; // How far the chip's own box reaches along that direction from // its middle, so its nearest edge, not its middle, is `gap` out. let reach = u.x.abs() * size.x * 0.5 + u.y.abs() * size.y * 0.5; let mid = centre + u * (out + reach); mid - size * 0.5 } }; Rect { pos: dvec2( span_inboard(pos.x, size.x, window.pos.x, window.size.x), span_inboard(pos.y, size.y, window.pos.y, window.size.y), ), size, } } /// Which side of its action the chip of an action on arc or line `row` goes /// on, and how far past the action's own arc or line it is carried. /// /// On an arc the chip goes straight out, past the outermost arc. Beside a /// single row or column it goes on the side facing the interior. Once a /// row or a column has folded, the lines beside the action's own would be /// under that chip, so it is carried out past them: past the last line on /// the side the lines were added, or, for lines centred on the button, out /// on the side of the set its own line is nearer, where fewer lines are in /// the way. pub fn chip_place(anchor: FloatingAnchor, dial: SpeedDialLayout, row: usize, plan: &DialPlan, item: f64, gap: f64) -> (ChipSide, f64) { let side = chip_side(anchor, dial); if dial == SpeedDialLayout::Radial { let outermost = plan.radii.last().copied().unwrap_or(0.0); let own = plan.radii.get(row).copied().unwrap_or(outermost); return (side, (outermost - own).max(0.0)); } let rows = plan.row_count(); if rows <= 1 { return (side, 0.0); } let step = item + gap; let (run, across) = line_axes(anchor, dial); let last = rows - 1; if run != 0.0 && across == 0.0 { // Centred lines run from the negative side, the side `chip_side` // names, to the positive one. if row * 2 <= last { (side, row as f64 * step) } else { (opposite(side), (last - row) as f64 * step) } } else { // The chip already faces the way further lines go. (side, last.saturating_sub(row) as f64 * step) } } fn opposite(side: ChipSide) -> ChipSide { match side { ChipSide::Left => ChipSide::Right, ChipSide::Right => ChipSide::Left, ChipSide::Above => ChipSide::Below, ChipSide::Below => ChipSide::Above, ChipSide::Outward => ChipSide::Outward, } } /// Whether any of `rect` lies within `radius` of `centre`. pub fn covers_disc(rect: Rect, centre: DVec2, radius: f64) -> bool { // Not `clamp`, which panics on a rect with no size or a NaN edge, and a // panic in a draw cannot unwind out of every platform's window callback. let nearest = dvec2( centre.x.max(rect.pos.x).min(rect.pos.x + rect.size.x), centre.y.max(rect.pos.y).min(rect.pos.y + rect.size.y), ); (nearest - centre).length() < radius - 1e-9 } /// Where a chip goes: where [`chip_rect`] puts it, unless pulling it inside /// `window` has put it over its own action's face, and a chip that hides /// the action it names names nothing. Then it goes beside the face on /// another side: across the way it was pulled, the side facing the interior /// (`inward`) first, and back the other way last. The first of those that /// covers none of `faces` (each a middle and a diameter, the action's own /// among them) wins, or failing that the first that leaves its own face /// clear. pub fn place_chip( centre: DVec2, item: f64, gap: f64, size: DVec2, side: ChipSide, outward: DVec2, beyond: f64, inward: DVec2, window: Rect, faces: &[(DVec2, f64)], ) -> Rect { let own = |rect: Rect| covers_disc(rect, centre, item * 0.5); let first = chip_rect(centre, item, gap, size, side, outward, beyond, window); if !own(first) { return first; } let upright = match side { ChipSide::Above | ChipSide::Below => true, ChipSide::Left | ChipSide::Right => false, ChipSide::Outward => outward.y.abs() >= outward.x.abs(), }; let away = |lean: f64, fallback: f64| if lean > 1e-9 { 1.0 } else if lean < -1e-9 { -1.0 } else { fallback }; let tries = if upright { let (near, far) = if away(inward.x, away(outward.x, -1.0)) > 0.0 { (ChipSide::Right, ChipSide::Left) } else { (ChipSide::Left, ChipSide::Right) }; let back = if side == ChipSide::Below || (side == ChipSide::Outward && outward.y > 0.0) { ChipSide::Above } else { ChipSide::Below }; [near, far, back] } else { let (near, far) = if away(inward.y, away(outward.y, -1.0)) > 0.0 { (ChipSide::Below, ChipSide::Above) } else { (ChipSide::Above, ChipSide::Below) }; let back = if side == ChipSide::Right || (side == ChipSide::Outward && outward.x > 0.0) { ChipSide::Left } else { ChipSide::Right }; [near, far, back] }; let beside: Vec = tries .iter() .map(|side| chip_rect(centre, item, gap, size, *side, outward, 0.0, window)) .filter(|rect| !own(*rect)) .collect(); let clear = |rect: &&Rect| !faces.iter().any(|(middle, d)| covers_disc(**rect, *middle, d * 0.5)); beside.iter().find(clear).or(beside.first()).copied().unwrap_or(first) } /// The action an arrow key moves to from `from` (or from `origin`, the main /// button, when no action has focus), given every action's position. /// /// Only actions within the arrow's cone count. Of those the nearest wins, /// and among near-ties the one better aligned with the arrow, then the /// earlier one. Arrows follow the direction the actions went rather than /// stepping through a list: in an arc, "next" has no direction, and the /// hand already knows where the actions are. With no candidate, focus /// stays where it is. pub fn arrow_target(points: &[DVec2], from: Option, origin: DVec2, dir: DVec2) -> Option { let base = match from { Some(i) => *points.get(i)?, None => origin, }; nearest_in_cone( base, points.iter().copied().enumerate().filter(|(j, _)| Some(*j) != from), dir, ) } /// The action an arrow key moves to on an arc, given every action's /// position in the order the arcs are filled: from the button, the one /// `arrow_target` picks on the inner arc (its first `first_arc` points); /// from an action, whichever of its two neighbours in that order the arrow /// points toward, the one it points at more squarely when it points toward /// both, and none when it points toward neither. /// /// An arc is walked in order rather than by distance. Across two arcs the /// nearest action the arrow's way is often on the other arc, and a walk /// that hops by distance passes some actions by for good. In order, the /// inner arc is walked to its end and then on round the outer one. Where /// the inner arc ends along one edge and the outer one starts back along /// the other, the step between them goes the other way from the steps /// before it, and the arrow that takes it is one that points that way. /// Every step can still be taken and taken back: of two different /// directions, one of the four arrows always points toward the first and /// more squarely at it than at the second. pub fn arc_walk_target(points: &[DVec2], first_arc: usize, from: Option, origin: DVec2, dir: DVec2) -> Option { let Some(i) = from else { let inner = if first_arc == 0 { points.len() } else { first_arc.min(points.len()) }; return arrow_target(&points[..inner], None, origin, dir); }; let here = *points.get(i)?; // How squarely the arrow points at neighbour `j`, when it points // toward it at all. let toward = |j: Option| -> Option<(usize, f64)> { let j = j?; let v = points[j] - here; let len = v.length(); if len < 1e-9 { return None; } let along = (v.x * dir.x + v.y * dir.y) / len; (along > 1e-9).then_some((j, along)) }; let before = toward(i.checked_sub(1)); let after = toward((i + 1 < points.len()).then_some(i + 1)); match (after, before) { (Some(a), Some(b)) if (a.1 - b.1).abs() <= 1e-9 => None, (Some(a), Some(b)) => Some(if a.1 > b.1 { a.0 } else { b.0 }), (Some(a), None) => Some(a.0), (None, Some(b)) => Some(b.0), (None, None) => None, } } /// Of `candidates`, the one an arrow along `dir` from `base` means: only /// those within the arrow's cone count, the nearest wins, and among /// near-ties the better aligned, then the earlier. fn nearest_in_cone(base: DVec2, candidates: impl Iterator, dir: DVec2) -> Option { let cone = ARROW_CONE_DEGREES.to_radians().cos(); let within: Vec<(usize, f64, f64)> = candidates .filter_map(|(j, p)| { let v = p - base; let len = v.length(); if len < 1e-9 { return None; } let along = (v.x * dir.x + v.y * dir.y) / len; (along > cone).then_some((j, len, along)) }) .collect(); let nearest = within.iter().map(|c| c.1).fold(f64::INFINITY, f64::min); within .iter() .filter(|c| c.1 <= nearest + ARROW_TIE) .fold(None::<(usize, f64, f64)>, |best, c| match best { // Candidates come in index order, so keeping the earlier one on // an exact tie is keeping the lower index. Some(b) if b.2 >= c.2 => Some(b), _ => Some(*c), }) .map(|c| c.0) } /// How far action `i` has travelled out, `elapsed` seconds after a closed /// set opened: 0 at the button, 1 in place. /// /// Each action starts `stagger` later than the one before, so the set reads /// as coming out in order rather than as one lump, and moves along `ease`, /// one of the theme's motion easings. An ease that overshoots carries the /// value past 1 and back, which is what choosing it asks for, so the value /// is not clamped. With motion reduced everything is already in place. pub fn enter_progress(i: usize, elapsed: f64, enter: f64, stagger: f64, ease: &Ease, reduced: bool) -> f64 { if reduced { return 1.0; } // The leg a closed set gives its action `i` when it opens. legs_to(&vec![0.0; i + 1], 1.0, enter, stagger)[i].value(elapsed, ease) } /// How far action `i` of `n` still is from the button, `elapsed` seconds /// after an open set was put away: 1 in place, 0 back under the button. /// /// The last action out is the first one back, so the set folds up the way /// it came instead of the first action crossing the rest on its way in. pub fn exit_progress(i: usize, n: usize, elapsed: f64, exit: f64, stagger: f64, ease: &Ease, reduced: bool) -> f64 { if reduced { return 0.0; } // The leg an open set gives its action `i` when it is put away. legs_to(&vec![1.0; n.max(i + 1)], 0.0, exit, stagger)[i].value(elapsed, ease) } /// `ease` at `x`, exact at both ends: some easings only come near their end /// value, and an action that never quite lands keeps asking for frames. fn eased(ease: &Ease, x: f64) -> f64 { if x <= 0.0 { 0.0 } else if x >= 1.0 { 1.0 } else { ease.map(x) } } /// The delay between one action setting off and the next, for a set of `n`: /// the `stagger` asked for, shrunk when the whole set would otherwise take /// more than [`STAGGER_BUDGET`] longer to open than one action does. pub fn stagger_step(n: usize, stagger: f64) -> f64 { let stagger = stagger.max(0.0); if n < 2 { stagger } else { stagger.min(STAGGER_BUDGET / (n - 1) as f64) } } /// One stretch of an action's travel, or of the glyph's turn: from where it /// was when the stretch began to where it ends, setting off after `delay` /// and taking `secs`. Positions are shares of the way out, 0 under the /// button and 1 in place. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Leg { pub from: f64, pub to: f64, pub delay: f64, pub secs: f64, } impl Leg { /// Already where it is going. pub fn at_rest(value: f64) -> Leg { Leg { from: value, to: value, delay: 0.0, secs: 0.0, } } /// Where the leg is `elapsed` seconds after the legs set off. Not clamped /// between its ends, for the ease that overshoots. pub fn value(&self, elapsed: f64, ease: &Ease) -> f64 { self.from + (self.to - self.from) * eased(ease, self.share(elapsed)) } /// Whether the leg has arrived by `elapsed`. pub fn done(&self, elapsed: f64) -> bool { self.share(elapsed) >= 1.0 } fn share(&self, elapsed: f64) -> f64 { let t = elapsed - self.delay; if self.secs <= 0.0 { if t >= 0.0 { 1.0 } else { 0.0 } } else { (t / self.secs).clamp(0.0, 1.0) } } } /// The legs a set's actions take toward `to` (1 out, 0 back) from `now`, /// their positions at this moment, over `secs` and `step` apart. /// /// From a set entirely at the other end they go in turn: out in the order /// the actions were declared, back the other way round. From anywhere else, /// a set reopened on its way back or put away while still coming out, every /// action turns round at once from where it is and takes the share of /// `secs` its distance asks for, so an action nearly home does not crawl /// the last of the way and none waits in mid-air for its turn. pub fn legs_to(now: &[f64], to: f64, secs: f64, step: f64) -> Vec { let n = now.len(); let origin = 1.0 - to; let fresh = now.iter().all(|v| (v - origin).abs() < 1e-9); now.iter() .enumerate() .map(|(i, &from)| { if fresh { let order = if to > 0.5 { i } else { n - 1 - i }; Leg { from, to, delay: order as f64 * step, secs, } } else { Leg { from, to, delay: 0.0, secs: secs * (to - from).abs().min(1.0), } } }) .collect() } /// A disc of `size` centred at `centre`, moved just far enough to lie /// inside `room`. An ease that overshoots carries an action past its place, /// and past the window's edge the overshoot would be cut off; it stops at /// the edge instead. `room` includes the resting place, so a resting action /// is never moved, even in a window too small for the set. pub fn keep_inside(centre: DVec2, size: f64, room: Rect) -> DVec2 { let half = size * 0.5; dvec2( span_inboard(centre.x - half, size, room.pos.x, room.size.x) + half, span_inboard(centre.y - half, size, room.pos.y, room.size.y) + half, ) } /// What a set pinned to a partly hidden container may draw in: where the /// container's `visible` part stops on the sides that are hidden, and the /// `window`'s edges on every other side. /// /// The actions reach past their container by design, and an overshoot /// further, so the container's own edges cut nothing; only an edge that /// really hides the container, a scrolled view's, hides the set as well. A /// container hidden entirely leaves nothing to draw in. pub fn cut_sides(visible: Rect, host: Rect, window: Rect) -> Rect { if visible.size.x <= 0.0 || visible.size.y <= 0.0 { return Rect { pos: visible.pos, size: dvec2(0.0, 0.0), }; } let (vx1, vy1) = (visible.pos.x + visible.size.x, visible.pos.y + visible.size.y); let x0 = if visible.pos.x > host.pos.x + HONEST_SLACK { visible.pos.x } else { window.pos.x }; let y0 = if visible.pos.y > host.pos.y + HONEST_SLACK { visible.pos.y } else { window.pos.y }; let x1 = if vx1 < host.pos.x + host.size.x - HONEST_SLACK { vx1 } else { window.pos.x + window.size.x }; let y1 = if vy1 < host.pos.y + host.size.y - HONEST_SLACK { vy1 } else { window.pos.y + window.size.y }; Rect { pos: dvec2(x0, y0), size: dvec2((x1 - x0).max(0.0), (y1 - y0).max(0.0)), } } /// What a set pinned to a partly hidden container may draw in this draw, /// and the part of the container still on screen: `cut_sides` of what the /// event side last read (`visible_then` of `host_then`), laid over the /// container where this draw pins it. /// /// The edges that hid the container then are the edges of a view it is /// scrolled in, and a scroll moves the container under them without moving /// them. So they stay where they were read. Were they carried along with /// the container, every scroll step would draw the set once over whatever /// sits beyond that view's edge before the next read put the cut back. pub fn cut_now(visible_then: Rect, host_then: Rect, host_now: Rect, window: Rect) -> (Rect, Rect) { let open = cut_sides(visible_then, host_then, window); (intersect(host_now, open), open) } /// The part both rects cover, empty when they do not meet. fn intersect(a: Rect, b: Rect) -> Rect { let x0 = a.pos.x.max(b.pos.x); let y0 = a.pos.y.max(b.pos.y); let x1 = (a.pos.x + a.size.x).min(b.pos.x + b.size.x); let y1 = (a.pos.y + a.size.y).min(b.pos.y + b.size.y); Rect { pos: dvec2(x0, y0), size: dvec2((x1 - x0).max(0.0), (y1 - y0).max(0.0)), } } fn contains_rect(outer: Rect, inner: Rect) -> bool { inner.pos.x >= outer.pos.x - HONEST_SLACK && inner.pos.y >= outer.pos.y - HONEST_SLACK && inner.pos.x + inner.size.x <= outer.pos.x + outer.size.x + HONEST_SLACK && inner.pos.y + inner.size.y <= outer.pos.y + outer.size.y + HONEST_SLACK } /// The smallest rect holding both. fn bounding(a: Rect, b: Rect) -> Rect { let x0 = a.pos.x.min(b.pos.x); let y0 = a.pos.y.min(b.pos.y); let x1 = (a.pos.x + a.size.x).max(b.pos.x + b.size.x); let y1 = (a.pos.y + a.size.y).max(b.pos.y + b.size.y); Rect { pos: dvec2(x0, y0), size: dvec2(x1 - x0, y1 - y0), } } /// The widget's state as the test tree reads it: whether the set is out, /// how it is laid out, and where it is pinned. pub fn snapshot_text(open: bool, pinned: bool, dial: SpeedDialLayout, anchor: FloatingAnchor) -> String { let state = if pinned { "pinned" } else if open { "open" } else { "closed" }; format!("{state} {} {}", dial.slug(), anchor.slug()) } /// `rect` grown by `by` on every side. fn grown(rect: Rect, by: f64) -> Rect { Rect { pos: rect.pos - by, size: rect.size + by * 2.0, } } fn near(a: Rect, b: Rect) -> bool { (a.pos.x - b.pos.x).abs() <= HONEST_SLACK && (a.pos.y - b.pos.y).abs() <= HONEST_SLACK && (a.size.x - b.size.x).abs() <= HONEST_SLACK && (a.size.y - b.size.y).abs() <= HONEST_SLACK } fn has_size(rect: Rect) -> bool { rect.size.x > 0.0 && rect.size.y > 0.0 } /// Where Tab goes from `focus` among `stops`, in their order and round the /// ends; from outside them, to the first, or with `back` to the last. fn tab_target(stops: &[T], focus: T, back: bool) -> Option { let len = stops.len(); if len == 0 { return None; } Some(match (stops.iter().position(|stop| *stop == focus), back) { (Some(i), false) => stops[(i + 1) % len], (Some(i), true) => stops[(i + len - 1) % len], (None, false) => stops[0], (None, true) => stops[len - 1], }) } /// How far a shadow reaches past its disc: half as far again as its blur, /// where the shade has faded to well under a level, plus its drop. fn shadow_reach(draw: &DrawFloatingShadow, cx: &mut Cx) -> f64 { let mut blur = [0.0f32]; let mut drop = [0.0f32]; draw.get_uniform(cx, live_id!(blur), &mut blur); draw.get_uniform(cx, live_id!(offset_y), &mut drop); (blur[0] as f64 * 1.5 + (drop[0] as f64).abs()).max(2.0) } /// Read the named children of a widget out of the object it was applied /// with, building the new ones and re-applying the ones it has. /// /// Only named children are read: an action is reported by the id it was /// declared under, and an anonymous one would have nothing to report. fn apply_children( vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue, children: &mut Vec<(LiveId, WidgetRef)>, order: &mut Vec, ) { if !apply.is_eval() { if let Some(object) = value.as_object() { vm.vec_with(object, |vm, values| { for value in values { let Some(id) = value.key.as_id() else { continue; }; if !WidgetRef::value_is_newable_widget(vm, value.value) { continue; } if apply.is_reload() { order.push(id); } if let Some((_, child)) = children.iter_mut().find(|(child_id, _)| *child_id == id) { child.script_apply(vm, apply, scope, value.value); } else { children.push((id, WidgetRef::script_from_value_scoped(vm, scope, value.value))); } } }); } } // A reload rewrites the declaration order, and the declaration order is // the order the set is laid out in. if apply.is_reload() && (!order.is_empty() || children.is_empty()) { for (index, id) in order.iter().enumerate() { if let Some(position) = children.iter().position(|(old, _)| old == id) { children.swap(index, position); } } children.truncate(order.len()); } } /// A soft disc under a round face. #[derive(Script, ScriptHook)] #[repr(C)] pub struct DrawFloatingShadow { #[deref] draw_super: DrawQuad, /// The disc's radius, in points. #[live] pub radius: f32, /// How dark, as a share of the shadow's ink. #[live] pub weight: f32, } /// The rounded box behind a label chip. #[derive(Script, ScriptHook)] #[repr(C)] pub struct DrawFloatingChip { #[deref] draw_super: DrawQuad, } /// Where the set puts an action this draw. Written by the floating action /// before it draws the action, because only the set knows where its /// actions go. #[derive(Clone, Copy, Debug, Default)] struct ItemPlace { centre: DVec2, size: f64, side: ChipSide, outward: DVec2, beyond: f64, /// The way into the container, for a chip that has to find another /// side of its action. inward: DVec2, window: Rect, show_chip: bool, } /// One action in a floating action's set. #[derive(Script, WidgetRef, WidgetSet, WidgetRegister)] pub struct FloatingActionItem { #[uid] uid: WidgetUid, #[source] source: ScriptObjectRef, #[walk] walk: Walk, #[layout] layout: Layout, #[live] draw_shadow: DrawFloatingShadow, #[live] draw_chip: DrawFloatingChip, #[live] draw_chip_text: DrawText, /// The words on the chip. #[live] pub label: String, /// Whether the action answers presses. #[live(true)] pub enabled: bool, /// Whether the action is in the set at all. A hidden action is left out /// of the layout, so the set closes up rather than keeping its place. #[live(true)] pub visible: bool, #[live(8.0)] pub chip_gap: f64, #[live] pub chip_padding: Inset, #[rust] children: Vec<(LiveId, WidgetRef)>, #[rust] update_order: Vec, /// The `button :=` child. #[rust] button: WidgetRef, #[rust] place: ItemPlace, /// The chip drawn last, window-absolute, so a press on it is not read /// as a press outside the set. #[rust] chip_rect: Option, /// The list of the set the action is in, written by the set. The set /// lays its actions out, so a change to one is a change to the set; and /// an action never drawn has no area of its own to redraw from. #[rust] owner_list: Option, } impl ScriptHook for FloatingActionItem { fn on_before_apply(&mut self, _vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, _value: ScriptValue) { if !apply.is_eval() { self.update_order.clear(); } } fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue) { apply_children(vm, apply, scope, value, &mut self.children, &mut self.update_order); self.button = self .children .iter() .find(|(id, _)| *id == live_id!(button)) .map(|(_, child)| child.clone()) .unwrap_or_default(); let uid = self.uid; let enabled = self.enabled; let button = self.button.clone(); let owner = self.owner_list; vm.with_cx_mut(|cx| { cx.widget_tree_mark_dirty(uid); // `enabled` is the item's to say, and the button is what has to // refuse the press and draw itself dimmed. sync_enabled(cx, &button, enabled); // A change to `visible` changes the whole set's layout. redraw_set(cx, owner, &button); }); } } /// Redraw the set an action is in: its list when the set has said which, /// and otherwise the list the action's button was last drawn in, which is /// the set's too. fn redraw_set(cx: &mut Cx, owner: Option, button: &WidgetRef) { match owner { Some(list) => cx.redraw_list(list), None => button.redraw(cx), } } fn sync_enabled(cx: &mut Cx, button: &WidgetRef, enabled: bool) { if button.is_empty() { return; } // Two switches: `enabled` is what refuses the press and picks the // cursor, the disabled track is what draws the face dimmed. button.as_button().set_enabled(cx, enabled); button.set_disabled(cx, !enabled); } impl WidgetNode for FloatingActionItem { fn widget_uid(&self) -> WidgetUid { self.uid } fn walk(&mut self, _cx: &mut Cx) -> Walk { self.walk } fn area(&self) -> Area { self.button.area() } fn redraw(&mut self, cx: &mut Cx) { redraw_set(cx, self.owner_list, &self.button); } fn children(&self, visit: &mut dyn FnMut(LiveId, WidgetRef)) { if !self.button.is_empty() { visit(live_id!(button), self.button.clone()); } } fn find_widgets_from_point(&self, cx: &Cx, point: DVec2, found: &mut dyn FnMut(&WidgetRef)) { self.button.find_widgets_from_point(cx, point, found); } fn set_visible(&mut self, cx: &mut Cx, visible: bool) { if self.visible != visible { self.visible = visible; redraw_set(cx, self.owner_list, &self.button); } } fn visible(&self) -> bool { self.visible } } impl FloatingActionItem { pub fn set_label(&mut self, cx: &mut Cx, label: &str) { if self.label != label { self.label = label.to_string(); redraw_set(cx, self.owner_list, &self.button); } } pub fn set_enabled(&mut self, cx: &mut Cx, enabled: bool) { if self.enabled != enabled { self.enabled = enabled; sync_enabled(cx, &self.button, enabled); } } /// The shadow and the button, where the set placed them this draw. fn draw_face(&mut self, cx: &mut Cx2d, scope: &mut Scope) { let place = self.place; let d = place.size.max(1.0); let half = d * 0.5; let face = Rect { pos: place.centre - half, size: dvec2(d, d), }; let reach = shadow_reach(&self.draw_shadow, cx); self.draw_shadow.radius = half as f32; self.draw_shadow.weight = ITEM_SHADOW_WEIGHT; self.draw_shadow.draw_abs(cx, grown(face, reach)); let _ = self.button.draw_walk(cx, scope, Walk::fixed(d, d).with_abs_pos(face.pos)); self.chip_rect = None; } /// The label chip, when the set asked for one. The set draws every /// action's face before any chip, so a chip is never drawn under a /// neighbouring face; `faces` are where those faces rest, the button's /// among them, for a chip that has to find a side clear of them. fn draw_label_chip(&mut self, cx: &mut Cx2d, faces: &[(DVec2, f64)]) { let place = self.place; self.chip_rect = None; if !place.show_chip || self.label.is_empty() { return; } let d = place.size.max(1.0); let label = self.label.clone(); let laid = self .draw_chip_text .layout(cx, 0.0, 0.0, None, false, Align::default(), &label); let scale = self.draw_chip_text.font_scale as f64; let text = dvec2( laid.size_in_lpxs.width as f64 * scale, laid.size_in_lpxs.height as f64 * scale, ); let pad = self.chip_padding; let size = dvec2(text.x + pad.left + pad.right, text.y + pad.top + pad.bottom); let rect = place_chip( place.centre, d, self.chip_gap, size, place.side, place.outward, place.beyond, place.inward, place.window, faces, ); self.draw_chip.draw_abs(cx, rect); self.draw_chip_text.draw_abs(cx, rect.pos + dvec2(pad.left, pad.top), &label); self.chip_rect = Some(rect); } } impl Widget for FloatingActionItem { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.button.handle_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, _walk: Walk) -> DrawStep { self.draw_face(cx, scope); let own = [(self.place.centre, self.place.size)]; self.draw_label_chip(cx, &own); DrawStep::done() } fn text(&self) -> String { self.label.clone() } fn set_text(&mut self, cx: &mut Cx, v: &str) { self.set_label(cx, v); } } impl FloatingActionItemRef { pub fn set_label(&self, cx: &mut Cx, label: &str) { if let Some(mut inner) = self.borrow_mut() { inner.set_label(cx, label); } } pub fn set_enabled(&self, cx: &mut Cx, enabled: bool) { if let Some(mut inner) = self.borrow_mut() { inner.set_enabled(cx, enabled); } } } /// The one action a screen exists for, and the set behind it. #[derive(Script, WidgetRef, WidgetSet, WidgetRegister)] pub struct FloatingAction { #[uid] uid: WidgetUid, #[source] source: ScriptObjectRef, #[walk] walk: Walk, #[layout] layout: Layout, #[live] draw_shadow: DrawFloatingShadow, #[live] pub anchor: FloatingAnchor, #[live] pub pin_to: FloatingPin, /// Laid out in the parent's flow; `pin_to` and `edge_margin` are not /// read. #[live(false)] pub inline: bool, #[live] pub edge_margin: Inset, /// Named `dial` because `layout` is the widget's own layout. #[live] pub dial: SpeedDialLayout, #[live] pub labels: SpeedDialLabels, #[live(56.0)] pub size: f64, #[live(40.0)] pub item_size: f64, #[live(12.0)] pub item_gap: f64, #[live(88.0)] pub radial_radius: f64, /// The furthest an arc is laid from the button's middle; 0 is no limit. /// Left to the window alone a large set grows one arc as wide as the /// window lets it be, and eight from a corner rest four and a half /// buttons away from the hand that opened them. Held inside this radius /// the set goes on round further arcs a step apart, the inner filled /// first, as it does in a window that small. The default holds up to /// twelve from a corner. A set no arcs inside it can hold is neither /// shrunk nor cut: it rests on the arcs of the least room past this /// radius that holds it. #[live(200.0)] pub radial_max_radius: f64, #[live(true)] pub close_on_pick: bool, /// Out from the first draw and kept out: no pointer grab, no outside /// or Escape close, picks still reported. For catalogues and teaching /// overlays, as the pie menu's `pinned` is. #[live(false)] pub pinned: bool, #[live(0.2)] pub enter_secs: f64, /// How the actions travel out and the plus turns to the cross. #[live(Ease::OutCubic)] pub enter_ease: Ease, #[live(0.15)] pub exit_secs: f64, /// How the actions travel back and the cross turns back to the plus. #[live(Ease::InCubic)] pub exit_ease: Ease, #[live(0.03)] pub stagger_secs: f64, #[live(false)] pub reduced_motion: bool, #[live(true)] visible: bool, #[rust] children: Vec<(LiveId, WidgetRef)>, #[rust] update_order: Vec, /// The `main :=` child. #[rust] main: WidgetRef, /// Every `FloatingActionItem` child, in declaration order. #[rust] items: Vec<(LiveId, WidgetRef)>, /// Children of another type, already reported once. #[rust] ignored: Vec, #[rust] draw_list: Option, #[rust] open: bool, /// When the legs below set off. #[rust] motion_started: f64, /// Each shown action's leg, in layout order. #[rust] legs: Vec, /// The glyph's leg: 0 the plus, 1 the cross. #[rust] turn: Leg, /// The glyph value last written to the main face. #[rust] turn_written: f64, /// Where the shown actions rest, laid out by the last draw around /// `plan_centre`. Presses and arrow keys read this, never where an /// action happens to be mid-travel. #[rust] plan: DialPlan, #[rust] plan_centre: DVec2, /// An action pressed while the set was still moving. Its button did not /// hear the press, so the set picks it when the release comes over the /// same action. #[rust] pressed_item: Option, /// From the press that opened the set until its release: a release on /// an action picks it, so press, drag and release is one gesture. #[rust] opening_press: bool, /// The sweep lock is owed but not taken: the set opened on a press, and /// the main button must still see the release of that press or it /// stays drawn as held. #[rust] lock_pending: bool, #[rust] locked: bool, /// The area the lock was last taken with. #[rust] lock_area: Area, /// A press outside closed the set; its release is eaten before the lock /// goes, so the dismissing click does not act on what was underneath. #[rust] swallow_up: bool, /// The action under the pointer, for Hot chips. #[rust] hot_item: Option, /// Opened from the keyboard: key focus moves to the first action once /// the actions have areas to take it with. #[rust] focus_first: bool, /// The trap was begun before the set was drawn and must be pointed at /// the draw that has the stops in it. #[rust] trap_pending: bool, #[rust] focus_trap: FocusTrap, /// The container's rect, recorded in its own list so the event side can /// read where alignment really put it. #[rust] host_area: Area, #[rust] host_honest: Rect, /// The container's rect as seen mid-draw, the last draw. #[rust] host_seen: Rect, /// The container rect the last draw pinned against. #[rust] host_used: Rect, /// The part of the container its own ancestors leave on screen, when /// that is not all of it: a page scrolled until the container is half /// under its top edge. The overlay is cut to it, or a button pinned to /// a container that has scrolled away would float on over whatever the /// page scrolled under. #[rust] host_visible: Option, /// The main button's rect, window-absolute and after alignment. #[rust] main_honest: Rect, /// Inline: the main button's rect as seen mid-draw, the last draw. #[rust] main_seen: Rect, /// The centre the set was last laid out around. #[rust] centre_used: DVec2, #[rust] next_frame: NextFrame, /// One frame after a draw, to compare what was drawn with where /// alignment put it. #[rust] check_frame: NextFrame, #[rust] was_pinned: bool, } impl ScriptHook for FloatingAction { fn on_after_new(&mut self, vm: &mut ScriptVm) { self.draw_list = Some(DrawList2d::script_new(vm)); } fn on_before_apply(&mut self, _vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, _value: ScriptValue) { if !apply.is_eval() { self.update_order.clear(); } } fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue) { apply_children(vm, apply, scope, value, &mut self.children, &mut self.update_order); self.sort_children(); let uid = self.uid; vm.with_cx_mut(|cx| { cx.widget_tree_mark_dirty(uid); // Unpinned while out: the set was never given a lock or a trap, // so it is put away rather than left out with neither. let unpinned = self.was_pinned && !self.pinned; if self.open && (unpinned || self.shown_items().is_empty()) { self.shut(cx, false); } // Pinned while out: a pinned set never closes, so a grab and a // trap taken for the open would never be given back, and every // press outside the set would go on being turned away. if self.open && self.pinned && !self.was_pinned { self.opening_press = false; self.lock_pending = false; self.swallow_up = false; self.unlock(cx); self.trap_pending = false; self.focus_trap.end(cx); } self.was_pinned = self.pinned; self.redraw(cx); }); } } impl WidgetNode for FloatingAction { fn widget_uid(&self) -> WidgetUid { self.uid } /// Pinned, it claims no room: the button floats over the container and /// a walk of any size would push the container's content aside, or, /// as a fill, take a share of it. Inline, it is the button's own size. fn walk(&mut self, _cx: &mut Cx) -> Walk { if self.inline { Walk { width: Size::Fixed(self.size), height: Size::Fixed(self.size), ..self.walk } } else { Walk::empty() } } /// The main button's area, so the test tree reports the button's rect /// for the widget. fn area(&self) -> Area { self.main.area() } fn redraw(&mut self, cx: &mut Cx) { self.main.redraw(cx); if let Some(draw_list) = &self.draw_list { draw_list.redraw(cx); } } fn children(&self, visit: &mut dyn FnMut(LiveId, WidgetRef)) { if !self.main.is_empty() { visit(live_id!(main), self.main.clone()); } for (id, item) in &self.items { visit(*id, item.clone()); } } fn find_widgets_from_point(&self, cx: &Cx, point: DVec2, found: &mut dyn FnMut(&WidgetRef)) { self.main.find_widgets_from_point(cx, point, found); if self.open { for (_, item) in &self.items { item.find_widgets_from_point(cx, point, found); } } } fn set_visible(&mut self, cx: &mut Cx, visible: bool) { if self.visible == visible { return; } self.visible = visible; if !visible { self.shut(cx, false); self.unlock(cx); self.swallow_up = false; // Nothing is drawn to watch the set go away, so it is away now. self.land(0.0); } if visible && self.main.area().is_empty() { // Never drawn, so nothing knows where to redraw it from. cx.redraw_all(); } else { self.host_area.redraw(cx); self.redraw(cx); } } fn visible(&self) -> bool { self.visible } } /// The keys that press the set's button, as they press any button. fn is_activation_key(key_code: KeyCode) -> bool { matches!(key_code, KeyCode::Space | KeyCode::ReturnKey | KeyCode::NumpadEnter) } impl FloatingAction { fn sort_children(&mut self) { self.main = WidgetRef::default(); let mut items = Vec::new(); let owner = self.draw_list.as_ref().map(|list| list.id()); for (id, child) in &self.children { if *id == live_id!(main) { self.main = child.clone(); } else if let Some(mut inner) = child.borrow_mut::() { inner.owner_list = owner; items.push((*id, child.clone())); } else if !self.ignored.contains(id) { self.ignored.push(*id); log!("FloatingAction: child {} is not a FloatingActionItem and is ignored", id); } } if items.len() != self.items.len() { self.hot_item = None; self.pressed_item = None; } self.items = items; } fn item_visible(&self, i: usize) -> bool { self.items .get(i) .and_then(|(_, item)| item.borrow::().map(|inner| inner.visible)) .unwrap_or(false) } /// The actions in the set, as indices into `items`, in layout order. fn shown_items(&self) -> Vec { (0..self.items.len()).filter(|i| self.item_visible(*i)).collect() } fn ease_toward(&self, to: f64) -> Ease { if to > 0.5 { self.enter_ease } else { self.exit_ease } } /// Every shown action's position `now`. The legs are matched to the /// shown set first: an action shown or hidden while the set is out joins /// it where the set is headed. fn positions(&mut self, now: f64) -> Vec { let n = self.shown_items().len(); if self.legs.len() != n { let rest = Leg::at_rest(if self.open { 1.0 } else { 0.0 }); self.legs.resize(n, rest); } let elapsed = now - self.motion_started; let (enter, exit) = (self.enter_ease, self.exit_ease); self.legs .iter() .map(|leg| leg.value(elapsed, if leg.to > 0.5 { &enter } else { &exit })) .collect() } fn turn_value(&self, now: f64) -> f64 { self.turn.value(now - self.motion_started, &self.ease_toward(self.turn.to)) } /// Whether any action is still on its way. fn travelling(&self, now: f64) -> bool { let elapsed = now - self.motion_started; self.legs.iter().any(|leg| !leg.done(elapsed)) } /// Whether anything, an action or the glyph, is still on its way. fn moving(&self, now: f64) -> bool { self.travelling(now) || !self.turn.done(now - self.motion_started) } /// Set every action and the glyph off toward `to`, from wherever each is. /// Called before `open` changes, so an action that joined the set since /// the last draw starts from where the set was. fn set_off(&mut self, cx: &mut Cx, to: f64) { let now = cx.seconds_since_app_start(); let positions = self.positions(now); let turn = self.turn_value(now); if self.reduced_motion { self.land(to); } else { let secs = if to > 0.5 { self.enter_secs } else { self.exit_secs }; let step = stagger_step(positions.len(), self.stagger_secs); self.legs = legs_to(&positions, to, secs, step); self.turn = legs_to(&[turn], to, secs, 0.0)[0]; } self.motion_started = now; self.next_frame = cx.new_next_frame(); } /// Everything already at `to`. fn land(&mut self, to: f64) { let n = self.shown_items().len(); self.legs = vec![Leg::at_rest(to); n]; self.turn = Leg::at_rest(to); } /// Write the glyph's turn into the main face. /// /// The button's own open track is cut to its end the moment the set /// opens or closes, and the set plays the turn instead, on its own eases /// and clock, so the plus turns with the actions rather than on a curve /// of its own. It is written the way an animation frame is, which /// updates the face without rebuilding the button, and on every draw /// while it turns: the button's other tracks re-apply the cut value on /// their own frames. fn write_turn(&mut self, cx: &mut Cx, now: f64) { let value = self.turn_value(now); let turning = !self.turn.done(now - self.motion_started); if !turning && (value - self.turn_written).abs() < 1e-9 { return; } let Some(mut button) = self.main.borrow_mut::