use crate::application_types::{
ApplicationLifecycle, LifecycleTransition, RuntimeErrorReport, WindowError,
};
#[cfg(feature = "devtools")]
use crate::environment::DEV_TASK_COMPLETION;
#[cfg(feature = "devtools")]
use crate::environment::ReactiveRootId;
use crate::environment::{
BuildScope, BuildScopeGuard, InitialBuildDependencies, ReactiveQueue, environment_change_mask,
install_focus_scope_watch,
};
use crate::profiling;
use crate::profiling::FrameTimings;
use crate::tasks::{
self, RuntimeDiagnostics, RuntimeSpawner, RuntimeWake, Task, TaskFailure, TaskHandle,
TaskScope, TokioHandle, UiDispatcher,
};
use crate::undo::UndoHistoryController;
use crate::window_commands::{WindowHandle, WindowOpener};
use crate::window_state::WindowManager;
use incular_config::{Constraints, RuntimeEnvironment};
use incular_core::{
Code, ImeEvent, InputEvent, KeyboardEvent, Modifiers, Offset, PRIMARY_POINTER_BUTTON,
PointerPhase, PointerSampleMetadata, Rect,
};
use incular_platform::{
Clipboard, ClipboardCapabilities, ClipboardError, ClipboardWriteReport, DataTransfer,
ExternalDragEvent, ExternalDragResponse, MemoryClipboard, PlatformEvent, PlatformLifecycle,
TextInputAction, TextInputClientId, TextInputCommand, TextInputConfiguration, TextInputState,
TextInputType, TransferReadRequest, WindowId, WindowMetrics, WindowOperation, WindowOptions,
};
use incular_rendering::DisplayList;
use incular_semantics::{SemanticAction, SemanticNodeId};
#[cfg(feature = "devtools")]
use incular_widgets::internal::InvalidationCause;
use incular_widgets::internal::{
ActionId, Diagnostics, ElementId, MouseCursor, PlatformMenuBinding, PointerDeviceKind,
PointerEvent, RawPointerEvent, TextEditingController, TextRange, TextSelection, TreeError,
WidgetTree, WindowInteraction,
};
use incular_widgets::{TextInputActionHint, TextInputTypeHint, Widget};
use std::{
cell::{Cell, RefCell},
collections::{HashMap, VecDeque},
rc::{Rc, Weak},
sync::atomic::{AtomicU64, Ordering},
time::Instant,
};
static RUNTIME_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FrameStats {
pub updated_elements: usize,
pub rebuilt_elements: u64,
pub laid_out_render_objects: u64,
pub repainted_render_objects: u64,
pub composited: u64,
pub active_animations: u64,
pub display_list_commands: usize,
pub requested_another_frame: bool,
pub timings: FrameTimings,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EventTarget {
pub element: ElementId,
pub action: Option<ActionId>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FocusDiagnostics {
pub focused_element: Option<ElementId>,
pub text_pointer_capture: Option<ElementId>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EditingDiagnostics {
pub key_down_received: u64,
pub key_up_received: u64,
pub backspace_commands: u64,
pub delete_commands: u64,
pub text_commits: u64,
pub ime_events: u64,
}
struct ImeCompositionOwner {
field: ElementId,
controller: TextEditingController,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum InitialFocus {
AwaitingLayout,
Settled,
}
pub struct Runtime {
pub(crate) tree: WidgetTree,
pub(crate) pending_event_processing_us: u32,
pub(crate) pending: HashMap<ElementId, Widget>,
pub(crate) order: VecDeque<ElementId>,
pub(crate) reactive: Rc<RefCell<ReactiveQueue>>,
pub(crate) builders: HashMap<ElementId, Box<dyn FnMut() -> Widget>>,
pub(crate) handlers: HashMap<ActionId, Rc<dyn Fn()>>,
handlers_stale: bool,
pub(crate) legacy_action_handler: Option<Box<dyn FnMut(ActionId)>>,
pub(crate) hovered_button: Option<ElementId>,
pub(crate) pressed_button: Option<ElementId>,
pub(crate) legacy_pointer: Option<(u64, u64)>,
pub(crate) last_pointer: Offset,
pub(crate) pointer_inside: bool,
pub(crate) pointer_position_known: bool,
pub(crate) focused: Option<ElementId>,
initial_focus: InitialFocus,
pub(crate) captured_text_field: Option<ElementId>,
pub(crate) captured_selectable_text: Option<ElementId>,
ime_composition: Option<ImeCompositionOwner>,
pub(crate) text_histories: HashMap<ElementId, UndoHistoryController>,
pub(crate) text_input_commands: VecDeque<TextInputCommand>,
pub(crate) text_input_client: Option<TextInputClientId>,
pub(crate) text_input_configuration: Option<TextInputConfiguration>,
pub(crate) text_input_state: Option<TextInputState>,
pub(crate) text_input_caret_rect: Option<Rect>,
pub(crate) clipboard: Box<dyn Clipboard>,
pub(crate) editing_diagnostics: EditingDiagnostics,
pub(crate) frame_requested: bool,
pub(crate) scheduler: Rc<RefCell<tasks::TaskScheduler>>,
pub(crate) environment: Rc<RefCell<RuntimeEnvironment>>,
pub(crate) lifecycle: ApplicationLifecycle,
pub(crate) lifecycle_observers: Vec<Rc<dyn Fn(LifecycleTransition)>>,
pub(crate) error_observers: Vec<Rc<dyn Fn(RuntimeErrorReport)>>,
pub(crate) environment_generation: u64,
pub(crate) environment_dependencies: Rc<Cell<u16>>,
pub(crate) application_root: Option<ElementId>,
pub(crate) owner_scopes: HashMap<ElementId, TaskScope>,
pub(crate) window_id: Option<WindowId>,
pub(crate) window_scope: TaskScope,
pub(crate) window_manager: Option<WindowManager>,
driver_domain: Rc<u64>,
}
pub(crate) fn diagnostic_input_trigger(event: &InputEvent) -> String {
match event {
InputEvent::Pointer { phase, position } => {
format!("pointer({phase:?}, {:.1}, {:.1})", position.x, position.y)
}
InputEvent::PointerWithId {
pointer,
phase,
position,
} => format!(
"pointer({pointer}, {phase:?}, {:.1}, {:.1})",
position.x, position.y
),
InputEvent::PointerWithMetadata {
pointer,
device,
kind,
buttons,
button,
sample: _,
phase,
position,
} => format!(
"pointer({pointer}, device={device}, {kind:?}, buttons={buttons:#x}, changed={button:?}, {phase:?}, {:.1}, {:.1})",
position.x, position.y
),
InputEvent::TrackpadGesture(event) => format!(
"trackpad(device={}, kind={:?}, phase={:?})",
event.device(),
event.kind(),
event.phase()
),
InputEvent::Scroll { delta } => format!("scroll({:.1}, {:.1})", delta.x, delta.y),
InputEvent::Key(event) => format!("key({:?}, {:?})", event.state, event.code),
InputEvent::Text(text) => format!("text_input(chars={})", text.chars().count()),
InputEvent::Ime(ImeEvent::Preedit { text, .. }) => {
format!("ime_preedit(chars={})", text.chars().count())
}
InputEvent::Ime(ImeEvent::Commit(text)) => {
format!("ime_commit(chars={})", text.chars().count())
}
InputEvent::Ime(ImeEvent::End) => "ime_end".to_owned(),
InputEvent::WindowResized { size, scale_factor } => format!(
"window_resize({:.1}x{:.1}, scale={scale_factor:.2})",
size.width, size.height
),
}
}
impl Runtime {
pub fn new(root: Widget) -> Result<Self, TreeError> {
Self::with_scheduler(root, tasks::TaskScheduler::new())
}
fn with_scheduler(
root: Widget,
scheduler: Rc<RefCell<tasks::TaskScheduler>>,
) -> Result<Self, TreeError> {
let window_scope = tasks::TaskScheduler::spawner(&scheduler).scope();
Self::with_window(root, scheduler, None, window_scope, None)
}
pub(crate) fn with_window(
root: Widget,
scheduler: Rc<RefCell<tasks::TaskScheduler>>,
window_id: Option<WindowId>,
window_scope: TaskScope,
window_manager: Option<WindowManager>,
) -> Result<Self, TreeError> {
Self::with_window_and_reactive(
root,
scheduler,
window_id,
window_scope,
window_manager,
ReactiveQueue::new(),
)
}
pub(crate) fn with_window_and_reactive(
mut root: Widget,
scheduler: Rc<RefCell<tasks::TaskScheduler>>,
window_id: Option<WindowId>,
window_scope: TaskScope,
window_manager: Option<WindowManager>,
reactive: Rc<RefCell<ReactiveQueue>>,
) -> Result<Self, TreeError> {
let mut tree = WidgetTree::new();
let mut handlers = HashMap::new();
root.bind_callbacks(&mut |callback| {
let action = tree.allocate_action();
handlers.insert(action, callback);
action
});
tree.mount(root)?;
for (action, handler) in tree.take_pending_handlers() {
handlers.insert(action, handler);
}
let mut runtime = Self {
pending_event_processing_us: 0,
tree,
pending: HashMap::new(),
order: VecDeque::new(),
reactive,
builders: HashMap::new(),
handlers,
handlers_stale: false,
legacy_action_handler: None,
hovered_button: None,
pressed_button: None,
legacy_pointer: None,
last_pointer: Offset::ZERO,
pointer_inside: false,
pointer_position_known: false,
focused: None,
initial_focus: InitialFocus::AwaitingLayout,
captured_text_field: None,
captured_selectable_text: None,
ime_composition: None,
text_histories: HashMap::new(),
text_input_commands: VecDeque::new(),
text_input_client: None,
text_input_configuration: None,
text_input_state: None,
text_input_caret_rect: None,
clipboard: Box::new(MemoryClipboard::default()),
editing_diagnostics: EditingDiagnostics::default(),
frame_requested: true,
scheduler,
environment: Rc::new(RefCell::new(RuntimeEnvironment::default())),
lifecycle: ApplicationLifecycle::Starting,
lifecycle_observers: Vec::new(),
error_observers: Vec::new(),
environment_generation: 0,
environment_dependencies: Rc::new(Cell::new(0)),
application_root: None,
owner_scopes: HashMap::new(),
window_id,
window_scope,
window_manager,
driver_domain: Rc::new(
RUNTIME_SEQUENCE
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
.expect("runtime driver identity space exhausted"),
),
};
if let Some(autofocus) = runtime.tree.autofocus_element() {
runtime.set_focus(Some(autofocus));
}
Ok(runtime)
}
#[must_use]
pub fn id(&self) -> u64 {
*self.driver_domain
}
pub(crate) fn driver_token(&self) -> Weak<u64> {
Rc::downgrade(&self.driver_domain)
}
#[must_use]
pub fn spawner(&self) -> RuntimeSpawner {
self.window_id.map_or_else(
|| tasks::TaskScheduler::spawner(&self.scheduler),
|window_id| tasks::TaskScheduler::spawner_for(&self.scheduler, window_id),
)
}
#[must_use]
pub fn dispatcher(&self) -> UiDispatcher {
self.scheduler.borrow().dispatcher_for(self.window_id)
}
#[must_use]
pub fn tokio_handle(&self) -> TokioHandle {
self.scheduler.borrow().tokio_handle()
}
pub fn set_wake_handler(&mut self, wake: std::sync::Arc<dyn RuntimeWake>) {
self.scheduler.borrow_mut().set_wake(wake);
}
#[must_use]
pub const fn window_id(&self) -> Option<WindowId> {
self.window_id
}
#[doc(hidden)]
#[must_use]
pub fn platform_menu_bindings(&self) -> Vec<PlatformMenuBinding> {
self.tree.platform_menu_bindings()
}
pub fn request_frame(&mut self) {
self.frame_requested = true;
}
pub fn take_text_input_commands(&mut self) -> Vec<TextInputCommand> {
self.text_input_commands.drain(..).collect()
}
pub fn open_window(
&mut self,
options: WindowOptions,
root: Widget,
) -> Result<WindowHandle, WindowError> {
self.window_manager
.as_ref()
.ok_or(WindowError::ApplicationStopped)?
.open_window(options, root)
}
#[must_use]
pub fn window_opener(&self) -> Option<WindowOpener> {
self.window_manager
.as_ref()
.cloned()
.map(|manager| WindowOpener { manager })
}
#[must_use]
pub fn window_handle(&self) -> Option<WindowHandle> {
let id = self.window_id?;
self.window_manager.as_ref()?.handle(id)
}
#[must_use]
pub fn window_task_scope(&self) -> TaskScope {
self.window_scope.clone()
}
pub fn spawn_blocking<T>(
&self,
work: impl FnOnce() -> T + Send + 'static,
complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
) -> TaskHandle
where
T: Send + 'static,
{
self.spawner()
.spawn_blocking_in(&self.window_scope, work, complete)
}
pub fn spawn<F, T>(&self, future: F) -> Task<T>
where
F: std::future::Future<Output = T> + Send + 'static,
T: Send + 'static,
{
self.spawner().spawn_in(&self.window_scope, future)
}
pub fn spawn_into<F, T>(
&self,
future: F,
complete: impl FnOnce(Result<T, TaskFailure>, &mut Runtime) + 'static,
) -> TaskHandle
where
F: std::future::Future<Output = T> + Send + 'static,
T: Send + 'static,
{
self.spawner()
.spawn_into_in(&self.window_scope, future, complete)
}
pub fn process_runtime_work_at(&mut self, _now: Instant) {
let scheduler = self.scheduler.clone();
let work = scheduler.borrow_mut().take_turn();
for work in work {
self.process_ui_work(&scheduler, work);
}
if self.reactive.borrow().has_work() || !self.pending.is_empty() {
self.frame_requested = true;
}
}
pub(crate) fn process_ui_work(
&mut self,
scheduler: &Rc<RefCell<tasks::TaskScheduler>>,
work: tasks::UiWork,
) {
let mut failures = Vec::new();
match work {
tasks::UiWork::Dispatch { callback, .. } => {
if scheduler.borrow().accepting()
&& std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(self)))
.is_err()
{
failures.push(TaskFailure::Panicked);
}
}
tasks::UiWork::Task(pending) => {
scheduler.borrow().detach_scope(&pending.control);
let stale_owner = pending.control.owner_is_stale(self);
if !scheduler.borrow().accepting() || pending.control.is_cancelled() || stale_owner
{
(pending.discard)();
scheduler
.borrow_mut()
.record_discard(pending.blocking, stale_owner);
} else {
#[cfg(feature = "devtools")]
let previous_task_context = DEV_TASK_COMPLETION.with(|context| {
let previous = context.get();
context.set(true);
previous
});
let failure = (pending.finish)(self);
#[cfg(feature = "devtools")]
DEV_TASK_COMPLETION.with(|context| context.set(previous_task_context));
if let Some(failure) = scheduler
.borrow_mut()
.record_completion(pending.blocking, failure)
{
failures.push(failure);
}
}
}
}
for failure in failures {
for observer in &self.error_observers {
observer(RuntimeErrorReport {
failure: failure.clone(),
});
}
}
if self.reactive.borrow().has_work() || !self.pending.is_empty() {
self.frame_requested = true;
}
}
pub fn process_runtime_work(&mut self) {
self.process_runtime_work_at(Instant::now());
}
#[must_use]
pub fn runtime_diagnostics(&self) -> RuntimeDiagnostics {
self.scheduler.borrow().diagnostics()
}
#[must_use]
pub fn debug_dump(&self) -> String {
let diagnostics = self.runtime_diagnostics();
let (application_tasks, owner_tasks) = self.scheduler.borrow().active_ownership();
format!(
"Lifecycle: {:?}\nTokio tasks tracked: {}\n application: {}\n owner-scoped: {}\nPending UI messages: {}\nEnvironment generation: {}",
self.lifecycle,
diagnostics.active_tracked_tasks,
application_tasks,
owner_tasks,
diagnostics
.ui_messages_enqueued
.saturating_sub(diagnostics.ui_messages_processed),
self.environment_generation,
)
}
#[must_use]
pub fn environment(&self) -> RuntimeEnvironment {
self.environment.borrow().clone()
}
#[must_use]
pub const fn lifecycle(&self) -> ApplicationLifecycle {
self.lifecycle
}
#[must_use]
pub const fn environment_generation(&self) -> u64 {
self.environment_generation
}
pub fn set_environment(&mut self, environment: RuntimeEnvironment) -> bool {
let environment = environment.normalized();
let previous = self.environment.borrow().clone();
if previous == environment {
return false;
}
let changed = environment_change_mask(&previous, &environment);
*self.environment.borrow_mut() = environment.clone();
let retained_environment_dirty = self.tree.set_environment(environment);
self.environment_generation = self
.environment_generation
.checked_add(1)
.expect("runtime environment generation exhausted");
if retained_environment_dirty {
self.frame_requested = true;
}
if self.environment_dependencies.get() & changed != 0
&& let Some(root) = self.application_root
{
#[cfg(feature = "devtools")]
self.tree
.note_invalidation(root, InvalidationCause::EnvironmentChanged);
let _ = self.rebuild_from_builder(root);
self.frame_requested = true;
}
true
}
pub fn transition_lifecycle(&mut self, lifecycle: ApplicationLifecycle) -> bool {
if self.lifecycle == lifecycle {
return false;
}
let transition = LifecycleTransition {
previous: self.lifecycle,
current: lifecycle,
};
self.lifecycle = lifecycle;
for observer in &self.lifecycle_observers {
observer(transition);
}
if matches!(
lifecycle,
ApplicationLifecycle::Stopping | ApplicationLifecycle::Terminated
) {
self.reactive.borrow_mut().clear();
self.scheduler.borrow_mut().shutdown();
}
true
}
pub fn observe_lifecycle(&mut self, observer: impl Fn(LifecycleTransition) + 'static) {
self.lifecycle_observers.push(Rc::new(observer));
}
pub fn observe_runtime_error(&mut self, observer: impl Fn(RuntimeErrorReport) + 'static) {
self.error_observers.push(Rc::new(observer));
}
pub fn shutdown(&mut self) {
if self.focused.is_some() {
self.set_focus(None);
} else {
self.cancel_ime_composition();
self.sync_text_input_client();
}
self.tree.cancel_all_gesture_streams();
self.captured_text_field = None;
self.captured_selectable_text = None;
self.hovered_button = None;
self.pressed_button = None;
self.legacy_pointer = None;
self.text_histories.clear();
self.tree.release_edit_subscriptions();
let _ = self.transition_lifecycle(ApplicationLifecycle::Stopping);
for (_, scope) in self.owner_scopes.drain() {
scope.cancel();
}
self.scheduler.borrow_mut().shutdown();
self.lifecycle = ApplicationLifecycle::Terminated;
}
pub(crate) fn dispose_window(&mut self) {
if self.focused.is_some() {
self.set_focus(None);
} else {
self.cancel_ime_composition();
self.sync_text_input_client();
}
self.tree.cancel_all_gesture_streams();
self.tree.release_edit_subscriptions();
self.legacy_pointer = None;
self.window_scope.cancel();
for (_, scope) in self.owner_scopes.drain() {
scope.cancel();
}
self.reactive.borrow_mut().clear();
self.builders.clear();
self.pending.clear();
self.order.clear();
self.handlers.clear();
self.legacy_action_handler = None;
self.text_histories.clear();
self.text_input_commands.clear();
self.text_input_client = None;
self.text_input_configuration = None;
self.text_input_state = None;
self.text_input_caret_rect = None;
self.captured_text_field = None;
self.captured_selectable_text = None;
self.hovered_button = None;
self.pressed_button = None;
}
#[must_use]
pub fn tree(&self) -> &WidgetTree {
&self.tree
}
#[cfg(feature = "devtools")]
pub(crate) fn devtools_reactive_root(&self) -> ReactiveRootId {
self.reactive.borrow().root
}
#[must_use]
pub fn tree_mut(&mut self) -> &mut WidgetTree {
&mut self.tree
}
#[must_use]
pub fn content_sensitivity(&self) -> incular_config::ContentSensitivity {
self.tree.content_sensitivity()
}
#[must_use]
pub fn transient_surfaces(&self) -> Vec<incular_widgets::TransientSurfaceSnapshot> {
self.tree.transient_surfaces()
}
pub fn set_native_transient_bounds(&mut self, bounds: Option<incular_core::Rect>) -> bool {
let changed = self.tree.set_native_transient_bounds(bounds);
self.frame_requested |= changed;
changed
}
pub fn set_native_transient_presentations(
&mut self,
ids: impl IntoIterator<Item = incular_widgets::TransientSurfaceId>,
) -> bool {
let changed = self.tree.set_native_transient_presentations(ids);
self.frame_requested |= changed;
changed
}
pub fn dismiss_transients(&mut self, reason: incular_widgets::TransientDismissReason) -> bool {
let dismissed = self.tree.dismiss_transients(reason) > 0;
self.frame_requested |= dismissed;
dismissed
}
#[cfg(feature = "devtools")]
pub(crate) fn devtools_edit_property(
&mut self,
id: incular_devtools_protocol::DevWidgetId,
name: &str,
value: &incular_devtools_protocol::DebugValue,
) -> bool {
let edited = self.tree.devtools_edit_property(id, name, value);
self.frame_requested |= edited;
edited
}
pub fn set_animation_time_scale(&mut self, scale: f32) {
self.tree.set_animation_time_scale(scale);
}
#[must_use]
pub fn focused_element(&self) -> Option<ElementId> {
self.focused
}
pub fn dispatch_semantic_action(
&mut self,
node: SemanticNodeId,
action: SemanticAction,
) -> bool {
let Some(element) = self.tree.element_for_semantic_node(node) else {
return false;
};
self.tree.note_semantic_action();
let handled = match action {
SemanticAction::Focus => {
if self.tree.can_request_focus(element) {
self.set_focus(Some(element));
true
} else {
false
}
}
SemanticAction::Activate => {
if let Some(callback) = self.tree.semantic_action_callback(
element,
incular_semantics::SemanticActionKind::Activate,
) {
callback();
true
} else if self.tree.semantic_activate_executable(element) {
self.tree
.action_for_element(element)
.map(|action| self.activate_action(action))
.unwrap_or(false)
} else {
false
}
}
SemanticAction::SetText(text) => {
if !self.tree.text_field_is_editable(element) {
false
} else {
self.tree
.text_controller(element)
.map(|controller| {
controller.set_text(text);
true
})
.unwrap_or(false)
}
}
SemanticAction::SetSelection { base, extent } => self
.tree
.text_controller(element)
.map(|controller| {
let length = controller.text().len();
controller.set_selection(TextSelection {
base: base.min(length),
extent: extent.min(length),
});
true
})
.unwrap_or(false),
SemanticAction::ScrollForward => self
.tree
.semantic_action_callback(
element,
incular_semantics::SemanticActionKind::ScrollForward,
)
.map(|callback| {
callback();
true
})
.unwrap_or_else(|| self.tree.semantic_scroll(element, true)),
SemanticAction::ScrollBackward => self
.tree
.semantic_action_callback(
element,
incular_semantics::SemanticActionKind::ScrollBackward,
)
.map(|callback| {
callback();
true
})
.unwrap_or_else(|| self.tree.semantic_scroll(element, false)),
SemanticAction::Increment => self
.tree
.semantic_action_callback(element, incular_semantics::SemanticActionKind::Increment)
.map(|callback| {
callback();
true
})
.unwrap_or_else(|| self.tree.dispatch_semantic_increment(element, true)),
SemanticAction::Decrement => self
.tree
.semantic_action_callback(element, incular_semantics::SemanticActionKind::Decrement)
.map(|callback| {
callback();
true
})
.unwrap_or_else(|| self.tree.dispatch_semantic_increment(element, false)),
};
if handled {
self.frame_requested = true;
}
handled
}
#[must_use]
pub fn focus_diagnostics(&self) -> FocusDiagnostics {
FocusDiagnostics {
focused_element: self.focused,
text_pointer_capture: self.captured_text_field,
}
}
#[must_use]
pub const fn editing_diagnostics(&self) -> EditingDiagnostics {
self.editing_diagnostics
}
pub fn set_clipboard(&mut self, clipboard: Box<dyn Clipboard>) {
self.clipboard = clipboard;
}
#[must_use]
pub fn clipboard_capabilities(&self) -> ClipboardCapabilities {
self.clipboard.capabilities()
}
pub fn read_clipboard(
&mut self,
request: TransferReadRequest,
) -> Result<DataTransfer, ClipboardError> {
self.clipboard.read(request)
}
pub fn write_clipboard(
&mut self,
transfer: DataTransfer,
) -> Result<ClipboardWriteReport, ClipboardError> {
self.clipboard.write(transfer)
}
pub fn set_clipboard_text(&mut self, text: impl Into<String>) {
self.clipboard.set_text(text.into());
}
#[must_use]
pub fn handle_external_drag(&mut self, event: ExternalDragEvent) -> ExternalDragResponse {
let response = self.tree.dispatch_external_drag(event);
self.frame_requested = true;
response
}
pub fn handle_platform_event(&mut self, event: PlatformEvent) -> Option<EventTarget> {
match event {
PlatformEvent::Input(input) => self.handle_input(input),
PlatformEvent::TextInputAction(action) => {
self.handle_text_input_action(action);
None
}
PlatformEvent::Metrics(metrics) => {
self.update_window_metrics(metrics);
None
}
PlatformEvent::Environment(environment) => {
self.set_environment(environment);
None
}
PlatformEvent::Lifecycle(lifecycle) => {
self.transition_lifecycle(match lifecycle {
PlatformLifecycle::Active => ApplicationLifecycle::Active,
PlatformLifecycle::Inactive => ApplicationLifecycle::Inactive,
PlatformLifecycle::Suspended => ApplicationLifecycle::Suspended,
PlatformLifecycle::Resumed => ApplicationLifecycle::Active,
PlatformLifecycle::Stopping => ApplicationLifecycle::Stopping,
});
None
}
PlatformEvent::ExternalDrag(event) => {
let _ = self.handle_external_drag(event);
None
}
PlatformEvent::CloseRequested => {
self.shutdown();
None
}
}
}
pub fn update_window_metrics(&mut self, metrics: WindowMetrics) -> bool {
let mut environment = self.environment();
environment.viewport = metrics.logical_size();
environment.physical_width = metrics.physical_size.width;
environment.physical_height = metrics.physical_size.height;
environment.scale_factor = metrics.scale_factor;
self.set_environment(environment)
}
pub fn register_builder(
&mut self,
id: ElementId,
builder: impl FnMut() -> Widget + 'static,
) -> Result<(), TreeError> {
self.register_builder_without_rebuild(id, builder)?;
self.rebuild_from_builder(id)
}
pub(crate) fn register_builder_without_rebuild(
&mut self,
id: ElementId,
builder: impl FnMut() -> Widget + 'static,
) -> Result<(), TreeError> {
if !self.tree.element_exists(id) {
return Err(TreeError::MissingElement(id));
}
self.builders.insert(id, Box::new(builder));
Ok(())
}
pub(crate) fn install_initial_dependencies(
&mut self,
id: ElementId,
initial_dependencies: Rc<RefCell<InitialBuildDependencies>>,
) {
let initial_dependencies = std::mem::take(&mut *initial_dependencies.borrow_mut());
let queue = self.reactive.clone();
let root = queue.borrow().root;
let queue_weak = Rc::downgrade(&queue);
for dependency in initial_dependencies.signals {
let tracked =
crate::reactive::subscribe_element(dependency, root, id, queue_weak.clone());
queue.borrow_mut().record(id, tracked);
}
for scope in initial_dependencies.focus_scopes {
install_focus_scope_watch(&queue, id, &scope);
}
}
pub fn schedule_update(&mut self, id: ElementId, widget: Widget) -> Result<(), TreeError> {
if !self.tree.element_exists(id) {
return Err(TreeError::MissingElement(id));
}
#[cfg(feature = "devtools")]
self.tree
.note_invalidation(id, InvalidationCause::WidgetConfigurationChanged);
self.tree.mark_build(id)?;
let mut widget = widget;
self.prepare_widget(&mut widget);
if self.pending.insert(id, widget).is_none() {
self.order.push_back(id);
}
self.frame_requested = true;
Ok(())
}
#[must_use]
pub fn frame_requested(&self) -> bool {
self.frame_requested || self.reactive.borrow().has_work()
}
#[must_use]
pub fn handle_input(&mut self, event: InputEvent) -> Option<EventTarget> {
let trigger = diagnostic_input_trigger(&event);
self.handle_input_with_trigger(event, trigger)
}
pub(crate) fn handle_input_with_trigger(
&mut self,
event: InputEvent,
trigger: impl Into<String>,
) -> Option<EventTarget> {
self.tree.set_diagnostic_trigger(trigger);
let started = std::time::Instant::now();
let target = self.handle_input_inner(event);
self.pending_event_processing_us = self
.pending_event_processing_us
.saturating_add(us_since_instant(started));
target
}
pub fn set_diagnostic_trigger(&self, trigger: impl Into<String>) {
self.tree.set_diagnostic_trigger(trigger);
}
fn handle_input_inner(&mut self, event: InputEvent) -> Option<EventTarget> {
let gesture_window = self.window_id.map_or(0, |window| {
(u64::from(window.index()) << 32) | u64::from(window.generation())
});
let (pointer, device, kind, buttons, button, sample, phase, position, metadata_rich) =
match event {
InputEvent::Pointer { phase, position } => (
0,
0,
PointerDeviceKind::Mouse,
0,
None,
PointerSampleMetadata::default(),
phase,
position,
false,
),
InputEvent::PointerWithId {
pointer,
phase,
position,
} => (
pointer,
0,
PointerDeviceKind::Mouse,
0,
None,
PointerSampleMetadata::default(),
phase,
position,
false,
),
InputEvent::PointerWithMetadata {
pointer,
device,
kind,
buttons,
button,
sample,
phase,
position,
} => (
pointer, device, kind, buttons, button, sample, phase, position, true,
),
event => {
match event {
InputEvent::Scroll { delta } => {
if self.tree.scroll_at(self.last_pointer, delta) {
self.frame_requested = true;
}
}
InputEvent::Key(key) => {
self.handle_key(key);
}
InputEvent::Text(text) => {
self.insert_text(&text);
}
InputEvent::Ime(ime) => {
self.handle_ime(ime);
}
InputEvent::TrackpadGesture(gesture) => {
if self.pointer_position_known
&& let Some(element) =
self.tree.dispatch_trackpad_gesture_in_window(
gesture_window,
self.last_pointer,
gesture,
)
{
self.frame_requested = true;
return Some(EventTarget {
element,
action: None,
});
}
}
InputEvent::WindowResized { .. } => {}
InputEvent::Pointer { .. } => unreachable!(),
InputEvent::PointerWithId { .. } => unreachable!(),
InputEvent::PointerWithMetadata { .. } => unreachable!(),
}
return None;
}
};
let hover_capable = matches!(
kind,
PointerDeviceKind::Mouse
| PointerDeviceKind::Trackpad
| PointerDeviceKind::Stylus
| PointerDeviceKind::InvertedStylus
);
if hover_capable {
match phase {
PointerPhase::Enter => {
self.pointer_inside = true;
self.pointer_position_known = false;
}
PointerPhase::Exit => {
self.pointer_inside = false;
self.pointer_position_known = false;
}
_ => {
self.pointer_inside = true;
self.pointer_position_known = true;
}
}
}
if phase != PointerPhase::Enter {
self.last_pointer = position;
}
if phase == PointerPhase::Down && self.tree.dismiss_transients_for_pointer(position) > 0 {
self.frame_requested = true;
}
let raw_target = self.tree.dispatch_raw_pointer_in_window(
gesture_window,
RawPointerEvent {
pointer,
device,
kind,
buttons,
button,
sample,
position,
phase,
time: Instant::now(),
},
);
if matches!(phase, PointerPhase::Enter | PointerPhase::Exit) {
if phase == PointerPhase::Exit {
self.set_hover(None);
if self.tree.scrollbar_pointer(PointerPhase::Exit, position) {
self.frame_requested = true;
}
}
return None;
}
let primary_changed = !metadata_rich || button == Some(PRIMARY_POINTER_BUTTON);
let stream = (device, pointer);
let active_primary = self.legacy_pointer == Some(stream);
let legacy_pointer = match phase {
PointerPhase::Down if primary_changed && self.legacy_pointer.is_none() => {
self.legacy_pointer = Some(stream);
true
}
PointerPhase::Down => primary_changed && active_primary,
PointerPhase::Move => {
active_primary || (hover_capable && self.legacy_pointer.is_none())
}
PointerPhase::Up => primary_changed && active_primary,
PointerPhase::Cancel => active_primary,
PointerPhase::Enter | PointerPhase::Exit => false,
};
if let Some(element) = raw_target {
self.frame_requested = true;
self.release_legacy_pointer(device, pointer, phase, primary_changed);
return Some(EventTarget {
element,
action: None,
});
}
let ordinary_gesture_allowed = legacy_pointer || !metadata_rich || !hover_capable;
if ordinary_gesture_allowed
&& let Some(element) = self.tree.dispatch_device_gesture_in_window(
gesture_window,
device,
PointerEvent {
pointer,
position,
phase,
time: Instant::now(),
},
)
{
self.frame_requested = true;
self.release_legacy_pointer(device, pointer, phase, primary_changed);
return Some(EventTarget {
element,
action: None,
});
}
if !legacy_pointer {
return None;
}
if self.tree.scrollbar_pointer(phase, position) {
self.frame_requested = true;
self.release_legacy_pointer(device, pointer, phase, primary_changed);
return None;
}
let text_target = self.tree.text_field_at(position);
let selectable_target = self.tree.selectable_text_at(position);
let target = self
.tree
.hit_test(position)
.and_then(|render| self.tree.element_for_render(render))
.and_then(|element| self.tree.button_ancestor(element));
let result = match phase {
PointerPhase::Enter | PointerPhase::Exit => {
unreachable!("pointer boundary handled above")
}
PointerPhase::Move => {
if let Some(field) = self.captured_text_field {
self.tree
.text_field_set_caret(field, position, true, Instant::now());
self.frame_requested = true;
}
if let Some(label) = self.captured_selectable_text {
if let Some(target) = selectable_target {
if self
.tree
.selectable_text_set_selection(target, position, true)
{
self.captured_selectable_text = Some(target);
self.set_focus(Some(target));
self.frame_requested = true;
}
} else if self.tree.is_selectable_text(label) {
self.frame_requested = true;
}
}
self.set_hover(target.map(|(element, _)| element));
target.map(|(element, action)| EventTarget { element, action })
}
PointerPhase::Down => {
if let Some(label) = selectable_target {
self.set_focus(Some(label));
self.captured_selectable_text = Some(label);
if self
.tree
.selectable_text_set_selection(label, position, false)
{
self.frame_requested = true;
return Some(EventTarget {
element: label,
action: None,
});
}
}
if selectable_target.is_none()
&& text_target.is_none()
&& target.is_none()
&& let Some((element, interaction)) = self.tree.window_interaction_at(position)
&& let (Some(window_id), Some(manager)) =
(self.window_id, self.window_manager.as_ref())
{
let operation = match interaction {
WindowInteraction::Move => WindowOperation::BeginMoveDrag,
WindowInteraction::Resize(direction) => {
WindowOperation::BeginResizeDrag(direction)
}
};
if manager.send_window_operation(window_id, operation) {
self.release_legacy_pointer(device, pointer, phase, primary_changed);
return Some(EventTarget {
element,
action: None,
});
}
}
let keep_text_field_focus = self
.focused
.is_some_and(|focused| self.tree.is_text_field(focused))
&& self.tree.preserves_text_field_focus(position);
self.set_focus(if keep_text_field_focus {
self.focused
} else {
text_target
});
self.captured_text_field = text_target;
if let Some(field) = text_target {
self.tree
.text_field_set_caret(field, position, false, Instant::now());
self.frame_requested = true;
return Some(EventTarget {
element: field,
action: None,
});
}
self.set_hover(target.map(|(element, _)| element));
self.pressed_button = target.map(|(element, _)| element);
if let Some(element) = self.pressed_button {
let _ = self
.tree
.set_button_interaction(element, None, Some(true), None);
self.frame_requested = true;
}
target.map(|(element, action)| EventTarget { element, action })
}
PointerPhase::Up => {
self.captured_text_field = None;
self.captured_selectable_text = None;
let pressed = self.pressed_button.take();
let valid = pressed
.zip(target)
.filter(|(pressed, (target, _))| pressed == target)
.map(|(_, target)| target);
if let Some(element) = pressed {
let _ = self
.tree
.set_button_interaction(element, None, Some(false), None);
}
if let Some((element, action)) = valid {
if let Some(action) = action {
self.activate_action(action);
}
Some(EventTarget { element, action })
} else {
None
}
}
PointerPhase::Cancel => {
self.captured_text_field = None;
self.captured_selectable_text = None;
if let Some(element) = self.pressed_button.take() {
let _ = self
.tree
.set_button_interaction(element, None, Some(false), None);
self.frame_requested = true;
}
None
}
};
self.release_legacy_pointer(device, pointer, phase, primary_changed);
result
}
fn release_legacy_pointer(
&mut self,
device: u64,
pointer: u64,
phase: PointerPhase,
primary_changed: bool,
) {
if (phase == PointerPhase::Cancel || (phase == PointerPhase::Up && primary_changed))
&& self.legacy_pointer == Some((device, pointer))
{
self.legacy_pointer = None;
}
}
#[must_use]
pub fn effective_mouse_cursor(&self) -> MouseCursor {
if !self.pointer_inside || !self.pointer_position_known {
return MouseCursor::Basic;
}
match self.tree.mouse_cursor_at(self.last_pointer) {
MouseCursor::Defer => MouseCursor::Basic,
cursor => cursor,
}
}
pub(crate) fn set_focus(&mut self, next: Option<ElementId>) {
self.initial_focus = InitialFocus::Settled;
if self.focused == next {
return;
}
self.cancel_ime_composition();
if let Some(next) = next
&& self.tree.dismiss_transients_for_focus(next) > 0
{
self.frame_requested = true;
}
if let Some(previous) = self.focused {
self.tree.set_keyboard_focus(previous, false);
let _ = self.tree.set_focused(previous, false, Instant::now());
}
self.focused = next;
if let Some(current) = next {
let _ = self.tree.set_focused(current, true, Instant::now());
self.tree.set_keyboard_focus(current, true);
if self.tree.is_text_field(current) {
let _ = self.ensure_text_history(current);
}
}
self.frame_requested = true;
self.sync_text_input_client();
}
fn reconcile_focus_and_selection_captures(&mut self) {
if self.captured_text_field.is_some_and(|field| {
!self.tree.is_text_field(field) || !self.tree.can_request_focus(field)
}) {
self.captured_text_field = None;
}
if self.captured_selectable_text.is_some_and(|label| {
!self.tree.is_selectable_text(label) || !self.tree.can_request_focus(label)
}) {
self.captured_selectable_text = None;
}
if self
.focused
.is_some_and(|focused| !self.tree.can_request_focus(focused))
{
self.set_focus(None);
}
self.reconcile_button_tenure();
}
fn reconcile_button_tenure(&mut self) {
if self.pressed_button.is_some_and(|pressed| {
self.tree
.button_ancestor(pressed)
.map(|(ancestor, _)| ancestor)
!= Some(pressed)
}) && let Some(pressed) = self.pressed_button.take()
{
let _ = self
.tree
.set_button_interaction(pressed, None, Some(false), None);
self.frame_requested = true;
}
if self.hovered_button.is_some_and(|hovered| {
self.tree
.button_ancestor(hovered)
.map(|(ancestor, _)| ancestor)
!= Some(hovered)
}) {
self.set_hover(None);
}
}
fn ensure_text_history(&mut self, field: ElementId) -> Option<UndoHistoryController> {
let editor = self.tree.text_controller(field)?;
let replace = self
.text_histories
.get(&field)
.is_none_or(|history| history.editor() != editor);
if replace {
self.text_histories
.insert(field, UndoHistoryController::new(editor));
}
let history = self.text_histories.get(&field).cloned()?;
if let Some(max_entries) = self.tree.text_field_history_max_entries(field)
&& history.max_entries() != max_entries
{
history.set_max_entries(max_entries);
}
Some(history)
}
fn sync_text_input_client(&mut self) {
let snapshot = self
.focused
.and_then(|id| self.tree.text_field_input_snapshot(id));
let Some(snapshot) = snapshot else {
if let Some(client) = self.text_input_client.take() {
self.text_input_commands
.push_back(TextInputCommand::Hide { client });
self.text_input_commands
.push_back(TextInputCommand::Clear { client });
}
self.text_input_configuration = None;
self.text_input_state = None;
self.text_input_caret_rect = None;
return;
};
let client = TextInputClientId::new(snapshot.client_id);
let input_type = match snapshot.input_type {
TextInputTypeHint::Text if snapshot.multiline => TextInputType::Multiline,
TextInputTypeHint::Text => TextInputType::Text,
TextInputTypeHint::Multiline => TextInputType::Multiline,
TextInputTypeHint::Number => TextInputType::Number,
TextInputTypeHint::Phone => TextInputType::Phone,
TextInputTypeHint::Email => TextInputType::Email,
TextInputTypeHint::Url => TextInputType::Url,
TextInputTypeHint::Password => TextInputType::Password,
};
let action = match snapshot.input_action {
TextInputActionHint::Unspecified => {
if snapshot.multiline {
TextInputAction::Newline
} else {
TextInputAction::Done
}
}
TextInputActionHint::None => TextInputAction::None,
TextInputActionHint::Done => TextInputAction::Done,
TextInputActionHint::Go => TextInputAction::Go,
TextInputActionHint::Search => TextInputAction::Search,
TextInputActionHint::Send => TextInputAction::Send,
TextInputActionHint::Next => TextInputAction::Next,
TextInputActionHint::Previous => TextInputAction::Previous,
TextInputActionHint::Newline => TextInputAction::Newline,
};
let configuration = TextInputConfiguration {
client,
input_type: if snapshot.obscure_text {
TextInputType::Password
} else {
input_type
},
action,
multiline: snapshot.multiline,
enabled: snapshot.enabled,
read_only: snapshot.read_only,
obscure_text: snapshot.obscure_text,
};
let state = TextInputState {
text: snapshot.text,
selection_start: snapshot.selection.base,
selection_end: snapshot.selection.extent,
composing: snapshot.composing.map(|range| (range.start, range.end)),
};
let client_changed = self.text_input_client != Some(client);
let configuration_changed = self.text_input_configuration.as_ref() != Some(&configuration);
if client_changed || configuration_changed {
self.text_input_commands
.push_back(TextInputCommand::SetClient {
configuration: configuration.clone(),
state: state.clone(),
caret_rect: snapshot.caret_rect,
});
} else if self.text_input_state.as_ref() != Some(&state) {
self.text_input_commands
.push_back(TextInputCommand::Update {
client,
state: state.clone(),
});
}
if !client_changed
&& self.text_input_caret_rect != Some(snapshot.caret_rect)
&& !configuration_changed
{
self.text_input_commands
.push_back(TextInputCommand::SetCaretRect {
client,
rect: snapshot.caret_rect,
});
}
self.text_input_client = Some(client);
self.text_input_configuration = Some(configuration);
self.text_input_state = Some(state);
self.text_input_caret_rect = Some(snapshot.caret_rect);
}
fn focus_next(&mut self, reverse: bool) {
let fields = self.tree.focusable_elements();
if fields.is_empty() {
return;
}
let index = self
.focused
.and_then(|focused| fields.iter().position(|id| *id == focused));
let next = match index {
Some(index) if reverse => fields[(index + fields.len() - 1) % fields.len()],
Some(index) => fields[(index + 1) % fields.len()],
None if reverse => *fields.last().expect("not empty"),
None => fields[0],
};
self.set_focus(Some(next));
}
fn handle_key(&mut self, event: KeyboardEvent) {
if event.state.is_down() {
self.editing_diagnostics.key_down_received += 1;
} else {
self.editing_diagnostics.key_up_received += 1;
}
if let Some(external) = self.tree.focused_keyboard_element()
&& self.focused != Some(external)
{
self.set_focus(Some(external));
}
if self.tree.dispatch_keyboard(self.focused, event.clone()) {
self.frame_requested = true;
return;
}
if !event.state.is_down() {
return;
}
if event.code == Code::Escape
&& self
.tree
.dismiss_transients(incular_widgets::TransientDismissReason::Escape)
> 0
{
self.frame_requested = true;
return;
}
if matches!(
event.code,
Code::ArrowUp | Code::ArrowDown | Code::ArrowLeft | Code::ArrowRight
) && let Some(focused) = self.focused
&& let Some(next) = self.tree.menu_directional_focus_target(focused, event.code)
{
self.set_focus(Some(next));
return;
}
if matches!(event.code, Code::Enter | Code::NumpadEnter | Code::Space)
&& let Some(focused) = self.focused
&& let Some((owner, Some(action))) = self.tree.button_ancestor(focused)
&& (owner == focused || self.tree.is_menu_item_focus(focused))
&& self.activate_action(action)
{
return;
}
if event.code == Code::Tab {
self.focus_next(event.modifiers.shift());
return;
}
if let Some(label) = self.focused.filter(|id| self.tree.is_selectable_text(*id)) {
let extend = event.modifiers.shift();
let handled = if command_modifier(event.modifiers) {
match event.code {
Code::KeyA => self.tree.selectable_text_select_all(label),
Code::KeyC => {
if let Some(text) = self.tree.selectable_text_selected_text(label) {
self.clipboard.set_text(text);
true
} else {
false
}
}
_ => false,
}
} else {
match event.code {
Code::ArrowLeft => self.tree.selectable_text_move(label, false, extend),
Code::ArrowRight => self.tree.selectable_text_move(label, true, extend),
Code::Home => self.tree.selectable_text_move_to_edge(label, false, extend),
Code::End => self.tree.selectable_text_move_to_edge(label, true, extend),
_ => false,
}
};
if handled {
self.frame_requested = true;
}
return;
}
let Some(field) = self.focused.filter(|id| self.tree.is_text_field(*id)) else {
return;
};
let Some(controller) = self.tree.text_controller(field) else {
return;
};
let editable = self.tree.text_field_is_editable(field);
let extend = event.modifiers.shift();
if command_modifier(event.modifiers) {
match event.code {
Code::KeyA => controller.select_all(),
Code::KeyC => self.clipboard.set_text(controller.selected_text()),
Code::KeyZ => {
let Some(history) = self.ensure_text_history(field) else {
return;
};
let changed = if event.modifiers.shift() {
history.redo()
} else {
history.undo()
};
if !changed {
return;
}
}
Code::KeyY => {
let Some(history) = self.ensure_text_history(field) else {
return;
};
if !history.redo() {
return;
}
}
Code::KeyX => {
if !editable {
return;
}
self.clipboard.set_text(controller.selected_text());
controller.replace_selection("");
}
Code::KeyV => {
if !editable {
return;
}
if let Some(text) = self.clipboard.get_text() {
controller.insert(&text);
}
}
_ => return,
}
} else {
match event.code {
Code::Backspace => {
if !editable {
return;
}
controller.backspace();
self.editing_diagnostics.backspace_commands += 1;
}
Code::Delete => {
if !editable {
return;
}
controller.delete();
self.editing_diagnostics.delete_commands += 1;
}
Code::ArrowLeft => {
if !self.tree.text_field_move_horizontal(field, false, extend) {
controller.move_left(extend);
}
}
Code::ArrowRight => {
if !self.tree.text_field_move_horizontal(field, true, extend) {
controller.move_right(extend);
}
}
Code::ArrowUp => {
let _ = self.tree.text_field_move_vertical(field, false, extend);
}
Code::ArrowDown => {
let _ = self.tree.text_field_move_vertical(field, true, extend);
}
Code::Home => {
if !self.tree.text_field_move_line_edge(field, false, extend) {
controller.move_home(extend);
}
}
Code::End => {
if !self.tree.text_field_move_line_edge(field, true, extend) {
controller.move_end(extend);
}
}
Code::Enter => {
if !editable {
return;
}
if self.tree.is_multiline_text_field(field) {
controller.insert("\n");
} else {
self.tree.submit_text_field(field);
}
}
_ => return,
}
}
controller.reset_caret(Instant::now());
self.frame_requested = true;
}
fn insert_text(&mut self, text: &str) {
let Some(field) = self.focused.filter(|id| self.tree.is_text_field(*id)) else {
return;
};
if !self.tree.text_field_is_editable(field) {
return;
}
if text.chars().any(char::is_control) {
return;
}
if let Some(controller) = self.tree.text_controller(field) {
controller.insert(text);
self.editing_diagnostics.text_commits += 1;
controller.reset_caret(Instant::now());
self.frame_requested = true;
}
}
pub fn handle_text_input_action(&mut self, action: TextInputAction) -> bool {
let Some(field) = self.focused.filter(|id| self.tree.is_text_field(*id)) else {
return false;
};
let editable = self.tree.text_field_is_editable(field);
if !editable {
return false;
}
let handled = match action {
TextInputAction::Newline => {
if self.tree.is_multiline_text_field(field) {
self.tree.text_controller(field).is_some_and(|controller| {
controller.insert("\n");
controller.reset_caret(Instant::now());
true
})
} else {
self.tree.submit_text_field(field)
}
}
TextInputAction::Next => {
let submitted = self.tree.submit_text_field(field);
self.focus_next(false);
submitted
}
TextInputAction::Previous => {
let submitted = self.tree.submit_text_field(field);
self.focus_next(true);
submitted
}
TextInputAction::Done
| TextInputAction::Go
| TextInputAction::Search
| TextInputAction::Send => self.tree.submit_text_field(field),
TextInputAction::Unspecified | TextInputAction::None => false,
};
if handled {
self.frame_requested = true;
}
handled
}
fn cancel_ime_composition(&mut self) {
let owner = self.ime_composition.take();
if let Some(owner) = owner {
owner.controller.clear_preedit();
}
}
fn ime_owner_current(&self) -> bool {
self.ime_composition.as_ref().is_some_and(|owner| {
self.focused == Some(owner.field)
&& self.tree.is_text_field(owner.field)
&& self.tree.text_field_is_editable(owner.field)
&& self
.tree
.text_controller(owner.field)
.is_some_and(|current| current == owner.controller)
})
}
fn ime_focused_target(&self) -> Option<ImeCompositionOwner> {
let field = self
.focused
.filter(|id| self.tree.text_field_is_editable(*id))?;
Some(ImeCompositionOwner {
field,
controller: self.tree.text_controller(field)?,
})
}
fn validate_ime_composition(&mut self) {
if !self.ime_owner_current() {
self.cancel_ime_composition();
}
}
fn handle_ime(&mut self, event: ImeEvent) {
self.editing_diagnostics.ime_events += 1;
self.validate_ime_composition();
let target = match &event {
ImeEvent::Preedit { .. } => {
if self.ime_composition.is_none() {
self.ime_composition = self.ime_focused_target();
}
self.ime_composition
.as_ref()
.map(|owner| owner.controller.clone())
}
ImeEvent::Commit(_) => self
.ime_composition
.take()
.or_else(|| self.ime_focused_target())
.map(|owner| owner.controller),
ImeEvent::End => self.ime_composition.take().map(|owner| owner.controller),
};
let Some(controller) = target else {
return;
};
match event {
ImeEvent::Preedit { text, selection } => {
controller.set_preedit(
text,
selection.map(|(start, end)| TextRange::new(start, end)),
);
}
ImeEvent::Commit(text) => {
controller.commit_preedit(&text);
}
ImeEvent::End => {
controller.clear_preedit();
}
}
controller.reset_caret(Instant::now());
self.frame_requested = true;
}
pub fn run_frame(
&mut self,
constraints: Constraints,
) -> Result<(DisplayList, FrameStats), TreeError> {
self.run_frame_at(constraints, Instant::now())
}
pub fn run_frame_at(
&mut self,
constraints: Constraints,
now: Instant,
) -> Result<(DisplayList, FrameStats), TreeError> {
let message_guard = tracing::info_span!("incular.messages").entered();
let message_span = profiling::PhaseSpan::start();
self.process_runtime_work_at(now);
let runtime_messages = message_span.elapsed_us();
drop(message_guard);
let before = self.tree.diagnostics();
let build_guard =
tracing::info_span!("incular.build", elements_updated = tracing::field::Empty,)
.entered();
let build_span = profiling::PhaseSpan::start();
let mut updated = 0;
while let Some(id) = self.order.pop_front() {
if let Some(widget) = self.pending.remove(&id)
&& self.tree.element_exists(id)
{
self.tree.update(id, widget)?;
updated += 1;
}
}
loop {
while let Some((root, node)) = { self.reactive.borrow_mut().take_node() } {
if let Some(node) = node.upgrade() {
node.run(root);
}
}
while let Some(id) = { self.reactive.borrow_mut().take() } {
if self.tree.element_exists(id) && self.builders.contains_key(&id) {
#[cfg(feature = "devtools")]
for cause in self.reactive.borrow_mut().take_causes(id) {
self.tree.note_invalidation(id, cause);
}
self.rebuild_from_builder(id)?;
updated += 1;
}
}
if !self.reactive.borrow().has_work() {
break;
}
}
for id in self.tree.take_unmounted() {
self.handlers_stale = true;
self.builders.remove(&id);
self.reactive.borrow_mut().forget(id);
if let Some(scope) = self.owner_scopes.remove(&id) {
scope.cancel();
}
self.text_histories.remove(&id);
}
self.reconcile_focus_and_selection_captures();
self.validate_ime_composition();
self.prune_handlers();
let build = build_span.elapsed_us();
drop(build_guard);
let reduced_motion = self.environment.borrow().reduced_motion;
let fling_live = self.tree.pump_scroll_flings(now, reduced_motion);
let _layout_guard = tracing::info_span!("incular.layout").entered();
let layout_span = profiling::PhaseSpan::start();
self.tree.layout(constraints)?;
let layout = layout_span.elapsed_us();
drop(_layout_guard);
for (action, handler) in self.tree.take_pending_handlers() {
self.handlers_stale = true;
self.handlers.insert(action, handler);
}
for id in self.tree.take_unmounted() {
self.handlers_stale = true;
self.builders.remove(&id);
self.reactive.borrow_mut().forget(id);
if let Some(scope) = self.owner_scopes.remove(&id) {
scope.cancel();
}
self.text_histories.remove(&id);
}
self.reconcile_focus_and_selection_captures();
self.validate_ime_composition();
self.prune_handlers();
if self.initial_focus == InitialFocus::AwaitingLayout {
self.set_focus(self.tree.autofocus_element());
}
let _composite_guard = tracing::info_span!("incular.composite").entered();
let composite_span = profiling::PhaseSpan::start();
let (composited, animations_active) = self.tree.update_compositor(now)?;
let composite = composite_span.elapsed_us();
drop(_composite_guard);
let _semantics_guard = tracing::info_span!("incular.semantics").entered();
let semantics_span = profiling::PhaseSpan::start();
self.tree.update_semantics();
let semantics = semantics_span.elapsed_us();
drop(_semantics_guard);
let _paint_guard = tracing::info_span!("incular.paint").entered();
let paint_span = profiling::PhaseSpan::start();
let display_list = self.tree.paint();
let paint = paint_span.elapsed_us();
drop(_paint_guard);
self.sync_text_input_client();
self.frame_requested = fling_live
|| !self.pending.is_empty()
|| self.reactive.borrow().has_work()
|| animations_active;
let after = self.tree.diagnostics();
let timings = FrameTimings {
event_processing: std::mem::take(&mut self.pending_event_processing_us),
runtime_messages,
build,
layout,
composite,
semantics,
paint,
cpu_total: runtime_messages + build + layout + composite + semantics + paint,
};
tracing::debug!(
target: "incular::frame",
cpu_total_us = timings.cpu_total,
build_us = timings.build,
layout_us = timings.layout,
paint_us = timings.paint,
);
Ok((
display_list.clone(),
FrameStats {
updated_elements: updated,
rebuilt_elements: after.rebuilds - before.rebuilds,
laid_out_render_objects: after.layouts - before.layouts,
repainted_render_objects: after.paints - before.paints,
composited: u64::from(composited),
active_animations: u64::from(animations_active),
display_list_commands: display_list.len(),
requested_another_frame: self.frame_requested,
timings,
},
))
}
#[must_use]
pub fn diagnostics(&self) -> Diagnostics {
self.tree.diagnostics()
}
#[cfg(feature = "hot-reload")]
pub(crate) fn reassemble(&mut self) {
let root = self.application_root;
let ids = root
.into_iter()
.chain(self.builders.keys().copied().filter(|id| Some(*id) != root))
.collect::<Vec<_>>();
let mut reactive = self.reactive.borrow_mut();
for id in ids {
#[cfg(feature = "devtools")]
reactive.note_cause(id, InvalidationCause::Manual);
reactive.enqueue(id);
}
drop(reactive);
self.frame_requested = true;
}
pub(crate) fn rebuild_from_builder(&mut self, id: ElementId) -> Result<(), TreeError> {
self.reactive.borrow_mut().refresh(id);
let widget = {
let _build_scope = BuildScopeGuard::enter(BuildScope {
root: self.reactive.borrow().root,
element: Some(id),
queue: Rc::downgrade(&self.reactive),
initial_dependencies: None,
spawner: self.spawner(),
owner_scope: self
.owner_scopes
.get(&id)
.cloned()
.unwrap_or_else(|| self.window_scope.clone()),
});
self.builders.get_mut(&id).expect("registered builder")()
};
let mut widget = widget;
self.prepare_widget(&mut widget);
self.tree.update(id, widget)?;
Ok(())
}
fn prepare_widget(&mut self, widget: &mut Widget) {
self.handlers_stale = true;
let handlers = &mut self.handlers;
let tree = &mut self.tree;
widget.bind_callbacks(&mut |callback| {
let id = tree.allocate_action();
handlers.insert(id, callback);
id
});
}
fn activate_action(&mut self, action: ActionId) -> bool {
let mut handled = false;
if let Some(callback) = self.handlers.get(&action).cloned() {
callback();
handled = true;
}
if let Some(callback) = self.legacy_action_handler.as_mut() {
callback(action);
handled = true;
}
self.frame_requested |= handled;
handled
}
fn prune_handlers(&mut self) {
if !std::mem::take(&mut self.handlers_stale) {
return;
}
let active = self.tree.action_ids();
self.handlers.retain(|id, _| active.contains(id));
}
fn set_hover(&mut self, next: Option<ElementId>) {
if self.hovered_button == next {
return;
}
if let Some(previous) = self.hovered_button {
let _ = self
.tree
.set_button_interaction(previous, Some(false), None, None);
if let Some(action) = self.tree.hover_actions_for_element(previous).1
&& let Some(callback) = self.handlers.get(&action).cloned()
{
callback();
}
}
self.hovered_button = next;
if let Some(current) = next {
if self.pressed_button != Some(current) {
let _ = self
.tree
.set_button_interaction(current, Some(true), None, None);
}
if let Some(action) = self.tree.hover_actions_for_element(current).0
&& let Some(callback) = self.handlers.get(&action).cloned()
{
callback();
}
}
self.frame_requested = true;
}
}
fn command_modifier(modifiers: Modifiers) -> bool {
modifiers.ctrl() || modifiers.meta()
}
fn us_since_instant(started: std::time::Instant) -> u32 {
started.elapsed().as_micros().try_into().unwrap_or(u32::MAX)
}