Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

features/drag.rs

//! `on_drag`: a press-move-release on a node arrives as `drag` events —
//! `start` at the press, `move` once the pointer has travelled past the
//! slop, `end` at the release — with the pointer in viewport px (`x`,
//! `y`), the displacement *since the press* (`dx`, `dy`, absolute against
//! the press point so no delta is ever summed), and the node's `parent`
//! rect, so a handler can turn a position into a fraction of its
//! container without a geometry query. The core captures the pointer for
//! the drag: it keeps reporting after the pointer leaves the node, and
//! the release lands wherever it lands.
//!
//! Two things it is for: a **slider** whose value is what it was at the
//! press plus `dx` over the track — drawn by hand here because the drag
//! is the subject; an app's slider is the stock one
//! (`widgets::slider`, ADR 0034), which does this and the keys — and a
//! **card** whose float offset is
//! where it was at the press plus the displacement, kept inside the stage
//! (its own size from `on_layout`, the stage's from the event's `parent`).
//! A drag that started on the card is the card's until it ends, whatever
//! the pointer crosses.
//!
//! The cursor is declared, not derived: an `on_drag` node with no `cursor`
//! is the plain arrow, so both declare `grab` at rest and `grabbing`
//! while their drag runs — the model already knows which drag is on, and
//! the shape is one more thing the view says from it. The core holds
//! whichever shape the dragged node declared for as long as the pointer
//! is captured, wherever it goes.
//!
//! Run: cargo run -p kui-native --example drag [-- --headless]

use kui_devtools::{Drive, Example};
use kui_native::{
    Align, App, Core, CursorShape, FloatConfig, NodeSpec, Sizing, TextStyle, Ui, UiEvent, Value,
};

const TRACK_W: f32 = 320.0;

#[derive(Default)]
struct Drag {
    /// The slider's value, 0..1.
    value: f32,
    /// The card's offset in its stage.
    card: (f32, f32),
    /// What was true at the press, for the displacement to add to.
    at_press: (f32, (f32, f32)),
    /// The card's own size, from its `on_layout`, so it stays inside the
    /// stage whose rect the drag event carries.
    card_size: (f32, f32),
    dragging: Option<String>,
    log: String,
}

impl App for Drag {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        // The open hand over a handle at rest, the closed one while its
        // drag runs: `dragging` names which.
        let hand = |tag: &str| {
            if self.dragging.as_deref() == Some(tag) {
                CursorShape::Grabbing
            } else {
                CursorShape::Grab
            }
        };
        ui.with(
            NodeSpec::column()
                .fill()
                .pad(24.0)
                .gap(18.0)
                .cross_align(Align::Start),
            |ui| {
                ui.text(
                    &format!("a slider: the drag's x is the value · {:.0}%", self.value * 100.0),
                    TextStyle::new(12.0).color(t.muted),
                );
                // The track owns the drag, and its fill is the value: a
                // drag anywhere on it moves the value by the travel.
                ui.with_keyed(
                    "track",
                    NodeSpec::row()
                        .size(TRACK_W, 24.0)
                        .bg(t.sunken)
                        .radius(12.0)
                        .on_drag("slider")
                        .cursor(hand("slider")),
                    |ui| {
                        ui.leaf(
                            NodeSpec::row()
                                .width(Sizing::Percent(self.value))
                                .grow_height()
                                .bg(t.accent)
                                .radius(12.0));
                    },
                );

                ui.text(
                    "a card: the drag's steps move its float offset · the pointer is captured until the release",
                    TextStyle::new(12.0).color(t.muted),
                );
                ui.with_keyed(
                    "stage",
                    NodeSpec::row()
                        .size(420.0, 200.0)
                        .bg(t.surface)
                        .radius(10.0)
                        .border(1.0, t.border),
                    |ui| {
                        let grabbing = self.dragging.as_deref() == Some("card");
                        ui.with_keyed(
                            "card",
                            NodeSpec::column()
                                .float(
                                    FloatConfig::parent()
                                        .inside(Align::Start, Align::Start)
                                        .offset(self.card.0, self.card.1),
                                )
                                .pad_xy(16.0, 12.0)
                                .bg(if grabbing { t.accent_soft } else { t.raised })
                                .radius(8.0)
                                .border(1.0, if grabbing { t.accent } else { t.border })
                                .on_drag("card")
                                .cursor(hand("card"))
                                .on_layout("card"),
                            |ui| {
                                ui.text("drag me", TextStyle::new(14.0));
                                ui.text(
                                    &format!("at {:.0}, {:.0}", self.card.0, self.card.1),
                                    TextStyle::new(11.0).color(t.muted),
                                );
                            },
                        );
                    },
                );
                ui.text(&self.log, TextStyle::new(12.0).color(t.faint));
            },
        );
    }

    fn on_event(&mut self, ev: UiEvent) {
        let num = |k: &str| ev.payload.get(k).and_then(Value::as_float).unwrap_or(0.0) as f32;
        match ev.kind() {
            Some("layout") => {
                self.card_size = (num("w"), num("h"));
                return;
            }
            Some("drag") => {}
            _ => return,
        }
        let phase = ev.payload.get_str("phase").unwrap_or("");
        let tag = ev.payload.get_str("tag").unwrap_or("");
        self.log = format!(
            "{tag} {phase} x {:.0} y {:.0} dx {:.0} dy {:.0}",
            num("x"),
            num("y"),
            num("dx"),
            num("dy")
        );
        match (tag, phase) {
            ("slider", "start") => {
                self.dragging = Some("slider".into());
                self.at_press.0 = self.value;
            }
            ("slider", "end") => self.dragging = None,
            ("slider", _) => {
                // The value at the press plus the displacement over the
                // track's width: absolute, so nothing accumulates.
                self.value = (self.at_press.0 + num("dx") / TRACK_W).clamp(0.0, 1.0);
            }
            ("card", "start") => {
                self.dragging = Some("card".into());
                self.at_press.1 = self.card;
            }
            ("card", "move") => {
                // The offset at the press plus the displacement, kept
                // inside the stage: the event carries the parent's rect,
                // and the card's own size came from its `on_layout`.
                let (ox, oy) = self.at_press.1;
                let parent = ev.payload.get("parent");
                let dim = |k: &str| {
                    parent
                        .and_then(|p| p.get(k))
                        .and_then(Value::as_float)
                        .unwrap_or(0.0) as f32
                };
                let max_x = (dim("w") - self.card_size.0).max(0.0);
                let max_y = (dim("h") - self.card_size.1).max(0.0);
                self.card = (
                    (ox + num("dx")).clamp(0.0, max_x),
                    (oy + num("dy")).clamp(0.0, max_y),
                );
            }
            ("card", "end") => self.dragging = None,
            _ => {}
        }
    }
}

