1use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
4
5#[derive(Clone, Default)]
9pub struct FullOutput {
10 pub platform_output: PlatformOutput,
12
13 pub textures_delta: epaint::textures::TexturesDelta,
20
21 pub shapes: Vec<epaint::ClippedShape>,
25
26 pub pixels_per_point: f32,
30
31 pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
36}
37
38impl FullOutput {
39 pub fn append(&mut self, newer: Self) {
41 use std::collections::btree_map::Entry;
42
43 let Self {
44 platform_output,
45 textures_delta,
46 shapes,
47 pixels_per_point,
48 viewport_output,
49 } = newer;
50
51 self.platform_output.append(platform_output);
52 self.textures_delta.append(textures_delta);
53 self.shapes = shapes; self.pixels_per_point = pixels_per_point; for (id, new_viewport) in viewport_output {
57 match self.viewport_output.entry(id) {
58 Entry::Vacant(entry) => {
59 entry.insert(new_viewport);
60 }
61 Entry::Occupied(mut entry) => {
62 entry.get_mut().append(new_viewport);
63 }
64 }
65 }
66 }
67}
68
69#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
74pub struct IMEOutput {
75 pub rect: crate::Rect,
77
78 pub cursor_rect: crate::Rect,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
89pub enum OutputCommand {
90 CopyText(String),
94
95 CopyImage(crate::ColorImage),
97
98 OpenUrl(OpenUrl),
100}
101
102#[derive(Default, Clone, PartialEq)]
108#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
109pub struct PlatformOutput {
110 pub commands: Vec<OutputCommand>,
112
113 pub cursor_icon: CursorIcon,
115
116 pub events: Vec<OutputEvent>,
118
119 pub mutable_text_under_cursor: bool,
122
123 pub ime: Option<IMEOutput>,
127
128 #[cfg(feature = "accesskit")]
132 pub accesskit_update: Option<accesskit::TreeUpdate>,
133
134 pub num_completed_passes: usize,
141
142 #[cfg_attr(feature = "serde", serde(skip))]
148 pub request_discard_reasons: Vec<RepaintCause>,
149}
150
151impl PlatformOutput {
152 pub fn events_description(&self) -> String {
154 if let Some(event) = self.events.iter().next_back() {
156 match event {
157 OutputEvent::Clicked(widget_info)
158 | OutputEvent::DoubleClicked(widget_info)
159 | OutputEvent::TripleClicked(widget_info)
160 | OutputEvent::FocusGained(widget_info)
161 | OutputEvent::TextSelectionChanged(widget_info)
162 | OutputEvent::ValueChanged(widget_info) => {
163 return widget_info.description();
164 }
165 }
166 }
167 Default::default()
168 }
169
170 pub fn append(&mut self, newer: Self) {
172 let Self {
173 mut commands,
174 cursor_icon,
175 mut events,
176 mutable_text_under_cursor,
177 ime,
178 #[cfg(feature = "accesskit")]
179 accesskit_update,
180 num_completed_passes,
181 mut request_discard_reasons,
182 } = newer;
183
184 self.commands.append(&mut commands);
185 self.cursor_icon = cursor_icon;
186 self.events.append(&mut events);
187 self.mutable_text_under_cursor = mutable_text_under_cursor;
188 self.ime = ime.or(self.ime);
189 self.num_completed_passes += num_completed_passes;
190 self.request_discard_reasons
191 .append(&mut request_discard_reasons);
192
193 #[cfg(feature = "accesskit")]
194 {
195 self.accesskit_update = accesskit_update;
198 }
199 }
200
201 pub fn take(&mut self) -> Self {
203 let taken = std::mem::take(self);
204 self.cursor_icon = taken.cursor_icon; taken
206 }
207
208 pub fn requested_discard(&self) -> bool {
210 !self.request_discard_reasons.is_empty()
211 }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
218#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
219pub struct OpenUrl {
220 pub url: String,
221
222 pub new_tab: bool,
226}
227
228impl OpenUrl {
229 #[expect(clippy::needless_pass_by_value)]
230 pub fn same_tab(url: impl ToString) -> Self {
231 Self {
232 url: url.to_string(),
233 new_tab: false,
234 }
235 }
236
237 #[expect(clippy::needless_pass_by_value)]
238 pub fn new_tab(url: impl ToString) -> Self {
239 Self {
240 url: url.to_string(),
241 new_tab: true,
242 }
243 }
244}
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
252#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
253pub enum UserAttentionType {
254 Critical,
256
257 Informational,
259
260 Reset,
262}
263
264#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
270#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
271pub enum CursorIcon {
272 #[default]
274 Default,
275
276 None,
278
279 ContextMenu,
283
284 Help,
286
287 PointingHand,
289
290 Progress,
292
293 Wait,
295
296 Cell,
300
301 Crosshair,
303
304 Text,
306
307 VerticalText,
309
310 Alias,
314
315 Copy,
317
318 Move,
320
321 NoDrop,
323
324 NotAllowed,
326
327 Grab,
329
330 Grabbing,
332
333 AllScroll,
336
337 ResizeHorizontal,
341
342 ResizeNeSw,
344
345 ResizeNwSe,
347
348 ResizeVertical,
350
351 ResizeEast,
355
356 ResizeSouthEast,
358
359 ResizeSouth,
361
362 ResizeSouthWest,
364
365 ResizeWest,
367
368 ResizeNorthWest,
370
371 ResizeNorth,
373
374 ResizeNorthEast,
376
377 ResizeColumn,
380
381 ResizeRow,
383
384 ZoomIn,
388
389 ZoomOut,
391}
392
393impl CursorIcon {
394 pub const ALL: [Self; 35] = [
395 Self::Default,
396 Self::None,
397 Self::ContextMenu,
398 Self::Help,
399 Self::PointingHand,
400 Self::Progress,
401 Self::Wait,
402 Self::Cell,
403 Self::Crosshair,
404 Self::Text,
405 Self::VerticalText,
406 Self::Alias,
407 Self::Copy,
408 Self::Move,
409 Self::NoDrop,
410 Self::NotAllowed,
411 Self::Grab,
412 Self::Grabbing,
413 Self::AllScroll,
414 Self::ResizeHorizontal,
415 Self::ResizeNeSw,
416 Self::ResizeNwSe,
417 Self::ResizeVertical,
418 Self::ResizeEast,
419 Self::ResizeSouthEast,
420 Self::ResizeSouth,
421 Self::ResizeSouthWest,
422 Self::ResizeWest,
423 Self::ResizeNorthWest,
424 Self::ResizeNorth,
425 Self::ResizeNorthEast,
426 Self::ResizeColumn,
427 Self::ResizeRow,
428 Self::ZoomIn,
429 Self::ZoomOut,
430 ];
431}
432
433#[derive(Clone, PartialEq)]
437#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
438pub enum OutputEvent {
439 Clicked(WidgetInfo),
441
442 DoubleClicked(WidgetInfo),
444
445 TripleClicked(WidgetInfo),
447
448 FocusGained(WidgetInfo),
450
451 TextSelectionChanged(WidgetInfo),
453
454 ValueChanged(WidgetInfo),
456}
457
458impl OutputEvent {
459 pub fn widget_info(&self) -> &WidgetInfo {
460 match self {
461 Self::Clicked(info)
462 | Self::DoubleClicked(info)
463 | Self::TripleClicked(info)
464 | Self::FocusGained(info)
465 | Self::TextSelectionChanged(info)
466 | Self::ValueChanged(info) => info,
467 }
468 }
469}
470
471impl std::fmt::Debug for OutputEvent {
472 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
473 match self {
474 Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
475 Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
476 Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
477 Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
478 Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
479 Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
480 }
481 }
482}
483
484#[derive(Clone, PartialEq)]
486#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
487pub struct WidgetInfo {
488 pub typ: WidgetType,
490
491 pub enabled: bool,
493
494 pub label: Option<String>,
496
497 pub current_text_value: Option<String>,
499
500 pub prev_text_value: Option<String>,
502
503 pub selected: Option<bool>,
505
506 pub value: Option<f64>,
508
509 pub text_selection: Option<std::ops::RangeInclusive<usize>>,
511
512 pub hint_text: Option<String>,
514}
515
516impl std::fmt::Debug for WidgetInfo {
517 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518 let Self {
519 typ,
520 enabled,
521 label,
522 current_text_value: text_value,
523 prev_text_value,
524 selected,
525 value,
526 text_selection,
527 hint_text,
528 } = self;
529
530 let mut s = f.debug_struct("WidgetInfo");
531
532 s.field("typ", typ);
533
534 if !enabled {
535 s.field("enabled", enabled);
536 }
537
538 if let Some(label) = label {
539 s.field("label", label);
540 }
541 if let Some(text_value) = text_value {
542 s.field("text_value", text_value);
543 }
544 if let Some(prev_text_value) = prev_text_value {
545 s.field("prev_text_value", prev_text_value);
546 }
547 if let Some(selected) = selected {
548 s.field("selected", selected);
549 }
550 if let Some(value) = value {
551 s.field("value", value);
552 }
553 if let Some(text_selection) = text_selection {
554 s.field("text_selection", text_selection);
555 }
556 if let Some(hint_text) = hint_text {
557 s.field("hint_text", hint_text);
558 }
559
560 s.finish()
561 }
562}
563
564impl WidgetInfo {
565 pub fn new(typ: WidgetType) -> Self {
566 Self {
567 typ,
568 enabled: true,
569 label: None,
570 current_text_value: None,
571 prev_text_value: None,
572 selected: None,
573 value: None,
574 text_selection: None,
575 hint_text: None,
576 }
577 }
578
579 #[expect(clippy::needless_pass_by_value)]
580 pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
581 Self {
582 enabled,
583 label: Some(label.to_string()),
584 ..Self::new(typ)
585 }
586 }
587
588 #[expect(clippy::needless_pass_by_value)]
590 pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
591 Self {
592 enabled,
593 label: Some(label.to_string()),
594 selected: Some(selected),
595 ..Self::new(typ)
596 }
597 }
598
599 pub fn drag_value(enabled: bool, value: f64) -> Self {
600 Self {
601 enabled,
602 value: Some(value),
603 ..Self::new(WidgetType::DragValue)
604 }
605 }
606
607 #[expect(clippy::needless_pass_by_value)]
608 pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
609 let label = label.to_string();
610 Self {
611 enabled,
612 label: if label.is_empty() { None } else { Some(label) },
613 value: Some(value),
614 ..Self::new(WidgetType::Slider)
615 }
616 }
617
618 #[expect(clippy::needless_pass_by_value)]
619 pub fn text_edit(
620 enabled: bool,
621 prev_text_value: impl ToString,
622 text_value: impl ToString,
623 hint_text: impl ToString,
624 ) -> Self {
625 let text_value = text_value.to_string();
626 let prev_text_value = prev_text_value.to_string();
627 let hint_text = hint_text.to_string();
628 let prev_text_value = if text_value == prev_text_value {
629 None
630 } else {
631 Some(prev_text_value)
632 };
633 Self {
634 enabled,
635 current_text_value: Some(text_value),
636 prev_text_value,
637 hint_text: Some(hint_text),
638 ..Self::new(WidgetType::TextEdit)
639 }
640 }
641
642 #[expect(clippy::needless_pass_by_value)]
643 pub fn text_selection_changed(
644 enabled: bool,
645 text_selection: std::ops::RangeInclusive<usize>,
646 current_text_value: impl ToString,
647 ) -> Self {
648 Self {
649 enabled,
650 text_selection: Some(text_selection),
651 current_text_value: Some(current_text_value.to_string()),
652 ..Self::new(WidgetType::TextEdit)
653 }
654 }
655
656 pub fn description(&self) -> String {
658 let Self {
659 typ,
660 enabled,
661 label,
662 current_text_value: text_value,
663 prev_text_value: _,
664 selected,
665 value,
666 text_selection: _,
667 hint_text: _,
668 } = self;
669
670 let widget_type = match typ {
672 WidgetType::Link => "link",
673 WidgetType::TextEdit => "text edit",
674 WidgetType::Button => "button",
675 WidgetType::Checkbox => "checkbox",
676 WidgetType::RadioButton => "radio",
677 WidgetType::RadioGroup => "radio group",
678 WidgetType::SelectableLabel => "selectable",
679 WidgetType::ComboBox => "combo",
680 WidgetType::Slider => "slider",
681 WidgetType::DragValue => "drag value",
682 WidgetType::ColorButton => "color button",
683 WidgetType::Image => "image",
684 WidgetType::CollapsingHeader => "collapsing header",
685 WidgetType::Panel => "panel",
686 WidgetType::ProgressIndicator => "progress indicator",
687 WidgetType::Window => "window",
688 WidgetType::Label | WidgetType::Other => "",
689 };
690
691 let mut description = widget_type.to_owned();
692
693 if let Some(selected) = selected {
694 if *typ == WidgetType::Checkbox {
695 let state = if *selected { "checked" } else { "unchecked" };
696 description = format!("{state} {description}");
697 } else {
698 description += if *selected { "selected" } else { "" };
699 }
700 }
701
702 if let Some(label) = label {
703 description = format!("{label}: {description}");
704 }
705
706 if typ == &WidgetType::TextEdit {
707 let text = if let Some(text_value) = text_value {
708 if text_value.is_empty() {
709 "blank".into()
710 } else {
711 text_value.clone()
712 }
713 } else {
714 "blank".into()
715 };
716 description = format!("{text}: {description}");
717 }
718
719 if let Some(value) = value {
720 description += " ";
721 description += &value.to_string();
722 }
723
724 if !enabled {
725 description += ": disabled";
726 }
727 description.trim().to_owned()
728 }
729}