feather_ui/component/
paragraph.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use crate::color::sRGB;
5use crate::component::ChildOf;
6use crate::component::text::Text;
7use crate::layout::{Desc, Layout, base, flex, leaf};
8use crate::persist::{FnPersist, VectorMap};
9use crate::{SourceID, UNSIZED_AXIS, gen_id, layout};
10use core::f32;
11use derive_where::derive_where;
12use std::rc::Rc;
13use std::sync::Arc;
14
15#[derive(feather_macro::StateMachineChild)]
16#[derive_where(Clone)]
17pub struct Paragraph<T> {
18    pub id: Arc<SourceID>,
19    props: Rc<T>,
20    children: im::Vector<Option<Box<ChildOf<dyn flex::Prop>>>>,
21}
22
23#[derive(Clone, Copy, Default, PartialEq, PartialOrd)]
24struct MinimalFlexChild {
25    grow: f32,
26}
27
28impl flex::Child for MinimalFlexChild {
29    fn grow(&self) -> f32 {
30        self.grow
31    }
32
33    fn shrink(&self) -> f32 {
34        0.0
35    }
36
37    fn basis(&self) -> crate::DValue {
38        crate::DValue {
39            dp: 0.0,
40            px: 0.0,
41            rel: UNSIZED_AXIS,
42        }
43    }
44}
45
46impl base::Area for MinimalFlexChild {
47    fn area(&self) -> &crate::DRect {
48        &crate::AUTO_DRECT
49    }
50}
51
52impl base::Anchor for MinimalFlexChild {}
53impl base::Order for MinimalFlexChild {}
54impl base::Margin for MinimalFlexChild {}
55impl base::RLimits for MinimalFlexChild {}
56impl base::Limits for MinimalFlexChild {}
57impl base::Padding for MinimalFlexChild {}
58impl leaf::Prop for MinimalFlexChild {}
59impl leaf::Padded for MinimalFlexChild {}
60
61impl<T: flex::Prop + 'static> Paragraph<T> {
62    pub fn new(id: Arc<SourceID>, props: T) -> Self {
63        Self {
64            id,
65            props: props.into(),
66            children: im::Vector::new(),
67        }
68    }
69
70    pub fn append(&mut self, child: Box<ChildOf<dyn flex::Prop>>) {
71        self.children.push_back(Some(child));
72    }
73
74    pub fn prepend(&mut self, child: Box<ChildOf<dyn flex::Prop>>) {
75        self.children.push_front(Some(child));
76    }
77
78    #[allow(clippy::too_many_arguments)]
79    pub fn set_text(
80        &mut self,
81        text: &str,
82        font_size: f32,
83        line_height: f32,
84        font: cosmic_text::FamilyOwned,
85        color: sRGB,
86        weight: cosmic_text::Weight,
87        style: cosmic_text::Style,
88        fullwidth: bool,
89    ) {
90        self.children.clear();
91        for (i, word) in text.split_ascii_whitespace().enumerate() {
92            let text = Text::<MinimalFlexChild>::new(
93                gen_id!(gen_id!(self.id), i),
94                MinimalFlexChild {
95                    grow: if fullwidth { 1.0 } else { 0.0 },
96                },
97                font_size,
98                line_height,
99                word.to_owned() + " ",
100                font.clone(),
101                color,
102                weight,
103                style,
104                cosmic_text::Wrap::None,
105                None, // paragraph does it's own alignment so we don't set any here
106            );
107            self.children.push_back(Some(Box::new(text)));
108        }
109    }
110}
111
112impl<T: flex::Prop + 'static> super::Component for Paragraph<T> {
113    type Props = T;
114
115    fn layout(
116        &self,
117        manager: &mut crate::StateManager,
118        driver: &crate::graphics::Driver,
119        window: &Arc<SourceID>,
120    ) -> Box<dyn Layout<T>> {
121        let mut map = VectorMap::new(crate::persist::Persist::new(
122            |child: &Option<Box<ChildOf<dyn flex::Prop>>>| -> Option<Box<dyn Layout<<dyn flex::Prop as Desc>::Child>>> {
123                Some(child.as_ref()?.layout(manager, driver, window))
124            })
125        );
126
127        let (_, children) = map.call(Default::default(), &self.children);
128        Box::new(layout::Node::<T, dyn flex::Prop> {
129            props: self.props.clone(),
130            children,
131            id: Arc::downgrade(&self.id),
132            renderable: None,
133            layer: None,
134        })
135    }
136}