impl Example for Drag {
    fn window(&self) -> kui_devtools::Window {
        kui_devtools::Window::default().size(500.0, 380.0)
    }

    /// A drag along the track moves the value by the travel over the
    /// track's width; a drag across the card moves it by the pointer's
    /// travel and keeps reporting after the pointer has left it.
    fn headless(&mut self, core: &mut Core) -> Result<(), String> {
        let mut d = Drive::new(core, 500.0, 380.0);
        d.frame(self);
        let track = d.key_of("track").ok_or("no track")?;
        let r = d.rect_of(track).ok_or("no track rect")?;
        let (x0, y0) = (r.x + 10.0, r.y + 12.0);
        d.input(
            self,
            kui_native::InputEvent::CursorMoved(kui_native::Vec2::new(x0, y0)),
        );
        d.check(
            d.core.cursor_shape() == CursorShape::Grab,
            "the track declares the open hand at rest",
        )?;
        let evs = d.input(self, kui_native::InputEvent::mouse_down(1));
        d.check(
            evs.iter()
                .any(|e| e.payload.get_str("phase") == Some("start")),
            "a press on the track starts a drag",
        )?;
        d.frame(self);
        d.check(
            d.core.cursor_shape() == CursorShape::Grabbing,
            "and the frame after the start declares the closed one",
        )?;
        d.input(
            self,
            kui_native::InputEvent::CursorMoved(kui_native::Vec2::new(x0 + TRACK_W * 0.5, y0)),
        );
        d.check(
            (self.value - 0.5).abs() < 0.02,
            "half the track's width of travel is half the value",
        )?;
        d.input(
            self,
            kui_native::InputEvent::CursorMoved(kui_native::Vec2::new(x0 + TRACK_W * 0.9, y0)),
        );
        d.check(
            (self.value - 0.9).abs() < 0.02,
            "and the value follows the pointer, not the steps",
        )?;
        d.input(self, kui_native::InputEvent::mouse_up());
        d.frame(self);
        d.check(
            d.core.cursor_shape() == CursorShape::Grab,
            "the release opens the hand again",
        )?;

        let card = d.key_of("card").ok_or("no card")?;
        let c = d.rect_of(card).ok_or("no card rect")?;
        let (x0, y0) = (c.x + c.w / 2.0, c.y + c.h / 2.0);
        d.input(
            self,
            kui_native::InputEvent::CursorMoved(kui_native::Vec2::new(x0, y0)),
        );
        d.input(self, kui_native::InputEvent::mouse_down(1));
        d.check(
            self.dragging.is_some(),
            "a press on the card starts its drag",
        )?;
        d.input(
            self,
            kui_native::InputEvent::CursorMoved(kui_native::Vec2::new(x0 + 60.0, y0 + 30.0)),
        );
        d.frame(self);
        d.check(
            (self.card.0 - 60.0).abs() < 1.0 && (self.card.1 - 30.0).abs() < 1.0,
            "a move of 60,30 moves the card by 60,30",
        )?;
        // Far outside the card and the stage: the capture keeps the drag,
        // and the card stops at the stage's edge.
        d.input(
            self,
            kui_native::InputEvent::CursorMoved(kui_native::Vec2::new(x0 + 600.0, y0 + 30.0)),
        );
        d.frame(self);
        d.check(
            self.card.0 > 200.0 && self.card.0 + self.card_size.0 <= 420.0 + 0.5,
            "the pointer leaving the card does not end the drag, and the card stops at the edge",
        )?;
        d.input(self, kui_native::InputEvent::mouse_up());
        d.check(
            self.dragging.is_none(),
            "the release ends it, wherever it lands",
        )
    }
}

kui_devtools::main!(Drag::default());