1use super::*;
54use crate::draw_context::DrawCtx;
55use std::fmt::Write;
56use std::rc::Rc;
57
58pub struct WidgetCtx<'a> {
60 id: Id,
61 rect: Recti,
62 draw: DrawCtx<'a>,
63 focus: &'a mut Option<Id>,
64 updated_focus: &'a mut bool,
65 in_hover_root: bool,
66 input: Option<Rc<InputSnapshot>>,
67}
68
69impl<'a> WidgetCtx<'a> {
70 pub(crate) fn new(
72 id: Id,
73 rect: Recti,
74 commands: &'a mut Vec<Command>,
75 clip_stack: &'a mut Vec<Recti>,
76 style: &'a Style,
77 atlas: &'a AtlasHandle,
78 focus: &'a mut Option<Id>,
79 updated_focus: &'a mut bool,
80 in_hover_root: bool,
81 input: Option<Rc<InputSnapshot>>,
82 ) -> Self {
83 Self {
84 id,
85 rect,
86 draw: DrawCtx::new(commands, clip_stack, style, atlas),
87 focus,
88 updated_focus,
89 in_hover_root,
90 input,
91 }
92 }
93
94 pub fn id(&self) -> Id { self.id }
96
97 pub fn rect(&self) -> Recti { self.rect }
99
100 pub fn input(&self) -> Option<&InputSnapshot> { self.input.as_deref() }
102
103 pub fn set_focus(&mut self) {
105 *self.focus = Some(self.id);
106 *self.updated_focus = true;
107 }
108
109 pub fn clear_focus(&mut self) {
111 *self.focus = None;
112 *self.updated_focus = true;
113 }
114
115 pub fn push_clip_rect(&mut self, rect: Recti) { self.draw.push_clip_rect(rect); }
117
118 pub fn pop_clip_rect(&mut self) { self.draw.pop_clip_rect(); }
120
121 pub fn with_clip<F: FnOnce(&mut Self)>(&mut self, rect: Recti, f: F) {
123 self.push_clip_rect(rect);
124 f(self);
125 self.pop_clip_rect();
126 }
127
128 fn current_clip_rect(&self) -> Recti { self.draw.current_clip_rect() }
129
130 pub(crate) fn style(&self) -> &Style { self.draw.style() }
131
132 pub(crate) fn atlas(&self) -> &AtlasHandle { self.draw.atlas() }
133
134 pub fn push_command(&mut self, cmd: Command) { self.draw.push_command(cmd); }
136
137 pub fn set_clip(&mut self, rect: Recti) { self.draw.set_clip(rect); }
139
140 pub fn check_clip(&self, r: Recti) -> Clip { self.draw.check_clip(r) }
142
143 pub(crate) fn draw_rect(&mut self, rect: Recti, color: Color) { self.draw.draw_rect(rect, color); }
144
145 pub fn draw_box(&mut self, r: Recti, color: Color) { self.draw.draw_box(r, color); }
147
148 pub(crate) fn draw_text(&mut self, font: FontId, text: &str, pos: Vec2i, color: Color) {
149 self.draw.draw_text(font, text, pos, color);
150 }
151
152 pub(crate) fn draw_icon(&mut self, id: IconId, rect: Recti, color: Color) { self.draw.draw_icon(id, rect, color); }
153
154 pub(crate) fn push_image(&mut self, image: Image, rect: Recti, color: Color) { self.draw.push_image(image, rect, color); }
155
156 pub(crate) fn draw_slot_with_function(&mut self, id: SlotId, rect: Recti, color: Color, f: Rc<dyn Fn(usize, usize) -> Color4b>) {
157 self.draw.draw_slot_with_function(id, rect, color, f);
158 }
159
160 pub(crate) fn draw_frame(&mut self, rect: Recti, colorid: ControlColor) { self.draw.draw_frame(rect, colorid); }
161
162 pub(crate) fn draw_widget_frame(&mut self, control: &ControlState, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
163 self.draw.draw_widget_frame(control.focused, control.hovered, rect, colorid, opt);
164 }
165
166 pub(crate) fn draw_control_text(&mut self, text: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
167 self.draw.draw_control_text(text, rect, colorid, opt);
168 }
169
170 pub(crate) fn mouse_over(&self, rect: Recti) -> bool {
171 let input = match self.input.as_ref() {
172 Some(input) => input,
173 None => return false,
174 };
175 if !self.in_hover_root {
176 return false;
177 }
178 let clip_rect = self.current_clip_rect();
179 rect.contains(&input.mouse_pos) && clip_rect.contains(&input.mouse_pos)
180 }
181}
182
183#[derive(Clone, Copy)]
184pub enum NodeStateValue {
186 Expanded,
188 Closed,
190}
191
192impl NodeStateValue {
193 pub fn is_expanded(&self) -> bool {
195 match self {
196 Self::Expanded => true,
197 _ => false,
198 }
199 }
200
201 pub fn is_closed(&self) -> bool {
203 match self {
204 Self::Closed => true,
205 _ => false,
206 }
207 }
208}
209
210#[derive(Clone)]
211pub struct Node {
213 pub label: String,
215 pub state: NodeStateValue,
217 pub opt: WidgetOption,
219 pub bopt: WidgetBehaviourOption,
221}
222
223impl Node {
224 pub fn new(label: impl Into<String>, state: NodeStateValue) -> Self {
226 Self { label: label.into(), state, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
227 }
228
229 pub fn with_opt(label: impl Into<String>, state: NodeStateValue, opt: WidgetOption) -> Self {
231 Self { label: label.into(), state, opt, bopt: WidgetBehaviourOption::NONE }
232 }
233
234 pub fn is_expanded(&self) -> bool { self.state.is_expanded() }
236
237 pub fn is_closed(&self) -> bool { self.state.is_closed() }
239}
240
241impl Widget for Node {
242 fn widget_opt(&self) -> &WidgetOption { &self.opt }
243 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
244 fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
245 if control.clicked {
246 self.state = if self.state.is_expanded() { NodeStateValue::Closed } else { NodeStateValue::Expanded };
247 ResourceState::CHANGE
248 } else {
249 ResourceState::NONE
250 }
251 }
252}
253
254fn widget_fill_color(control: &ControlState, base: ControlColor, fill: WidgetFillOption) -> Option<ControlColor> {
255 if control.focused && fill.fill_click() {
256 let mut color = base;
257 color.focus();
258 Some(color)
259 } else if control.hovered && fill.fill_hover() {
260 let mut color = base;
261 color.hover();
262 Some(color)
263 } else if fill.fill_normal() {
264 Some(base)
265 } else {
266 None
267 }
268}
269
270#[derive(Clone)]
271pub enum ButtonContent {
273 Text {
275 label: String,
277 icon: Option<IconId>,
279 },
280 Image {
282 label: String,
284 image: Option<Image>,
286 },
287 Slot {
289 label: String,
291 slot: SlotId,
293 paint: Rc<dyn Fn(usize, usize) -> Color4b>,
295 },
296}
297
298#[derive(Clone)]
299pub struct Button {
301 pub content: ButtonContent,
303 pub opt: WidgetOption,
305 pub bopt: WidgetBehaviourOption,
307 pub fill: WidgetFillOption,
309}
310
311impl Button {
312 pub fn new(label: impl Into<String>) -> Self {
314 Self {
315 content: ButtonContent::Text { label: label.into(), icon: None },
316 opt: WidgetOption::NONE,
317 bopt: WidgetBehaviourOption::NONE,
318 fill: WidgetFillOption::ALL,
319 }
320 }
321
322 pub fn with_opt(label: impl Into<String>, opt: WidgetOption) -> Self {
324 Self {
325 content: ButtonContent::Text { label: label.into(), icon: None },
326 opt,
327 bopt: WidgetBehaviourOption::NONE,
328 fill: WidgetFillOption::ALL,
329 }
330 }
331
332 pub fn with_image(label: impl Into<String>, image: Option<Image>, opt: WidgetOption, fill: WidgetFillOption) -> Self {
334 Self {
335 content: ButtonContent::Image { label: label.into(), image },
336 opt,
337 bopt: WidgetBehaviourOption::NONE,
338 fill,
339 }
340 }
341
342 pub fn with_slot(
344 label: impl Into<String>,
345 slot: SlotId,
346 paint: Rc<dyn Fn(usize, usize) -> Color4b>,
347 opt: WidgetOption,
348 fill: WidgetFillOption,
349 ) -> Self {
350 Self {
351 content: ButtonContent::Slot { label: label.into(), slot, paint },
352 opt,
353 bopt: WidgetBehaviourOption::NONE,
354 fill,
355 }
356 }
357}
358
359impl Widget for Button {
360 fn widget_opt(&self) -> &WidgetOption { &self.opt }
361 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
362 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
363 let mut res = ResourceState::NONE;
364 if control.clicked {
365 res |= ResourceState::SUBMIT;
366 }
367 let rect = ctx.rect();
368 if !self.opt.has_no_frame() {
369 if let Some(colorid) = widget_fill_color(control, ControlColor::Button, self.fill) {
370 ctx.draw_frame(rect, colorid);
371 }
372 }
373 match &self.content {
374 ButtonContent::Text { label, icon } => {
375 if !label.is_empty() {
376 ctx.draw_control_text(label, rect, ControlColor::Text, self.opt);
377 }
378 if let Some(icon) = icon {
379 let color = ctx.style().colors[ControlColor::Text as usize];
380 ctx.draw_icon(*icon, rect, color);
381 }
382 }
383 ButtonContent::Image { label, image } => {
384 if !label.is_empty() {
385 ctx.draw_control_text(label, rect, ControlColor::Text, self.opt);
386 }
387 if let Some(image) = *image {
388 let color = ctx.style().colors[ControlColor::Text as usize];
389 ctx.push_image(image, rect, color);
390 }
391 }
392 ButtonContent::Slot { label, slot, paint } => {
393 if !label.is_empty() {
394 ctx.draw_control_text(label, rect, ControlColor::Text, self.opt);
395 }
396 let color = ctx.style().colors[ControlColor::Text as usize];
397 ctx.draw_slot_with_function(*slot, rect, color, paint.clone());
398 }
399 }
400 res
401 }
402}
403
404#[derive(Clone)]
405pub struct ListItem {
407 pub label: String,
409 pub icon: Option<IconId>,
411 pub opt: WidgetOption,
413 pub bopt: WidgetBehaviourOption,
415}
416
417impl ListItem {
418 pub fn new(label: impl Into<String>) -> Self {
420 Self { label: label.into(), icon: None, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
421 }
422
423 pub fn with_opt(label: impl Into<String>, opt: WidgetOption) -> Self {
425 Self { label: label.into(), icon: None, opt, bopt: WidgetBehaviourOption::NONE }
426 }
427
428 pub fn with_icon(label: impl Into<String>, icon: IconId) -> Self {
430 Self { label: label.into(), icon: Some(icon), opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
431 }
432
433 pub fn with_icon_opt(label: impl Into<String>, icon: IconId, opt: WidgetOption) -> Self {
435 Self { label: label.into(), icon: Some(icon), opt, bopt: WidgetBehaviourOption::NONE }
436 }
437}
438
439impl Widget for ListItem {
440 fn widget_opt(&self) -> &WidgetOption { &self.opt }
441 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
442 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
443 let mut res = ResourceState::NONE;
444 let bounds = ctx.rect();
445 if control.clicked {
446 res |= ResourceState::SUBMIT;
447 }
448
449 if control.focused || control.hovered {
450 let mut color = ControlColor::Button;
451 if control.focused {
452 color.focus();
453 } else {
454 color.hover();
455 }
456 let fill = ctx.style().colors[color as usize];
457 ctx.draw_rect(bounds, fill);
458 }
459
460 let mut text_rect = bounds;
461 if let Some(icon) = self.icon {
462 let padding = ctx.style().padding.max(0);
463 let icon_size = ctx.atlas().get_icon_size(icon);
464 let icon_x = bounds.x + padding;
465 let icon_y = bounds.y + ((bounds.height - icon_size.height) / 2).max(0);
466 let icon_rect = rect(icon_x, icon_y, icon_size.width, icon_size.height);
467 let consumed = icon_size.width + padding * 2;
468 text_rect.x += consumed;
469 text_rect.width = (text_rect.width - consumed).max(0);
470 let color = ctx.style().colors[ControlColor::Text as usize];
471 ctx.draw_icon(icon, icon_rect, color);
472 }
473
474 if !self.label.is_empty() {
475 ctx.draw_control_text(&self.label, text_rect, ControlColor::Text, self.opt);
476 }
477 res
478 }
479}
480
481#[derive(Clone)]
482pub struct ListBox {
484 pub label: String,
486 pub image: Option<Image>,
488 pub opt: WidgetOption,
490 pub bopt: WidgetBehaviourOption,
492}
493
494impl ListBox {
495 pub fn new(label: impl Into<String>, image: Option<Image>) -> Self {
497 Self { label: label.into(), image, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
498 }
499
500 pub fn with_opt(label: impl Into<String>, image: Option<Image>, opt: WidgetOption) -> Self {
502 Self { label: label.into(), image, opt, bopt: WidgetBehaviourOption::NONE }
503 }
504}
505
506impl Widget for ListBox {
507 fn widget_opt(&self) -> &WidgetOption { &self.opt }
508 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
509 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
510 let mut res = ResourceState::NONE;
511 let rect = ctx.rect();
512 if control.clicked {
513 res |= ResourceState::SUBMIT;
514 }
515 if !self.opt.has_no_frame() {
516 if let Some(colorid) = widget_fill_color(control, ControlColor::Button, WidgetFillOption::HOVER | WidgetFillOption::CLICK) {
517 ctx.draw_frame(rect, colorid);
518 }
519 }
520 if !self.label.is_empty() {
521 ctx.draw_control_text(&self.label, rect, ControlColor::Text, self.opt);
522 }
523 if let Some(image) = self.image {
524 let color = ctx.style().colors[ControlColor::Text as usize];
525 ctx.push_image(image, rect, color);
526 }
527 res
528 }
529}
530
531#[derive(Clone)]
532pub struct Checkbox {
534 pub label: String,
536 pub value: bool,
538 pub opt: WidgetOption,
540 pub bopt: WidgetBehaviourOption,
542}
543
544impl Checkbox {
545 pub fn new(label: impl Into<String>, value: bool) -> Self {
547 Self { label: label.into(), value, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
548 }
549
550 pub fn with_opt(label: impl Into<String>, value: bool, opt: WidgetOption) -> Self {
552 Self { label: label.into(), value, opt, bopt: WidgetBehaviourOption::NONE }
553 }
554}
555
556impl Widget for Checkbox {
557 fn widget_opt(&self) -> &WidgetOption { &self.opt }
558 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
559 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
560 let mut res = ResourceState::NONE;
561 let bounds = ctx.rect();
562 let box_rect = rect(bounds.x, bounds.y, bounds.height, bounds.height);
563 if control.clicked {
564 res |= ResourceState::CHANGE;
565 self.value = !self.value;
566 }
567 ctx.draw_widget_frame(control, box_rect, ControlColor::Base, self.opt);
568 if self.value {
569 let color = ctx.style().colors[ControlColor::Text as usize];
570 ctx.draw_icon(CHECK_ICON, box_rect, color);
571 }
572 let text_rect = rect(bounds.x + box_rect.width, bounds.y, bounds.width - box_rect.width, bounds.height);
573 if !self.label.is_empty() {
574 ctx.draw_control_text(&self.label, text_rect, ControlColor::Text, self.opt);
575 }
576 res
577 }
578}
579
580#[derive(Clone)]
581pub struct Textbox {
583 pub buf: String,
585 pub cursor: usize,
587 pub opt: WidgetOption,
589 pub bopt: WidgetBehaviourOption,
591}
592
593impl Textbox {
594 pub fn new(buf: impl Into<String>) -> Self {
596 let buf = buf.into();
597 let cursor = buf.len();
598 Self { buf, cursor, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
599 }
600
601 pub fn with_opt(buf: impl Into<String>, opt: WidgetOption) -> Self {
603 let buf = buf.into();
604 let cursor = buf.len();
605 Self { buf, cursor, opt, bopt: WidgetBehaviourOption::NONE }
606 }
607}
608
609fn textbox_handle(
610 ctx: &mut WidgetCtx<'_>,
611 control: &ControlState,
612 buf: &mut String,
613 cursor: &mut usize,
614 opt: WidgetOption,
615) -> ResourceState {
616 let mut res = ResourceState::NONE;
617 let r = ctx.rect();
618 if !control.focused {
619 *cursor = buf.len();
620 }
621 let mut cursor_pos = (*cursor).min(buf.len());
622
623 let (mouse_pressed, mouse_pos, should_submit) = {
624 let default_input = InputSnapshot::default();
625 let input = ctx.input().unwrap_or(&default_input);
626 let mut should_submit = false;
627
628 if control.focused {
629 if !input.text_input.is_empty() {
630 let insert_at = cursor_pos.min(buf.len());
631 buf.insert_str(insert_at, input.text_input.as_str());
632 cursor_pos = insert_at + input.text_input.len();
633 res |= ResourceState::CHANGE;
634 }
635
636 if input.key_pressed.is_backspace() && cursor_pos > 0 && !buf.is_empty() {
637 let mut new_cursor = cursor_pos.min(buf.len());
638 new_cursor -= 1;
639 while new_cursor > 0 && !buf.is_char_boundary(new_cursor) {
640 new_cursor -= 1;
641 }
642 buf.replace_range(new_cursor..cursor_pos, "");
643 cursor_pos = new_cursor;
644 res |= ResourceState::CHANGE;
645 }
646
647 if input.key_code_pressed.is_left() && cursor_pos > 0 {
648 let mut new_cursor = cursor_pos - 1;
649 while new_cursor > 0 && !buf.is_char_boundary(new_cursor) {
650 new_cursor -= 1;
651 }
652 cursor_pos = new_cursor;
653 }
654
655 if input.key_code_pressed.is_right() && cursor_pos < buf.len() {
656 let mut new_cursor = cursor_pos + 1;
657 while new_cursor < buf.len() && !buf.is_char_boundary(new_cursor) {
658 new_cursor += 1;
659 }
660 cursor_pos = new_cursor;
661 }
662
663 if input.key_pressed.is_return() {
664 should_submit = true;
665 }
666 }
667
668 (input.mouse_pressed, input.mouse_pos, should_submit)
669 };
670
671 if should_submit {
672 ctx.clear_focus();
673 res |= ResourceState::SUBMIT;
674 }
675
676 ctx.draw_widget_frame(control, r, ControlColor::Base, opt);
677
678 let font = ctx.style().font;
679 let line_height = ctx.atlas().get_font_height(font) as i32;
680 let baseline = ctx.atlas().get_font_baseline(font);
681 let descent = (line_height - baseline).max(0);
682
683 let mut texty = r.y + r.height / 2 - line_height / 2;
684 if texty < r.y {
685 texty = r.y;
686 }
687 let max_texty = (r.y + r.height - line_height).max(r.y);
688 if texty > max_texty {
689 texty = max_texty;
690 }
691 let baseline_y = texty + line_height - descent;
692
693 let text_metrics = ctx.atlas().get_text_size(font, buf.as_str());
694 let padding = ctx.style().padding;
695 let ofx = r.width - padding - text_metrics.width - 1;
696 let textx = r.x + if ofx < padding { ofx } else { padding };
697
698 if control.focused && mouse_pressed.is_left() && ctx.mouse_over(r) {
699 let click_x = mouse_pos.x - textx;
700 if click_x <= 0 {
701 cursor_pos = 0;
702 } else {
703 let mut last_width = 0;
704 let mut new_cursor = buf.len();
705 for (idx, ch) in buf.char_indices() {
706 let next = idx + ch.len_utf8();
707 let width = ctx.atlas().get_text_size(font, &buf[..next]).width;
708 if click_x < width {
709 if click_x < (last_width + width) / 2 {
710 new_cursor = idx;
711 } else {
712 new_cursor = next;
713 }
714 break;
715 }
716 last_width = width;
717 }
718 cursor_pos = new_cursor.min(buf.len());
719 }
720 }
721
722 cursor_pos = cursor_pos.min(buf.len());
723 *cursor = cursor_pos;
724
725 let caret_offset = if cursor_pos == 0 {
726 0
727 } else {
728 ctx.atlas().get_text_size(font, &buf[..cursor_pos]).width
729 };
730
731 if control.focused {
732 let color = ctx.style().colors[ControlColor::Text as usize];
733 ctx.push_clip_rect(r);
734 ctx.draw_text(font, buf.as_str(), vec2(textx, texty), color);
735 let caret_top = (baseline_y - baseline + 2).max(r.y).min(r.y + r.height);
736 let caret_bottom = (baseline_y + descent - 2).max(r.y).min(r.y + r.height);
737 let caret_height = (caret_bottom - caret_top).max(1);
738 ctx.draw_rect(rect(textx + caret_offset, caret_top, 1, caret_height), color);
739 ctx.pop_clip_rect();
740 } else {
741 ctx.draw_control_text(buf.as_str(), r, ControlColor::Text, opt);
742 }
743 res
744}
745
746impl Widget for Textbox {
747 fn widget_opt(&self) -> &WidgetOption { &self.opt }
748 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
749 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
750 textbox_handle(ctx, control, &mut self.buf, &mut self.cursor, self.opt)
751 }
752}
753
754#[derive(Clone)]
755pub struct Slider {
757 pub value: Real,
759 pub low: Real,
761 pub high: Real,
763 pub step: Real,
765 pub precision: usize,
767 pub opt: WidgetOption,
769 pub bopt: WidgetBehaviourOption,
771 pub edit: NumberEditState,
773}
774
775impl Slider {
776 pub fn new(value: Real, low: Real, high: Real) -> Self {
778 Self {
779 value,
780 low,
781 high,
782 step: 0.0,
783 precision: 0,
784 opt: WidgetOption::NONE,
785 bopt: WidgetBehaviourOption::GRAB_SCROLL,
786 edit: NumberEditState::default(),
787 }
788 }
789
790 pub fn with_opt(value: Real, low: Real, high: Real, step: Real, precision: usize, opt: WidgetOption) -> Self {
792 Self {
793 value,
794 low,
795 high,
796 step,
797 precision,
798 opt,
799 bopt: WidgetBehaviourOption::GRAB_SCROLL,
800 edit: NumberEditState::default(),
801 }
802 }
803}
804
805fn number_textbox_handle(
806 ctx: &mut WidgetCtx<'_>,
807 control: &ControlState,
808 edit: &mut NumberEditState,
809 precision: usize,
810 value: &mut Real,
811) -> ResourceState {
812 let shift_click = {
813 let default_input = InputSnapshot::default();
814 let input = ctx.input().unwrap_or(&default_input);
815 input.mouse_pressed.is_left() && input.key_mods.is_shift() && control.hovered
816 };
817
818 if shift_click {
819 edit.editing = true;
820 edit.buf.clear();
821 let _ = write!(edit.buf, "{:.*}", precision, value);
822 edit.cursor = edit.buf.len();
823 }
824
825 if edit.editing {
826 let res = textbox_handle(ctx, control, &mut edit.buf, &mut edit.cursor, WidgetOption::NONE);
827 if res.is_submitted() || !control.focused {
828 if let Ok(v) = edit.buf.parse::<f32>() {
829 *value = v as Real;
830 }
831 edit.editing = false;
832 edit.cursor = 0;
833 } else {
834 return ResourceState::ACTIVE;
835 }
836 }
837 ResourceState::NONE
838}
839
840impl Widget for Slider {
841 fn widget_opt(&self) -> &WidgetOption { &self.opt }
842 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
843 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
844 let mut res = ResourceState::NONE;
845 let base = ctx.rect();
846 let last = self.value;
847 let mut v = last;
848 if !number_textbox_handle(ctx, control, &mut self.edit, self.precision, &mut v).is_none() {
849 return res;
850 }
851 if let Some(delta) = control.scroll_delta {
852 let range = self.high - self.low;
853 if range != 0.0 {
854 let wheel = if delta.y != 0 { delta.y.signum() } else { delta.x.signum() };
855 if wheel != 0 {
856 let step_amount = if self.step != 0. { self.step } else { range / 100.0 };
857 v += wheel as Real * step_amount;
858 if self.step != 0. {
859 v = (v + self.step / 2 as Real) / self.step * self.step;
860 }
861 }
862 }
863 }
864 let default_input = InputSnapshot::default();
865 let input = ctx.input().unwrap_or(&default_input);
866 let range = self.high - self.low;
867 if control.focused && (!input.mouse_down.is_none() || input.mouse_pressed.is_left()) && base.width > 0 && range != 0.0 {
868 v = self.low + (input.mouse_pos.x - base.x) as Real * range / base.width as Real;
869 if self.step != 0. {
870 v = (v + self.step / 2 as Real) / self.step * self.step;
871 }
872 }
873 if range == 0.0 {
874 v = self.low;
875 }
876 v = if self.high < (if self.low > v { self.low } else { v }) {
877 self.high
878 } else if self.low > v {
879 self.low
880 } else {
881 v
882 };
883 self.value = v;
884 if last != v {
885 res |= ResourceState::CHANGE;
886 }
887 ctx.draw_widget_frame(control, base, ControlColor::Base, self.opt);
888 let w = ctx.style().thumb_size;
889 let available = (base.width - w).max(0);
890 let x = if range != 0.0 && available > 0 {
891 ((v - self.low) * available as Real / range) as i32
892 } else {
893 0
894 };
895 let thumb = rect(base.x + x, base.y, w, base.height);
896 ctx.draw_widget_frame(control, thumb, ControlColor::Button, self.opt);
897 self.edit.buf.clear();
898 let _ = write!(self.edit.buf, "{:.*}", self.precision, self.value);
899 ctx.draw_control_text(self.edit.buf.as_str(), base, ControlColor::Text, self.opt);
900 res
901 }
902}
903
904#[derive(Clone)]
905pub struct Number {
907 pub value: Real,
909 pub step: Real,
911 pub precision: usize,
913 pub opt: WidgetOption,
915 pub bopt: WidgetBehaviourOption,
917 pub edit: NumberEditState,
919}
920
921#[derive(Clone, Default)]
922pub struct NumberEditState {
924 pub editing: bool,
926 pub buf: String,
928 pub cursor: usize,
930}
931
932impl Number {
933 pub fn new(value: Real, step: Real, precision: usize) -> Self {
935 Self {
936 value,
937 step,
938 precision,
939 opt: WidgetOption::NONE,
940 bopt: WidgetBehaviourOption::NONE,
941 edit: NumberEditState::default(),
942 }
943 }
944
945 pub fn with_opt(value: Real, step: Real, precision: usize, opt: WidgetOption) -> Self {
947 Self {
948 value,
949 step,
950 precision,
951 opt,
952 bopt: WidgetBehaviourOption::NONE,
953 edit: NumberEditState::default(),
954 }
955 }
956}
957
958impl Widget for Number {
959 fn widget_opt(&self) -> &WidgetOption { &self.opt }
960 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
961 fn handle(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
962 let mut res = ResourceState::NONE;
963 let base = ctx.rect();
964 let last = self.value;
965 if !number_textbox_handle(ctx, control, &mut self.edit, self.precision, &mut self.value).is_none() {
966 return res;
967 }
968 let default_input = InputSnapshot::default();
969 let input = ctx.input().unwrap_or(&default_input);
970 if control.focused && input.mouse_down.is_left() {
971 self.value += input.mouse_delta.x as Real * self.step;
972 }
973 if self.value != last {
974 res |= ResourceState::CHANGE;
975 }
976 ctx.draw_widget_frame(control, base, ControlColor::Base, self.opt);
977 self.edit.buf.clear();
978 let _ = write!(self.edit.buf, "{:.*}", self.precision, self.value);
979 ctx.draw_control_text(self.edit.buf.as_str(), base, ControlColor::Text, self.opt);
980 res
981 }
982}
983
984#[derive(Clone)]
985pub struct Custom {
987 pub name: String,
989 pub opt: WidgetOption,
991 pub bopt: WidgetBehaviourOption,
993}
994
995impl Custom {
996 pub fn new(name: impl Into<String>) -> Self {
998 Self {
999 name: name.into(),
1000 opt: WidgetOption::NONE,
1001 bopt: WidgetBehaviourOption::NONE,
1002 }
1003 }
1004
1005 pub fn with_opt(name: impl Into<String>, opt: WidgetOption, bopt: WidgetBehaviourOption) -> Self {
1007 Self { name: name.into(), opt, bopt }
1008 }
1009}
1010
1011impl Widget for Custom {
1012 fn widget_opt(&self) -> &WidgetOption { &self.opt }
1013 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
1014 fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState { ResourceState::NONE }
1015}
1016
1017#[derive(Clone)]
1018pub struct Internal {
1020 pub tag: &'static str,
1022 pub opt: WidgetOption,
1024 pub bopt: WidgetBehaviourOption,
1026}
1027
1028impl Internal {
1029 pub fn new(tag: &'static str) -> Self {
1031 Self {
1032 tag,
1033 opt: WidgetOption::NONE,
1034 bopt: WidgetBehaviourOption::NONE,
1035 }
1036 }
1037}
1038
1039impl Widget for Internal {
1040 fn widget_opt(&self) -> &WidgetOption { &self.opt }
1041 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
1042 fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState { ResourceState::NONE }
1043}
1044
1045#[derive(Clone)]
1047pub struct Combo {
1048 pub popup: WindowHandle,
1050 pub selected: usize,
1052 pub open: bool,
1054 pub opt: WidgetOption,
1056 pub bopt: WidgetBehaviourOption,
1058}
1059
1060impl Combo {
1061 pub fn new(popup: WindowHandle) -> Self {
1063 Self { popup, selected: 0, open: false, opt: WidgetOption::NONE, bopt: WidgetBehaviourOption::NONE }
1064 }
1065
1066 pub fn with_opt(popup: WindowHandle, opt: WidgetOption, bopt: WidgetBehaviourOption) -> Self {
1068 Self { popup, selected: 0, open: false, opt, bopt }
1069 }
1070}
1071
1072impl Widget for Combo {
1073 fn widget_opt(&self) -> &WidgetOption { &self.opt }
1074 fn behaviour_opt(&self) -> &WidgetBehaviourOption { &self.bopt }
1075 fn handle(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState { ResourceState::NONE }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use crate::{AtlasSource, FontEntry, SourceFormat};
1082
1083 const ICON_NAMES: [&str; 6] = ["white", "close", "expand", "collapse", "check", "expand_down"];
1084
1085 fn make_test_atlas() -> AtlasHandle {
1086 let pixels: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
1087 let icons: Vec<(&str, Recti)> = ICON_NAMES
1088 .iter()
1089 .map(|name| (*name, Recti::new(0, 0, 1, 1)))
1090 .collect();
1091 let entries = vec![
1092 (
1093 '_',
1094 CharEntry {
1095 offset: Vec2i::new(0, 0),
1096 advance: Vec2i::new(8, 0),
1097 rect: Recti::new(0, 0, 1, 1),
1098 },
1099 ),
1100 (
1101 'a',
1102 CharEntry {
1103 offset: Vec2i::new(0, 0),
1104 advance: Vec2i::new(8, 0),
1105 rect: Recti::new(0, 0, 1, 1),
1106 },
1107 ),
1108 (
1109 'b',
1110 CharEntry {
1111 offset: Vec2i::new(0, 0),
1112 advance: Vec2i::new(8, 0),
1113 rect: Recti::new(0, 0, 1, 1),
1114 },
1115 ),
1116 ];
1117 let fonts = vec![(
1118 "default",
1119 FontEntry {
1120 line_size: 10,
1121 baseline: 8,
1122 font_size: 10,
1123 entries: &entries,
1124 },
1125 )];
1126 let source = AtlasSource {
1127 width: 1,
1128 height: 1,
1129 pixels: &pixels,
1130 icons: &icons,
1131 fonts: &fonts,
1132 format: SourceFormat::Raw,
1133 slots: &[],
1134 };
1135 AtlasHandle::from(&source)
1136 }
1137
1138 #[test]
1139 fn slider_zero_range_keeps_value() {
1140 let atlas = make_test_atlas();
1141 let style = Style::default();
1142 let mut commands = Vec::new();
1143 let mut clip_stack = Vec::new();
1144 let mut focus = None;
1145 let mut updated_focus = false;
1146
1147 let mut slider = Slider::new(5.0, 5.0, 5.0);
1148 let id = slider.get_id();
1149 let rect = rect(0, 0, 100, 20);
1150 let text_input = String::new();
1151 let input = Rc::new(InputSnapshot {
1152 mouse_pos: vec2(50, 10),
1153 mouse_delta: vec2(5, 0),
1154 mouse_down: MouseButton::LEFT,
1155 mouse_pressed: MouseButton::LEFT,
1156 text_input,
1157 ..Default::default()
1158 });
1159 let mut ctx = WidgetCtx::new(
1160 id,
1161 rect,
1162 &mut commands,
1163 &mut clip_stack,
1164 &style,
1165 &atlas,
1166 &mut focus,
1167 &mut updated_focus,
1168 true,
1169 Some(input),
1170 );
1171 let control = ControlState {
1172 hovered: true,
1173 focused: true,
1174 clicked: false,
1175 active: true,
1176 scroll_delta: None,
1177 };
1178
1179 let res = slider.handle(&mut ctx, &control);
1180
1181 assert!(res.is_none());
1182 assert!(slider.value.is_finite());
1183 assert_eq!(slider.value, 5.0);
1184 }
1185}