1use std::cell::Cell;
4use std::rc::Rc;
5
6use ravenrs_core::{Border, Color, CursorIcon, Insets, Key, Role};
7use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
8use ravenrs_reactive::{Binding, State};
9use ravenrs_style::{Semantic, TextStyle, Weight};
10
11use crate::context::{BuildCx, NodeId, Paint, Placement, QuadPaint, Semantics};
12use crate::date::Date;
13use crate::{ButtonSize, IconButton, IconName, View, util};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
18pub struct DateRange {
19 pub start: Date,
21 pub end: Date,
23}
24
25#[cfg_attr(feature = "live", ravenrs_live::expose)]
26impl DateRange {
27 pub fn new(a: Date, b: Date) -> Self {
29 if a <= b { Self { start: a, end: b } } else { Self { start: b, end: a } }
30 }
31
32 pub fn contains(self, date: Date) -> bool {
34 self.start <= date && date <= self.end
35 }
36
37 pub fn days(self) -> i32 {
39 (self.end.to_days() - self.start.to_days()) as i32 + 1
40 }
41}
42
43#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
58pub struct Calendar {
59 selected: Binding<Option<Date>>,
60 initial_month: Date,
61 today: Option<Date>,
62 min: Option<Date>,
63 max: Option<Date>,
64 on_pick: Option<Rc<dyn Fn(Date)>>,
65 span: Option<DateRange>,
66}
67
68impl std::fmt::Debug for Calendar {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("Calendar").field("month", &self.initial_month).finish_non_exhaustive()
71 }
72}
73
74#[cfg_attr(feature = "live", ravenrs_live::expose)]
75impl Calendar {
76 pub fn new(selected: Binding<Option<Date>>, month: Date) -> Self {
78 Self {
79 selected,
80 initial_month: month.first_of_month(),
81 today: None,
82 min: None,
83 max: None,
84 on_pick: None,
85 span: None,
86 }
87 }
88
89 pub fn today(mut self, today: Date) -> Self {
91 self.today = Some(today);
92 self
93 }
94
95 pub fn range(mut self, min: Option<Date>, max: Option<Date>) -> Self {
97 self.min = min;
98 self.max = max;
99 self
100 }
101
102 pub(crate) fn on_pick(mut self, f: Rc<dyn Fn(Date)>) -> Self {
103 self.on_pick = Some(f);
104 self
105 }
106
107 fn span(mut self, span: Option<DateRange>) -> Self {
109 self.span = span;
110 self
111 }
112}
113
114struct Day<'a> {
115 date: Date,
116 in_month: bool,
117 calendar: &'a Calendar,
118 active: bool,
120 keys: &'a GridKeys,
123}
124
125#[derive(Clone)]
127pub(crate) struct GridKeys {
128 pub month: Binding<Date>,
129 pub cursor: State<Option<Date>>,
130 pub focus_to: Rc<Cell<Option<Date>>>,
131}
132
133#[cfg_attr(feature = "live", ravenrs_live::expose)]
134impl GridKeys {
135 pub fn handler(&self, from: Date) -> crate::context::KeyHandler {
137 let keys = self.clone();
138 Rc::new(move |key, _| {
139 let to = match key {
140 Key::ArrowLeft => from.add_days(-1),
141 Key::ArrowRight => from.add_days(1),
142 Key::ArrowUp => from.add_days(-7),
143 Key::ArrowDown => from.add_days(7),
144 Key::Home => from.add_days(-i32::from(from.weekday())),
145 Key::End => from.add_days(6 - i32::from(from.weekday())),
146 Key::PageUp => from.add_months(-1),
147 Key::PageDown => from.add_months(1),
148 _ => return false,
149 };
150 if to.first_of_month() != keys.month.get() {
151 keys.month.set(to.first_of_month());
152 }
153 keys.cursor.set(Some(to));
154 keys.focus_to.set(Some(to));
155 true
156 })
157 }
158}
159
160impl View for Day<'_> {
161 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
162 let theme = *cx.theme();
163 let c = self.calendar;
164 let is_end = c.span.is_some_and(|s| s.start == self.date || s.end == self.date);
165 let inside = c.span.is_some_and(|s| s.contains(self.date));
166 let selected = c.selected.get() == Some(self.date) || is_end;
167 let enabled = c.min.is_none_or(|m| self.date >= m) && c.max.is_none_or(|m| self.date <= m);
168 let hovered = (cx.is_hovered() && enabled) || cx.is_focus_visible();
169 let ink = theme.color(Semantic::Ink);
170 let color = if selected {
171 theme.color(Semantic::OnAccent)
172 } else if !enabled {
173 theme.color(Semantic::InkMuted).with_alpha(0.4)
174 } else if self.in_month {
175 ink
176 } else {
177 theme.color(Semantic::InkMuted)
178 };
179 let weight = (selected || c.today == Some(self.date)).then_some(Weight::SEMIBOLD);
180 let label = util::label(cx, &self.date.day().to_string(), TextStyle::Callout, weight, color);
181 let fill = if selected {
182 theme.color(Semantic::Accent)
183 } else if hovered {
184 ink.with_alpha(0.08)
185 } else if inside {
186 theme.color(Semantic::Accent).with_alpha(0.14)
187 } else {
188 Color::TRANSPARENT
189 };
190 let border = (c.today == Some(self.date) && !selected)
191 .then(|| Border { width: 1.0, color: theme.color(Semantic::Accent) });
192 let node = cx.container(
193 LayoutStyle {
194 width: Some(36.0),
195 height: Some(36.0),
196 align: Align::Center,
197 justify: Justify::Center,
198 ..LayoutStyle::stack(Axis::Horizontal)
199 },
200 &[label],
201 Paint::Quad(QuadPaint { corner_radius: 9.0, border, ..QuadPaint::new(fill) }),
202 );
203 let labels = cx.labels();
205 let name = labels.long_date(self.date);
206 let mut semantics =
207 Semantics::new(Role::Button).name(name).selected(selected || inside).disabled(!enabled);
208 if c.today == Some(self.date) {
209 semantics = semantics.description(labels.today.clone());
210 }
211 cx.semantics(node, semantics);
212 if self.keys.focus_to.get() == Some(self.date) {
213 self.keys.focus_to.set(None);
214 cx.request_focus();
215 }
216 if self.active {
217 cx.focusable(node, 9.0);
218 cx.on_key(node, self.keys.handler(self.date));
219 }
220 if enabled {
221 let (sel, date, pick) = (c.selected.clone(), self.date, c.on_pick.clone());
222 cx.on_click(
223 node,
224 Rc::new(move || {
225 sel.set(Some(date));
226 if let Some(p) = &pick {
227 p(date);
228 }
229 }),
230 );
231 cx.cursor(node, CursorIcon::Pointer);
232 }
233 node
234 }
235}
236
237impl View for Calendar {
238 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
239 let theme = *cx.theme();
240 let initial = self.selected.get().map_or(self.initial_month, Date::first_of_month);
241 let month: State<Date> = cx.use_state(move || initial);
242 let shown = month.get();
243 let labels = cx.labels();
244 let (prev, next) = (month.clone(), month.clone());
245 let back = cx.child(
246 &IconButton::new(IconName::ChevronLeft)
247 .label(labels.previous_month.clone())
248 .size(ButtonSize::Small)
249 .on_click(move || prev.update(|m| *m = m.add_months(-1))),
250 );
251 let title = util::label(
252 cx,
253 &labels.month_title(shown),
254 TextStyle::Body,
255 Some(Weight::SEMIBOLD),
256 theme.color(Semantic::Ink),
257 );
258 let title = cx.container(
259 LayoutStyle { grow: 1.0, justify: Justify::Center, ..LayoutStyle::stack(Axis::Horizontal) },
260 &[title],
261 Paint::None,
262 );
263 let forward = cx.child(
264 &IconButton::new(IconName::ChevronRight)
265 .label(labels.next_month.clone())
266 .size(ButtonSize::Small)
267 .on_click(move || next.update(|m| *m = m.add_months(1))),
268 );
269 let header = cx.container(
270 LayoutStyle {
271 align: Align::Center,
272 align_self: Some(Align::Stretch),
273 ..LayoutStyle::stack(Axis::Horizontal)
274 },
275 &[back, title, forward],
276 Paint::None,
277 );
278 let mut cells: Vec<NodeId> = labels
279 .weekdays
280 .iter()
281 .map(|d| {
282 let l = util::label(cx, d, TextStyle::Caption, None, theme.color(Semantic::InkMuted));
283 cx.semantics(l, Semantics::hidden());
285 cx.container(
286 LayoutStyle {
287 width: Some(36.0),
288 height: Some(28.0),
289 align: Align::Center,
290 justify: Justify::Center,
291 ..LayoutStyle::stack(Axis::Horizontal)
292 },
293 &[l],
294 Paint::None,
295 )
296 })
297 .collect();
298 let keys = GridKeys {
299 month: month.binding(),
300 cursor: cx.use_state(|| None),
301 focus_to: cx.use_state(|| Rc::new(Cell::new(None))).get(),
302 };
303 let in_shown = |d: &Date| d.first_of_month() == shown;
306 let active = [keys.cursor.get(), self.selected.get(), self.today]
307 .into_iter()
308 .flatten()
309 .find(in_shown)
310 .unwrap_or(shown);
311 let start = shown.add_days(-i32::from(shown.weekday()));
312 for i in 0..42 {
313 let date = start.add_days(i);
314 let in_month = date.month() == shown.month();
315 let day = Day { date, in_month, calendar: self, active: date == active, keys: &keys };
316 cells.push(cx.keyed_child(date.to_days() as u64, &day));
317 }
318 let grid = cx.container(
319 LayoutStyle { grid_columns: Some(7), spacing: 2.0, ..LayoutStyle::stack(Axis::Vertical) },
320 &cells,
321 Paint::None,
322 );
323 cx.semantics(grid, Semantics::new(Role::Calendar).name(shown.month_title()));
324 cx.container(
325 LayoutStyle {
326 spacing: 10.0,
327 width: Some(7.0 * 36.0 + 12.0),
328 align_self: Some(Align::Fit),
329 ..LayoutStyle::stack(Axis::Vertical)
330 },
331 &[header, grid],
332 Paint::None,
333 )
334 }
335}
336
337#[derive(Debug)]
339#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
340pub struct DatePicker {
341 value: Binding<Option<Date>>,
342 month: Date,
343 placeholder: Option<String>,
344 today: Option<Date>,
345 width: f32,
346 fill: bool,
347 label: Option<String>,
348 state: util::EditorState,
349}
350
351#[cfg_attr(feature = "live", ravenrs_live::expose)]
352impl DatePicker {
353 pub fn new(value: Binding<Option<Date>>, month: Date) -> Self {
355 Self {
356 value,
357 month,
358 placeholder: None,
359 today: None,
360 width: 220.0,
361 fill: false,
362 label: None,
363 state: util::EditorState::default(),
364 }
365 }
366
367 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
369 self.placeholder = Some(text.into());
370 self
371 }
372
373 pub fn today(mut self, today: Date) -> Self {
375 self.today = Some(today);
376 self
377 }
378
379 pub fn fill(mut self) -> Self {
381 self.fill = true;
382 self
383 }
384
385 pub fn width(mut self, width: f32) -> Self {
387 self.width = width;
388 self
389 }
390
391 pub fn disabled(mut self, disabled: bool) -> Self {
393 self.state.disabled = disabled;
394 self
395 }
396
397 pub fn read_only(mut self, read_only: bool) -> Self {
399 self.state.read_only = read_only;
400 self
401 }
402}
403
404struct CalendarPanel<'a>(&'a DatePicker, State<bool>);
405
406impl View for CalendarPanel<'_> {
407 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
408 let theme = *cx.theme();
409 let close = self.1.clone();
410 let mut cal =
411 Calendar::new(self.0.value.clone(), self.0.month).on_pick(Rc::new(move |_| close.set(false)));
412 if let Some(t) = self.0.today {
413 cal = cal.today(t);
414 }
415 let child = cx.child(&cal);
416 let node = cx.container(
417 LayoutStyle { padding: Insets::uniform(12.0), ..LayoutStyle::stack(Axis::Vertical) },
418 &[child],
419 Paint::Quad(QuadPaint {
420 corner_radius: theme.radii.lg,
421 border: Some(Border { width: 1.0, color: theme.color(Semantic::Border) }),
422 shadow: Some(theme.elevation.raised),
423 ..QuadPaint::new(theme.color(Semantic::SurfaceElevated))
424 }),
425 );
426 cx.cursor(node, CursorIcon::Default);
427 node
428 }
429}
430
431#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
432impl DatePicker {
433 pub fn label(mut self, label: impl Into<String>) -> Self {
436 self.label = Some(label.into());
437 self
438 }
439}
440
441impl View for DatePicker {
442 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
443 let theme = *cx.theme();
444 let open: State<bool> = cx.use_state(|| false);
445 let state = self.state;
446 let is_open = open.get() && state.accepts_input();
447 let hovered = cx.is_hovered() && state.accepts_input();
448 let icon = util::icon(cx, IconName::Calendar, 16.0, state.ink(theme.color(Semantic::InkMuted)));
449 let labels = cx.labels();
450 let (text, color) = match self.value.get() {
451 Some(d) => (labels.short_date(d), theme.color(Semantic::Ink)),
452 None => {
453 let hint = self.placeholder.clone().unwrap_or_else(|| labels.pick_date.clone());
454 (hint, theme.color(Semantic::InkMuted))
455 }
456 };
457 let label = util::label(cx, &text, TextStyle::Body, None, state.ink(color));
458 let border = if is_open {
459 Border { width: 1.5, color: theme.color(Semantic::Accent) }
460 } else {
461 let base = theme.color(Semantic::Border);
462 Border {
463 width: 1.0,
464 color: state.ink(if hovered { base.lerp(theme.color(Semantic::Ink), 0.25) } else { base }),
465 }
466 };
467 let node = cx.container(
468 LayoutStyle {
469 width: (!self.fill).then_some(self.width),
470 height: Some(40.0),
471 padding: Insets::symmetric(0.0, 12.0),
472 spacing: 10.0,
473 align: Align::Center,
474 align_self: Some(if self.fill { Align::Stretch } else { Align::Fit }),
475 ..LayoutStyle::stack(Axis::Horizontal)
476 },
477 &[icon, label],
478 Paint::Quad(QuadPaint {
479 corner_radius: theme.radii.md,
480 border: Some(border),
481 ..QuadPaint::new(state.fill(theme.color(Semantic::Control)))
482 }),
483 );
484 cx.semantics(label, Semantics::hidden());
485 let value = self.value.get().map(|d| labels.long_date(d)).unwrap_or_default();
486 let mut semantics = Semantics::new(Role::ComboBox).value(value).expanded(is_open);
487 if let Some(label) = &self.label {
488 semantics = semantics.name(label.clone());
489 }
490 cx.semantics(node, state.semantics(semantics));
491 if state.disabled {
492 return node;
493 }
494 if !state.accepts_input() {
495 cx.focusable(node, theme.radii.md);
496 return node;
497 }
498 let toggle = open.clone();
499 cx.on_click(node, Rc::new(move || toggle.update(|o| *o = !*o)));
500 cx.focusable(node, theme.radii.md);
501 cx.cursor(node, CursorIcon::Pointer);
502 if is_open {
503 let close = open.clone();
504 cx.overlay(
505 node,
506 Placement::Below,
507 &CalendarPanel(self, open.clone()),
508 Some(Rc::new(move || close.set(false))),
509 );
510 }
511 node
512 }
513}
514
515#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
530pub struct DateRangePicker {
531 value: Binding<Option<DateRange>>,
532 month: Date,
533 today: Option<Date>,
534 min: Option<Date>,
535 max: Option<Date>,
536 presets: Vec<(String, DateRange)>,
537 placeholder: Option<String>,
538 width: f32,
539 label: Option<String>,
540 state: util::EditorState,
541}
542
543impl std::fmt::Debug for DateRangePicker {
544 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545 f.debug_struct("DateRangePicker").field("month", &self.month).finish_non_exhaustive()
546 }
547}
548
549#[cfg_attr(feature = "live", ravenrs_live::expose)]
550impl DateRangePicker {
551 pub fn new(value: Binding<Option<DateRange>>, month: Date) -> Self {
553 Self {
554 value,
555 month,
556 today: None,
557 min: None,
558 max: None,
559 presets: Vec::new(),
560 placeholder: None,
561 width: 340.0,
562 label: None,
563 state: util::EditorState::default(),
564 }
565 }
566
567 pub fn preset(mut self, name: impl Into<String>, range: DateRange) -> Self {
569 self.presets.push((name.into(), range));
570 self
571 }
572
573 pub fn today(mut self, today: Date) -> Self {
575 self.today = Some(today);
576 self
577 }
578
579 pub fn range(mut self, min: Option<Date>, max: Option<Date>) -> Self {
581 self.min = min;
582 self.max = max;
583 self
584 }
585
586 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
588 self.placeholder = Some(text.into());
589 self
590 }
591
592 pub fn width(mut self, width: f32) -> Self {
594 self.width = width;
595 self
596 }
597
598 pub fn label(mut self, label: impl Into<String>) -> Self {
600 self.label = Some(label.into());
601 self
602 }
603
604 pub fn disabled(mut self, disabled: bool) -> Self {
606 self.state.disabled = disabled;
607 self
608 }
609
610 pub fn read_only(mut self, read_only: bool) -> Self {
612 self.state.read_only = read_only;
613 self
614 }
615}
616
617struct RangePanel<'a>(&'a DateRangePicker, State<bool>);
618
619impl View for RangePanel<'_> {
620 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
621 let theme = *cx.theme();
622 let picker = self.0;
623 let started: State<Option<Date>> = cx.use_state(|| None);
624 let pending = started.get();
625 let chosen = picker.value.get();
626 let span = match pending {
627 Some(day) => Some(DateRange::new(day, day)),
628 None => chosen,
629 };
630 let first = chosen.map_or(picker.month, |r| r.start).first_of_month();
631 let pick: Rc<dyn Fn(Date)> = {
632 let (started, value, close) = (started.clone(), picker.value.clone(), self.1.clone());
633 Rc::new(move |day| match started.get() {
634 None => started.set(Some(day)),
635 Some(from) => {
636 started.set(None);
637 value.set(Some(DateRange::new(from, day)));
638 close.set(false);
639 }
640 })
641 };
642 let inert = || Binding::new(|| None, |_: Option<Date>| {});
644 let calendar = |month: Date| {
645 let mut calendar =
646 Calendar::new(inert(), month).range(picker.min, picker.max).on_pick(pick.clone()).span(span);
647 if let Some(today) = picker.today {
648 calendar = calendar.today(today);
649 }
650 calendar
651 };
652 let mut parts = Vec::new();
653 if !picker.presets.is_empty() {
654 let mut rows = Vec::with_capacity(picker.presets.len());
655 for (i, (name, range)) in picker.presets.iter().enumerate() {
656 let (value, close, range) = (picker.value.clone(), self.1.clone(), *range);
657 let button = crate::Button::new(name.clone())
658 .style(crate::ButtonStyle::Ghost)
659 .size(ButtonSize::Small)
660 .on_click(move || {
661 value.set(Some(range));
662 close.set(false);
663 });
664 rows.push(cx.keyed_child(i as u64, &button));
665 }
666 parts.push(cx.container(
667 LayoutStyle { spacing: 2.0, align: Align::Stretch, ..LayoutStyle::stack(Axis::Vertical) },
668 &rows,
669 Paint::None,
670 ));
671 }
672 parts.push(cx.keyed_child(1 << 32, &calendar(first)));
673 if cx.viewport().width >= 720.0 {
675 parts.push(cx.keyed_child(1 << 33, &calendar(first.add_months(1))));
676 }
677 let node = cx.container(
678 LayoutStyle {
679 padding: Insets::uniform(12.0),
680 spacing: 16.0,
681 ..LayoutStyle::stack(Axis::Horizontal)
682 },
683 &parts,
684 Paint::Quad(QuadPaint {
685 corner_radius: theme.radii.lg,
686 border: Some(Border { width: 1.0, color: theme.color(Semantic::Border) }),
687 shadow: Some(theme.elevation.raised),
688 ..QuadPaint::new(theme.color(Semantic::SurfaceElevated))
689 }),
690 );
691 cx.cursor(node, CursorIcon::Default);
692 node
693 }
694}
695
696impl View for DateRangePicker {
697 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
698 let theme = *cx.theme();
699 let open: State<bool> = cx.use_state(|| false);
700 let state = self.state;
701 let is_open = open.get() && state.accepts_input();
702 let hovered = cx.is_hovered() && state.accepts_input();
703 let icon = util::icon(cx, IconName::Calendar, 16.0, state.ink(theme.color(Semantic::InkMuted)));
704 let labels = cx.labels();
705 let chosen = self.value.get();
706 let (text, color) = match chosen {
707 Some(r) => (
708 format!("{} – {}", labels.short_date(r.start), labels.short_date(r.end)),
709 theme.color(Semantic::Ink),
710 ),
711 None => (
712 self.placeholder.clone().unwrap_or_else(|| labels.pick_date_range.clone()),
713 theme.color(Semantic::InkMuted),
714 ),
715 };
716 let label = cx.text_truncated(
717 &text,
718 &util::attrs(&theme, TextStyle::Body, None),
719 state.ink(color),
720 LayoutStyle { grow: 1.0, min_width: Some(0.0), ..LayoutStyle::leaf() },
721 );
722 cx.semantics(label, Semantics::hidden());
723 let mut children = vec![icon, label];
724 if chosen.is_some() && state.accepts_input() {
725 let value = self.value.clone();
726 children.push(
727 cx.child(
728 &IconButton::new(IconName::X)
729 .label(labels.clear.clone())
730 .size(ButtonSize::Small)
731 .on_click(move || value.set(None)),
732 ),
733 );
734 }
735 let border = if is_open {
736 Border { width: 1.5, color: theme.color(Semantic::Accent) }
737 } else {
738 let base = theme.color(Semantic::Border);
739 Border {
740 width: 1.0,
741 color: state.ink(if hovered { base.lerp(theme.color(Semantic::Ink), 0.25) } else { base }),
742 }
743 };
744 let node = cx.container(
745 LayoutStyle {
746 width: Some(self.width),
747 height: Some(40.0),
748 padding: Insets::symmetric(0.0, 12.0),
749 spacing: 10.0,
750 align: Align::Center,
751 align_self: Some(Align::Fit),
752 ..LayoutStyle::stack(Axis::Horizontal)
753 },
754 &children,
755 Paint::Quad(QuadPaint {
756 corner_radius: theme.radii.md,
757 border: Some(border),
758 ..QuadPaint::new(state.fill(theme.color(Semantic::Control)))
759 }),
760 );
761 let mut semantics = Semantics::new(Role::ComboBox).value(text).expanded(is_open);
762 if let Some(label) = &self.label {
763 semantics = semantics.name(label.clone());
764 }
765 cx.semantics(node, state.semantics(semantics));
766 if state.disabled {
767 return node;
768 }
769 cx.focusable(node, theme.radii.md);
770 if !state.accepts_input() {
771 return node;
772 }
773 let toggle = open.clone();
774 cx.on_click(node, Rc::new(move || toggle.update(|o| *o = !*o)));
775 let opener = open.clone();
776 cx.on_key(
777 node,
778 Rc::new(move |key, _| {
779 let opens = matches!(key, Key::ArrowDown | Key::ArrowUp);
780 if opens {
781 opener.set(true);
782 }
783 opens
784 }),
785 );
786 cx.cursor(node, CursorIcon::Pointer);
787 let was_open = cx.use_state(|| Rc::new(Cell::new(false))).get();
788 if is_open {
789 was_open.set(true);
790 let close = open.clone();
791 cx.overlay(
792 node,
793 Placement::Below,
794 &RangePanel(self, open.clone()),
795 Some(Rc::new(move || close.set(false))),
796 );
797 } else if was_open.replace(false) {
798 cx.request_focus();
799 }
800 node
801 }
802}