1use crate::ui::{Ui, DrawCommand, OverlayRectDraw, ScrollDrag};
2use crate::text::TextBuilder;
3use zenthra_core::{Color, EdgeInsets, Id, Role, SemanticNode, Rect};
4use zenthra_platform::event::PlatformEvent;
5use zenthra_text::prelude::{TextOptions, CosmicFontProvider, Padding, ShapedGlyph};
6use zenthra_text::traits::FontProvider;
7
8pub struct InputBuilder<'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: Option<f32>,
23 min_width: f32,
24 scrollable: bool,
25 fill_x: bool,
26 text_bg_fill_x: bool,
27 radius: [f32; 4],
28 border_color: Option<Color>,
29 border_width: f32,
30 opacity: f32,
31 shadow_color: Option<Color>,
32 shadow_offset: [f32; 2],
33 shadow_blur: f32,
34 shadow_opacity: f32,
35 render_mode: Option<zenthra_core::RenderMode>,
36}
37
38impl<'u, 'a, 'b> InputBuilder<'u, 'a, 'b> {
39 pub fn new(ui: &'u mut Ui<'a>, buffer: &'b mut String) -> Self {
40 let id = ui.id();
41 let x = ui.cursor_x;
42 let y = ui.cursor_y;
43 Self {
44 ui,
45 buffer,
46 id,
47 x,
48 y,
49 font_size: 18.0,
50 color: Color::WHITE,
51 bg: Some(Color::rgb(0.2, 0.2, 0.2)),
52 text_bg: None,
53 padding: EdgeInsets::ZERO,
54 text_padding: EdgeInsets::ZERO,
55 line_height: 1.2,
56 width: None,
57 min_width: 200.0,
58 scrollable: true,
59 fill_x: false,
60 text_bg_fill_x: false,
61 highlight: None,
62 radius: [4.0; 4],
63 border_color: None,
64 border_width: 0.0,
65 opacity: 1.0,
66 shadow_color: None,
67 shadow_offset: [0.0; 2],
68 shadow_blur: 0.0,
69 shadow_opacity: 1.0,
70 render_mode: None,
71 }
72 }
73
74 pub fn id(mut self, id: impl std::hash::Hash) -> Self {
75 let mut hasher = std::collections::hash_map::DefaultHasher::new();
76 use std::hash::Hasher;
77 id.hash(&mut hasher);
78 self.id = Id::from_u64(hasher.finish());
79 self
80 }
81
82 pub fn size(mut self, size: f32) -> Self {
83 self.font_size = size;
84 self
85 }
86
87 pub fn color(mut self, color: Color) -> Self {
88 self.color = color;
89 self
90 }
91
92 pub fn bg(mut self, bg: Color) -> Self {
93 self.bg = Some(bg);
94 self
95 }
96
97 pub fn text_bg(mut self, bg: Color) -> Self {
98 self.text_bg = Some(bg);
99 self
100 }
101
102 pub fn highlight(mut self, color: Color) -> Self {
103 self.highlight = Some(color);
104 self
105 }
106
107 pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
108 self.padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
109 self
110 }
111 pub fn padding_x(mut self, x: f32) -> Self {
112 self.padding.left = x;
113 self.padding.right = x;
114 self
115 }
116 pub fn padding_y(mut self, y: f32) -> Self {
117 self.padding.top = y;
118 self.padding.bottom = y;
119 self
120 }
121 pub fn padding_top(mut self, t: f32) -> Self {
122 self.padding.top = t;
123 self
124 }
125 pub fn padding_bottom(mut self, b: f32) -> Self {
126 self.padding.bottom = b;
127 self
128 }
129 pub fn padding_left(mut self, l: f32) -> Self {
130 self.padding.left = l;
131 self
132 }
133 pub fn padding_right(mut self, r: f32) -> Self {
134 self.padding.right = r;
135 self
136 }
137
138 pub fn text_padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
139 self.text_padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
140 self
141 }
142 pub fn text_padding_x(mut self, x: f32) -> Self {
143 self.text_padding.left = x;
144 self.text_padding.right = x;
145 self
146 }
147 pub fn text_padding_y(mut self, y: f32) -> Self {
148 self.text_padding.top = y;
149 self.text_padding.bottom = y;
150 self
151 }
152 pub fn text_padding_top(mut self, t: f32) -> Self {
153 self.text_padding.top = t;
154 self
155 }
156 pub fn text_padding_bottom(mut self, b: f32) -> Self {
157 self.text_padding.bottom = b;
158 self
159 }
160 pub fn text_padding_left(mut self, l: f32) -> Self {
161 self.text_padding.left = l;
162 self
163 }
164 pub fn text_padding_right(mut self, r: f32) -> Self {
165 self.text_padding.right = r;
166 self
167 }
168
169 pub fn line_height(mut self, lh: f32) -> Self {
170 self.line_height = lh;
171 self
172 }
173
174 pub fn width(mut self, w: f32) -> Self {
175 self.width = Some(w);
176 self
177 }
178
179 pub fn min_width(mut self, w: f32) -> Self {
180 self.min_width = w;
181 self
182 }
183
184 pub fn scrollable(mut self, s: bool) -> Self {
185 self.scrollable = s;
186 self
187 }
188
189 pub fn fill_x(mut self) -> Self {
190 self.fill_x = true;
191 self
192 }
193
194 pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
195 self.radius = [tl, tr, br, bl];
196 self
197 }
198
199 pub fn radius_all(mut self, r: f32) -> Self {
200 self.radius = [r, r, r, r];
201 self
202 }
203
204 pub fn radius_top(mut self, r: f32) -> Self {
205 self.radius[0] = r;
206 self.radius[1] = r;
207 self
208 }
209
210 pub fn radius_bottom(mut self, r: f32) -> Self {
211 self.radius[2] = r;
212 self.radius[3] = r;
213 self
214 }
215
216 pub fn radius_top_left(mut self, r: f32) -> Self {
217 self.radius[0] = r;
218 self
219 }
220
221 pub fn radius_top_right(mut self, r: f32) -> Self {
222 self.radius[1] = r;
223 self
224 }
225
226 pub fn radius_bottom_right(mut self, r: f32) -> Self {
227 self.radius[2] = r;
228 self
229 }
230
231 pub fn radius_bottom_left(mut self, r: f32) -> Self {
232 self.radius[3] = r;
233 self
234 }
235
236 pub fn radius_left(mut self, r: f32) -> Self {
237 self.radius[0] = r;
238 self.radius[3] = r;
239 self
240 }
241
242 pub fn radius_right(mut self, r: f32) -> Self {
243 self.radius[1] = r;
244 self.radius[2] = r;
245 self
246 }
247
248 pub fn border(mut self, color: Color, width: f32) -> Self {
249 self.border_color = Some(color);
250 self.border_width = width;
251 self
252 }
253
254 pub fn shadow(mut self, color: Color, ox: f32, oy: f32, blur: f32) -> Self {
255 self.shadow_color = Some(color);
256 self.shadow_offset = [ox, oy];
257 self.shadow_blur = blur;
258 self
259 }
260
261 pub fn opacity(mut self, o: f32) -> Self {
262 self.opacity = o;
263 self
264 }
265
266 pub fn text_bg_fill_x(mut self, enabled: bool) -> Self {
267 self.text_bg_fill_x = enabled;
268 self
269 }
270
271 pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
272 self.render_mode = Some(mode);
273 self
274 }
275
276 pub fn on_change<F>(self, mut f: F) -> Self
277 where
278 F: FnMut(String) + 'a,
279 {
280 self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
281 if let crate::ui::WidgetEvent::Change(crate::ui::EventValue::String(val)) = event {
282 f(val.clone());
283 }
284 });
285 self
286 }
287
288 pub fn on_hover<F>(self, mut f: F) -> Self
289 where
290 F: FnMut(bool) + 'a,
291 {
292 self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
293 if let crate::ui::WidgetEvent::Hover(hovered) = event {
294 f(*hovered);
295 }
296 });
297 self
298 }
299
300 pub fn on_event<F>(self, phase: crate::ui::EventPhase, f: F) -> Self
301 where
302 F: FnMut(&mut crate::ui::EventContext, &crate::ui::WidgetEvent) + 'a,
303 {
304 self.ui.add_listener(self.id, phase, f);
305 self
306 }
307
308 pub fn show(self) -> zenthra_core::Response {
309 if let Some(mode) = self.render_mode {
310 self.ui.render_mode_stack.push(mode);
311 }
312 let is_focused = self.ui.focused_id == Some(self.id);
313
314 let (mut w_text_raw, h_content, mut shaped_buffer) = if let Some(fs) = self.ui.font_system.as_ref() {
316 let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
317 let t_padding = Padding::from(self.text_padding);
318 adapter.set_layout_size(1000000.0, 10000.0);
319 let options = TextOptions::new()
320 .font_size(self.font_size)
321 .line_height(self.line_height)
322 .wrap(zenthra_text::prelude::TextWrap::None);
323 let buffer = adapter.shape(&self.buffer, &options);
324 let (cw, _ch) = buffer.content_size();
325 let m = adapter.metrics(&options);
326 (cw + t_padding.horizontal(), m.line_height() + t_padding.vertical(), Some(buffer))
327 } else {
328 (self.min_width, 20.0, None)
329 };
330
331 let max_available_w = (self.ui.max_x - self.x).max(self.min_width);
332 let mut w_box = if self.fill_x { max_available_w } else { self.width.unwrap_or_else(|| (w_text_raw + self.padding.horizontal()).min(max_available_w)).max(self.min_width) };
333 let h_box = h_content + self.padding.vertical();
334 let mut w_view = w_box - self.padding.horizontal();
335
336 let mut cursor_index = *self.ui.cursor_state.get(&self.id).unwrap_or(&self.buffer.len());
338 cursor_index = cursor_index.min(self.buffer.len());
339 if !self.buffer.is_char_boundary(cursor_index) {
340 cursor_index = self.buffer.len();
341 }
342
343 let (actual_x, actual_y) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
344 (rect.origin.x + self.ui.offset_x, rect.origin.y + self.ui.offset_y)
345 } else {
346 (self.x + self.ui.offset_x, self.y + self.ui.offset_y)
347 };
348 let is_hovered = self.ui.is_hovered(self.id, actual_x, actual_y, w_box, h_box);
349 if is_hovered {
350 self.ui.cursor_icon = crate::text::CursorIcon::Text;
351 }
352 self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Hover(is_hovered));
353 let mut needs_auto_scroll = false;
354 let mut changed = false;
355
356 if is_focused || is_hovered || self.ui.active_drag.is_some() {
357 let events = std::mem::take(&mut self.ui.input_events);
358 for event in &events {
359 match event {
360 PlatformEvent::CharTyped(c) if is_focused => {
361 if *c != '\r' && *c != '\n' {
362 self.buffer.insert(cursor_index, *c);
363 cursor_index += c.len_utf8();
364 needs_auto_scroll = true;
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 needs_auto_scroll = true;
379 changed = true;
380 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
381 }
382 }
383 }
384 winit::keyboard::KeyCode::ArrowLeft => {
385 if cursor_index > 0 {
386 let mut chars = self.buffer[..cursor_index].chars();
387 if let Some(c) = chars.next_back() {
388 cursor_index -= c.len_utf8();
389 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
390 self.ui.needs_redraw = true;
391 needs_auto_scroll = true;
392 }
393 }
394 }
395 winit::keyboard::KeyCode::ArrowRight => {
396 if cursor_index < self.buffer.len() {
397 let mut chars = self.buffer[cursor_index..].chars();
398 if let Some(c) = chars.next() {
399 cursor_index += c.len_utf8();
400 self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
401 self.ui.needs_redraw = true;
402 needs_auto_scroll = true;
403 }
404 }
405 }
406 _ => {}
407 }
408 }
409 PlatformEvent::MouseWheel { delta_x, delta_y } if is_hovered => {
410 let _id = self.id;
411 let (mut sx, sy) = *self.ui.scroll_state.get(&self.id).unwrap_or(&(0.0, 0.0));
412 let effect = if delta_x.abs() < 0.001 { *delta_y } else { *delta_x };
413 sx -= effect * 30.0;
414 self.ui.scroll_state.insert(self.id, (sx, sy));
415 }
416 _ => {}
417 }
418 }
419
420 if self.ui.clicked && is_hovered {
421 self.ui.focused_id = Some(self.id);
422 needs_auto_scroll = true;
423 self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Click);
424 }
425
426 self.ui.input_events = events;
427 self.ui.cursor_state.insert(self.id, cursor_index);
428
429 if changed {
430 self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Change(crate::ui::EventValue::String(self.buffer.clone())));
431 }
432 }
433
434 if changed {
436 if let Some(fs) = self.ui.font_system.as_ref() {
437 let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
438 let t_padding = Padding::from(self.text_padding);
439 adapter.set_layout_size(1000000.0, 10000.0);
440 let options = TextOptions::new()
441 .font_size(self.font_size)
442 .line_height(self.line_height)
443 .wrap(zenthra_text::prelude::TextWrap::None);
444 let buffer = adapter.shape(&self.buffer, &options);
445 let (cw, _ch) = buffer.content_size();
446 w_text_raw = cw + t_padding.horizontal();
447 shaped_buffer = Some(buffer);
448
449 w_box = if self.fill_x { max_available_w } else { self.width.unwrap_or_else(|| (w_text_raw + self.padding.horizontal()).min(max_available_w)).max(self.min_width) };
451 w_view = w_box - self.padding.horizontal();
452 }
453 }
454
455 let scroll_state_id = self.id;
457 let (mut scroll_x, scroll_y) = *self.ui.scroll_state.get(&scroll_state_id).unwrap_or(&(0.0, 0.0));
458 let max_scroll = (w_text_raw - w_view).max(0.0f32);
459
460 let scroll_bar_h = 4.0;
461 let scroll_bar_y = self.y + h_box - scroll_bar_h - 2.0;
462 let thumb_w = if w_text_raw > w_view { (w_view / w_text_raw) * w_view } else { w_view };
463 let thumb_w = thumb_w.max(20.0);
464 let scroll_percent = if max_scroll > 0.0 { scroll_x / max_scroll } else { 0.0 };
465 let track_width = (w_view - thumb_w).max(0.0);
466 let thumb_x = self.x + self.padding.left + scroll_percent * track_width;
467
468 let actual_thumb_x = actual_x + self.padding.left + scroll_percent * track_width;
469 let actual_scroll_bar_y = actual_y + h_box - scroll_bar_h - 2.0;
470 let is_over_thumb = self.ui.mouse_in_rect(actual_thumb_x, actual_scroll_bar_y - 2.0, thumb_w, scroll_bar_h + 4.0);
471
472 if let Some(drag) = self.ui.active_drag {
474 if drag.id == scroll_state_id {
475 let delta_mouse = self.ui.mouse_x - drag.start_mouse;
476 if track_width > 0.0 {
477 let scroll_delta = (delta_mouse / track_width) * max_scroll;
478 scroll_x = (drag.start_scroll + scroll_delta).clamp(0.0, max_scroll);
479 }
480 }
481 }
482
483 if self.ui.clicked && is_over_thumb {
484 self.ui.active_drag = Some(ScrollDrag {
485 id: scroll_state_id,
486 start_mouse: self.ui.mouse_x,
487 start_scroll: scroll_x,
488 });
489 }
490
491 let is_dragging_this = self.ui.active_drag.map(|d| d.id == scroll_state_id).unwrap_or(false);
493 if is_focused && !is_dragging_this && needs_auto_scroll {
494 if let Some(sb) = &shaped_buffer {
495 let mut clx = 0.0;
496 let mut found = false;
497 for g in sb.glyphs() {
498 if g.cluster == cursor_index {
499 clx = g.x;
500 found = true;
501 break;
502 }
503 }
504 if !found && cursor_index == self.buffer.len() {
505 clx = sb.glyphs().last().map(|g: &ShapedGlyph| g.x + g.width).unwrap_or(0.0);
506 }
507
508 let cursor_x_v = clx + self.text_padding.left;
509 if cursor_x_v > scroll_x + w_view - 40.0 {
510 scroll_x = cursor_x_v - w_view + 40.0;
511 } else if cursor_x_v < scroll_x + 10.0 {
512 scroll_x = (cursor_x_v - 10.0).max(0.0);
513 }
514 }
515 }
516
517 scroll_x = scroll_x.clamp(0.0, max_scroll);
518 self.ui.scroll_state.insert(scroll_state_id, (scroll_x, scroll_y));
519
520 let start_draw = self.ui.draws.len();
522 if let Some(bg) = self.bg {
523 use crate::ui::RectDraw;
524 use zenthra_render::RectInstance;
525 self.ui.draws.push(DrawCommand::Rect(RectDraw {
526 instance: RectInstance {
527 pos: [self.x, self.y],
528 size: [w_box, h_box],
529 color: bg.to_array(),
530 radius: [
531 self.radius[3],
532 self.radius[2],
533 self.radius[1],
534 self.radius[0],
535 ],
536 border_width: if is_focused { self.border_width.max(1.0) } else { self.border_width },
537 border_color: if is_focused { self.border_color.unwrap_or(Color::rgb(0.4, 0.7, 1.0)).to_array() } else { self.border_color.unwrap_or(Color::TRANSPARENT).to_array() },
538 shadow_color: self.shadow_color.map(|mut c| { c.a *= self.shadow_opacity; c.to_array() }).unwrap_or([0.0; 4]),
539 shadow_offset: self.shadow_offset,
540 shadow_blur: self.shadow_blur,
541 clip_rect: [0.0, 0.0, 9999.0, 9999.0],
542 grayscale: 0.0,
543 brightness: 1.0,
544 opacity: self.opacity,
545 ..Default::default()
546 }
547 }));
548 }
549
550 let mut text_builder = TextBuilder::new(self.ui, &self.buffer)
552 .size(self.font_size)
553 .line_height(self.line_height)
554 .color(self.color)
555 .fill_x(self.fill_x)
556 .padding(self.text_padding.top, self.text_padding.right, self.text_padding.bottom, self.text_padding.left)
557 .wrap(zenthra_text::prelude::TextWrap::None)
558 .max_width(1000000.0)
559 .pos(self.x + self.padding.left - scroll_x, self.y + self.padding.top)
560 .clip_rect(self.x, self.y, w_box, h_box);
561
562 if self.text_bg_fill_x {
563 text_builder = text_builder.min_width(w_view);
564 }
565
566 if let Some(tbg) = self.text_bg {
567 text_builder = text_builder.bg(tbg).fill_x(false);
568 }
569
570 if let Some(h) = self.highlight {
571 text_builder = text_builder.highlight(h);
572 }
573
574 let (_, _, final_sb, _) = text_builder.draw_and_measure();
575
576 if is_focused {
578 let font_size = self.font_size;
579 let lh = self.line_height;
580 let cursor_height = font_size * lh;
581
582 if let Some(sb) = final_sb {
583 let mut lx = 0.0;
584 let mut found = false;
585 for g in sb.glyphs() {
586 if g.cluster == cursor_index {
587 lx = g.x;
588 found = true;
589 break;
590 }
591 }
592
593 if !found && cursor_index == self.buffer.len() {
594 lx = sb.glyphs().last().map(|g: &ShapedGlyph| g.x + g.width).unwrap_or(0.0);
595 }
596
597 let cx = lx + self.x + self.padding.left + self.text_padding.left - scroll_x;
598 let cy = self.y + self.padding.top + self.text_padding.top;
599
600 let last_activity = *self.ui.interaction_state.get(&self.id).unwrap_or(&0.0);
602 let time_since_activity = self.ui.elapsed_time - last_activity;
603
604 let is_blink_visible = if time_since_activity < 0.5 {
605 true } else {
607 (self.ui.elapsed_time - last_activity).fract() < 0.5
610 };
611
612 if is_blink_visible {
613 self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
614 x: cx,
615 y: cy,
616 width: 2.0,
617 height: cursor_height,
618 color: Color::WHITE,
619 clip: [self.x, self.y, w_box, h_box],
620 }));
621 }
622 }
623 }
624
625 if self.scrollable && w_text_raw > w_view {
627 let is_dragging = self.ui.active_drag.map(|d| d.id == scroll_state_id).unwrap_or(false);
628
629 self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
630 x: thumb_x,
631 y: scroll_bar_y,
632 width: thumb_w,
633 height: scroll_bar_h,
634 color: if is_dragging { Color::rgba(1.0, 1.0, 1.0, 0.8) } else { Color::rgba(1.0, 1.0, 1.0, 0.4) },
635 clip: [self.x, self.y, w_box, h_box],
636 }));
637 }
638
639 self.ui.register_semantic(
641 SemanticNode::new(self.id, Role::TextInput, Rect::new(self.x, self.y, w_box, h_box))
642 .with_label(self.buffer.clone())
643 .with_focus(is_focused)
644 );
645
646 self.ui.record_layout(self.id, Rect::new(self.x, self.y, w_box, h_box));
648 self.ui.advance(w_box, h_box, start_draw);
649
650 if self.render_mode.is_some() {
651 self.ui.render_mode_stack.pop();
652 }
653
654 zenthra_core::Response {
655 clicked: self.ui.clicked && is_hovered,
656 hovered: is_hovered,
657 pressed: is_hovered && self.ui.mouse_down,
658 }
659 }
660}