Skip to main content

zenthra_widgets/
button.rs

1// crates/zenthra-widgets/src/button.rs
2
3use crate::ui::{DrawCommand, RectDraw, TextDraw, Ui};
4// use crate::text::TextBuilder;
5use zenthra_core::{Color, EdgeInsets, Id, Rect, Role, SemanticNode};
6use zenthra_render::RectInstance;
7
8pub struct ButtonBuilder<'u, 'a> {
9    ui: &'u mut Ui<'a>,
10    id: Id,
11    label: String,
12
13    // Position & Layout
14    pos: Option<(f32, f32)>,
15    width: Option<f32>,
16    height: Option<f32>,
17    padding: EdgeInsets,
18
19    // Visuals (Idle)
20    bg: Color,
21    text_color: Color,
22    radius: [f32; 4],
23    font_size: f32,
24    border_width: f32,
25    border_color: Color,
26
27    // Shadows
28    shadow_offset: [f32; 2],
29    shadow_blur: f32,
30    shadow_color: Color,
31    shadow_opacity: f32,
32
33    // States
34    hover_bg: Option<Color>,
35    active_bg: Option<Color>,
36
37    // Other
38    opacity: f32,
39    render_mode: Option<zenthra_core::RenderMode>,
40    hover_brightness: f32,
41    hover_scale: f32,
42    hover_border_color: Option<Color>,
43    hover_border_width: Option<f32>,
44    active_border_color: Option<Color>,
45    active_border_width: Option<f32>,
46    wrap: zenthra_text::prelude::TextWrap,
47    fill_x: bool,
48    align: zenthra_core::Align,
49    font_family: Option<String>,
50}
51
52impl<'u, 'a> ButtonBuilder<'u, 'a> {
53    pub fn new(ui: &'u mut Ui<'a>, label: &str) -> Self {
54        let id = ui.id();
55        Self {
56            ui,
57            id,
58            label: label.to_string(),
59            pos: None,
60            width: None,
61            height: None,
62            padding: EdgeInsets::symmetric(6.0, 12.0),
63            bg: Color::rgb(0.2, 0.2, 0.25),
64            text_color: Color::WHITE,
65            radius: [4.0; 4],
66            font_size: 14.0,
67            border_width: 0.0,
68            border_color: Color::TRANSPARENT,
69            shadow_offset: [0.0, 0.0],
70            shadow_blur: 0.0,
71            shadow_color: Color::TRANSPARENT,
72            shadow_opacity: 0.0,
73            hover_bg: None,
74            active_bg: None,
75            opacity: 1.0,
76            render_mode: None,
77            hover_brightness: 1.0,
78            hover_scale: 1.0,
79            hover_border_color: None,
80            hover_border_width: None,
81            active_border_color: None,
82            active_border_width: None,
83            wrap: zenthra_text::prelude::TextWrap::Word,
84            fill_x: false,
85            align: zenthra_core::Align::Center,
86            font_family: None,
87        }
88    }
89
90    pub fn id(mut self, id: impl std::hash::Hash) -> Self {
91        let mut hasher = std::collections::hash_map::DefaultHasher::new();
92        use std::hash::Hasher;
93        id.hash(&mut hasher);
94        self.id = Id::from_u64(hasher.finish());
95        self
96    }
97
98    pub fn pos(mut self, x: f32, y: f32) -> Self {
99        self.pos = Some((x, y));
100        self
101    }
102
103    pub fn width(mut self, w: f32) -> Self {
104        self.width = Some(w);
105        self
106    }
107
108    pub fn height(mut self, h: f32) -> Self {
109        self.height = Some(h);
110        self
111    }
112
113    pub fn bg(mut self, color: Color) -> Self {
114        self.bg = color;
115        self
116    }
117
118    pub fn text_color(mut self, color: Color) -> Self {
119        self.text_color = color;
120        self
121    }
122
123    pub fn hover_bg(mut self, color: Color) -> Self {
124        self.hover_bg = Some(color);
125        self
126    }
127
128    pub fn active_bg(mut self, color: Color) -> Self {
129        self.active_bg = Some(color);
130        self
131    }
132
133    pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
134        self.radius = [tl, tr, br, bl];
135        self
136    }
137
138    pub fn radius_all(mut self, r: f32) -> Self {
139        self.radius = [r, r, r, r];
140        self
141    }
142
143    pub fn radius_top(mut self, r: f32) -> Self {
144        self.radius[0] = r;
145        self.radius[1] = r;
146        self
147    }
148
149    pub fn radius_bottom(mut self, r: f32) -> Self {
150        self.radius[2] = r;
151        self.radius[3] = r;
152        self
153    }
154
155    pub fn radius_top_left(mut self, r: f32) -> Self {
156        self.radius[0] = r;
157        self
158    }
159
160    pub fn radius_top_right(mut self, r: f32) -> Self {
161        self.radius[1] = r;
162        self
163    }
164
165    pub fn radius_bottom_right(mut self, r: f32) -> Self {
166        self.radius[2] = r;
167        self
168    }
169
170    pub fn radius_bottom_left(mut self, r: f32) -> Self {
171        self.radius[3] = r;
172        self
173    }
174
175    pub fn radius_left(mut self, r: f32) -> Self {
176        self.radius[0] = r;
177        self.radius[3] = r;
178        self
179    }
180
181    pub fn radius_right(mut self, r: f32) -> Self {
182        self.radius[1] = r;
183        self.radius[2] = r;
184        self
185    }
186
187    pub fn border(mut self, color: Color, width: f32) -> Self {
188        self.border_color = color;
189        self.border_width = width;
190        self
191    }
192
193    pub fn shadow(mut self, color: Color, x: f32, y: f32, blur: f32) -> Self {
194        self.shadow_color = color;
195        self.shadow_offset = [x, y];
196        self.shadow_blur = blur;
197        self
198    }
199
200    pub fn shadow_opacity(mut self, opacity: f32) -> Self {
201        self.shadow_opacity = opacity;
202        self
203    }
204
205    pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
206        self.padding = zenthra_core::EdgeInsets { top: t, right: r, bottom: b, left: l };
207        self
208    }
209
210    pub fn padding_x(mut self, x: f32) -> Self {
211        self.padding.left = x;
212        self.padding.right = x;
213        self
214    }
215
216    pub fn padding_y(mut self, y: f32) -> Self {
217        self.padding.top = y;
218        self.padding.bottom = y;
219        self
220    }
221
222    pub fn padding_top(mut self, t: f32) -> Self {
223        self.padding.top = t;
224        self
225    }
226
227    pub fn padding_bottom(mut self, b: f32) -> Self {
228        self.padding.bottom = b;
229        self
230    }
231
232    pub fn padding_left(mut self, l: f32) -> Self {
233        self.padding.left = l;
234        self
235    }
236
237    pub fn padding_right(mut self, r: f32) -> Self {
238        self.padding.right = r;
239        self
240    }
241
242    pub fn opacity(mut self, o: f32) -> Self {
243        self.opacity = o;
244        self
245    }
246
247    pub fn hover_brightness(mut self, b: f32) -> Self {
248        self.hover_brightness = b;
249        self
250    }
251
252    pub fn hover_scale(mut self, s: f32) -> Self {
253        self.hover_scale = s;
254        self
255    }
256
257    pub fn hover_border(mut self, color: Color, weight: f32) -> Self {
258        self.hover_border_color = Some(color);
259        self.hover_border_width = Some(weight);
260        self
261    }
262
263    pub fn active_border(mut self, color: Color, weight: f32) -> Self {
264        self.active_border_color = Some(color);
265        self.active_border_width = Some(weight);
266        self
267    }
268
269    pub fn size(mut self, size: f32) -> Self {
270        self.font_size = size;
271        self
272    }
273
274    pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
275        self.render_mode = Some(mode);
276        self
277    }
278
279    pub fn wrap(mut self, strategy: impl Into<zenthra_text::prelude::TextWrap>) -> Self {
280        self.wrap = strategy.into();
281        self
282    }
283
284    pub fn fill_x(mut self) -> Self {
285        self.fill_x = true;
286        self
287    }
288
289    pub fn align(mut self, strategy: zenthra_core::Align) -> Self {
290        self.align = strategy;
291        self
292    }
293
294    pub fn font_family(mut self, family: impl Into<String>) -> Self {
295        self.font_family = Some(family.into());
296        self
297    }
298
299    pub fn on_click<F>(self, mut f: F) -> Self
300    where
301        F: FnMut() + 'a,
302    {
303        self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
304            if let crate::ui::WidgetEvent::Click = event {
305                f();
306            }
307        });
308        self
309    }
310
311    pub fn on_hover<F>(self, mut f: F) -> Self
312    where
313        F: FnMut(bool) + 'a,
314    {
315        self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
316            if let crate::ui::WidgetEvent::Hover(hovered) = event {
317                f(*hovered);
318            }
319        });
320        self
321    }
322
323    pub fn on_scroll<F>(self, mut f: F) -> Self
324    where
325        F: FnMut(f32, f32) + 'a,
326    {
327        self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
328            if let crate::ui::WidgetEvent::Scroll(dx, dy) = event {
329                f(*dx, *dy);
330            }
331        });
332        self
333    }
334
335    pub fn on_event<F>(self, phase: crate::ui::EventPhase, f: F) -> Self
336    where
337        F: FnMut(&mut crate::ui::EventContext, &crate::ui::WidgetEvent) + 'a,
338    {
339        self.ui.add_listener(self.id, phase, f);
340        self
341    }
342
343    pub fn show(self) -> zenthra_core::Response {
344        if let Some(mode) = self.render_mode {
345            self.ui.render_mode_stack.push(mode);
346        }
347
348        let (x, y) = self.pos.unwrap_or((self.ui.cursor_x, self.ui.cursor_y));
349
350        let mut clicked = false;
351        let ox = x + self.ui.offset_x;
352        let oy = y + self.ui.offset_y;
353
354        let is_hovered = self.ui.is_hovered(
355            self.id,
356            ox,
357            oy,
358            self.width.unwrap_or(100.0),
359            self.height.unwrap_or(40.0),
360        );
361
362        let is_pressed = is_hovered && self.ui.mouse_down;
363
364        if self.ui.clicked && is_hovered {
365            clicked = true;
366        }
367
368        // Dispatch events
369        self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Hover(is_hovered));
370
371        if clicked {
372            self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Click);
373        }
374
375        let events = std::mem::take(&mut self.ui.input_events);
376        for event in &events {
377            if let zenthra_platform::event::PlatformEvent::MouseWheel { delta_x, delta_y } = event {
378                if is_hovered {
379                    self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Scroll(*delta_x, *delta_y));
380                }
381            }
382        }
383        self.ui.input_events = events;
384
385        // Determine effective colors based on state
386        let mut current_bg = self.bg;
387        let current_text = self.text_color;
388        let mut current_brightness = 1.0;
389
390        if is_pressed {
391            current_bg = self.active_bg.unwrap_or(self.bg);
392            current_brightness = 0.8;
393        } else if is_hovered {
394            current_bg = self.hover_bg.unwrap_or(self.bg);
395            current_brightness = self.hover_brightness;
396        }
397
398        let mut final_border_w = self.border_width;
399        let mut final_border_c = self.border_color;
400
401        if is_pressed {
402            if let Some(c) = self.active_border_color { final_border_c = c; }
403            if let Some(w) = self.active_border_width { final_border_w = w; }
404        } else if is_hovered {
405            if let Some(c) = self.hover_border_color { final_border_c = c; }
406            if let Some(w) = self.hover_border_width { final_border_w = w; }
407        }
408
409        // Measure Text
410        let mut text_w = 0.0;
411        let mut text_h = 0.0;
412        if let Some(fs) = self.ui.font_system.as_ref() {
413            // use zenthra_text::traits::FontProvider;
414            let mut adapter =
415                zenthra_text::prelude::CosmicFontProvider::new_with_system(fs.clone());
416            let mut options = zenthra_text::prelude::TextOptions::new()
417                .font_size(self.font_size)
418                .wrap(self.wrap);
419            if let Some(ref family) = self.font_family {
420                options = options.font_family(family.clone());
421            }
422            let buffer = adapter.shape(&self.label, &options);
423            let (cw, ch) = buffer.content_size();
424            text_w = cw;
425            text_h = ch;
426        }
427
428        let mut final_w = if self.fill_x {
429            self.ui.available_width
430        } else {
431            self.width.unwrap_or(text_w + self.padding.horizontal())
432        };
433        let mut final_h = self.height.unwrap_or(text_h + self.padding.vertical());
434
435        let final_border_w = final_border_w;
436        let final_border_c = final_border_c;
437
438        if is_hovered {
439            final_w *= self.hover_scale;
440            final_h *= self.hover_scale;
441        }
442
443        let start_draw = self.ui.draws.len();
444
445        // 1. Draw Background
446        self.ui.draws.push(DrawCommand::Rect(RectDraw {
447            instance: RectInstance {
448                pos: [x, y],
449                size: [final_w, final_h],
450                color: current_bg.to_array(),
451                radius: [
452                    self.radius[3],
453                    self.radius[2],
454                    self.radius[1],
455                    self.radius[0],
456                ],
457                border_width: final_border_w,
458                border_color: final_border_c.to_array(),
459                shadow_color: {
460                    let mut c = self.shadow_color;
461                    c.a *= self.shadow_opacity;
462                    c.to_array()
463                },
464                shadow_offset: self.shadow_offset,
465                shadow_blur: self.shadow_blur,
466                clip_rect: [0.0, 0.0, 9999.0, 9999.0],
467                grayscale: 0.0,
468                brightness: current_brightness,
469                opacity: self.opacity,
470                ..Default::default()
471            },
472        }));
473
474        // 2. Draw Text (aligned accordingly)
475        let tx = match self.align {
476            zenthra_core::Align::Left => x + self.padding.left,
477            zenthra_core::Align::Right => x + final_w - text_w - self.padding.right,
478            _ => x + (final_w - text_w) / 2.0,
479        };
480        let ty = y + (final_h - text_h) / 2.0;
481
482        let mut text_opts = zenthra_text::prelude::TextOptions::new()
483            .font_size(self.font_size)
484            .color(current_text)
485            .wrap(self.wrap)
486            .at(tx, ty);
487        if let Some(ref family) = self.font_family {
488            text_opts = text_opts.font_family(family.clone());
489        }
490
491        self.ui.draws.push(DrawCommand::Text(TextDraw {
492            text: self.label.clone(),
493            pos: [tx, ty],
494            options: text_opts,
495            clip: [0.0, 0.0, 9999.0, 9999.0],
496        }));
497
498        // 3. Register Semantic
499        self.ui.register_semantic(
500            SemanticNode::new(self.id, Role::Button, Rect::new(x, y, final_w, final_h))
501                .with_label(self.label.clone()),
502        );
503
504        self.ui
505            .record_layout(self.id, Rect::new(x, y, final_w, final_h));
506        self.ui.advance(final_w, final_h, start_draw);
507
508        if self.render_mode.is_some() {
509            self.ui.render_mode_stack.pop();
510        }
511
512        zenthra_core::Response {
513            clicked,
514            hovered: is_hovered,
515            pressed: is_pressed,
516        }
517    }
518}