5555 lines
227 KiB
Rust
5555 lines
227 KiB
Rust
//! 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<DVec2> {
|
|
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<DVec2>,
|
|
/// 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<usize>,
|
|
/// Each arc's radius, innermost first; empty for a row or a column.
|
|
pub radii: Vec<f64>,
|
|
}
|
|
|
|
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<f64> {
|
|
let mut radii: Vec<f64> = 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<usize> {
|
|
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<f64> = (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<Vec<usize>> {
|
|
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<usize> {
|
|
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<Rect> = 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<usize>, origin: DVec2, dir: DVec2) -> Option<usize> {
|
|
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<usize>, origin: DVec2, dir: DVec2) -> Option<usize> {
|
|
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<usize>| -> 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<Item = (usize, DVec2)>, dir: DVec2) -> Option<usize> {
|
|
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<Leg> {
|
|
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<T: Copy + PartialEq>(stops: &[T], focus: T, back: bool) -> Option<T> {
|
|
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<LiveId>,
|
|
) {
|
|
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<LiveId>,
|
|
/// 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<Rect>,
|
|
/// 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<DrawListId>,
|
|
}
|
|
|
|
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<DrawListId>, 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<LiveId>,
|
|
/// 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<LiveId>,
|
|
#[rust]
|
|
draw_list: Option<DrawList2d>,
|
|
|
|
#[rust]
|
|
open: bool,
|
|
/// When the legs below set off.
|
|
#[rust]
|
|
motion_started: f64,
|
|
/// Each shown action's leg, in layout order.
|
|
#[rust]
|
|
legs: Vec<Leg>,
|
|
/// 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<usize>,
|
|
/// 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<usize>,
|
|
/// 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<Rect>,
|
|
/// 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::<FloatingActionItem>() {
|
|
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::<FloatingActionItem>().map(|inner| inner.visible))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// The actions in the set, as indices into `items`, in layout order.
|
|
fn shown_items(&self) -> Vec<usize> {
|
|
(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<f64> {
|
|
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::<Button>() else {
|
|
return;
|
|
};
|
|
self.turn_written = value;
|
|
cx.with_vm(|vm| {
|
|
let face = vm.bx.heap.new_object();
|
|
vm.bx.heap.set_value_def(face, live_id!(open).into(), value.into());
|
|
let apply = vm.bx.heap.new_object();
|
|
vm.bx.heap.set_value_def(apply, live_id!(draw_bg).into(), face.into());
|
|
button.script_apply(vm, &Apply::Animate, &mut Scope::empty(), apply.into());
|
|
});
|
|
}
|
|
|
|
pub fn is_open(&self) -> bool {
|
|
self.open
|
|
}
|
|
|
|
/// Bring the set out. Nothing happens without a set: a floating action
|
|
/// with no actions behind it is just the action.
|
|
pub fn open(&mut self, cx: &mut Cx) {
|
|
self.open_with(cx, false);
|
|
}
|
|
|
|
/// Put the set away. A pinned set stays: there is nothing to dismiss.
|
|
pub fn close(&mut self, cx: &mut Cx) {
|
|
if !self.pinned {
|
|
self.shut(cx, false);
|
|
}
|
|
}
|
|
|
|
pub fn toggle(&mut self, cx: &mut Cx) {
|
|
if self.open {
|
|
self.close(cx);
|
|
} else {
|
|
self.open(cx);
|
|
}
|
|
}
|
|
|
|
pub fn set_anchor(&mut self, cx: &mut Cx, anchor: FloatingAnchor) {
|
|
if self.anchor != anchor {
|
|
self.anchor = anchor;
|
|
self.redraw(cx);
|
|
}
|
|
}
|
|
|
|
pub fn set_dial(&mut self, cx: &mut Cx, dial: SpeedDialLayout) {
|
|
if self.dial != dial {
|
|
self.dial = dial;
|
|
self.hot_item = None;
|
|
self.redraw(cx);
|
|
}
|
|
}
|
|
|
|
pub fn set_item_enabled(&mut self, cx: &mut Cx, id: LiveId, enabled: bool) {
|
|
if let Some((_, item)) = self.items.iter().find(|(item_id, _)| *item_id == id) {
|
|
if let Some(mut inner) = item.borrow_mut::<FloatingActionItem>() {
|
|
inner.set_enabled(cx, enabled);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The main button's rect, window-absolute and after alignment.
|
|
pub fn main_rect(&self) -> Rect {
|
|
self.main_honest
|
|
}
|
|
|
|
fn open_with(&mut self, cx: &mut Cx, from_press: bool) {
|
|
if self.open || self.shown_items().is_empty() {
|
|
return;
|
|
}
|
|
self.set_off(cx, 1.0);
|
|
self.open = true;
|
|
self.hot_item = None;
|
|
self.pressed_item = None;
|
|
self.swallow_up = false;
|
|
if !self.pinned {
|
|
self.opening_press = from_press;
|
|
if self.locked {
|
|
// Still held from a dismissing press whose release has not
|
|
// come: keep it, it is the same grab.
|
|
} else if from_press {
|
|
self.lock_pending = true;
|
|
} else {
|
|
self.lock(cx);
|
|
}
|
|
self.focus_trap.begin(cx, self.main.area());
|
|
self.trap_pending = true;
|
|
}
|
|
// The turn is played by the set; the button's track only has to end
|
|
// where the set is going.
|
|
self.main.as_button().set_open_with(cx, true, Animate::No);
|
|
self.redraw(cx);
|
|
cx.widget_action(self.uid, FloatingActionAction::Opened);
|
|
}
|
|
|
|
/// Put the set away, pinned or not. `keep_lock` holds the grab until the
|
|
/// release of the press that caused it.
|
|
fn shut(&mut self, cx: &mut Cx, keep_lock: bool) {
|
|
if !self.open {
|
|
return;
|
|
}
|
|
self.set_off(cx, 0.0);
|
|
self.open = false;
|
|
self.opening_press = false;
|
|
self.lock_pending = false;
|
|
self.hot_item = None;
|
|
self.pressed_item = None;
|
|
for (_, item) in &self.items {
|
|
if let Some(mut inner) = item.borrow_mut::<FloatingActionItem>() {
|
|
inner.chip_rect = None;
|
|
}
|
|
}
|
|
self.focus_first = false;
|
|
self.trap_pending = false;
|
|
if keep_lock && self.locked {
|
|
self.swallow_up = true;
|
|
} else {
|
|
self.swallow_up = false;
|
|
self.unlock(cx);
|
|
}
|
|
self.focus_trap.end(cx);
|
|
self.main.as_button().set_open_with(cx, false, Animate::No);
|
|
self.redraw(cx);
|
|
cx.widget_action(self.uid, FloatingActionAction::Closed);
|
|
}
|
|
|
|
fn lock(&mut self, cx: &mut Cx) {
|
|
let area = self.main.area();
|
|
cx.sweep_lock(area);
|
|
self.lock_area = area;
|
|
self.locked = true;
|
|
}
|
|
|
|
fn unlock(&mut self, cx: &mut Cx) {
|
|
if self.locked {
|
|
// Both: a redraw moves the lock to the button's newer area, and
|
|
// an order change between a closed and an open draw can give
|
|
// that area another slot than the one it was taken with.
|
|
cx.sweep_unlock(self.lock_area);
|
|
cx.sweep_unlock(self.main.area());
|
|
self.locked = false;
|
|
}
|
|
}
|
|
|
|
fn pick(&mut self, cx: &mut Cx, id: LiveId) {
|
|
cx.widget_action(self.uid, FloatingActionAction::Picked(id));
|
|
if self.close_on_pick {
|
|
self.close(cx);
|
|
}
|
|
}
|
|
|
|
fn item_enabled(&self, i: usize) -> bool {
|
|
self.items
|
|
.get(i)
|
|
.and_then(|(_, item)| item.borrow::<FloatingActionItem>().map(|inner| inner.enabled))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn item_button_area(&self, i: usize) -> Area {
|
|
self.items
|
|
.get(i)
|
|
.and_then(|(_, item)| item.borrow::<FloatingActionItem>().map(|inner| inner.button.area()))
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Every area a press on this set can belong to: the button, and each
|
|
/// action's face. Asked of [`CxFingers::is_mouse_held_outside`], so
|
|
/// that the set's OWN press is never mistaken for somebody else's —
|
|
/// which matters here more than anywhere, because the set's one gesture
|
|
/// is a press on the button, a drag down the dial and a release on an
|
|
/// action, held by the BUTTON from beginning to end.
|
|
fn own_areas(&self) -> Vec<Area> {
|
|
let mut areas = vec![self.main.area(), self.lock_area];
|
|
areas.extend((0..self.items.len()).map(|i| self.item_button_area(i)));
|
|
areas
|
|
}
|
|
|
|
/// Whether the mouse is held by a control this set does not own.
|
|
///
|
|
/// The set reads presses and moves from RAW events — it has to, because
|
|
/// while the actions are travelling their buttons hit-test places they
|
|
/// have not reached yet — and a raw reader is told nothing about who
|
|
/// holds the pointer. So it asks. A slider, a scroll bar, a resizer
|
|
/// mid-drag holds the mouse until its release, and until then a press
|
|
/// or a hover read here would be a second answer to one press.
|
|
///
|
|
/// Only the mouse locks: a touch capture answers false, and the touch
|
|
/// paths below are left as they are.
|
|
fn mouse_held_elsewhere(&self, cx: &Cx) -> bool {
|
|
cx.fingers.is_mouse_held_outside(&self.own_areas())
|
|
}
|
|
|
|
/// The shown action resting under `abs`: where the set laid it out, not
|
|
/// where its travel has it this frame. The layout keeps resting squares
|
|
/// apart; in a window too small for the set, where they can meet, the
|
|
/// nearer middle wins.
|
|
fn item_at(&self, abs: DVec2) -> Option<usize> {
|
|
if !self.open {
|
|
return None;
|
|
}
|
|
let half = self.item_size * 0.5;
|
|
self.shown_items()
|
|
.into_iter()
|
|
.zip(self.plan.offsets.iter())
|
|
.filter_map(|(i, offset)| {
|
|
let d = abs - (self.plan_centre + *offset);
|
|
(d.x.abs() <= half && d.y.abs() <= half).then_some((i, d.length()))
|
|
})
|
|
.fold(None::<(usize, f64)>, |best, c| match best {
|
|
Some(b) if b.1 <= c.1 => Some(b),
|
|
_ => Some(c),
|
|
})
|
|
.map(|c| c.0)
|
|
}
|
|
|
|
/// Whether `abs` is on anything the set drew: the button, an action or
|
|
/// a chip. A press anywhere else is a press outside.
|
|
fn on_dial(&self, cx: &Cx, abs: DVec2) -> bool {
|
|
let main = self.main.area();
|
|
if !main.is_empty() && has_size(main.rect(cx)) && main.rect(cx).contains(abs) {
|
|
return true;
|
|
}
|
|
if self.item_at(abs).is_some() {
|
|
return true;
|
|
}
|
|
self.items.iter().any(|(_, item)| {
|
|
item.borrow::<FloatingActionItem>()
|
|
.and_then(|inner| inner.chip_rect)
|
|
.is_some_and(|rect| rect.contains(abs))
|
|
})
|
|
}
|
|
|
|
fn focused_item(&self, cx: &Cx) -> Option<usize> {
|
|
(0..self.items.len()).find(|i| {
|
|
let area = self.item_button_area(*i);
|
|
!area.is_empty() && cx.has_key_focus(area)
|
|
})
|
|
}
|
|
|
|
fn focus_item(&mut self, cx: &mut Cx, i: usize) {
|
|
let area = self.item_button_area(i);
|
|
if !area.is_empty() {
|
|
cx.set_key_focus(area);
|
|
}
|
|
}
|
|
|
|
/// The shown actions that answer presses, in layout order.
|
|
fn enabled_items(&self) -> Vec<usize> {
|
|
self.shown_items().into_iter().filter(|i| self.item_enabled(*i)).collect()
|
|
}
|
|
|
|
/// Move key focus the way an arrow points, among the enabled actions'
|
|
/// resting places. An arc is walked in order, inner arc first; a row or
|
|
/// a column, folded or not, by direction, where a folded line's
|
|
/// neighbour across is the one the arrow means.
|
|
fn step_focus(&mut self, cx: &mut Cx, focus: Option<usize>, dir: DVec2) {
|
|
let shown = self.shown_items();
|
|
let mut enabled = Vec::new();
|
|
let mut points = Vec::new();
|
|
let mut first_arc = 0;
|
|
for (k, i) in shown.iter().enumerate() {
|
|
let (Some(offset), Some(row)) = (self.plan.offsets.get(k), self.plan.rows.get(k)) else {
|
|
continue;
|
|
};
|
|
if self.item_enabled(*i) {
|
|
enabled.push(*i);
|
|
points.push(*offset);
|
|
if *row == 0 {
|
|
first_arc += 1;
|
|
}
|
|
}
|
|
}
|
|
let from = focus.and_then(|f| enabled.iter().position(|i| *i == f));
|
|
let origin = dvec2(0.0, 0.0);
|
|
let target = if self.dial == SpeedDialLayout::Radial {
|
|
arc_walk_target(&points, first_arc, from, origin, dir)
|
|
} else {
|
|
arrow_target(&points, from, origin, dir)
|
|
};
|
|
if let Some(k) = target {
|
|
self.focus_item(cx, enabled[k]);
|
|
}
|
|
}
|
|
|
|
/// Read where alignment really put the host and, inline, the button,
|
|
/// and redraw when the set was laid out against somewhere else.
|
|
fn refresh_honest(&mut self, cx: &mut Cx) {
|
|
let mut moved = false;
|
|
if self.inline {
|
|
let area = self.main.area();
|
|
if !area.is_empty() {
|
|
let rect = area.rect(cx);
|
|
if has_size(rect) {
|
|
// Closed as well as open: the shadow is laid out around
|
|
// the same centre as the actions.
|
|
let centre = rect.center();
|
|
if (centre.x - self.centre_used.x).abs() > HONEST_SLACK
|
|
|| (centre.y - self.centre_used.y).abs() > HONEST_SLACK
|
|
{
|
|
moved = true;
|
|
}
|
|
self.main_honest = rect;
|
|
}
|
|
}
|
|
} else if self.pin_to == FloatingPin::Container && !self.host_area.is_empty() {
|
|
let rect = self.host_area.rect(cx);
|
|
if has_size(rect) {
|
|
if !near(rect, self.host_used) {
|
|
moved = true;
|
|
}
|
|
self.host_honest = rect;
|
|
let clipped = self.host_area.clipped_rect(cx);
|
|
let finite = clipped.pos.x.is_finite()
|
|
&& clipped.pos.y.is_finite()
|
|
&& clipped.size.x.is_finite()
|
|
&& clipped.size.y.is_finite();
|
|
let visible = (finite && !near(clipped, rect)).then_some(clipped);
|
|
let same = match (visible, self.host_visible) {
|
|
(Some(a), Some(b)) => near(a, b),
|
|
(None, None) => true,
|
|
_ => false,
|
|
};
|
|
if !same {
|
|
moved = true;
|
|
}
|
|
self.host_visible = visible;
|
|
}
|
|
}
|
|
if moved {
|
|
self.redraw(cx);
|
|
}
|
|
}
|
|
|
|
fn release_at(&mut self, cx: &mut Cx, abs: DVec2) {
|
|
if !self.opening_press {
|
|
return;
|
|
}
|
|
self.opening_press = false;
|
|
if self.open && self.lock_pending {
|
|
self.lock_pending = false;
|
|
self.lock(cx);
|
|
}
|
|
if let Some(i) = self.item_at(abs) {
|
|
if self.item_enabled(i) {
|
|
let id = self.items[i].0;
|
|
self.pick(cx, id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A press while the actions are still moving, on the resting place of
|
|
/// one of them. Their buttons are not hearing the pointer, so the set
|
|
/// holds the press itself. Answers whether it did.
|
|
fn press_moving_item(&mut self, abs: DVec2) -> bool {
|
|
match self.item_at(abs) {
|
|
Some(i) => {
|
|
self.pressed_item = Some(i);
|
|
true
|
|
}
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// The release of a press `press_moving_item` held: a pick when it comes
|
|
/// over the same action, as a click on its button would be.
|
|
fn release_moving_item(&mut self, cx: &mut Cx, abs: DVec2) {
|
|
if let Some(i) = self.pressed_item.take() {
|
|
if self.item_at(abs) == Some(i) && self.item_enabled(i) {
|
|
let id = self.items[i].0;
|
|
self.pick(cx, id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A press from a mouse button or the first touch. Answers whether the
|
|
/// press was taken.
|
|
fn press_at(&mut self, cx: &mut Cx, abs: DVec2, on_top: bool) -> bool {
|
|
self.refresh_honest(cx);
|
|
if self.open && !self.pinned && on_top && !self.on_dial(cx, abs) {
|
|
self.shut(cx, true);
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
|
|
let main = self.main.as_button();
|
|
if self.shown_items().is_empty() {
|
|
if main.clicked(actions) {
|
|
cx.widget_action(self.uid, FloatingActionAction::Pressed);
|
|
}
|
|
} else if main.pressed(actions) && !self.pinned {
|
|
// On the press, not the click: the set is out while the hand is
|
|
// still down, so dragging to an action and letting go picks it.
|
|
// The click that follows is the same gesture and is not read.
|
|
if self.open {
|
|
self.close(cx);
|
|
} else {
|
|
self.open_with(cx, true);
|
|
}
|
|
}
|
|
let picked = self.items.iter().find_map(|(id, item)| {
|
|
let button = item.borrow::<FloatingActionItem>().map(|inner| inner.button.clone())?;
|
|
button.as_button().clicked(actions).then_some(*id)
|
|
});
|
|
if let Some(id) = picked {
|
|
self.pick(cx, id);
|
|
}
|
|
}
|
|
|
|
fn key_down(&mut self, cx: &mut Cx, event: &Event, ke: &KeyEvent, on_top: bool) {
|
|
let activate = is_activation_key(ke.key_code);
|
|
let main_area = self.main.area();
|
|
let main_focused = !main_area.is_empty() && cx.has_key_focus(main_area);
|
|
if !self.open {
|
|
if activate && main_focused {
|
|
if self.shown_items().is_empty() {
|
|
cx.widget_action(self.uid, FloatingActionAction::Pressed);
|
|
} else if !self.pinned {
|
|
self.open_with(cx, false);
|
|
self.focus_first = true;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if ke.key_code == KeyCode::Escape {
|
|
if !self.pinned && on_top && claim_escape(cx) {
|
|
self.close(cx);
|
|
cx.set_key_focus(self.main.area());
|
|
}
|
|
return;
|
|
}
|
|
if ke.key_code == KeyCode::Tab && self.focus_trap.is_active() {
|
|
self.tab(cx, ke.modifiers.shift);
|
|
return;
|
|
}
|
|
if self.focus_trap.handle_event(cx, event) {
|
|
return;
|
|
}
|
|
let focus = self.focused_item(cx);
|
|
// The keyboard is somewhere else on the page: a pinned set must not
|
|
// take the arrows from a text field.
|
|
if focus.is_none() && !main_focused {
|
|
return;
|
|
}
|
|
match ke.key_code {
|
|
KeyCode::ArrowUp => self.step_focus(cx, focus, dvec2(0.0, -1.0)),
|
|
KeyCode::ArrowDown => self.step_focus(cx, focus, dvec2(0.0, 1.0)),
|
|
KeyCode::ArrowLeft => self.step_focus(cx, focus, dvec2(-1.0, 0.0)),
|
|
KeyCode::ArrowRight => self.step_focus(cx, focus, dvec2(1.0, 0.0)),
|
|
KeyCode::Home => {
|
|
if let Some(first) = self.enabled_items().first().copied() {
|
|
self.focus_item(cx, first);
|
|
}
|
|
}
|
|
KeyCode::End => {
|
|
if let Some(last) = self.enabled_items().last().copied() {
|
|
self.focus_item(cx, last);
|
|
}
|
|
}
|
|
_ if activate => match focus {
|
|
Some(i) if self.item_enabled(i) => {
|
|
let id = self.items[i].0;
|
|
self.pick(cx, id);
|
|
if !self.open {
|
|
cx.set_key_focus(self.main.area());
|
|
}
|
|
}
|
|
Some(_) => {}
|
|
None => self.close(cx),
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// The trap's Tab, without the disabled actions. The trap walks every
|
|
/// stop under the button's list, and a disabled action's button
|
|
/// registers one like any other, so Tab would rest on an action that
|
|
/// Return cannot pick.
|
|
fn tab(&mut self, cx: &mut Cx, back: bool) {
|
|
let disabled: Vec<Area> = (0..self.items.len())
|
|
.filter(|i| !self.item_enabled(*i) || !self.item_visible(*i))
|
|
.map(|i| self.item_button_area(i))
|
|
.collect();
|
|
let stops: Vec<Area> = self
|
|
.focus_trap
|
|
.stops(cx)
|
|
.into_iter()
|
|
.filter(|stop| !disabled.contains(stop))
|
|
.collect();
|
|
if let Some(next) = tab_target(&stops, cx.key_focus(), back) {
|
|
cx.set_key_focus(next);
|
|
}
|
|
}
|
|
|
|
fn draw_main_shadow(&mut self, cx: &mut Cx2d, face: Rect) {
|
|
let reach = shadow_reach(&self.draw_shadow, cx);
|
|
self.draw_shadow.radius = (face.size.x.min(face.size.y) * 0.5) as f32;
|
|
self.draw_shadow.weight = 1.0;
|
|
self.draw_shadow.draw_abs(cx, grown(face, reach));
|
|
}
|
|
|
|
/// Draw every shown action around `centre`, each where its travel has
|
|
/// got to, and lay out where they rest for the event side.
|
|
fn draw_items(&mut self, cx: &mut Cx2d, scope: &mut Scope, centre: DVec2, window: Rect) {
|
|
let now = cx.seconds_since_app_start();
|
|
let shown = self.shown_items();
|
|
let plan = dial_plan(
|
|
self.anchor,
|
|
self.dial,
|
|
shown.len(),
|
|
self.size,
|
|
self.item_size,
|
|
self.item_gap,
|
|
self.radial_radius,
|
|
self.radial_max_radius,
|
|
centre,
|
|
window,
|
|
);
|
|
let positions = self.positions(now);
|
|
let elapsed = now - self.motion_started;
|
|
let inward = interior(self.anchor);
|
|
let labels = labels_shown_in(self.labels, self.dial, plan.row_count());
|
|
let mut faces: Vec<(DVec2, f64)> = plan.offsets.iter().map(|offset| (centre + *offset, self.item_size)).collect();
|
|
faces.push((centre, self.size));
|
|
for (k, &i) in shown.iter().enumerate() {
|
|
let v = positions[k];
|
|
let offset = plan.offsets[k];
|
|
let focused = {
|
|
let area = self.item_button_area(i);
|
|
!area.is_empty() && cx.has_key_focus(area)
|
|
};
|
|
// Words only once the action has arrived, and not on its way
|
|
// back: a chip beside a disc still travelling hangs in the air.
|
|
let arrived = self.open && self.legs[k].done(elapsed);
|
|
let show_chip = arrived
|
|
&& match labels {
|
|
SpeedDialLabels::Always => true,
|
|
SpeedDialLabels::Hot => self.hot_item == Some(i) || focused,
|
|
SpeedDialLabels::Never | SpeedDialLabels::Auto => false,
|
|
};
|
|
let size = self.item_size * (START_SCALE + (1.0 - START_SCALE) * v.clamp(0.0, 1.0));
|
|
let rest = Rect {
|
|
pos: centre + offset - self.item_size * 0.5,
|
|
size: dvec2(self.item_size, self.item_size),
|
|
};
|
|
let (side, beyond) = chip_place(self.anchor, self.dial, plan.rows[k], &plan, self.item_size, self.item_gap);
|
|
let item = self.items[i].1.clone();
|
|
if let Some(mut inner) = item.borrow_mut::<FloatingActionItem>() {
|
|
inner.place = ItemPlace {
|
|
centre: keep_inside(centre + offset * v, size, bounding(window, rest)),
|
|
size,
|
|
side,
|
|
outward: offset,
|
|
beyond,
|
|
inward,
|
|
window,
|
|
show_chip,
|
|
};
|
|
inner.draw_face(cx, scope);
|
|
};
|
|
}
|
|
for &i in &shown {
|
|
if let Some(mut inner) = self.items[i].1.borrow_mut::<FloatingActionItem>() {
|
|
inner.draw_label_chip(cx, &faces);
|
|
}
|
|
}
|
|
for (i, (_, item)) in self.items.iter().enumerate() {
|
|
if !shown.contains(&i) {
|
|
if let Some(mut inner) = item.borrow_mut::<FloatingActionItem>() {
|
|
inner.chip_rect = None;
|
|
}
|
|
}
|
|
}
|
|
self.plan = plan;
|
|
self.plan_centre = centre;
|
|
if self.moving(now) {
|
|
self.next_frame = cx.new_next_frame();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for FloatingAction {
|
|
fn drop(&mut self) {
|
|
if self.locked {
|
|
orphan_sweep_locks(&[self.lock_area, self.main.area()]);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Widget for FloatingAction {
|
|
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
|
|
// Before anything can take a lock of its own this event. The window
|
|
// does this too; this is for a tree with no window above it.
|
|
release_orphaned_sweep_locks(cx);
|
|
// The clock is read only by the events that need it.
|
|
if self.next_frame.is_event(event).is_some() && (self.open || self.moving(cx.seconds_since_app_start())) {
|
|
self.redraw(cx);
|
|
}
|
|
if self.check_frame.is_event(event).is_some() {
|
|
self.refresh_honest(cx);
|
|
}
|
|
// A set cannot outlive its window's focus, and a release a
|
|
// dismissing press was waiting for is never coming.
|
|
if matches!(event, Event::WindowLostFocus(_) | Event::Pause | Event::Background) {
|
|
if !self.pinned {
|
|
self.shut(cx, false);
|
|
}
|
|
self.swallow_up = false;
|
|
self.pressed_item = None;
|
|
self.unlock(cx);
|
|
}
|
|
if !self.visible {
|
|
return;
|
|
}
|
|
|
|
// Whether this widget's lock is the innermost, read before it is
|
|
// lifted: an overlay opened above it gets Escape and the outside
|
|
// press first.
|
|
let on_top = !self.locked || {
|
|
let top = cx.sweep_lock_area();
|
|
top == Some(self.lock_area) || top == Some(self.main.area())
|
|
};
|
|
|
|
// Lifted only while it is the innermost. Taken again, a lock that
|
|
// was not on top would go back on top, above the overlay that had
|
|
// opened over this set, and that overlay's hits would go dead; and
|
|
// under another lock the children's plain hits are turned away
|
|
// whether this one is lifted or not.
|
|
let held = self.locked && on_top;
|
|
if held {
|
|
cx.sweep_unlock(self.lock_area);
|
|
cx.sweep_unlock(self.main.area());
|
|
}
|
|
// While the actions are still moving, the hand aims at where they are
|
|
// going, not where they happen to be this frame. Their buttons would
|
|
// hit-test the frame's place, so they do not hear the pointer until
|
|
// they arrive, and the set reads presses on the resting places.
|
|
let pointer = matches!(
|
|
event,
|
|
Event::MouseDown(_) | Event::MouseMove(_) | Event::MouseUp(_) | Event::MouseLeave(_) | Event::TouchUpdate(_) | Event::LongPress(_)
|
|
);
|
|
let shielded = pointer && (self.pressed_item.is_some() || self.travelling(cx.seconds_since_app_start()));
|
|
// The set answers Return and Space on its button itself (key_down),
|
|
// so the button does not also activate from them: its press would
|
|
// shut the set the key just opened.
|
|
let keyed = matches!(event, Event::KeyDown(ke) | Event::KeyUp(ke) if is_activation_key(ke.key_code));
|
|
if !keyed {
|
|
self.main.handle_event(cx, event, scope);
|
|
}
|
|
if !shielded {
|
|
for (_, item) in &self.items {
|
|
item.handle_event(cx, event, scope);
|
|
}
|
|
}
|
|
if held {
|
|
let area = self.main.area();
|
|
cx.sweep_lock(area);
|
|
self.lock_area = area;
|
|
}
|
|
|
|
match event {
|
|
Event::Actions(actions) => self.handle_actions(cx, actions),
|
|
Event::MouseDown(me) => {
|
|
// Holding a travelling action is a press-like state taken
|
|
// from a raw press, so it asks first. The dismissal below
|
|
// does not: putting a set away because the person pressed
|
|
// somewhere else is not a gesture competing for the pointer,
|
|
// and a set left out over a page being worked is the bug it
|
|
// was written to fix.
|
|
let taken = (shielded
|
|
&& self.open
|
|
&& !self.mouse_held_elsewhere(cx)
|
|
&& self.press_moving_item(me.abs))
|
|
|| self.press_at(cx, me.abs, on_top);
|
|
if taken && me.handled.get().is_empty() {
|
|
me.handled.set(self.main.area());
|
|
}
|
|
}
|
|
Event::MouseMove(me) => {
|
|
if self.open {
|
|
// An action lit under a pointer somebody else is holding
|
|
// offers a press that cannot arrive: that mouse is going
|
|
// back to the control that took it, whatever it passes
|
|
// over on the way. The set's own press is not elsewhere,
|
|
// so the hand still lights each action it travels over
|
|
// on its way down the dial.
|
|
let hot = if self.mouse_held_elsewhere(cx) {
|
|
None
|
|
} else {
|
|
self.item_at(me.abs)
|
|
};
|
|
if hot != self.hot_item {
|
|
self.hot_item = hot;
|
|
if labels_shown_in(self.labels, self.dial, self.plan.row_count()) == SpeedDialLabels::Hot {
|
|
self.redraw(cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Event::MouseUp(me) => {
|
|
if self.swallow_up {
|
|
self.swallow_up = false;
|
|
if !self.open {
|
|
self.unlock(cx);
|
|
}
|
|
}
|
|
self.release_moving_item(cx, me.abs);
|
|
self.release_at(cx, me.abs);
|
|
}
|
|
// The mouse press itself taken away: its bookkeeping ends as a
|
|
// release's would, but nothing is picked.
|
|
Event::FingerCancel(c) if c.device.is_mouse() && cx.fingers.press_taken_away(c.digit_id) => {
|
|
if self.swallow_up {
|
|
self.swallow_up = false;
|
|
if !self.open {
|
|
self.unlock(cx);
|
|
}
|
|
}
|
|
self.pressed_item = None;
|
|
if std::mem::take(&mut self.opening_press) && self.open && self.lock_pending {
|
|
self.lock_pending = false;
|
|
self.lock(cx);
|
|
}
|
|
}
|
|
// Touch never becomes a mouse press: without this arm a set
|
|
// opens on a phone and a tap outside never puts it away. The
|
|
// first touch only.
|
|
Event::TouchUpdate(te) => {
|
|
if let Some(touch) = te.touches.first() {
|
|
match touch.state {
|
|
TouchState::Start => {
|
|
let taken = (shielded && self.open && self.press_moving_item(touch.abs))
|
|
|| self.press_at(cx, touch.abs, on_top);
|
|
if taken && touch.handled.get().is_empty() {
|
|
touch.handled.set(self.main.area());
|
|
}
|
|
}
|
|
TouchState::Stop => {
|
|
if self.swallow_up {
|
|
self.swallow_up = false;
|
|
if !self.open {
|
|
self.unlock(cx);
|
|
}
|
|
}
|
|
self.release_moving_item(cx, touch.abs);
|
|
self.release_at(cx, touch.abs);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
Event::KeyDown(ke) => self.key_down(cx, event, ke, on_top),
|
|
_ => {}
|
|
}
|
|
if self.open && !self.pinned && on_top && event.back_pressed() {
|
|
self.close(cx);
|
|
}
|
|
}
|
|
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
|
|
// The overlay list is begun on every draw, shown or not: a list that
|
|
// is not begun keeps showing what it showed last. The handle is taken
|
|
// out of `self` so the borrow does not pin every other field.
|
|
let Some(mut draw_list) = self.draw_list.take() else {
|
|
return DrawStep::done();
|
|
};
|
|
let owner = Some(draw_list.id());
|
|
for (_, item) in &self.items {
|
|
if let Some(mut inner) = item.borrow_mut::<FloatingActionItem>() {
|
|
inner.owner_list = owner;
|
|
}
|
|
}
|
|
// Only a parent that draws its children whatever their flag gets
|
|
// here. A view skips a hidden child altogether, and the list then
|
|
// goes on the next draw of the list it was begun in: the window's
|
|
// overlay drops a list its parent was redrawn without.
|
|
if !self.visible {
|
|
draw_list.begin_overlay_reuse(cx);
|
|
cx.begin_root_turtle_for_pass(Layout::default());
|
|
cx.end_pass_sized_turtle();
|
|
draw_list.end(cx);
|
|
self.draw_list = Some(draw_list);
|
|
return DrawStep::done();
|
|
}
|
|
|
|
if self.pinned && !self.open && !self.shown_items().is_empty() {
|
|
// Out from the first draw, already turned: a pinned set was never
|
|
// closed, so there is nothing to watch it open from.
|
|
self.open = true;
|
|
self.land(1.0);
|
|
self.main.as_button().set_open_with(cx, true, Animate::No);
|
|
}
|
|
let now = cx.seconds_since_app_start();
|
|
let dial_drawn = (self.open || self.moving(now)) && !self.shown_items().is_empty();
|
|
|
|
let pass = cx.current_pass_size();
|
|
let insets = cx.display_context.safe_area_insets;
|
|
let window = window_bounds(pass, insets);
|
|
|
|
if self.inline {
|
|
draw_list.begin_overlay_reuse(cx);
|
|
let main_walk = Walk {
|
|
width: Size::Fixed(self.size),
|
|
height: Size::Fixed(self.size),
|
|
..walk
|
|
};
|
|
// Positions read mid-draw are before alignment. The event side's
|
|
// rect is the true one, and it holds while this draw sees the
|
|
// button where the last one did.
|
|
let seen = cx.peek_walk_turtle(main_walk);
|
|
let honest = has_size(self.main_honest) && near(seen, self.main_seen);
|
|
let centre = if honest { self.main_honest.center() } else { seen.center() };
|
|
self.main_seen = seen;
|
|
self.centre_used = centre;
|
|
// The shadow and the actions float, on a root turtle: in the
|
|
// parent's flow the shadow would be cut to the slot it stands in,
|
|
// which is the button's own square, and its soft edge would end
|
|
// in a hard one. Both go before the button, so they sit under it.
|
|
cx.begin_root_turtle_for_pass(Layout::default());
|
|
let face = Rect {
|
|
pos: centre - self.size * 0.5,
|
|
size: dvec2(self.size, self.size),
|
|
};
|
|
self.draw_main_shadow(cx, face);
|
|
if dial_drawn {
|
|
self.draw_items(cx, scope, centre, window);
|
|
}
|
|
cx.end_pass_sized_turtle();
|
|
// The button in its parent's flow, so the parent's alignment
|
|
// moves it with everything else.
|
|
self.write_turn(cx, now);
|
|
let _ = self.main.draw_walk(cx, scope, main_walk);
|
|
draw_list.end(cx);
|
|
} else {
|
|
let whole = Rect {
|
|
pos: dvec2(0.0, 0.0),
|
|
size: pass,
|
|
};
|
|
// Whether the container is where the event side last read it,
|
|
// and so whether that read's cut still holds.
|
|
let mut read_holds = true;
|
|
let mut ancestor_clip = (dvec2(f64::NEG_INFINITY, f64::NEG_INFINITY), dvec2(f64::INFINITY, f64::INFINITY));
|
|
let host = match self.pin_to {
|
|
FloatingPin::Window => whole,
|
|
FloatingPin::Container => {
|
|
let seen = cx.turtle().rect_unscrolled();
|
|
// A container still measuring itself has no extent yet;
|
|
// the window is the only rect there is to pin to.
|
|
let seen = if seen.size.x.is_finite() && seen.size.y.is_finite() && has_size(seen) {
|
|
seen
|
|
} else {
|
|
whole
|
|
};
|
|
cx.add_aligned_rect_area(&mut self.host_area, seen);
|
|
ancestor_clip = cx.turtle_ancestor_clip();
|
|
let honest = has_size(self.host_honest) && near(seen, self.host_seen);
|
|
self.host_seen = seen;
|
|
read_holds = honest;
|
|
if honest {
|
|
self.host_honest
|
|
} else {
|
|
seen
|
|
}
|
|
}
|
|
};
|
|
self.host_used = host;
|
|
|
|
draw_list.begin_overlay_reuse(cx);
|
|
cx.begin_root_turtle_for_pass(Layout::default());
|
|
let bounds = pin_bounds(host, pass, insets);
|
|
let face = main_rect(self.anchor, bounds, self.size, self.edge_margin);
|
|
// Read on the event side, against the host where it was then. The
|
|
// edges that hid it stay where they were, since a scroll moves
|
|
// the host under them, and cut the host where it is now.
|
|
//
|
|
// Unless the container has moved since that read: a scroll step
|
|
// that slides it under the view's edge is drawn before any event
|
|
// reads it again, and the read then says it was whole. So on that
|
|
// draw the cut comes from the turtles around the container, as
|
|
// this draw has them, laid over the container where it is now.
|
|
let cut = match (self.pin_to, self.host_visible) {
|
|
(FloatingPin::Container, _) if !read_holds => {
|
|
let visible = host.clip(ancestor_clip);
|
|
(!near(visible, host)).then(|| (visible, cut_sides(visible, host, whole)))
|
|
}
|
|
(FloatingPin::Container, Some(visible)) => Some(cut_now(visible, self.host_honest, host, whole)),
|
|
_ => None,
|
|
};
|
|
// The set is laid out inside what the cut leaves, so a container
|
|
// half scrolled away folds its set into the part still on screen.
|
|
let room = cut.map_or(window, |(_, open)| intersect(window, open));
|
|
let clip = cut.map(|(visible, open)| {
|
|
// With its button still in view, only the edges that hide the
|
|
// container hide the set. With the button going out of view the
|
|
// whole set goes with the container, rather than its actions
|
|
// floating on over whatever the page scrolled under.
|
|
if contains_rect(open, face) && contains_rect(visible, face) {
|
|
open
|
|
} else {
|
|
visible
|
|
}
|
|
});
|
|
if let Some(clip) = clip {
|
|
cx.begin_turtle(Walk::fixed(clip.size.x, clip.size.y).with_abs_pos(clip.pos), Layout::default());
|
|
}
|
|
self.draw_main_shadow(cx, face);
|
|
// The actions before the button, so they come out from under it.
|
|
if dial_drawn {
|
|
self.draw_items(cx, scope, face.center(), room);
|
|
}
|
|
self.write_turn(cx, now);
|
|
let _ = self
|
|
.main
|
|
.draw_walk(cx, scope, Walk::fixed(self.size, self.size).with_abs_pos(face.pos));
|
|
if clip.is_some() {
|
|
cx.end_turtle();
|
|
}
|
|
cx.end_pass_sized_turtle();
|
|
draw_list.end(cx);
|
|
// Absolute in a root turtle, which no alignment moves.
|
|
self.main_honest = face;
|
|
self.centre_used = face.center();
|
|
}
|
|
self.draw_list = Some(draw_list);
|
|
|
|
if self.open && self.trap_pending {
|
|
// On a first open the trap was begun with an area from before the
|
|
// actions were drawn, and the stops live in the draw just made.
|
|
self.trap_pending = false;
|
|
self.focus_trap.retarget(self.main.area());
|
|
}
|
|
if self.open && self.focus_first {
|
|
self.focus_first = false;
|
|
if let Some(first) = self.enabled_items().first().copied() {
|
|
self.focus_item(cx, first);
|
|
}
|
|
}
|
|
if self.inline || self.pin_to == FloatingPin::Container {
|
|
self.check_frame = cx.new_next_frame();
|
|
}
|
|
DrawStep::done()
|
|
}
|
|
|
|
fn snapshot_value(&self, _cx: &Cx) -> Option<String> {
|
|
Some(snapshot_text(self.open, self.pinned, self.dial, self.anchor))
|
|
}
|
|
}
|
|
|
|
impl FloatingActionRef {
|
|
pub fn open(&self, cx: &mut Cx) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.open(cx);
|
|
}
|
|
}
|
|
|
|
pub fn close(&self, cx: &mut Cx) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.close(cx);
|
|
}
|
|
}
|
|
|
|
pub fn toggle(&self, cx: &mut Cx) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.toggle(cx);
|
|
}
|
|
}
|
|
|
|
pub fn is_open(&self) -> bool {
|
|
self.borrow().is_some_and(|inner| inner.is_open())
|
|
}
|
|
|
|
pub fn set_anchor(&self, cx: &mut Cx, anchor: FloatingAnchor) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.set_anchor(cx, anchor);
|
|
}
|
|
}
|
|
|
|
pub fn set_dial(&self, cx: &mut Cx, dial: SpeedDialLayout) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.set_dial(cx, dial);
|
|
}
|
|
}
|
|
|
|
pub fn set_item_enabled(&self, cx: &mut Cx, id: LiveId, enabled: bool) {
|
|
if let Some(mut inner) = self.borrow_mut() {
|
|
inner.set_item_enabled(cx, id, enabled);
|
|
}
|
|
}
|
|
|
|
pub fn main_rect(&self) -> Rect {
|
|
self.borrow().map(|inner| inner.main_rect()).unwrap_or_default()
|
|
}
|
|
|
|
fn has(&self, actions: &Actions, wanted: FloatingActionAction) -> bool {
|
|
let uid = self.widget_uid();
|
|
actions.iter().any(|action| {
|
|
action
|
|
.as_widget_action()
|
|
.is_some_and(|wa| wa.widget_uid == uid && wa.cast::<FloatingActionAction>() == wanted)
|
|
})
|
|
}
|
|
|
|
/// The button was clicked with no set behind it: the action itself.
|
|
pub fn pressed(&self, actions: &Actions) -> bool {
|
|
self.has(actions, FloatingActionAction::Pressed)
|
|
}
|
|
|
|
/// The action picked this pass, by its child id.
|
|
pub fn picked(&self, actions: &Actions) -> Option<LiveId> {
|
|
let uid = self.widget_uid();
|
|
actions
|
|
.iter()
|
|
.filter_map(|action| action.as_widget_action())
|
|
.filter(|wa| wa.widget_uid == uid)
|
|
.find_map(|wa| match wa.cast::<FloatingActionAction>() {
|
|
FloatingActionAction::Picked(id) => Some(id),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
pub fn opened(&self, actions: &Actions) -> bool {
|
|
self.has(actions, FloatingActionAction::Opened)
|
|
}
|
|
|
|
pub fn closed(&self, actions: &Actions) -> bool {
|
|
self.has(actions, FloatingActionAction::Closed)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
fn test_cx() -> crate::PooledCx {
|
|
crate::checkout_test_cx()
|
|
}
|
|
use super::*;
|
|
|
|
const BOUNDS: Rect = Rect {
|
|
pos: DVec2 { x: 0.0, y: 0.0 },
|
|
size: DVec2 { x: 800.0, y: 600.0 },
|
|
};
|
|
const MARGIN: Inset = Inset {
|
|
left: 16.0,
|
|
top: 16.0,
|
|
right: 16.0,
|
|
bottom: 16.0,
|
|
};
|
|
const MAIN: f64 = 56.0;
|
|
const ITEM: f64 = 40.0;
|
|
const GAP: f64 = 12.0;
|
|
const MIN_R: f64 = 88.0;
|
|
/// The widget's own furthest radius.
|
|
const MAX_R: f64 = 200.0;
|
|
/// A furthest radius of 0: the arcs are bounded by the window alone.
|
|
const NO_MAX: f64 = 0.0;
|
|
|
|
const LAYOUTS: [SpeedDialLayout; 3] = [
|
|
SpeedDialLayout::Radial,
|
|
SpeedDialLayout::Horizontal,
|
|
SpeedDialLayout::Vertical,
|
|
];
|
|
|
|
fn close_to(a: DVec2, b: DVec2, tolerance: f64) -> bool {
|
|
(a.x - b.x).abs() <= tolerance && (a.y - b.y).abs() <= tolerance
|
|
}
|
|
|
|
fn no_insets() -> SafeAreaInsets {
|
|
SafeAreaInsets {
|
|
top: 0.0,
|
|
right: 0.0,
|
|
bottom: 0.0,
|
|
left: 0.0,
|
|
}
|
|
}
|
|
|
|
/// The button is `edge_margin` in from the edges its anchor touches and
|
|
/// centred on an axis it does not touch. A wrong sign here pins the
|
|
/// button off screen on exactly one of the eight, which a single anchor
|
|
/// in a catalogue would never show.
|
|
#[test]
|
|
fn the_main_button_lands_at_each_anchor() {
|
|
crate::on_test_cx(|| {
|
|
for (anchor, centre) in [
|
|
(FloatingAnchor::TopLeft, dvec2(44.0, 44.0)),
|
|
(FloatingAnchor::TopCenter, dvec2(400.0, 44.0)),
|
|
(FloatingAnchor::TopRight, dvec2(756.0, 44.0)),
|
|
(FloatingAnchor::CenterLeft, dvec2(44.0, 300.0)),
|
|
(FloatingAnchor::CenterRight, dvec2(756.0, 300.0)),
|
|
(FloatingAnchor::BottomLeft, dvec2(44.0, 556.0)),
|
|
(FloatingAnchor::BottomCenter, dvec2(400.0, 556.0)),
|
|
(FloatingAnchor::BottomRight, dvec2(756.0, 556.0)),
|
|
] {
|
|
let rect = main_rect(anchor, BOUNDS, MAIN, MARGIN);
|
|
assert_eq!(rect.center(), centre, "{anchor:?}");
|
|
assert_eq!(rect.size, dvec2(MAIN, MAIN), "{anchor:?}");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A container smaller than the button cannot hold it, and the button
|
|
/// overruns to the far side rather than hanging off the near one, where
|
|
/// its top would be under whatever sits above the container.
|
|
#[test]
|
|
fn a_container_smaller_than_the_button_keeps_its_low_edge() {
|
|
crate::on_test_cx(|| {
|
|
let tiny = Rect {
|
|
pos: dvec2(10.0, 10.0),
|
|
size: dvec2(40.0, 40.0),
|
|
};
|
|
let rect = main_rect(FloatingAnchor::BottomRight, tiny, MAIN, MARGIN);
|
|
assert_eq!(rect.pos, dvec2(10.0, 10.0));
|
|
});
|
|
}
|
|
|
|
/// A notch or a home indicator takes its insets out of the rect the
|
|
/// button pins to, so a bottom corner button sits above the indicator
|
|
/// rather than under it.
|
|
#[test]
|
|
fn pin_bounds_leave_the_safe_area() {
|
|
crate::on_test_cx(|| {
|
|
let insets = SafeAreaInsets {
|
|
top: 40.0,
|
|
bottom: 30.0,
|
|
..no_insets()
|
|
};
|
|
let bounds = pin_bounds(BOUNDS, BOUNDS.size, insets);
|
|
assert_eq!(bounds, Rect {
|
|
pos: dvec2(0.0, 40.0),
|
|
size: dvec2(800.0, 530.0),
|
|
});
|
|
let rect = main_rect(FloatingAnchor::BottomRight, bounds, MAIN, MARGIN);
|
|
assert_eq!(rect.center().y, 40.0 + 530.0 - 16.0 - 28.0);
|
|
// A host entirely inside the safe area is untouched.
|
|
let host = Rect {
|
|
pos: dvec2(100.0, 100.0),
|
|
size: dvec2(200.0, 200.0),
|
|
};
|
|
assert_eq!(pin_bounds(host, BOUNDS.size, insets), host);
|
|
});
|
|
}
|
|
|
|
/// Every arc starts along one edge its anchor touches and ends along
|
|
/// the other, running through the interior; the offsets it produces are
|
|
/// the table's.
|
|
#[test]
|
|
fn the_arc_table_opens_inward() {
|
|
crate::on_test_cx(|| {
|
|
for (anchor, start, dir, span) in [
|
|
(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),
|
|
] {
|
|
let (s, d, w) = radial_arc(anchor);
|
|
assert!((s - f64::to_radians(start)).abs() < 1e-12, "{anchor:?} start");
|
|
assert_eq!(d, dir, "{anchor:?} direction");
|
|
assert!((w - f64::to_radians(span)).abs() < 1e-12, "{anchor:?} span");
|
|
}
|
|
let offsets = dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 3, MAIN, ITEM, GAP, MIN_R, NO_MAX);
|
|
for (got, want) in offsets.iter().zip([
|
|
dvec2(0.0, -88.0),
|
|
dvec2(-62.23, -62.23),
|
|
dvec2(-88.0, 0.0),
|
|
]) {
|
|
assert!(close_to(*got, want, 0.01), "{got:?} is not {want:?}");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// The arc grows before two actions' discs would touch, rather than
|
|
/// the discs shrinking, and never closes in on the main button.
|
|
#[test]
|
|
fn the_fan_grows_before_neighbours_touch() {
|
|
crate::on_test_cx(|| {
|
|
let quarter = f64::to_radians(90.0);
|
|
let half = f64::to_radians(180.0);
|
|
assert!((radial_radius(4, quarter, ITEM, GAP, MAIN, MIN_R) - 100.46).abs() < 0.01);
|
|
assert_eq!(radial_radius(3, quarter, ITEM, GAP, MAIN, MIN_R), 88.0);
|
|
for span in [quarter, half] {
|
|
for n in 2..=8usize {
|
|
let r = radial_radius(n, span, ITEM, GAP, MAIN, MIN_R);
|
|
let step = span / (n - 1) as f64;
|
|
let chord = 2.0 * r * (step * 0.5).sin();
|
|
assert!(chord >= ITEM + GAP - 1e-9, "n {n} span {span}: chord {chord}");
|
|
assert!(r >= MAIN * 0.5 + GAP + ITEM * 0.5, "n {n} span {span}: radius {r}");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Past the chord rule, an arc grows until no two squares on it meet and
|
|
/// none meets the button's: a press is aimed at a square, and four on a
|
|
/// quarter arc at the chord rule's radius share corners on the diagonal.
|
|
#[test]
|
|
fn the_arc_grows_before_neighbouring_squares_meet() {
|
|
crate::on_test_cx(|| {
|
|
let quarter = f64::to_radians(90.0);
|
|
let half = f64::to_radians(180.0);
|
|
assert!(arc_radius(4, quarter, ITEM, GAP, MAIN, MIN_R) > radial_radius(4, quarter, ITEM, GAP, MAIN, MIN_R));
|
|
assert_eq!(arc_radius(3, quarter, ITEM, GAP, MAIN, MIN_R), 88.0, "the spec's three are untouched");
|
|
for (item, gap, main, min_r) in [(ITEM, GAP, MAIN, MIN_R), (32.0, 8.0, 40.0, 64.0), (64.0, 0.0, 96.0, 48.0)] {
|
|
for (anchor, span) in [(FloatingAnchor::BottomRight, quarter), (FloatingAnchor::TopCenter, half)] {
|
|
for n in 1..=12usize {
|
|
let r = arc_radius(n, span, item, gap, main, min_r);
|
|
let offsets = dial_offsets(anchor, SpeedDialLayout::Radial, n, main, item, gap, min_r, NO_MAX);
|
|
for (i, a) in offsets.iter().enumerate() {
|
|
assert!((a.length() - r).abs() < 1e-9, "n {n}: one arc");
|
|
assert!(a.x.abs().max(a.y.abs()) >= (main + item) * 0.5 - 1e-9, "n {n}: {i} meets the button");
|
|
for b in &offsets[i + 1..] {
|
|
let d = *b - *a;
|
|
assert!(d.x.abs().max(d.y.abs()) >= item - 1e-9, "item {item} n {n}: squares meet at {a:?} and {b:?}");
|
|
assert!(d.length() >= item + gap - 1e-9, "item {item} n {n}: faces too close");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(arc_step(ITEM, GAP) >= ITEM * std::f64::consts::SQRT_2);
|
|
assert_eq!(arc_step(ITEM, 30.0), ITEM + 30.0);
|
|
});
|
|
}
|
|
|
|
/// No action ever goes outward, past the edge the button is pinned to.
|
|
/// A row or column along its own axis, and a single action on a corner
|
|
/// arc, go strictly inward on both axes.
|
|
#[test]
|
|
fn every_offset_points_inward() {
|
|
crate::on_test_cx(|| {
|
|
for anchor in FloatingAnchor::ALL {
|
|
let inward = interior(anchor);
|
|
for dial in LAYOUTS {
|
|
for n in 1..=6usize {
|
|
for offset in dial_offsets(anchor, dial, n, MAIN, ITEM, GAP, MIN_R, NO_MAX) {
|
|
let along = offset.x * inward.x + offset.y * inward.y;
|
|
assert!(along >= -1e-9, "{anchor:?} {dial:?} n {n}: {offset:?}");
|
|
let strict = anchor.is_corner()
|
|
&& (dial != SpeedDialLayout::Radial || n == 1);
|
|
if strict {
|
|
assert!(along > 0.0, "{anchor:?} {dial:?} n {n}: {offset:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// One step is an action's diameter and the gap. A row with no inward
|
|
/// run along its own axis is centred on the button, one step in.
|
|
#[test]
|
|
fn rows_and_columns_step_by_size_and_gap() {
|
|
crate::on_test_cx(|| {
|
|
let run = |anchor, dial| dial_offsets(anchor, dial, 3, MAIN, ITEM, GAP, MIN_R, NO_MAX);
|
|
assert_eq!(
|
|
run(FloatingAnchor::BottomRight, SpeedDialLayout::Horizontal),
|
|
vec![dvec2(-60.0, 0.0), dvec2(-112.0, 0.0), dvec2(-164.0, 0.0)]
|
|
);
|
|
assert_eq!(
|
|
run(FloatingAnchor::BottomRight, SpeedDialLayout::Vertical),
|
|
vec![dvec2(0.0, -60.0), dvec2(0.0, -112.0), dvec2(0.0, -164.0)]
|
|
);
|
|
assert_eq!(
|
|
run(FloatingAnchor::BottomCenter, SpeedDialLayout::Horizontal),
|
|
vec![dvec2(-52.0, -60.0), dvec2(0.0, -60.0), dvec2(52.0, -60.0)]
|
|
);
|
|
assert_eq!(
|
|
run(FloatingAnchor::CenterRight, SpeedDialLayout::Vertical),
|
|
vec![dvec2(-60.0, -52.0), dvec2(-60.0, 0.0), dvec2(-60.0, 52.0)]
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Chips go on the side that faces the interior, for all twenty-four
|
|
/// anchor and layout pairs.
|
|
#[test]
|
|
fn chips_go_on_the_inside() {
|
|
crate::on_test_cx(|| {
|
|
use ChipSide::*;
|
|
use FloatingAnchor::*;
|
|
for (anchor, vertical, horizontal) in [
|
|
(TopLeft, Right, Below),
|
|
(TopCenter, Left, Below),
|
|
(TopRight, Left, Below),
|
|
(CenterLeft, Right, Above),
|
|
(CenterRight, Left, Above),
|
|
(BottomLeft, Right, Above),
|
|
(BottomCenter, Left, Above),
|
|
(BottomRight, Left, Above),
|
|
] {
|
|
assert_eq!(chip_side(anchor, SpeedDialLayout::Vertical), vertical, "{anchor:?} column");
|
|
assert_eq!(chip_side(anchor, SpeedDialLayout::Horizontal), horizontal, "{anchor:?} row");
|
|
assert_eq!(chip_side(anchor, SpeedDialLayout::Radial), Outward, "{anchor:?} arc");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A chip pulled toward the window's edge stays inside it, whichever
|
|
/// side it asked for, and an outward chip clears its disc by the gap.
|
|
#[test]
|
|
fn a_chip_stays_in_the_window() {
|
|
crate::on_test_cx(|| {
|
|
let window = Rect {
|
|
pos: dvec2(6.0, 6.0),
|
|
size: dvec2(300.0, 200.0),
|
|
};
|
|
let size = dvec2(80.0, 20.0);
|
|
let left = chip_rect(dvec2(30.0, 100.0), ITEM, 8.0, size, ChipSide::Left, dvec2(0.0, -60.0), 0.0, window);
|
|
assert_eq!(left.pos.x, 6.0, "pulled in from the left edge");
|
|
let right = chip_rect(dvec2(100.0, 100.0), ITEM, 8.0, size, ChipSide::Right, dvec2(0.0, -60.0), 0.0, window);
|
|
assert_eq!(right.pos, dvec2(128.0, 90.0), "beside the disc, centred on it");
|
|
let up = chip_rect(dvec2(150.0, 100.0), ITEM, 8.0, size, ChipSide::Outward, dvec2(0.0, -60.0), 0.0, window);
|
|
assert_eq!(up.pos, dvec2(110.0, 52.0), "straight out, its near edge a gap clear");
|
|
// An action on an inner arc puts its chip out past the outer arc.
|
|
let past = chip_rect(dvec2(150.0, 100.0), ITEM, 8.0, size, ChipSide::Outward, dvec2(0.0, -60.0), 30.0, window);
|
|
assert_eq!(past.pos, dvec2(110.0, 22.0), "carried out past the arc beyond it");
|
|
});
|
|
}
|
|
|
|
/// Pulled back inside the window, a chip that would land on its own
|
|
/// action's face goes beside the face instead, on the side facing the
|
|
/// interior when that covers nothing and on the other side when it would.
|
|
#[test]
|
|
fn a_chip_pulled_over_its_own_action_goes_beside_it() {
|
|
crate::on_test_cx(|| {
|
|
let window = Rect {
|
|
pos: dvec2(6.0, 6.0),
|
|
size: dvec2(400.0, 300.0),
|
|
};
|
|
let size = dvec2(60.0, 20.0);
|
|
let centre = dvec2(200.0, 30.0);
|
|
let up = dvec2(0.0, -150.0);
|
|
let inward = interior(FloatingAnchor::BottomRight);
|
|
let pulled = chip_rect(centre, ITEM, 8.0, size, ChipSide::Outward, up, 0.0, window);
|
|
assert!(covers_disc(pulled, centre, ITEM * 0.5), "the edge pulls it over its face");
|
|
|
|
let placed = place_chip(centre, ITEM, 8.0, size, ChipSide::Outward, up, 0.0, inward, window, &[(centre, ITEM)]);
|
|
assert!(!covers_disc(placed, centre, ITEM * 0.5), "{placed:?}");
|
|
assert!(inside(placed, window));
|
|
assert_eq!(placed.pos.x + placed.size.x, centre.x - ITEM * 0.5 - 8.0, "beside it, on the side facing in");
|
|
|
|
let neighbour = dvec2(140.0, 30.0);
|
|
let placed = place_chip(centre, ITEM, 8.0, size, ChipSide::Outward, up, 0.0, inward, window, &[(centre, ITEM), (neighbour, ITEM)]);
|
|
assert_eq!(placed.pos.x, centre.x + ITEM * 0.5 + 8.0, "the other side, clear of the neighbour");
|
|
|
|
let low = dvec2(200.0, 200.0);
|
|
assert_eq!(
|
|
place_chip(low, ITEM, 8.0, size, ChipSide::Outward, up, 0.0, inward, window, &[(low, ITEM)]),
|
|
chip_rect(low, ITEM, 8.0, size, ChipSide::Outward, up, 0.0, window),
|
|
"with room, where it was asked for"
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Once a row or a column has folded, a chip is carried out past the
|
|
/// lines beside its own, or for lines centred on the button out on the
|
|
/// nearer side of the set, so it covers no other action and not the
|
|
/// button, from every anchor.
|
|
#[test]
|
|
fn a_chip_on_folded_lines_covers_no_other_action() {
|
|
crate::on_test_cx(|| {
|
|
let size = dvec2(90.0, 22.0);
|
|
for anchor in FloatingAnchor::ALL {
|
|
for (dial, w, h) in [(SpeedDialLayout::Horizontal, 300.0, 900.0), (SpeedDialLayout::Vertical, 900.0, 300.0)] {
|
|
let (host, window) = small_window(w, h);
|
|
let (centre, plan) = plan_in(anchor, dial, 12, host, window);
|
|
let what = format!("{anchor:?} {dial:?}");
|
|
assert!(plan.row_count() >= 2, "{what}: folded {:?}", plan.rows);
|
|
let mut faces: Vec<(DVec2, f64)> = plan.offsets.iter().map(|offset| (centre + *offset, ITEM)).collect();
|
|
faces.push((centre, MAIN));
|
|
for (k, offset) in plan.offsets.iter().enumerate() {
|
|
let (side, beyond) = chip_place(anchor, dial, plan.rows[k], &plan, ITEM, GAP);
|
|
let rect = place_chip(centre + *offset, ITEM, 8.0, size, side, *offset, beyond, interior(anchor), window, &faces);
|
|
assert!(inside(rect, window), "{what}: chip {k} at {rect:?}");
|
|
for (j, (face, d)) in faces.iter().enumerate() {
|
|
assert!(!covers_disc(rect, *face, d * 0.5), "{what}: chip {k} ({side:?} {beyond}) covers face {j}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// One line: beside it, on the inside, carried nowhere.
|
|
let (host, window) = small_window(1400.0, 900.0);
|
|
let (_, plan) = plan_in(FloatingAnchor::BottomRight, SpeedDialLayout::Vertical, 3, host, window);
|
|
assert_eq!(chip_place(FloatingAnchor::BottomRight, SpeedDialLayout::Vertical, 0, &plan, ITEM, GAP), (ChipSide::Left, 0.0));
|
|
});
|
|
}
|
|
|
|
/// A column's chips never meet, so Auto shows them all; an arc's or a
|
|
/// row's would, so Auto shows the one being aimed at.
|
|
#[test]
|
|
fn auto_labels_resolve_by_layout() {
|
|
crate::on_test_cx(|| {
|
|
assert_eq!(labels_shown(SpeedDialLabels::Auto, SpeedDialLayout::Vertical), SpeedDialLabels::Always);
|
|
assert_eq!(labels_shown(SpeedDialLabels::Auto, SpeedDialLayout::Radial), SpeedDialLabels::Hot);
|
|
assert_eq!(labels_shown(SpeedDialLabels::Auto, SpeedDialLayout::Horizontal), SpeedDialLabels::Hot);
|
|
for labels in [SpeedDialLabels::Always, SpeedDialLabels::Hot, SpeedDialLabels::Never] {
|
|
for dial in LAYOUTS {
|
|
assert_eq!(labels_shown(labels, dial), labels);
|
|
}
|
|
}
|
|
// Folded into lines, a chip beside one line lies over the next, so
|
|
// only the one aimed at shows; an arc's chips go out past the arcs.
|
|
use SpeedDialLabels::*;
|
|
assert_eq!(labels_shown_in(Always, SpeedDialLayout::Vertical, 2), Hot);
|
|
assert_eq!(labels_shown_in(Auto, SpeedDialLayout::Vertical, 2), Hot);
|
|
assert_eq!(labels_shown_in(Always, SpeedDialLayout::Horizontal, 3), Hot);
|
|
assert_eq!(labels_shown_in(Auto, SpeedDialLayout::Vertical, 1), Always);
|
|
assert_eq!(labels_shown_in(Always, SpeedDialLayout::Radial, 2), Always);
|
|
assert_eq!(labels_shown_in(Never, SpeedDialLayout::Horizontal, 3), Never);
|
|
});
|
|
}
|
|
|
|
/// Arrows go where the actions went. Along a column the arrow the
|
|
/// column runs in steps outward and the other steps back, a sideways
|
|
/// arrow has nowhere to go, and at the end of the column there is
|
|
/// nothing further. On an arc two actions can be equally near, and the
|
|
/// better aligned one is the one the arrow means.
|
|
#[test]
|
|
fn arrows_follow_the_direction_the_actions_went() {
|
|
crate::on_test_cx(|| {
|
|
let up = dvec2(0.0, -1.0);
|
|
let down = dvec2(0.0, 1.0);
|
|
let left = dvec2(-1.0, 0.0);
|
|
let origin = dvec2(0.0, 0.0);
|
|
let column = [dvec2(0.0, -60.0), dvec2(0.0, -112.0), dvec2(0.0, -164.0)];
|
|
assert_eq!(arrow_target(&column, None, origin, up), Some(0));
|
|
assert_eq!(arrow_target(&column, Some(0), origin, up), Some(1));
|
|
assert_eq!(arrow_target(&column, Some(2), origin, up), None);
|
|
assert_eq!(arrow_target(&column, Some(1), origin, down), Some(0));
|
|
assert_eq!(arrow_target(&column, Some(1), origin, left), None);
|
|
|
|
let arc = dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 3, MAIN, ITEM, GAP, MIN_R, NO_MAX);
|
|
assert_eq!(arrow_target(&arc, None, origin, left), Some(2));
|
|
assert_eq!(arrow_target(&arc, None, origin, up), Some(0));
|
|
});
|
|
}
|
|
|
|
/// Each action sets off a stagger after the one before and eases out
|
|
/// into place; with motion reduced every one is already there.
|
|
#[test]
|
|
fn items_arrive_in_turn() {
|
|
crate::on_test_cx(|| {
|
|
let (enter, stagger) = (0.2, 0.03);
|
|
let ease = Ease::OutCubic;
|
|
assert_eq!(enter_progress(0, 0.0, enter, stagger, &ease, false), 0.0);
|
|
assert_eq!(enter_progress(2, 0.06, enter, stagger, &ease, false), 0.0);
|
|
assert_eq!(enter_progress(2, 0.26, enter, stagger, &ease, false), 1.0);
|
|
assert!((enter_progress(1, 0.13, enter, stagger, &ease, false) - 0.875).abs() < 1e-9);
|
|
for (i, elapsed) in [(0usize, 0.0), (3, 0.01), (7, -1.0), (1, 0.13)] {
|
|
assert_eq!(enter_progress(i, elapsed, enter, stagger, &ease, true), 1.0);
|
|
}
|
|
});
|
|
}
|
|
|
|
const COUNTS: [usize; 6] = [1, 2, 3, 5, 8, 12];
|
|
|
|
/// A host of `w` by `h` filling the window, and the rect the actions
|
|
/// must stay inside: the window a little in from its edges.
|
|
fn small_window(w: f64, h: f64) -> (Rect, Rect) {
|
|
let host = Rect {
|
|
pos: dvec2(0.0, 0.0),
|
|
size: dvec2(w, h),
|
|
};
|
|
(host, window_bounds(host.size, no_insets()))
|
|
}
|
|
|
|
/// The sizes a set is laid out with.
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct Sizes {
|
|
main: f64,
|
|
item: f64,
|
|
gap: f64,
|
|
min_r: f64,
|
|
max_r: f64,
|
|
margin: Inset,
|
|
}
|
|
|
|
const DEFAULT_SIZES: Sizes = Sizes {
|
|
main: MAIN,
|
|
item: ITEM,
|
|
gap: GAP,
|
|
min_r: MIN_R,
|
|
max_r: NO_MAX,
|
|
margin: MARGIN,
|
|
};
|
|
|
|
/// The set of `n` laid out from `anchor` in `host`, and its button's
|
|
/// centre.
|
|
fn plan_in(anchor: FloatingAnchor, dial: SpeedDialLayout, n: usize, host: Rect, window: Rect) -> (DVec2, DialPlan) {
|
|
plan_sized(DEFAULT_SIZES, anchor, dial, n, host, window)
|
|
}
|
|
|
|
fn plan_sized(s: Sizes, anchor: FloatingAnchor, dial: SpeedDialLayout, n: usize, host: Rect, window: Rect) -> (DVec2, DialPlan) {
|
|
let centre = main_rect(anchor, host, s.main, s.margin).center();
|
|
(centre, dial_plan(anchor, dial, n, s.main, s.item, s.gap, s.min_r, s.max_r, centre, window))
|
|
}
|
|
|
|
fn resting_rect(centre: DVec2, offset: DVec2) -> Rect {
|
|
square(centre + offset, ITEM)
|
|
}
|
|
|
|
fn square(middle: DVec2, side: f64) -> Rect {
|
|
Rect {
|
|
pos: middle - side * 0.5,
|
|
size: dvec2(side, side),
|
|
}
|
|
}
|
|
|
|
fn inside(rect: Rect, window: Rect) -> bool {
|
|
rect.pos.x >= window.pos.x - 1e-6
|
|
&& rect.pos.y >= window.pos.y - 1e-6
|
|
&& rect.pos.x + rect.size.x <= window.pos.x + window.size.x + 1e-6
|
|
&& rect.pos.y + rect.size.y <= window.pos.y + window.size.y + 1e-6
|
|
}
|
|
|
|
fn overlap(a: Rect, b: Rect) -> bool {
|
|
a.pos.x < b.pos.x + b.size.x - 1e-6
|
|
&& b.pos.x < a.pos.x + a.size.x - 1e-6
|
|
&& a.pos.y < b.pos.y + b.size.y - 1e-6
|
|
&& b.pos.y < a.pos.y + a.size.y - 1e-6
|
|
}
|
|
|
|
/// Every action inside the window, clear of the button, no two faces
|
|
/// closer than the gap, no two squares overlapping and none over the
|
|
/// button's, and the arcs or lines filled in order.
|
|
fn assert_laid_out(what: &str, centre: DVec2, plan: &DialPlan, n: usize, window: Rect) {
|
|
assert_laid_out_sized(what, DEFAULT_SIZES, centre, plan, n, window)
|
|
}
|
|
|
|
fn assert_laid_out_sized(what: &str, s: Sizes, centre: DVec2, plan: &DialPlan, n: usize, window: Rect) {
|
|
assert_eq!(plan.offsets.len(), n, "{what}");
|
|
assert_eq!(plan.rows.len(), n, "{what}");
|
|
assert!(plan.rows.windows(2).all(|w| w[0] <= w[1]), "{what}: rows out of order {:?}", plan.rows);
|
|
let button = square(centre, s.main);
|
|
for (i, offset) in plan.offsets.iter().enumerate() {
|
|
let rect = square(centre + *offset, s.item);
|
|
assert!(inside(rect, window), "{what}: action {i} at {rect:?} leaves {window:?}");
|
|
assert!(offset.length() >= s.main * 0.5 + s.gap + s.item * 0.5 - 1e-6, "{what}: action {i} on the button");
|
|
assert!(!overlap(rect, button), "{what}: action {i} over the button's square");
|
|
for j in i + 1..n {
|
|
let apart = (*offset - plan.offsets[j]).length();
|
|
assert!(apart >= s.item + s.gap - 1e-6, "{what}: actions {i} and {j} are {apart} apart");
|
|
assert!(!overlap(rect, square(centre + plan.offsets[j], s.item)), "{what}: squares {i} and {j} overlap");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Any number of actions, from every anchor, in every layout, in a
|
|
/// window that holds twelve only by folding: none leaves the window and
|
|
/// none overlaps another.
|
|
#[test]
|
|
fn any_number_of_actions_stay_in_the_window_and_apart() {
|
|
crate::on_test_cx(|| {
|
|
let (host, window) = small_window(480.0, 360.0);
|
|
for anchor in FloatingAnchor::ALL {
|
|
for dial in LAYOUTS {
|
|
for n in COUNTS {
|
|
let (centre, plan) = plan_in(anchor, dial, n, host, window);
|
|
assert_laid_out(&format!("{anchor:?} {dial:?} n {n}"), centre, &plan, n, window);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A small button pinned close to the window's edge has less room
|
|
/// between its middle and the edge than half an action, and the actions
|
|
/// level with it reach that far: the set moves in by the difference and
|
|
/// still leaves the window nowhere, at every count, from every anchor,
|
|
/// in every layout.
|
|
#[test]
|
|
fn a_small_button_at_the_edge_keeps_its_set_in_the_window() {
|
|
crate::on_test_cx(|| {
|
|
let flush = Inset {
|
|
left: 0.0,
|
|
top: 0.0,
|
|
right: 0.0,
|
|
bottom: 0.0,
|
|
};
|
|
let small = Sizes {
|
|
main: 40.0,
|
|
item: 32.0,
|
|
gap: 8.0,
|
|
min_r: 64.0,
|
|
max_r: NO_MAX,
|
|
margin: flush,
|
|
};
|
|
let big_actions = Sizes {
|
|
main: 40.0,
|
|
item: 64.0,
|
|
gap: 12.0,
|
|
min_r: 88.0,
|
|
max_r: NO_MAX,
|
|
margin: MARGIN,
|
|
};
|
|
for (s, w, h) in [(small, 480.0, 360.0), (small, 1400.0, 900.0), (big_actions, 1400.0, 900.0)] {
|
|
let (host, window) = small_window(w, h);
|
|
for anchor in FloatingAnchor::ALL {
|
|
for dial in LAYOUTS {
|
|
for n in 1..=12usize {
|
|
let (centre, plan) = plan_sized(s, anchor, dial, n, host, window);
|
|
assert_laid_out_sized(&format!("{s:?} {w}x{h} {anchor:?} {dial:?} n {n}"), s, centre, &plan, n, window);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Where there is room the set is not moved at all.
|
|
let (host, window) = small_window(1400.0, 900.0);
|
|
let (_, plan) = plan_in(FloatingAnchor::BottomRight, SpeedDialLayout::Vertical, 3, host, window);
|
|
assert_eq!(plan.offsets, dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Vertical, 3, MAIN, ITEM, GAP, MIN_R, NO_MAX));
|
|
});
|
|
}
|
|
|
|
/// The window the live checks force: eight and twelve on a corner arc
|
|
/// take a second arc, and twelve in a column from the bottom edge or in
|
|
/// a row from a corner fold into lines, all still inside.
|
|
#[test]
|
|
fn a_crowded_set_takes_a_second_arc_or_a_second_line() {
|
|
crate::on_test_cx(|| {
|
|
let (host, window) = small_window(400.0, 300.0);
|
|
for (anchor, dial, n) in [
|
|
(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 8),
|
|
(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 12),
|
|
(FloatingAnchor::BottomCenter, SpeedDialLayout::Vertical, 12),
|
|
(FloatingAnchor::TopLeft, SpeedDialLayout::Horizontal, 12),
|
|
] {
|
|
let what = format!("{anchor:?} {dial:?} n {n}");
|
|
let (centre, plan) = plan_in(anchor, dial, n, host, window);
|
|
assert!(plan.row_count() >= 2, "{what}: one row {:?}", plan.rows);
|
|
assert_laid_out(&what, centre, &plan, n, window);
|
|
}
|
|
// Three still take the single arc of the spec.
|
|
let (centre, plan) = plan_in(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 3, host, window);
|
|
assert_eq!(plan.row_count(), 1);
|
|
assert_eq!(plan.offsets, dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 3, MAIN, ITEM, GAP, MIN_R, NO_MAX));
|
|
assert!(plan.offsets.iter().all(|offset| inside(resting_rect(centre, *offset), window)));
|
|
});
|
|
}
|
|
|
|
/// An arc takes all it can hold before the arc outside it takes any:
|
|
/// every arc but the last could not hold one more action at the widest
|
|
/// the window lets it be, with the arcs outside it still inside.
|
|
#[test]
|
|
fn the_inner_arc_fills_first() {
|
|
crate::on_test_cx(|| {
|
|
let (host, window) = small_window(400.0, 300.0);
|
|
let anchor = FloatingAnchor::BottomRight;
|
|
let (_, _, span) = radial_arc(anchor);
|
|
let centre = main_rect(anchor, host, MAIN, MARGIN).center();
|
|
let room = arc_room(anchor, centre, ITEM, window);
|
|
for n in [8usize, 12] {
|
|
let counts = arc_counts(anchor, n, MAIN, ITEM, GAP, MIN_R, NO_MAX, centre, window);
|
|
assert_eq!(counts.iter().sum::<usize>(), n);
|
|
let arcs = counts.len();
|
|
assert!(arcs >= 2, "n {n}: {counts:?}");
|
|
for (k, count) in counts.iter().enumerate().take(arcs - 1) {
|
|
let widest = room - (arcs - 1 - k) as f64 * arc_step(ITEM, GAP);
|
|
let one_more = arc_radius(count + 1, span, ITEM, GAP, MAIN, MIN_R);
|
|
assert!(one_more > widest, "n {n}: arc {k} of {counts:?} had room for another");
|
|
}
|
|
let plan = dial_plan(anchor, SpeedDialLayout::Radial, n, MAIN, ITEM, GAP, MIN_R, NO_MAX, centre, window);
|
|
for (k, count) in counts.iter().enumerate() {
|
|
assert_eq!(plan.rows.iter().filter(|row| **row == k).count(), *count, "n {n}: arc {k}");
|
|
}
|
|
assert!(plan.radii.windows(2).all(|w| w[1] >= w[0] + arc_step(ITEM, GAP) - 1e-9), "n {n}: {:?}", plan.radii);
|
|
assert!(*plan.radii.last().unwrap() <= room + 1e-9, "n {n}: the outer arc leaves the window");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// How many actions rest on each arc or line, innermost first.
|
|
fn per_row(plan: &DialPlan) -> Vec<usize> {
|
|
(0..plan.row_count()).map(|k| plan.rows.iter().filter(|row| **row == k).count()).collect()
|
|
}
|
|
|
|
/// Held to a furthest radius, a set the arcs inside it can hold lies
|
|
/// wholly inside it, from every anchor and at every count, no two
|
|
/// squares overlapping and the arcs filled in order. Whether those arcs
|
|
/// can hold the set is counted apart from the layout: the arcs a step
|
|
/// apart from the least radius out to the furthest, each holding all
|
|
/// its spacing allows.
|
|
#[test]
|
|
fn a_furthest_radius_holds_every_arc_inside_it() {
|
|
crate::on_test_cx(|| {
|
|
let mut spread = 0;
|
|
for max_r in [MAX_R, 120.0, 156.0, 260.0] {
|
|
for anchor in FloatingAnchor::ALL {
|
|
let (_, _, span) = radial_arc(anchor);
|
|
let step = arc_step(ITEM, GAP);
|
|
let mut held_inside = 0;
|
|
let mut r = arc_radius(1, span, ITEM, GAP, MAIN, MIN_R);
|
|
while r <= max_r + 1e-9 {
|
|
held_inside += arc_capacity(r, span, ITEM, GAP, MAIN, MIN_R, usize::MAX);
|
|
r += step;
|
|
}
|
|
for n in 1..=16usize {
|
|
let what = format!("max {max_r} {anchor:?} n {n}");
|
|
let plan = dial_plan(anchor, SpeedDialLayout::Radial, n, MAIN, ITEM, GAP, MIN_R, max_r, dvec2(0.0, 0.0), UNBOUNDED);
|
|
assert_laid_out(&what, dvec2(0.0, 0.0), &plan, n, UNBOUNDED);
|
|
if n > held_inside {
|
|
continue;
|
|
}
|
|
for (i, offset) in plan.offsets.iter().enumerate() {
|
|
assert!(offset.length() <= max_r + 1e-9, "{what}: action {i} rests {} out", offset.length());
|
|
}
|
|
assert!(plan.radii.iter().all(|r| *r <= max_r + 1e-9), "{what}: {:?}", plan.radii);
|
|
if arc_radius(n, span, ITEM, GAP, MAIN, MIN_R) > max_r + 1e-9 {
|
|
spread += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(spread > 0, "no set here would have reached past its furthest radius on one arc");
|
|
});
|
|
}
|
|
|
|
/// The small preset's sizes and furthest radius.
|
|
const SMALL_SIZES: Sizes = Sizes {
|
|
main: 40.0,
|
|
item: 32.0,
|
|
gap: 8.0,
|
|
min_r: 64.0,
|
|
max_r: 156.0,
|
|
margin: MARGIN,
|
|
};
|
|
|
|
/// The large preset's sizes and furthest radius.
|
|
const LARGE_SIZES: Sizes = Sizes {
|
|
main: 72.0,
|
|
item: 48.0,
|
|
gap: 14.0,
|
|
min_r: 108.0,
|
|
max_r: 238.0,
|
|
margin: MARGIN,
|
|
};
|
|
|
|
/// Each preset's furthest radius is the least a step of 2 reaches that
|
|
/// holds twelve from a corner, the most the anchors page shows, so
|
|
/// every count up to twelve rests inside it from every anchor. From a
|
|
/// corner three keep their one arc at the least radius, eight take arcs
|
|
/// of five and three and twelve arcs of five and seven. Left to a large
|
|
/// window, eight of the default size would rest on one arc 252.6 out
|
|
/// and twelve on one 396.5 out.
|
|
#[test]
|
|
fn the_default_furthest_radius_holds_twelve_from_a_corner() {
|
|
crate::on_test_cx(|| {
|
|
let (host, window) = small_window(1400.0, 900.0);
|
|
let default = Sizes {
|
|
max_r: MAX_R,
|
|
..DEFAULT_SIZES
|
|
};
|
|
let quarter = f64::to_radians(90.0);
|
|
for s in [default, SMALL_SIZES, LARGE_SIZES] {
|
|
let twelve = least_room(12, quarter, s.main, s.item, s.gap, s.min_r, 0.0);
|
|
assert!(twelve <= s.max_r && s.max_r < twelve + 2.0, "{s:?}: twelve need {twelve}");
|
|
for anchor in FloatingAnchor::ALL {
|
|
for n in 1..=12usize {
|
|
let what = format!("{s:?} {anchor:?} n {n}");
|
|
let (centre, plan) = plan_sized(s, anchor, SpeedDialLayout::Radial, n, host, window);
|
|
assert_laid_out_sized(&what, s, centre, &plan, n, window);
|
|
for (i, offset) in plan.offsets.iter().enumerate() {
|
|
assert!(offset.length() <= s.max_r + 1e-9, "{what}: action {i} rests {} out", offset.length());
|
|
}
|
|
}
|
|
}
|
|
for (n, counts) in [(3usize, vec![3usize]), (8, vec![5, 3]), (12, vec![5, 7])] {
|
|
let (_, plan) = plan_sized(s, FloatingAnchor::BottomRight, SpeedDialLayout::Radial, n, host, window);
|
|
assert_eq!(per_row(&plan), counts, "{s:?} n {n}: {:?}", plan.radii);
|
|
if n == 3 {
|
|
assert!((plan.radii[0] - s.min_r).abs() < 1e-9, "{s:?}: {:?}", plan.radii);
|
|
}
|
|
}
|
|
}
|
|
for (n, r) in [(8usize, 252.62), (12, 396.48)] {
|
|
let (_, wide) = plan_in(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, n, host, window);
|
|
assert_eq!(wide.row_count(), 1, "n {n}");
|
|
assert!((wide.radii[0] - r).abs() < 0.01, "n {n}: {:?}", wide.radii);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Held to a furthest radius as to a small window, a set takes more
|
|
/// than one arc exactly when one arc would reach past it, and an arc
|
|
/// takes all it can hold before the arc outside it takes any: every arc
|
|
/// but the last could not hold one more at the widest the furthest
|
|
/// radius lets it be with the arcs outside it still inside. Twelve from
|
|
/// a corner and sixteen from an edge fit inside the default.
|
|
#[test]
|
|
fn inside_a_furthest_radius_the_inner_arc_fills_first() {
|
|
crate::on_test_cx(|| {
|
|
let step = arc_step(ITEM, GAP);
|
|
for (anchor, most) in [(FloatingAnchor::BottomRight, 12usize), (FloatingAnchor::TopCenter, 16)] {
|
|
let (_, _, span) = radial_arc(anchor);
|
|
for n in 1..=most {
|
|
let what = format!("{anchor:?} n {n}");
|
|
let counts = arc_counts(anchor, n, MAIN, ITEM, GAP, MIN_R, MAX_R, dvec2(0.0, 0.0), UNBOUNDED);
|
|
assert_eq!(counts.iter().sum::<usize>(), n, "{what}");
|
|
let one_arc_fits = arc_radius(n, span, ITEM, GAP, MAIN, MIN_R) <= MAX_R + 1e-9;
|
|
assert_eq!(counts.len() == 1, one_arc_fits, "{what}: {counts:?}");
|
|
let arcs = counts.len();
|
|
for (k, count) in counts.iter().enumerate().take(arcs - 1) {
|
|
let widest = MAX_R - (arcs - 1 - k) as f64 * step;
|
|
let one_more = arc_radius(count + 1, span, ITEM, GAP, MAIN, MIN_R);
|
|
assert!(one_more > widest, "{what}: arc {k} of {counts:?} had room for another");
|
|
}
|
|
let plan = dial_plan(anchor, SpeedDialLayout::Radial, n, MAIN, ITEM, GAP, MIN_R, MAX_R, dvec2(0.0, 0.0), UNBOUNDED);
|
|
assert_eq!(per_row(&plan), counts, "{what}");
|
|
assert!(plan.rows.windows(2).all(|w| w[0] <= w[1]), "{what}: {:?}", plan.rows);
|
|
assert!(plan.radii.iter().all(|r| *r <= MAX_R + 1e-9), "{what}: {:?}", plan.radii);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A furthest radius of 0 is no limit: the layout is the one a radius
|
|
/// past anything the set would reach gives, at every count, and eight
|
|
/// from a corner go back to the one wide arc a large window allows,
|
|
/// where the widget's own furthest radius takes two.
|
|
#[test]
|
|
fn a_furthest_radius_of_zero_is_no_limit() {
|
|
crate::on_test_cx(|| {
|
|
let (host, window) = small_window(1400.0, 900.0);
|
|
let quarter = f64::to_radians(90.0);
|
|
let laid = |max_r: f64, n: usize| {
|
|
let s = Sizes {
|
|
max_r,
|
|
..DEFAULT_SIZES
|
|
};
|
|
plan_sized(s, FloatingAnchor::BottomRight, SpeedDialLayout::Radial, n, host, window).1
|
|
};
|
|
for n in 1..=12usize {
|
|
assert_eq!(laid(NO_MAX, n), laid(1.0e6, n), "n {n}");
|
|
}
|
|
let wide = laid(NO_MAX, 8);
|
|
assert_eq!(wide.row_count(), 1);
|
|
assert!((wide.radii[0] - arc_radius(8, quarter, ITEM, GAP, MAIN, MIN_R)).abs() < 1e-9, "{:?}", wide.radii);
|
|
assert_eq!(laid(MAX_R, 8).row_count(), 2);
|
|
assert_eq!(
|
|
dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 8, MAIN, ITEM, GAP, MIN_R, NO_MAX),
|
|
dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 8, MAIN, ITEM, GAP, MIN_R, 1.0e6)
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Lowering the furthest radius never moves an action further out,
|
|
/// past what the arcs inside it can hold as well: every count to
|
|
/// sixteen, from a corner and from an edge, with each preset's sizes,
|
|
/// at every furthest radius the Max radius control can set.
|
|
#[test]
|
|
fn lowering_the_furthest_radius_never_moves_the_set_out() {
|
|
crate::on_test_cx(|| {
|
|
let furthest = |offsets: Vec<DVec2>| offsets.iter().map(|offset| offset.length()).fold(0.0, f64::max);
|
|
for s in [DEFAULT_SIZES, SMALL_SIZES, LARGE_SIZES] {
|
|
for anchor in [FloatingAnchor::BottomRight, FloatingAnchor::TopCenter] {
|
|
for n in 1..=16usize {
|
|
let mut nearer: Option<(f64, f64)> = None;
|
|
for twos in 1..=200 {
|
|
let max_r = twos as f64 * 2.0;
|
|
let reach = furthest(dial_offsets(anchor, SpeedDialLayout::Radial, n, s.main, s.item, s.gap, s.min_r, max_r));
|
|
if let Some((lower, was)) = nearer {
|
|
assert!(
|
|
reach >= was - 1e-9,
|
|
"{s:?} {anchor:?} n {n}: held to {lower} the set reaches {was}, held to {max_r} only {reach}"
|
|
);
|
|
}
|
|
nearer = Some((max_r, reach));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A set more than the arcs inside its furthest radius can hold keeps
|
|
/// every action, each at its full size and none over another, and rests
|
|
/// as near the button as its spacing allows: on the arcs of the least
|
|
/// room past the furthest radius that holds it, never further out than
|
|
/// arcs laid tight from the least radius, and each arc filled before the
|
|
/// next. From a corner held to 156, nine take arcs of four and five
|
|
/// 165.9 out, where arcs from the least radius out would leave one
|
|
/// action alone on a third arc 201.1 out. In a window that holds the set
|
|
/// only past the furthest radius, nothing leaves the window.
|
|
#[test]
|
|
fn a_set_too_large_for_its_furthest_radius_rests_as_near_as_it_can_past_it() {
|
|
crate::on_test_cx(|| {
|
|
let step = arc_step(ITEM, GAP);
|
|
let (host, window) = small_window(1400.0, 900.0);
|
|
for max_r in [156.0, 100.0, 40.0] {
|
|
let s = Sizes {
|
|
max_r,
|
|
..DEFAULT_SIZES
|
|
};
|
|
for anchor in FloatingAnchor::ALL {
|
|
let (_, _, span) = radial_arc(anchor);
|
|
let least = arc_radius(1, span, ITEM, GAP, MAIN, MIN_R);
|
|
for n in [20usize, 30] {
|
|
let what = format!("max {max_r} {anchor:?} n {n}");
|
|
let (centre, plan) = plan_sized(s, anchor, SpeedDialLayout::Radial, n, host, window);
|
|
assert_laid_out(&what, centre, &plan, n, window);
|
|
assert!(plan.radii.iter().any(|r| *r > max_r + 1e-9), "{what}: squeezed inside {:?}", plan.radii);
|
|
// Arcs a step apart from the least radius out, each
|
|
// holding all its spacing allows, reach this far.
|
|
let mut tight = least;
|
|
let mut left = n;
|
|
loop {
|
|
left -= arc_capacity(tight, span, ITEM, GAP, MAIN, MIN_R, left);
|
|
if left == 0 {
|
|
break;
|
|
}
|
|
tight += step;
|
|
}
|
|
let outer = *plan.radii.last().unwrap();
|
|
assert!(outer <= tight + 1e-9, "{what}: the outer arc at {outer} is further out than tight arcs reach, {tight}");
|
|
let counts = per_row(&plan);
|
|
for k in 0..counts.len() - 1 {
|
|
let one_more = arc_radius(counts[k] + 1, span, ITEM, GAP, MAIN, MIN_R);
|
|
let tightest = least + k as f64 * step;
|
|
assert!(one_more > tightest, "{what}: arc {k} of {counts:?} had room for another");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let quarter = f64::to_radians(90.0);
|
|
for (n, counts, reach) in [(9usize, vec![4usize, 5], 165.9), (12, vec![5, 7], 199.2), (16, vec![4, 5, 7], 222.4)] {
|
|
let offsets = dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, n, MAIN, ITEM, GAP, MIN_R, 156.0);
|
|
let room = least_room(n, quarter, MAIN, ITEM, GAP, MIN_R, 156.0);
|
|
let plan = dial_plan(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, n, MAIN, ITEM, GAP, MIN_R, 156.0, dvec2(0.0, 0.0), UNBOUNDED);
|
|
assert_eq!(per_row(&plan), counts, "n {n}: {:?}", plan.radii);
|
|
let outer = *plan.radii.last().unwrap();
|
|
assert!((outer - reach).abs() < 0.05, "n {n}: {:?}", plan.radii);
|
|
assert!((room - reach).abs() < 0.05, "n {n}: the least room is {room}");
|
|
// Held to that least room, the set is laid out the same.
|
|
let at_least = dial_offsets(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, n, MAIN, ITEM, GAP, MIN_R, room);
|
|
assert_eq!(offsets, at_least, "n {n}");
|
|
}
|
|
// Sixteen from a corner of a window 310 high: the least room that
|
|
// holds them, 222.4, is past the furthest radius and inside the 240
|
|
// the window leaves.
|
|
let (host, window) = small_window(1400.0, 310.0);
|
|
let s = Sizes {
|
|
max_r: MAX_R,
|
|
..DEFAULT_SIZES
|
|
};
|
|
let centre = main_rect(FloatingAnchor::BottomRight, host, MAIN, MARGIN).center();
|
|
assert!((arc_room(FloatingAnchor::BottomRight, centre, ITEM, window) - 240.0).abs() < 1e-9);
|
|
let (centre, plan) = plan_sized(s, FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 16, host, window);
|
|
assert_laid_out("sixteen under a low window", centre, &plan, 16, window);
|
|
assert_eq!(per_row(&plan), vec![4, 5, 7], "{:?}", plan.radii);
|
|
assert!((plan.radii[2] - 222.4).abs() < 0.05, "{:?}", plan.radii);
|
|
});
|
|
}
|
|
|
|
/// A small button pinned flush to a corner or an edge moves its set in
|
|
/// off the window's edge, and the furthest radius is still measured
|
|
/// from the button's own middle: every set that fits inside it that far
|
|
/// nearer rests inside it, where laying the arcs around the moved
|
|
/// centre alone would have left some of them past it.
|
|
#[test]
|
|
fn a_set_moved_in_off_the_edge_keeps_inside_its_furthest_radius() {
|
|
crate::on_test_cx(|| {
|
|
let flush = Inset {
|
|
left: 0.0,
|
|
top: 0.0,
|
|
right: 0.0,
|
|
bottom: 0.0,
|
|
};
|
|
let (host, window) = small_window(1400.0, 900.0);
|
|
let mut would_have_passed = 0;
|
|
for anchor in FloatingAnchor::ALL {
|
|
let (_, _, span) = radial_arc(anchor);
|
|
for max_r in [150.0, 210.0, 266.0, 300.0] {
|
|
let s = Sizes {
|
|
main: 40.0,
|
|
item: 64.0,
|
|
gap: 12.0,
|
|
min_r: 88.0,
|
|
max_r,
|
|
margin: flush,
|
|
};
|
|
for n in 1..=8usize {
|
|
let what = format!("{anchor:?} max {max_r} n {n}");
|
|
let (centre, plan) = plan_sized(s, anchor, SpeedDialLayout::Radial, n, host, window);
|
|
assert_laid_out_sized(&what, s, centre, &plan, n, window);
|
|
let unmoved = radial_plan(anchor, n, s.main, s.item, s.gap, s.min_r, max_r, centre, window);
|
|
let shift = pinned_overrun(anchor, &unmoved.offsets, centre, s.item, window);
|
|
if least_room(n, span, s.main, s.item, s.gap, s.min_r, 0.0) > max_r - shift.length() {
|
|
continue;
|
|
}
|
|
for (i, offset) in plan.offsets.iter().enumerate() {
|
|
assert!(offset.length() <= max_r + 1e-9, "{what}: action {i} rests {} from the button", offset.length());
|
|
}
|
|
let around_moved = radial_plan(anchor, n, s.main, s.item, s.gap, s.min_r, max_r, centre + shift, window);
|
|
if around_moved.offsets.iter().any(|offset| (*offset + shift).length() > max_r + 1e-9) {
|
|
would_have_passed += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(would_have_passed > 0, "no set here was moved past its furthest radius");
|
|
});
|
|
}
|
|
|
|
/// The arrows walk an arc in order: from the button onto the inner arc,
|
|
/// then one action at a time along it and on round the outer arc, never
|
|
/// skipping one, and every step can be taken back. An arrow only ever
|
|
/// moves the way it points, the step from the end of the inner arc back
|
|
/// to the start of the outer one included. A folded row is walked by
|
|
/// direction instead, across to the line beside it.
|
|
#[test]
|
|
fn arrows_walk_the_inner_arc_then_the_outer() {
|
|
crate::on_test_cx(|| {
|
|
let up = dvec2(0.0, -1.0);
|
|
let down = dvec2(0.0, 1.0);
|
|
let left = dvec2(-1.0, 0.0);
|
|
let right = dvec2(1.0, 0.0);
|
|
let arrows = [up, down, left, right];
|
|
let origin = dvec2(0.0, 0.0);
|
|
let (host, window) = small_window(400.0, 300.0);
|
|
let (_, plan) = plan_in(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 12, host, window);
|
|
let points = &plan.offsets;
|
|
let first = plan.rows.iter().filter(|row| **row == 0).count();
|
|
assert!(first < points.len(), "two arcs");
|
|
assert_eq!(arc_walk_target(points, first, None, origin, up), Some(0), "up from the button: the top of the inner arc");
|
|
assert_eq!(arc_walk_target(points, first, None, origin, left), Some(first - 1), "left: its other end");
|
|
|
|
let mut walked = vec![0];
|
|
let mut at = 0;
|
|
while at + 1 < points.len() {
|
|
for dir in arrows {
|
|
let to = arc_walk_target(points, first, Some(at), origin, dir);
|
|
assert!(to.is_none() || to == Some(at + 1) || to.map(|t| t + 1) == Some(at), "an arrow from {at} jumped to {to:?}");
|
|
}
|
|
let next = at + 1;
|
|
assert!(
|
|
arrows.iter().any(|dir| arc_walk_target(points, first, Some(at), origin, *dir) == Some(next)),
|
|
"no arrow moves on from {at}"
|
|
);
|
|
assert!(
|
|
arrows.iter().any(|dir| arc_walk_target(points, first, Some(next), origin, *dir) == Some(at)),
|
|
"no arrow comes back to {at}"
|
|
);
|
|
walked.push(next);
|
|
at = next;
|
|
}
|
|
assert_eq!(walked, (0..12).collect::<Vec<_>>());
|
|
let rows: Vec<usize> = walked.iter().map(|i| plan.rows[*i]).collect();
|
|
assert!(rows.windows(2).all(|w| w[0] <= w[1]), "the inner arc before the outer: {rows:?}");
|
|
|
|
// From the left end of the inner arc the outer arc starts back at
|
|
// the top: Down does not go up there, Up and Right do not go on down.
|
|
let seam = first - 1;
|
|
assert!(points[first].y < points[seam].y, "the outer arc starts above the inner arc's end");
|
|
assert_ne!(arc_walk_target(points, first, Some(seam), origin, down), Some(first), "Down went up to the outer arc");
|
|
assert_ne!(arc_walk_target(points, first, Some(first), origin, up), Some(seam), "Up went down to the inner arc");
|
|
|
|
for (w, h, n) in [(400.0, 300.0, 8usize), (400.0, 300.0, 12), (1400.0, 360.0, 8), (1400.0, 360.0, 12), (1400.0, 420.0, 12)] {
|
|
let (host, window) = small_window(w, h);
|
|
for anchor in FloatingAnchor::ALL {
|
|
let (_, plan) = plan_in(anchor, SpeedDialLayout::Radial, n, host, window);
|
|
let points = &plan.offsets;
|
|
let first = plan.rows.iter().filter(|row| **row == 0).count();
|
|
let what = format!("{anchor:?} {w}x{h} n {n}");
|
|
for at in 0..n {
|
|
for dir in arrows {
|
|
if let Some(to) = arc_walk_target(points, first, Some(at), origin, dir) {
|
|
assert!(to == at + 1 || to + 1 == at, "{what}: from {at} to {to}");
|
|
let step = points[to] - points[at];
|
|
assert!(step.x * dir.x + step.y * dir.y > 0.0, "{what}: from {at} the arrow {dir:?} went to {to}, against it");
|
|
}
|
|
}
|
|
if at + 1 < n {
|
|
assert!(arrows.iter().any(|dir| arc_walk_target(points, first, Some(at), origin, *dir) == Some(at + 1)), "{what}: stuck at {at}");
|
|
assert!(arrows.iter().any(|dir| arc_walk_target(points, first, Some(at + 1), origin, *dir) == Some(at)), "{what}: no way back to {at}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let (_, grid) = plan_in(FloatingAnchor::TopLeft, SpeedDialLayout::Horizontal, 12, host, window);
|
|
let per_line = grid.rows.iter().filter(|row| **row == 0).count();
|
|
assert!(per_line < 12, "folded");
|
|
assert_eq!(arrow_target(&grid.offsets, Some(0), origin, right), Some(1));
|
|
assert_eq!(arrow_target(&grid.offsets, Some(0), origin, down), Some(per_line));
|
|
assert_eq!(arrow_target(&grid.offsets, Some(per_line), origin, up), Some(0));
|
|
});
|
|
}
|
|
|
|
/// Twelve open about as quickly as three: however many actions, the
|
|
/// last one is in place no more than the budget after the first.
|
|
#[test]
|
|
fn a_large_set_opens_about_as_quickly_as_a_small_one() {
|
|
crate::on_test_cx(|| {
|
|
let (enter, stagger) = (0.2, 0.03);
|
|
for n in 1..=48usize {
|
|
let step = stagger_step(n, stagger);
|
|
let last_in_place = enter + n.saturating_sub(1) as f64 * step;
|
|
assert!(last_in_place <= enter + STAGGER_BUDGET + 1e-12, "{n}: {last_in_place}");
|
|
if n <= 6 {
|
|
assert!((step - stagger).abs() < 1e-12, "{n}: a small set keeps its stagger");
|
|
}
|
|
let legs = legs_to(&vec![0.0; n], 1.0, enter, step);
|
|
assert!(legs.iter().all(|leg| leg.done(enter + STAGGER_BUDGET + 1e-9)), "{n}");
|
|
}
|
|
assert!(stagger_step(12, stagger) < stagger);
|
|
});
|
|
}
|
|
|
|
/// Going away, the last action out is the first one back; and a set
|
|
/// reopened part way back turns every action round at once, from where
|
|
/// it is, with no jump.
|
|
#[test]
|
|
fn the_last_out_is_the_first_back_and_a_reopened_set_turns_round() {
|
|
crate::on_test_cx(|| {
|
|
let linear = Ease::Linear;
|
|
let (exit, stagger) = (0.15, 0.03);
|
|
let still_out: Vec<f64> = (0..3).map(|i| exit_progress(i, 3, 0.05, exit, stagger, &linear, false)).collect();
|
|
assert!(still_out[2] < still_out[1] && still_out[1] < still_out[0], "{still_out:?}");
|
|
let legs = legs_to(&[1.0, 1.0, 1.0], 0.0, exit, stagger);
|
|
for (i, leg) in legs.iter().enumerate() {
|
|
assert!((leg.delay - (2 - i) as f64 * stagger).abs() < 1e-12, "{i}: {leg:?}");
|
|
assert!((leg.value(0.05, &linear) - still_out[i]).abs() < 1e-12, "{i}: the legs run the same curve");
|
|
}
|
|
|
|
let back_out = legs_to(&still_out, 1.0, 0.2, stagger);
|
|
for (leg, from) in back_out.iter().zip(&still_out) {
|
|
assert_eq!(leg.delay, 0.0, "none waits for its turn");
|
|
assert_eq!(leg.value(0.0, &linear), *from, "no jump");
|
|
assert_eq!(leg.value(0.2, &linear), 1.0);
|
|
assert!((leg.secs - 0.2 * (1.0 - from)).abs() < 1e-12, "the share of the time its distance asks for");
|
|
}
|
|
assert_eq!(exit_progress(0, 3, 0.0, exit, stagger, &linear, true), 0.0, "reduced motion is home at once");
|
|
});
|
|
}
|
|
|
|
/// The theme easing a token names, read from the running theme.
|
|
fn theme_value(vm: &mut ScriptVm, name: &str) -> ScriptValue {
|
|
let theme = vm.module(id!(theme));
|
|
vm.bx.heap.value(theme, LiveId::from_str(name).into(), NoTrap)
|
|
}
|
|
|
|
const THEME_EASES: [&str; 8] = [
|
|
"motion_ease_standard",
|
|
"motion_ease_standard_decelerate",
|
|
"motion_ease_standard_accelerate",
|
|
"motion_ease_emphasized_decelerate",
|
|
"motion_ease_emphasized_accelerate",
|
|
"motion_ease_linear",
|
|
"motion_ease_spring",
|
|
"motion_ease_bounce",
|
|
];
|
|
|
|
/// Travel follows the theme's own evaluation of each easing token, in
|
|
/// and out; the defaults are the tokens the addendum names; and a
|
|
/// spring may carry an action past its place.
|
|
#[test]
|
|
fn the_motion_runs_on_the_theme_eases() {
|
|
crate::on_test_cx(|| {
|
|
let mut cx = test_cx();
|
|
cx.with_vm(|vm| {
|
|
crate::script_mod(vm);
|
|
let (enter, exit, stagger) = (0.2, 0.15, 0.03);
|
|
for name in THEME_EASES {
|
|
let value = theme_value(vm, name);
|
|
let ease = Ease::script_from_value(vm, value);
|
|
for x in [0.25, 0.5, 0.75] {
|
|
let want = ease.map(x);
|
|
let out = enter_progress(2, 2.0 * stagger + x * enter, enter, stagger, &ease, false);
|
|
assert!((out - want).abs() < 1e-9, "{name} out at {x}: {out}, the theme says {want}");
|
|
let back = exit_progress(0, 3, 2.0 * stagger + x * exit, exit, stagger, &ease, false);
|
|
assert!((back - (1.0 - want)).abs() < 1e-9, "{name} back at {x}: {back}");
|
|
// Turned round part way, out again and back again: each
|
|
// leg from where its action had got to, on the same curve
|
|
// over the share of the time its distance asks for.
|
|
let part_way = [0.3, 0.6, 0.9];
|
|
for (to, secs) in [(1.0, enter), (0.0, exit)] {
|
|
for (leg, from) in legs_to(&part_way, to, secs, stagger).iter().zip(part_way) {
|
|
assert_eq!(leg.delay, 0.0, "{name}: turned round at once");
|
|
let got = leg.value(x * leg.secs, &ease);
|
|
let expected = from + (to - from) * want;
|
|
assert!((got - expected).abs() < 1e-9, "{name} turned toward {to} at {x}: {got}, the theme says {expected}");
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(enter_progress(0, 0.0, enter, stagger, &ease, false), 0.0, "{name} starts at the button");
|
|
assert_eq!(enter_progress(0, enter, enter, stagger, &ease, false), 1.0, "{name} lands exactly");
|
|
}
|
|
|
|
let spring = theme_value(vm, "motion_ease_spring");
|
|
let spring = Ease::script_from_value(vm, spring);
|
|
let peak = (1..100)
|
|
.map(|k| enter_progress(0, k as f64 * 0.01 * enter, enter, 0.0, &spring, false))
|
|
.fold(0.0, f64::max);
|
|
assert!(peak > 1.0, "a spring goes past its place: {peak}");
|
|
|
|
let value = crate::script_eval!(vm, {use mod.widgets.* FloatingAction{}});
|
|
let widget = FloatingAction::script_from_value(vm, value);
|
|
let token = theme_value(vm, "motion_ease_emphasized_decelerate");
|
|
assert_eq!(widget.enter_ease, Ease::script_from_value(vm, token));
|
|
let token = theme_value(vm, "motion_ease_standard_accelerate");
|
|
assert_eq!(widget.exit_ease, Ease::script_from_value(vm, token));
|
|
assert_eq!(Some(widget.enter_secs), theme_value(vm, "motion_short_4").as_f64());
|
|
assert_eq!(Some(widget.exit_secs), theme_value(vm, "motion_short_3").as_f64());
|
|
});
|
|
});
|
|
}
|
|
|
|
/// An overshoot moves the face and never the resting place: at rest a
|
|
/// face is where the layout put it, past its place it stops at the
|
|
/// window's edge, and the layout presses and arrows read has no idea
|
|
/// how far along the travel is.
|
|
#[test]
|
|
fn an_overshoot_moves_the_face_not_the_resting_place() {
|
|
crate::on_test_cx(|| {
|
|
let (host, window) = small_window(400.0, 300.0);
|
|
let (centre, plan) = plan_in(FloatingAnchor::BottomRight, SpeedDialLayout::Radial, 12, host, window);
|
|
for (k, offset) in plan.offsets.iter().enumerate() {
|
|
let rest = resting_rect(centre, *offset);
|
|
let room = bounding(window, rest);
|
|
let still = keep_inside(centre + *offset, ITEM, room);
|
|
assert!(close_to(still, centre + *offset, 1e-9), "{k}: moved at rest");
|
|
let over = keep_inside(centre + *offset * 1.4, ITEM, room);
|
|
assert!(inside(Rect { pos: over - ITEM * 0.5, size: dvec2(ITEM, ITEM) }, window), "{k}: the overshoot is cut by the edge");
|
|
}
|
|
// A window too small for the set: the resting place overruns and
|
|
// still is not moved.
|
|
let tiny = Rect {
|
|
pos: dvec2(6.0, 6.0),
|
|
size: dvec2(60.0, 60.0),
|
|
};
|
|
let far = dvec2(-200.0, -200.0);
|
|
let rest = resting_rect(dvec2(40.0, 40.0), far);
|
|
assert!(close_to(keep_inside(dvec2(40.0, 40.0) + far, ITEM, bounding(tiny, rest)), dvec2(40.0, 40.0) + far, 1e-9));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn snapshot_value_spells_the_state() {
|
|
crate::on_test_cx(|| {
|
|
assert_eq!(
|
|
snapshot_text(true, false, SpeedDialLayout::Radial, FloatingAnchor::BottomRight),
|
|
"open radial bottom-right"
|
|
);
|
|
assert_eq!(
|
|
snapshot_text(true, true, SpeedDialLayout::Vertical, FloatingAnchor::TopLeft),
|
|
"pinned vertical top-left"
|
|
);
|
|
assert_eq!(
|
|
snapshot_text(false, false, SpeedDialLayout::Horizontal, FloatingAnchor::CenterLeft),
|
|
"closed horizontal center-left"
|
|
);
|
|
});
|
|
}
|
|
|
|
/// The source of the DSL between `from` and the first line that closes
|
|
/// the macro, a brace in the first column.
|
|
fn block<'a>(source: &'a str, from: &str) -> &'a str {
|
|
let start = source.find(from).unwrap_or_else(|| panic!("no {from}"));
|
|
let rest = &source[start..];
|
|
let end = rest.find("\n}").unwrap_or(rest.len());
|
|
&rest[..end]
|
|
}
|
|
|
|
/// A splatted variant is exported as a bare name, and TopLeft,
|
|
/// TopCenter and BottomCenter already are; and the short prefix belongs
|
|
/// to the property-panel controls. Neither may creep into this DSL or
|
|
/// into the two faces in button.rs.
|
|
#[test]
|
|
fn no_enum_is_splatted_and_no_name_starts_with_fab() {
|
|
crate::on_test_cx(|| {
|
|
let own = block(include_str!("floating_action.rs"), "script_mod! {");
|
|
let faces = block(include_str!("../../../core/src/button.rs"), "mod.widgets.ButtonFloating = ");
|
|
for (name, text) in [("floating_action.rs", own), ("button.rs", faces)] {
|
|
assert!(text.contains("mod.widgets."), "{name}: the block was not found");
|
|
assert!(!text.contains(concat!("splat", "(")), "{name} splats an enum");
|
|
assert!(!text.contains(concat!("mod.widgets.", "Fab")), "{name} names a widget with the short prefix");
|
|
}
|
|
assert!(own.contains("FloatingAnchor.BottomRight"));
|
|
});
|
|
}
|
|
|
|
/// A widget dropped with its set out cannot release its own lock, and
|
|
/// a lock nobody holds turns every press in the window away. The next
|
|
/// floating action to hear an event releases it.
|
|
#[test]
|
|
fn a_lock_left_by_a_dropped_widget_is_released() {
|
|
crate::on_test_cx(|| {
|
|
let mut cx = test_cx();
|
|
cx.with_vm(|vm| {
|
|
crate::script_mod(vm);
|
|
let value = crate::script_eval!(vm, {use mod.widgets.* FloatingAction{
|
|
one := FloatingActionItem{label: "One"}
|
|
}});
|
|
let mut dropped = FloatingAction::script_from_value(vm, value);
|
|
let mut survivor = FloatingAction::script_from_value(vm, value);
|
|
vm.with_cx_mut(|cx| {
|
|
// As an open set holds it; opening itself reads the clock,
|
|
// which a test has none of.
|
|
dropped.open = true;
|
|
dropped.lock(cx);
|
|
assert!(dropped.locked);
|
|
assert!(cx.sweep_lock_area().is_some());
|
|
drop(dropped);
|
|
assert!(cx.sweep_lock_area().is_some(), "nothing could release it on drop");
|
|
survivor.handle_event(cx, &Event::Signal, &mut Scope::empty());
|
|
assert_eq!(cx.sweep_lock_area(), None, "the next one released it");
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/// The DSL is markup the compiler never reads: the presets, the item's
|
|
/// face, the two shaders and the enum defaults only exist once it runs.
|
|
#[test]
|
|
fn the_presets_build_and_the_shaders_compile() {
|
|
crate::on_test_cx(|| {
|
|
let mut cx = test_cx();
|
|
cx.with_vm(|vm| {
|
|
crate::script_mod(vm);
|
|
for name in ["DrawFloatingShadowBase", "DrawFloatingChipBase"] {
|
|
let value = match name {
|
|
"DrawFloatingShadowBase" => crate::script_eval!(vm, {
|
|
mod.shader.test_compile_draw_source(mod.widgets.FloatingAction.draw_shadow, "glsl", false)
|
|
}),
|
|
_ => crate::script_eval!(vm, {
|
|
mod.shader.test_compile_draw_source(mod.widgets.FloatingActionItem.draw_chip, "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}");
|
|
}
|
|
|
|
let value = crate::script_eval!(vm, {use mod.widgets.* FloatingActionLg{
|
|
upload := FloatingActionItem{label: "Upload" enabled: false}
|
|
folder := FloatingActionItem{label: "Folder"}
|
|
stray := Label{text: "not an action"}
|
|
}});
|
|
let widget = FloatingAction::script_from_value(vm, value);
|
|
assert_eq!(widget.size, 72.0);
|
|
assert_eq!(widget.radial_max_radius, LARGE_SIZES.max_r);
|
|
assert_eq!(widget.anchor, FloatingAnchor::BottomRight);
|
|
assert_eq!(widget.dial, SpeedDialLayout::Radial);
|
|
assert_eq!(widget.labels, SpeedDialLabels::Auto);
|
|
assert_eq!(widget.edge_margin.left, 16.0);
|
|
assert!(!widget.main.is_empty(), "the main face was built");
|
|
assert!(widget.main.borrow::<Button>().is_some(), "the main face is a button");
|
|
let ids: Vec<LiveId> = widget.items.iter().map(|(id, _)| *id).collect();
|
|
assert_eq!(ids, vec![live_id!(upload), live_id!(folder)], "declaration order, the stray left out");
|
|
let upload = widget.items[0].1.borrow::<FloatingActionItem>().unwrap();
|
|
assert_eq!(upload.label, "Upload");
|
|
assert!(!upload.enabled);
|
|
assert!(upload.button.borrow::<Button>().is_some(), "each action has its face");
|
|
assert_eq!(snapshot_text(widget.open, widget.pinned, widget.dial, widget.anchor), "closed radial bottom-right");
|
|
});
|
|
});
|
|
}
|
|
|
|
/// One frame of `root` into a window-less pass of `size`, with the
|
|
/// overlay a window gives the tree to hang its own lists off.
|
|
struct Target {
|
|
pass: DrawPass,
|
|
draw_list: DrawList2d,
|
|
overlay: Overlay,
|
|
}
|
|
|
|
impl Target {
|
|
fn new(cx: &mut Cx) -> Self {
|
|
let overlay = cx.with_vm(|vm| Overlay::script_new(vm));
|
|
Target { pass: DrawPass::new(cx), draw_list: DrawList2d::new(cx), overlay }
|
|
}
|
|
|
|
fn draw(&mut self, cx: &mut Cx, root: &WidgetRef) {
|
|
let size = dvec2(600.0, 400.0);
|
|
self.pass.set_size(cx, size);
|
|
let event = DrawEvent::default();
|
|
let mut draw = crate::makepad_draw::cx_draw::CxDraw::new(cx, &event);
|
|
let mut cx2d = Cx2d::new(&mut draw);
|
|
cx2d.begin_pass(&self.pass, None);
|
|
self.draw_list.begin_always(&mut cx2d);
|
|
self.overlay.begin(&mut cx2d);
|
|
cx2d.begin_root_turtle(size, Layout::flow_down());
|
|
root.draw_all(&mut cx2d, &mut Scope::empty());
|
|
cx2d.end_pass_sized_turtle();
|
|
self.overlay.end(&mut cx2d);
|
|
self.draw_list.end(&mut cx2d);
|
|
cx2d.end_pass(&self.pass);
|
|
}
|
|
}
|
|
|
|
/// A 600 by 400 box with a column of three pinned to its bottom right,
|
|
/// the last one disabled, drawn once. No travel, so a draw lands every
|
|
/// action where it belongs.
|
|
fn set_in_a_box() -> (crate::PooledCx, WidgetRef, Target) {
|
|
let mut cx = test_cx();
|
|
let root = cx.with_vm(|vm| {
|
|
let value = crate::script_eval!(vm, {
|
|
use mod.widgets.*
|
|
View{
|
|
width: 600
|
|
height: 400
|
|
fab := FloatingAction{
|
|
dial: mod.widgets.SpeedDialLayout.Vertical
|
|
reduced_motion: true
|
|
one := FloatingActionItem{label: "One"}
|
|
two := FloatingActionItem{label: "Two"}
|
|
later := FloatingActionItem{label: "Later" enabled: false}
|
|
}
|
|
}
|
|
});
|
|
WidgetRef::script_from_value(vm, value)
|
|
});
|
|
let mut target = Target::new(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
(cx, root, target)
|
|
}
|
|
|
|
/// As `set_in_a_box`, with motion, and so slow that nothing arrives
|
|
/// while a test runs.
|
|
fn slow_set_in_a_box() -> (crate::PooledCx, WidgetRef, Target) {
|
|
let mut cx = test_cx();
|
|
let root = cx.with_vm(|vm| {
|
|
let value = crate::script_eval!(vm, {
|
|
use mod.widgets.*
|
|
View{
|
|
width: 600
|
|
height: 400
|
|
fab := FloatingAction{
|
|
dial: mod.widgets.SpeedDialLayout.Vertical
|
|
enter_secs: 60.0
|
|
exit_secs: 60.0
|
|
stagger_secs: 0.0
|
|
one := FloatingActionItem{label: "One"}
|
|
two := FloatingActionItem{label: "Two"}
|
|
later := FloatingActionItem{label: "Later" enabled: false}
|
|
}
|
|
}
|
|
});
|
|
WidgetRef::script_from_value(vm, value)
|
|
});
|
|
let mut target = Target::new(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
(cx, root, target)
|
|
}
|
|
|
|
fn with_set<R>(root: &WidgetRef, cx: &Cx, read: impl FnOnce(&FloatingAction) -> R) -> R {
|
|
let fab = root.widget(cx, ids!(fab));
|
|
let inner = fab.borrow::<FloatingAction>().expect("fab is a floating action");
|
|
read(&inner)
|
|
}
|
|
|
|
fn main_area_in(root: &WidgetRef, cx: &Cx) -> Area {
|
|
let fab = root.widget(cx, ids!(fab));
|
|
let inner = fab.borrow::<FloatingAction>().expect("fab is a floating action");
|
|
inner.main.area()
|
|
}
|
|
|
|
fn main_area(root: &WidgetRef, cx: &Cx) -> Area {
|
|
with_set(root, cx, |set| set.main.area())
|
|
}
|
|
|
|
fn action_area(root: &WidgetRef, cx: &Cx, i: usize) -> Area {
|
|
with_set(root, cx, |set| set.item_button_area(i))
|
|
}
|
|
|
|
fn is_open(root: &WidgetRef, cx: &Cx) -> bool {
|
|
with_set(root, cx, |set| set.open)
|
|
}
|
|
|
|
fn reports(actions: &Actions) -> Vec<FloatingActionAction> {
|
|
actions
|
|
.iter()
|
|
.filter_map(|action| action.as_widget_action())
|
|
.map(|action| action.cast::<FloatingActionAction>())
|
|
.filter(|action| *action != FloatingActionAction::None)
|
|
.collect()
|
|
}
|
|
|
|
/// Moves the key focus the way the event loop does between events.
|
|
fn settle_focus(cx: &mut Cx) {
|
|
cx.action(());
|
|
cx.handle_actions();
|
|
}
|
|
|
|
/// One event to `root`, then the actions it raised, round after round
|
|
/// as the event loop delivers them, with the focus settled after; the
|
|
/// set's own reports from all of it.
|
|
fn deliver(cx: &mut Cx, root: &WidgetRef, event: &Event) -> Vec<FloatingActionAction> {
|
|
let mut reported = Vec::new();
|
|
let mut actions = cx.capture_actions(|cx| root.handle_event(cx, event, &mut Scope::empty()));
|
|
while !actions.is_empty() {
|
|
reported.extend(reports(&actions));
|
|
let round = Event::Actions(actions);
|
|
actions = cx.capture_actions(|cx| root.handle_event(cx, &round, &mut Scope::empty()));
|
|
}
|
|
settle_focus(cx);
|
|
reported
|
|
}
|
|
|
|
fn press(abs: DVec2) -> Event {
|
|
Event::MouseDown(MouseDownEvent {
|
|
abs,
|
|
button: MouseButton::PRIMARY,
|
|
window_id: WindowId(1, 1),
|
|
modifiers: KeyModifiers::default(),
|
|
handled: std::cell::Cell::new(Area::Empty),
|
|
time: 0.0,
|
|
})
|
|
}
|
|
|
|
fn release(abs: DVec2) -> Event {
|
|
Event::MouseUp(MouseUpEvent {
|
|
abs,
|
|
button: MouseButton::PRIMARY,
|
|
window_id: WindowId(1, 1),
|
|
modifiers: KeyModifiers::default(),
|
|
time: 0.0,
|
|
})
|
|
}
|
|
|
|
fn move_to(abs: DVec2) -> Event {
|
|
Event::MouseMove(MouseMoveEvent {
|
|
abs,
|
|
lock_delta: DVec2::default(),
|
|
window_id: WindowId(1, 1),
|
|
modifiers: KeyModifiers::default(),
|
|
handled: std::cell::Cell::new(Area::Empty),
|
|
time: 0.0,
|
|
})
|
|
}
|
|
|
|
/// A set out over a page with an ordinary button beside it to take the
|
|
/// mouse and keep it. Pinned, so the press on that button does not put
|
|
/// the set away: dismissal is a different rule, and these two tests are
|
|
/// about the pointer.
|
|
fn a_set_and_a_grabber() -> (crate::PooledCx, WidgetRef, Target) {
|
|
let mut cx = test_cx();
|
|
let root = cx.with_vm(|vm| {
|
|
let value = crate::script_eval!(vm, {
|
|
use mod.widgets.*
|
|
View{
|
|
width: 600
|
|
height: 400
|
|
grabber := Button{width: 90. height: 32. text: "hold me"}
|
|
fab := FloatingAction{
|
|
pinned: true
|
|
dial: mod.widgets.SpeedDialLayout.Vertical
|
|
reduced_motion: true
|
|
one := FloatingActionItem{label: "One"}
|
|
two := FloatingActionItem{label: "Two"}
|
|
}
|
|
}
|
|
});
|
|
WidgetRef::script_from_value(vm, value)
|
|
});
|
|
let mut target = Target::new(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
target.draw(&mut cx, &root);
|
|
(cx, root, target)
|
|
}
|
|
|
|
/// The app-wide rule, at the place this set can break it. The actions
|
|
/// are read from RAW moves — they have to be, because a travelling
|
|
/// action's button hit-tests a place it has not reached yet — and a raw
|
|
/// reader is told nothing about who holds the pointer. So a mouse a
|
|
/// button elsewhere is holding used to light an action under it, and
|
|
/// offer a press that the button's release was always going to cancel.
|
|
#[test]
|
|
fn an_action_does_not_light_under_a_mouse_another_control_holds() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, _target) = a_set_and_a_grabber();
|
|
assert!(is_open(&root, &cx), "a pinned set is out from its first draw");
|
|
let over_one = action_area(&root, &cx, 0).rect(&cx).center();
|
|
|
|
// Nobody holding: the pointer lights the action it is over.
|
|
deliver(&mut cx, &root, &move_to(over_one));
|
|
assert_eq!(with_set(&root, &cx, |set| set.hot_item), Some(0), "lit for a free pointer");
|
|
|
|
// The button takes the mouse and keeps it until its release.
|
|
let grabber = root.widget(&cx, ids!(grabber)).area().rect(&cx).center();
|
|
deliver(&mut cx, &root, &press(grabber));
|
|
assert!(cx.fingers.any_areas_captured(), "the button holds the mouse");
|
|
deliver(&mut cx, &root, &move_to(over_one));
|
|
assert_eq!(
|
|
with_set(&root, &cx, |set| set.hot_item),
|
|
None,
|
|
"a pointer on loan to another control is not the set's to read"
|
|
);
|
|
|
|
// Let go, and it is the set's pointer again. A release drops the
|
|
// hold in the platform's own mouse-up bookkeeping, which these tests
|
|
// dispatch past — `unhandle` is that one step, by hand.
|
|
let grabber_area = root.widget(&cx, ids!(grabber)).area();
|
|
press(grabber).unhandle(&mut cx, &grabber_area);
|
|
deliver(&mut cx, &root, &move_to(over_one));
|
|
assert_eq!(
|
|
with_set(&root, &cx, |set| set.hot_item),
|
|
Some(0),
|
|
"lit again once the hold is let go: the gate is the hold, not a mood"
|
|
);
|
|
});
|
|
}
|
|
|
|
/// And the other half, which is what keeps the set working at all: the
|
|
/// set's OWN press is not somebody else's. Press, drag down the dial,
|
|
/// release is one gesture, and the mouse is held by the button for the
|
|
/// whole of it — so every action the hand travels over still lights up.
|
|
#[test]
|
|
fn the_sets_own_press_still_lights_the_actions_it_travels_over() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
let main = main_area(&root, &cx).rect(&cx).center();
|
|
deliver(&mut cx, &root, &press(main));
|
|
target.draw(&mut cx, &root);
|
|
assert!(cx.fingers.any_areas_captured(), "the button holds the mouse for the gesture");
|
|
let over_two = action_area(&root, &cx, 1).rect(&cx).center();
|
|
deliver(&mut cx, &root, &move_to(over_two));
|
|
assert_eq!(
|
|
with_set(&root, &cx, |set| set.hot_item),
|
|
Some(1),
|
|
"the set's own press is not a press elsewhere"
|
|
);
|
|
});
|
|
}
|
|
|
|
fn key(key_code: KeyCode, shift: bool) -> Event {
|
|
Event::KeyDown(KeyEvent {
|
|
key_code,
|
|
is_repeat: false,
|
|
modifiers: KeyModifiers { shift, ..KeyModifiers::default() },
|
|
time: 0.0,
|
|
})
|
|
}
|
|
|
|
/// Press the button, drag to an action and let go: the set is out on
|
|
/// the press, and the release picks, puts the set away and lets go of
|
|
/// the pointer. One gesture, not a click and then another.
|
|
#[test]
|
|
fn a_press_opens_the_set_and_its_release_on_an_action_picks() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
let main = main_area(&root, &cx).rect(&cx).center();
|
|
let reported = deliver(&mut cx, &root, &press(main));
|
|
assert!(reported.contains(&FloatingActionAction::Opened), "{reported:?}");
|
|
assert!(with_set(&root, &cx, |set| set.open && set.lock_pending), "out on the press, the grab owed to its release");
|
|
target.draw(&mut cx, &root);
|
|
let two = action_area(&root, &cx, 1).rect(&cx).center();
|
|
let reported = deliver(&mut cx, &root, &release(two));
|
|
assert!(reported.contains(&FloatingActionAction::Picked(live_id!(two))), "{reported:?}");
|
|
assert!(reported.contains(&FloatingActionAction::Closed), "{reported:?}");
|
|
assert!(!is_open(&root, &cx));
|
|
assert_eq!(cx.sweep_lock_area(), None, "the pick let go of the pointer");
|
|
});
|
|
}
|
|
|
|
/// Opened while the lock is still held from a dismissing press whose
|
|
/// release has not come, the set keeps that grab and still reads the
|
|
/// release as the end of its opening press.
|
|
#[test]
|
|
fn a_set_opened_under_a_held_lock_still_picks_on_the_release() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
let fab = root.widget(&cx, ids!(fab)).as_floating_action();
|
|
fab.open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let main = main_area(&root, &cx).rect(&cx).center();
|
|
deliver(&mut cx, &root, &press(dvec2(40.0, 40.0)));
|
|
assert!(!is_open(&root, &cx) && with_set(&root, &cx, |set| set.locked), "closed, the grab kept for the release");
|
|
let reported = deliver(&mut cx, &root, &press(main));
|
|
assert!(reported.contains(&FloatingActionAction::Opened), "{reported:?}");
|
|
assert!(with_set(&root, &cx, |set| set.opening_press), "the held grab does not make this press any less the opening one");
|
|
target.draw(&mut cx, &root);
|
|
let one = action_area(&root, &cx, 0).rect(&cx).center();
|
|
let reported = deliver(&mut cx, &root, &release(one));
|
|
assert!(reported.contains(&FloatingActionAction::Picked(live_id!(one))), "{reported:?}");
|
|
});
|
|
}
|
|
|
|
/// A press that misses the set puts it away and is eaten: the grab
|
|
/// stays until its release, so the dismissing click reaches nothing.
|
|
#[test]
|
|
fn a_press_outside_puts_the_set_away_and_its_release_reaches_nothing() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
root.widget(&cx, ids!(fab)).as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let held = cx.sweep_lock_area();
|
|
assert!(held.is_some(), "an open set holds the pointer");
|
|
let outside = dvec2(40.0, 40.0);
|
|
let reported = deliver(&mut cx, &root, &press(outside));
|
|
assert!(reported.contains(&FloatingActionAction::Closed), "{reported:?}");
|
|
assert_eq!(cx.sweep_lock_area(), held, "held until the release");
|
|
deliver(&mut cx, &root, &release(outside));
|
|
assert_eq!(cx.sweep_lock_area(), None);
|
|
});
|
|
}
|
|
|
|
/// Return on the button brings the set out with the first action
|
|
/// holding the keyboard; Escape puts it away and hands the keyboard
|
|
/// back to the button.
|
|
#[test]
|
|
fn return_opens_on_the_first_action_and_escape_gives_the_button_back_the_keyboard() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
let focus = main_area(&root, &cx);
|
|
cx.set_key_focus(focus);
|
|
settle_focus(&mut cx);
|
|
let reported = deliver(&mut cx, &root, &key(KeyCode::ReturnKey, false));
|
|
assert!(reported.contains(&FloatingActionAction::Opened), "{reported:?}");
|
|
target.draw(&mut cx, &root);
|
|
settle_focus(&mut cx);
|
|
assert!(cx.has_key_focus(action_area(&root, &cx, 0)), "the first action has the keyboard");
|
|
let reported = deliver(&mut cx, &root, &key(KeyCode::Escape, false));
|
|
assert!(reported.contains(&FloatingActionAction::Closed), "{reported:?}");
|
|
assert!(cx.has_key_focus(main_area(&root, &cx)), "the button has it back");
|
|
});
|
|
}
|
|
|
|
/// Tab and Shift+Tab go round the set's stops without resting on a
|
|
/// disabled action, which Return could not pick.
|
|
#[test]
|
|
fn tab_passes_over_a_disabled_action() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
root.widget(&cx, ids!(fab)).as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let two = action_area(&root, &cx, 1);
|
|
cx.set_key_focus(two);
|
|
settle_focus(&mut cx);
|
|
deliver(&mut cx, &root, &key(KeyCode::Tab, false));
|
|
assert!(!cx.has_key_focus(action_area(&root, &cx, 2)), "Tab rested on Later");
|
|
assert!(cx.has_key_focus(main_area(&root, &cx)), "past Later to the button");
|
|
deliver(&mut cx, &root, &key(KeyCode::Tab, true));
|
|
assert!(cx.has_key_focus(two), "and back past it again");
|
|
});
|
|
}
|
|
|
|
/// Pinned while it is out, a set lets go of the grab and the trap it
|
|
/// took for the open: a pinned set never closes to give them back.
|
|
#[test]
|
|
fn pinning_an_open_set_lets_go_of_the_pointer() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
let mut fab = root.widget(&cx, ids!(fab));
|
|
fab.as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
assert!(cx.sweep_lock_area().is_some());
|
|
script_apply_eval!(cx, fab, {pinned: true});
|
|
assert!(is_open(&root, &cx), "a pinned set stays out");
|
|
assert!(with_set(&root, &cx, |set| !set.locked && !set.focus_trap.is_active()));
|
|
assert_eq!(cx.sweep_lock_area(), None, "and holds no grab");
|
|
});
|
|
}
|
|
|
|
/// An overlay that opened over the set holds the innermost lock. The
|
|
/// set's own lifting and retaking around its children must not put its
|
|
/// lock back on top of that one.
|
|
#[test]
|
|
fn a_lock_under_another_overlay_stays_under_it() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
root.widget(&cx, ids!(fab)).as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
// Any drawn area that is not the button stands in for the overlay.
|
|
let above = action_area(&root, &cx, 0);
|
|
cx.sweep_lock(above);
|
|
root.handle_event(&mut cx, &Event::Signal, &mut Scope::empty());
|
|
assert_eq!(cx.sweep_lock_area(), Some(above), "the overlay above is still on top");
|
|
});
|
|
}
|
|
|
|
/// A container half scrolled under a view's top edge hides its set along
|
|
/// that edge only: the set's actions reach past the container, and its
|
|
/// container's other edges must not cut them off.
|
|
#[test]
|
|
fn only_the_edges_that_hide_the_container_cut_the_set() {
|
|
crate::on_test_cx(|| {
|
|
let window = Rect {
|
|
pos: dvec2(0.0, 0.0),
|
|
size: dvec2(800.0, 600.0),
|
|
};
|
|
let host = Rect {
|
|
pos: dvec2(100.0, 20.0),
|
|
size: dvec2(200.0, 200.0),
|
|
};
|
|
let visible = Rect {
|
|
pos: dvec2(100.0, 60.0),
|
|
size: dvec2(200.0, 160.0),
|
|
};
|
|
assert_eq!(cut_sides(visible, host, window), Rect {
|
|
pos: dvec2(0.0, 60.0),
|
|
size: dvec2(800.0, 540.0),
|
|
});
|
|
assert_eq!(cut_sides(host, host, window), window, "nothing hidden, nothing cut");
|
|
let gone = Rect {
|
|
pos: dvec2(100.0, 60.0),
|
|
size: dvec2(200.0, 0.0),
|
|
};
|
|
assert_eq!(cut_sides(gone, host, window).size, dvec2(0.0, 0.0), "hidden entirely, nothing drawn");
|
|
});
|
|
}
|
|
|
|
/// A scroll moves the container under the edge of the view it scrolls
|
|
/// in, and not the edge: on the draw after a scroll step, before the
|
|
/// container is read again, the set is still cut where that edge is.
|
|
#[test]
|
|
fn a_scrolled_container_is_cut_where_the_view_edge_is() {
|
|
crate::on_test_cx(|| {
|
|
let window = Rect {
|
|
pos: dvec2(0.0, 0.0),
|
|
size: dvec2(800.0, 600.0),
|
|
};
|
|
let host_then = Rect {
|
|
pos: dvec2(100.0, 20.0),
|
|
size: dvec2(200.0, 200.0),
|
|
};
|
|
let visible_then = Rect {
|
|
pos: dvec2(100.0, 60.0),
|
|
size: dvec2(200.0, 160.0),
|
|
};
|
|
let scrolled = Rect {
|
|
pos: dvec2(100.0, -60.0),
|
|
size: dvec2(200.0, 200.0),
|
|
};
|
|
let (visible, open) = cut_now(visible_then, host_then, scrolled, window);
|
|
assert_eq!(open, Rect {
|
|
pos: dvec2(0.0, 60.0),
|
|
size: dvec2(800.0, 540.0),
|
|
}, "the view's edge has not moved");
|
|
assert_eq!(visible, Rect {
|
|
pos: dvec2(100.0, 60.0),
|
|
size: dvec2(200.0, 80.0),
|
|
});
|
|
let (visible, open) = cut_now(host_then, host_then, scrolled, window);
|
|
assert_eq!(open, window, "nothing hidden then, nothing cut now");
|
|
assert_eq!(visible, Rect {
|
|
pos: dvec2(100.0, 0.0),
|
|
size: dvec2(200.0, 140.0),
|
|
});
|
|
});
|
|
}
|
|
|
|
/// The first draw after a scroll step slides a container under the top
|
|
/// edge of the view it scrolls in already cuts its set at that edge,
|
|
/// with no event in between to read the container again. The set used
|
|
/// to be cut only from the next read, so that one draw showed its
|
|
/// button over whatever sat above the view: a toolbar, in the catalogue.
|
|
#[test]
|
|
fn a_container_scrolled_under_the_edge_is_cut_on_the_very_next_draw() {
|
|
crate::on_test_cx(|| {
|
|
let mut cx = test_cx();
|
|
let root = cx.with_vm(|vm| {
|
|
let value = crate::script_eval!(vm, {
|
|
use mod.prelude.widgets.*
|
|
use mod.widgets.*
|
|
View{
|
|
width: 600
|
|
height: 400
|
|
flow: Down
|
|
bar := View{width: Fill height: 60}
|
|
scroller := View{
|
|
width: Fill
|
|
height: 300
|
|
flow: Down
|
|
box := View{
|
|
width: 400
|
|
height: 200
|
|
margin: Inset{top: 20}
|
|
fab := FloatingAction{
|
|
anchor: mod.widgets.FloatingAnchor.TopLeft
|
|
dial: mod.widgets.SpeedDialLayout.Vertical
|
|
reduced_motion: true
|
|
one := FloatingActionItem{label: "One"}
|
|
two := FloatingActionItem{label: "Two"}
|
|
}
|
|
}
|
|
filler := View{width: Fill height: 800}
|
|
}
|
|
}
|
|
});
|
|
WidgetRef::script_from_value(vm, value)
|
|
});
|
|
let mut target = Target::new(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
root.handle_event(&mut cx, &Event::Signal, &mut Scope::empty());
|
|
target.draw(&mut cx, &root);
|
|
// Under the 60 point bar.
|
|
let edge = 60.0;
|
|
let face = main_area_in(&root, &cx).clipped_rect(&cx);
|
|
assert!(face.pos.y >= edge, "nothing scrolled yet, and the button is below the edge: {face:?}");
|
|
|
|
for step in 1..=3 {
|
|
root.widget(&cx, ids!(scroller)).set_scroll_pos(&mut cx, dvec2(0.0, 20.0 * step as f64));
|
|
root.redraw(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let face = main_area_in(&root, &cx).clipped_rect(&cx);
|
|
assert!(
|
|
face.size.y <= 0.0 || face.pos.y >= edge - 0.5,
|
|
"scroll step {step}: the button is drawn from {} above the view's edge at {edge}",
|
|
face.pos.y
|
|
);
|
|
root.handle_event(&mut cx, &Event::Signal, &mut Scope::empty());
|
|
}
|
|
});
|
|
}
|
|
|
|
/// While the actions are still on their way, a press and release where
|
|
/// one of them will rest picks it, though its face is still drawn next
|
|
/// to the button: the hand aims at where the action is going.
|
|
#[test]
|
|
fn a_press_where_an_action_will_rest_picks_it_while_it_is_on_its_way() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = slow_set_in_a_box();
|
|
root.widget(&cx, ids!(fab)).as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let (resting, drawn) = with_set(&root, &cx, |set| (set.plan_centre + set.plan.offsets[1], set.item_button_area(1)));
|
|
let drawn = drawn.rect(&cx).center();
|
|
assert!((drawn - resting).length() > 40.0, "still near the button: drawn at {drawn:?}, resting at {resting:?}");
|
|
assert!(with_set(&root, &cx, |set| set.travelling(cx.seconds_since_app_start())));
|
|
let reported = deliver(&mut cx, &root, &press(resting));
|
|
assert!(!reported.contains(&FloatingActionAction::Closed), "a press on a resting place is not a press outside: {reported:?}");
|
|
let reported = deliver(&mut cx, &root, &release(resting));
|
|
assert!(reported.contains(&FloatingActionAction::Picked(live_id!(two))), "{reported:?}");
|
|
});
|
|
}
|
|
|
|
/// The plus turns on the set's own legs: out on the enter ease over the
|
|
/// enter time, and when the set is put away before the turn got far, back
|
|
/// from where it had got to on the exit ease. The button is told it is
|
|
/// open at once, so its own track has nothing left to play over the top.
|
|
#[test]
|
|
fn the_glyph_turns_on_the_same_eases_as_the_set() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = slow_set_in_a_box();
|
|
let fab = root.widget(&cx, ids!(fab)).as_floating_action();
|
|
fab.open(&mut cx);
|
|
with_set(&root, &cx, |set| {
|
|
assert_eq!(set.turn, Leg { from: 0.0, to: 1.0, delay: 0.0, secs: set.enter_secs });
|
|
assert_eq!(set.ease_toward(set.turn.to), set.enter_ease);
|
|
let button = set.main.borrow::<Button>().expect("the main face is a button");
|
|
assert!(button.open(), "the button's track is at its end");
|
|
});
|
|
target.draw(&mut cx, &root);
|
|
fab.close(&mut cx);
|
|
with_set(&root, &cx, |set| {
|
|
assert_eq!(set.turn.to, 0.0);
|
|
assert_eq!(set.ease_toward(set.turn.to), set.exit_ease);
|
|
assert!(set.turn.from < 0.5, "back from where it had got to, not from the cross: {:?}", set.turn);
|
|
assert!(set.moving(cx.seconds_since_app_start()), "the actions are still on their way back");
|
|
});
|
|
});
|
|
}
|
|
|
|
/// The set's own actions travel on its own eases, the theme's tokens by
|
|
/// default: out along `enter_ease`, and, put away part way, back along
|
|
/// `exit_ease` from wherever each had got to.
|
|
#[test]
|
|
fn the_actions_travel_on_the_sets_own_eases() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, _target) = slow_set_in_a_box();
|
|
let fab = root.widget(&cx, ids!(fab));
|
|
fab.as_floating_action().open(&mut cx);
|
|
{
|
|
let mut set = fab.borrow_mut::<FloatingAction>().expect("a floating action");
|
|
let (start, secs, ease) = (set.motion_started, set.enter_secs, set.enter_ease);
|
|
for x in [0.25, 0.5, 0.75] {
|
|
let at = set.positions(start + x * secs);
|
|
assert!(at.iter().all(|v| (v - ease.map(x)).abs() < 1e-9), "out along the enter ease at {x}: {at:?}");
|
|
}
|
|
// Half the enter time gone, as far as the set's clock can tell.
|
|
set.motion_started -= secs * 0.5;
|
|
}
|
|
fab.as_floating_action().close(&mut cx);
|
|
let mut set = fab.borrow_mut::<FloatingAction>().expect("a floating action");
|
|
let (start, enter, exit) = (set.motion_started, set.enter_ease, set.exit_ease);
|
|
let legs = set.legs.clone();
|
|
assert_eq!(legs.len(), 3);
|
|
for leg in &legs {
|
|
assert!((leg.from - enter.map(0.5)).abs() < 1e-3, "back from half way out: {leg:?}");
|
|
assert_eq!((leg.to, leg.delay), (0.0, 0.0), "turned round at once: {leg:?}");
|
|
}
|
|
for x in [0.25, 0.5, 0.75] {
|
|
let at = set.positions(start + x * legs[0].secs);
|
|
for (v, leg) in at.iter().zip(&legs) {
|
|
assert!((v - leg.from * (1.0 - exit.map(x))).abs() < 1e-9, "back along the exit ease at {x}: {v}");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// A hidden action is left out of the set: the others close up and
|
|
/// take its place, a press on that place picks the one there now, and
|
|
/// the hidden one is not drawn.
|
|
#[test]
|
|
fn a_hidden_action_leaves_no_gap() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
root.widget(&cx, ids!(fab)).as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
assert!(has_size(action_area(&root, &cx, 1).rect(&cx)), "Two is drawn while it is shown");
|
|
root.widget(&cx, ids!(two)).set_visible(&mut cx, false);
|
|
target.draw(&mut cx, &root);
|
|
with_set(&root, &cx, |set| {
|
|
assert_eq!(set.shown_items(), vec![0, 2]);
|
|
assert_eq!(
|
|
set.plan.offsets,
|
|
dial_offsets(set.anchor, set.dial, 2, set.size, set.item_size, set.item_gap, set.radial_radius, set.radial_max_radius)
|
|
);
|
|
assert_eq!(set.item_at(set.plan_centre + set.plan.offsets[1]), Some(2), "Later rests where Two did");
|
|
});
|
|
assert!(!has_size(action_area(&root, &cx, 1).rect(&cx)), "Two is not drawn once hidden");
|
|
});
|
|
}
|
|
|
|
/// Drawn with the widget's own furthest radius, eight from the corner
|
|
/// of a box with room for one wide arc rest on two arcs inside that
|
|
/// radius, and everything that reads the layout reads those two arcs:
|
|
/// the arrow keys walk the inner arc and then the outer one, the chip of
|
|
/// an action on the inner arc is carried out past the outer, and a press
|
|
/// and release on each resting place picks the action resting there.
|
|
#[test]
|
|
fn a_set_held_inside_its_furthest_radius_is_walked_labelled_and_picked_where_it_rests() {
|
|
crate::on_test_cx(|| {
|
|
let mut cx = test_cx();
|
|
let root = cx.with_vm(|vm| {
|
|
let value = crate::script_eval!(vm, {
|
|
use mod.widgets.*
|
|
View{
|
|
width: 600
|
|
height: 400
|
|
fab := FloatingAction{
|
|
reduced_motion: true
|
|
one := FloatingActionItem{label: "One"}
|
|
two := FloatingActionItem{label: "Two"}
|
|
three := FloatingActionItem{label: "Three"}
|
|
four := FloatingActionItem{label: "Four"}
|
|
five := FloatingActionItem{label: "Five"}
|
|
six := FloatingActionItem{label: "Six"}
|
|
seven := FloatingActionItem{label: "Seven"}
|
|
eight := FloatingActionItem{label: "Eight"}
|
|
}
|
|
}
|
|
});
|
|
WidgetRef::script_from_value(vm, value)
|
|
});
|
|
let ids = [
|
|
live_id!(one),
|
|
live_id!(two),
|
|
live_id!(three),
|
|
live_id!(four),
|
|
live_id!(five),
|
|
live_id!(six),
|
|
live_id!(seven),
|
|
live_id!(eight),
|
|
];
|
|
let mut target = Target::new(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let fab = root.widget(&cx, ids!(fab)).as_floating_action();
|
|
fab.open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let (max_r, plan, centre) = with_set(&root, &cx, |set| (set.radial_max_radius, set.plan.clone(), set.plan_centre));
|
|
assert_eq!(max_r, MAX_R, "the widget's own furthest radius");
|
|
assert_eq!(plan.rows, vec![0, 0, 0, 0, 0, 1, 1, 1], "{:?}", plan.radii);
|
|
assert!(plan.offsets.iter().all(|offset| offset.length() <= max_r + 1e-9), "{:?}", plan.offsets);
|
|
|
|
let origin = dvec2(0.0, 0.0);
|
|
let arrows = [
|
|
(KeyCode::ArrowUp, dvec2(0.0, -1.0)),
|
|
(KeyCode::ArrowDown, dvec2(0.0, 1.0)),
|
|
(KeyCode::ArrowLeft, dvec2(-1.0, 0.0)),
|
|
(KeyCode::ArrowRight, dvec2(1.0, 0.0)),
|
|
];
|
|
let focus = action_area(&root, &cx, 0);
|
|
cx.set_key_focus(focus);
|
|
settle_focus(&mut cx);
|
|
for at in 0..7 {
|
|
let (code, _) = arrows
|
|
.iter()
|
|
.find(|(_, dir)| arc_walk_target(&plan.offsets, 5, Some(at), origin, *dir) == Some(at + 1))
|
|
.unwrap_or_else(|| panic!("no arrow moves on from {at}"));
|
|
deliver(&mut cx, &root, &key(*code, false));
|
|
assert!(cx.has_key_focus(action_area(&root, &cx, at + 1)), "{code:?} from {at} did not move on");
|
|
}
|
|
|
|
let focus = action_area(&root, &cx, 0);
|
|
cx.set_key_focus(focus);
|
|
settle_focus(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let chip = root
|
|
.widget(&cx, ids!(one))
|
|
.borrow::<FloatingActionItem>()
|
|
.and_then(|item| item.chip_rect)
|
|
.expect("the focused action shows its chip");
|
|
assert!(
|
|
chip.pos.y + chip.size.y <= centre.y - plan.radii[1] - ITEM * 0.5 + 1e-6,
|
|
"the chip at {chip:?} lies over the outer arc {:?}",
|
|
plan.radii
|
|
);
|
|
|
|
for (k, id) in ids.iter().enumerate() {
|
|
if !is_open(&root, &cx) {
|
|
fab.open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
}
|
|
let resting = centre + plan.offsets[k];
|
|
deliver(&mut cx, &root, &press(resting));
|
|
let reported = deliver(&mut cx, &root, &release(resting));
|
|
assert!(reported.contains(&FloatingActionAction::Picked(*id)), "action {k}: {reported:?}");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Shown for the first time while the set is out, an action that was
|
|
/// never drawn has no area to redraw from; the set it joins is asked to
|
|
/// redraw, or the action would not appear until something else redrew.
|
|
#[test]
|
|
fn an_action_shown_for_the_first_time_redraws_its_set() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
root.widget(&cx, ids!(two)).set_visible(&mut cx, false);
|
|
root.widget(&cx, ids!(fab)).as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
assert!(action_area(&root, &cx, 1).is_empty(), "Two was never drawn");
|
|
let list = with_set(&root, &cx, |set| set.draw_list.as_ref().unwrap().id());
|
|
cx.new_draw_event.draw_lists.clear();
|
|
cx.new_draw_event.redraw_all = false;
|
|
root.widget(&cx, ids!(two)).set_visible(&mut cx, true);
|
|
assert!(cx.new_draw_event.draw_lists.contains(&list), "the set was asked to redraw");
|
|
target.draw(&mut cx, &root);
|
|
assert!(has_size(action_area(&root, &cx, 1).rect(&cx)), "and Two is drawn");
|
|
});
|
|
}
|
|
|
|
/// Hidden, the set is put away and its list comes off the screen: a
|
|
/// view does not draw a hidden child, and the window's overlay drops a
|
|
/// list whose parent was redrawn without it.
|
|
#[test]
|
|
fn a_hidden_set_comes_off_the_screen() {
|
|
crate::on_test_cx(|| {
|
|
let (mut cx, root, mut target) = set_in_a_box();
|
|
let fab = root.widget(&cx, ids!(fab));
|
|
fab.as_floating_action().open(&mut cx);
|
|
target.draw(&mut cx, &root);
|
|
let list = with_set(&root, &cx, |set| set.draw_list.as_ref().unwrap().id());
|
|
let overlay = target.overlay.draw_list.id();
|
|
let shown = |cx: &Cx| {
|
|
let items = &cx.draw_lists[overlay].draw_items;
|
|
(0..items.len()).any(|i| items[i].sub_list() == Some(list))
|
|
};
|
|
assert!(shown(&cx), "drawn on the window's overlay");
|
|
fab.set_visible(&mut cx, false);
|
|
assert!(!is_open(&root, &cx));
|
|
assert_eq!(cx.sweep_lock_area(), None);
|
|
target.draw(&mut cx, &root);
|
|
assert!(!shown(&cx), "the overlay no longer shows it");
|
|
});
|
|
}
|
|
}
|