Clean up shared UI runtime state
This commit is contained in:
1 parent
9b4c690916
commit
5ca244528f
27 files changed
+355
-499
No files matched your search
@@ -0,0 +1,18 @@
|
||||
use accesskit::{ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, TreeUpdate};
|
||||
|
||||
pub struct NullActivationHandler;
|
||||
impl ActivationHandler for NullActivationHandler {
|
||||
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NullActionHandler;
|
||||
impl ActionHandler for NullActionHandler {
|
||||
fn do_action(&mut self, _request: ActionRequest) {}
|
||||
}
|
||||
|
||||
pub struct NullDeactivationHandler;
|
||||
impl DeactivationHandler for NullDeactivationHandler {
|
||||
fn deactivate_accessibility(&mut self) {}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
|
||||
window::WindowId,
|
||||
};
|
||||
|
||||
pub trait AppState {
|
||||
type Event: 'static;
|
||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
|
||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
|
||||
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
|
||||
fn exit(&mut self);
|
||||
|
||||
fn run()
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
App::<Self>::run();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct App<State: AppState> {
|
||||
state: Option<State>,
|
||||
proxy: EventLoopProxy<State::Event>,
|
||||
}
|
||||
|
||||
impl<State: AppState> App<State> {
|
||||
pub fn run() {
|
||||
super::logging::install(log::LevelFilter::Info);
|
||||
let event_loop = EventLoop::with_user_event().build().unwrap();
|
||||
let proxy = event_loop.create_proxy();
|
||||
event_loop
|
||||
.run_app(&mut App::<State> { state: None, proxy })
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.state.is_none() {
|
||||
let state = State::new(event_loop, self.proxy.clone());
|
||||
self.state = Some(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.window_event(event, event_loop);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: State::Event) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.event(event, event_loop);
|
||||
}
|
||||
|
||||
fn exiting(&mut self, _: &ActiveEventLoop) {
|
||||
let state = self.state.as_mut().unwrap();
|
||||
state.exit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::prelude::*;
|
||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
|
||||
impl<T: HasDesktopUiState> FocusHost for T {
|
||||
fn recent_click(&mut self) -> bool {
|
||||
crate::attr::recent_click(&mut self.desktop_state_mut().last_click)
|
||||
}
|
||||
|
||||
fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) {
|
||||
self.desktop_state_mut().focus = id;
|
||||
}
|
||||
|
||||
fn is_focused(&self, id: WeakWidget<TextEdit>) -> bool {
|
||||
self.desktop_state().focus == Some(id)
|
||||
}
|
||||
|
||||
fn focus_gained(&mut self, region: Option<PixelRegion>) {
|
||||
let state = self.desktop_state_mut();
|
||||
let Some(region) = region else { return };
|
||||
state.window.set_ime_allowed(true);
|
||||
state.window.set_ime_cursor_area(
|
||||
PhysicalPosition::<f32>::from(region.top_left.tuple()),
|
||||
PhysicalSize::<f32>::from(region.size().tuple()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// `CursorState::time` is the sample's own time on every backend. winit
|
||||
// carries no timestamp on a pointer event, so the moment it is handed to
|
||||
// us is the closest measurement available here -- which is also what the
|
||||
// drag code used to do for itself with `Instant::now()`, before Android's
|
||||
// batched samples made the difference matter (see `sense::CursorState`).
|
||||
use crate::prelude::*;
|
||||
use std::time::Instant;
|
||||
use winit::{
|
||||
event::{MouseButton, MouseScrollDelta, WindowEvent},
|
||||
keyboard::{Key, NamedKey},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Input {
|
||||
cursor: CursorState,
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// winit's pointer coordinates are physical pixels, which is the
|
||||
/// space the whole tree is laid out and hit-tested in -- see
|
||||
/// `desktop::content_scale`. Nothing is converted here; `dp(...)`
|
||||
/// resolves against the density at layout time instead.
|
||||
pub fn event(&mut self, event: &WindowEvent) -> bool {
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.cursor.pos = Vec2::new(position.x as f32, position.y as f32);
|
||||
self.cursor.exists = true;
|
||||
self.cursor.time = Instant::now();
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
self.cursor.time = Instant::now();
|
||||
let buttons = &mut self.cursor.buttons;
|
||||
let pressed = state.is_pressed();
|
||||
match button {
|
||||
MouseButton::Left => buttons.left.update(pressed),
|
||||
MouseButton::Right => buttons.right.update(pressed),
|
||||
MouseButton::Middle => buttons.middle.update(pressed),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
let mut delta = match *delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
|
||||
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
|
||||
};
|
||||
if delta.x == 0.0 && self.modifiers.shift {
|
||||
delta.x = delta.y;
|
||||
delta.y = 0.0;
|
||||
}
|
||||
self.cursor.scroll_delta = delta;
|
||||
self.cursor.time = Instant::now();
|
||||
}
|
||||
WindowEvent::CursorLeft { .. } => {
|
||||
self.cursor.exists = false;
|
||||
self.modifiers.clear();
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let Key::Named(named) = event.logical_key {
|
||||
let pressed = event.state.is_pressed();
|
||||
match named {
|
||||
NamedKey::Control => {
|
||||
self.modifiers.control = pressed;
|
||||
}
|
||||
NamedKey::Shift => {
|
||||
self.modifiers.shift = pressed;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn end_frame(&mut self) {
|
||||
self.cursor.end_frame();
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopUiState {
|
||||
/// Physical pixels, matching `WindowEvent::Resized` (what
|
||||
/// `UiRenderState::resize` is given) and the swapchain -- see
|
||||
/// `desktop::content_scale`.
|
||||
pub fn window_size(&self) -> Vec2 {
|
||||
let size = self.renderer.window().inner_size();
|
||||
Vec2::new(size.width as f32, size.height as f32)
|
||||
}
|
||||
|
||||
pub fn cursor_state(&self) -> &CursorState {
|
||||
&self.input.cursor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use std::io::Write;
|
||||
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record};
|
||||
|
||||
/// Reads one level name from `RUST_LOG` -- `off`, `error`, `warn`,
|
||||
/// `info`, `debug`, `trace`, case-insensitively. **Not env_logger's
|
||||
/// per-module filter syntax**: anything else is ignored and the default
|
||||
/// stands, rather than being silently read as "off", since a typo that
|
||||
/// turned logging off would be indistinguishable from a quiet program.
|
||||
fn level_from_env(default: LevelFilter) -> LevelFilter {
|
||||
match std::env::var("RUST_LOG") {
|
||||
Ok(text) => text.trim().parse().unwrap_or(default),
|
||||
Err(_) => default,
|
||||
}
|
||||
}
|
||||
|
||||
struct StderrLogger {
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
impl Log for StderrLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
// One write, not a `writeln!` per part: two threads logging at
|
||||
// once interleave otherwise, and the frame and input traces are
|
||||
// both written from whichever thread produced them.
|
||||
let line = format!(
|
||||
"{level:<5} {target}: {args}\n",
|
||||
level = match record.level() {
|
||||
Level::Error => "ERROR",
|
||||
Level::Warn => "WARN",
|
||||
Level::Info => "INFO",
|
||||
Level::Debug => "DEBUG",
|
||||
Level::Trace => "TRACE",
|
||||
},
|
||||
target = record.target(),
|
||||
args = record.args(),
|
||||
);
|
||||
let _ = std::io::stderr().write_all(line.as_bytes());
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
let _ = std::io::stderr().flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install(default: LevelFilter) {
|
||||
let level = level_from_env(default);
|
||||
let logger = Box::leak(Box::new(StderrLogger { level }));
|
||||
if log::set_logger(logger).is_ok() {
|
||||
log::set_max_level(level);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
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<DesktopRsc<State>> for DesktopUiState {
|
||||
fn set_root(&mut self, rsc: &mut DesktopRsc<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 DesktopRsc<Self>, proxy: Proxy<Self::Event>)
|
||||
-> Self;
|
||||
#[allow(unused_variables)]
|
||||
fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc<Self>) {}
|
||||
#[allow(unused_variables)]
|
||||
fn exit(&mut self, rsc: &mut DesktopRsc<Self>) {}
|
||||
#[allow(unused_variables)]
|
||||
fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc<Self>) {}
|
||||
fn window_attributes() -> WindowAttributes {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub type DesktopRsc<State> = AppRsc<State>;
|
||||
|
||||
pub struct DesktopApp<State: DesktopAppState> {
|
||||
rsc: DesktopRsc<State>,
|
||||
state: State,
|
||||
task_recv: TaskMsgReceiver<DesktopRsc<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) = AppRsc::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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::platform::OpenUrl;
|
||||
use crate::prelude::HasDesktopUiState;
|
||||
|
||||
impl<T: HasDesktopUiState> OpenUrl for T {
|
||||
fn open_url(&mut self, url: &str) {
|
||||
let (program, first): (&str, &[&str]) = if cfg!(target_os = "macos") {
|
||||
("open", &[])
|
||||
} else if cfg!(target_os = "windows") {
|
||||
("cmd", &["/C", "start", ""])
|
||||
} else {
|
||||
("xdg-open", &[])
|
||||
};
|
||||
match std::process::Command::new(program)
|
||||
.args(first)
|
||||
.arg(url)
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => log::warn!("could not open {url} with {program}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
use crate::task::RequestRedraw;
|
||||
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use wgpu::*;
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
|
||||
|
||||
impl RequestRedraw for Window {
|
||||
fn request_redraw(&self) {
|
||||
Window::request_redraw(self);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiRenderer {
|
||||
window: Arc<Window>,
|
||||
surface: Surface<'static>,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
view_format: TextureFormat,
|
||||
encoder: CommandEncoder,
|
||||
pub ui: UiRenderNode,
|
||||
}
|
||||
|
||||
impl UiRenderer {
|
||||
pub fn update(&mut self, ui: &mut Ui) {
|
||||
self.ui.update(&self.device, &self.queue, ui);
|
||||
}
|
||||
|
||||
/// The two waits, so a desktop frame divides up the same way an
|
||||
/// Android one does -- see `AndroidRenderer::draw` for why the
|
||||
/// swapchain acquire is measured apart from the work.
|
||||
pub fn draw(&mut self) -> FrameParts {
|
||||
let acquire_start = Instant::now();
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture)
|
||||
| CurrentSurfaceTexture::Suboptimal(texture) => texture,
|
||||
// wgpu 30 turned this Result into an enum; every arm here was an
|
||||
// `Err` the previous `.unwrap()` panicked on, except `Occluded`,
|
||||
// which is new. Named rather than swallowed: a window that stops
|
||||
// presenting silently is the state this file's `pre_present_notify`
|
||||
// comment was written about.
|
||||
other => panic!("no surface texture to draw into: {other:?}"),
|
||||
};
|
||||
let acquire = acquire_start.elapsed();
|
||||
let view = output.texture.create_view(&TextureViewDescriptor {
|
||||
format: Some(self.view_format),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(CLEAR_COLOR.to_wgpu()),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
..Default::default()
|
||||
});
|
||||
self.ui.draw(render_pass);
|
||||
}
|
||||
|
||||
let submit_start = Instant::now();
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
FrameParts::waits(acquire, submit_start.elapsed())
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
self.config.width = size.width;
|
||||
self.config.height = size.height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.ui.resize(
|
||||
Vec2::new(size.width as f32, size.height as f32),
|
||||
&self.queue,
|
||||
);
|
||||
}
|
||||
|
||||
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||
device.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
// `force-gles` on the desktop too, not just on Android: the
|
||||
// GLES backend has behaviour of its own (a one-layer array
|
||||
// texture is a `GL_TEXTURE_2D` -- see
|
||||
// `GpuTextures::create_array_texture`), and a machine with a
|
||||
// real GPU is where that is cheap to reproduce and screenshot.
|
||||
let mut backends = if cfg!(feature = "force-gles") {
|
||||
Backends::GL
|
||||
} else {
|
||||
Backends::PRIMARY
|
||||
};
|
||||
// The display handle comes from the window rather than being left
|
||||
// out: wgpu 30 asks for it whenever a GLES surface is going to be
|
||||
// presented on Wayland, which is exactly what the fallback below
|
||||
// produces on this machine.
|
||||
let mut instance = Instance::new(InstanceDescriptor {
|
||||
backends,
|
||||
..InstanceDescriptor::new_with_display_handle(Box::new(window.clone()))
|
||||
});
|
||||
if backends != Backends::GL && instance.enumerate_adapters(backends).block_on().is_empty() {
|
||||
log::warn!(
|
||||
"iris renderer: no {backends:?} adapter on this machine, falling back to GLES"
|
||||
);
|
||||
backends = Backends::GL;
|
||||
instance = Instance::new(InstanceDescriptor {
|
||||
backends,
|
||||
..InstanceDescriptor::new_with_display_handle(Box::new(window.clone()))
|
||||
});
|
||||
}
|
||||
|
||||
let surface = instance
|
||||
.create_surface(window.clone())
|
||||
.expect("Could not create window surface!");
|
||||
|
||||
let adapter = instance
|
||||
.request_adapter(&RequestAdapterOptions {
|
||||
power_preference: PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("No usable GPU adapter for backends {backends:?}: {error}")
|
||||
});
|
||||
|
||||
{
|
||||
let info = adapter.get_info();
|
||||
log::info!(
|
||||
"iris renderer: {name} ({backend:?}, {driver}{driver_info}) on {backends:?}",
|
||||
name = info.name,
|
||||
backend = info.backend,
|
||||
driver = info.driver,
|
||||
driver_info = if info.driver_info.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", info.driver_info)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// No features beyond what wgpu asks for by default, and no
|
||||
// binding-array limits: the atlas is one texture_2d_array and a
|
||||
// standalone image is its own ordinary bind group, neither of which
|
||||
// needs descriptor indexing. See TEXTURES.md's "Recommended shape"
|
||||
// for why the old binding array asked for
|
||||
// VK_EXT_descriptor_indexing unconditionally and did not survive a
|
||||
// real share of Android GPUs. `iris_core::device_limits()` is
|
||||
// shared with the Android backend; see its own doc for why it is
|
||||
// not simply `Limits::default()`.
|
||||
let (device, queue) = adapter
|
||||
.request_device(&DeviceDescriptor {
|
||||
required_limits: iris_core::device_limits(),
|
||||
..Default::default()
|
||||
})
|
||||
.block_on()
|
||||
.expect("Could not get device!");
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let formats = iris_core::srgb_surface_format(&surface_caps)
|
||||
.expect("Could not select an sRGB iris surface format");
|
||||
log::info!(
|
||||
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||
formats.surface,
|
||||
formats.view,
|
||||
);
|
||||
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: formats.surface,
|
||||
// wgpu 30's new field; `Auto` is what every earlier version did.
|
||||
color_space: SurfaceColorSpace::Srgb,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
// Vsync, because a toolkit aiming at battery life must not present
|
||||
// frames a display will never show: AutoNoVsync accepts them as
|
||||
// fast as the GPU will take them, so a redraw burst costs whatever
|
||||
// the hardware can be made to do rather than one frame.
|
||||
// AutoVsync picks Fifo, which every backend supports.
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: (formats.view != formats.surface)
|
||||
.then_some(formats.view)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
|
||||
// Unlike the Android backend, the desktop backend has no on-screen
|
||||
// fallback to show a diagnostic through, so a renderer-creation
|
||||
// failure still panics here -- but now with wgpu's full "Caused
|
||||
// by:" chain as the message, since `UiRenderNode::new` returns it
|
||||
// rather than letting wgpu's own default handler panic first (see
|
||||
// that function's doc comment).
|
||||
// Physical size, the same units the swapchain, `WindowEvent::
|
||||
// Resized`, the pointer and the widget tree all use -- see
|
||||
// `desktop::content_scale` for why this backend stopped dividing
|
||||
// into a separate logical space, and what disagreed while it did.
|
||||
let physical_size = Vec2::new(size.width as f32, size.height as f32);
|
||||
let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size)
|
||||
.expect("Could not create iris render node!");
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
view_format: formats.view,
|
||||
encoder,
|
||||
ui,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn window(&self) -> &Window {
|
||||
self.window.as_ref()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user