Skip to main content

ravenrs_widgets/scheduler/
view.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{BTreeSet, HashMap};
3use std::fmt;
4use std::rc::Rc;
5
6use ravenrs_core::{Border, CursorIcon, Insets, Key, Point, Rect, Role};
7use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
8use ravenrs_reactive::{Binding, State};
9use ravenrs_style::{Semantic, TextStyle, Theme, Weight};
10
11use super::time::{TimeAxis, TimePoint, TimeScale, assign_lanes};
12use crate::context::{BuildCx, Drag, DragPhase, NodeId, Paint, Placement, QuadPaint, Semantics};
13use crate::display::Tone;
14use crate::overlay::{Entry, MenuList};
15use crate::{Button, ButtonSize, ButtonStyle, IconButton, IconName, Labels, MenuItem, Text, View, util};
16
17const LANE_HEIGHT: f32 = 30.0;
18/// A lane whose bars have a second line.
19const TALL_LANE_HEIGHT: f32 = 48.0;
20const CARD_BAR_GAP: f32 = 2.0;
21/// A drag this close to an edge of the timeline scrolls it.
22const AUTO_SCROLL_EDGE: f32 = 32.0;
23const REVEAL_MARGIN: f32 = 16.0;
24const AUTO_SCROLL_STEP: f32 = 12.0;
25const AUTO_SCROLL_EVERY: f64 = 1.0 / 60.0;
26/// How much of the row under the pointer is tinted.
27const ROW_HOVER_TINT: f32 = 0.03;
28/// How far a pinch goes before it zooms one step.
29const PINCH_STEP: f32 = 0.2;
30const AVATAR_SIZE: f32 = 28.0;
31const AVATAR_GAP: f32 = 10.0;
32const TINT_REST: f32 = 0.13;
33const TINT_HOVERED: f32 = 0.22;
34const TINT_SELECTED: f32 = 0.3;
35const TINT_DONE: f32 = 0.06;
36const TINT_TEXT_TOWARD_INK: f32 = 0.4;
37const STRIPE_WIDTH: f32 = 3.0;
38const STRIPE_INSET: f32 = 3.0;
39/// How much of today's column is tinted.
40const TODAY_TINT: f32 = 0.05;
41/// Where a name starts in a row taller than its bars: in line with the first bar.
42const NAME_TOP: f32 = 12.0;
43const ROW_PADDING: f32 = 4.0;
44const GROUP_HEIGHT: f32 = 30.0;
45const HEADER_ROW_HEIGHT: f32 = 26.0;
46const MIN_BAR_WIDTH: f32 = 6.0;
47const HANDLE_WIDTH: f32 = 6.0;
48/// Rows and ticks built beyond the edges, so a quick scroll shows no gap.
49const OVERSCAN: f32 = 200.0;
50
51/// A row of a [`Scheduler`]: a person, a room, a machine, an account.
52#[derive(Clone, Debug, PartialEq, Eq)]
53#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
54pub struct Resource {
55    /// What events refer to.
56    pub id: u64,
57    /// What the row is called.
58    pub name: String,
59    /// A second line under the name.
60    pub subtitle: Option<String>,
61    /// The collapsible group it goes under; rows with the same group stay together.
62    pub group: Option<String>,
63}
64
65#[cfg_attr(feature = "live", ravenrs_live::expose)]
66impl Resource {
67    /// A row with this name.
68    pub fn new(id: u64, name: impl Into<String>) -> Self {
69        Self { id, name: name.into(), subtitle: None, group: None }
70    }
71
72    /// Sets the second line.
73    pub fn subtitle(mut self, text: impl Into<String>) -> Self {
74        self.subtitle = Some(text.into());
75        self
76    }
77
78    /// Puts it in a group.
79    pub fn group(mut self, name: impl Into<String>) -> Self {
80        self.group = Some(name.into());
81        self
82    }
83}
84
85/// A bar on a [`Scheduler`]. One with no length shows as a milestone.
86#[derive(Clone, Debug, PartialEq, Eq)]
87#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
88pub struct SchedulerEvent {
89    /// Given back when it changes.
90    pub id: u64,
91    /// The row it is on.
92    pub resource: u64,
93    /// When it begins.
94    pub start: TimePoint,
95    /// When it ends.
96    pub end: TimePoint,
97    /// What it is.
98    pub title: String,
99    /// Its color; say what it means in the title or next to the scheduler too.
100    pub tone: Tone,
101    /// It already happened, so it stands back.
102    pub done: bool,
103    /// It cannot be moved or resized.
104    pub locked: bool,
105    /// Its length can change.
106    pub resizable: bool,
107}
108
109#[cfg_attr(feature = "live", ravenrs_live::expose)]
110impl SchedulerEvent {
111    /// A bar on this row, from `start` to `end`.
112    pub fn new(id: u64, resource: u64, start: TimePoint, end: TimePoint, title: impl Into<String>) -> Self {
113        Self {
114            id,
115            resource,
116            start,
117            end,
118            title: title.into(),
119            tone: Tone::Accent,
120            done: false,
121            locked: false,
122            resizable: true,
123        }
124    }
125
126    /// Sets the color.
127    pub fn tone(mut self, tone: Tone) -> Self {
128        self.tone = tone;
129        self
130    }
131
132    /// Marks it as already done.
133    pub fn done(mut self, done: bool) -> Self {
134        self.done = done;
135        self
136    }
137
138    /// Stops it being moved or resized.
139    pub fn locked(mut self, locked: bool) -> Self {
140        self.locked = locked;
141        self
142    }
143
144    /// Whether its length can change (on by default). It can still be moved.
145    pub fn resizable(mut self, resizable: bool) -> Self {
146        self.resizable = resizable;
147        self
148    }
149}
150
151/// A shaded stretch of time across all rows, such as a holiday or a deadline.
152#[derive(Clone, Debug, PartialEq, Eq)]
153#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
154pub struct SchedulerRange {
155    /// When it begins.
156    pub start: TimePoint,
157    /// When it ends.
158    pub end: TimePoint,
159    /// Its color.
160    pub tone: Tone,
161}
162
163/// An event moved, resized, or drawn for the first time: where it would now be.
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
166pub struct SchedulerChange {
167    /// The event; for one being created, `None`.
168    pub id: Option<u64>,
169    /// Its row.
170    pub resource: u64,
171    /// When it begins.
172    pub start: TimePoint,
173    /// When it ends.
174    pub end: TimePoint,
175}
176
177type CanDrop = Rc<dyn Fn(&SchedulerChange) -> bool>;
178type OnChange = Rc<dyn Fn(SchedulerChange)>;
179
180/// A timeline of resources: a row for each person, room or account, a time axis
181/// across the top, and events as bars that can be dragged along the time and
182/// between rows, resized by their ends, and drawn on an empty stretch. Weekends
183/// are shaded, and only what is in view gets built, so large plans stay smooth.
184///
185/// The app holds the data: `on_change` reports where an event would go, and the
186/// app stores it and passes the events again. `can_drop` can refuse a place.
187///
188/// From the keyboard, Tab reaches the events. Left and Right move one by a step,
189/// Up and Down to the next row, Shift with Left and Right resize it, Enter opens
190/// it and Delete removes it.
191///
192/// ```
193/// use ravenrs_reactive::State;
194/// use ravenrs_widgets::{Date, Resource, Scheduler, SchedulerEvent, TimeAxis, TimePoint, Ui};
195///
196/// let first = Date::new(2026, 11, 1).unwrap();
197/// let axis = State::new(TimeAxis::days(first, 30));
198/// let start = TimePoint::from_date(first.add_days(4));
199/// let scheduler = Scheduler::new(axis.binding())
200///     .resources([Resource::new(1, "Ana"), Resource::new(2, "Bruno")])
201///     .events([SchedulerEvent::new(7, 1, start, start.plus_days(3), "Férias")])
202///     .on_change(|change| println!("{change:?}"));
203/// let mut ui = Ui::new(scheduler, ravenrs_style::Theme::raven_light());
204/// ui.update();
205/// ```
206#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
207pub struct Scheduler {
208    axis: Binding<TimeAxis>,
209    resources: Vec<Resource>,
210    events: Vec<SchedulerEvent>,
211    ranges: Vec<SchedulerRange>,
212    now: Option<TimePoint>,
213    selected: Option<Binding<Option<u64>>>,
214    resource_width: f32,
215    read_only: bool,
216    toolbar: bool,
217    fill_rows: bool,
218    card: bool,
219    avatars: bool,
220    event_detail: Option<EventDetail>,
221    event_menu: Option<EventMenu>,
222    lane_menu: Option<LaneMenu>,
223    on_change: Option<OnChange>,
224    on_create: Option<OnChange>,
225    on_open: Option<Rc<dyn Fn(u64)>>,
226    on_delete: Option<Rc<dyn Fn(u64)>>,
227    can_drop: Option<CanDrop>,
228}
229
230impl fmt::Debug for Scheduler {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        f.debug_struct("Scheduler")
233            .field("resources", &self.resources.len())
234            .field("events", &self.events.len())
235            .finish_non_exhaustive()
236    }
237}
238
239#[cfg_attr(feature = "live", ravenrs_live::expose)]
240impl Scheduler {
241    /// A scheduler over this stretch of time. Zooming and moving through time change `axis`.
242    pub fn new(axis: Binding<TimeAxis>) -> Self {
243        Self {
244            axis,
245            resources: Vec::new(),
246            events: Vec::new(),
247            ranges: Vec::new(),
248            now: None,
249            selected: None,
250            resource_width: 200.0,
251            read_only: false,
252            toolbar: true,
253            fill_rows: false,
254            card: false,
255            avatars: false,
256            event_detail: None,
257            event_menu: None,
258            lane_menu: None,
259            on_change: None,
260            on_create: None,
261            on_open: None,
262            on_delete: None,
263            can_drop: None,
264        }
265    }
266
267    /// The rows.
268    pub fn resources(mut self, resources: impl IntoIterator<Item = Resource>) -> Self {
269        self.resources = resources.into_iter().collect();
270        self
271    }
272
273    /// The bars. Those on no row are ignored.
274    pub fn events(mut self, events: impl IntoIterator<Item = SchedulerEvent>) -> Self {
275        self.events = events.into_iter().collect();
276        self
277    }
278
279    /// Shaded stretches of time across all rows.
280    pub fn ranges(mut self, ranges: impl IntoIterator<Item = SchedulerRange>) -> Self {
281        self.ranges = ranges.into_iter().collect();
282        self
283    }
284
285    /// Draws a line at this moment, and offers a button to come back to it.
286    pub fn now(mut self, now: TimePoint) -> Self {
287        self.now = Some(now);
288        self
289    }
290
291    /// Holds the selected event.
292    pub fn selection(mut self, selected: Binding<Option<u64>>) -> Self {
293        self.selected = Some(selected);
294        self
295    }
296
297    /// The width of the resources column (200 by default).
298    pub fn resource_width(mut self, width: f32) -> Self {
299        self.resource_width = width.max(80.0);
300        self
301    }
302
303    /// Rows share the height left over, so a few of them do not leave the
304    /// timeline half empty. Off by default; a row never gets shorter than its bars.
305    pub fn fill_rows(mut self, fill: bool) -> Self {
306        self.fill_rows = fill;
307        self
308    }
309
310    /// The look of a full scheduler, as a card among other cards: rounded and with a
311    /// soft shadow, a tinted header with the month over the days of the week, a line
312    /// between the rows' names and the timeline, and today's column marked. Bars are a
313    /// soft tint with a stripe of their color. Opens with today in the first column.
314    pub fn card(mut self, card: bool) -> Self {
315        self.card = card;
316        self
317    }
318
319    /// Shows the initials of each row's name beside it, for rows that are people.
320    pub fn avatars(mut self, avatars: bool) -> Self {
321        self.avatars = avatars;
322        self
323    }
324
325    /// A second line on the bars, such as an amount, by the event's id. Rows get taller.
326    pub fn event_detail(mut self, detail: impl Fn(u64) -> Option<String> + 'static) -> Self {
327        self.event_detail = Some(Rc::new(detail));
328        self
329    }
330
331    /// What a right click on a bar offers, by the event's id.
332    pub fn event_menu(mut self, menu: impl Fn(u64) -> Vec<MenuItem> + 'static) -> Self {
333        self.event_menu = Some(Rc::new(menu));
334        self
335    }
336
337    /// What a right click on an empty stretch offers, given the row and the time there.
338    pub fn lane_menu(mut self, menu: impl Fn(SchedulerChange) -> Vec<MenuItem> + 'static) -> Self {
339        self.lane_menu = Some(Rc::new(menu));
340        self
341    }
342
343    /// Nothing can be moved, resized or created.
344    pub fn read_only(mut self, read_only: bool) -> Self {
345        self.read_only = read_only;
346        self
347    }
348
349    /// The buttons to move through time and zoom (on by default).
350    pub fn toolbar(mut self, show: bool) -> Self {
351        self.toolbar = show;
352        self
353    }
354
355    /// An event was moved or resized.
356    pub fn on_change(mut self, handler: impl Fn(SchedulerChange) + 'static) -> Self {
357        self.on_change = Some(Rc::new(handler));
358        self
359    }
360
361    /// A stretch of time was drawn on a row, by dragging or double-clicking.
362    pub fn on_create(mut self, handler: impl Fn(SchedulerChange) + 'static) -> Self {
363        self.on_create = Some(Rc::new(handler));
364        self
365    }
366
367    /// An event was double-clicked, or Enter was pressed on it.
368    pub fn on_open(mut self, handler: impl Fn(u64) + 'static) -> Self {
369        self.on_open = Some(Rc::new(handler));
370        self
371    }
372
373    /// Delete was pressed on an event.
374    pub fn on_delete(mut self, handler: impl Fn(u64) + 'static) -> Self {
375        self.on_delete = Some(Rc::new(handler));
376        self
377    }
378
379    /// Says whether a place is allowed. A refused one shows in red and changes nothing.
380    pub fn can_drop(mut self, rule: impl Fn(&SchedulerChange) -> bool + 'static) -> Self {
381        self.can_drop = Some(Rc::new(rule));
382        self
383    }
384}
385
386/// A line of the timeline.
387#[derive(Clone)]
388enum Row {
389    Group { name: String, count: usize, collapsed: bool, y: f32 },
390    Resource { index: usize, y: f32, height: f32 },
391}
392
393#[cfg_attr(feature = "live", ravenrs_live::expose)]
394impl Row {
395    fn y(&self) -> f32 {
396        match self {
397            Row::Group { y, .. } | Row::Resource { y, .. } => *y,
398        }
399    }
400
401    fn height(&self) -> f32 {
402        match self {
403            Row::Group { .. } => GROUP_HEIGHT,
404            Row::Resource { height, .. } => *height,
405        }
406    }
407}
408
409/// Goes on with a drag, given where the pointer now is in the timeline's content.
410type FollowOn = Rc<RefCell<Option<Rc<dyn Fn(Point)>>>>;
411type EventDetail = Rc<dyn Fn(u64) -> Option<String>>;
412type EventMenu = Rc<dyn Fn(u64) -> Vec<MenuItem>>;
413type LaneMenu = Rc<dyn Fn(SchedulerChange) -> Vec<MenuItem>>;
414
415/// Opens a menu of these items at this place, until something closes it.
416fn open_menu(cx: &mut BuildCx<'_>, anchor: NodeId, at: Point, items: Vec<MenuItem>, close: Rc<dyn Fn()>) {
417    let entries: Vec<Entry> = items.into_iter().map(Entry::Item).collect();
418    cx.overlay(
419        anchor,
420        Placement::At(at),
421        &MenuList { entries: &entries, close: close.clone(), on_side: None },
422        Some(close),
423    );
424}
425
426/// What a drag is doing to an event.
427#[derive(Clone, Copy, Debug, PartialEq)]
428enum DragKind {
429    /// Along with the pointer; `grab` is how far into the event it was taken.
430    Move {
431        grab: i64,
432    },
433    ResizeStart,
434    ResizeEnd,
435}
436
437/// Where the event under a drag would land.
438#[derive(Clone, Copy, Debug, PartialEq)]
439struct Preview {
440    change: SchedulerChange,
441    allowed: bool,
442}
443
444/// What a drag or a key needs, shared by every bar and by the background.
445#[derive(Clone)]
446struct Editing {
447    axis: TimeAxis,
448    rows: Rc<Vec<(u64, f32, f32)>>,
449    preview: State<Option<Preview>>,
450    on_change: Option<OnChange>,
451    on_create: Option<OnChange>,
452    can_drop: Option<CanDrop>,
453    /// How far the timeline has scrolled, as of the last build.
454    scroll: Rc<Cell<f32>>,
455    /// Where a drag is: across the timeline in view, and down its content.
456    pointer: Rc<Cell<Option<(f32, f32)>>>,
457    /// Goes on with the drag where the timeline scrolled to, with the pointer still.
458    follow_on: FollowOn,
459    /// How wide the timeline in view is, as of the last build.
460    view_width: Rc<Cell<f32>>,
461    /// A stretch of the content that has to come into view.
462    reveal: Rc<Cell<Option<(f32, f32)>>>,
463}
464
465#[cfg_attr(feature = "live", ravenrs_live::expose)]
466impl Editing {
467    fn resource_at(&self, y: f32) -> Option<u64> {
468        let found = self.rows.iter().find(|(_, top, height)| y >= *top && y < top + height);
469        found.or_else(|| if y < 0.0 { self.rows.first() } else { self.rows.last() }).map(|r| r.0)
470    }
471
472    fn allowed(&self, change: &SchedulerChange) -> bool {
473        self.can_drop.as_ref().is_none_or(|rule| rule(change))
474    }
475
476    fn propose(&self, change: SchedulerChange) {
477        self.preview.set(Some(Preview { change, allowed: self.allowed(&change) }));
478    }
479
480    /// Applies a finished change if it is allowed and really is one.
481    fn commit(&self, original: Option<SchedulerChange>) {
482        let Some(preview) = *self.preview.peek() else { return };
483        self.preview.set(None);
484        if !preview.allowed || original == Some(preview.change) {
485            return;
486        }
487        let handler = if preview.change.id.is_some() { &self.on_change } else { &self.on_create };
488        if let Some(handler) = handler {
489            handler(preview.change);
490        }
491    }
492
493    /// The change a drag makes with the pointer here, in the timeline's content.
494    fn placed(&self, event: &SchedulerEvent, kind: DragKind, at: Point) -> SchedulerChange {
495        let time = self.axis.snap(self.axis.time_at(at.x));
496        let original = SchedulerChange {
497            id: Some(event.id),
498            resource: event.resource,
499            start: event.start,
500            end: event.end,
501        };
502        let step = self.axis.snap_minutes();
503        match kind {
504            DragKind::Move { grab } => {
505                let start = self.axis.snap(self.axis.time_at(at.x).plus_minutes(-grab));
506                let length = event.end.minutes() - event.start.minutes();
507                let resource = self.resource_at(at.y).unwrap_or(event.resource);
508                SchedulerChange { start, end: start.plus_minutes(length), resource, ..original }
509            }
510            DragKind::ResizeStart => {
511                SchedulerChange { start: time.min(event.end.plus_minutes(-step)), ..original }
512            }
513            DragKind::ResizeEnd => {
514                SchedulerChange { end: time.max(event.start.plus_minutes(step)), ..original }
515            }
516        }
517    }
518
519    /// Remembers where a drag is, for scrolling when it nears an edge.
520    fn track(&self, at: Point, follow_on: Rc<dyn Fn(Point)>) {
521        self.pointer.set(Some((at.x - self.scroll.get(), at.y)));
522        *self.follow_on.borrow_mut() = Some(follow_on);
523    }
524
525    fn stop_tracking(&self) {
526        self.pointer.set(None);
527        *self.follow_on.borrow_mut() = None;
528    }
529
530    /// Follows a drag on an event or on one of its ends.
531    fn follow(&self, event: &SchedulerEvent, kind: DragKind, origin: &Cell<Point>, drag: Drag, bar: Point) {
532        // Where the timeline's start was when the drag began, had it not scrolled.
533        if drag.phase == DragPhase::Start {
534            origin
535                .set(Point::new(drag.container.x() - bar.x + self.scroll.get(), drag.container.y() - bar.y));
536        }
537        let at = Point::new(
538            drag.position.x - origin.get().x + self.scroll.get(),
539            drag.position.y - origin.get().y,
540        );
541        let change = self.placed(event, kind, at);
542        match drag.phase {
543            DragPhase::End => {
544                self.stop_tracking();
545                self.propose(change);
546                self.commit(Some(SchedulerChange {
547                    id: Some(event.id),
548                    resource: event.resource,
549                    start: event.start,
550                    end: event.end,
551                }));
552            }
553            _ => {
554                let (editor, event) = (self.clone(), event.clone());
555                self.track(at, Rc::new(move |at| editor.propose(editor.placed(&event, kind, at))));
556                self.propose(change);
557            }
558        }
559    }
560}
561
562struct EndHandle {
563    event: SchedulerEvent,
564    kind: DragKind,
565    editing: Editing,
566    bar: Point,
567    width: f32,
568    height: f32,
569}
570
571impl View for EndHandle {
572    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
573        let at_start = self.kind == DragKind::ResizeStart;
574        let x = if at_start { 0.0 } else { (self.width - HANDLE_WIDTH).max(0.0) };
575        let node = cx.container(
576            LayoutStyle {
577                absolute: Some((x, 0.0)),
578                width: Some(HANDLE_WIDTH),
579                height: Some(self.height),
580                ..LayoutStyle::leaf()
581            },
582            &[],
583            Paint::None,
584        );
585        let origin = cx.use_state(|| Rc::new(Cell::new(Point::ZERO))).get();
586        let (event, kind, editing) = (self.event.clone(), self.kind, self.editing.clone());
587        let bar = self.bar;
588        cx.on_drag(
589            node,
590            // A handle's parent is the bar, so the timeline's origin comes from where the bar is.
591            Rc::new(move |drag: Drag| editing.follow(&event, kind, &origin, drag, bar)),
592        );
593        cx.cursor(node, CursorIcon::ColResize);
594        node
595    }
596}
597
598/// The bubble over a bar: its whole name, when it is and its second line.
599struct EventTip<'a> {
600    title: &'a str,
601    when: String,
602    detail: Option<&'a str>,
603}
604
605impl View for EventTip<'_> {
606    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
607        let theme = *cx.theme();
608        // Inverted colors read as a tooltip in every theme.
609        let ink = theme.color(Semantic::Canvas);
610        let title = util::label(cx, self.title, TextStyle::Callout, Some(Weight::SEMIBOLD), ink);
611        let line = match self.detail {
612            Some(detail) => format!("{} · {detail}", self.when),
613            None => self.when.clone(),
614        };
615        let line = util::label(cx, &line, TextStyle::Callout, None, ink.with_alpha(0.8));
616        let node = cx.container(
617            LayoutStyle {
618                padding: Insets::symmetric(6.0, 10.0),
619                spacing: 2.0,
620                ..LayoutStyle::stack(Axis::Vertical)
621            },
622            &[title, line],
623            Paint::Quad(QuadPaint { corner_radius: 7.0, ..QuadPaint::new(theme.color(Semantic::Ink)) }),
624        );
625        // The bar already says this to screen readers.
626        cx.semantics(node, Semantics::hidden());
627        crate::motion::popup(cx, node);
628        node
629    }
630}
631
632/// A row's background, which lights up under the pointer.
633struct RowBand {
634    frame: Rect,
635    draw: Option<Rc<dyn Fn(Drag)>>,
636}
637
638impl View for RowBand {
639    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
640        let ink = cx.theme().color(Semantic::Ink);
641        let paint = if cx.is_hovered() {
642            Paint::Quad(QuadPaint::new(ink.with_alpha(ROW_HOVER_TINT)))
643        } else {
644            Paint::None
645        };
646        let node = cx.container(
647            LayoutStyle {
648                absolute: Some((self.frame.x(), self.frame.y())),
649                width: Some(self.frame.width()),
650                height: Some(self.frame.height()),
651                ..LayoutStyle::leaf()
652            },
653            &[],
654            paint,
655        );
656        // Hoverable though it does nothing else: right clicks go on to the timeline by
657        // themselves, and a drag has to be handed over, as coming from the timeline.
658        cx.cursor(node, CursorIcon::Default);
659        if let Some(draw) = self.draw.clone() {
660            cx.on_drag(node, Rc::new(move |drag: Drag| draw(Drag { frame: drag.container, ..drag })));
661        }
662        node
663    }
664}
665
666struct Bar<'a> {
667    event: &'a SchedulerEvent,
668    /// Its second line.
669    detail: Option<String>,
670    /// Where it is now, which a drag may have changed.
671    at: (TimePoint, TimePoint),
672    menu: Option<EventMenu>,
673    /// A tint over this surface with a stripe of its color, instead of a solid bar.
674    tinted: Option<ravenrs_core::Color>,
675    /// Where it is drawn, in the timeline's content.
676    frame: Rect,
677    title: String,
678    /// It is the one a drag is moving, and this says if that place is allowed.
679    dragging: Option<bool>,
680    selected: bool,
681    /// The event keyboard users reach with Tab.
682    tab_stop: bool,
683    editable: bool,
684    editing: Editing,
685    /// Row by row, for Up and Down.
686    selection: Option<Binding<Option<u64>>>,
687    on_open: Option<Rc<dyn Fn(u64)>>,
688    on_delete: Option<Rc<dyn Fn(u64)>>,
689}
690
691#[cfg_attr(feature = "live", ravenrs_live::expose)]
692impl Bar<'_> {
693    fn color(&self, theme: &Theme) -> ravenrs_core::Color {
694        match self.dragging {
695            Some(false) => theme.color(Semantic::Danger),
696            _ if self.event.done => theme.color(Semantic::InkMuted),
697            _ => self.event.tone.color(theme),
698        }
699    }
700
701    fn key_handler(&self) -> crate::KeyHandler {
702        let event = self.event.clone();
703        let (editing, editable) = (self.editing.clone(), self.editable && !event.locked);
704        let (on_open, on_delete) = (self.on_open.clone(), self.on_delete.clone());
705        Rc::new(move |key, modifiers| {
706            let step = editing.axis.snap_minutes();
707            let original = SchedulerChange {
708                id: Some(event.id),
709                resource: event.resource,
710                start: event.start,
711                end: event.end,
712            };
713            let change = match key {
714                Key::Enter => {
715                    if let Some(open) = &on_open {
716                        open(event.id);
717                    }
718                    return on_open.is_some();
719                }
720                Key::Delete | Key::Backspace if editable => {
721                    if let Some(delete) = &on_delete {
722                        delete(event.id);
723                    }
724                    return on_delete.is_some();
725                }
726                Key::ArrowLeft | Key::ArrowRight if editable => {
727                    let by = if *key == Key::ArrowLeft { -step } else { step };
728                    if modifiers.shift && !event.resizable {
729                        return true;
730                    }
731                    if modifiers.shift {
732                        let end = event.end.plus_minutes(by);
733                        SchedulerChange { end: end.max(event.start.plus_minutes(step)), ..original }
734                    } else {
735                        SchedulerChange {
736                            start: event.start.plus_minutes(by),
737                            end: event.end.plus_minutes(by),
738                            ..original
739                        }
740                    }
741                }
742                Key::ArrowUp | Key::ArrowDown if editable => {
743                    let here = editing.rows.iter().position(|r| r.0 == event.resource);
744                    let there =
745                        here.and_then(|i| if *key == Key::ArrowUp { i.checked_sub(1) } else { Some(i + 1) });
746                    let Some(row) = there.and_then(|i| editing.rows.get(i)) else { return true };
747                    SchedulerChange { resource: row.0, ..original }
748                }
749                _ => return false,
750            };
751            editing.propose(change);
752            editing.commit(Some(original));
753            true
754        })
755    }
756}
757
758impl View for Bar<'_> {
759    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
760        let theme = *cx.theme();
761        let e = self.event;
762        let color = self.color(&theme);
763        let hovered = cx.is_hovered() || cx.is_focus_visible();
764        let solid = !e.done && self.dragging.is_none();
765        let fill = match self.tinted {
766            // Solid over the lines behind it, so its text keeps its contrast.
767            Some(surface) => {
768                let strength = if self.selected {
769                    TINT_SELECTED
770                } else if hovered && solid {
771                    TINT_HOVERED
772                } else if e.done {
773                    TINT_DONE
774                } else {
775                    TINT_REST
776                };
777                surface.lerp(color, strength)
778            }
779            None if self.selected || hovered && solid => color,
780            None => color.with_alpha(if e.done { 0.16 } else { 0.8 }),
781        };
782        let ink = match self.tinted {
783            Some(_) => color.lerp(theme.color(Semantic::Ink), TINT_TEXT_TOWARD_INK),
784            None if e.done => theme.color(Semantic::Ink),
785            None => theme.color(Semantic::OnAccent),
786        };
787        let mut children = Vec::new();
788        if e.done {
789            children.push(util::icon(cx, IconName::Check, 12.0, color));
790        }
791        let title =
792            cx.child(&Text::new(self.title.clone()).style(TextStyle::Caption).foreground(ink).truncate());
793        let mut lines = vec![title];
794        if let Some(subtitle) = &self.detail {
795            lines.push(
796                cx.child(
797                    &Text::new(subtitle.clone())
798                        .style(TextStyle::Caption)
799                        .font_weight(Weight::REGULAR)
800                        .foreground(ink)
801                        .truncate(),
802                ),
803            );
804        }
805        children.push(cx.container(
806            LayoutStyle {
807                grow: 1.0,
808                min_width: Some(0.0),
809                overflow_hidden: true,
810                ..LayoutStyle::stack(Axis::Vertical)
811            },
812            &lines,
813            Paint::None,
814        ));
815        if self.tinted.is_some() {
816            children.push(cx.container(
817                LayoutStyle {
818                    absolute: Some((STRIPE_INSET, STRIPE_INSET)),
819                    width: Some(STRIPE_WIDTH),
820                    height: Some((self.frame.height() - 2.0 * STRIPE_INSET).max(0.0)),
821                    ..LayoutStyle::leaf()
822                },
823                &[],
824                Paint::Quad(QuadPaint { corner_radius: STRIPE_WIDTH / 2.0, ..QuadPaint::new(color) }),
825            ));
826        }
827        let bar_point = Point::new(self.frame.x(), self.frame.y());
828        if self.editable && !e.locked && e.resizable {
829            for kind in [DragKind::ResizeStart, DragKind::ResizeEnd] {
830                let handle = EndHandle {
831                    event: e.clone(),
832                    kind,
833                    editing: self.editing.clone(),
834                    bar: bar_point,
835                    width: self.frame.width(),
836                    height: self.frame.height(),
837                };
838                children.push(cx.child(&handle));
839            }
840        }
841        let border = (self.selected || self.dragging.is_some())
842            .then(|| Border { width: 1.5, color: theme.color(Semantic::Ink).with_alpha(0.55) });
843        let node = cx.container(
844            LayoutStyle {
845                absolute: Some((self.frame.x(), self.frame.y())),
846                width: Some(self.frame.width()),
847                height: Some(self.frame.height()),
848                padding: if self.tinted.is_some() {
849                    Insets { top: 0.0, bottom: 0.0, left: 11.0, right: 6.0 }
850                } else {
851                    Insets::symmetric(0.0, 8.0)
852                },
853                spacing: 4.0,
854                align: Align::Center,
855                overflow_hidden: true,
856                ..LayoutStyle::stack(Axis::Horizontal)
857            },
858            &children,
859            Paint::Quad(QuadPaint { corner_radius: theme.radii.sm, border, ..QuadPaint::new(fill) }),
860        );
861        let labels = cx.labels();
862        let range = time_range(&labels, self.editing.axis.scale, e.start, e.end);
863        cx.semantics(
864            node,
865            Semantics::new(Role::Button).name(e.title.clone()).description(range).selected(self.selected),
866        );
867        let menu_at: State<Option<Point>> = cx.use_state(|| None);
868        if let Some(menu) = self.menu.clone() {
869            let (write, selection, id) = (menu_at.clone(), self.selection.clone(), e.id);
870            cx.on_context_menu(
871                node,
872                Rc::new(move |at| {
873                    if let Some(selection) = &selection {
874                        selection.set(Some(id));
875                    }
876                    write.set(Some(at));
877                }),
878            );
879            if let Some(at) = menu_at.get() {
880                let close = menu_at.clone();
881                open_menu(cx, node, at, menu(e.id), Rc::new(move || close.set(None)));
882            }
883        }
884        // What the bar has no room to say, and where a drag is taking it.
885        if self.tinted.is_some() && menu_at.get().is_none() && (cx.is_hovered() || self.dragging.is_some()) {
886            let tip = EventTip {
887                title: &e.title,
888                when: time_range(&labels, self.editing.axis.scale, self.at.0, self.at.1),
889                detail: self.detail.as_deref(),
890            };
891            cx.overlay(node, Placement::AboveCenter, &tip, None);
892        }
893        if cx.is_focus_visible() {
894            let (from, to) = (self.frame.x(), self.frame.x() + self.frame.width());
895            let scrolled = self.editing.scroll.get();
896            if from < scrolled || to > scrolled + self.editing.view_width.get() {
897                self.editing.reveal.set(Some((from, to)));
898                let now = cx.now();
899                cx.wake_at(now);
900            }
901        }
902        if self.tab_stop {
903            cx.focusable(node, theme.radii.sm);
904            cx.on_key(node, self.key_handler());
905        }
906        let (selection, id) = (self.selection.clone(), e.id);
907        cx.on_click(
908            node,
909            Rc::new(move || {
910                if let Some(selection) = &selection {
911                    selection.set(Some(id));
912                }
913            }),
914        );
915        if let Some(open) = self.on_open.clone() {
916            cx.on_double_click(node, Rc::new(move || open(id)));
917        }
918        if self.editable && !e.locked {
919            let origin = cx.use_state(|| Rc::new(Cell::new(Point::ZERO))).get();
920            let grab = cx.use_state(|| Rc::new(Cell::new(0_i64))).get();
921            let (event, editing) = (e.clone(), self.editing.clone());
922            cx.on_drag(
923                node,
924                Rc::new(move |drag: Drag| {
925                    if drag.phase == DragPhase::Start {
926                        // The pointer stays over the same part of the event while it moves.
927                        let inside = drag.position.x - drag.frame.x();
928                        let minutes = (f64::from(inside)
929                            / f64::from(editing.axis.x_of(editing.axis.start.plus_days(1)).max(0.001))
930                            * 1440.0) as i64;
931                        grab.set(minutes);
932                    }
933                    let kind = DragKind::Move { grab: grab.get() };
934                    let content_origin = Point::ZERO;
935                    editing.follow(&event, kind, &origin, drag, content_origin);
936                }),
937            );
938        }
939        cx.cursor(node, CursorIcon::Pointer);
940        node
941    }
942}
943
944/// The empty part of the timeline: draws a new event, and holds the bars.
945struct Lanes {
946    /// Its size.
947    width: f32,
948    height: f32,
949    children: Rc<Vec<NodeId>>,
950    draw: Option<Rc<dyn Fn(Drag)>>,
951}
952
953#[cfg_attr(feature = "live", ravenrs_live::expose)]
954impl Lanes {
955    fn build(&self, cx: &mut BuildCx<'_>, s: &Scheduler, editing: &Editing) -> NodeId {
956        let node = cx.container(
957            LayoutStyle {
958                width: Some(self.width),
959                height: Some(self.height),
960                ..LayoutStyle::stack(Axis::Vertical)
961            },
962            &self.children,
963            Paint::None,
964        );
965        if let Some(menu) = s.lane_menu.clone() {
966            let open: State<Option<(Point, SchedulerChange)>> = cx.use_state(|| None);
967            let (write, editor) = (open.clone(), editing.clone());
968            cx.on_context_menu_in(
969                node,
970                Rc::new(move |position, frame| {
971                    let at = Point::new(position.x - frame.x(), position.y - frame.y());
972                    // The stretch the click is in, not the nearest edge of one.
973                    let step = editor.axis.snap_minutes();
974                    let start = editor.axis.snap(editor.axis.time_at(at.x).plus_minutes(-step / 2));
975                    let Some(resource) = editor.resource_at(at.y) else { return };
976                    let end = start.plus_minutes(step);
977                    write.set(Some((position, SchedulerChange { id: None, resource, start, end })));
978                }),
979            );
980            if let Some((at, change)) = open.get() {
981                let close = open.clone();
982                open_menu(cx, node, at, menu(change), Rc::new(move || close.set(None)));
983            }
984        }
985        if let Some(draw) = self.draw.clone() {
986            cx.on_drag(node, draw);
987        }
988        node
989    }
990}
991
992/// What drawing from `begin` to `time` on a row proposes.
993fn drawn(editor: &Editing, begin: TimePoint, resource: u64, time: TimePoint) -> SchedulerChange {
994    let step = editor.axis.snap_minutes();
995    let (start, end) =
996        if time >= begin { (begin, time.max(begin.plus_minutes(step))) } else { (time, begin) };
997    SchedulerChange { id: None, resource, start, end }
998}
999
1000/// What drawing on an empty stretch does, if the scheduler can create.
1001fn draw_handler(cx: &mut BuildCx<'_>, s: &Scheduler, editing: &Editing) -> Option<Rc<dyn Fn(Drag)>> {
1002    if s.read_only || s.on_create.is_none() {
1003        return None;
1004    }
1005    let from: Rc<Cell<Option<(TimePoint, u64)>>> = cx.use_state(|| Rc::new(Cell::new(None))).get();
1006    let editor = editing.clone();
1007    Some(Rc::new(move |drag: Drag| {
1008        let at = Point::new(drag.position.x - drag.frame.x(), drag.position.y - drag.frame.y());
1009        let time = editor.axis.snap(editor.axis.time_at(at.x));
1010        if drag.phase == DragPhase::Start
1011            && let Some(resource) = editor.resource_at(at.y)
1012        {
1013            from.set(Some((time, resource)));
1014        }
1015        let Some((begin, resource)) = from.get() else { return };
1016        editor.propose(drawn(&editor, begin, resource, time));
1017        if drag.phase == DragPhase::End {
1018            from.set(None);
1019            editor.stop_tracking();
1020            editor.commit(None);
1021        } else {
1022            let again = editor.clone();
1023            editor.track(
1024                at,
1025                Rc::new(move |at| {
1026                    let time = again.axis.snap(again.axis.time_at(at.x));
1027                    again.propose(drawn(&again, begin, resource, time));
1028                }),
1029            );
1030        }
1031    }))
1032}
1033
1034/// A moment as a person reads it on this scale.
1035fn time_label(labels: &Labels, scale: TimeScale, time: TimePoint) -> String {
1036    let date = labels.short_date(time.date());
1037    match scale {
1038        TimeScale::Hours => {
1039            let (hour, minute) = time.hour_minute();
1040            format!("{date} {hour:02}:{minute:02}")
1041        }
1042        _ => date,
1043    }
1044}
1045
1046/// A stretch of time as a person reads it. Days are whole, so the last one shown is the last one in it.
1047fn time_range(labels: &Labels, scale: TimeScale, start: TimePoint, end: TimePoint) -> String {
1048    let last = if scale == TimeScale::Hours { end } else { end.plus_minutes(-1).max(start) };
1049    let (from, to) = (time_label(labels, scale, start), time_label(labels, scale, last));
1050    if from == to { from } else { format!("{from} – {to}") }
1051}
1052
1053fn tick_text(labels: &Labels, scale: TimeScale, time: TimePoint) -> String {
1054    let date = time.date();
1055    match scale {
1056        TimeScale::Hours => format!("{:02}", time.hour_minute().0),
1057        TimeScale::Days => date.day().to_string(),
1058        TimeScale::Weeks => format!("{:02}", date.day()),
1059        TimeScale::Months => {
1060            let name = &labels.months[usize::from(date.month().clamp(1, 12)) - 1];
1061            name.chars().take(3).collect()
1062        }
1063    }
1064}
1065
1066fn group_text(labels: &Labels, scale: TimeScale, time: TimePoint) -> String {
1067    let date = time.date();
1068    match scale {
1069        TimeScale::Hours => labels.short_date(date),
1070        TimeScale::Days | TimeScale::Weeks => labels.month_title(date),
1071        TimeScale::Months => date.year().to_string(),
1072    }
1073}
1074
1075/// A filled stretch at a fixed place.
1076fn band(cx: &mut BuildCx<'_>, frame: Rect, fill: ravenrs_core::Color) -> NodeId {
1077    let node = cx.container(
1078        LayoutStyle {
1079            absolute: Some((frame.x(), frame.y())),
1080            width: Some(frame.width()),
1081            height: Some(frame.height()),
1082            ..LayoutStyle::leaf()
1083        },
1084        &[],
1085        Paint::Quad(QuadPaint::new(fill)),
1086    );
1087    cx.semantics(node, Semantics::hidden());
1088    node
1089}
1090
1091/// A text at a fixed place.
1092fn placed_text(
1093    cx: &mut BuildCx<'_>,
1094    frame: Rect,
1095    content: &str,
1096    style: TextStyle,
1097    color: ravenrs_core::Color,
1098) -> NodeId {
1099    let label = util::label(cx, content, style, None, color);
1100    cx.container(
1101        LayoutStyle {
1102            absolute: Some((frame.x(), frame.y())),
1103            width: Some(frame.width()),
1104            height: Some(frame.height()),
1105            align: Align::Center,
1106            justify: Justify::Center,
1107            overflow_hidden: true,
1108            ..LayoutStyle::stack(Axis::Horizontal)
1109        },
1110        &[label],
1111        Paint::None,
1112    )
1113}
1114
1115#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
1116impl Scheduler {
1117    /// Groups stay together, in the order their first resource appears.
1118    fn lane_height(&self) -> f32 {
1119        if self.event_detail.is_some() { TALL_LANE_HEIGHT } else { LANE_HEIGHT }
1120    }
1121
1122    fn rows(&self, lanes_of: &HashMap<u64, usize>, collapsed: &BTreeSet<String>) -> (Vec<Row>, f32) {
1123        let mut order: Vec<Option<&str>> = Vec::new();
1124        for resource in &self.resources {
1125            let group = resource.group.as_deref();
1126            if !order.contains(&group) {
1127                order.push(group);
1128            }
1129        }
1130        let mut rows = Vec::with_capacity(self.resources.len() + order.len());
1131        let mut y = 0.0;
1132        for group in order {
1133            let members: Vec<usize> =
1134                (0..self.resources.len()).filter(|i| self.resources[*i].group.as_deref() == group).collect();
1135            let mut hidden = false;
1136            if let Some(name) = group {
1137                hidden = collapsed.contains(name);
1138                rows.push(Row::Group { name: name.to_owned(), count: members.len(), collapsed: hidden, y });
1139                y += GROUP_HEIGHT;
1140            }
1141            if hidden {
1142                continue;
1143            }
1144            for index in members {
1145                let lanes = lanes_of.get(&self.resources[index].id).copied().unwrap_or(1);
1146                let height = lanes as f32 * self.lane_height() + 2.0 * ROW_PADDING;
1147                rows.push(Row::Resource { index, y, height });
1148                y += height;
1149            }
1150        }
1151        (rows, y)
1152    }
1153
1154    fn toolbar_view(&self, cx: &mut BuildCx<'_>, axis: TimeAxis) -> NodeId {
1155        let labels = cx.labels();
1156        let (back, ahead, zoom_in, zoom_out) =
1157            (self.axis.clone(), self.axis.clone(), self.axis.clone(), self.axis.clone());
1158        let mut items = Vec::new();
1159        let today_button = self.now.map(|now| {
1160            let today = self.axis.clone();
1161            cx.child(
1162                &Button::new(labels.today.clone())
1163                    .style(if self.card { ButtonStyle::Secondary } else { ButtonStyle::Ghost })
1164                    .size(ButtonSize::Small)
1165                    .on_click(move || {
1166                        let length = axis.end.minutes() - axis.start.minutes();
1167                        let start = axis.snap(now.plus_minutes(-length / 8));
1168                        today.set(TimeAxis { start, end: start.plus_minutes(length), ..axis });
1169                    }),
1170            )
1171        });
1172        // In a card the month is on the header, and today comes first.
1173        if self.card {
1174            items.extend(today_button);
1175        }
1176        items.push(
1177            cx.child(
1178                &IconButton::new(IconName::ChevronLeft)
1179                    .label(labels.previous_month.clone())
1180                    .size(ButtonSize::Small)
1181                    .on_click(move || back.set(axis.shifted(-1))),
1182            ),
1183        );
1184        items.push(
1185            cx.child(
1186                &IconButton::new(IconName::ChevronRight)
1187                    .label(labels.next_month.clone())
1188                    .size(ButtonSize::Small)
1189                    .on_click(move || ahead.set(axis.shifted(1))),
1190            ),
1191        );
1192        if !self.card {
1193            let title = format!(
1194                "{} – {}",
1195                labels.short_date(axis.start.date()),
1196                labels.short_date(axis.end.plus_minutes(-1).date())
1197            );
1198            items.push(util::label(cx, &title, TextStyle::Headline, None, cx.theme().color(Semantic::Ink)));
1199            items.extend(today_button);
1200        }
1201        items.push(cx.container(LayoutStyle { grow: 1.0, ..LayoutStyle::leaf() }, &[], Paint::None));
1202        let mut out =
1203            IconButton::new(IconName::ZoomOut).label(labels.zoom_out.clone()).size(ButtonSize::Small);
1204        if let Some(farther) = axis.zoomed_out() {
1205            out = out.on_click(move || zoom_out.set(farther));
1206        }
1207        items.push(cx.child(&out));
1208        let mut near =
1209            IconButton::new(IconName::ZoomIn).label(labels.zoom_in.clone()).size(ButtonSize::Small);
1210        if let Some(nearer) = axis.zoomed_in() {
1211            near = near.on_click(move || zoom_in.set(nearer));
1212        }
1213        items.push(cx.child(&near));
1214        cx.container(
1215            LayoutStyle {
1216                spacing: 6.0,
1217                align: Align::Center,
1218                padding: if self.card { Insets::symmetric(10.0, 16.0) } else { Insets::symmetric(8.0, 8.0) },
1219                ..LayoutStyle::stack(Axis::Horizontal)
1220            },
1221            &items,
1222            Paint::None,
1223        )
1224    }
1225}
1226
1227impl View for Scheduler {
1228    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1229        let theme = *cx.theme();
1230        let labels = cx.labels();
1231        let axis = self.axis.get();
1232        let preview: State<Option<Preview>> = cx.use_state(|| None);
1233        let collapsed: State<BTreeSet<String>> = cx.use_state(BTreeSet::new);
1234        let own_selection: State<Option<u64>> = cx.use_state(|| None);
1235        let selection = self.selected.clone().unwrap_or_else(|| own_selection.binding());
1236        let selected = selection.get();
1237        let current = preview.get();
1238
1239        // Where each event is drawn, the one being dragged at its new place.
1240        let placed: Vec<(usize, SchedulerChange)> = self
1241            .events
1242            .iter()
1243            .enumerate()
1244            .map(|(i, e)| {
1245                let here =
1246                    SchedulerChange { id: Some(e.id), resource: e.resource, start: e.start, end: e.end };
1247                (i, current.filter(|p| p.change.id == Some(e.id)).map_or(here, |p| p.change))
1248            })
1249            .collect();
1250        let mut by_resource: HashMap<u64, Vec<usize>> = HashMap::new();
1251        for (slot, (_, change)) in placed.iter().enumerate() {
1252            by_resource.entry(change.resource).or_default().push(slot);
1253        }
1254        let mut lane_of = vec![0_usize; placed.len()];
1255        let mut lanes_of: HashMap<u64, usize> = HashMap::new();
1256        for (resource, slots) in &by_resource {
1257            let spans: Vec<(TimePoint, TimePoint)> =
1258                slots.iter().map(|s| (placed[*s].1.start, placed[*s].1.end)).collect();
1259            let (lanes, count) = assign_lanes(&spans);
1260            for (slot, lane) in slots.iter().zip(lanes) {
1261                lane_of[*slot] = lane;
1262            }
1263            lanes_of.insert(*resource, count);
1264        }
1265        let lane_height = self.lane_height();
1266        let (mut rows, mut total_height) = self.rows(&lanes_of, &collapsed.get());
1267        if self.fill_rows {
1268            match cx.scroll_viewport() {
1269                Some(view) => total_height = share_height(&mut rows, total_height, view.height),
1270                // The height comes from the first layout, and nothing else asks for a second build.
1271                None => {
1272                    let now = cx.now();
1273                    cx.wake_at(now);
1274                }
1275            }
1276        }
1277        let top_of: HashMap<u64, f32> = rows
1278            .iter()
1279            .filter_map(|row| match row {
1280                Row::Resource { index, y, .. } => Some((self.resources[*index].id, *y)),
1281                Row::Group { .. } => None,
1282            })
1283            .collect();
1284        let row_lines: Rc<Vec<(u64, f32, f32)>> = Rc::new(
1285            rows.iter()
1286                .filter_map(|row| match row {
1287                    Row::Resource { index, y, height, .. } => Some((self.resources[*index].id, *y, *height)),
1288                    Row::Group { .. } => None,
1289                })
1290                .collect(),
1291        );
1292
1293        let scroll = cx.scroll_offset();
1294        let window = cx.viewport();
1295        let drag_scroll: Rc<Cell<f32>> = cx.use_state(|| Rc::new(Cell::new(0.0))).get();
1296        drag_scroll.set(scroll.x);
1297        let view_width: Rc<Cell<f32>> = cx.use_state(|| Rc::new(Cell::new(0.0))).get();
1298        let reveal: Rc<Cell<Option<(f32, f32)>>> = cx.use_state(|| Rc::new(Cell::new(None))).get();
1299        let drag_pointer: Rc<Cell<Option<(f32, f32)>>> = cx.use_state(|| Rc::new(Cell::new(None))).get();
1300        let drag_follow: FollowOn = cx.use_state(|| Rc::new(RefCell::new(None))).get();
1301        // Opens at now, once its size is known, with some of what came before in view.
1302        let opened: Rc<Cell<bool>> = cx.use_state(|| Rc::new(Cell::new(false))).get();
1303        if !opened.get() && self.now.is_some() {
1304            match cx.scroll_viewport() {
1305                Some(size) => {
1306                    opened.set(true);
1307                    if let Some(now) = self.now {
1308                        let lead = if self.card { 0.0 } else { size.width / 4.0 };
1309                        cx.set_scroll_offset(Point::new((axis.x_of(now) - lead).max(0.0), 0.0));
1310                    }
1311                }
1312                // The size comes from the first layout, and nothing else asks for a second build.
1313                None => {
1314                    let now = cx.now();
1315                    cx.wake_at(now);
1316                }
1317            }
1318        }
1319        let view = cx.scroll_viewport().unwrap_or(window);
1320        let (left, right) = (scroll.x - OVERSCAN, scroll.x + view.width + OVERSCAN);
1321        let (top, bottom) = (scroll.y - OVERSCAN, scroll.y + view.height + OVERSCAN);
1322        let width = axis.width();
1323        let line = theme.color(Semantic::Border);
1324        let muted = theme.color(Semantic::InkMuted);
1325        let ink = theme.color(Semantic::Ink);
1326
1327        let editing = Editing {
1328            axis,
1329            rows: row_lines,
1330            preview: preview.clone(),
1331            on_change: self.on_change.clone(),
1332            on_create: self.on_create.clone(),
1333            can_drop: self.can_drop.clone(),
1334            scroll: drag_scroll.clone(),
1335            pointer: drag_pointer.clone(),
1336            follow_on: drag_follow.clone(),
1337            view_width: view_width.clone(),
1338            reveal: reveal.clone(),
1339        };
1340
1341        view_width.set(view.width);
1342        // What keyboard focus reached comes into view, with some air around it.
1343        if let Some((from, to)) = reveal.take() {
1344            let wanted = if from < scroll.x { from - REVEAL_MARGIN } else { to + REVEAL_MARGIN - view.width };
1345            cx.set_scroll_offset(Point::new(wanted.clamp(0.0, (width - view.width).max(0.0)), scroll.y));
1346        }
1347
1348        // A drag near an edge takes the timeline with it, also with the pointer still.
1349        if let Some((x, y)) = drag_pointer.get() {
1350            let push = if x > view.width - AUTO_SCROLL_EDGE {
1351                AUTO_SCROLL_STEP
1352            } else if x < AUTO_SCROLL_EDGE {
1353                -AUTO_SCROLL_STEP
1354            } else {
1355                0.0
1356            };
1357            let moved = (scroll.x + push).clamp(0.0, (width - view.width).max(0.0));
1358            if moved != scroll.x {
1359                cx.set_scroll_offset(Point::new(moved, scroll.y));
1360                drag_scroll.set(moved);
1361                let follow_on = drag_follow.borrow().clone();
1362                if let Some(follow_on) = follow_on {
1363                    follow_on(Point::new(x + moved, y));
1364                }
1365                let now = cx.now();
1366                cx.wake_at(now + AUTO_SCROLL_EVERY);
1367            }
1368        }
1369
1370        // The header: months or days above, ticks below; it follows the sideways scroll.
1371        let ticks = axis.ticks(left, right);
1372        let mut header_cells = Vec::new();
1373        let mut previous = None;
1374        let accent = theme.color(Semantic::Accent);
1375        let today = self.now.map(|now| now.date());
1376        let header_height = 2.0 * HEADER_ROW_HEIGHT;
1377        for (i, tick) in ticks.iter().enumerate() {
1378            if axis.starts_group(tick.start, previous) {
1379                let until = ticks[i + 1..]
1380                    .iter()
1381                    .find(|t| axis.starts_group(t.start, Some(tick.start)))
1382                    .map_or(right.min(width), |t| t.x);
1383                let label = group_text(&labels, axis.scale, tick.start);
1384                let cell = util::label(cx, &label, TextStyle::Caption, Some(Weight::SEMIBOLD), ink);
1385                // It stays at the edge while its month is in view, however far that has scrolled.
1386                let x = (tick.x.max(scroll.x) + 6.0).min(until);
1387                header_cells.push(cx.container(
1388                    LayoutStyle {
1389                        absolute: Some((x, 0.0)),
1390                        width: Some((until - x).max(0.0)),
1391                        height: Some(HEADER_ROW_HEIGHT),
1392                        align: Align::Center,
1393                        overflow_hidden: true,
1394                        ..LayoutStyle::stack(Axis::Horizontal)
1395                    },
1396                    &[cell],
1397                    Paint::None,
1398                ));
1399            }
1400            previous = Some(tick.start);
1401            let text = if self.card && axis.scale == TimeScale::Days {
1402                let date = tick.start.date();
1403                format!("{} {}", labels.weekdays[usize::from(date.weekday()).min(6)], date.day())
1404            } else {
1405                tick_text(&labels, axis.scale, tick.start)
1406            };
1407            let frame = Rect::new(tick.x, HEADER_ROW_HEIGHT, tick.width, HEADER_ROW_HEIGHT);
1408            let is_today = self.card && axis.scale == TimeScale::Days && today == Some(tick.start.date());
1409            header_cells.push(placed_text(
1410                cx,
1411                frame,
1412                &text,
1413                TextStyle::Caption,
1414                if is_today { accent } else { muted },
1415            ));
1416            if is_today {
1417                // A mark under today's name, where the eye looks for it.
1418                header_cells.push(band(cx, Rect::new(tick.x, header_height - 2.0, tick.width, 2.0), accent));
1419            }
1420            header_cells.push(band(cx, Rect::new(tick.x, HEADER_ROW_HEIGHT, 1.0, HEADER_ROW_HEIGHT), line));
1421        }
1422        // A tinted band, in a card, under its toolbar.
1423        let header_paint = Paint::Quad(QuadPaint::new(theme.color(if self.card {
1424            Semantic::Canvas
1425        } else {
1426            Semantic::Surface
1427        })));
1428        let header_row = cx.container(
1429            LayoutStyle {
1430                width: Some(width),
1431                height: Some(header_height),
1432                ..LayoutStyle::stack(Axis::Vertical)
1433            },
1434            &header_cells,
1435            Paint::None,
1436        );
1437        cx.translate(header_row, Point::new(-scroll.x, 0.0));
1438        let header_view = cx.container(
1439            LayoutStyle {
1440                grow: 1.0,
1441                min_width: Some(0.0),
1442                height: Some(header_height),
1443                overflow_hidden: true,
1444                ..LayoutStyle::stack(Axis::Horizontal)
1445            },
1446            &[header_row],
1447            header_paint.clone(),
1448        );
1449        cx.clip(header_view);
1450        let corner_text =
1451            util::label(cx, &labels.scheduler_resources, TextStyle::Caption, Some(Weight::SEMIBOLD), muted);
1452        let corner = cx.container(
1453            LayoutStyle {
1454                width: Some(self.resource_width),
1455                height: Some(header_height),
1456                padding: Insets::symmetric(0.0, 10.0),
1457                align: Align::Center,
1458                ..LayoutStyle::stack(Axis::Horizontal)
1459            },
1460            &[corner_text],
1461            header_paint,
1462        );
1463        // A card draws the line between the names and the timeline all the way down.
1464        let divider = |cx: &mut BuildCx<'_>| {
1465            cx.container(
1466                LayoutStyle { width: Some(1.0), ..LayoutStyle::leaf() },
1467                &[],
1468                Paint::Quad(QuadPaint::new(line)),
1469            )
1470        };
1471        let mut header_parts = vec![corner];
1472        if self.card {
1473            header_parts.push(divider(cx));
1474        }
1475        header_parts.push(header_view);
1476        let header = cx.container(
1477            LayoutStyle { align: Align::Stretch, ..LayoutStyle::stack(Axis::Horizontal) },
1478            &header_parts,
1479            Paint::None,
1480        );
1481
1482        let draw = draw_handler(cx, self, &editing);
1483        // Behind the bars: weekends, marked stretches, row lines.
1484        let mut under = Vec::new();
1485        // In a card the lines go on below the last row, and stop short of the round corners.
1486        let area = if self.card { total_height.max(view.height) } else { total_height };
1487        let lines_height = if self.card { area - theme.radii.xl } else { area };
1488        if self.card && axis.scale == TimeScale::Days {
1489            for tick in ticks.iter().filter(|tick| today == Some(tick.start.date())) {
1490                under.push(band(
1491                    cx,
1492                    Rect::new(tick.x, 0.0, tick.width, lines_height),
1493                    accent.with_alpha(TODAY_TINT),
1494                ));
1495            }
1496        }
1497        if axis.scale == TimeScale::Days || axis.scale == TimeScale::Hours {
1498            for tick in &ticks {
1499                let weekday = tick.start.date().weekday();
1500                if weekday == 0 || weekday == 6 {
1501                    under.push(band(
1502                        cx,
1503                        Rect::new(tick.x, 0.0, tick.width, lines_height),
1504                        ink.with_alpha(0.035),
1505                    ));
1506                }
1507            }
1508        }
1509
1510        for tick in &ticks {
1511            under.push(band(cx, Rect::new(tick.x, 0.0, 1.0, lines_height), line.faded(0.5)));
1512        }
1513        for range in &self.ranges {
1514            let (from, to) = (axis.x_of(range.start), axis.x_of(range.end));
1515            if to > left && from < right {
1516                let color = range.tone.color(&theme).with_alpha(0.12);
1517                under.push(band(cx, Rect::new(from, 0.0, (to - from).max(1.0), total_height), color));
1518            }
1519        }
1520        let mut names = Vec::new();
1521        for row in rows.iter().filter(|r| r.y() + r.height() > top && r.y() < bottom) {
1522            let y = row.y();
1523            if self.card
1524                && let Row::Resource { index, height, .. } = row
1525            {
1526                let band = RowBand { frame: Rect::new(0.0, y, width, *height), draw: draw.clone() };
1527                let key = 0x726f_7700_0000_0000 ^ self.resources[*index].id;
1528                under.insert(0, cx.keyed_child(key, &band));
1529            }
1530            if !self.card || y + row.height() < area {
1531                under.push(band(cx, Rect::new(0.0, y + row.height() - 1.0, width, 1.0), line));
1532            }
1533            match row {
1534                Row::Group { name, count, collapsed: is_collapsed, .. } => {
1535                    under.push(band(cx, Rect::new(0.0, y, width, GROUP_HEIGHT), ink.with_alpha(0.03)));
1536                    let chevron = if *is_collapsed { IconName::ChevronRight } else { IconName::ChevronDown };
1537                    let icon = util::icon(cx, chevron, 14.0, muted);
1538                    let label = util::label(cx, name, TextStyle::Caption, Some(Weight::SEMIBOLD), ink);
1539                    let note = util::label(cx, &count.to_string(), TextStyle::Caption, None, muted);
1540                    let node = cx.keyed_child(
1541                        0x6772_6f75_7000_0000 ^ hash_text(name),
1542                        &GroupHeader {
1543                            name: name.clone(),
1544                            collapsed: *is_collapsed,
1545                            state: collapsed.clone(),
1546                            children: Rc::new(vec![icon, label, note]),
1547                            frame: Rect::new(0.0, y, self.resource_width, GROUP_HEIGHT),
1548                        },
1549                    );
1550                    names.push(node);
1551                }
1552                Row::Resource { index, height, .. } => {
1553                    let resource = &self.resources[*index];
1554                    let mut name = Text::new(resource.name.clone()).style(TextStyle::Callout).truncate();
1555                    if self.card {
1556                        name = name.font_weight(Weight::SEMIBOLD);
1557                    }
1558                    let name = cx.child(&name);
1559                    let avatar = self
1560                        .avatars
1561                        .then(|| cx.child(&crate::Avatar::new(resource.name.clone()).size(AVATAR_SIZE)));
1562                    let mut lines = vec![name];
1563                    if let Some(subtitle) = &resource.subtitle {
1564                        lines.push(
1565                            cx.child(
1566                                &Text::new(subtitle.clone())
1567                                    .style(TextStyle::Caption)
1568                                    .foreground(Semantic::InkMuted)
1569                                    .truncate(),
1570                            ),
1571                        );
1572                    }
1573                    let row_parts = match avatar {
1574                        Some(avatar) => {
1575                            let text = cx.container(
1576                                LayoutStyle {
1577                                    grow: 1.0,
1578                                    min_width: Some(0.0),
1579                                    align: Align::Stretch,
1580                                    ..LayoutStyle::stack(Axis::Vertical)
1581                                },
1582                                &lines,
1583                                Paint::None,
1584                            );
1585                            vec![avatar, text]
1586                        }
1587                        None => lines,
1588                    };
1589                    let node = cx.container(
1590                        LayoutStyle {
1591                            absolute: Some((0.0, y)),
1592                            width: Some(self.resource_width),
1593                            height: Some(*height),
1594                            padding: if self.fill_rows {
1595                                Insets { top: NAME_TOP, right: 10.0, bottom: 0.0, left: 10.0 }
1596                            } else {
1597                                Insets::symmetric(0.0, 10.0)
1598                            },
1599                            justify: if self.fill_rows || self.avatars {
1600                                Justify::Start
1601                            } else {
1602                                Justify::Center
1603                            },
1604                            spacing: AVATAR_GAP,
1605                            align: if self.avatars { Align::Center } else { Align::Stretch },
1606                            overflow_hidden: true,
1607                            ..LayoutStyle::stack(if self.avatars { Axis::Horizontal } else { Axis::Vertical })
1608                        },
1609                        &row_parts,
1610                        Paint::None,
1611                    );
1612                    cx.semantics(node, Semantics::new(Role::Row).name(resource.name.clone()));
1613                    names.push(node);
1614                    if !self.card || y + height < area {
1615                        names.push(band(
1616                            cx,
1617                            Rect::new(0.0, y + height - 1.0, self.resource_width, 1.0),
1618                            line,
1619                        ));
1620                    }
1621                }
1622            }
1623        }
1624
1625        // The bars in view.
1626        let tab_stop = selected
1627            .filter(|id| self.events.iter().any(|e| e.id == *id))
1628            .or_else(|| self.events.first().map(|e| e.id));
1629        let mut bars = Vec::new();
1630        for (slot, (index, change)) in placed.iter().enumerate() {
1631            let Some(row_top) = top_of.get(&change.resource) else { continue };
1632            let event = &self.events[*index];
1633            let x = axis.x_of(change.start);
1634            let bar_width = (axis.x_of(change.end) - x).max(MIN_BAR_WIDTH);
1635            let y = row_top + ROW_PADDING + lane_of[slot] as f32 * lane_height + 2.0;
1636            let is_tab_stop = tab_stop == Some(event.id);
1637            let in_view = x + bar_width > left && x < right && y + lane_height > top && y < bottom;
1638            // The keyboard's way in stays built, or Tab could not reach the plan.
1639            if !in_view && !is_tab_stop {
1640                continue;
1641            }
1642            let dragging = current.filter(|p| p.change.id == Some(event.id)).map(|p| p.allowed);
1643            let title = match dragging {
1644                Some(_) => time_range(&labels, axis.scale, change.start, change.end),
1645                None => event.title.clone(),
1646            };
1647            let bar = Bar {
1648                detail: self.event_detail.as_ref().and_then(|detail| detail(event.id)),
1649                at: (change.start, change.end),
1650                menu: self.event_menu.clone(),
1651                tinted: self.card.then(|| theme.color(Semantic::SurfaceElevated)),
1652                event,
1653                frame: if self.card {
1654                    // A little air between the bars of neighboring days.
1655                    Rect::new(
1656                        x + CARD_BAR_GAP,
1657                        y,
1658                        (bar_width - 2.0 * CARD_BAR_GAP).max(MIN_BAR_WIDTH),
1659                        lane_height - 4.0,
1660                    )
1661                } else {
1662                    Rect::new(x, y, bar_width, lane_height - 4.0)
1663                },
1664                title,
1665                dragging,
1666                selected: selected == Some(event.id),
1667                tab_stop: is_tab_stop,
1668                editable: !self.read_only && self.on_change.is_some(),
1669                editing: editing.clone(),
1670                selection: Some(selection.clone()),
1671                on_open: self.on_open.clone(),
1672                on_delete: self.on_delete.clone(),
1673            };
1674            bars.push(cx.keyed_child(event.id, &bar));
1675        }
1676        // What is being drawn.
1677        if let Some(Preview { change, allowed }) = current.filter(|p| p.change.id.is_none())
1678            && let Some(row_top) = top_of.get(&change.resource)
1679        {
1680            let x = axis.x_of(change.start);
1681            let frame = Rect::new(
1682                x,
1683                row_top + ROW_PADDING + 2.0,
1684                (axis.x_of(change.end) - x).max(MIN_BAR_WIDTH),
1685                lane_height - 4.0,
1686            );
1687            let color = if allowed { theme.color(Semantic::Accent) } else { theme.color(Semantic::Danger) };
1688            bars.push(band(cx, frame, color.with_alpha(0.45)));
1689        }
1690        if let Some(now) = self.now.filter(|_| !self.card) {
1691            let x = axis.x_of(now);
1692            if x >= 0.0 && x <= width {
1693                bars.push(band(cx, Rect::new(x, 0.0, 2.0, total_height), theme.color(Semantic::Danger)));
1694            }
1695        }
1696        under.extend(bars);
1697
1698        let lanes = Lanes { width, height: area, children: Rc::new(under), draw };
1699        let content = lanes.build(cx, self, &editing);
1700        let body = cx.container(
1701            LayoutStyle {
1702                grow: 1.0,
1703                min_width: Some(0.0),
1704                min_height: Some(0.0),
1705                overflow_hidden: true,
1706                ..LayoutStyle::stack(Axis::Vertical)
1707            },
1708            &[content],
1709            Paint::None,
1710        );
1711        cx.scrollable_virtual(body, true);
1712        let (pinched, zooming): (Rc<Cell<f32>>, _) =
1713            (cx.use_state(|| Rc::new(Cell::new(0.0))).get(), self.axis.clone());
1714        cx.on_pinch(
1715            body,
1716            Rc::new(move |delta| {
1717                pinched.set(pinched.get() + delta);
1718                if pinched.get().abs() < PINCH_STEP {
1719                    return;
1720                }
1721                let current = zooming.get();
1722                let next = if pinched.get() > 0.0 { current.zoomed_in() } else { current.zoomed_out() };
1723                pinched.set(0.0);
1724                if let Some(next) = next {
1725                    zooming.set(next);
1726                }
1727            }),
1728        );
1729        cx.semantics(body, Semantics::new(Role::Grid).name(labels.scheduler_resources.clone()));
1730
1731        // The names follow the vertical scroll.
1732        let names_content = cx.container(
1733            LayoutStyle {
1734                width: Some(self.resource_width),
1735                height: Some(total_height),
1736                ..LayoutStyle::stack(Axis::Vertical)
1737            },
1738            &names,
1739            Paint::None,
1740        );
1741        cx.translate(names_content, Point::new(0.0, -scroll.y));
1742        let names_view = cx.container(
1743            LayoutStyle {
1744                width: Some(self.resource_width),
1745                min_height: Some(0.0),
1746                overflow_hidden: true,
1747                ..LayoutStyle::stack(Axis::Vertical)
1748            },
1749            &[names_content],
1750            Paint::None,
1751        );
1752        cx.clip(names_view);
1753        cx.scroll_with_widget(names_view);
1754        let mut middle_parts = vec![names_view];
1755        if self.card {
1756            middle_parts.push(divider(cx));
1757        }
1758        middle_parts.push(body);
1759        let middle = cx.container(
1760            LayoutStyle {
1761                grow: 1.0,
1762                min_height: Some(0.0),
1763                align: Align::Stretch,
1764                overflow_hidden: true,
1765                ..LayoutStyle::stack(Axis::Horizontal)
1766            },
1767            &middle_parts,
1768            Paint::None,
1769        );
1770
1771        let hairline = |cx: &mut BuildCx<'_>| {
1772            cx.container(
1773                LayoutStyle { height: Some(1.0), ..LayoutStyle::leaf() },
1774                &[],
1775                Paint::Quad(QuadPaint::new(line)),
1776            )
1777        };
1778        let mut parts = Vec::new();
1779        if self.toolbar {
1780            parts.push(self.toolbar_view(cx, axis));
1781            if self.card {
1782                parts.push(hairline(cx));
1783            }
1784        }
1785        parts.push(header);
1786        if self.card {
1787            parts.push(hairline(cx));
1788        }
1789        parts.push(middle);
1790        let root = cx.container(
1791            LayoutStyle {
1792                grow: 1.0,
1793                min_height: Some(0.0),
1794                align: Align::Stretch,
1795                overflow_hidden: true,
1796                ..LayoutStyle::stack(Axis::Vertical)
1797            },
1798            &parts,
1799            Paint::Quad(QuadPaint {
1800                corner_radius: if self.card { theme.radii.xl } else { theme.radii.md },
1801                border: Some(Border { width: 1.0, color: line }),
1802                shadow: self.card.then_some(theme.elevation.soft),
1803                ..QuadPaint::new(theme.color(Semantic::SurfaceElevated))
1804            }),
1805        );
1806        cx.clip(root);
1807        root
1808    }
1809}
1810
1811/// Hands the height left over to the resource rows, evenly, and says the new total.
1812fn share_height(rows: &mut [Row], total: f32, available: f32) -> f32 {
1813    let count = rows.iter().filter(|row| matches!(row, Row::Resource { .. })).count();
1814    if count == 0 || available <= total {
1815        return total;
1816    }
1817    let extra = (available - total) / count as f32;
1818    let mut shift = 0.0;
1819    for row in rows {
1820        match row {
1821            Row::Group { y, .. } => *y += shift,
1822            Row::Resource { y, height, .. } => {
1823                *y += shift;
1824                *height += extra;
1825                shift += extra;
1826            }
1827        }
1828    }
1829    available
1830}
1831
1832fn hash_text(text: &str) -> u64 {
1833    text.bytes()
1834        .fold(0xcbf2_9ce4_8422_2325, |hash, byte| (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3))
1835}
1836
1837struct GroupHeader {
1838    name: String,
1839    collapsed: bool,
1840    state: State<BTreeSet<String>>,
1841    children: Rc<Vec<NodeId>>,
1842    frame: Rect,
1843}
1844
1845impl View for GroupHeader {
1846    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1847        let radius = cx.theme().radii.sm;
1848        let node = cx.container(
1849            LayoutStyle {
1850                absolute: Some((self.frame.x(), self.frame.y())),
1851                width: Some(self.frame.width()),
1852                height: Some(self.frame.height()),
1853                padding: Insets::symmetric(0.0, 8.0),
1854                spacing: 6.0,
1855                align: Align::Center,
1856                ..LayoutStyle::stack(Axis::Horizontal)
1857            },
1858            &self.children,
1859            Paint::None,
1860        );
1861        cx.semantics(node, Semantics::new(Role::Button).name(self.name.clone()).expanded(!self.collapsed));
1862        let (state, name) = (self.state.clone(), self.name.clone());
1863        cx.on_click(
1864            node,
1865            Rc::new(move || {
1866                state.update(|set| {
1867                    if !set.remove(&name) {
1868                        set.insert(name.clone());
1869                    }
1870                });
1871            }),
1872        );
1873        cx.focusable(node, radius);
1874        cx.cursor(node, CursorIcon::Pointer);
1875        node
1876    }
1877}