use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
#[derive(Clone, Default)]
pub struct FullOutput {
pub platform_output: PlatformOutput,
pub textures_delta: epaint::textures::TexturesDelta,
pub shapes: Vec<epaint::ClippedShape>,
pub pixels_per_point: f32,
pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
}
impl FullOutput {
pub fn append(&mut self, newer: Self) {
use std::collections::btree_map::Entry;
let Self {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = newer;
self.platform_output.append(platform_output);
self.textures_delta.append(textures_delta);
self.shapes = shapes; self.pixels_per_point = pixels_per_point;
for (id, new_viewport) in viewport_output {
match self.viewport_output.entry(id) {
Entry::Vacant(entry) => {
entry.insert(new_viewport);
}
Entry::Occupied(mut entry) => {
entry.get_mut().append(new_viewport);
}
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct IMEOutput {
pub rect: crate::Rect,
pub cursor_rect: crate::Rect,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum OutputCommand {
CopyText(String),
CopyImage(crate::ColorImage),
OpenUrl(OpenUrl),
}
#[derive(Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PlatformOutput {
pub commands: Vec<OutputCommand>,
pub cursor_icon: CursorIcon,
pub events: Vec<OutputEvent>,
pub mutable_text_under_cursor: bool,
pub ime: Option<IMEOutput>,
#[cfg(feature = "accesskit")]
pub accesskit_update: Option<accesskit::TreeUpdate>,
pub num_completed_passes: usize,
#[cfg_attr(feature = "serde", serde(skip))]
pub request_discard_reasons: Vec<RepaintCause>,
}
impl PlatformOutput {
pub fn events_description(&self) -> String {
if let Some(event) = self.events.iter().next_back() {
match event {
OutputEvent::Clicked(widget_info)
| OutputEvent::DoubleClicked(widget_info)
| OutputEvent::TripleClicked(widget_info)
| OutputEvent::FocusGained(widget_info)
| OutputEvent::TextSelectionChanged(widget_info)
| OutputEvent::ValueChanged(widget_info) => {
return widget_info.description();
}
}
}
Default::default()
}
pub fn append(&mut self, newer: Self) {
let Self {
mut commands,
cursor_icon,
mut events,
mutable_text_under_cursor,
ime,
#[cfg(feature = "accesskit")]
accesskit_update,
num_completed_passes,
mut request_discard_reasons,
} = newer;
self.commands.append(&mut commands);
self.cursor_icon = cursor_icon;
self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor;
self.ime = ime.or(self.ime);
self.num_completed_passes += num_completed_passes;
self.request_discard_reasons
.append(&mut request_discard_reasons);
#[cfg(feature = "accesskit")]
{
self.accesskit_update = accesskit_update;
}
}
pub fn take(&mut self) -> Self {
let taken = std::mem::take(self);
self.cursor_icon = taken.cursor_icon; taken
}
pub fn requested_discard(&self) -> bool {
!self.request_discard_reasons.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct OpenUrl {
pub url: String,
pub new_tab: bool,
}
impl OpenUrl {
#[expect(clippy::needless_pass_by_value)]
pub fn same_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
new_tab: false,
}
}
#[expect(clippy::needless_pass_by_value)]
pub fn new_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
new_tab: true,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum UserAttentionType {
Critical,
Informational,
Reset,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum CursorIcon {
#[default]
Default,
None,
ContextMenu,
Help,
PointingHand,
Progress,
Wait,
Cell,
Crosshair,
Text,
VerticalText,
Alias,
Copy,
Move,
NoDrop,
NotAllowed,
Grab,
Grabbing,
AllScroll,
ResizeHorizontal,
ResizeNeSw,
ResizeNwSe,
ResizeVertical,
ResizeEast,
ResizeSouthEast,
ResizeSouth,
ResizeSouthWest,
ResizeWest,
ResizeNorthWest,
ResizeNorth,
ResizeNorthEast,
ResizeColumn,
ResizeRow,
ZoomIn,
ZoomOut,
}
impl CursorIcon {
pub const ALL: [Self; 35] = [
Self::Default,
Self::None,
Self::ContextMenu,
Self::Help,
Self::PointingHand,
Self::Progress,
Self::Wait,
Self::Cell,
Self::Crosshair,
Self::Text,
Self::VerticalText,
Self::Alias,
Self::Copy,
Self::Move,
Self::NoDrop,
Self::NotAllowed,
Self::Grab,
Self::Grabbing,
Self::AllScroll,
Self::ResizeHorizontal,
Self::ResizeNeSw,
Self::ResizeNwSe,
Self::ResizeVertical,
Self::ResizeEast,
Self::ResizeSouthEast,
Self::ResizeSouth,
Self::ResizeSouthWest,
Self::ResizeWest,
Self::ResizeNorthWest,
Self::ResizeNorth,
Self::ResizeNorthEast,
Self::ResizeColumn,
Self::ResizeRow,
Self::ZoomIn,
Self::ZoomOut,
];
}
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum OutputEvent {
Clicked(WidgetInfo),
DoubleClicked(WidgetInfo),
TripleClicked(WidgetInfo),
FocusGained(WidgetInfo),
TextSelectionChanged(WidgetInfo),
ValueChanged(WidgetInfo),
}
impl OutputEvent {
pub fn widget_info(&self) -> &WidgetInfo {
match self {
Self::Clicked(info)
| Self::DoubleClicked(info)
| Self::TripleClicked(info)
| Self::FocusGained(info)
| Self::TextSelectionChanged(info)
| Self::ValueChanged(info) => info,
}
}
}
impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
}
}
}
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct WidgetInfo {
pub typ: WidgetType,
pub enabled: bool,
pub label: Option<String>,
pub current_text_value: Option<String>,
pub prev_text_value: Option<String>,
pub selected: Option<bool>,
pub value: Option<f64>,
pub text_selection: Option<std::ops::RangeInclusive<usize>>,
pub hint_text: Option<String>,
}
impl std::fmt::Debug for WidgetInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
typ,
enabled,
label,
current_text_value: text_value,
prev_text_value,
selected,
value,
text_selection,
hint_text,
} = self;
let mut s = f.debug_struct("WidgetInfo");
s.field("typ", typ);
if !enabled {
s.field("enabled", enabled);
}
if let Some(label) = label {
s.field("label", label);
}
if let Some(text_value) = text_value {
s.field("text_value", text_value);
}
if let Some(prev_text_value) = prev_text_value {
s.field("prev_text_value", prev_text_value);
}
if let Some(selected) = selected {
s.field("selected", selected);
}
if let Some(value) = value {
s.field("value", value);
}
if let Some(text_selection) = text_selection {
s.field("text_selection", text_selection);
}
if let Some(hint_text) = hint_text {
s.field("hint_text", hint_text);
}
s.finish()
}
}
impl WidgetInfo {
pub fn new(typ: WidgetType) -> Self {
Self {
typ,
enabled: true,
label: None,
current_text_value: None,
prev_text_value: None,
selected: None,
value: None,
text_selection: None,
hint_text: None,
}
}
#[expect(clippy::needless_pass_by_value)]
pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
Self {
enabled,
label: Some(label.to_string()),
..Self::new(typ)
}
}
#[expect(clippy::needless_pass_by_value)]
pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
Self {
enabled,
label: Some(label.to_string()),
selected: Some(selected),
..Self::new(typ)
}
}
pub fn drag_value(enabled: bool, value: f64) -> Self {
Self {
enabled,
value: Some(value),
..Self::new(WidgetType::DragValue)
}
}
#[expect(clippy::needless_pass_by_value)]
pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
let label = label.to_string();
Self {
enabled,
label: if label.is_empty() { None } else { Some(label) },
value: Some(value),
..Self::new(WidgetType::Slider)
}
}
#[expect(clippy::needless_pass_by_value)]
pub fn text_edit(
enabled: bool,
prev_text_value: impl ToString,
text_value: impl ToString,
hint_text: impl ToString,
) -> Self {
let text_value = text_value.to_string();
let prev_text_value = prev_text_value.to_string();
let hint_text = hint_text.to_string();
let prev_text_value = if text_value == prev_text_value {
None
} else {
Some(prev_text_value)
};
Self {
enabled,
current_text_value: Some(text_value),
prev_text_value,
hint_text: Some(hint_text),
..Self::new(WidgetType::TextEdit)
}
}
#[expect(clippy::needless_pass_by_value)]
pub fn text_selection_changed(
enabled: bool,
text_selection: std::ops::RangeInclusive<usize>,
current_text_value: impl ToString,
) -> Self {
Self {
enabled,
text_selection: Some(text_selection),
current_text_value: Some(current_text_value.to_string()),
..Self::new(WidgetType::TextEdit)
}
}
pub fn description(&self) -> String {
let Self {
typ,
enabled,
label,
current_text_value: text_value,
prev_text_value: _,
selected,
value,
text_selection: _,
hint_text: _,
} = self;
let widget_type = match typ {
WidgetType::Link => "link",
WidgetType::TextEdit => "text edit",
WidgetType::Button => "button",
WidgetType::Checkbox => "checkbox",
WidgetType::RadioButton => "radio",
WidgetType::RadioGroup => "radio group",
WidgetType::SelectableLabel => "selectable",
WidgetType::ComboBox => "combo",
WidgetType::Slider => "slider",
WidgetType::DragValue => "drag value",
WidgetType::ColorButton => "color button",
WidgetType::Image => "image",
WidgetType::CollapsingHeader => "collapsing header",
WidgetType::Panel => "panel",
WidgetType::ProgressIndicator => "progress indicator",
WidgetType::Window => "window",
WidgetType::Label | WidgetType::Other => "",
};
let mut description = widget_type.to_owned();
if let Some(selected) = selected {
if *typ == WidgetType::Checkbox {
let state = if *selected { "checked" } else { "unchecked" };
description = format!("{state} {description}");
} else {
description += if *selected { "selected" } else { "" };
}
}
if let Some(label) = label {
description = format!("{label}: {description}");
}
if typ == &WidgetType::TextEdit {
let text = if let Some(text_value) = text_value {
if text_value.is_empty() {
"blank".into()
} else {
text_value.clone()
}
} else {
"blank".into()
};
description = format!("{text}: {description}");
}
if let Some(value) = value {
description += " ";
description += &value.to_string();
}
if !enabled {
description += ": disabled";
}
description.trim().to_owned()
}
}