use std::path::PathBuf;
use crate::{
AppEffect, AppEffectOutcome, AppEffectRequest, DialogResponse, DirectoryDialogSpec,
FileDialogSpec, NativeDialogSpec, SaveFileDialogSpec, ZsuiResult,
};
#[cfg(feature = "text-input-core")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ZsTextSelection {
pub anchor: usize,
pub caret: usize,
}
#[cfg(feature = "text-input-core")]
impl ZsTextSelection {
pub const fn collapsed(caret: usize) -> Self {
Self {
anchor: caret,
caret,
}
}
pub const fn ordered(self) -> (usize, usize) {
if self.anchor <= self.caret {
(self.anchor, self.caret)
} else {
(self.caret, self.anchor)
}
}
pub const fn is_collapsed(self) -> bool {
self.anchor == self.caret
}
}
#[cfg(feature = "text-input-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ZsTextEditCommand {
Undo,
Cut,
Copy,
Paste,
SelectAll,
}
#[cfg(feature = "text-input-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ZsTextEditCommandRequest {
pub widget: Option<WidgetId>,
pub command: ZsTextEditCommand,
}
#[cfg(feature = "text-input-core")]
impl ZsTextEditCommandRequest {
pub const fn focused(command: ZsTextEditCommand) -> Self {
Self {
widget: None,
command,
}
}
pub const fn for_widget(widget: WidgetId, command: ZsTextEditCommand) -> Self {
Self {
widget: Some(widget),
command,
}
}
}
#[cfg(feature = "text-input-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ViewTextEditCapabilities {
select_all: bool,
copy: bool,
cut: bool,
paste: bool,
undo: bool,
}
#[cfg(feature = "text-input-core")]
impl ViewTextEditCapabilities {
#[allow(dead_code)]
pub(crate) const PLAIN: Self = Self {
select_all: true,
copy: true,
cut: true,
paste: true,
undo: true,
};
#[cfg(feature = "password-box")]
pub(crate) const PROTECTED: Self = Self {
select_all: true,
copy: false,
cut: false,
paste: true,
undo: true,
};
pub(crate) const fn allows(self, command: ZsTextEditCommand) -> bool {
match command {
ZsTextEditCommand::Undo => self.undo,
ZsTextEditCommand::Cut => self.cut,
ZsTextEditCommand::Copy => self.copy,
ZsTextEditCommand::Paste => self.paste,
ZsTextEditCommand::SelectAll => self.select_all,
}
}
pub(crate) const fn supports_undo(self) -> bool {
self.undo
}
}
#[cfg(feature = "text-input-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ViewTextEditChangeKind {
#[cfg(feature = "textbox")]
TextEdited,
TextChanged,
#[cfg(feature = "password-box")]
PasswordChanged,
}
#[cfg(feature = "text-input-core")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ViewEditableTextDescriptor {
pub(crate) capabilities: ViewTextEditCapabilities,
pub(crate) change_kind: ViewTextEditChangeKind,
pub(crate) multiline: bool,
pub(crate) secure: bool,
}
#[cfg(feature = "text-input-core")]
impl ViewEditableTextDescriptor {
#[cfg(feature = "textbox")]
pub(crate) const fn textbox(multiline: bool) -> Self {
Self {
capabilities: ViewTextEditCapabilities::PLAIN,
change_kind: ViewTextEditChangeKind::TextEdited,
multiline,
secure: false,
}
}
#[cfg(any(
feature = "number-box",
feature = "auto-suggest",
feature = "command-palette"
))]
pub(crate) const fn plain_text_changed() -> Self {
Self {
capabilities: ViewTextEditCapabilities::PLAIN,
change_kind: ViewTextEditChangeKind::TextChanged,
multiline: false,
secure: false,
}
}
#[cfg(feature = "password-box")]
pub(crate) const fn password() -> Self {
Self {
capabilities: ViewTextEditCapabilities::PROTECTED,
change_kind: ViewTextEditChangeKind::PasswordChanged,
multiline: false,
secure: true,
}
}
#[cfg(feature = "textbox")]
pub(crate) const fn emits_selection_event(self) -> bool {
matches!(self.change_kind, ViewTextEditChangeKind::TextEdited)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ViewEvent {
#[cfg(feature = "window-chrome")]
WindowStateChanged { state: crate::ZsWindowState },
#[cfg(feature = "resize")]
Resized {
widget: WidgetId,
target: crate::ZsResizeTarget,
extent: Dp,
},
Click {
widget: WidgetId,
},
#[cfg(feature = "canvas")]
CanvasPointer {
event: crate::ZsCanvasPointerEvent,
},
#[cfg(feature = "canvas")]
CanvasHover {
widget: WidgetId,
region: Option<u64>,
},
TextChanged {
widget: WidgetId,
value: String,
},
#[cfg(feature = "textbox")]
TextEdited {
widget: WidgetId,
value: String,
selection: ZsTextSelection,
},
#[cfg(feature = "textbox")]
TextSelectionChanged {
widget: WidgetId,
selection: ZsTextSelection,
},
#[cfg(feature = "shortcuts")]
Shortcut {
accelerator: crate::ZsAccelerator,
},
#[cfg(feature = "textbox")]
TextSubmitted {
widget: WidgetId,
value: String,
},
#[cfg(feature = "password-box")]
PasswordChanged {
widget: WidgetId,
#[serde(skip, default)]
value: crate::ZsPassword,
},
Toggled {
widget: WidgetId,
checked: bool,
},
#[cfg(feature = "slider")]
SliderChanged {
widget: WidgetId,
value: f32,
},
#[cfg(feature = "number-box")]
NumberBoxValueChanged {
widget: WidgetId,
value: Option<f64>,
},
#[cfg(feature = "number-box")]
NumberBoxStep {
widget: WidgetId,
steps: i32,
large: bool,
},
#[cfg(feature = "number-box")]
NumberBoxCommit {
widget: WidgetId,
},
#[cfg(feature = "number-box")]
NumberBoxReset {
widget: WidgetId,
},
#[cfg(feature = "radio")]
RadioSelected {
widget: WidgetId,
},
#[cfg(feature = "auto-suggest")]
AutoSuggestExpandedChanged {
widget: WidgetId,
expanded: bool,
},
#[cfg(feature = "auto-suggest")]
AutoSuggestHighlighted {
widget: WidgetId,
suggestion: crate::ZsAutoSuggestionId,
},
#[cfg(feature = "auto-suggest")]
AutoSuggestCleared {
widget: WidgetId,
},
#[cfg(feature = "auto-suggest")]
AutoSuggestSubmitted {
widget: WidgetId,
suggestion: Option<crate::ZsAutoSuggestionId>,
},
#[cfg(feature = "tree")]
TreeNodeExpandedChanged {
widget: WidgetId,
node: crate::ZsTreeNodeId,
expanded: bool,
},
#[cfg(feature = "tree")]
TreeNodeSelected {
widget: WidgetId,
node: crate::ZsTreeNodeId,
},
#[cfg(feature = "tree")]
TreeNodeInvoked {
widget: WidgetId,
node: crate::ZsTreeNodeId,
},
#[cfg(feature = "grid-view")]
GridViewItemSelected {
widget: WidgetId,
item: crate::ZsGridViewItemId,
},
#[cfg(feature = "grid-view")]
GridViewItemInvoked {
widget: WidgetId,
item: crate::ZsGridViewItemId,
},
#[cfg(feature = "table")]
TableRowSelected {
widget: WidgetId,
row: crate::ZsTableRowId,
},
#[cfg(feature = "table")]
TableSorted {
widget: WidgetId,
column: crate::ZsTableColumnId,
},
#[cfg(feature = "table")]
TableRowInvoked {
widget: WidgetId,
row: crate::ZsTableRowId,
},
#[cfg(feature = "dialog")]
ContentDialogFocused {
widget: WidgetId,
button: crate::ZsContentDialogButton,
},
#[cfg(feature = "dialog")]
ContentDialogResponded {
widget: WidgetId,
button: crate::ZsContentDialogButton,
},
#[cfg(feature = "command-palette")]
CommandPaletteHighlighted {
widget: WidgetId,
item: crate::ZsCommandPaletteItemId,
},
#[cfg(feature = "command-palette")]
CommandPaletteInvoked {
widget: WidgetId,
item: crate::ZsCommandPaletteItemId,
},
#[cfg(feature = "command-palette")]
CommandPaletteOpenChanged {
widget: WidgetId,
open: bool,
},
#[cfg(feature = "toast")]
ToastFocused {
widget: WidgetId,
toast: crate::ZsToastId,
control: crate::ZsToastControl,
},
#[cfg(feature = "toast")]
ToastResponded {
widget: WidgetId,
toast: crate::ZsToastId,
response: crate::ZsToastResponse,
},
#[cfg(feature = "teaching-tip")]
TeachingTipFocused {
widget: WidgetId,
control: crate::ZsTeachingTipControl,
},
#[cfg(feature = "teaching-tip")]
TeachingTipResponded {
widget: WidgetId,
response: crate::ZsTeachingTipResponse,
},
#[cfg(feature = "flyout")]
FlyoutDismissed {
widget: WidgetId,
reason: crate::ZsFlyoutDismissReason,
},
#[cfg(feature = "split-view")]
SplitViewOpenChanged {
widget: WidgetId,
open: bool,
},
#[cfg(feature = "menu-flyout")]
MenuFlyoutHighlighted {
widget: WidgetId,
path: crate::ZsMenuFlyoutPath,
},
#[cfg(feature = "menu-flyout")]
MenuFlyoutSubmenuChanged {
widget: WidgetId,
submenu: Option<crate::ZsMenuFlyoutPath>,
},
#[cfg(feature = "menu-flyout")]
MenuFlyoutInvoked {
widget: WidgetId,
path: crate::ZsMenuFlyoutPath,
},
#[cfg(feature = "menu-flyout")]
MenuFlyoutOpenChanged {
widget: WidgetId,
open: bool,
},
#[cfg(feature = "context-menu")]
ContextMenuRequested {
widget: WidgetId,
anchor: Point,
},
#[cfg(feature = "info-bar")]
InfoBarFocused {
widget: WidgetId,
control: crate::ZsInfoBarControl,
},
#[cfg(feature = "info-bar")]
InfoBarInvoked {
widget: WidgetId,
event: crate::ZsInfoBarEvent,
},
#[cfg(feature = "breadcrumb")]
BreadcrumbFocused {
widget: WidgetId,
target: crate::ZsBreadcrumbFocusTarget,
},
#[cfg(feature = "breadcrumb")]
BreadcrumbExpandedChanged {
widget: WidgetId,
expanded: bool,
},
#[cfg(feature = "breadcrumb")]
BreadcrumbSelected {
widget: WidgetId,
item: crate::ZsBreadcrumbId,
},
#[cfg(feature = "combo")]
ComboBoxExpandedChanged {
widget: WidgetId,
expanded: bool,
},
#[cfg(feature = "combo")]
ComboBoxSelected {
widget: WidgetId,
index: usize,
},
#[cfg(feature = "combo")]
ComboBoxScrolled {
widget: WidgetId,
first_visible_index: usize,
},
#[cfg(feature = "date-picker")]
DatePickerExpandedChanged {
widget: WidgetId,
expanded: bool,
},
#[cfg(feature = "date-picker")]
DatePickerMonthChanged {
widget: WidgetId,
month: ZsDate,
},
#[cfg(feature = "date-picker")]
DateChanged {
widget: WidgetId,
value: ZsDate,
},
#[cfg(feature = "time-picker")]
TimePickerExpandedChanged {
widget: WidgetId,
expanded: bool,
},
#[cfg(feature = "time-picker")]
TimeChanged {
widget: WidgetId,
value: ZsTime,
},
#[cfg(feature = "color-picker")]
ColorPickerExpandedChanged {
widget: WidgetId,
expanded: bool,
},
#[cfg(feature = "color-picker")]
ColorPickerChannelChanged {
widget: WidgetId,
channel: ZsColorChannel,
},
#[cfg(feature = "color-picker")]
ColorChanged {
widget: WidgetId,
color: crate::Color,
},
#[cfg(feature = "tabs")]
TabSelected {
widget: WidgetId,
tab: ZsTabId,
},
#[cfg(any(
feature = "auto-suggest",
feature = "breadcrumb",
feature = "color-picker",
feature = "combo",
feature = "date-picker",
feature = "flyout",
feature = "menu-flyout",
feature = "time-picker"
))]
DismissPopupOverlays {
except: Option<WidgetId>,
},
#[cfg(feature = "scroll")]
ScrollBy {
widget: WidgetId,
delta_y: Dp,
},
#[cfg(feature = "scroll")]
ScrollToRatio {
widget: WidgetId,
ratio: f32,
},
#[cfg(feature = "virtual-list")]
ItemsRepeaterScrollToRatio {
widget: WidgetId,
ratio: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewHitTarget {
pub widget: WidgetId,
pub bounds: Rect,
pub kind: ViewHitTargetKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViewFocusBehavior {
None,
TabStop,
}
impl ViewFocusBehavior {
pub const fn is_tab_stop(self) -> bool {
matches!(self, Self::TabStop)
}
}
#[cfg(feature = "tooltip")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewTooltipTarget {
pub widget: WidgetId,
pub bounds: Rect,
pub spec: crate::ZsTooltipSpec,
}
#[cfg(feature = "tooltip")]
impl ViewTooltipTarget {
pub fn contains(&self, point: Point) -> bool {
self.bounds.contains(point)
}
}
impl ViewHitTarget {
pub const fn new(widget: WidgetId, bounds: Rect) -> Self {
Self {
widget,
bounds,
kind: ViewHitTargetKind::Unknown,
}
}
pub const fn with_kind(widget: WidgetId, bounds: Rect, kind: ViewHitTargetKind) -> Self {
Self {
widget,
bounds,
kind,
}
}
pub const fn contains(self, point: Point) -> bool {
self.bounds.contains(point)
}
pub const fn focus_behavior(self) -> ViewFocusBehavior {
self.kind.focus_behavior()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViewHitTargetKind {
Unknown,
#[cfg(feature = "resize")]
ResizeHandle(crate::ZsResizeHandle),
#[cfg(feature = "canvas")]
Canvas,
Button,
#[cfg(feature = "label")]
NavigationViewToggle,
#[cfg(feature = "label")]
NavigationViewScrim,
#[cfg(feature = "split-view")]
SplitViewScrim,
#[cfg(feature = "scroll")]
ScrollbarTrack,
#[cfg(feature = "scroll")]
ScrollbarThumb,
#[cfg(feature = "virtual-list")]
ItemsRepeaterScrollbarTrack,
#[cfg(feature = "virtual-list")]
ItemsRepeaterScrollbarThumb,
#[cfg(feature = "toggle-button")]
ToggleButton,
Textbox,
TextEditor,
#[cfg(feature = "password-box")]
PasswordBox,
#[cfg(feature = "password-box")]
PasswordBoxReveal,
Checkbox,
Toggle,
#[cfg(feature = "slider")]
Slider,
#[cfg(feature = "number-box")]
NumberBox,
#[cfg(feature = "number-box")]
NumberBoxDecrement,
#[cfg(feature = "number-box")]
NumberBoxIncrement,
#[cfg(feature = "radio")]
RadioButton,
#[cfg(feature = "auto-suggest")]
AutoSuggestBox,
#[cfg(feature = "auto-suggest")]
AutoSuggestSearch,
#[cfg(feature = "auto-suggest")]
AutoSuggestClear,
#[cfg(feature = "auto-suggest")]
AutoSuggestSuggestion {
suggestion: crate::ZsAutoSuggestionId,
},
#[cfg(feature = "tree")]
TreeView,
#[cfg(feature = "tree")]
TreeNode {
node: crate::ZsTreeNodeId,
},
#[cfg(feature = "tree")]
TreeNodeExpander {
node: crate::ZsTreeNodeId,
},
#[cfg(feature = "grid-view")]
GridView,
#[cfg(feature = "grid-view")]
GridViewItem {
item: crate::ZsGridViewItemId,
},
#[cfg(feature = "table")]
DataGrid,
#[cfg(feature = "table")]
TableHeader {
column: crate::ZsTableColumnId,
},
#[cfg(feature = "table")]
TableRow {
row: crate::ZsTableRowId,
},
#[cfg(feature = "dialog")]
ContentDialog,
#[cfg(feature = "dialog")]
ContentDialogScrim,
#[cfg(feature = "dialog")]
ContentDialogButton {
button: crate::ZsContentDialogButton,
},
#[cfg(feature = "flyout")]
Flyout,
#[cfg(feature = "flyout")]
FlyoutScrim,
#[cfg(feature = "menu-flyout")]
MenuFlyout,
#[cfg(feature = "context-menu")]
ContextMenuRegion,
#[cfg(feature = "menu-flyout")]
MenuFlyoutScrim,
#[cfg(feature = "menu-flyout")]
MenuFlyoutItem {
path: crate::ZsMenuFlyoutPath,
row_kind: crate::ZsMenuFlyoutRowKind,
expanded: bool,
highlighted: bool,
},
#[cfg(feature = "command-palette")]
CommandPalette,
#[cfg(feature = "command-palette")]
CommandPaletteScrim,
#[cfg(feature = "command-palette")]
CommandPaletteClear,
#[cfg(feature = "command-palette")]
CommandPaletteItem {
item: crate::ZsCommandPaletteItemId,
},
#[cfg(feature = "toast")]
Toast,
#[cfg(feature = "toast")]
ToastAction,
#[cfg(feature = "toast")]
ToastClose,
#[cfg(feature = "teaching-tip")]
TeachingTip,
#[cfg(feature = "teaching-tip")]
TeachingTipAction,
#[cfg(feature = "teaching-tip")]
TeachingTipClose,
#[cfg(feature = "info-bar")]
InfoBar,
#[cfg(feature = "info-bar")]
InfoBarAction,
#[cfg(feature = "info-bar")]
InfoBarClose,
#[cfg(feature = "breadcrumb")]
BreadcrumbBar,
#[cfg(feature = "breadcrumb")]
BreadcrumbOverflow,
#[cfg(feature = "breadcrumb")]
BreadcrumbItem {
item: crate::ZsBreadcrumbId,
},
#[cfg(feature = "breadcrumb")]
BreadcrumbOverflowItem {
item: crate::ZsBreadcrumbId,
},
#[cfg(feature = "combo")]
ComboBox,
#[cfg(feature = "combo")]
ComboBoxOption {
index: usize,
},
#[cfg(feature = "date-picker")]
DatePicker,
#[cfg(feature = "date-picker")]
DatePickerDay {
date: ZsDate,
},
#[cfg(feature = "date-picker")]
DatePickerPreviousMonth,
#[cfg(feature = "date-picker")]
DatePickerNextMonth,
#[cfg(feature = "time-picker")]
TimePicker,
#[cfg(feature = "time-picker")]
TimePickerChoice {
value: ZsTime,
},
#[cfg(feature = "color-picker")]
ColorPicker,
#[cfg(feature = "color-picker")]
ColorPickerPopup,
#[cfg(feature = "color-picker")]
ColorPickerSpectrum,
#[cfg(feature = "color-picker")]
ColorPickerHue,
#[cfg(feature = "color-picker")]
ColorPickerChannel {
channel: ZsColorChannel,
},
#[cfg(feature = "tabs")]
Tab {
tab_view: WidgetId,
tab: ZsTabId,
index: usize,
},
#[cfg(feature = "scroll")]
Scroll,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ViewInteractionPlan {
pub hit_targets: Vec<ViewHitTarget>,
#[cfg(feature = "accessibility")]
#[serde(default)]
pub accessibility_nodes: Vec<crate::ZsAccessibilityNode>,
#[cfg(feature = "tooltip")]
#[serde(default)]
pub tooltip_targets: Vec<ViewTooltipTarget>,
#[cfg(feature = "window-chrome")]
#[serde(default)]
pub window_drag_regions: Vec<Rect>,
}
impl ViewInteractionPlan {
pub fn new(hit_targets: impl IntoIterator<Item = ViewHitTarget>) -> Self {
Self {
hit_targets: hit_targets.into_iter().collect(),
#[cfg(feature = "accessibility")]
accessibility_nodes: Vec::new(),
#[cfg(feature = "tooltip")]
tooltip_targets: Vec::new(),
#[cfg(feature = "window-chrome")]
window_drag_regions: Vec::new(),
}
}
pub fn from_layout_output(layout: &LayoutOutput) -> Self {
Self::new(
layout
.children
.iter()
.map(|node| ViewHitTarget::new(node.component.into(), node.bounds)),
)
}
pub fn hit_target_count(&self) -> usize {
self.hit_targets.len()
}
#[cfg(feature = "accessibility")]
pub fn accessibility_node_count(&self) -> usize {
self.accessibility_nodes.len()
}
#[cfg(all(feature = "accessibility", feature = "dialog"))]
pub(crate) fn accessibility_action_for_widget(
&self,
widget: WidgetId,
) -> Option<crate::ZsAccessibilityActionTarget> {
self.accessibility_nodes
.iter()
.find(|node| node.widget == widget)
.and_then(|node| node.action_target)
}
pub fn target_at(&self, point: Point) -> Option<WidgetId> {
self.hit_target_at(point).map(|target| target.widget)
}
pub fn hit_target_at(&self, point: Point) -> Option<ViewHitTarget> {
self.hit_targets
.iter()
.rev()
.copied()
.find(|target| target.contains(point))
}
#[cfg(feature = "window-chrome")]
pub fn window_drag_region_at(&self, point: Point) -> bool {
self.window_drag_regions
.iter()
.any(|region| region.contains(point))
&& self.hit_target_at(point).is_none()
}
pub fn hit_target_for_widget(&self, widget: WidgetId) -> Option<ViewHitTarget> {
#[cfg(feature = "menu-flyout")]
if let Some(target) =
self.hit_targets.iter().copied().find(|target| {
target.widget == widget && target.kind == ViewHitTargetKind::MenuFlyout
})
{
return Some(target);
}
#[cfg(feature = "flyout")]
if let Some(target) = self
.hit_targets
.iter()
.copied()
.find(|target| target.widget == widget && target.kind == ViewHitTargetKind::Flyout)
{
return Some(target);
}
#[cfg(feature = "command-palette")]
if let Some(target) = self.hit_targets.iter().copied().find(|target| {
target.widget == widget && target.kind == ViewHitTargetKind::CommandPalette
}) {
return Some(target);
}
self.hit_targets
.iter()
.copied()
.find(|target| target.widget == widget)
}
#[cfg(feature = "tooltip")]
pub fn tooltip_target_at(&self, point: Point) -> Option<ViewTooltipTarget> {
self.tooltip_targets
.iter()
.rev()
.find(|target| target.contains(point))
.cloned()
}
#[cfg(feature = "tooltip")]
pub fn tooltip_for_widget(&self, widget: WidgetId) -> Option<ViewTooltipTarget> {
self.tooltip_targets
.iter()
.find(|target| target.widget == widget)
.cloned()
}
pub fn target_kind_at(&self, point: Point) -> Option<ViewHitTargetKind> {
self.hit_target_at(point).map(|target| target.kind)
}
#[cfg(feature = "combo")]
pub(crate) fn combo_visible_option_range(
&self,
widget: WidgetId,
) -> Option<std::ops::Range<usize>> {
let mut indices = self.hit_targets.iter().filter_map(|target| {
if target.widget != widget {
return None;
}
match target.kind {
ViewHitTargetKind::ComboBoxOption { index } => Some(index),
_ => None,
}
});
let first = indices.next()?;
let (minimum, maximum) = indices.fold((first, first), |(minimum, maximum), index| {
(minimum.min(index), maximum.max(index))
});
Some(minimum..maximum.saturating_add(1))
}
pub fn click_event_at(&self, point: Point) -> Option<ViewEvent> {
self.target_at(point)
.map(|widget| ViewEvent::Click { widget })
}
pub fn first_focus_target(&self) -> Option<ViewHitTarget> {
self.hit_targets
.iter()
.copied()
.find(|target| target.accepts_focus() && self.accepts_focus_scope(*target))
}
pub(crate) fn focus_target_for_widget(&self, widget: WidgetId) -> Option<ViewHitTarget> {
self.hit_targets.iter().copied().find(|target| {
target.widget == widget && target.accepts_focus() && self.accepts_focus_scope(*target)
})
}
pub(crate) fn modal_focus_target(&self) -> Option<ViewHitTarget> {
#[cfg(feature = "menu-flyout")]
if let Some(target) = self
.hit_targets
.iter()
.rev()
.copied()
.find(|target| target.kind == ViewHitTargetKind::MenuFlyout)
{
return Some(target);
}
#[cfg(feature = "flyout")]
if let Some((surface_index, surface)) = self
.hit_targets
.iter()
.enumerate()
.rev()
.find(|(_, target)| target.kind == ViewHitTargetKind::Flyout)
.map(|(index, target)| (index, *target))
{
return self
.hit_targets
.iter()
.skip(surface_index.saturating_add(1))
.copied()
.find(|target| {
target.kind != ViewHitTargetKind::FlyoutScrim
&& target.kind != ViewHitTargetKind::Flyout
&& target.kind != ViewHitTargetKind::Unknown
&& target.accepts_focus()
&& rect_contains_rect(surface.bounds, target.bounds)
})
.or(Some(surface));
}
#[cfg(feature = "command-palette")]
if let Some(target) = self
.hit_targets
.iter()
.rev()
.copied()
.find(|target| target.kind == ViewHitTargetKind::CommandPalette)
{
return Some(target);
}
#[cfg(feature = "dialog")]
if let Some(target) = self
.hit_targets
.iter()
.rev()
.copied()
.find(|target| target.kind == ViewHitTargetKind::ContentDialog)
{
return Some(target);
}
None
}
pub fn next_focus_target(
&self,
current: Option<WidgetId>,
offset: isize,
) -> Option<ViewHitTarget> {
let focus_targets = self
.hit_targets
.iter()
.copied()
.filter(|target| target.accepts_focus() && self.accepts_focus_scope(*target))
.collect::<Vec<_>>();
let len = focus_targets.len();
if len == 0 {
return None;
}
let current_index = current.and_then(|widget| {
focus_targets
.iter()
.position(|target| target.widget == widget)
});
let next_index = match current_index {
Some(index) => (index as isize + offset).rem_euclid(len as isize) as usize,
None if offset < 0 => len - 1,
None => 0,
};
focus_targets.get(next_index).copied()
}
pub(crate) fn next_focus_target_where(
&self,
current: Option<WidgetId>,
offset: isize,
mut accepts_tab_focus: impl FnMut(ViewHitTarget) -> bool,
) -> Option<ViewHitTarget> {
let len = self.hit_targets.len();
if len == 0 || offset == 0 {
return None;
}
let step = offset.signum();
let start = current
.and_then(|widget| {
self.hit_targets
.iter()
.position(|target| target.widget == widget)
})
.map(|index| index as isize)
.unwrap_or(if step < 0 { 0 } else { -1 });
(1..=len).find_map(|distance| {
let index = (start + step * distance as isize).rem_euclid(len as isize) as usize;
let target = self.hit_targets[index];
(target.accepts_focus()
&& self.accepts_focus_scope(target)
&& accepts_tab_focus(target))
.then_some(target)
})
}
fn accepts_focus_scope(&self, _target: ViewHitTarget) -> bool {
#[cfg(feature = "flyout")]
if let Some((surface_index, surface)) = self
.hit_targets
.iter()
.enumerate()
.rev()
.find(|(_, target)| target.kind == ViewHitTargetKind::Flyout)
.map(|(index, target)| (index, *target))
{
return _target.kind == ViewHitTargetKind::Flyout
|| (rect_contains_rect(surface.bounds, _target.bounds)
&& self.hit_targets[surface_index.saturating_add(1)..].contains(&_target));
}
if let Some(modal) = self.modal_focus_target() {
return _target.widget == modal.widget && _target.kind == modal.kind;
}
true
}
}
impl ViewHitTarget {
fn accepts_focus(&self) -> bool {
self.focus_behavior().is_tab_stop()
}
}
#[cfg(feature = "flyout")]
fn rect_contains_rect(outer: Rect, inner: Rect) -> bool {
inner.x >= outer.x
&& inner.y >= outer.y
&& inner.x.saturating_add(inner.width) <= outer.x.saturating_add(outer.width)
&& inner.y.saturating_add(inner.height) <= outer.y.saturating_add(outer.height)
}
impl ViewHitTargetKind {
pub const fn focus_behavior(self) -> ViewFocusBehavior {
match self {
#[cfg(feature = "resize")]
Self::ResizeHandle(_) => ViewFocusBehavior::TabStop,
Self::Unknown | Self::Button | Self::Textbox | Self::TextEditor => {
ViewFocusBehavior::TabStop
}
#[cfg(feature = "canvas")]
Self::Canvas => ViewFocusBehavior::TabStop,
#[cfg(feature = "label")]
Self::NavigationViewToggle => ViewFocusBehavior::TabStop,
#[cfg(feature = "toggle-button")]
Self::ToggleButton => ViewFocusBehavior::TabStop,
#[cfg(feature = "password-box")]
Self::PasswordBox => ViewFocusBehavior::TabStop,
Self::Checkbox | Self::Toggle => ViewFocusBehavior::TabStop,
#[cfg(feature = "slider")]
Self::Slider => ViewFocusBehavior::TabStop,
#[cfg(feature = "number-box")]
Self::NumberBox => ViewFocusBehavior::TabStop,
#[cfg(feature = "radio")]
Self::RadioButton => ViewFocusBehavior::TabStop,
#[cfg(feature = "auto-suggest")]
Self::AutoSuggestBox => ViewFocusBehavior::TabStop,
#[cfg(feature = "tree")]
Self::TreeView => ViewFocusBehavior::TabStop,
#[cfg(feature = "grid-view")]
Self::GridView => ViewFocusBehavior::TabStop,
#[cfg(feature = "table")]
Self::DataGrid => ViewFocusBehavior::TabStop,
#[cfg(feature = "dialog")]
Self::ContentDialog => ViewFocusBehavior::TabStop,
#[cfg(feature = "flyout")]
Self::Flyout => ViewFocusBehavior::TabStop,
#[cfg(feature = "menu-flyout")]
Self::MenuFlyout => ViewFocusBehavior::TabStop,
#[cfg(feature = "command-palette")]
Self::CommandPalette => ViewFocusBehavior::TabStop,
#[cfg(feature = "toast")]
Self::Toast => ViewFocusBehavior::TabStop,
#[cfg(feature = "teaching-tip")]
Self::TeachingTip => ViewFocusBehavior::TabStop,
#[cfg(feature = "info-bar")]
Self::InfoBar => ViewFocusBehavior::TabStop,
#[cfg(feature = "breadcrumb")]
Self::BreadcrumbBar => ViewFocusBehavior::TabStop,
#[cfg(feature = "combo")]
Self::ComboBox => ViewFocusBehavior::TabStop,
#[cfg(feature = "date-picker")]
Self::DatePicker => ViewFocusBehavior::TabStop,
#[cfg(feature = "time-picker")]
Self::TimePicker => ViewFocusBehavior::TabStop,
#[cfg(feature = "color-picker")]
Self::ColorPicker => ViewFocusBehavior::TabStop,
#[cfg(feature = "tabs")]
Self::Tab { .. } => ViewFocusBehavior::TabStop,
#[cfg(feature = "label")]
Self::NavigationViewScrim => ViewFocusBehavior::None,
#[cfg(feature = "split-view")]
Self::SplitViewScrim => ViewFocusBehavior::None,
#[cfg(feature = "scroll")]
Self::ScrollbarTrack | Self::ScrollbarThumb | Self::Scroll => ViewFocusBehavior::None,
#[cfg(feature = "virtual-list")]
Self::ItemsRepeaterScrollbarTrack | Self::ItemsRepeaterScrollbarThumb => {
ViewFocusBehavior::None
}
#[cfg(feature = "password-box")]
Self::PasswordBoxReveal => ViewFocusBehavior::None,
#[cfg(feature = "number-box")]
Self::NumberBoxDecrement | Self::NumberBoxIncrement => ViewFocusBehavior::None,
#[cfg(feature = "auto-suggest")]
Self::AutoSuggestSearch
| Self::AutoSuggestClear
| Self::AutoSuggestSuggestion { .. } => ViewFocusBehavior::None,
#[cfg(feature = "tree")]
Self::TreeNode { .. } | Self::TreeNodeExpander { .. } => ViewFocusBehavior::None,
#[cfg(feature = "grid-view")]
Self::GridViewItem { .. } => ViewFocusBehavior::None,
#[cfg(feature = "table")]
Self::TableHeader { .. } | Self::TableRow { .. } => ViewFocusBehavior::None,
#[cfg(feature = "dialog")]
Self::ContentDialogScrim | Self::ContentDialogButton { .. } => ViewFocusBehavior::None,
#[cfg(feature = "flyout")]
Self::FlyoutScrim => ViewFocusBehavior::None,
#[cfg(feature = "menu-flyout")]
Self::MenuFlyoutScrim | Self::MenuFlyoutItem { .. } => ViewFocusBehavior::None,
#[cfg(feature = "context-menu")]
Self::ContextMenuRegion => ViewFocusBehavior::None,
#[cfg(feature = "command-palette")]
Self::CommandPaletteScrim
| Self::CommandPaletteClear
| Self::CommandPaletteItem { .. } => ViewFocusBehavior::None,
#[cfg(feature = "toast")]
Self::ToastAction | Self::ToastClose => ViewFocusBehavior::None,
#[cfg(feature = "teaching-tip")]
Self::TeachingTipAction | Self::TeachingTipClose => ViewFocusBehavior::None,
#[cfg(feature = "info-bar")]
Self::InfoBarAction | Self::InfoBarClose => ViewFocusBehavior::None,
#[cfg(feature = "breadcrumb")]
Self::BreadcrumbOverflow
| Self::BreadcrumbItem { .. }
| Self::BreadcrumbOverflowItem { .. } => ViewFocusBehavior::None,
#[cfg(feature = "combo")]
Self::ComboBoxOption { .. } => ViewFocusBehavior::None,
#[cfg(feature = "date-picker")]
Self::DatePickerDay { .. }
| Self::DatePickerPreviousMonth
| Self::DatePickerNextMonth => ViewFocusBehavior::None,
#[cfg(feature = "time-picker")]
Self::TimePickerChoice { .. } => ViewFocusBehavior::None,
#[cfg(feature = "color-picker")]
Self::ColorPickerPopup
| Self::ColorPickerSpectrum
| Self::ColorPickerHue
| Self::ColorPickerChannel { .. } => ViewFocusBehavior::None,
}
}
#[cfg(all(target_os = "linux", feature = "linux-direct-accessibility"))]
pub(crate) fn accepts_text_input(self) -> bool {
let accepts = matches!(self, Self::Textbox | Self::TextEditor);
#[cfg(feature = "password-box")]
let accepts = accepts || self == Self::PasswordBox;
#[cfg(feature = "number-box")]
let accepts = accepts || self == Self::NumberBox;
#[cfg(feature = "auto-suggest")]
let accepts = accepts || self == Self::AutoSuggestBox;
#[cfg(feature = "command-palette")]
let accepts = accepts || self == Self::CommandPalette;
accepts
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ViewLayoutCx {
pub bounds: Rect,
pub dpi: Dpi,
pub(crate) typography_scale_per_mille: u16,
pub(crate) text_measurements: Arc<ViewTextMeasurements>,
depth: usize,
}
impl ViewLayoutCx {
pub fn new(bounds: Rect, dpi: Dpi) -> Self {
Self {
bounds,
dpi,
typography_scale_per_mille: crate::render_protocol::default_typography_scale_per_mille(
),
text_measurements: Arc::new(ViewTextMeasurements::default()),
depth: 0,
}
}
pub(crate) fn child(&self, bounds: Rect) -> Self {
Self {
bounds,
dpi: self.dpi,
typography_scale_per_mille: self.typography_scale_per_mille,
text_measurements: self.text_measurements.clone(),
depth: self.depth.saturating_add(1),
}
}
pub(crate) const fn is_root(&self) -> bool {
self.depth == 0
}
pub(crate) fn with_typography_scale(mut self, scale: f32) -> Self {
self.typography_scale_per_mille =
crate::render_protocol::normalize_typography_scale_per_mille(scale);
self
}
pub(crate) fn with_text_measurements(
mut self,
measurements: Arc<ViewTextMeasurements>,
) -> Self {
self.text_measurements = measurements;
self
}
pub(crate) fn typography_scale(&self) -> f32 {
f32::from(self.typography_scale_per_mille) / 1_000.0
}
}
#[derive(Debug, Clone)]
pub struct ViewEventCx<Msg> {
messages: Vec<Msg>,
}
impl<Msg> ViewEventCx<Msg> {
pub fn new() -> Self {
Self {
messages: Vec::new(),
}
}
pub fn emit(&mut self, message: Msg) {
self.messages.push(message);
}
pub fn messages(&self) -> &[Msg] {
&self.messages
}
pub fn into_messages(self) -> Vec<Msg> {
self.messages
}
}
impl<Msg> Default for ViewEventCx<Msg> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct AppCx {
allowance: crate::dispatch_queue::DispatchAllowance,
rejected: usize,
commands: Vec<Command>,
ui_commands: Vec<UiCommand>,
effects: Vec<AppEffectRequest>,
#[cfg(feature = "text-input-core")]
text_edit_commands: Vec<ZsTextEditCommandRequest>,
quit_requested: bool,
focus_request: Option<WidgetId>,
#[cfg(feature = "window-chrome")]
window_commands: Vec<crate::ZsWindowCommand>,
}
impl AppCx {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn with_allowance(allowance: crate::dispatch_queue::DispatchAllowance) -> Self {
Self {
allowance,
..Self::default()
}
}
fn admit(
&mut self,
queue: &'static str,
len: usize,
capacity: usize,
) -> Result<(), crate::QueueFull> {
if len < capacity {
Ok(())
} else {
self.rejected = self.rejected.saturating_add(1);
Err(crate::QueueFull { queue, capacity })
}
}
pub fn rejected_count(&self) -> usize {
self.rejected
}
pub fn try_command(&mut self, command: Command) -> Result<(), crate::QueueFull> {
self.admit(
"application command",
self.commands.len(),
self.allowance.commands,
)?;
self.commands.push(command);
Ok(())
}
pub fn command(&mut self, command: Command) {
let _ = self.try_command(command);
}
pub fn try_ui_command(&mut self, command: UiCommand) -> Result<(), crate::QueueFull> {
self.admit(
"UI command",
self.ui_commands.len(),
self.allowance.ui_commands,
)?;
self.ui_commands.push(command);
Ok(())
}
pub fn ui_command(&mut self, command: UiCommand) {
let _ = self.try_ui_command(command);
}
pub fn try_effect_request(
&mut self,
request: AppEffectRequest,
) -> Result<crate::AppEffectCancellation, crate::QueueFull> {
self.admit("native effect", self.effects.len(), self.allowance.effects)?;
let cancellation = request.cancellation();
self.effects.push(request);
Ok(cancellation)
}
pub fn try_effect_with<Msg: Send + 'static>(
&mut self,
effect: AppEffect,
respond: impl Fn(AppEffectOutcome) -> Msg + Send + Sync + 'static,
) -> Result<crate::AppEffectCancellation, crate::QueueFull> {
self.try_effect_request(AppEffectRequest::typed(effect, move |outcome| {
Some(respond(outcome))
}))
}
pub fn effect(&mut self, effect: AppEffect) {
let _ = self.try_effect_request(AppEffectRequest::new(effect));
}
pub fn effect_with<Msg>(
&mut self,
effect: AppEffect,
respond: impl Fn(AppEffectOutcome) -> Msg + Send + Sync + 'static,
) where
Msg: Send + 'static,
{
let _ = self.try_effect_request(AppEffectRequest::typed(effect, move |outcome| {
Some(respond(outcome))
}));
}
pub fn open_file_dialog<Msg>(
&mut self,
spec: FileDialogSpec,
respond: impl Fn(ZsuiResult<Option<Vec<PathBuf>>>) -> Msg + Send + Sync + 'static,
) where
Msg: Send + 'static,
{
let _ = self.try_effect_request(AppEffectRequest::typed(
AppEffect::OpenFileDialog(spec),
move |outcome| match outcome {
AppEffectOutcome::OpenFileDialog(result) => Some(respond(result)),
_ => None,
},
));
}
pub fn save_file_dialog<Msg>(
&mut self,
spec: SaveFileDialogSpec,
respond: impl Fn(ZsuiResult<Option<PathBuf>>) -> Msg + Send + Sync + 'static,
) where
Msg: Send + 'static,
{
let _ = self.try_effect_request(AppEffectRequest::typed(
AppEffect::SaveFileDialog(spec),
move |outcome| match outcome {
AppEffectOutcome::SaveFileDialog(result) => Some(respond(result)),
_ => None,
},
));
}
pub fn pick_directory<Msg>(
&mut self,
spec: DirectoryDialogSpec,
respond: impl Fn(ZsuiResult<Option<PathBuf>>) -> Msg + Send + Sync + 'static,
) where
Msg: Send + 'static,
{
let _ = self.try_effect_request(AppEffectRequest::typed(
AppEffect::PickDirectory(spec),
move |outcome| match outcome {
AppEffectOutcome::PickDirectory(result) => Some(respond(result)),
_ => None,
},
));
}
pub fn show_dialog<Msg>(
&mut self,
spec: NativeDialogSpec,
respond: impl Fn(ZsuiResult<DialogResponse>) -> Msg + Send + Sync + 'static,
) where
Msg: Send + 'static,
{
let _ = self.try_effect_request(AppEffectRequest::typed(
AppEffect::ShowDialog(spec),
move |outcome| match outcome {
AppEffectOutcome::ShowDialog(result) => Some(respond(result)),
_ => None,
},
));
}
pub fn try_open_file_dialog<Msg: Send + 'static>(
&mut self,
spec: FileDialogSpec,
respond: impl Fn(ZsuiResult<Option<Vec<PathBuf>>>) -> Msg + Send + Sync + 'static,
) -> Result<crate::AppEffectCancellation, crate::QueueFull> {
self.try_effect_request(AppEffectRequest::typed(
AppEffect::OpenFileDialog(spec),
move |outcome| match outcome {
AppEffectOutcome::OpenFileDialog(result) => Some(respond(result)),
_ => None,
},
))
}
pub fn try_save_file_dialog<Msg: Send + 'static>(
&mut self,
spec: SaveFileDialogSpec,
respond: impl Fn(ZsuiResult<Option<PathBuf>>) -> Msg + Send + Sync + 'static,
) -> Result<crate::AppEffectCancellation, crate::QueueFull> {
self.try_effect_request(AppEffectRequest::typed(
AppEffect::SaveFileDialog(spec),
move |outcome| match outcome {
AppEffectOutcome::SaveFileDialog(result) => Some(respond(result)),
_ => None,
},
))
}
pub fn try_pick_directory<Msg: Send + 'static>(
&mut self,
spec: DirectoryDialogSpec,
respond: impl Fn(ZsuiResult<Option<PathBuf>>) -> Msg + Send + Sync + 'static,
) -> Result<crate::AppEffectCancellation, crate::QueueFull> {
self.try_effect_request(AppEffectRequest::typed(
AppEffect::PickDirectory(spec),
move |outcome| match outcome {
AppEffectOutcome::PickDirectory(result) => Some(respond(result)),
_ => None,
},
))
}
pub fn try_show_dialog<Msg: Send + 'static>(
&mut self,
spec: NativeDialogSpec,
respond: impl Fn(ZsuiResult<DialogResponse>) -> Msg + Send + Sync + 'static,
) -> Result<crate::AppEffectCancellation, crate::QueueFull> {
self.try_effect_request(AppEffectRequest::typed(
AppEffect::ShowDialog(spec),
move |outcome| match outcome {
AppEffectOutcome::ShowDialog(result) => Some(respond(result)),
_ => None,
},
))
}
#[cfg(feature = "text-input-core")]
pub fn text_edit_command(&mut self, command: ZsTextEditCommand) {
let _ = self.try_text_edit_command(ZsTextEditCommandRequest::focused(command));
}
#[cfg(feature = "text-input-core")]
pub fn text_edit_command_for(&mut self, widget: WidgetId, command: ZsTextEditCommand) {
let _ = self.try_text_edit_command(ZsTextEditCommandRequest::for_widget(widget, command));
}
#[cfg(feature = "text-input-core")]
pub fn try_text_edit_command(
&mut self,
request: ZsTextEditCommandRequest,
) -> Result<(), crate::QueueFull> {
self.admit(
"text edit",
self.text_edit_commands.len(),
crate::DISPATCH_BATCH_SIZE,
)?;
self.text_edit_commands.push(request);
Ok(())
}
pub fn quit(&mut self) {
self.quit_requested = true;
}
pub fn focus(&mut self, widget: WidgetId) {
self.focus_request = Some(widget);
}
pub const fn focus_request(&self) -> Option<WidgetId> {
self.focus_request
}
#[cfg(feature = "window-chrome")]
pub fn window_command(&mut self, command: crate::ZsWindowCommand) {
let _ = self.try_window_command(command);
}
#[cfg(feature = "window-chrome")]
pub fn try_window_command(&mut self, command: crate::ZsWindowCommand) -> Result<(), crate::QueueFull> {
self.admit("window command", self.window_commands.len(), crate::DISPATCH_BATCH_SIZE)?;
self.window_commands.push(command);
Ok(())
}
#[cfg(feature = "window-chrome")]
pub fn window_commands(&self) -> &[crate::ZsWindowCommand] {
&self.window_commands
}
pub fn commands(&self) -> &[Command] {
&self.commands
}
pub fn ui_commands(&self) -> &[UiCommand] {
&self.ui_commands
}
pub fn effects(&self) -> &[AppEffectRequest] {
&self.effects
}
#[cfg(feature = "text-input-core")]
pub fn text_edit_commands(&self) -> &[ZsTextEditCommandRequest] {
&self.text_edit_commands
}
pub const fn quit_requested(&self) -> bool {
self.quit_requested
}
}