1use glam::Vec2;
2use ori_graphics::{Color, Quad, Rect, TextAlign, TextSection};
3use ori_macro::Build;
4
5use crate::{
6 BoxConstraints, Context, DrawContext, Event, EventContext, EventSignal, Key, KeyboardEvent,
7 LayoutContext, PointerEvent, Scope, SharedSignal, Signal, Style, View,
8};
9
10#[derive(Clone, Debug, Build)]
11pub struct TextInput {
12 #[prop]
13 placeholder: String,
14 #[bind]
15 text: SharedSignal<String>,
16 #[event]
17 on_input: Option<EventSignal<KeyboardEvent>>,
18 #[event]
19 on_submit: Option<EventSignal<String>>,
20}
21
22impl Default for TextInput {
23 fn default() -> Self {
24 Self {
25 placeholder: String::from("Enter text..."),
26 text: SharedSignal::new(String::new()),
27 on_input: None,
28 on_submit: None,
29 }
30 }
31}
32
33impl TextInput {
34 pub fn new(text: impl Into<String>) -> Self {
35 Self {
36 text: SharedSignal::new(text.into()),
37 ..Default::default()
38 }
39 }
40
41 pub fn with_text(self, text: impl Into<String>) -> Self {
42 self.text.set(text.into());
43 self
44 }
45
46 pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
47 self.placeholder = placeholder.into();
48 self
49 }
50
51 pub fn bind_text<'a>(self, cx: Scope<'a>, text: &'a Signal<String>) -> Self {
52 let signal = cx.alloc(self.text.clone());
53 cx.bind(text, &signal);
54 self
55 }
56
57 fn display_text(&self) -> String {
58 if self.text.get().is_empty() {
59 self.placeholder.clone()
60 } else {
61 self.text.cloned()
62 }
63 }
64
65 fn display_section(&self, state: &TextInputState, cx: &mut impl Context) -> TextSection {
66 let color = if self.text.get().is_empty() {
67 cx.style("placeholder-color")
68 } else {
69 cx.style("color")
70 };
71
72 TextSection {
73 rect: cx.rect().translate(Vec2::new(state.padding, 0.0)),
74 scale: state.font_size,
75 h_align: TextAlign::Start,
76 v_align: TextAlign::Center,
77 wrap: false,
78 text: self.display_text(),
79 font: cx.style("font"),
80 color,
81 }
82 }
83
84 fn section(&self, state: &TextInputState, cx: &mut impl Context) -> TextSection {
85 TextSection {
86 rect: cx.rect().translate(Vec2::new(state.padding, 0.0)),
87 scale: state.font_size,
88 h_align: TextAlign::Start,
89 v_align: TextAlign::Center,
90 wrap: false,
91 text: self.text.cloned(),
92 font: cx.style("font"),
93 ..Default::default()
94 }
95 }
96
97 fn handle_pointer_event(
98 &self,
99 state: &mut TextInputState,
100 cx: &mut EventContext,
101 event: &PointerEvent,
102 ) {
103 if event.is_press() && cx.hovered() {
104 let section = self.section(state, cx);
105 let hit = cx.renderer.hit_text(§ion, event.position);
106
107 if let Some(hit) = hit {
108 if hit.delta.x > 0.0 {
109 state.cursor = Some(hit.index + 1);
110 cx.focus();
111 } else {
112 state.cursor = Some(hit.index);
113 cx.focus()
114 }
115 } else {
116 state.cursor = Some(0);
117 cx.focus();
118 }
119 } else if event.is_press() && !cx.hovered() {
120 state.cursor = None;
121 cx.unfocus();
122 }
123 }
124
125 fn handle_keyboard_input(
126 &self,
127 state: &mut TextInputState,
128 cx: &mut EventContext,
129 event: &KeyboardEvent,
130 ) {
131 if event.is_press() {
132 if let Some(on_input) = &self.on_input {
133 on_input.emit(event.clone());
134 }
135
136 self.handle_key(state, cx, event.key.unwrap());
137 }
138
139 if let Some(c) = event.text {
140 if !c.is_control() {
141 if let Some(on_input) = &self.on_input {
142 on_input.emit(event.clone());
143 }
144
145 let mut text = self.text.modify();
146 if let Some(cursor) = state.cursor {
147 if cursor < text.len() {
148 text.insert(cursor, c);
149 } else {
150 text.push(c);
151 }
152
153 let new_cursor = cursor + c.len_utf8();
154 state.cursor = Some(new_cursor);
155 }
156
157 cx.request_redraw();
158 }
159 }
160 }
161
162 fn handle_key(&self, state: &mut TextInputState, cx: &mut EventContext, key: Key) {
163 match key {
164 Key::Right => {
165 state.cursor_right(&self.text.get());
166 cx.request_redraw();
167 }
168 Key::Left => {
169 state.cursor_left();
170 cx.request_redraw();
171 }
172 Key::Backspace => {
173 if let Some(cursor) = state.cursor {
174 if cursor > 0 {
175 let mut text = self.text.modify();
176 text.remove(cursor - 1);
177 state.cursor_left();
178 cx.request_redraw();
179 }
180 }
181 }
182 Key::Escape => {
183 state.cursor = None;
184 cx.unfocus();
185 }
186 Key::Enter => {
187 if let Some(on_submit) = &self.on_submit {
188 on_submit.emit(self.text.cloned());
189 state.cursor = None;
190 cx.unfocus();
191 }
192 }
193 _ => {}
194 }
195 }
196}
197
198#[derive(Clone, Debug, Default)]
199pub struct TextInputState {
200 font_size: f32,
201 blink: f32,
202 padding: f32,
203 cursor: Option<usize>,
204}
205
206impl TextInputState {
207 fn reset_blink(&mut self) {
208 self.blink = 0.0;
209 }
210
211 fn cursor_right(&mut self, text: &str) {
212 if let Some(cursor) = self.cursor {
213 if cursor < text.len() {
214 self.cursor = Some(cursor + 1);
215 }
216 }
217
218 self.reset_blink();
219 }
220
221 fn cursor_left(&mut self) {
222 if let Some(cursor) = self.cursor {
223 if cursor > 0 {
224 self.cursor = Some(cursor - 1);
225 }
226 }
227
228 self.reset_blink();
229 }
230}
231
232impl View for TextInput {
233 type State = TextInputState;
234
235 fn build(&self) -> Self::State {
236 TextInputState::default()
237 }
238
239 fn style(&self) -> Style {
240 Style::new("text-input")
241 }
242
243 fn event(&self, state: &mut Self::State, cx: &mut EventContext, event: &Event) {
244 if let Some(pointer_event) = event.get::<PointerEvent>() {
245 self.handle_pointer_event(state, cx, pointer_event);
246 }
247
248 if let Some(keyboard_event) = event.get::<KeyboardEvent>() {
249 self.handle_keyboard_input(state, cx, keyboard_event);
250 }
251 }
252
253 fn layout(&self, state: &mut Self::State, cx: &mut LayoutContext, bc: BoxConstraints) -> Vec2 {
254 let font_size = cx.style_range("font-size", 0.0..bc.max.y);
255
256 state.font_size = font_size;
257
258 let padding = cx.style_range("padding", 0.0..bc.max.min_element() / 2.0);
259 state.padding = padding;
260
261 let min_width = cx.style_range_group("width", "min-width", bc.width());
262 let max_width = cx.style_range_group("width", "max-width", bc.width());
263
264 let mut min_height = cx.style_range_group("height", "min-height", bc.height());
265 let max_height = cx.style_range_group("height", "max-height", bc.height());
266
267 min_height = min_height.max(font_size + padding * 2.0);
268
269 let min_size = bc.constrain(Vec2::new(min_width, min_height));
270 let max_size = bc.constrain(Vec2::new(max_width, max_height));
271
272 let section = self.display_section(state, cx);
273 let mut size = cx.messure_text(§ion).unwrap_or_default().size();
274 size += padding * 2.0;
275 size.clamp(min_size, max_size)
276 }
277
278 fn draw(&self, state: &mut Self::State, cx: &mut DrawContext) {
279 cx.draw_quad();
280
281 let section = self.display_section(state, cx);
282 cx.draw(section);
283
284 if let Some(cursor) = state.cursor {
285 state.blink += cx.state.delta() * 10.0;
286 cx.request_redraw();
287
288 let section = TextSection {
289 text: self.text.get()[..cursor].into(),
290 ..self.section(state, cx)
291 };
292
293 let bounds = cx.renderer.messure_text(§ion).unwrap_or_default();
294 let cursor = f32::max(bounds.max.x, cx.rect().min.x + state.padding);
295
296 let quad = Quad {
297 rect: Rect::min_size(
298 Vec2::new(cursor, cx.rect().min.y + state.padding),
299 Vec2::new(1.0, cx.rect().height() - state.padding * 2.0),
300 )
301 .round(),
302 background: cx.style::<Color>("color") * state.blink.cos(),
303 ..Quad::default()
304 };
305
306 cx.draw(quad);
307 }
308 }
309}