nvim_gtk/ui_model/
line.rs

1use std::ops::{Index, IndexMut};
2use std::rc::Rc;
3
4use pango;
5
6use super::cell::Cell;
7use super::item::Item;
8use crate::color;
9use crate::render;
10use crate::highlight::{HighlightMap, Highlight};
11
12pub struct Line {
13    pub line: Box<[Cell]>,
14
15    // format of item line is
16    // [Item1, Item2, None, None, Item3]
17    // Item2 take 3 cells and renders as one
18    pub item_line: Box<[Option<Item>]>,
19    cell_to_item: Box<[i32]>,
20
21    pub dirty_line: bool,
22}
23
24impl Line {
25    pub fn new(columns: usize) -> Self {
26        Line {
27            line: vec![Cell::new_empty(); columns].into_boxed_slice(),
28            item_line: vec![None; columns].into_boxed_slice(),
29            cell_to_item: vec![-1; columns].into_boxed_slice(),
30            dirty_line: true,
31        }
32    }
33
34    pub fn swap_with(&mut self, target: &mut Self, left: usize, right: usize) {
35        // swap is faster then clone
36        target.line[left..=right].swap_with_slice(&mut self.line[left..=right]);
37
38        // this is because copy can change Item layout
39        target.dirty_line = true;
40        for cell in &mut target.line[left..=right] {
41            cell.dirty = true;
42        }
43    }
44
45    pub fn clear(&mut self, left: usize, right: usize, default_hl: &Rc<Highlight>) {
46        for cell in &mut self.line[left..=right] {
47            cell.clear(default_hl.clone());
48        }
49        self.dirty_line = true;
50    }
51
52    pub fn clear_glyphs(&mut self) {
53        for i in 0..self.item_line.len() {
54            self.item_line[i] = None;
55            self.cell_to_item[i] = -1;
56        }
57        self.dirty_line = true;
58    }
59
60    fn set_cell_to_empty(&mut self, cell_idx: usize) -> bool {
61        if self.is_binded_to_item(cell_idx) {
62            self.item_line[cell_idx] = None;
63            self.cell_to_item[cell_idx] = -1;
64            self.line[cell_idx].dirty = true;
65            true
66        } else {
67            false
68        }
69    }
70
71    fn set_cell_to_item(&mut self, new_item: &PangoItemPosition) -> bool {
72        let start_item_idx = self.cell_to_item(new_item.start_cell);
73        let start_item_cells_count = if start_item_idx >= 0 {
74            self.item_line[start_item_idx as usize]
75                .as_ref()
76                .map_or(-1, |item| item.cells_count as i32)
77        } else {
78            -1
79        };
80
81        let end_item_idx = self.cell_to_item(new_item.end_cell);
82
83        // start_item == idx of item start cell
84        // in case different item length was in previous iteration
85        // mark all item as dirty
86        if start_item_idx != new_item.start_cell as i32
87            || new_item.cells_count() != start_item_cells_count
88            || start_item_idx == -1
89            || end_item_idx == -1
90        {
91            self.initialize_cell_item(new_item.start_cell, new_item.end_cell, new_item.item);
92            true
93        } else {
94            // update only if cell marked as dirty
95            if self.line[new_item.start_cell..=new_item.end_cell]
96                .iter()
97                .any(|c| c.dirty)
98            {
99                self.item_line[new_item.start_cell]
100                    .as_mut()
101                    .unwrap()
102                    .update(new_item.item.clone());
103                self.line[new_item.start_cell].dirty = true;
104                true
105            } else {
106                false
107            }
108        }
109    }
110
111    pub fn merge(&mut self, old_items: &StyledLine, pango_items: &[pango::Item]) {
112        let mut pango_item_iter = pango_items
113            .iter()
114            .map(|item| PangoItemPosition::new(old_items, item));
115
116        let mut next_item = pango_item_iter.next();
117        let mut move_to_next_item = false;
118
119        let mut cell_idx = 0;
120        while cell_idx < self.line.len() {
121            let dirty = match next_item {
122                None => self.set_cell_to_empty(cell_idx),
123                Some(ref new_item) => {
124                    if cell_idx < new_item.start_cell {
125                        self.set_cell_to_empty(cell_idx)
126                    } else if cell_idx == new_item.start_cell {
127                        move_to_next_item = true;
128                        self.set_cell_to_item(new_item)
129                    } else {
130                        false
131                    }
132                }
133            };
134
135            self.dirty_line = self.dirty_line || dirty;
136            if move_to_next_item {
137                let new_item = next_item.unwrap();
138                cell_idx += new_item.end_cell - new_item.start_cell + 1;
139                next_item = pango_item_iter.next();
140                move_to_next_item = false;
141            } else {
142                cell_idx += 1;
143            }
144        }
145    }
146
147    fn initialize_cell_item(
148        &mut self,
149        start_cell: usize,
150        end_cell: usize,
151        new_item: &pango::Item,
152    ) {
153        for i in start_cell..=end_cell {
154            self.line[i].dirty = true;
155            self.cell_to_item[i] = start_cell as i32;
156        }
157        for i in start_cell + 1..=end_cell {
158            self.item_line[i] = None;
159        }
160        self.item_line[start_cell] = Some(Item::new(new_item.clone(), end_cell - start_cell + 1));
161    }
162
163    pub fn get_item(&self, cell_idx: usize) -> Option<&Item> {
164        let item_idx = self.cell_to_item(cell_idx);
165        if item_idx >= 0 {
166            self.item_line[item_idx as usize].as_ref()
167        } else {
168            None
169        }
170    }
171
172    #[inline]
173    pub fn cell_to_item(&self, cell_idx: usize) -> i32 {
174        self.cell_to_item[cell_idx]
175    }
176
177    pub fn item_len_from_idx(&self, start_idx: usize) -> usize {
178        debug_assert!(
179            start_idx < self.line.len(),
180            "idx={}, len={}",
181            start_idx,
182            self.line.len()
183        );
184
185        let item_idx = self.cell_to_item(start_idx);
186
187        if item_idx >= 0 {
188            let item_idx = item_idx as usize;
189            let cells_count = self.item_line[item_idx].as_ref().unwrap().cells_count;
190            let offset = start_idx - item_idx;
191
192            cells_count - offset
193        } else {
194            1
195        }
196    }
197
198    #[inline]
199    pub fn is_binded_to_item(&self, cell_idx: usize) -> bool {
200        self.cell_to_item[cell_idx] >= 0
201    }
202}
203
204impl Index<usize> for Line {
205    type Output = Cell;
206
207    fn index(&self, index: usize) -> &Cell {
208        &self.line[index]
209    }
210}
211
212impl IndexMut<usize> for Line {
213    fn index_mut(&mut self, index: usize) -> &mut Cell {
214        &mut self.line[index]
215    }
216}
217
218struct PangoItemPosition<'a> {
219    item: &'a pango::Item,
220    start_cell: usize,
221    end_cell: usize,
222}
223
224impl<'a> PangoItemPosition<'a> {
225    pub fn new(styled_line: &StyledLine, item: &'a pango::Item) -> Self {
226        let offset = item.offset() as usize;
227        let length = item.length() as usize;
228        let start_cell = styled_line.cell_to_byte[offset];
229        let end_cell = styled_line.cell_to_byte[offset + length - 1];
230
231        PangoItemPosition {
232            item,
233            start_cell,
234            end_cell,
235        }
236    }
237
238    #[inline]
239    fn cells_count(&self) -> i32 {
240        (self.end_cell - self.start_cell) as i32 + 1
241    }
242}
243
244pub struct StyledLine {
245    pub line_str: String,
246    cell_to_byte: Box<[usize]>,
247    pub attr_list: pango::AttrList,
248}
249
250impl StyledLine {
251    pub fn from(
252        line: &Line,
253        hl: &HighlightMap,
254        font_features: &render::FontFeatures,
255    ) -> Self {
256        let average_capacity = line.line.len() * 4 * 2; // code bytes * grapheme cluster
257
258        let mut line_str = String::with_capacity(average_capacity);
259        let mut cell_to_byte = Vec::with_capacity(average_capacity);
260        let attr_list = pango::AttrList::new();
261        let mut byte_offset = 0;
262        let mut style_attr = StyleAttr::new();
263
264        for (cell_idx, cell) in line.line.iter().enumerate() {
265            if cell.double_width {
266                continue;
267            }
268
269            if !cell.ch.is_empty() {
270                line_str.push_str(&cell.ch);
271            } else {
272                line_str.push(' ');
273            }
274            let len = line_str.len() - byte_offset;
275
276            for _ in 0..len {
277                cell_to_byte.push(cell_idx);
278            }
279
280            let next = style_attr.next(byte_offset, byte_offset + len, cell, hl);
281            if let Some(next) = next {
282                style_attr.insert_into(&attr_list);
283                style_attr = next;
284            }
285
286            byte_offset += len;
287        }
288
289        style_attr.insert_into(&attr_list);
290        font_features.insert_into(&attr_list);
291
292        StyledLine {
293            line_str,
294            cell_to_byte: cell_to_byte.into_boxed_slice(),
295            attr_list,
296        }
297    }
298}
299
300struct StyleAttr<'c> {
301    italic: bool,
302    bold: bool,
303    foreground: Option<&'c color::Color>,
304    background: Option<&'c color::Color>,
305    empty: bool,
306    space: bool,
307
308    start_idx: usize,
309    end_idx: usize,
310}
311
312impl<'c> StyleAttr<'c> {
313    fn new() -> Self {
314        StyleAttr {
315            italic: false,
316            bold: false,
317            foreground: None,
318            background: None,
319            empty: true,
320            space: false,
321
322            start_idx: 0,
323            end_idx: 0,
324        }
325    }
326
327    fn from(
328        start_idx: usize,
329        end_idx: usize,
330        cell: &'c Cell,
331        hl: &'c HighlightMap,
332    ) -> Self {
333        StyleAttr {
334            italic: cell.hl.italic,
335            bold: cell.hl.bold,
336            foreground: hl.cell_fg(cell),
337            background: hl.cell_bg(cell),
338            empty: false,
339            space: cell.ch.is_empty(),
340
341            start_idx,
342            end_idx,
343        }
344    }
345
346    fn next(
347        &mut self,
348        start_idx: usize,
349        end_idx: usize,
350        cell: &'c Cell,
351        hl: &'c HighlightMap,
352    ) -> Option<StyleAttr<'c>> {
353        // don't check attr for space
354        if self.space && cell.ch.is_empty() {
355            self.end_idx = end_idx;
356            return None;
357        }
358
359
360        let style_attr = Self::from(start_idx, end_idx, cell, hl);
361
362        if self != &style_attr {
363            Some(style_attr)
364        } else {
365            self.end_idx = end_idx;
366            None
367        }
368    }
369
370    fn insert_into(&self, attr_list: &pango::AttrList) {
371        if self.empty {
372            return;
373        }
374
375        if self.italic {
376            self.insert_attr(
377                attr_list,
378                pango::Attribute::new_style(pango::Style::Italic).unwrap(),
379            );
380        }
381
382        if self.bold {
383            self.insert_attr(
384                attr_list,
385                pango::Attribute::new_weight(pango::Weight::Bold).unwrap(),
386            );
387        }
388
389        if let Some(fg) = self.foreground {
390            let (r, g, b) = fg.to_u16();
391            self.insert_attr(
392                attr_list,
393                pango::Attribute::new_foreground(r, g, b).unwrap(),
394            );
395        }
396
397        if let Some(bg) = self.background {
398            let (r, g, b) = bg.to_u16();
399            self.insert_attr(
400                attr_list,
401                pango::Attribute::new_background(r, g, b).unwrap(),
402            );
403        }
404    }
405
406    #[inline]
407    fn insert_attr(&self, attr_list: &pango::AttrList, mut attr: pango::Attribute) {
408        attr.set_start_index(self.start_idx as u32);
409        attr.set_end_index(self.end_idx as u32);
410        attr_list.insert(attr);
411    }
412}
413
414impl<'c> PartialEq for StyleAttr<'c> {
415    fn eq(&self, other: &Self) -> bool {
416        self.italic == other.italic
417            && self.bold == other.bold
418            && self.foreground == other.foreground
419            && self.empty == other.empty
420            && self.background == other.background
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_styled_line() {
430        let mut line = Line::new(3);
431        line[0].ch = "a".to_owned();
432        line[1].ch = "b".to_owned();
433        line[2].ch = "c".to_owned();
434
435        let styled_line = StyledLine::from(
436            &line,
437            &HighlightMap::new(),
438            &render::FontFeatures::new(),
439        );
440        assert_eq!("abc", styled_line.line_str);
441        assert_eq!(3, styled_line.cell_to_byte.len());
442        assert_eq!(0, styled_line.cell_to_byte[0]);
443        assert_eq!(1, styled_line.cell_to_byte[1]);
444        assert_eq!(2, styled_line.cell_to_byte[2]);
445    }
446}