Skip to main content

zenthra_widgets/
text.rs

1use zenthra_text::prelude::*;
2pub use zenthra_text::prelude::{FontWeight, TextWrap, FontStyle};
3// use zenthra_text::traits::FontProvider;
4use crate::ui::{DrawCommand, TextDraw, Ui};
5use zenthra_core::{Color, EdgeInsets, Role, SemanticNode, Rect, Align, Id};
6
7pub struct TextBuilder<'u, 'a> {
8    ui: &'u mut Ui<'a>,
9    id: Id,
10    content: String,
11    options: TextOptions,
12    
13    // Container/Widget-level styling
14    padding: Padding,
15    bg_color: Option<Color>,
16    fill_x: bool,
17    
18    margin: EdgeInsets,
19    radius: [f32; 4],
20    border_color: Option<Color>,
21    border_width: f32,
22    shadow_color: Option<Color>,
23    shadow_offset: [f32; 2],
24    shadow_blur: f32,
25    shadow_opacity: f32,
26    opacity: f32,
27    cursor: CursorIcon,
28    render_mode: Option<zenthra_core::RenderMode>,
29    start_x: f32,
30    start_y: f32,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum CursorIcon {
35    Default,
36    Text,
37    Pointer,
38    Crosshair,
39    ColResize,
40}
41
42impl<'u, 'a> TextBuilder<'u, 'a> {
43    pub fn new(ui: &'u mut Ui<'a>, content: &str) -> Self {
44        let x = ui.cursor_x;
45        let y = ui.cursor_y;
46        let sf = ui.scale_factor;
47        
48        // --- STABLE DETERMINISTIC ID ---
49        let mut hasher = std::collections::hash_map::DefaultHasher::new();
50        use std::hash::{Hash, Hasher};
51        content.hash(&mut hasher);
52        // Include parent ID in hash to differentiate same text in different containers
53        if let Some(parent) = ui.semantic_stack.last() {
54            parent.hash(&mut hasher);
55        }
56        let id_raw = hasher.finish();
57        let id = Id::from_u64(id_raw);
58        
59        
60        Self {
61            ui,
62            id,
63            content: content.to_string(),
64            options: TextOptions::new()
65                .at(0.0, 0.0) // Relative to pos
66                .scale_factor(sf),
67            padding: Padding::ZERO,
68            bg_color: None,
69            fill_x: false,
70            margin: EdgeInsets::ZERO,
71            radius: [0.0; 4],
72            border_color: None,
73            border_width: 0.0,
74            shadow_color: None,
75            shadow_offset: [0.0; 2],
76            shadow_blur: 0.0,
77            shadow_opacity: 1.0,
78            opacity: 1.0,
79            cursor: CursorIcon::Default,
80            render_mode: None,
81            start_x: x,
82            start_y: y,
83        }
84    }
85
86    pub fn min_width(mut self, w: f32) -> Self {
87        self.options = self.options.min_width(w);
88        self
89    }
90
91    pub fn size(mut self, s: f32) -> Self {
92        self.options = self.options.font_size(s);
93        self
94    }
95    pub fn color(mut self, c: Color) -> Self {
96        self.options = self.options.color(c);
97        self
98    }
99    pub fn weight(mut self, w: impl Into<FontWeight>) -> Self {
100        self.options = self.options.font_weight(w.into());
101        self
102    }
103    pub fn bold(mut self) -> Self {
104        self.options = self.options.font_weight(FontWeight::Bold);
105        self
106    }
107    pub fn italic(mut self) -> Self {
108        self.options = self.options.font_style(FontStyle::Italic);
109        self
110    }
111    pub fn family(mut self, f: impl Into<String>) -> Self {
112        self.options = self.options.font_family(f);
113        self
114    }
115    pub fn monospace(mut self) -> Self {
116        self.options = self.options.font_family("monospace");
117        self
118    }
119    pub fn pos(mut self, x: f32, y: f32) -> Self {
120        self.start_x = x;
121        self.start_y = y;
122        self.options = self.options.at(0.0, 0.0);
123        self
124    }
125    pub fn max_width(mut self, w: f32) -> Self {
126        self.options = self.options.max_width(w);
127        self
128    }
129
130    pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
131        self.padding = Padding { top: t, right: r, bottom: b, left: l };
132        self
133    }
134    pub fn padding_x(mut self, p: f32) -> Self {
135        self.padding.left = p;
136        self.padding.right = p;
137        self
138    }
139    pub fn padding_y(mut self, p: f32) -> Self {
140        self.padding.top = p;
141        self.padding.bottom = p;
142        self
143    }
144    pub fn padding_top(mut self, p: f32) -> Self {
145        self.padding.top = p;
146        self
147    }
148    pub fn padding_bottom(mut self, p: f32) -> Self {
149        self.padding.bottom = p;
150        self
151    }
152    pub fn padding_left(mut self, p: f32) -> Self {
153        self.padding.left = p;
154        self
155    }
156    pub fn padding_right(mut self, p: f32) -> Self {
157        self.padding.right = p;
158        self
159    }
160
161    pub fn margin(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
162        self.margin = EdgeInsets { top: t, right: r, bottom: b, left: l };
163        self
164    }
165    pub fn margin_x(mut self, m: f32) -> Self {
166        self.margin.left = m;
167        self.margin.right = m;
168        self
169    }
170    pub fn margin_y(mut self, m: f32) -> Self {
171        self.margin.top = m;
172        self.margin.bottom = m;
173        self
174    }
175    pub fn margin_top(mut self, m: f32) -> Self {
176        self.margin.top = m;
177        self
178    }
179    pub fn margin_bottom(mut self, m: f32) -> Self {
180        self.margin.bottom = m;
181        self
182    }
183    pub fn margin_left(mut self, m: f32) -> Self {
184        self.margin.left = m;
185        self
186    }
187    pub fn margin_right(mut self, m: f32) -> Self {
188        self.margin.right = m;
189        self
190    }
191
192    pub fn line_height(mut self, lh: f32) -> Self {
193        self.options = self.options.line_height(lh);
194        self
195    }
196
197    pub fn bg(mut self, c: Color) -> Self {
198        self.bg_color = Some(c);
199        self
200    }
201
202    pub fn border(mut self, color: Color, width: f32) -> Self {
203        self.border_color = Some(color);
204        self.border_width = width;
205        self
206    }
207
208    pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
209        self.radius = [tl, tr, br, bl];
210        self
211    }
212
213    pub fn radius_all(mut self, r: f32) -> Self {
214        self.radius = [r; 4];
215        self
216    }
217
218    pub fn radius_top(mut self, r: f32) -> Self {
219        self.radius[0] = r;
220        self.radius[1] = r;
221        self
222    }
223
224    pub fn radius_bottom(mut self, r: f32) -> Self {
225        self.radius[2] = r;
226        self.radius[3] = r;
227        self
228    }
229
230    pub fn radius_top_left(mut self, r: f32) -> Self {
231        self.radius[0] = r;
232        self
233    }
234
235    pub fn radius_top_right(mut self, r: f32) -> Self {
236        self.radius[1] = r;
237        self
238    }
239
240    pub fn radius_bottom_right(mut self, r: f32) -> Self {
241        self.radius[2] = r;
242        self
243    }
244
245    pub fn radius_bottom_left(mut self, r: f32) -> Self {
246        self.radius[3] = r;
247        self
248    }
249
250    pub fn radius_left(mut self, r: f32) -> Self {
251        self.radius[0] = r;
252        self.radius[3] = r;
253        self
254    }
255
256    pub fn radius_right(mut self, r: f32) -> Self {
257        self.radius[1] = r;
258        self.radius[2] = r;
259        self
260    }
261
262    pub fn shadow(mut self, color: Color, ox: f32, oy: f32, blur: f32) -> Self {
263        self.shadow_color = Some(color);
264        self.shadow_offset = [ox, oy];
265        self.shadow_blur = blur;
266        self
267    }
268
269    pub fn opacity(mut self, o: f32) -> Self {
270        self.opacity = o;
271        self
272    }
273
274    pub fn highlight(mut self, c: Color) -> Self {
275        self.options = self.options.highlight(c);
276        self
277    }
278
279    pub fn fill_x(mut self, enabled: bool) -> Self {
280        self.fill_x = enabled;
281        self
282    }
283
284    pub fn id(mut self, id: impl std::hash::Hash) -> Self {
285        let mut hasher = std::collections::hash_map::DefaultHasher::new();
286        use std::hash::{Hash, Hasher};
287        id.hash(&mut hasher);
288        if let Some(parent) = self.ui.semantic_stack.last() {
289            parent.hash(&mut hasher);
290        }
291        self.id = zenthra_core::Id::from_u64(hasher.finish());
292        self
293    }
294    
295    pub fn wrap(mut self, strategy: impl Into<TextWrap>) -> Self {
296        self.options = self.options.wrap(strategy.into());
297        self
298    }
299
300    pub fn ellipsis(mut self, enabled: bool) -> Self {
301        self.options = self.options.ellipsis(enabled);
302        self
303    }
304
305
306    pub fn align(mut self, alignment: Align) -> Self {
307        let halign = match alignment {
308            Align::Left => HorizontalAlignment::Left,
309            Align::Center => HorizontalAlignment::Center,
310            Align::Right => HorizontalAlignment::Right,
311            _ => HorizontalAlignment::Left,
312        };
313        self.options = self.options.align(halign);
314        self
315    }
316
317    pub fn align_left(mut self) -> Self {
318        self.options = self.options.align(HorizontalAlignment::Left);
319        self
320    }
321
322    pub fn align_center(mut self) -> Self {
323        self.options = self.options.align(HorizontalAlignment::Center);
324        self
325    }
326
327    pub fn align_right(mut self) -> Self {
328        self.options = self.options.align(HorizontalAlignment::Right);
329        self
330    }
331
332    pub fn halign(self, alignment: Align) -> Self {
333        self.align(alignment)
334    }
335
336    pub fn valign(mut self, alignment: Align) -> Self {
337        let valign = match alignment {
338            Align::Top => VerticalAlignment::Top,
339            Align::Center => VerticalAlignment::Center,
340            Align::Bottom => VerticalAlignment::Bottom,
341            _ => VerticalAlignment::Top,
342        };
343        self.options = self.options.valign(valign);
344        self
345    }
346    
347    pub fn cursor(mut self, c: CursorIcon) -> Self {
348        self.cursor = c;
349        self
350    }
351    pub fn cursor_text(mut self) -> Self {
352        self.cursor = CursorIcon::Text;
353        self
354    }
355    pub fn cursor_pointer(mut self) -> Self {
356        self.cursor = CursorIcon::Pointer;
357        self
358    }
359
360    pub fn clip_rect(mut self, x: f32, y: f32, w: f32, h: f32) -> Self {
361        self.options = self.options.clip_rect(x, y, w, h);
362        self
363    }
364
365    pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
366        self.render_mode = Some(mode);
367        self
368    }
369
370    pub fn continuous(mut self) -> Self {
371        self.render_mode = Some(zenthra_core::RenderMode::Continuous);
372        self
373    }
374
375    pub fn static_mode(mut self) -> Self {
376        self.render_mode = Some(zenthra_core::RenderMode::Static);
377        self
378    }
379
380    pub fn show(mut self) -> (zenthra_core::Response, Option<ShapedBuffer>) {
381        if let Some(mode) = self.render_mode {
382            self.ui.render_mode_stack.push(mode);
383        }
384
385        let horiz = self.margin.horizontal();
386        let vert = self.margin.vertical();
387        let (w, h, buffer, start) = self.draw_and_measure();
388        
389        self.ui.register_semantic(
390            SemanticNode::new(self.id, Role::Label, Rect::new(self.start_x, self.start_y, w, h))
391                .with_label(self.content.clone())
392        );
393
394        self.ui.advance(w + horiz, h + vert, start);
395        
396        if self.render_mode.is_some() {
397            self.ui.render_mode_stack.pop();
398        }
399
400        let is_hovered = self.ui.mouse_in_rect(self.start_x + self.ui.offset_x, self.start_y + self.ui.offset_y, w, h);
401        let response = zenthra_core::Response {
402            clicked: self.ui.clicked && is_hovered,
403            hovered: is_hovered,
404            pressed: is_hovered && self.ui.mouse_down,
405        };
406
407        (response, buffer)
408    }
409
410    pub fn draw_and_measure(&mut self) -> (f32, f32, Option<ShapedBuffer>, usize) {
411        let (w, h, buffer) = if let Some(fs) = self.ui.font_system.as_ref() {
412             let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
413             
414             // Use explicitly set max_width if available, otherwise fallback to container width
415             let layout_width = self.options.max_width.unwrap_or_else(|| {
416                 (self.ui.available_width - self.padding.horizontal()).max(0.0)
417             });
418             
419             self.options.max_width = Some(layout_width);
420             
421             adapter.set_layout_size(layout_width, self.ui.height);
422             
423             let buffer = adapter.shape(&self.content, &self.options);
424             let (cw, ch) = buffer.size();
425
426             // Important: record layout for next frame culling
427             let mut w = cw + self.padding.horizontal();
428             if self.fill_x {
429                 w = self.ui.max_x - self.start_x;
430             } else if let Some(min_w) = self.options.min_width {
431                 if w < min_w { w = min_w; }
432             }
433             let h = ch + self.padding.vertical();
434             self.ui.record_layout(self.id, Rect::new(self.start_x, self.start_y, w, h));
435
436             (w, h, Some(buffer))
437        } else {
438            (100.0, 20.0, None)
439        };
440
441        let start_draw = self.ui.draws.len();
442        let clip = self.options.clip_rect.unwrap_or([-100000.0, -100000.0, 2000000.0, 2000000.0]);
443
444        // Background
445        if let Some(bg) = self.bg_color {
446            use zenthra_render::RectInstance;
447            use crate::ui::RectDraw;
448
449            self.ui.draws.push(DrawCommand::Rect(RectDraw {
450                instance: RectInstance {
451                    pos: [self.start_x, self.start_y],
452                    size: [w, h],
453                    color: bg.to_array(),
454                    radius: [
455                        self.radius[3],
456                        self.radius[2],
457                        self.radius[1],
458                        self.radius[0],
459                    ],
460                    border_width: self.border_width,
461                    border_color: self.border_color.unwrap_or(Color::TRANSPARENT).to_array(),
462                    shadow_color: self.shadow_color.map(|mut c| { c.a *= self.shadow_opacity; c.to_array() }).unwrap_or([0.0; 4]),
463                    shadow_offset: self.shadow_offset,
464                    shadow_blur: self.shadow_blur,
465                    clip_rect: clip,
466                    grayscale: 0.0,
467                    brightness: 1.0,
468                    opacity: self.opacity,
469                    ..Default::default()
470                }
471            }));
472        }
473
474        // --- 2. Draw Text (Padded) ---
475        self.ui.draws.push(DrawCommand::Text(TextDraw {
476            text: self.content.clone().into(),
477            pos: [self.start_x + self.padding.left, self.start_y + self.padding.top],
478            options: self.options.clone(),
479            clip,
480        }));
481
482        (w, h, buffer, start_draw)
483    }
484}