use crate::ui::{Ui, DrawCommand, OverlayRectDraw, ScrollDrag};
use crate::text::TextBuilder;
use zenthra_core::{Color, EdgeInsets, Id, Role, SemanticNode, Rect};
use zenthra_platform::event::PlatformEvent;
use zenthra_text::prelude::{TextOptions, CosmicFontProvider, Padding, ShapedGlyph};
use zenthra_text::traits::FontProvider;
pub struct InputBuilder<'u, 'a, 'b> {
ui: &'u mut Ui<'a>,
buffer: &'b mut String,
id: Id,
x: f32,
y: f32,
font_size: f32,
color: Color,
bg: Option<Color>,
text_bg: Option<Color>,
highlight: Option<Color>,
padding: EdgeInsets,
text_padding: EdgeInsets,
line_height: f32,
width: Option<f32>,
min_width: f32,
scrollable: bool,
fill_x: bool,
text_bg_fill_x: bool,
radius: [f32; 4],
border_color: Option<Color>,
border_width: f32,
opacity: f32,
shadow_color: Option<Color>,
shadow_offset: [f32; 2],
shadow_blur: f32,
shadow_opacity: f32,
render_mode: Option<zenthra_core::RenderMode>,
}
impl<'u, 'a, 'b> InputBuilder<'u, 'a, 'b> {
pub fn new(ui: &'u mut Ui<'a>, buffer: &'b mut String) -> Self {
let id = ui.id();
let x = ui.cursor_x;
let y = ui.cursor_y;
Self {
ui,
buffer,
id,
x,
y,
font_size: 18.0,
color: Color::WHITE,
bg: Some(Color::rgb(0.2, 0.2, 0.2)),
text_bg: None,
padding: EdgeInsets::ZERO,
text_padding: EdgeInsets::ZERO,
line_height: 1.2,
width: None,
min_width: 200.0,
scrollable: true,
fill_x: false,
text_bg_fill_x: false,
highlight: None,
radius: [4.0; 4],
border_color: None,
border_width: 0.0,
opacity: 1.0,
shadow_color: None,
shadow_offset: [0.0; 2],
shadow_blur: 0.0,
shadow_opacity: 1.0,
render_mode: None,
}
}
pub fn id(mut self, id: impl std::hash::Hash) -> Self {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
use std::hash::Hasher;
id.hash(&mut hasher);
self.id = Id::from_u64(hasher.finish());
self
}
pub fn size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn bg(mut self, bg: Color) -> Self {
self.bg = Some(bg);
self
}
pub fn text_bg(mut self, bg: Color) -> Self {
self.text_bg = Some(bg);
self
}
pub fn highlight(mut self, color: Color) -> Self {
self.highlight = Some(color);
self
}
pub fn padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
self.padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
self
}
pub fn padding_x(mut self, x: f32) -> Self {
self.padding.left = x;
self.padding.right = x;
self
}
pub fn padding_y(mut self, y: f32) -> Self {
self.padding.top = y;
self.padding.bottom = y;
self
}
pub fn padding_top(mut self, t: f32) -> Self {
self.padding.top = t;
self
}
pub fn padding_bottom(mut self, b: f32) -> Self {
self.padding.bottom = b;
self
}
pub fn padding_left(mut self, l: f32) -> Self {
self.padding.left = l;
self
}
pub fn padding_right(mut self, r: f32) -> Self {
self.padding.right = r;
self
}
pub fn text_padding(mut self, t: f32, r: f32, b: f32, l: f32) -> Self {
self.text_padding = EdgeInsets { top: t, right: r, bottom: b, left: l };
self
}
pub fn text_padding_x(mut self, x: f32) -> Self {
self.text_padding.left = x;
self.text_padding.right = x;
self
}
pub fn text_padding_y(mut self, y: f32) -> Self {
self.text_padding.top = y;
self.text_padding.bottom = y;
self
}
pub fn text_padding_top(mut self, t: f32) -> Self {
self.text_padding.top = t;
self
}
pub fn text_padding_bottom(mut self, b: f32) -> Self {
self.text_padding.bottom = b;
self
}
pub fn text_padding_left(mut self, l: f32) -> Self {
self.text_padding.left = l;
self
}
pub fn text_padding_right(mut self, r: f32) -> Self {
self.text_padding.right = r;
self
}
pub fn line_height(mut self, lh: f32) -> Self {
self.line_height = lh;
self
}
pub fn width(mut self, w: f32) -> Self {
self.width = Some(w);
self
}
pub fn min_width(mut self, w: f32) -> Self {
self.min_width = w;
self
}
pub fn scrollable(mut self, s: bool) -> Self {
self.scrollable = s;
self
}
pub fn fill_x(mut self) -> Self {
self.fill_x = true;
self
}
pub fn radius(mut self, tl: f32, tr: f32, br: f32, bl: f32) -> Self {
self.radius = [tl, tr, br, bl];
self
}
pub fn radius_all(mut self, r: f32) -> Self {
self.radius = [r, r, r, r];
self
}
pub fn radius_top(mut self, r: f32) -> Self {
self.radius[0] = r;
self.radius[1] = r;
self
}
pub fn radius_bottom(mut self, r: f32) -> Self {
self.radius[2] = r;
self.radius[3] = r;
self
}
pub fn radius_top_left(mut self, r: f32) -> Self {
self.radius[0] = r;
self
}
pub fn radius_top_right(mut self, r: f32) -> Self {
self.radius[1] = r;
self
}
pub fn radius_bottom_right(mut self, r: f32) -> Self {
self.radius[2] = r;
self
}
pub fn radius_bottom_left(mut self, r: f32) -> Self {
self.radius[3] = r;
self
}
pub fn radius_left(mut self, r: f32) -> Self {
self.radius[0] = r;
self.radius[3] = r;
self
}
pub fn radius_right(mut self, r: f32) -> Self {
self.radius[1] = r;
self.radius[2] = r;
self
}
pub fn border(mut self, color: Color, width: f32) -> Self {
self.border_color = Some(color);
self.border_width = width;
self
}
pub fn shadow(mut self, color: Color, ox: f32, oy: f32, blur: f32) -> Self {
self.shadow_color = Some(color);
self.shadow_offset = [ox, oy];
self.shadow_blur = blur;
self
}
pub fn opacity(mut self, o: f32) -> Self {
self.opacity = o;
self
}
pub fn text_bg_fill_x(mut self, enabled: bool) -> Self {
self.text_bg_fill_x = enabled;
self
}
pub fn render_mode(mut self, mode: zenthra_core::RenderMode) -> Self {
self.render_mode = Some(mode);
self
}
pub fn on_change<F>(self, mut f: F) -> Self
where
F: FnMut(String) + 'a,
{
self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
if let crate::ui::WidgetEvent::Change(crate::ui::EventValue::String(val)) = event {
f(val.clone());
}
});
self
}
pub fn on_hover<F>(self, mut f: F) -> Self
where
F: FnMut(bool) + 'a,
{
self.ui.add_listener(self.id, crate::ui::EventPhase::Bubble, move |_, event| {
if let crate::ui::WidgetEvent::Hover(hovered) = event {
f(*hovered);
}
});
self
}
pub fn on_event<F>(self, phase: crate::ui::EventPhase, f: F) -> Self
where
F: FnMut(&mut crate::ui::EventContext, &crate::ui::WidgetEvent) + 'a,
{
self.ui.add_listener(self.id, phase, f);
self
}
pub fn show(self) -> zenthra_core::Response {
if let Some(mode) = self.render_mode {
self.ui.render_mode_stack.push(mode);
}
let is_focused = self.ui.focused_id == Some(self.id);
let (mut w_text_raw, h_content, mut shaped_buffer) = if let Some(fs) = self.ui.font_system.as_ref() {
let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
let t_padding = Padding::from(self.text_padding);
adapter.set_layout_size(1000000.0, 10000.0);
let options = TextOptions::new()
.font_size(self.font_size)
.line_height(self.line_height)
.wrap(zenthra_text::prelude::TextWrap::None);
let buffer = adapter.shape(&self.buffer, &options);
let (cw, _ch) = buffer.content_size();
let m = adapter.metrics(&options);
(cw + t_padding.horizontal(), m.line_height() + t_padding.vertical(), Some(buffer))
} else {
(self.min_width, 20.0, None)
};
let max_available_w = (self.ui.max_x - self.x).max(self.min_width);
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) };
let h_box = h_content + self.padding.vertical();
let mut w_view = w_box - self.padding.horizontal();
let mut cursor_index = *self.ui.cursor_state.get(&self.id).unwrap_or(&self.buffer.len());
cursor_index = cursor_index.min(self.buffer.len());
if !self.buffer.is_char_boundary(cursor_index) {
cursor_index = self.buffer.len();
}
let (actual_x, actual_y) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
(rect.origin.x + self.ui.offset_x, rect.origin.y + self.ui.offset_y)
} else {
(self.x + self.ui.offset_x, self.y + self.ui.offset_y)
};
let is_hovered = self.ui.is_hovered(self.id, actual_x, actual_y, w_box, h_box);
if is_hovered {
self.ui.cursor_icon = crate::text::CursorIcon::Text;
}
self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Hover(is_hovered));
let mut needs_auto_scroll = false;
let mut changed = false;
if is_focused || is_hovered || self.ui.active_drag.is_some() {
let events = std::mem::take(&mut self.ui.input_events);
for event in &events {
match event {
PlatformEvent::CharTyped(c) if is_focused => {
if *c != '\r' && *c != '\n' {
self.buffer.insert(cursor_index, *c);
cursor_index += c.len_utf8();
needs_auto_scroll = true;
changed = true;
self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
}
}
PlatformEvent::KeyDown { key } if is_focused => {
match key {
winit::keyboard::KeyCode::Backspace => {
if cursor_index > 0 {
let mut chars = self.buffer[..cursor_index].chars();
if let Some(c) = chars.next_back() {
let len = c.len_utf8();
self.buffer.remove(cursor_index - len);
cursor_index -= len;
needs_auto_scroll = true;
changed = true;
self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
}
}
}
winit::keyboard::KeyCode::ArrowLeft => {
if cursor_index > 0 {
let mut chars = self.buffer[..cursor_index].chars();
if let Some(c) = chars.next_back() {
cursor_index -= c.len_utf8();
self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
self.ui.needs_redraw = true;
needs_auto_scroll = true;
}
}
}
winit::keyboard::KeyCode::ArrowRight => {
if cursor_index < self.buffer.len() {
let mut chars = self.buffer[cursor_index..].chars();
if let Some(c) = chars.next() {
cursor_index += c.len_utf8();
self.ui.interaction_state.insert(self.id, self.ui.elapsed_time);
self.ui.needs_redraw = true;
needs_auto_scroll = true;
}
}
}
_ => {}
}
}
PlatformEvent::MouseWheel { delta_x, delta_y } if is_hovered => {
let _id = self.id;
let (mut sx, sy) = *self.ui.scroll_state.get(&self.id).unwrap_or(&(0.0, 0.0));
let effect = if delta_x.abs() < 0.001 { *delta_y } else { *delta_x };
sx -= effect * 30.0;
self.ui.scroll_state.insert(self.id, (sx, sy));
}
_ => {}
}
}
if self.ui.clicked && is_hovered {
self.ui.focused_id = Some(self.id);
needs_auto_scroll = true;
self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Click);
}
self.ui.input_events = events;
self.ui.cursor_state.insert(self.id, cursor_index);
if changed {
self.ui.dispatch_event(self.id, crate::ui::WidgetEvent::Change(crate::ui::EventValue::String(self.buffer.clone())));
}
}
if changed {
if let Some(fs) = self.ui.font_system.as_ref() {
let mut adapter = CosmicFontProvider::new_with_system(fs.clone());
let t_padding = Padding::from(self.text_padding);
adapter.set_layout_size(1000000.0, 10000.0);
let options = TextOptions::new()
.font_size(self.font_size)
.line_height(self.line_height)
.wrap(zenthra_text::prelude::TextWrap::None);
let buffer = adapter.shape(&self.buffer, &options);
let (cw, _ch) = buffer.content_size();
w_text_raw = cw + t_padding.horizontal();
shaped_buffer = Some(buffer);
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) };
w_view = w_box - self.padding.horizontal();
}
}
let scroll_state_id = self.id;
let (mut scroll_x, scroll_y) = *self.ui.scroll_state.get(&scroll_state_id).unwrap_or(&(0.0, 0.0));
let max_scroll = (w_text_raw - w_view).max(0.0f32);
let scroll_bar_h = 4.0;
let scroll_bar_y = self.y + h_box - scroll_bar_h - 2.0;
let thumb_w = if w_text_raw > w_view { (w_view / w_text_raw) * w_view } else { w_view };
let thumb_w = thumb_w.max(20.0);
let scroll_percent = if max_scroll > 0.0 { scroll_x / max_scroll } else { 0.0 };
let track_width = (w_view - thumb_w).max(0.0);
let thumb_x = self.x + self.padding.left + scroll_percent * track_width;
let actual_thumb_x = actual_x + self.padding.left + scroll_percent * track_width;
let actual_scroll_bar_y = actual_y + h_box - scroll_bar_h - 2.0;
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);
if let Some(drag) = self.ui.active_drag {
if drag.id == scroll_state_id {
let delta_mouse = self.ui.mouse_x - drag.start_mouse;
if track_width > 0.0 {
let scroll_delta = (delta_mouse / track_width) * max_scroll;
scroll_x = (drag.start_scroll + scroll_delta).clamp(0.0, max_scroll);
}
}
}
if self.ui.clicked && is_over_thumb {
self.ui.active_drag = Some(ScrollDrag {
id: scroll_state_id,
start_mouse: self.ui.mouse_x,
start_scroll: scroll_x,
});
}
let is_dragging_this = self.ui.active_drag.map(|d| d.id == scroll_state_id).unwrap_or(false);
if is_focused && !is_dragging_this && needs_auto_scroll {
if let Some(sb) = &shaped_buffer {
let mut clx = 0.0;
let mut found = false;
for g in sb.glyphs() {
if g.cluster == cursor_index {
clx = g.x;
found = true;
break;
}
}
if !found && cursor_index == self.buffer.len() {
clx = sb.glyphs().last().map(|g: &ShapedGlyph| g.x + g.width).unwrap_or(0.0);
}
let cursor_x_v = clx + self.text_padding.left;
if cursor_x_v > scroll_x + w_view - 40.0 {
scroll_x = cursor_x_v - w_view + 40.0;
} else if cursor_x_v < scroll_x + 10.0 {
scroll_x = (cursor_x_v - 10.0).max(0.0);
}
}
}
scroll_x = scroll_x.clamp(0.0, max_scroll);
self.ui.scroll_state.insert(scroll_state_id, (scroll_x, scroll_y));
let start_draw = self.ui.draws.len();
if let Some(bg) = self.bg {
use crate::ui::RectDraw;
use zenthra_render::RectInstance;
self.ui.draws.push(DrawCommand::Rect(RectDraw {
instance: RectInstance {
pos: [self.x, self.y],
size: [w_box, h_box],
color: bg.to_array(),
radius: [
self.radius[3],
self.radius[2],
self.radius[1],
self.radius[0],
],
border_width: if is_focused { self.border_width.max(1.0) } else { self.border_width },
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() },
shadow_color: self.shadow_color.map(|mut c| { c.a *= self.shadow_opacity; c.to_array() }).unwrap_or([0.0; 4]),
shadow_offset: self.shadow_offset,
shadow_blur: self.shadow_blur,
clip_rect: [0.0, 0.0, 9999.0, 9999.0],
grayscale: 0.0,
brightness: 1.0,
opacity: self.opacity,
..Default::default()
}
}));
}
let mut text_builder = TextBuilder::new(self.ui, &self.buffer)
.size(self.font_size)
.line_height(self.line_height)
.color(self.color)
.fill_x(self.fill_x)
.padding(self.text_padding.top, self.text_padding.right, self.text_padding.bottom, self.text_padding.left)
.wrap(zenthra_text::prelude::TextWrap::None)
.max_width(1000000.0)
.pos(self.x + self.padding.left - scroll_x, self.y + self.padding.top)
.clip_rect(self.x, self.y, w_box, h_box);
if self.text_bg_fill_x {
text_builder = text_builder.min_width(w_view);
}
if let Some(tbg) = self.text_bg {
text_builder = text_builder.bg(tbg).fill_x(false);
}
if let Some(h) = self.highlight {
text_builder = text_builder.highlight(h);
}
let (_, _, final_sb, _) = text_builder.draw_and_measure();
if is_focused {
let font_size = self.font_size;
let lh = self.line_height;
let cursor_height = font_size * lh;
if let Some(sb) = final_sb {
let mut lx = 0.0;
let mut found = false;
for g in sb.glyphs() {
if g.cluster == cursor_index {
lx = g.x;
found = true;
break;
}
}
if !found && cursor_index == self.buffer.len() {
lx = sb.glyphs().last().map(|g: &ShapedGlyph| g.x + g.width).unwrap_or(0.0);
}
let cx = lx + self.x + self.padding.left + self.text_padding.left - scroll_x;
let cy = self.y + self.padding.top + self.text_padding.top;
let last_activity = *self.ui.interaction_state.get(&self.id).unwrap_or(&0.0);
let time_since_activity = self.ui.elapsed_time - last_activity;
let is_blink_visible = if time_since_activity < 0.5 {
true } else {
(self.ui.elapsed_time - last_activity).fract() < 0.5
};
if is_blink_visible {
self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
x: cx,
y: cy,
width: 2.0,
height: cursor_height,
color: Color::WHITE,
clip: [self.x, self.y, w_box, h_box],
}));
}
}
}
if self.scrollable && w_text_raw > w_view {
let is_dragging = self.ui.active_drag.map(|d| d.id == scroll_state_id).unwrap_or(false);
self.ui.draws.push(DrawCommand::OverlayRect(OverlayRectDraw {
x: thumb_x,
y: scroll_bar_y,
width: thumb_w,
height: scroll_bar_h,
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) },
clip: [self.x, self.y, w_box, h_box],
}));
}
self.ui.register_semantic(
SemanticNode::new(self.id, Role::TextInput, Rect::new(self.x, self.y, w_box, h_box))
.with_label(self.buffer.clone())
.with_focus(is_focused)
);
self.ui.record_layout(self.id, Rect::new(self.x, self.y, w_box, h_box));
self.ui.advance(w_box, h_box, start_draw);
if self.render_mode.is_some() {
self.ui.render_mode_stack.pop();
}
zenthra_core::Response {
clicked: self.ui.clicked && is_hovered,
hovered: is_hovered,
pressed: is_hovered && self.ui.mouse_down,
}
}
}