Skip to main content

sequoia_elements/
container.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Edges, Length};
5use sequoia_input::{CursorIcon, Event, EventPhase, Key, PointerButton, PointerEvent};
6use sequoia_layout::{
7    Align, Axis, ContainerStyle, Inset, Justify, LayoutStyle, Overflow, Position,
8};
9use sequoia_reactive::{Environment, Signal};
10use sequoia_style::{BoxStyle, Theme, VisualState};
11use sequoia_view::{AnyView, EventCtx, Live, PaintCtx, Prop, Unit, View};
12
13type EnvAction = Rc<dyn Fn(&Environment)>;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum ClickTrigger {
18    #[default]
19    Release,
20
21    Press,
22}
23
24#[derive(Clone)]
25pub struct Container {
26    child: Option<AnyView>,
27    deco: BoxStyle,
28    style: LayoutStyle,
29    layout: ContainerStyle,
30    clip: bool,
31    on_click: Option<Rc<dyn Fn()>>,
32    on_click_env: Option<EnvAction>,
33    trigger: ClickTrigger,
34    disabled: bool,
35    hover: Option<Color>,
36    pressed: Option<Color>,
37    disabled_background: Option<Color>,
38    background_live: Option<Live<Color>>,
39
40    blend_rate: f32,
41    hovered: Option<Signal<bool>>,
42    pressed_flag: Rc<Cell<bool>>,
43}
44
45impl Default for Container {
46    fn default() -> Self {
47        Self {
48            child: None,
49            deco: BoxStyle::new(),
50            clip: false,
51            style: LayoutStyle {
52                width: Length::Fit,
53                height: Length::Fit,
54                padding: Edges::ZERO,
55                ..LayoutStyle::default()
56            },
57            layout: ContainerStyle::default(),
58            on_click: None,
59            on_click_env: None,
60            trigger: ClickTrigger::default(),
61            disabled: false,
62            hover: None,
63            pressed: None,
64            disabled_background: None,
65            background_live: None,
66            blend_rate: 0.0,
67            hovered: None,
68            pressed_flag: Rc::new(Cell::new(false)),
69        }
70    }
71}
72
73impl Container {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    pub fn fill() -> Self {
79        Self {
80            child: None,
81            deco: BoxStyle::new(),
82            clip: false,
83            style: LayoutStyle {
84                width: Length::Fill(1.0),
85                height: Length::Fill(1.0),
86                padding: Edges::ZERO,
87                ..LayoutStyle::default()
88            },
89            layout: ContainerStyle::default(),
90            on_click: None,
91            on_click_env: None,
92            trigger: ClickTrigger::default(),
93            disabled: false,
94            hover: None,
95            pressed: None,
96            disabled_background: None,
97            background_live: None,
98            blend_rate: 0.0,
99            hovered: None,
100            pressed_flag: Rc::new(Cell::new(false)),
101        }
102    }
103
104    pub fn hug() -> Self {
105        Self::new()
106    }
107
108    pub fn child<V: View + 'static>(mut self, child: V) -> Self {
109        debug_assert!(
110            self.child.is_none(),
111            "Container takes a single child - a second `.child()` would overwrite the first. Use Row/Column for many."
112        );
113        self.child = Some(AnyView::new(child));
114        self
115    }
116
117    pub fn content<V: View + 'static>(self, child: V) -> Self {
118        self.child(child)
119    }
120
121    pub fn child_if<V: View + 'static>(mut self, cond: bool, make: impl FnOnce() -> V) -> Self {
122        if cond {
123            self.child = Some(AnyView::new(make()));
124        }
125        self
126    }
127
128    pub fn child_opt<V: View + 'static>(mut self, child: Option<V>) -> Self {
129        if let Some(v) = child {
130            self.child = Some(AnyView::new(v));
131        }
132        self
133    }
134
135    /// Sets the background from a color or a live color.
136    ///
137    /// `Live<Color>` accepts signals, memos, derived values, and animation
138    /// specs, so animated backgrounds work on `Container` without wrapping it
139    /// in a separate view.
140    pub fn background(mut self, color: impl Into<Live<Color>>) -> Self {
141        let live = color.into();
142        self.deco = self.deco.background(live.target_untracked());
143        self.background_live = Some(live);
144        self
145    }
146
147    pub fn border(mut self, width: f32, color: Color) -> Self {
148        self.deco = self.deco.border(width, color);
149        self
150    }
151
152    pub fn border_left(mut self, width: f32, color: Color) -> Self {
153        self.deco = self.deco.border_left(width, color);
154        self
155    }
156
157    pub fn border_right(mut self, width: f32, color: Color) -> Self {
158        self.deco = self.deco.border_right(width, color);
159        self
160    }
161
162    pub fn border_top(mut self, width: f32, color: Color) -> Self {
163        self.deco = self.deco.border_top(width, color);
164        self
165    }
166
167    pub fn border_bottom(mut self, width: f32, color: Color) -> Self {
168        self.deco = self.deco.border_bottom(width, color);
169        self
170    }
171
172    pub fn border_x(mut self, width: f32, color: Color) -> Self {
173        self.deco = self.deco.border_x(width, color);
174        self
175    }
176
177    pub fn border_y(mut self, width: f32, color: Color) -> Self {
178        self.deco = self.deco.border_y(width, color);
179        self
180    }
181
182    pub fn box_style(mut self, style: BoxStyle) -> Self {
183        self.deco = style;
184        self
185    }
186
187    pub fn style(self, style: BoxStyle) -> Self {
188        self.box_style(style)
189    }
190
191    pub fn radius(mut self, radius: f32) -> Self {
192        self.deco = self.deco.radius(radius);
193        self
194    }
195
196    pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
197        self.style = self.style.padding(padding);
198        self
199    }
200
201    pub fn pad(self, padding: impl Into<Edges>) -> Self {
202        self.padding(padding)
203    }
204
205    pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
206        self.style = self.style.padding_xy(x, y);
207        self
208    }
209
210    pub fn padding_x(mut self, x: f32) -> Self {
211        self.style = self.style.padding_x(x);
212        self
213    }
214
215    pub fn padding_y(mut self, y: f32) -> Self {
216        self.style = self.style.padding_y(y);
217        self
218    }
219
220    pub fn padding_left(mut self, v: f32) -> Self {
221        self.style = self.style.padding_left(v);
222        self
223    }
224
225    pub fn padding_right(mut self, v: f32) -> Self {
226        self.style = self.style.padding_right(v);
227        self
228    }
229
230    pub fn padding_top(mut self, v: f32) -> Self {
231        self.style = self.style.padding_top(v);
232        self
233    }
234
235    pub fn padding_bottom(mut self, v: f32) -> Self {
236        self.style = self.style.padding_bottom(v);
237        self
238    }
239
240    pub fn width(mut self, width: impl Into<Length>) -> Self {
241        self.style = self.style.width(width.into());
242        self
243    }
244
245    pub fn height(mut self, height: impl Into<Length>) -> Self {
246        self.style = self.style.height(height.into());
247        self
248    }
249
250    pub fn size(self, side: impl Into<Length>) -> Self {
251        self.square(side)
252    }
253
254    pub fn square(mut self, side: impl Into<Length>) -> Self {
255        let side = side.into();
256        self.style = self.style.width(side).height(side);
257        self
258    }
259
260    pub fn min_width(mut self, v: f32) -> Self {
261        self.style = self.style.min_width(v);
262        self
263    }
264
265    pub fn max_width(mut self, v: f32) -> Self {
266        self.style = self.style.max_width(v);
267        self
268    }
269
270    pub fn min_height(mut self, v: f32) -> Self {
271        self.style = self.style.min_height(v);
272        self
273    }
274
275    pub fn max_height(mut self, v: f32) -> Self {
276        self.style = self.style.max_height(v);
277        self
278    }
279
280    pub fn overflow(mut self, overflow: Overflow) -> Self {
281        self.style = self.style.overflow(overflow);
282        self
283    }
284
285    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
286        self.style = self.style.margin(margin);
287        self
288    }
289
290    pub fn margin_xy(mut self, x: f32, y: f32) -> Self {
291        self.style = self.style.margin_xy(x, y);
292        self
293    }
294
295    pub fn margin_x(mut self, x: f32) -> Self {
296        self.style = self.style.margin_x(x);
297        self
298    }
299
300    pub fn margin_y(mut self, y: f32) -> Self {
301        self.style = self.style.margin_y(y);
302        self
303    }
304
305    pub fn margin_left(mut self, v: f32) -> Self {
306        self.style = self.style.margin_left(v);
307        self
308    }
309
310    pub fn margin_right(mut self, v: f32) -> Self {
311        self.style = self.style.margin_right(v);
312        self
313    }
314
315    pub fn margin_top(mut self, v: f32) -> Self {
316        self.style = self.style.margin_top(v);
317        self
318    }
319
320    pub fn margin_bottom(mut self, v: f32) -> Self {
321        self.style = self.style.margin_bottom(v);
322        self
323    }
324
325    pub fn flex(mut self, weight: f32) -> Self {
326        self.style = self.style.flex(weight);
327        self
328    }
329
330    pub fn flex_shrink(mut self, shrink: f32) -> Self {
331        self.style = self.style.flex_shrink(shrink);
332        self
333    }
334
335    pub fn flex_basis(mut self, basis: Option<Length>) -> Self {
336        self.style = self.style.flex_basis(basis);
337        self
338    }
339
340    pub fn align_self(mut self, align: Option<Align>) -> Self {
341        self.style = self.style.align_self(align);
342        self
343    }
344
345    pub fn aspect(mut self, ratio: f32) -> Self {
346        self.style = self.style.aspect(ratio);
347        self
348    }
349
350    pub fn position(mut self, position: Position) -> Self {
351        self.style = self.style.position(position);
352        self
353    }
354
355    pub fn absolute(mut self) -> Self {
356        self.style = self.style.absolute();
357        self
358    }
359
360    pub fn absolute_at(mut self, left: f32, top: f32) -> Self {
361        self.style = self.style.absolute_at(left, top);
362        self
363    }
364
365    pub fn absolute_inset(mut self, inset: Inset) -> Self {
366        self.style = self.style.absolute_inset(inset);
367        self
368    }
369
370    pub fn expand(self) -> Self {
371        self.flex(1.0)
372    }
373
374    pub fn fill_width(mut self) -> Self {
375        self.style = self.style.width(Length::Fill(1.0));
376        self
377    }
378
379    pub fn fill_height(mut self) -> Self {
380        self.style = self.style.height(Length::Fill(1.0));
381        self
382    }
383
384    pub fn fill_size(self) -> Self {
385        self.fill_width().fill_height()
386    }
387
388    pub fn fit(mut self) -> Self {
389        self.style = self.style.fit();
390        self
391    }
392
393    pub fn container_style(mut self, layout: ContainerStyle) -> Self {
394        self.layout = layout;
395        self
396    }
397
398    pub fn axis(mut self, axis: Axis) -> Self {
399        self.layout.axis = axis;
400        self
401    }
402
403    pub fn justify_content(mut self, justify: Justify) -> Self {
404        self.layout.justify = justify;
405        self
406    }
407
408    pub fn justify(self, justify: Justify) -> Self {
409        self.justify_content(justify)
410    }
411
412    pub fn align_items(mut self, align: Align) -> Self {
413        self.layout.align = align;
414        self
415    }
416
417    pub fn align(self, align: Align) -> Self {
418        self.align_items(align)
419    }
420
421    pub fn center(mut self) -> Self {
422        self.layout.align = Align::Center;
423        self.layout.justify = Justify::Center;
424        self
425    }
426
427    pub fn hit_testable(mut self, on: bool) -> Self {
428        self.style = self.style.hit_testable(on);
429        self
430    }
431
432    pub fn clip(mut self, clip: bool) -> Self {
433        self.clip = clip;
434        self
435    }
436
437    pub fn clip_children(self) -> Self {
438        self.clip(true)
439    }
440
441    fn is_interactive(&self) -> bool {
442        self.on_click.is_some() || self.on_click_env.is_some()
443    }
444
445    pub fn on_click(mut self, on_click: impl Fn() + 'static) -> Self {
446        self.on_click = Some(Rc::new(on_click));
447        self
448    }
449
450    pub fn on_press(mut self, on_press: impl Fn() + 'static) -> Self {
451        self.on_click = Some(Rc::new(on_press));
452        self.trigger = ClickTrigger::Press;
453        self
454    }
455
456    pub fn trigger(mut self, trigger: ClickTrigger) -> Self {
457        self.trigger = trigger;
458        self
459    }
460
461    pub fn on_click_env(mut self, on_click: impl Fn(&Environment) + 'static) -> Self {
462        self.on_click_env = Some(Rc::new(on_click));
463        self
464    }
465
466    pub fn hovered(mut self, signal: Signal<bool>) -> Self {
467        self.hovered = Some(signal);
468        self
469    }
470
471    pub fn on_hover(mut self, hovered: Signal<bool>) -> Self {
472        self.hovered = Some(hovered);
473        self
474    }
475
476    pub fn disabled(mut self, disabled: bool) -> Self {
477        self.disabled = disabled;
478        self
479    }
480
481    pub fn hover(mut self, color: Color) -> Self {
482        self.hover = Some(color);
483        self
484    }
485
486    pub fn pressed(mut self, color: Color) -> Self {
487        self.pressed = Some(color);
488        self
489    }
490
491    pub fn disabled_background(mut self, color: Color) -> Self {
492        self.disabled_background = Some(color);
493        self
494    }
495
496    pub fn blend_rate(mut self, rate: f32) -> Self {
497        self.blend_rate = rate;
498        self
499    }
500
501    #[cfg(test)]
502    pub(crate) fn effective_blend_rate(&self) -> f32 {
503        self.blend_rate
504    }
505
506    fn idle_background(&self) -> Color {
507        self.background_live
508            .as_ref()
509            .map(|live| live.target_untracked())
510            .unwrap_or(self.deco.background)
511    }
512
513    fn state_background(&self, theme: &Theme, mut state: VisualState, idle: Color) -> Color {
514        if self.disabled {
515            state.disabled = true;
516        }
517        if state.disabled {
518            self.disabled_background.unwrap_or(theme.disabled)
519        } else if state.pressed {
520            self.pressed.unwrap_or(idle)
521        } else if state.hovered {
522            self.hover.unwrap_or(idle)
523        } else {
524            idle
525        }
526    }
527
528    pub fn target_background(&self, theme: &Theme, state: VisualState) -> Color {
529        self.state_background(theme, state, self.idle_background())
530    }
531
532    fn fire(&self, env: &Environment) {
533        if let Some(f) = &self.on_click {
534            f();
535        }
536        if let Some(f) = &self.on_click_env {
537            f(env);
538        }
539    }
540}
541
542impl View for Container {
543    fn body(&self) -> impl View + 'static {
544        Unit
545    }
546    fn clip_self(&self) -> bool {
547        self.clip
548    }
549    fn clip_radius(&self) -> f32 {
550        if self.clip { self.deco.radius } else { 0.0 }
551    }
552    fn children_override(&self) -> Option<Vec<AnyView>> {
553        self.child.clone().map(|c| vec![c])
554    }
555    fn layout_style(&self) -> LayoutStyle {
556        let mut style = self.style;
557        let edges = self.deco.border_edges();
558        style.padding.left += edges.left;
559        style.padding.top += edges.top;
560        style.padding.right += edges.right;
561        style.padding.bottom += edges.bottom;
562        style
563    }
564    fn container(&self) -> Option<ContainerStyle> {
565        (self.layout != ContainerStyle::default()).then_some(self.layout)
566    }
567    fn paint_self(&self, ctx: &mut PaintCtx) {
568        if let Some(sig) = &self.hovered {
569            let hovered = ctx.hovered_subtree;
570            if sig.get_untracked() != hovered {
571                sig.set(hovered);
572            }
573        }
574        let idle = match &self.background_live {
575            Some(live) if live.is_animated() => {
576                ctx.runtime()
577                    .live_color(
578                        ctx.element,
579                        Prop::BACKGROUND,
580                        live.target_untracked(),
581                        live.tween_spec().as_ref(),
582                    )
583                    .0
584            }
585            Some(live) => live.target_untracked(),
586            None => self.deco.background,
587        };
588        if !self.is_interactive() {
589            let mut deco = self.deco;
590            deco.background = idle;
591            ctx.paint_box(deco);
592            return;
593        }
594        let target = self.state_background(&ctx.theme(), ctx.state, idle);
595        let bg = if self.blend_rate <= 0.0 {
596            target
597        } else {
598            ctx.live_color_exp(Prop::BACKGROUND, target, self.blend_rate)
599        };
600        let mut deco = self.deco;
601        deco.background = bg;
602        ctx.paint_box(deco);
603    }
604
605    fn has_visual_state(&self) -> bool {
606        self.is_interactive()
607    }
608
609    fn focusable(&self) -> bool {
610        self.is_interactive() && !self.disabled
611    }
612
613    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
614        if self.is_interactive() && !self.disabled {
615            CursorIcon::Pointer
616        } else {
617            CursorIcon::Default
618        }
619    }
620
621    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
622        if self.disabled || !self.is_interactive() {
623            return;
624        }
625        match event {
626            Event::Pointer(pe) => match pe {
627                PointerEvent::Down {
628                    button: PointerButton::Left,
629                    pos,
630                    ..
631                } => {
632                    let inside = ctx.rect.contains(*pos);
633                    self.pressed_flag.set(inside);
634                    if self.trigger == ClickTrigger::Press
635                        && ctx.phase != EventPhase::Capture
636                        && inside
637                    {
638                        self.pressed_flag.set(false);
639                        self.fire(ctx.environment);
640                        ctx.handled();
641                    }
642                }
643                PointerEvent::Up {
644                    button: PointerButton::Left,
645                    pos,
646                    ..
647                } if ctx.phase != EventPhase::Capture
648                    && self.pressed_flag.take()
649                    && ctx.rect.contains(*pos) =>
650                {
651                    self.fire(ctx.environment);
652                    ctx.handled();
653                }
654                _ => {}
655            },
656
657            Event::Key(key)
658                if ctx.phase == EventPhase::Target
659                    && ctx.state.focused
660                    && (key.key == Key::ENTER || key.key == Key::SPACE) =>
661            {
662                self.fire(ctx.environment);
663                ctx.handled();
664            }
665            _ => {}
666        }
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673
674    #[test]
675    fn background_accepts_live_colors() {
676        let bg = Signal::new(Color::RED);
677        let c = Container::new().background(bg.clone());
678        assert_eq!(
679            c.target_background(&Theme::dark(), VisualState::default()),
680            Color::RED
681        );
682
683        bg.set(Color::BLUE);
684        assert_eq!(
685            c.target_background(&Theme::dark(), VisualState::default()),
686            Color::BLUE
687        );
688    }
689}