Files
iris/src/desktop/mod.rs
T

319 lines
12 KiB
Rust

use crate::prelude::*;
use arboard::Clipboard;
use std::{marker::Sized, sync::Arc, time::Instant};
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::{Window, WindowAttributes},
};
mod access;
mod app;
mod attr;
mod input;
mod logging;
mod platform;
mod render;
pub use access::*;
pub use app::*;
pub use input::*;
pub use render::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
/// Physical pixels per dp. Layout and input stay in physical pixels; only
/// `dp(...)` resolves through this scale.
pub fn content_scale(window: &Window) -> f32 {
match std::env::var("IRIS_SCALE") {
Err(_) => window.scale_factor() as f32,
Ok(text) => match text.trim().parse::<f32>() {
Ok(scale) if scale > 0.0 => scale,
_ => {
log::warn!("IRIS_SCALE={text:?} is not a positive number; using the window's own");
window.scale_factor() as f32
}
},
}
}
pub struct DesktopUiState {
pub root: Option<StrongWidget>,
pub renderer: UiRenderer,
pub input: Input,
pub focus: Option<WeakWidget<TextEdit>>,
pub clipboard: Clipboard,
pub window: Arc<Window>,
pub ime: usize,
pub last_click: Instant,
pub access_adapter: accesskit_winit::Adapter,
pub access: AccessTree,
}
impl<State: 'static> HasRoot<StdRsc<State>> for DesktopUiState {
fn set_root(&mut self, rsc: &mut StdRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
}
}
impl DesktopUiState {
pub fn new(window: impl Into<Arc<Window>>, access_adapter: accesskit_winit::Adapter) -> Self {
let window = window.into();
Self {
root: None,
renderer: UiRenderer::new(window.clone()),
window,
input: Input::default(),
clipboard: Clipboard::new().unwrap(),
ime: 0,
last_click: Instant::now(),
focus: None,
access_adapter,
access: AccessTree::new(),
}
}
}
pub trait HasDesktopUiState: Sized + 'static {
fn desktop_state(&self) -> &DesktopUiState;
fn desktop_state_mut(&mut self) -> &mut DesktopUiState;
}
pub trait DesktopAppState: HasDesktopUiState {
type Event = ();
fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<Self::Event>) -> Self;
#[allow(unused_variables)]
fn event(&mut self, event: Self::Event, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)]
fn exit(&mut self, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent, rsc: &mut StdRsc<Self>) {}
fn window_attributes() -> WindowAttributes {
Default::default()
}
}
pub struct DesktopApp<State: DesktopAppState> {
rsc: StdRsc<State>,
state: State,
task_recv: TaskMsgReceiver<StdRsc<State>>,
}
impl<State: DesktopAppState> AppState for DesktopApp<State> {
type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop
.create_window(State::window_attributes().with_visible(false))
.unwrap();
let access_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop,
&window,
NullActivationHandler,
NullActionHandler,
NullDeactivationHandler,
);
window.set_visible(true);
let desktop_state = DesktopUiState::new(window, access_adapter);
let (mut rsc, task_recv) = StdRsc::new(desktop_state.window.clone());
// Set before building widgets so the first text shape uses the right density.
let scale = content_scale(desktop_state.window.as_ref());
rsc.ui.set_density(scale);
let state = State::new(desktop_state, &mut rsc, proxy);
Self {
rsc,
state,
task_recv,
}
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc);
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self {
rsc,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.desktop_state_mut();
// Required by `accesskit_winit` on every window event, not just the
// ones this backend otherwise cares about -- some platform adapters
// rely on it to notice activation (a screen reader turning on).
ui_state
.access_adapter
.process_event(&ui_state.window, &event);
let input_changed = ui_state.input.event(&event);
let cursor_state = ui_state.cursor_state().clone();
let old = ui_state.focus;
if cursor_state.buttons.left.is_start() {
ui_state.focus = None;
}
if input_changed {
// Winit delivers one sample at a time, so there is no history batch.
if crate::diagnostics::trace_enabled() {
let action = if cursor_state.buttons.left.is_start() {
"down"
} else if cursor_state.buttons.left.is_end() {
"up"
} else {
"move"
};
let render_state = rsc.ui.render_state();
let t_ms = cursor_state
.time
.duration_since(render_state.get().epoch())
.as_millis() as u64;
crate::sense::log_input_event(
action,
cursor_state.pos.x,
cursor_state.pos.y,
t_ms,
&[],
);
}
let window_size = ui_state.window_size();
let render_state = rsc.ui.render_state();
render_state
.get()
.run_sensors(rsc, state, cursor_state, window_size);
}
let ui_state = state.desktop_state_mut();
if old != ui_state.focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
match &event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
// Advance animations before drawing and keep requesting frames while active.
let frame_start = std::time::Instant::now();
let animating = rsc.ui_mut().tick_animations(frame_start);
let ui_state = state.desktop_state_mut();
if animating {
ui_state.window.request_redraw();
}
rsc.draw(&ui_state.root);
ui_state.renderer.update(&mut rsc.ui);
let mut parts = ui_state.renderer.draw();
parts.total = frame_start.elapsed();
let render_state = rsc.ui.render_state();
let render_state = render_state.get();
crate::diagnostics::log_frame(&render_state, frame_start, parts, animating);
if let Some(tree_update) = ui_state.access.update(rsc.widgets(), &render_state, rsc)
{
ui_state.access_adapter.update_if_active(|| tree_update);
}
}
WindowEvent::Resized(size) => {
rsc.ui.resize((size.width, size.height));
ui_state.renderer.resize(size)
}
WindowEvent::ScaleFactorChanged { .. } => {
let scale = content_scale(ui_state.window.as_ref());
rsc.ui.set_density(scale);
ui_state.window.request_redraw();
}
WindowEvent::KeyboardInput { event, .. } => {
let requested = event.state.is_pressed().then(|| match &event.logical_key {
winit::keyboard::Key::Character(c) if ui_state.input.modifiers.control => {
match c.as_str().to_ascii_lowercase().as_str() {
"c" => Some(Command::Copy),
"a" => Some(Command::SelectAll),
_ => None,
}
}
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape) => {
Some(Command::Escape)
}
_ => None,
});
let command = requested
.flatten()
.map_or(CommandResult::Unused, |command| rsc.run_command(command));
let command_used = match command {
CommandResult::Copy(text) => {
if let Err(err) = ui_state.clipboard.set_text(text) {
eprintln!("failed to copy text to clipboard: {err}")
}
true
}
CommandResult::Used => true,
CommandResult::Unused => false,
};
if !command_used
&& !rsc.events.controllers.command_target_blocks_input()
&& let Some(sel) = ui_state.focus
&& event.state.is_pressed()
{
let mut text = sel.edit(rsc);
match text.apply_event(event, &ui_state.input.modifiers) {
TextInputResult::Unfocus => {
ui_state.focus = None;
ui_state.window.set_ime_allowed(false);
}
TextInputResult::Submit => {
rsc.run_event::<Submit>(sel, (), state);
}
TextInputResult::Paste => {
if let Ok(t) = ui_state.clipboard.get_text() {
text.insert(&t);
}
rsc.run_event::<Edited>(sel, (), state);
}
TextInputResult::Copy(text) => {
if let Err(err) = ui_state.clipboard.set_text(text) {
eprintln!("failed to copy text to clipboard: {err}")
}
}
TextInputResult::Used => {
rsc.run_event::<Edited>(sel, (), state);
}
TextInputResult::Unused => {}
}
}
}
WindowEvent::Ime(ime) => {
if !rsc.events.controllers.command_target_blocks_input()
&& let Some(sel) = ui_state.focus
{
let mut text = sel.edit(rsc);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real
text.replace(ui_state.ime, content);
ui_state.ime = content.chars().count();
}
Ime::Commit(content) => {
text.insert(content);
}
}
}
}
_ => (),
}
state.window_event(event, rsc);
let ui_state = self.state.desktop_state_mut();
let render_state = rsc.ui.render_state();
if render_state
.get()
.needs_redraw(&ui_state.root, rsc.widgets())
{
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
}
fn exit(&mut self) {
self.state.exit(&mut self.rsc);
}
}