1use core::ops::Range;
4
5use epaint::text::CharIndex;
6
7use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
8
9#[derive(Clone, Default)]
13pub struct FullOutput {
14 pub platform_output: PlatformOutput,
16
17 pub textures_delta: epaint::textures::TexturesDelta,
24
25 pub shapes: Vec<epaint::ClippedShape>,
29
30 pub pixels_per_point: f32,
34
35 pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
40}
41
42impl FullOutput {
43 pub fn append(&mut self, newer: Self) {
45 use std::collections::btree_map::Entry;
46
47 let Self {
48 platform_output,
49 textures_delta,
50 shapes,
51 pixels_per_point,
52 viewport_output,
53 } = newer;
54
55 self.platform_output.append(platform_output);
56 self.textures_delta.append(textures_delta);
57 self.shapes = shapes; self.pixels_per_point = pixels_per_point; for (id, new_viewport) in viewport_output {
61 match self.viewport_output.entry(id) {
62 Entry::Vacant(entry) => {
63 entry.insert(new_viewport);
64 }
65 Entry::Occupied(mut entry) => {
66 entry.get_mut().append(new_viewport);
67 }
68 }
69 }
70 }
71
72 pub fn drop_without_applying_deltas(mut self) {
75 self.textures_delta.clear();
76 }
77}
78
79#[derive(Clone, Default)]
84pub struct LogicOutput {
85 pub platform_output: PlatformOutput,
87
88 pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
93}
94
95#[derive(Copy, Clone, Debug, PartialEq, Eq)]
99#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
100pub struct IMEOutput {
101 pub purpose: crate::IMEPurpose,
103
104 pub rect: crate::Rect,
106
107 pub cursor_rect: crate::Rect,
111
112 pub should_interrupt_composition: bool,
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
120#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
121pub enum OutputCommand {
122 CopyText(String),
126
127 CopyImage(crate::ColorImage),
129
130 OpenUrl(OpenUrl),
132}
133
134#[derive(Default, Clone, PartialEq)]
140#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
141pub struct PlatformOutput {
142 pub commands: Vec<OutputCommand>,
144
145 pub cursor_icon: CursorIcon,
147
148 #[cfg_attr(feature = "serde", serde(skip))]
156 pub cursor_image: Option<CustomCursorImage>,
157
158 pub events: Vec<OutputEvent>,
160
161 pub mutable_text_under_cursor: bool,
164
165 pub ime: Option<IMEOutput>,
172
173 pub accesskit_update: Option<accesskit::TreeUpdate>,
177
178 pub num_completed_passes: usize,
185
186 #[cfg_attr(feature = "serde", serde(skip))]
192 pub request_discard_reasons: Vec<RepaintCause>,
193}
194
195impl PlatformOutput {
196 pub fn events_description(&self) -> String {
198 if let Some(event) = self.events.iter().next_back() {
200 match event {
201 OutputEvent::Clicked(widget_info)
202 | OutputEvent::DoubleClicked(widget_info)
203 | OutputEvent::TripleClicked(widget_info)
204 | OutputEvent::FocusGained(widget_info)
205 | OutputEvent::TextSelectionChanged(widget_info)
206 | OutputEvent::ValueChanged(widget_info) => {
207 return widget_info.description();
208 }
209 }
210 }
211 Default::default()
212 }
213
214 pub fn append(&mut self, newer: Self) {
216 let Self {
217 mut commands,
218 cursor_icon,
219 cursor_image,
220 mut events,
221 mutable_text_under_cursor,
222 ime,
223 accesskit_update,
224 num_completed_passes,
225 mut request_discard_reasons,
226 } = newer;
227
228 self.commands.append(&mut commands);
229 self.cursor_icon = cursor_icon;
230 self.cursor_image = cursor_image;
231 self.events.append(&mut events);
232 self.mutable_text_under_cursor = mutable_text_under_cursor;
233 self.ime = ime.or(self.ime);
234 self.num_completed_passes += num_completed_passes;
235 self.request_discard_reasons
236 .append(&mut request_discard_reasons);
237
238 self.accesskit_update = accesskit_update;
240 }
241
242 pub fn take(&mut self) -> Self {
245 let taken = core::mem::take(self);
246 self.cursor_icon = taken.cursor_icon; self.cursor_image = taken.cursor_image.clone(); taken
249 }
250
251 pub fn requested_discard(&self) -> bool {
253 !self.request_discard_reasons.is_empty()
254 }
255}
256
257#[derive(Clone, Debug, PartialEq, Eq)]
261#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
262pub struct OpenUrl {
263 pub url: String,
264
265 pub new_tab: bool,
269}
270
271impl OpenUrl {
272 #[expect(clippy::needless_pass_by_value)]
273 pub fn same_tab(url: impl ToString) -> Self {
274 Self {
275 url: url.to_string(),
276 new_tab: false,
277 }
278 }
279
280 #[expect(clippy::needless_pass_by_value)]
281 pub fn new_tab(url: impl ToString) -> Self {
282 Self {
283 url: url.to_string(),
284 new_tab: true,
285 }
286 }
287}
288
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
296pub enum UserAttentionType {
297 Critical,
299
300 Informational,
302
303 Reset,
305}
306
307#[derive(Clone, PartialEq, Eq)]
324pub struct CustomCursorImage {
325 pub rgba: std::sync::Arc<[u8]>,
326 pub size: [u16; 2],
327 pub hotspot: [u16; 2],
328}
329
330impl core::fmt::Debug for CustomCursorImage {
331 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
332 f.debug_struct("CustomCursorImage")
333 .field("size", &self.size)
334 .field("hotspot", &self.hotspot)
335 .field("rgba_len", &self.rgba.len())
336 .finish()
337 }
338}
339
340#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
346#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
347pub enum CursorIcon {
348 #[default]
350 Default,
351
352 None,
354
355 ContextMenu,
359
360 Help,
362
363 PointingHand,
365
366 Progress,
368
369 Wait,
371
372 Cell,
376
377 Crosshair,
379
380 Text,
382
383 VerticalText,
385
386 Alias,
390
391 Copy,
393
394 Move,
396
397 NoDrop,
399
400 NotAllowed,
402
403 Grab,
405
406 Grabbing,
408
409 AllScroll,
412
413 ResizeHorizontal,
417
418 ResizeNeSw,
420
421 ResizeNwSe,
423
424 ResizeVertical,
426
427 ResizeEast,
431
432 ResizeSouthEast,
434
435 ResizeSouth,
437
438 ResizeSouthWest,
440
441 ResizeWest,
443
444 ResizeNorthWest,
446
447 ResizeNorth,
449
450 ResizeNorthEast,
452
453 ResizeColumn,
456
457 ResizeRow,
459
460 ZoomIn,
464
465 ZoomOut,
467}
468
469impl CursorIcon {
470 pub const ALL: [Self; 35] = [
471 Self::Default,
472 Self::None,
473 Self::ContextMenu,
474 Self::Help,
475 Self::PointingHand,
476 Self::Progress,
477 Self::Wait,
478 Self::Cell,
479 Self::Crosshair,
480 Self::Text,
481 Self::VerticalText,
482 Self::Alias,
483 Self::Copy,
484 Self::Move,
485 Self::NoDrop,
486 Self::NotAllowed,
487 Self::Grab,
488 Self::Grabbing,
489 Self::AllScroll,
490 Self::ResizeHorizontal,
491 Self::ResizeNeSw,
492 Self::ResizeNwSe,
493 Self::ResizeVertical,
494 Self::ResizeEast,
495 Self::ResizeSouthEast,
496 Self::ResizeSouth,
497 Self::ResizeSouthWest,
498 Self::ResizeWest,
499 Self::ResizeNorthWest,
500 Self::ResizeNorth,
501 Self::ResizeNorthEast,
502 Self::ResizeColumn,
503 Self::ResizeRow,
504 Self::ZoomIn,
505 Self::ZoomOut,
506 ];
507}
508
509#[derive(Clone, PartialEq)]
513#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
514pub enum OutputEvent {
515 Clicked(WidgetInfo),
517
518 DoubleClicked(WidgetInfo),
520
521 TripleClicked(WidgetInfo),
523
524 FocusGained(WidgetInfo),
526
527 TextSelectionChanged(WidgetInfo),
529
530 ValueChanged(WidgetInfo),
532}
533
534impl OutputEvent {
535 pub fn widget_info(&self) -> &WidgetInfo {
536 match self {
537 Self::Clicked(info)
538 | Self::DoubleClicked(info)
539 | Self::TripleClicked(info)
540 | Self::FocusGained(info)
541 | Self::TextSelectionChanged(info)
542 | Self::ValueChanged(info) => info,
543 }
544 }
545}
546
547impl core::fmt::Debug for OutputEvent {
548 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
549 match self {
550 Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
551 Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
552 Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
553 Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
554 Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
555 Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
556 }
557 }
558}
559
560#[derive(Clone, PartialEq)]
562#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
563pub struct WidgetInfo {
564 pub typ: WidgetType,
566
567 pub enabled: bool,
569
570 pub label: Option<String>,
572
573 pub current_text_value: Option<String>,
575
576 pub prev_text_value: Option<String>,
578
579 pub selected: Option<bool>,
581
582 pub value: Option<f64>,
584
585 pub text_selection: Option<Range<CharIndex>>,
589
590 pub hint_text: Option<String>,
592}
593
594impl core::fmt::Debug for WidgetInfo {
595 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
596 let Self {
597 typ,
598 enabled,
599 label,
600 current_text_value: text_value,
601 prev_text_value,
602 selected,
603 value,
604 text_selection,
605 hint_text,
606 } = self;
607
608 let mut s = f.debug_struct("WidgetInfo");
609
610 s.field("typ", typ);
611
612 if !enabled {
613 s.field("enabled", enabled);
614 }
615
616 if let Some(label) = label {
617 s.field("label", label);
618 }
619 if let Some(text_value) = text_value {
620 s.field("text_value", text_value);
621 }
622 if let Some(prev_text_value) = prev_text_value {
623 s.field("prev_text_value", prev_text_value);
624 }
625 if let Some(selected) = selected {
626 s.field("selected", selected);
627 }
628 if let Some(value) = value {
629 s.field("value", value);
630 }
631 if let Some(text_selection) = text_selection {
632 s.field("text_selection", text_selection);
633 }
634 if let Some(hint_text) = hint_text {
635 s.field("hint_text", hint_text);
636 }
637
638 s.finish()
639 }
640}
641
642impl WidgetInfo {
643 pub fn new(typ: WidgetType) -> Self {
644 Self {
645 typ,
646 enabled: true,
647 label: None,
648 current_text_value: None,
649 prev_text_value: None,
650 selected: None,
651 value: None,
652 text_selection: None,
653 hint_text: None,
654 }
655 }
656
657 #[expect(clippy::needless_pass_by_value)]
658 pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
659 Self {
660 enabled,
661 label: Some(label.to_string()),
662 ..Self::new(typ)
663 }
664 }
665
666 #[expect(clippy::needless_pass_by_value)]
668 pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
669 Self {
670 enabled,
671 label: Some(label.to_string()),
672 selected: Some(selected),
673 ..Self::new(typ)
674 }
675 }
676
677 pub fn drag_value(enabled: bool, value: f64) -> Self {
678 Self {
679 enabled,
680 value: Some(value),
681 ..Self::new(WidgetType::DragValue)
682 }
683 }
684
685 #[expect(clippy::needless_pass_by_value)]
686 pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
687 let label = label.to_string();
688 Self {
689 enabled,
690 label: if label.is_empty() { None } else { Some(label) },
691 value: Some(value),
692 ..Self::new(WidgetType::Slider)
693 }
694 }
695
696 #[expect(clippy::needless_pass_by_value)]
697 pub fn text_edit(
698 enabled: bool,
699 prev_text_value: impl ToString,
700 text_value: impl ToString,
701 hint_text: impl ToString,
702 ) -> Self {
703 let text_value = text_value.to_string();
704 let prev_text_value = prev_text_value.to_string();
705 let hint_text = hint_text.to_string();
706 let prev_text_value = if text_value == prev_text_value {
707 None
708 } else {
709 Some(prev_text_value)
710 };
711 Self {
712 enabled,
713 current_text_value: Some(text_value),
714 prev_text_value,
715 hint_text: Some(hint_text),
716 ..Self::new(WidgetType::TextEdit)
717 }
718 }
719
720 #[expect(clippy::needless_pass_by_value)]
721 pub fn text_selection_changed(
722 enabled: bool,
723 text_selection: Range<CharIndex>,
724 current_text_value: impl ToString,
725 ) -> Self {
726 Self {
727 enabled,
728 text_selection: Some(text_selection),
729 current_text_value: Some(current_text_value.to_string()),
730 ..Self::new(WidgetType::TextEdit)
731 }
732 }
733
734 pub fn description(&self) -> String {
736 let Self {
737 typ,
738 enabled,
739 label,
740 current_text_value: text_value,
741 prev_text_value: _,
742 selected,
743 value,
744 text_selection: _,
745 hint_text: _,
746 } = self;
747
748 let widget_type = match typ {
750 WidgetType::Link => "link",
751 WidgetType::TextEdit => "text edit",
752 WidgetType::Button => "button",
753 WidgetType::Checkbox => "checkbox",
754 WidgetType::RadioButton => "radio",
755 WidgetType::RadioGroup => "radio group",
756 WidgetType::SelectableLabel => "selectable",
757 WidgetType::ComboBox => "combo",
758 WidgetType::Slider => "slider",
759 WidgetType::DragValue => "drag value",
760 WidgetType::ColorButton => "color button",
761 WidgetType::Image => "image",
762 WidgetType::CollapsingHeader => "collapsing header",
763 WidgetType::Panel => "panel",
764 WidgetType::ProgressIndicator => "progress indicator",
765 WidgetType::Window => "window",
766 WidgetType::ScrollBar => "scroll bar",
767 WidgetType::ResizeHandle => "resize handle",
768 WidgetType::Label | WidgetType::Other => "",
769 };
770
771 let mut description = widget_type.to_owned();
772
773 if let Some(selected) = selected {
774 if *typ == WidgetType::Checkbox {
775 let state = if *selected { "checked" } else { "unchecked" };
776 description = format!("{state} {description}");
777 } else {
778 description += if *selected { "selected" } else { "" };
779 }
780 }
781
782 if let Some(label) = label {
783 description = format!("{label}: {description}");
784 }
785
786 if typ == &WidgetType::TextEdit {
787 let text = if let Some(text_value) = text_value {
788 if text_value.is_empty() {
789 "blank".into()
790 } else {
791 text_value.clone()
792 }
793 } else {
794 "blank".into()
795 };
796 description = format!("{text}: {description}");
797 }
798
799 if let Some(value) = value {
800 description += " ";
801 description += &value.to_string();
802 }
803
804 if !enabled {
805 description += ": disabled";
806 }
807 description.trim().to_owned()
808 }
809}