1use crate::ui::{Ui, DrawCommand, OverlayRectDraw};
2use crate::text::TextBuilder;
3use zenthra_core::{Color, EdgeInsets, Id, Rect};
4use zenthra_platform::event::PlatformEvent;
5use zenthra_text::prelude::{TextOptions, CosmicFontProvider, Padding};
6pub struct TextAreaBuilder<'u, 'a, 'b> {
9 ui: &'u mut Ui<'a>,
10 buffer: &'b mut String,
11 id: Id,
12 x: f32,
13 y: f32,
14 font_size: f32,
15 color: Color,
16 bg: Option<Color>,
17 text_bg: Option<Color>,
18 highlight: Option<Color>,
19 padding: EdgeInsets,
20 text_padding: EdgeInsets,
21 line_height: f32,
22 width: f32,
23 height: Option<f32>,
24 scrollable: bool,
25 overflow_hidden: bool,
26 text_bg_fill_x: bool,
27 fill_x: bool,
28 wrap: zenthra_text::prelude::TextWrap,
29 radius: [f32; 4],
30 border_color: Option<Color>,
31 border_width: f32,
32 shadow_color: Option<Color>,
33 shadow_offset: [f32; 2],
34 shadow_blur: f32,
35 shadow_opacity: f32,
36 opacity: f32,
37 render_mode: Option<zenthra_core::RenderMode>,
38}
39
40impl<'u, 'a, 'b> TextAreaBuilder<'u, 'a, 'b> {
41 pub fn new(ui: &'u mut Ui<'a>, buffer: &'b mut String) -> Self {
42 let id = ui.id();
43 let x = ui.cursor_x;
44 let y = ui.cursor_y;
45 Self {
46 ui,
47 buffer,
48 id,
49 x,
50 y,
51 font_size: 18.0,
52 color: Color::WHITE,
53 bg: Some(Color::rgb(0.2, 0.2, 0.2)),
54 text_bg: None,
55 padding: EdgeInsets::ZERO,
56 text_padding: EdgeInsets::ZERO,
57 line_height: 1.2,
58 width: 300.0,
59 height: None,
60 scrollable: false,
61 overflow_hidden: false,
62 text_bg_fill_x: false,
63 fill_x: false,
64 highlight: None,
65 wrap: zenthra_text::prelude::TextWrap::Word,
66 radius: [4.0; 4],
67 border_color: None,
68 border_width: 0.0,
69 shadow_color: None,
70 shadow_offset: [0.0; 2],
71 shadow_blur: 0.0,
72 shadow_opacity: 1.0,
73 opacity: 1.0,
74 render_mode: None,
75 }
76 }
77
78 pub fn id(mut self, id: impl std::hash::Hash) -> Self {
79 let mut hasher = std::collections::hash_map::DefaultHasher::new();
80 use std::hash::Hasher;
81 id.hash(&mut hasher);
82 self.id = Id::from_u64(hasher.finish());
83 self
84 }
85
86 pub fn scrollable(mut self, enabled: bool) -> Self {
87 self.scrollable = enabled;
88 if enabled {
89 self.overflow_hidden = true;
90 }
91 self
92 }
93
94 pub fn overflow_hidden(mut self, enabled: bool) -> Self {
95 self.overflow_hidden = enabled;
96 self
97 }
98
99 pub fn size(mut self, size: f32) -> Self {
100 self.font_size = size;
101 self
102 }
103
104 pub fn color(mut self, color: Color) -> Self {
105 self.color = color;
106 self
107 }
108
109 pub fn bg(mut self, bg: Color) -> Self {
110 self.bg = Some(bg);
111 self
112 }
113
114 pub fn text_bg(mut self, bg: Color) -> Self {
115 self.text_bg = Some(bg);
116 self
117 }
118
119 pub fn highlight(mut self, color: Color) -> Self {
120 self.highlight = Some(color);
121 self
122 }
123
124 pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
125 self.padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
126 self
127 }
128 pub fn padding_x(mut self, x: f32) -> Self {
129 self.padding.left = x;
130 self.padding.right = x;
131 self
132 }
133 pub fn padding_y(mut self, y: f32) -> Self {
134 self.padding.top = y;
135 self.padding.bottom = y;
136 self
137 }
138 pub fn padding_top(mut self, t: f32) -> Self {
139 self.padding.top = t;
140 self
141 }
142 pub fn padding_bottom(mut self, b: f32) -> Self {
143 self.padding.bottom = b;
144 self
145 }
146 pub fn padding_left(mut self, l: f32) -> Self {
147 self.padding.left = l;
148 self
149 }
150 pub fn padding_right(mut self, r: f32) -> Self {
151 self.padding.right = r;
152 self
153 }
154
155 pub fn text_padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
156 self.text_padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
157 self
158 }
159 pub fn text_padding_x(mut self, x: f32) -> Self {
160 self.text_padding.left = x;
161 self.text_padding.right = x;
162 self
163 }
164 pub fn text_padding_y(mut self, y: f32) -> Self {
165 self.text_padding.top = y;
166 self.text_padding.bottom = y;
167 self
168 }
169 pub fn text_padding_top(mut self, t: f32) -> Self {
170 self.text_padding.top = t;
171 self
172 }
173 pub fn text_padding_bottom(mut self, b: f32) -> Self {
174 self.text_padding.bottom = b;
175 self
176 }
177 pub fn text_padding_left(mut self, l: f32) -> Self {
178 self.text_padding.left = l;
179 self
180 }
181 pub fn text_padding_right(mut self, r: f32) -> Self {
182 self.text_padding.right = r;
183 self
184 }
185
186 pub fn line_height(mut self, lh: f32) -> Self {
187 self.line_height = lh;
188 self
189 }
190
191 pub fn width(mut self, w: f32) -> Self {
192 self.width = w;
193 self
194 }
195
196 pub fn fill_x(mut self) -> Self {
197 self.fill_x = true;
198 self
199 }
200
201 pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
202 self.radius = [tl, tr, br, bl];
203 self
204 }
205
206 pub fn radius_all(mut self, r: f32) -> Self {
207 self.radius = [r; 4];
208 self
209 }
210
211 pub fn radius_top(mut self, r: f32) -> Self {
212 self.radius[0] = r;
213 self.radius[1] = r;
214 self
215 }
216
217 pub fn radius_bottom(mut self, r: f32) -> Self {
218 self.radius[2] = r;
219 self.radius[3] = r;
220 self
221 }
222
223 pub fn radius_top_left(mut self, r: f32) -> Self {
224 self.radius[0] = r;
225 self
226 }
227
228 pub fn radius_top_right(mut self, r: f32) -> Self {
229 self.radius[1] = r;
230 self
231 }
232
233 pub fn radius_bottom_right(mut self, r: f32) -> Self {
234 self.radius[2] = r;
235 self
236 }
237
238 pub fn radius_bottom_left(mut self, r: f32) -> Self {
239 self.radius[3] = r;
240 self
241 }
242
243 pub fn radius_left(mut self, r: f32) -> Self {
244 self.radius[0] = r;
245 self.radius[3] = r;
246 self
247 }
248
249 pub fn radius_right(mut self, r: f32) -> Self {
250 self.radius[1] = r;
251 self.radius[2] = r;
252 self
253 }
254
255 pub fn border(mut self, color: Color, width: f32) -> Self {
256 self.border_color = Some(color);
257 self.border_width = width;
258 self
259 }
260
261 pub fn shadow(mut self, color: Color, ox: f32, oy: f32, blur: f32) -> Self {
262 self.shadow_color = Some(color);
263 self.shadow_offset = [ox, oy];
264 self.shadow_blur = blur;
265 self
266 }
267
268 pub fn opacity(mut self, o: f32) -> Self {
269 self.opacity = o;
270 self
271 }
272
273 pub fn text_bg_fill_x(mut self, enabled: bool) -> Self {
274 self.text_bg_fill_x = enabled;
275 self
276 }
277
278 pub fn height(mut self, h: f32) -> Self {
279 self.height = Some(h);
280 self
281 }
282
283 pub fn wrap(mut self, strategy: zenthra_text::prelude::TextWrap) -> Self {
284 self.wrap = strategy;
285 self
286 }
287
288 pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
289 self.render_mode = Some(mode);
290 self
291 }
292
293 pub fn show(self) -> zenthra_core::Response {
294 if let Some(mode) = self.render_mode {
295 self.ui.render_mode_stack.push(mode);
296 }
297 let is_focused = self.ui.focused_id == Some(self.id);
298
299 let (scroll_x, mut scroll_y) = if self.scrollable {
301 *self.ui.scroll_state.get(&self.id).unwrap_or(&(0.0, 0.0))
302 } else {
303 (0.0, 0.0)
304 };
305
306 let actual_width = if self.fill_x {
308 (self.ui.max_x - self.x).max(self.width)
309 } else {
310 self.width
311 };
312
313 let (_content_w, mut h_content, mut shaped_buffer) = if let Some(fs) = self.ui.font_system.as_ref() {
314 let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
315 let t_padding = Padding::from(self.text_padding);
316 let layout_width = actual_width - self.padding.horizontal() - t_padding.horizontal();
317 adapter.set_layout_size(layout_width, 10000.0);
318
319 let options = TextOptions::new()
320 .font_size(self.font_size)
321 .line_height(self.line_height)
322 .wrap(self.wrap)
323 .max_width(layout_width);
324
325 let buffer = adapter.shape(&self.buffer, &options);
326 let (_, ch) = buffer.content_size();
327 (actual_width, ch + t_padding.vertical(), Some(buffer))
328 } else {
329 (actual_width, 20.0, None)
330 };
331
332 let mut h_box = if self.scrollable {
333 self.height.unwrap_or(200.0)
334 } else {
335 h_content + self.padding.vertical()
336 };
337
338 if self.scrollable && self.height.unwrap_or(0.0) == 0.0 {
339 h_box = 200.0;
340 }
341
342 let mut cursor_index = *self.ui.cursor_state.get(&self.id).unwrap_or(&self.buffer.len());
344 cursor_index = cursor_index.min(self.buffer.len());
345 if !self.buffer.is_char_boundary(cursor_index) {
346 cursor_index = self.buffer.len(); }
348
349 let (actual_x, actual_y) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
350 (rect.origin.x + self.ui.offset_x, rect.origin.y + self.ui.offset_y)
351 } else {
352 (self.x + self.ui.offset_x, self.y + self.ui.offset_y)
353 };
354 let is_hovered = self.ui.is_hovered(self.id, actual_x, actual_y, actual_width, h_box);
355
356 if is_focused || is_hovered {
357 let events = std::mem::take(&mut self.ui.input_events);
358 let mut changed = false;
359 for event in &events {
360 match event {
361 PlatformEvent::CharTyped(c) if is_focused => {
362 if *c != '\r' && *c != '\n' {
363 self.buffer.insert(cursor_index, *c);
364 cursor_index += c.len_utf8();
365 changed = true;
366 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
367 }
368 }
369 PlatformEvent::KeyDown { key } if is_focused => {
370 match key {
371 winit::keyboard::KeyCode::Backspace => {
372 if cursor_index > 0 {
373 let mut chars = self.buffer[..cursor_index].chars();
374 if let Some(c) = chars.next_back() {
375 let len = c.len_utf8();
376 self.buffer.remove(cursor_index - len);
377 cursor_index -= len;
378 changed = true;
379 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
380 }
381 }
382 }
383 winit::keyboard::KeyCode::ArrowLeft => {
384 if cursor_index > 0 {
385 let mut chars = self.buffer[..cursor_index].chars();
386 if let Some(c) = chars.next_back() {
387 cursor_index -= c.len_utf8();
388 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
389 self.ui.needs_redraw = true;
390 }
391 }
392 }
393 winit::keyboard::KeyCode::ArrowRight => {
394 if cursor_index < self.buffer.len() {
395 let mut chars = self.buffer[cursor_index..].chars();
396 if let Some(c) = chars.next() {
397 cursor_index += c.len_utf8();
398 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
399 self.ui.needs_redraw = true;
400 }
401 }
402 }
403 winit::keyboard::KeyCode::ArrowUp => {
404 if let Some(sb) = &shaped_buffer {
405 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
406 self.ui.needs_redraw = true;
407 let row_threshold = (self.font_size * self.line_height) * 0.5;
408
409 let mut current_line_idx = 0;
411 for (i, line) in sb.lines().iter().enumerate() {
412 if line.start_cluster <= cursor_index {
413 current_line_idx = i;
414 } else {
415 break;
416 }
417 }
418
419 if current_line_idx > 0 {
420 let target_line_idx = current_line_idx - 1;
422 let target_line = &sb.lines()[target_line_idx];
423
424 let mut target_x = 0.0;
426 if let Some(current_line) = sb.lines().get(current_line_idx) {
427 for g in sb.glyphs() {
428 if (g.y - current_line.y).abs() < row_threshold {
429 if g.cluster < cursor_index {
430 target_x = g.x + g.width;
431 } else if g.cluster == cursor_index {
432 target_x = g.x;
433 break;
434 }
435 }
436 }
437 }
438
439 let mut best_idx = None;
441 let mut best_dist = f32::INFINITY;
442 for g in sb.glyphs() {
443 if (g.y - target_line.y).abs() < row_threshold {
444 let dist = (g.x - target_x).abs();
445 if dist < best_dist {
446 best_dist = dist;
447 best_idx = Some(g.cluster);
448 }
449 }
450 }
451
452 if let Some(idx) = best_idx {
453 cursor_index = idx;
454 } else {
455 cursor_index = target_line.start_cluster;
457 }
458 }
459 }
460 }
461 winit::keyboard::KeyCode::ArrowDown => {
462 if let Some(sb) = &shaped_buffer {
463 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
464 self.ui.needs_redraw = true;
465 let row_threshold = (self.font_size * self.line_height) * 0.5;
466
467 let mut current_line_idx = 0;
469 for (i, line) in sb.lines().iter().enumerate() {
470 if line.start_cluster <= cursor_index {
471 current_line_idx = i;
472 } else {
473 break;
474 }
475 }
476
477 if current_line_idx < sb.lines().len() - 1 {
478 let target_line_idx = current_line_idx + 1;
480 let target_line = &sb.lines()[target_line_idx];
481
482 let mut target_x = 0.0;
484 if let Some(current_line) = sb.lines().get(current_line_idx) {
485 for g in sb.glyphs() {
486 if (g.y - current_line.y).abs() < row_threshold {
487 if g.cluster < cursor_index {
488 target_x = g.x + g.width;
489 } else if g.cluster == cursor_index {
490 target_x = g.x;
491 break;
492 }
493 }
494 }
495 }
496
497 let mut best_idx = None;
499 let mut best_dist = f32::INFINITY;
500 for g in sb.glyphs() {
501 if (g.y - target_line.y).abs() < row_threshold {
502 let dist = (g.x - target_x).abs();
503 if dist < best_dist {
504 best_dist = dist;
505 best_idx = Some(g.cluster);
506 }
507 }
508 }
509
510 if let Some(idx) = best_idx {
511 cursor_index = idx;
512 } else {
513 cursor_index = target_line.start_cluster;
515 }
516 }
517 }
518 }
519 winit::keyboard::KeyCode::Enter | winit::keyboard::KeyCode::NumpadEnter => {
520 self.buffer.insert(cursor_index, '\n');
521 cursor_index += 1;
522 changed = true;
523 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
524 }
525 _ => {}
526 }
527 }
528 PlatformEvent::MouseWheel { delta_x, delta_y } if self.scrollable && is_hovered => {
529 let total_delta = *delta_x + *delta_y;
530 scroll_y -= total_delta * 30.0;
531 }
532 _ => {}
533 }
534 }
535
536 if changed {
537 if let Some(fs) = self.ui.font_system.as_ref() {
539 let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
540 let t_padding = Padding::from(self.text_padding);
541 let layout_width = actual_width - self.padding.horizontal() - t_padding.horizontal();
542 adapter.set_layout_size(layout_width, 10000.0);
543
544 let options = TextOptions::new().font_size(self.font_size).line_height(self.line_height).wrap(self.wrap).max_width(layout_width);
545 let buffer = adapter.shape(&self.buffer, &options);
546 let (_cw, ch) = buffer.content_size();
547 h_content = ch + t_padding.vertical();
548 shaped_buffer = Some(buffer);
549
550 if !self.scrollable {
552 h_box = h_content + self.padding.vertical();
553 }
554 }
555
556 self.ui.cursor_state.insert(self.id, cursor_index);
557 }
558
559 if self.scrollable {
560 let usable_h = h_box - self.padding.vertical();
561 let max_scroll = (h_content - usable_h).max(0.0);
562
563 if let Some(sb) = &shaped_buffer {
565 let mut ly = 0.0;
566 let mut found = false;
567 for line in sb.lines() {
568 if line.start_cluster <= cursor_index {
569 ly = line.y;
570 found = true;
571 } else { break; }
572 }
573 if found {
574 let line_h = self.font_size * self.line_height;
575 let cursor_y_v = ly + self.text_padding.top;
576
577 if cursor_y_v > scroll_y + usable_h - line_h {
578 scroll_y = cursor_y_v - usable_h + line_h;
579 } else if cursor_y_v < scroll_y {
580 scroll_y = cursor_y_v;
581 }
582 }
583 }
584
585 scroll_y = scroll_y.clamp(0.0, max_scroll);
586 }
587 self.ui.input_events = events;
588 self.ui.cursor_state.insert(self.id, cursor_index);
589 self.ui.scroll_state.insert(self.id, (scroll_x, scroll_y));
590 }
591
592 let start_draw = self.ui.draws.len();
594 if let Some(bg) = self.bg {
595 use crate::ui::RectDraw;
596 use zenthra_render::RectInstance;
597 self.ui.draws.push(DrawCommand::Rect(RectDraw {
598 instance: RectInstance {
599 pos: [self.x, self.y],
600 size: [actual_width, h_box],
601 color: bg.to_array(),
602 radius: self.radius,
603 border_width: self.border_width.max(if is_focused { 1.0 } else { 0.0 }),
604 border_color: self.border_color.unwrap_or(Color::rgba(1.0, 1.0, 1.0, 0.4)).to_array(),
605 shadow_color: self.shadow_color.map(|c| {
606 let mut a = c.to_array();
607 a[3] *= self.shadow_opacity;
608 a
609 }).unwrap_or([0.0, 0.0, 0.0, 0.0]),
610 shadow_offset: self.shadow_offset,
611 shadow_blur: self.shadow_blur,
612 clip_rect: [0.0, 0.0, 9999.0, 9999.0],
613 grayscale: 0.0,
614 brightness: 1.0,
615 opacity: self.opacity,
616 ..Default::default()
617 }
618 }));
619 }
620
621 let pos_y = self.y + self.padding.top - scroll_y;
623 let mut text_builder = TextBuilder::new(self.ui, &self.buffer)
624 .size(self.font_size)
625 .line_height(self.line_height)
626 .color(self.color)
627 .fill_x(false)
628 .padding(self.text_padding.top, self.text_padding.right, self.text_padding.bottom, self.text_padding.left)
629 .wrap(self.wrap)
630 .max_width(actual_width - self.padding.horizontal() - self.text_padding.horizontal())
631 .pos(self.x + self.padding.left, pos_y);
632
633 if self.text_bg_fill_x {
634 text_builder = text_builder.min_width(actual_width - self.padding.horizontal());
635 }
636
637 if let Some(tbg) = self.text_bg {
638 text_builder = text_builder.bg(tbg).fill_x(false);
639 }
640
641 if let Some(h) = self.highlight {
642 text_builder = text_builder.highlight(h);
643 }
644
645 if self.overflow_hidden {
646 text_builder = text_builder.clip_rect(self.x, self.y, actual_width, h_box);
648 }
649
650 text_builder.draw_and_measure();
652
653 if self.scrollable {
655 self.ui.scroll_state.insert(self.id, (scroll_x, scroll_y));
656
657 if h_content > h_box - self.padding.vertical() {
659 let usable_h = h_box - self.padding.vertical();
660 let scroll_bar_w = 4.0;
661 let scroll_bar_x = self.x + actual_width - scroll_bar_w - 4.0;
662
663 let thumb_h = (usable_h / h_content) * usable_h;
664 let thumb_h = thumb_h.clamp(20.0, usable_h);
665
666 let max_scroll = (h_content - usable_h).max(1.0);
667 let scroll_percent = scroll_y / max_scroll;
668 let thumb_y = self.y + self.padding.top + scroll_percent * (usable_h - thumb_h);
669
670 self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
671 x: scroll_bar_x,
672 y: thumb_y,
673 width: scroll_bar_w,
674 height: thumb_h,
675 color: Color::rgba(1.0, 1.0, 1.0, 0.4),
676 clip: [self.x, self.y, actual_width, h_box],
677 }));
678 }
679 }
680
681 if self.ui.mouse_down {
683 if is_hovered {
684 self.ui.focused_id = Some(self.id);
685 }
686 }
687
688 if is_focused {
690 let font_size = self.font_size;
691 let lh = self.line_height;
692 let visual_ascent = font_size * (0.8 + (lh - 1.0) / 2.0);
693 let cursor_height = font_size * lh;
694
695 if let Some(sb) = shaped_buffer {
696 let first_line_y = sb.lines().first().map(|l| l.y).unwrap_or(visual_ascent);
697 let v_shift = visual_ascent - first_line_y;
698
699 let _final_border_w = self.border_width;
700 let _final_border_c = self.border_color.unwrap_or(Color::rgba(1.0, 1.0, 1.0, 0.4));
701 let mut lx = 0.0;
702 let mut ly = first_line_y;
703 let mut found = false;
704
705 for g in sb.glyphs() {
706 if g.cluster == cursor_index {
707 lx = g.x;
708 ly = g.y;
709 found = true;
710 break;
711 }
712 }
713
714 if !found {
715 let mut best_line = None;
716 for line in sb.lines() {
717 if line.start_cluster <= cursor_index {
718 best_line = Some(line);
719 } else {
720 break;
721 }
722 }
723
724 if let Some(line) = best_line {
725 ly = line.y;
726 if cursor_index > line.start_cluster {
729 lx = line.width;
730 } else {
731 lx = 0.0;
732 }
733 }
734
735 if cursor_index == self.buffer.len() {
736 if let Some(lg) = sb.glyphs().last() {
737 if (lg.y - ly).abs() < 2.0 {
738 lx = lg.x + lg.width;
739 } else {
740 if let Some(last_line) = sb.lines().last() {
742 ly = last_line.y;
743 lx = 0.0;
744 }
745 }
746 }
747 }
748 }
749
750 let cx = lx + self.x + self.padding.left + self.text_padding.left;
751 let cy = ly + self.y + self.padding.top + self.text_padding.top + v_shift - visual_ascent - scroll_y;
752
753 let last_activity = *self.ui.interaction_state.get(&self.id).unwrap_or(&0.0);
755 let time_since_activity = self.ui.elapsed_time - last_activity;
756
757 let is_blink_visible = if time_since_activity < 0.5 {
758 true } else {
760 (self.ui.elapsed_time - last_activity).fract() < 0.5
763 };
764
765 if cy >= self.y - 2.0 && cy + cursor_height <= self.y + h_box + 2.0 && is_blink_visible {
767 self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
768 x: cx,
769 y: cy,
770 width: 2.0,
771 height: cursor_height,
772 color: Color::WHITE,
773 clip: [self.x, self.y, actual_width, h_box],
774 }));
775 }
776 }
777 }
778
779 self.ui.record_layout(self.id, Rect::new(self.x, self.y, actual_width, h_box));
781 self.ui.advance(actual_width, h_box, start_draw);
782
783 if self.render_mode.is_some() {
784 self.ui.render_mode_stack.pop();
785 }
786
787 zenthra_core::Response {
788 clicked: self.ui.clicked && is_hovered,
789 hovered: is_hovered,
790 pressed: is_hovered && self.ui.mouse_down,
791 }
792 }
793}