pix_engine/gui/widgets/
field.rs1use crate::{gui::MOD_CTRL, ops::clamp_size, prelude::*};
40
41const TEXT_CURSOR: &str = "_";
42
43impl PixState {
44 pub fn text_field<L>(&mut self, label: L, value: &mut String) -> PixResult<bool>
63 where
64 L: AsRef<str>,
65 {
66 self.advanced_text_field(label, "", value, None)
67 }
68
69 pub fn advanced_text_field<L, H>(
93 &mut self,
94 label: L,
95 hint: H,
96 value: &mut String,
97 filter: Option<fn(char) -> bool>,
98 ) -> PixResult<bool>
99 where
100 L: AsRef<str>,
101 H: AsRef<str>,
102 {
103 let label = label.as_ref();
104 let hint = hint.as_ref();
105
106 let s = self;
107 let id = s.ui.get_id(&label);
108 let label = s.ui.get_label(label);
109 let pos = s.cursor_pos();
110 let spacing = s.theme.spacing;
111 let ipad = spacing.item_pad;
112
113 let width =
115 s.ui.next_width
116 .take()
117 .unwrap_or_else(|| s.ui_width().unwrap_or(100));
118 let (label_width, label_height) = s.text_size(label)?;
119 let [mut x, y] = pos.coords();
120 if !label.is_empty() {
121 x += label_width + ipad.x();
122 }
123 let input = rect![x, y, width, label_height + 2 * ipad.y()];
124
125 let hovered = s.focused() && s.ui.try_hover(id, &input);
127 let focused = s.focused() && s.ui.try_focus(id);
128 let disabled = s.ui.disabled;
129
130 s.push();
131 s.ui.push_cursor();
132
133 if !label.is_empty() {
135 s.set_cursor_pos([pos.x(), pos.y() + input.height() / 2 - label_height / 2]);
136 s.text(label)?;
137 }
138
139 s.rect_mode(RectMode::Corner);
141 if hovered {
142 s.frame_cursor(&Cursor::ibeam())?;
143 }
144 let [stroke, bg, fg] = s.widget_colors(id, ColorType::Background);
145 s.stroke(stroke);
146 s.fill(bg);
147 s.rect(input)?;
148
149 let clip = input.shrink(ipad);
151 let (text_width, text_height) = s.text_size(value)?;
152 let (cursor_width, _) = s.text_size(TEXT_CURSOR)?;
153 let width = text_width + cursor_width;
154 let (mut x, y) = (clip.x(), input.center().y() - text_height / 2);
155 if width > clip.width() {
156 x -= width - clip.width();
157 }
158
159 s.wrap(None);
160 s.set_cursor_pos([x, y]);
161 s.clip(clip)?;
162 s.stroke(None);
163 s.fill(fg);
164 if value.is_empty() {
165 s.ui.push_cursor();
167 s.disable(true);
168 s.text(hint)?;
169 if !disabled {
170 s.disable(false);
171 }
172 s.ui.pop_cursor();
173 if focused {
174 s.text(TEXT_CURSOR)?;
175 }
176 } else if focused {
177 s.text(format!("{value}{TEXT_CURSOR}"))?;
178 } else {
179 s.text(&value)?;
180 }
181
182 s.clip(None)?;
183 s.ui.pop_cursor();
184 s.pop();
185
186 let changed = focused && {
188 if let Some(Key::Return | Key::Escape) = s.ui.key_entered() {
189 s.ui.blur();
190 }
191 s.handle_text_events(value)?
192 };
193 if changed {
194 value.retain(|c| !c.is_control());
195 if let Some(filter) = filter {
196 value.retain(filter);
197 }
198 }
199 s.ui.handle_focus(id);
200 s.advance_cursor([input.right() - pos.x(), input.height()]);
201
202 Ok(changed)
203 }
204
205 pub fn text_area<L>(
224 &mut self,
225 label: L,
226 width: u32,
227 height: u32,
228 value: &mut String,
229 ) -> PixResult<bool>
230 where
231 L: AsRef<str>,
232 {
233 self.advanced_text_area(label, "", width, height, value, None)
234 }
235
236 pub fn advanced_text_area<L, H>(
262 &mut self,
263 label: L,
264 hint: H,
265 width: u32,
266 height: u32,
267 value: &mut String,
268 filter: Option<fn(char) -> bool>,
269 ) -> PixResult<bool>
270 where
271 L: AsRef<str>,
272 H: AsRef<str>,
273 {
274 let label = label.as_ref();
275 let hint = hint.as_ref();
276
277 let s = self;
278 let id = s.ui.get_id(&label);
279 let label = s.ui.get_label(label);
280 let pos = s.cursor_pos();
281 let spacing = s.theme.spacing;
282 let ipad = spacing.item_pad;
283
284 let (label_width, label_height) = s.text_size(label)?;
286 let [x, mut y] = pos.coords();
287 if !label.is_empty() {
288 y += label_height + 2 * ipad.y();
289 }
290 let input = rect![x, y, clamp_size(width), clamp_size(height)];
291
292 let hovered = s.focused() && s.ui.try_hover(id, &input);
294 let focused = s.focused() && s.ui.try_focus(id);
295 let disabled = s.ui.disabled;
296
297 s.push();
298 s.ui.push_cursor();
299
300 if !label.is_empty() {
302 s.set_cursor_pos([pos.x(), pos.y() + ipad.y()]);
303 s.text(label)?;
304 }
305
306 s.rect_mode(RectMode::Corner);
308 if hovered {
309 s.frame_cursor(&Cursor::ibeam())?;
310 }
311 let [stroke, bg, fg] = s.widget_colors(id, ColorType::Background);
312 s.stroke(stroke);
313 s.fill(bg);
314 s.rect(input)?;
315
316 let clip = input.shrink(ipad);
318 let scroll = s.ui.scroll(id);
319 s.wrap(clip.width() as u32);
320 let mut text_pos = input.top_left();
321 text_pos.offset(ipad - scroll);
322
323 s.set_cursor_pos(text_pos);
324 s.clip(clip)?;
325 s.stroke(None);
326 s.fill(fg);
327 let (_, text_height) = if value.is_empty() {
328 s.ui.push_cursor();
330 s.disable(true);
331 let size = s.text(hint)?;
332 if !disabled {
333 s.disable(false);
334 }
335 s.ui.pop_cursor();
336 if focused {
337 s.text(TEXT_CURSOR)?;
338 }
339 size
340 } else if focused {
341 s.text(format!("{value}{TEXT_CURSOR}"))?
342 } else {
343 s.text(&value)?
344 };
345
346 let mut text_height = clamp_size(text_height) + 2 * ipad.y();
348 let changed = focused && {
349 match s.ui.key_entered() {
350 Some(Key::Return) => {
351 value.push('\n');
352 true
353 }
354 Some(Key::Escape) => {
355 s.ui.blur();
356 false
357 }
358 _ => s.handle_text_events(value)?,
359 }
360 };
361
362 if changed {
363 value.retain(|c| c == '\n' || !c.is_control());
364 if let Some(filter) = filter {
365 value.retain(filter);
366 }
367 let (_, height) = s.text_size(&format!("{value}{TEXT_CURSOR}"))?;
368 text_height = height + 2 * ipad.y();
369
370 let mut scroll = s.ui.scroll(id);
372 if text_height < input.height() {
373 scroll.set_y(0);
374 } else {
375 scroll.set_y(text_height - input.height());
376 }
377 s.ui.set_scroll(id, scroll);
378 }
379
380 s.clip(None)?;
381 s.ui.pop_cursor();
382 s.pop();
383
384 s.ui.handle_focus(id);
385 let rect = s.scroll(id, input, 0, text_height)?;
387 s.advance_cursor([rect.width().max(label_width), rect.bottom() - pos.y()]);
388
389 Ok(changed)
390 }
391}
392
393impl PixState {
394 fn handle_text_events(&mut self, value: &mut String) -> PixResult<bool> {
396 let s = self;
397 let mut changed = false;
398 if let Some(key) = s.ui.key_entered() {
399 match key {
400 Key::Backspace if !value.is_empty() => {
401 if s.keymod_down(MOD_CTRL) {
402 value.clear();
403 } else if s.keymod_down(KeyMod::ALT) {
404 if value.chars().last().map(char::is_whitespace) == Some(true) {
407 value.pop();
408 }
409 if let Some(idx) = value.rfind(char::is_whitespace) {
410 value.truncate(idx + 1);
411 } else {
412 value.clear();
413 }
414 } else {
415 value.pop();
416 }
417 changed = true;
418 }
419 Key::X if s.keymod_down(MOD_CTRL) => {
420 s.set_clipboard_text(&value)?;
421 value.clear();
422 changed = true;
423 }
424 Key::C if s.keymod_down(MOD_CTRL) => {
425 s.set_clipboard_text(&value)?;
426 }
427 Key::V if s.keymod_down(MOD_CTRL) => {
428 value.push_str(&s.clipboard_text());
429 changed = true;
430 }
431 _ => (),
432 }
433 }
434 if let Some(text) = s.ui.keys.typed.take() {
435 value.push_str(&text);
436 changed = true;
437 }
438 Ok(changed)
439 }
440}