Skip to main content

ui/
text_surface.rs

1// Input/selection overlay for externally laid out rich text. New opt-in UI API.
2// The caller paints content; the existing textbox engine handles Unicode, clipboard and IME.
3use super::*;
4
5/// One visual line of a text editing surface, in absolute physical window coordinates.
6#[derive(Clone, Debug)]
7pub struct TextSurfaceLine {
8    /// UTF-8 byte range, excluding an explicit newline. Soft wraps share their boundary.
9    pub start: usize,
10    pub end: usize,
11    /// Hit/selection rectangle [x, y, width, height]. It may cover a different page per line.
12    pub rect: [i32; 4],
13    /// UTF-8 byte caret stops and their absolute x coordinates, in logical order.
14    pub carets: Vec<(usize, i32)>,
15}
16impl TextSurfaceLine {
17    fn x(&self, at: usize) -> i32 {
18        self.carets
19            .iter()
20            .rev()
21            .find(|(offset, _)| *offset <= at)
22            .map_or(self.rect[0], |(_, x)| *x)
23    }
24    fn hit(&self, x: i32) -> usize {
25        self.carets
26            .iter()
27            .min_by_key(|(_, px)| (i64::from(*px) - i64::from(x)).abs())
28            .map_or(self.start, |(offset, _)| *offset)
29    }
30}
31impl Ui {
32    /// Edit one UTF-8 buffer over caller-rendered rich text, without repainting its content.
33    /// Layout may span differently sized/aligned lines and pages; each UTF-8 scalar boundary
34    /// needs a caret stop. Text changes report EV_ACT and request a fresh layout on the next
35    /// pass. Invalid ranges reject the call without consuming input. The caller owns scroll
36    /// positioning and document history. Ordinary textboxes are unaffected.
37    #[cfg_attr(jfgui_dev, track_caller)]
38    pub fn textbox_surface(
39        &mut self,
40        id: u32,
41        text: &mut String,
42        cap_flags: u32,
43        lines: &[TextSurfaceLine],
44    ) -> u32 {
45        let _src = self.src_scope();
46        self.m.edit.surface_change = None;
47        self.m.edit.surface_start = None;
48        if lines.is_empty()
49            || lines.len() > 100000
50            || lines.iter().any(|line| {
51                line.start > line.end
52                    || !text.is_char_boundary(line.start)
53                    || !text.is_char_boundary(line.end)
54                    || line.rect[2] < 1
55                    || line.rect[3] < 1
56                    || line.carets.is_empty()
57                    || line.carets.iter().any(|(at, _)| {
58                        *at < line.start || *at > line.end || !text.is_char_boundary(*at)
59                    })
60                    || line.carets.first().map(|(at, _)| *at) != Some(line.start)
61                    || line.carets.last().map(|(at, _)| *at) != Some(line.end)
62                    || line.carets.windows(2).any(|p| p[0].0 >= p[1].0)
63                    || line.carets.len() != text[line.start..line.end].chars().count() + 1
64            })
65            || lines.windows(2).any(|p| p[0].end > p[1].start)
66        {
67            return 0;
68        }
69        let original = text.clone();
70        let mut bytes = core::mem::take(text).into_bytes();
71        core::mem::swap(&mut bytes, &mut self.m.edit.buf);
72        let prior = self.m.edit.surface.replace(lines.to_vec());
73        let flags = self.tb_start(id, cap_flags | TB_MULTI, TB_FLAG_MASK & !TB_PASSWORD);
74        let mut ev = 0;
75        let hb = HB_FOCUS
76            | HB_TEXT
77            | CUR_IBEAM << HB_CURSOR_SHIFT
78            | if flags & ST_DISABLED != 0 {
79                HB_DISABLED
80            } else {
81                0
82            };
83        let mut registered = false;
84        for line in lines {
85            let r = line.rect;
86            let before = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
87            ev |= self.hit_rect(id, r[0], r[1], r[2], r[3], hb);
88            if let Some(hit) = self.wp.ctx.hits[self.wp.ctx.hit_cur].get_mut(before) {
89                hit.continuation = registered;
90                registered = true;
91            }
92        }
93        let mut editing = self.tb_focus();
94        if editing {
95            self.m.edit.ed_multi = true;
96            self.m.edit.ml_page = 20;
97            let c = &self.wp.ctx;
98            if c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id {
99                let at = self.surface_hit(self.ltr_x(c.mx), c.my);
100                self.m.edit.ed_prefx = -1;
101                self.tb_press(ev, at);
102            } else if ev & EV_HELD != 0 && self.m.edit.ed_drag {
103                let at = self.surface_hit(self.ltr_x(c.mx), c.my);
104                self.tb_drag_to(at);
105            } else {
106                self.m.edit.ed_drag = false;
107            }
108            self.tb_keys();
109            self.tb_typed();
110            if self.m.edit.tb_chg {
111                self.m.edit.tb_ev |= EV_ACT;
112                self.repass();
113            }
114            editing = self.m.edit.ed_id == id;
115        }
116        if editing && !self.m.edit.tb_chg {
117            let (a, b) = self.tb_sel();
118            for line in lines {
119                let lo = a.max(line.start);
120                let hi = b.min(line.end);
121                if lo < hi || (lo == hi && a <= line.end && b > line.end) {
122                    let x = line.x(lo);
123                    let end = line
124                        .x(hi)
125                        .saturating_add(if b > line.end { self.dp(4) } else { 0 });
126                    self.draw_color((self.color(T_ACCENT) & 0x00ffffff) | 0x50000000);
127                    self.draw_rect(x, line.rect[1], (end - x).max(1), line.rect[3]);
128                }
129            }
130            let line = &lines[self.surface_line(self.m.edit.ed_caret)];
131            let x = line.x(self.m.edit.ed_caret);
132            if self.tb_caret_on() {
133                self.draw_color(0xff20272b);
134                self.draw_rect(x, line.rect[1], self.hair(), line.rect[3]);
135            }
136            self.ime_pos(x, line.rect[1], line.rect[3]);
137        }
138        self.wp.ctx.last_rect = lines[if editing {
139            self.surface_line(self.m.edit.ed_caret)
140        } else {
141            0
142        }]
143        .rect;
144        let result = self.tb_finish(ev, editing);
145        self.m.edit.surface = prior;
146        core::mem::swap(&mut bytes, &mut self.m.edit.buf);
147        *text = to_string(bytes);
148        if original != *text {
149            // Bound the prefix by the actual edit location: equal adjacent characters must
150            // inherit the format at the insertion point, not at a later matching seam.
151            let start = self
152                .m
153                .edit
154                .surface_start
155                .unwrap_or(0)
156                .min(original.len())
157                .min(text.len());
158            let start = Self::tb_fix_index(original.as_bytes(), start);
159            let mut suffix = original.as_bytes()[start..]
160                .iter()
161                .rev()
162                .zip(text.as_bytes()[start..].iter().rev())
163                .take_while(|(a, b)| a == b)
164                .count();
165            while !original.is_char_boundary(original.len() - suffix)
166                || !text.is_char_boundary(text.len() - suffix)
167            {
168                suffix -= 1;
169            }
170            self.m.edit.surface_change = Some((
171                start,
172                original.len() - suffix,
173                String::from(&text[start..text.len() - suffix]),
174            ));
175        }
176        result
177    }
178    /// Replacement in the original buffer from the most recent surface call. The range
179    /// respects the actual insertion location even when surrounding characters repeat.
180    pub fn textbox_surface_change(&self) -> Option<(usize, usize, String)> {
181        self.m.edit.surface_change.clone()
182    }
183    fn surface_line(&self, offset: usize) -> usize {
184        self.m
185            .edit
186            .surface
187            .as_ref()
188            .unwrap()
189            .iter()
190            .rposition(|l| l.start <= offset)
191            .unwrap_or(0)
192    }
193    fn surface_hit(&self, x: i32, y: i32) -> usize {
194        let lines = self.m.edit.surface.as_ref().unwrap();
195        let line = lines
196            .iter()
197            .min_by_key(|line| {
198                let top = i64::from(line.rect[1]);
199                let end = top + i64::from(line.rect[3]);
200                if i64::from(y) < top {
201                    top - i64::from(y)
202                } else {
203                    (i64::from(y) - end).max(0)
204                }
205            })
206            .unwrap();
207        Self::tb_fix_index(&self.m.edit.buf, line.hit(x))
208    }
209    pub(super) fn surface_vmove(&mut self, delta: i32, shift: bool) {
210        let lines = self.m.edit.surface.as_ref().unwrap();
211        let current = self.surface_line(self.m.edit.ed_caret);
212        let x = if self.m.edit.ed_prefx < 0 {
213            lines[current].x(self.m.edit.ed_caret)
214        } else {
215            self.m.edit.ed_prefx
216        };
217        let next = current as i64 + i64::from(delta);
218        let at = if next < 0 {
219            0
220        } else if next >= lines.len() as i64 {
221            self.m.edit.buf.len()
222        } else {
223            Self::tb_fix_index(&self.m.edit.buf, lines[next as usize].hit(x))
224        };
225        self.tb_move(at, shift);
226        self.m.edit.ed_prefx = x;
227    }
228}