Skip to main content

anathema_widgets/
paint.rs

1use std::ops::{ControlFlow, Deref};
2
3use anathema_geometry::{LocalPos, Pos, Region, Size};
4use anathema_store::indexmap::IndexMap;
5use anathema_store::slab::SlabIndex;
6use anathema_value_resolver::{AttributeStorage, Attributes};
7use unicode_segmentation::{Graphemes, UnicodeSegmentation};
8use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
9
10use crate::layout::Display;
11use crate::layout::display::DISPLAY;
12use crate::nodes::element::Element;
13use crate::tree::{FilterOutput, WidgetPositionFilter};
14use crate::widget::Style;
15use crate::{PaintChildren, WidgetContainer, WidgetKind};
16
17pub type GlyphMap = IndexMap<GlyphIndex, String>;
18
19pub struct Glyphs<'a> {
20    inner: Graphemes<'a>,
21}
22
23impl<'a> Glyphs<'a> {
24    pub fn new(src: &'a str) -> Self {
25        let inner = src.graphemes(true);
26        Self { inner }
27    }
28
29    pub fn next(&mut self, map: &mut GlyphMap) -> Option<Glyph> {
30        let g = self.inner.next()?;
31        let mut chars = g.chars();
32        let c = chars.next()?;
33
34        match chars.next() {
35            None => Glyph::Single(c, c.width().unwrap_or(0) as u8),
36            Some(_) => {
37                let width = g.width();
38                let glyph = map.insert(g.into());
39                Glyph::Cluster(glyph, width as u8)
40            }
41        }
42        .into()
43    }
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
47pub enum Glyph {
48    // Character and the number of cells it occypies
49    Single(char, u8),
50    // Glyph index and the number of cells it occypies
51    Cluster(GlyphIndex, u8),
52}
53
54impl Glyph {
55    pub fn space() -> Self {
56        Self::Single(' ', 1)
57    }
58
59    pub fn is_newline(&self) -> bool {
60        matches!(self, Self::Single('\n', _))
61    }
62
63    pub fn width(&self) -> usize {
64        match self {
65            Glyph::Single(_, width) | Glyph::Cluster(_, width) => *width as usize,
66        }
67    }
68
69    pub const fn from_char(c: char, width: u8) -> Self {
70        Self::Single(c, width)
71    }
72}
73
74pub trait WidgetRenderer {
75    fn draw_glyph(&mut self, glyph: Glyph, local_pos: Pos);
76
77    fn draw(&mut self) {
78        todo!(
79            "this function is only here to remind us that we should have a raw draw function for Kitty image protocol and such"
80        );
81    }
82
83    fn set_attributes(&mut self, attribs: &Attributes<'_>, local_pos: Pos);
84
85    fn set_style(&mut self, style: Style, local_pos: Pos);
86
87    fn size(&self) -> Size;
88}
89
90#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
91pub struct GlyphIndex(u32);
92
93impl SlabIndex for GlyphIndex {
94    const MAX: usize = u32::MAX as usize;
95
96    fn as_usize(&self) -> usize {
97        self.0 as usize
98    }
99
100    fn from_usize(index: usize) -> Self
101    where
102        Self: Sized,
103    {
104        Self(index as u32)
105    }
106}
107
108#[derive(Debug, Copy, Clone)]
109pub struct PaintFilter(WidgetPositionFilter);
110
111impl PaintFilter {
112    pub fn fixed() -> Self {
113        Self(WidgetPositionFilter::Fixed)
114    }
115
116    pub fn floating() -> Self {
117        Self(WidgetPositionFilter::Floating)
118    }
119}
120
121impl<'bp> crate::widget::Filter<'bp> for PaintFilter {
122    type Output = Element<'bp>;
123
124    fn filter<'a>(
125        &mut self,
126        widget: &'a mut WidgetContainer<'bp>,
127        attribute_storage: &AttributeStorage<'_>,
128    ) -> FilterOutput<&'a mut Self::Output, Self> {
129        match &mut widget.kind {
130            WidgetKind::Element(element) => {
131                let attributes = attribute_storage.get(element.id());
132                match attributes.get_as::<Display>(DISPLAY).unwrap_or_default() {
133                    Display::Show => match self.0 {
134                        WidgetPositionFilter::Floating => match element.is_floating() {
135                            true => FilterOutput::Include(element, PaintFilter::fixed()),
136                            false => FilterOutput::Continue,
137                        },
138                        WidgetPositionFilter::Fixed => match element.is_floating() {
139                            false => FilterOutput::Include(element, *self),
140                            true => FilterOutput::Exclude,
141                        },
142                        WidgetPositionFilter::All => FilterOutput::Include(element, *self),
143                        WidgetPositionFilter::None => FilterOutput::Exclude,
144                    },
145                    Display::Hide | Display::Exclude => FilterOutput::Exclude,
146                }
147            }
148            _ => FilterOutput::Continue,
149        }
150    }
151}
152
153pub fn paint<'bp>(
154    surface: &mut impl WidgetRenderer,
155    glyph_index: &mut GlyphMap,
156    mut widgets: PaintChildren<'_, 'bp>,
157    attribute_storage: &AttributeStorage<'bp>,
158) {
159    _ = widgets.each(|widget, children| {
160        let ctx = PaintCtx::new(surface, None, glyph_index);
161        widget.paint(children, ctx, attribute_storage);
162        ControlFlow::Continue(())
163    });
164}
165
166#[derive(Debug, Copy, Clone)]
167pub struct Unsized;
168
169pub struct SizePos {
170    pub local_size: Size,
171    pub global_pos: Pos,
172}
173
174impl SizePos {
175    pub fn new(local_size: Size, global_pos: Pos) -> Self {
176        Self { local_size, global_pos }
177    }
178}
179
180// -----------------------------------------------------------------------------
181//     - Paint context -
182// -----------------------------------------------------------------------------
183// * Context should draw in local coordinates and translate to the screen
184// * A child always starts at 0, 0 in local space
185/// Paint context used by the widgets to paint.
186/// It works in local coordinates, translated to screen position.
187pub struct PaintCtx<'surface, Size> {
188    surface: &'surface mut dyn WidgetRenderer,
189    pub clip: Option<Region>,
190    pub(crate) state: Size,
191    glyph_map: &'surface mut GlyphMap,
192}
193
194impl<'surface> Deref for PaintCtx<'surface, SizePos> {
195    type Target = SizePos;
196
197    fn deref(&self) -> &Self::Target {
198        &self.state
199    }
200}
201
202impl<'surface> PaintCtx<'surface, Unsized> {
203    pub fn new(
204        surface: &'surface mut dyn WidgetRenderer,
205        clip: Option<Region>,
206        glyph_map: &'surface mut GlyphMap,
207    ) -> Self {
208        Self {
209            surface,
210            clip,
211            state: Unsized,
212            glyph_map,
213        }
214    }
215
216    /// Create a sized context at a given position
217    pub fn into_sized(self, size: Size, global_pos: Pos) -> PaintCtx<'surface, SizePos> {
218        PaintCtx {
219            surface: self.surface,
220            glyph_map: self.glyph_map,
221            clip: self.clip,
222            state: SizePos::new(size, global_pos),
223        }
224    }
225}
226
227impl<'screen> PaintCtx<'screen, SizePos> {
228    pub fn to_unsized(&mut self) -> PaintCtx<'_, Unsized> {
229        PaintCtx::new(self.surface, self.clip, self.glyph_map)
230    }
231
232    pub fn update(&mut self, new_size: Size, new_pos: Pos) {
233        self.state.local_size = new_size;
234        self.state.global_pos = new_pos;
235    }
236
237    /// This will create an intersection with any previous regions
238    pub fn set_clip_region(&mut self, region: Region) {
239        let current = self.clip.get_or_insert(region);
240        *current = current.intersect_with(&region);
241    }
242
243    pub fn create_region(&self) -> Region {
244        let mut region = Region::new(
245            self.global_pos,
246            Pos::new(
247                self.global_pos.x + self.local_size.width as i32,
248                self.global_pos.y + self.local_size.height as i32,
249            ),
250        );
251
252        if let Some(existing) = self.clip {
253            region.constrain(&existing);
254        }
255
256        region
257    }
258
259    fn clip(&self, local_pos: LocalPos, clip: &Region) -> bool {
260        let pos = self.global_pos + local_pos;
261        clip.contains(pos)
262    }
263
264    fn pos_inside_local_region(&self, pos: LocalPos, width: u16) -> bool {
265        pos.x + width <= self.local_size.width && pos.y < self.local_size.height
266    }
267
268    // Translate local coordinates to screen coordinates.
269    // Will return `None` if the coordinates are outside the screen bounds
270    pub fn translate_to_global(&self, local: LocalPos) -> Option<Pos> {
271        let screen_x = local.x as i32 + self.global_pos.x;
272        let screen_y = local.y as i32 + self.global_pos.y;
273
274        let (width, height) = self.surface.size().into();
275        if screen_x < 0 || screen_y < 0 || screen_x >= width || screen_y >= height {
276            return None;
277        }
278
279        Some(Pos {
280            x: screen_x,
281            y: screen_y,
282        })
283    }
284
285    fn newline(&mut self, pos: LocalPos) -> Option<LocalPos> {
286        let y = pos.y + 1; // next line
287        if y >= self.local_size.height { None } else { Some(LocalPos { x: 0, y }) }
288    }
289
290    pub fn to_glyphs<'a>(&mut self, s: &'a str) -> Glyphs<'a> {
291        Glyphs::new(s)
292    }
293
294    pub fn place_glyphs(&mut self, mut glyphs: Glyphs<'_>, mut pos: LocalPos) -> Option<LocalPos> {
295        while let Some(glyph) = glyphs.next(self.glyph_map) {
296            pos = self.place_glyph(glyph, pos)?;
297        }
298        Some(pos)
299    }
300
301    pub fn set_style(&mut self, style: Style, pos: LocalPos) {
302        // Ensure that the position is inside provided clipping region
303        if let Some(clip) = self.clip.as_ref() {
304            if !self.clip(pos, clip) {
305                return;
306            }
307        }
308
309        let screen_pos = match self.translate_to_global(pos) {
310            Some(pos) => pos,
311            None => return,
312        };
313
314        self.surface.set_style(style, screen_pos);
315    }
316
317    pub fn set_attributes(&mut self, attrs: &Attributes<'_>, pos: LocalPos) {
318        // Ensure that the position is inside provided clipping region
319        if let Some(clip) = self.clip.as_ref() {
320            if !self.clip(pos, clip) {
321                return;
322            }
323        }
324
325        let screen_pos = match self.translate_to_global(pos) {
326            Some(pos) => pos,
327            None => return,
328        };
329
330        self.surface.set_attributes(attrs, screen_pos);
331    }
332
333    // Place a char on the screen buffer, return the next cursor position in local space.
334    //
335    // The `input_pos` is the position, in local space, where the character
336    // should be placed. This will (possibly) be offset if there is clipping available.
337    //
338    // The `output_pos` is the same as the `input_pos` unless clipping has been applied.
339    pub fn place_glyph(&mut self, glyph: Glyph, input_pos: LocalPos) -> Option<LocalPos> {
340        let width = glyph.width() as u16;
341        let next = LocalPos {
342            x: input_pos.x + width,
343            y: input_pos.y,
344        };
345
346        // Ensure that the position is inside provided clipping region
347        if let Some(clip) = self.clip.as_ref() {
348            if !self.clip(input_pos, clip) {
349                return Some(next);
350            }
351        }
352
353        // 1. Newline (yes / no)
354        if glyph.is_newline() {
355            return self.newline(input_pos);
356        }
357
358        // 2. Check if the char can be placed
359        if !self.pos_inside_local_region(input_pos, width) {
360            return None;
361        }
362
363        // 3. Find position on the screen
364        let screen_pos = match self.translate_to_global(input_pos) {
365            Some(pos) => pos,
366            None => return Some(next),
367        };
368
369        // 4. Place the char
370        self.surface.draw_glyph(glyph, screen_pos);
371
372        // 4. Advance the cursor (which might trigger another newline)
373        if input_pos.x >= self.local_size.width {
374            self.newline(input_pos)
375        } else {
376            Some(LocalPos {
377                x: input_pos.x + width,
378                y: input_pos.y,
379            })
380        }
381    }
382}