diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84ae7891a3..065102d1a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,6 +108,49 @@ jobs: args: --manifest-path=docs/book_examples/Cargo.toml if: contains(matrix.os, 'mac') + # we test the wayland backend as a separate job + test-stable-wayland: + runs-on: ubuntu-latest + name: cargo clippy+test (wayland) + steps: + - uses: actions/checkout@v2 + + - name: install wayland + run: | + sudo apt update + sudo apt install libwayland-dev libpango1.0-dev libxkbcommon-dev + + - name: install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + profile: minimal + override: true + + - name: restore cache + uses: Swatinem/rust-cache@v1 + + - name: cargo clippy druid-shell + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --manifest-path=druid-shell/Cargo.toml --all-targets --features wayland --no-default-features -- -D warnings + + - name: cargo test druid-shell + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid-shell/Cargo.toml --features wayland --no-default-features + + # We use --all-targets to skip doc tests; there are no wayland-specific + # doctests in the main druid crate anyway + - name: cargo test druid + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid/Cargo.toml --all-targets --features=svg,image,im --features wayland --no-default-features + # we test the gtk backend as a separate job because gtk install takes # a long time. test-stable-gtk: diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index 79a795b60d..cb6081bcaf 100755 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -17,6 +17,20 @@ default-target = "x86_64-pc-windows-msvc" default = ["gtk"] gtk = ["gdk-sys", "glib-sys", "gtk-sys", "gtk-rs"] x11 = ["x11rb", "nix", "cairo-sys-rs", "bindgen", "pkg-config"] +wayland = [ + "wayland-client", + "wayland-protocols/client", + "wayland-protocols/unstable_protocols", + "nix", + "cairo-sys-rs", + "rand", + "calloop", + "wayland-cursor", + "log", + "im", + "bindgen", + "pkg-config", +] # Implement HasRawWindowHandle for WindowHandle raw-win-handle = ["raw-window-handle"] @@ -86,6 +100,13 @@ glib-sys = { version = "0.14.0", optional = true } gtk-sys = { version = "0.14.0", optional = true } nix = { version = "0.18.0", optional = true } x11rb = { version = "0.8.0", features = ["allow-unsafe-code", "present", "render", "randr", "xfixes", "xkb", "resource_manager", "cursor"], optional = true } +wayland-client = { version = "0.29", optional = true } +wayland-protocols = { version = "0.29", optional = true } +wayland-cursor = { version = "0.29", optional = true } +rand = { version = "0.8.0", optional = true } +calloop = { version = "0.7.1", optional = true } +log = { version = "0.4.14", optional = true } +im = { version = "15.0.0", optional = true } [target.'cfg(target_arch="wasm32")'.dependencies] wasm-bindgen = "0.2.67" diff --git a/druid-shell/build.rs b/druid-shell/build.rs index c35d720e84..8bac82b53f 100644 --- a/druid-shell/build.rs +++ b/druid-shell/build.rs @@ -1,7 +1,7 @@ -#[cfg(not(feature = "x11"))] +#[cfg(not(any(feature = "x11", feature = "wayland")))] fn main() {} -#[cfg(feature = "x11")] +#[cfg(any(feature = "x11", feature = "wayland"))] fn main() { use pkg_config::probe_library; use std::env; @@ -14,19 +14,25 @@ fn main() { } probe_library("xkbcommon").unwrap(); + + #[cfg(feature = "x11")] probe_library("xkbcommon-x11").unwrap(); + let mut header = "\ +#include +#include +#include " + .to_string(); + + if cfg!(feature = "x11") { + header += " +#include "; + } + let bindings = bindgen::Builder::default() // The input header we would like to generate // bindings for. - .header_contents( - "wrapper.h", - "\ -#include -#include -#include -#include ", - ) + .header_contents("wrapper.h", &header) // Tell cargo to invalidate the built crate whenever any of the // included header files changed. .parse_callbacks(Box::new(bindgen::CargoCallbacks)) diff --git a/druid-shell/src/backend/mod.rs b/druid-shell/src/backend/mod.rs index f07150398d..9f7585c8f5 100644 --- a/druid-shell/src/backend/mod.rs +++ b/druid-shell/src/backend/mod.rs @@ -35,11 +35,30 @@ pub use x11::*; #[cfg(all(feature = "x11", any(target_os = "linux", target_os = "openbsd")))] pub(crate) mod shared; -#[cfg(all(not(feature = "x11"), any(target_os = "linux", target_os = "openbsd")))] +#[cfg(all(feature = "wayland", any(target_os = "linux", target_os = "openbsd")))] +mod wayland; +#[cfg(all(feature = "wayland", any(target_os = "linux", target_os = "openbsd")))] +pub use wayland::*; +#[cfg(all(feature = "wayland", any(target_os = "linux", target_os = "openbsd")))] +pub(crate) mod shared; + +#[cfg(all( + not(feature = "x11"), + not(feature = "wayland"), + any(target_os = "linux", target_os = "openbsd") +))] mod gtk; -#[cfg(all(not(feature = "x11"), any(target_os = "linux", target_os = "openbsd")))] +#[cfg(all( + not(feature = "x11"), + not(feature = "wayland"), + any(target_os = "linux", target_os = "openbsd") +))] pub use self::gtk::*; -#[cfg(all(not(feature = "x11"), any(target_os = "linux", target_os = "openbsd")))] +#[cfg(all( + not(feature = "x11"), + not(feature = "wayland"), + any(target_os = "linux", target_os = "openbsd") +))] pub(crate) mod shared; #[cfg(target_arch = "wasm32")] diff --git a/druid-shell/src/backend/shared/keyboard.rs b/druid-shell/src/backend/shared/keyboard.rs index 2d9ac9a999..db925b5f24 100644 --- a/druid-shell/src/backend/shared/keyboard.rs +++ b/druid-shell/src/backend/shared/keyboard.rs @@ -18,7 +18,10 @@ use keyboard_types::{Code, Location}; #[cfg(any( - all(feature = "x11", any(target_os = "linux", target_os = "openbsd")), + all( + any(feature = "x11", feature = "wayland"), + any(target_os = "linux", target_os = "openbsd") + ), target_os = "macos" ))] /// Map key code to location. diff --git a/druid-shell/src/backend/shared/linux/env.rs b/druid-shell/src/backend/shared/linux/env.rs new file mode 100644 index 0000000000..21a1a3d34d --- /dev/null +++ b/druid-shell/src/backend/shared/linux/env.rs @@ -0,0 +1,39 @@ +pub fn locale() -> String { + fn locale_env_var(var: &str) -> Option { + match std::env::var(var) { + Ok(s) if s.is_empty() => { + tracing::debug!("locale: ignoring empty env var {}", var); + None + } + Ok(s) => { + tracing::debug!("locale: env var {} found: {:?}", var, &s); + Some(s) + } + Err(std::env::VarError::NotPresent) => { + tracing::debug!("locale: env var {} not found", var); + None + } + Err(std::env::VarError::NotUnicode(_)) => { + tracing::debug!("locale: ignoring invalid unicode env var {}", var); + None + } + } + } + + // from gettext manual + // https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html#Locale-Environment-Variables + let mut locale = locale_env_var("LANGUAGE") + // the LANGUAGE value is priority list seperated by : + // See: https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html#The-LANGUAGE-variable + .and_then(|locale| locale.split(':').next().map(String::from)) + .or_else(|| locale_env_var("LC_ALL")) + .or_else(|| locale_env_var("LC_MESSAGES")) + .or_else(|| locale_env_var("LANG")) + .unwrap_or_else(|| "en-US".to_string()); + + // This is done because the locale parsing library we use expects an unicode locale, but these vars have an ISO locale + if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') { + locale.truncate(idx); + } + locale +} diff --git a/druid-shell/src/backend/shared/linux/mod.rs b/druid-shell/src/backend/shared/linux/mod.rs new file mode 100644 index 0000000000..d40b15d94a --- /dev/null +++ b/druid-shell/src/backend/shared/linux/mod.rs @@ -0,0 +1,2 @@ +// environment based utilities +pub mod env; diff --git a/druid-shell/src/backend/shared/mod.rs b/druid-shell/src/backend/shared/mod.rs index da805c19ab..998b929f5b 100644 --- a/druid-shell/src/backend/shared/mod.rs +++ b/druid-shell/src/backend/shared/mod.rs @@ -20,3 +20,11 @@ cfg_if::cfg_if! { pub use keyboard::*; } } +cfg_if::cfg_if! { + if #[cfg(all(target_os = "linux", any(feature = "x11", feature = "wayland")))] { + mod timer; + pub(crate) use timer::*; + pub(crate) mod xkb; + pub(crate) mod linux; + } +} diff --git a/druid-shell/src/backend/shared/timer.rs b/druid-shell/src/backend/shared/timer.rs new file mode 100644 index 0000000000..0d22589e6c --- /dev/null +++ b/druid-shell/src/backend/shared/timer.rs @@ -0,0 +1,44 @@ +use crate::TimerToken; +use std::{cmp::Ordering, time::Instant}; + +/// A timer is a deadline (`std::Time::Instant`) and a `TimerToken`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Timer { + deadline: Instant, + token: TimerToken, + pub data: T, +} + +impl Timer { + pub(crate) fn new(deadline: Instant, data: T) -> Self { + let token = TimerToken::next(); + Self { + deadline, + token, + data, + } + } + + pub(crate) fn deadline(&self) -> Instant { + self.deadline + } + + pub(crate) fn token(&self) -> TimerToken { + self.token + } +} + +impl Ord for Timer { + /// Ordering is so that earliest deadline sorts first + // "Earliest deadline first" that a std::collections::BinaryHeap will have the earliest timer + // at its head, which is just what is needed for timer management. + fn cmp(&self, other: &Self) -> Ordering { + self.deadline.cmp(&other.deadline).reverse() + } +} + +impl PartialOrd for Timer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/druid-shell/src/backend/x11/keycodes.rs b/druid-shell/src/backend/shared/xkb/keycodes.rs similarity index 99% rename from druid-shell/src/backend/x11/keycodes.rs rename to druid-shell/src/backend/shared/xkb/keycodes.rs index 3b5a0bb9ac..08adb56db2 100644 --- a/druid-shell/src/backend/x11/keycodes.rs +++ b/druid-shell/src/backend/shared/xkb/keycodes.rs @@ -8,6 +8,7 @@ use super::xkbcommon_sys::*; pub fn map_key(keysym: u32) -> Key { use Key::*; match keysym { + XKB_KEY_v => Key::Character("v".to_string()), XKB_KEY_BackSpace => Backspace, XKB_KEY_Tab | XKB_KEY_KP_Tab | XKB_KEY_ISO_Left_Tab => Tab, XKB_KEY_Clear | XKB_KEY_KP_Begin | XKB_KEY_XF86Clear => Clear, diff --git a/druid-shell/src/backend/x11/xkb.rs b/druid-shell/src/backend/shared/xkb/mod.rs similarity index 86% rename from druid-shell/src/backend/x11/xkb.rs rename to druid-shell/src/backend/shared/xkb/mod.rs index 576d5fd3fa..fbddfd2b4c 100644 --- a/druid-shell/src/backend/x11/xkb.rs +++ b/druid-shell/src/backend/shared/xkb/mod.rs @@ -14,19 +14,23 @@ //! A minimal wrapper around Xkb for our use. -use super::keycodes; -use super::xkbcommon_sys::*; +mod keycodes; +mod xkbcommon_sys; use crate::{ backend::shared::{code_to_location, hardware_keycode_to_code}, KeyEvent, KeyState, Modifiers, }; use keyboard_types::{Code, Key}; use std::convert::TryFrom; -use std::os::raw::{c_char, c_int}; +use std::os::raw::c_char; use std::ptr; +use xkbcommon_sys::*; + +#[cfg(feature = "x11")] use x11rb::xcb_ffi::XCBConnection; -pub struct DeviceId(c_int); +#[cfg(feature = "x11")] +pub struct DeviceId(std::os::raw::c_int); /// A global xkb context object. /// @@ -42,6 +46,7 @@ impl Context { unsafe { Self(xkb_context_new(XKB_CONTEXT_NO_FLAGS)) } } + #[cfg(feature = "x11")] pub fn core_keyboard_device_id(&self, conn: &XCBConnection) -> Option { let id = unsafe { xkb_x11_get_core_keyboard_device_id( @@ -55,6 +60,7 @@ impl Context { } } + #[cfg(feature = "x11")] pub fn keymap_from_device(&self, conn: &XCBConnection, device: DeviceId) -> Option { let key_map = unsafe { xkb_x11_keymap_new_from_device( @@ -70,10 +76,35 @@ impl Context { Some(Keymap(key_map)) } + /// Create a keymap from some given data. + /// + /// Uses `xkb_keymap_new_from_buffer` under the hood. + #[cfg(feature = "wayland")] + pub fn keymap_from_slice(&self, buffer: &[u8]) -> Keymap { + // TODO we hope that the keymap doesn't borrow the underlying data. If it does' we need to + // use Rc. We'll find out soon enough if we get a segfault. + // TODO we hope that the keymap inc's the reference count of the context. + assert!( + buffer.iter().copied().any(|byte| byte == 0), + "`keymap_from_slice` expects a null-terminated string" + ); + unsafe { + let keymap = xkb_keymap_new_from_string( + self.0, + buffer.as_ptr() as *const i8, + XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + assert!(!keymap.is_null()); + Keymap(keymap) + } + } + /// Set the log level using `tracing` levels. /// /// Because `xkb` has a `critical` error, each rust error maps to 1 above (e.g. error -> /// critical, warn -> error etc.) + #[allow(unused)] pub fn set_log_level(&self, level: tracing::Level) { use tracing::Level; let level = match level { diff --git a/druid-shell/src/backend/x11/xkbcommon_sys.rs b/druid-shell/src/backend/shared/xkb/xkbcommon_sys.rs similarity index 100% rename from druid-shell/src/backend/x11/xkbcommon_sys.rs rename to druid-shell/src/backend/shared/xkb/xkbcommon_sys.rs diff --git a/druid-shell/src/backend/wayland/.README.md b/druid-shell/src/backend/wayland/.README.md new file mode 100644 index 0000000000..8cbb7baae1 --- /dev/null +++ b/druid-shell/src/backend/wayland/.README.md @@ -0,0 +1,3 @@ +### development notes +- setting `export WAYLAND_DEBUG=1` allows you to see the various API calls and their values sent to wayland. +- wlroots repository was a bunch of examples you can run as a reference to see the output of `WAYLAND_DEBUG`. \ No newline at end of file diff --git a/druid-shell/src/backend/wayland/application.rs b/druid-shell/src/backend/wayland/application.rs new file mode 100644 index 0000000000..963e466d43 --- /dev/null +++ b/druid-shell/src/backend/wayland/application.rs @@ -0,0 +1,596 @@ +// Copyright 2019 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(clippy::single_match)] + +use super::{ + clipboard, display, error::Error, events::WaylandSource, keyboard, outputs, pointers, surfaces, + window::WindowHandle, +}; + +use crate::{backend, kurbo, mouse, AppHandler, TimerToken}; + +use calloop; + +use std::{ + cell::{Cell, RefCell}, + collections::{BTreeMap, BinaryHeap}, + rc::Rc, + time::{Duration, Instant}, +}; + +use crate::backend::shared::linux; +use wayland_client::protocol::wl_keyboard::WlKeyboard; +use wayland_client::protocol::wl_registry; +use wayland_client::{ + self as wl, + protocol::{ + wl_compositor::WlCompositor, + wl_pointer::WlPointer, + wl_seat::{self, WlSeat}, + wl_shm::{self, WlShm}, + wl_surface::WlSurface, + }, +}; +use wayland_cursor::CursorTheme; +use wayland_protocols::unstable::xdg_decoration::v1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use wayland_protocols::wlr::unstable::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1; +use wayland_protocols::xdg_shell::client::xdg_positioner::XdgPositioner; +use wayland_protocols::xdg_shell::client::xdg_surface; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Timer(backend::shared::Timer); + +impl Timer { + pub(crate) fn new(id: u64, deadline: Instant) -> Self { + Self(backend::shared::Timer::new(deadline, id)) + } + + pub(crate) fn id(self) -> u64 { + self.0.data + } + + pub(crate) fn deadline(&self) -> Instant { + self.0.deadline() + } + + pub fn token(&self) -> TimerToken { + self.0.token() + } +} + +impl std::cmp::Ord for Timer { + /// Ordering is so that earliest deadline sorts first + // "Earliest deadline first" that a std::collections::BinaryHeap will have the earliest timer + // at its head, which is just what is needed for timer management. + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.deadline().cmp(&other.0.deadline()).reverse() + } +} + +impl std::cmp::PartialOrd for Timer { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone)] +pub struct Application { + pub(super) data: std::sync::Arc, +} + +#[allow(dead_code)] +pub(crate) struct ApplicationData { + pub(super) wayland: std::sync::Arc, + pub(super) zxdg_decoration_manager_v1: wl::Main, + pub(super) zwlr_layershell_v1: wl::Main, + pub(super) wl_compositor: wl::Main, + pub(super) wl_shm: wl::Main, + /// A map of wayland object IDs to outputs. + /// + /// Wayland will update this if the output change. Keep a record of the `Instant` you last + /// observed a change, and use `Output::changed` to see if there are any newer changes. + /// + /// It's a BTreeMap so the ordering is consistent when enumerating outputs (not sure if this is + /// necessary, but it negligable cost). + pub(super) outputs: Rc>>, + pub(super) seats: Rc>>>>, + + /// Handles to any surfaces that have been created. + pub(super) handles: RefCell>, + + /// Available pixel formats + pub(super) formats: RefCell>, + /// Close flag + pub(super) shutdown: Cell, + /// The currently active surface, if any (by wayland object ID) + pub(super) active_surface_id: RefCell>, + // Stuff for timers + /// A calloop event source for timers. We always set it to fire at the next set timer, if any. + pub(super) timer_handle: calloop::timer::TimerHandle, + /// We stuff this here until the event loop, then `take` it and use it. + timer_source: RefCell>>, + /// Currently pending timers + /// + /// The extra data is the surface this timer is for. + pub(super) timers: RefCell>, + + pub(super) roundtrip_requested: RefCell, + + /// track if the display was flushed during the event loop. + /// prevents double flushing unnecessarily. + pub(super) display_flushed: RefCell, + /// reference to the pointer events manager. + pub(super) pointer: pointers::Pointer, + /// reference to the keyboard events manager. + keyboard: keyboard::Manager, + clipboard: clipboard::Manager, + // wakeup events when outputs are added/removed. + outputsqueue: RefCell>>, +} + +impl Application { + pub fn new() -> Result { + tracing::info!("wayland application initiated"); + + // Global objects that can come and go (so we must handle them dynamically). + // + // They have to be behind a shared pointer because wayland may need to add or remove them + // for the life of the application. Use weak rcs inside the callbacks to avoid leaking + // memory. + let dispatcher = display::Dispatcher::default(); + let outputqueue = outputs::auto(&dispatcher)?; + + let seats: Rc>>>> = + Rc::new(RefCell::new(BTreeMap::new())); + // This object will create a container for the global wayland objects, and request that + // it is populated by the server. Doesn't take ownership of the registry, we are + // responsible for keeping it alive. + let weak_seats = Rc::downgrade(&seats); + + display::GlobalEventDispatch::subscribe( + &dispatcher, + move |event: &'_ wl::GlobalEvent, + registry: &'_ wl::Attached, + _ctx: &'_ wl::DispatchData| { + match event { + wl::GlobalEvent::New { + id, + interface, + version, + } => { + let id = *id; + let version = *version; + + if !(interface.as_str() == "wl_seat" && version >= 7) { + return; + } + tracing::debug!("seat detected {:?} {:?} {:?}", interface, id, version); + let new_seat = registry.bind::(7, id); + let prev_seat = weak_seats + .upgrade() + .unwrap() + .borrow_mut() + .insert(id, Rc::new(RefCell::new(Seat::new(new_seat)))); + assert!( + prev_seat.is_none(), + "internal: wayland should always use new IDs" + ); + // Defer setting up the pointer/keyboard event handling until we've + // finished constructing the `Application`. That way we can pass it as a + // parameter. + } + wl::GlobalEvent::Removed { .. } => { + // nothing to do. + } + }; + }, + ); + + let env = display::new(dispatcher)?; + + display::print(&env.registry); + + let zxdg_decoration_manager_v1 = env + .registry + .instantiate_exact::(1) + .map_err(|e| Error::global("zxdg_decoration_manager_v1", 1, e))?; + let zwlr_layershell_v1 = env + .registry + .instantiate_exact::(1) + .map_err(|e| Error::global("zwlr_layershell_v1", 1, e))?; + let wl_compositor = env + .registry + .instantiate_exact::(4) + .map_err(|e| Error::global("wl_compositor", 4, e))?; + let wl_shm = env + .registry + .instantiate_exact::(1) + .map_err(|e| Error::global("wl_shm", 1, e))?; + + let timer_source = calloop::timer::Timer::new().unwrap(); + let timer_handle = timer_source.handle(); + + // TODO the cursor theme size needs more refinement, it should probably be the size needed to + // draw sharp cursors on the largest scaled monitor. + let pointer = pointers::Pointer::new( + CursorTheme::load(64, &wl_shm), + wl_compositor.create_surface(), + ); + + // We need to have keyboard events set up for our seats before the next roundtrip. + let app_data = std::sync::Arc::new(ApplicationData { + zxdg_decoration_manager_v1, + zwlr_layershell_v1, + wl_compositor, + wl_shm: wl_shm.clone(), + outputs: Rc::new(RefCell::new(BTreeMap::new())), + seats, + handles: RefCell::new(im::OrdMap::new()), + formats: RefCell::new(vec![]), + shutdown: Cell::new(false), + active_surface_id: RefCell::new(std::collections::VecDeque::with_capacity(20)), + timer_handle, + timer_source: RefCell::new(Some(timer_source)), + timers: RefCell::new(BinaryHeap::new()), + display_flushed: RefCell::new(false), + pointer, + keyboard: keyboard::Manager::default(), + clipboard: clipboard::Manager::new(&env.display, &env.registry)?, + roundtrip_requested: RefCell::new(false), + outputsqueue: RefCell::new(Some(outputqueue)), + wayland: env, + }); + + for m in outputs::current()? { + app_data.outputs.borrow_mut().insert(m.id(), m); + } + + // Collect the supported image formats. + wl_shm.quick_assign(with_cloned!(app_data; move |d1, event, d3| { + tracing::debug!("shared memory events {:?} {:?} {:?}", d1, event, d3); + match event { + wl_shm::Event::Format { format } => app_data.formats.borrow_mut().push(format), + _ => (), // ignore other messages + } + })); + + // Setup seat event listeners with our application + for (id, seat) in app_data.seats.borrow().iter() { + let id = *id; // move into closure. + let wl_seat = seat.borrow().wl_seat.clone(); + wl_seat.quick_assign(with_cloned!(seat, app_data; move |d1, event, d3| { + tracing::debug!("seat events {:?} {:?} {:?}", d1, event, d3); + let mut seat = seat.borrow_mut(); + app_data.clipboard.attach(&mut seat); + match event { + wl_seat::Event::Capabilities { capabilities } => { + seat.capabilities = capabilities; + if capabilities.contains(wl_seat::Capability::Keyboard) + && seat.keyboard.is_none() + { + seat.keyboard = Some(app_data.keyboard.attach(id, seat.wl_seat.clone())); + } + if capabilities.contains(wl_seat::Capability::Pointer) + && seat.pointer.is_none() + { + let pointer = seat.wl_seat.get_pointer(); + app_data.pointer.attach(pointer.detach()); + pointer.quick_assign({ + let app = app_data.clone(); + move |pointer, event, _| { + pointers::Pointer::consume(app.clone(), pointer.detach(), event); + } + }); + seat.pointer = Some(pointer); + } + // Dont worry if they go away - we will just stop receiving events. If the + // capability comes back we will start getting events again. + } + wl_seat::Event::Name { name } => { + seat.name = name; + } + _ => tracing::info!("seat quick assign unknown event {:?}", event), // ignore future events + } + })); + } + + // Let wayland finish setup before we allow the client to start creating windows etc. + app_data.sync()?; + + Ok(Application { data: app_data }) + } + + pub fn run(mut self, _handler: Option>) { + tracing::info!("wayland event loop initiated"); + // NOTE if we want to call this function more than once, we will need to put the timer + // source back. + let timer_source = self.data.timer_source.borrow_mut().take().unwrap(); + // flush pending events (otherwise anything we submitted since sync will never be sent) + self.data.wayland.display.flush().unwrap(); + + // Use calloop so we can epoll both wayland events and others (e.g. timers) + let mut eventloop = calloop::EventLoop::try_new().unwrap(); + let handle = eventloop.handle(); + + let wayland_dispatcher = WaylandSource::new(self.data.clone()).into_dispatcher(); + + self.data.keyboard.events(&handle); + + handle.register_dispatcher(wayland_dispatcher).unwrap(); + handle + .insert_source(self.data.outputsqueue.take().unwrap(), { + move |evt, _ignored, appdata| match evt { + calloop::channel::Event::Closed => {} + calloop::channel::Event::Msg(output) => match output { + outputs::Event::Located(output) => { + appdata + .outputs + .borrow_mut() + .insert(output.id(), output.clone()); + for (_, win) in appdata.handles_iter() { + surfaces::Outputs::inserted(&win, &output); + } + } + outputs::Event::Removed(output) => { + appdata.outputs.borrow_mut().remove(&output.id()); + for (_, win) in appdata.handles_iter() { + surfaces::Outputs::removed(&win, &output); + } + } + }, + } + }) + .unwrap(); + + handle + .insert_source(timer_source, move |token, _metadata, appdata| { + tracing::trace!("timer source {:?}", token); + appdata.handle_timer_event(token); + }) + .unwrap(); + + let signal = eventloop.get_signal(); + let handle = handle.clone(); + + let res = eventloop.run(Duration::from_millis(20), &mut self.data, move |appdata| { + if appdata.shutdown.get() { + tracing::debug!("shutting down, requested"); + signal.stop(); + return; + } + + if appdata.handles.borrow().len() == 0 { + tracing::debug!("shutting down, no window remaining"); + signal.stop(); + return; + } + + ApplicationData::idle_repaint(handle.clone()); + }); + + match res { + Ok(_) => tracing::info!("wayland event loop completed"), + Err(cause) => tracing::error!("wayland event loop failed {:?}", cause), + } + } + + pub fn quit(&self) { + self.data.shutdown.set(true); + } + + pub fn clipboard(&self) -> clipboard::Clipboard { + clipboard::Clipboard::from(&self.data.clipboard) + } + + pub fn get_locale() -> String { + linux::env::locale() + } +} + +impl surfaces::Compositor for ApplicationData { + fn output(&self, id: u32) -> Option { + self.outputs.borrow().get(&id).cloned() + } + + fn create_surface(&self) -> wl::Main { + self.wl_compositor.create_surface() + } + + fn shared_mem(&self) -> wl::Main { + self.wl_shm.clone() + } + + fn get_xdg_positioner(&self) -> wl::Main { + self.wayland.xdg_base.create_positioner() + } + + fn get_xdg_surface(&self, s: &wl::Main) -> wl::Main { + self.wayland.xdg_base.get_xdg_surface(s) + } + + fn zxdg_decoration_manager_v1(&self) -> wl::Main { + self.zxdg_decoration_manager_v1.clone() + } + + fn zwlr_layershell_v1(&self) -> wl::Main { + self.zwlr_layershell_v1.clone() + } +} + +impl ApplicationData { + pub(crate) fn set_cursor(&self, cursor: &mouse::Cursor) { + self.pointer.replace(cursor); + } + + /// Send all pending messages and process all received messages. + /// + /// Don't use this once the event loop has started. + pub(crate) fn sync(&self) -> Result<(), Error> { + self.wayland + .queue + .borrow_mut() + .sync_roundtrip(&mut (), |evt, _, _| { + panic!("unexpected wayland event: {:?}", evt) + }) + .map_err(Error::fatal)?; + Ok(()) + } + + fn current_window_id(&self) -> u64 { + static DEFAULT: u64 = 0_u64; + *self.active_surface_id.borrow().get(0).unwrap_or(&DEFAULT) + } + + pub(super) fn initial_window_size(&self, defaults: kurbo::Size) -> kurbo::Size { + // compute the initial window size. + let initialwidth = if defaults.width == 0.0 { + f64::INFINITY + } else { + defaults.width + }; + let initialheight = if defaults.height == 0.0 { + f64::INFINITY + } else { + defaults.height + }; + return self.outputs.borrow().iter().fold( + kurbo::Size::from((initialwidth, initialheight)), + |computed, entry| { + kurbo::Size::new( + computed.width.min(entry.1.logical.width.into()), + computed.height.min(entry.1.logical.height.into()), + ) + }, + ); + } + + pub(super) fn acquire_current_window(&self) -> Option { + self.handles + .borrow() + .get(&self.current_window_id()) + .cloned() + } + + fn handle_timer_event(&self, _token: TimerToken) { + // Don't borrow the timers in case the callbacks want to add more. + let mut expired_timers = Vec::with_capacity(1); + let mut timers = self.timers.borrow_mut(); + let now = Instant::now(); + while matches!(timers.peek(), Some(timer) if timer.deadline() < now) { + // timer has passed + expired_timers.push(timers.pop().unwrap()); + } + drop(timers); + for expired in expired_timers { + let win = match self.handles.borrow().get(&expired.id()).cloned() { + Some(s) => s, + None => { + // NOTE this might be expected + tracing::warn!( + "received event for surface that doesn't exist any more {:?} {:?}", + expired, + expired.id() + ); + continue; + } + }; + // re-entrancy + if let Some(data) = win.data() { + data.handler.borrow_mut().timer(expired.token()) + } + } + + for (_, win) in self.handles_iter() { + if let Some(data) = win.data() { + data.run_deferred_tasks() + } + } + + // Get the deadline soonest and queue it. + if let Some(timer) = self.timers.borrow().peek() { + self.timer_handle + .add_timeout(timer.deadline() - now, timer.token()); + } + // Now flush so the events actually get sent (we don't do this automatically because we + // aren't in a wayland callback. + self.wayland.display.flush().unwrap(); + } + + /// Shallow clones surfaces so we can modify it during iteration. + fn handles_iter(&self) -> impl Iterator { + self.handles.borrow().clone().into_iter() + } + + fn idle_repaint(loophandle: calloop::LoopHandle<'_, std::sync::Arc>) { + loophandle.insert_idle({ + move |appdata| { + match appdata.acquire_current_window() { + Some(winhandle) => { + tracing::trace!("idle processing initiated"); + + winhandle.request_anim_frame(); + winhandle.run_idle(); + // if we already flushed this cycle don't flush again. + if *appdata.display_flushed.borrow() { + tracing::trace!("idle repaint flushing display initiated"); + if let Err(cause) = appdata.wayland.queue.borrow().display().flush() { + tracing::warn!("unable to flush display: {:?}", cause); + } + } + tracing::trace!("idle processing completed"); + } + None => tracing::error!( + "unable to acquire current window, skipping idle processing" + ), + }; + } + }); + } +} + +impl From for surfaces::CompositorHandle { + fn from(app: Application) -> surfaces::CompositorHandle { + surfaces::CompositorHandle::from(app.data) + } +} + +impl From> for surfaces::CompositorHandle { + fn from(data: std::sync::Arc) -> surfaces::CompositorHandle { + surfaces::CompositorHandle::direct( + std::sync::Arc::downgrade(&data) as std::sync::Weak + ) + } +} + +#[derive(Debug, Clone)] +pub struct Seat { + pub(super) wl_seat: wl::Main, + name: String, + capabilities: wl_seat::Capability, + keyboard: Option>, + pointer: Option>, +} + +impl Seat { + fn new(wl_seat: wl::Main) -> Self { + Self { + wl_seat, + name: "".into(), + capabilities: wl_seat::Capability::empty(), + keyboard: None, + pointer: None, + } + } +} diff --git a/druid-shell/src/backend/wayland/clipboard.rs b/druid-shell/src/backend/wayland/clipboard.rs new file mode 100644 index 0000000000..a7e23a5daa --- /dev/null +++ b/druid-shell/src/backend/wayland/clipboard.rs @@ -0,0 +1,282 @@ +// Copyright 2019 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Interactions with the system pasteboard on wayland compositors. +use super::application; +use super::error as waylanderr; +use crate::clipboard::{ClipboardFormat, FormatId}; +use std::io::Read; +use wayland_client as wl; +use wayland_client::protocol::wl_data_device; +use wayland_client::protocol::wl_data_device_manager; +use wayland_client::protocol::wl_data_offer; +use wayland_client::protocol::wl_data_source; + +#[derive(Clone)] +struct Offer { + wobj: wl::Main, + mimetype: String, +} + +impl Offer { + fn new(d: wl::Main, mimetype: impl Into) -> Self { + Self { + wobj: d, + mimetype: mimetype.into(), + } + } +} + +#[derive(Default)] +struct Data { + pending: std::cell::RefCell>, + current: std::cell::RefCell>, +} + +impl Data { + fn receive(&self, mimetype: &str) -> Option { + for offer in self.current.borrow().iter() { + if !offer.mimetype.starts_with(mimetype) { + continue; + } + return Some(offer.clone()); + } + None + } +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Data") + .field("pending", &self.pending.borrow().len()) + .field("current", &self.current.borrow().len()) + .finish() + } +} + +impl From> for Data { + fn from(current: Vec) -> Self { + Self { + current: std::cell::RefCell::new(current), + pending: Default::default(), + } + } +} + +struct Inner { + display: wl::Display, + wobj: wl::Main, + wdsobj: wl::Main, + devices: std::rc::Rc>, +} + +impl std::fmt::Debug for Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("") + .field("wobj", &self.wobj) + .field("wdsobj", &self.wdsobj) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct Manager { + inner: std::sync::Arc, +} + +impl Manager { + pub(super) fn new( + display: &wl::Display, + gm: &wl::GlobalManager, + ) -> Result { + let m = gm + .instantiate_exact::(3) + .map_err(|e| waylanderr::Error::global("wl_data_device_manager", 1, e))?; + + m.quick_assign(|i, event, _ignored| { + tracing::info!("clipboard {:?} event {:?}", i, event); + }); + + let ds = m.create_data_source(); + ds.quick_assign(|i, event, _ignored| { + tracing::info!("clipboard {:?} event {:?}", i, event); + }); + + Ok(Self { + inner: std::sync::Arc::new(Inner { + wobj: m, + wdsobj: ds, + display: display.clone(), + devices: Default::default(), + }), + }) + } + + pub fn attach<'a>(&'a self, seat: &'a mut application::Seat) { + let device = self.inner.wobj.get_data_device(&seat.wl_seat); + device.quick_assign({ + let m = self.inner.clone(); + move |i, event, _ignored| match event { + wl_data_device::Event::DataOffer { id } => { + let offer = id; + offer.quick_assign({ + let m = m.clone(); + move |i, event, _ignored| match event { + wl_data_offer::Event::Offer { mime_type } => { + let data = m.devices.borrow_mut(); + let offer = Offer::new(i, mime_type); + data.pending.borrow_mut().push(offer); + } + _ => tracing::warn!("clipboard unhandled {:?} event {:?}", i, event), + } + }); + } + wl_data_device::Event::Selection { id } => { + if id.is_some() { + let data = m.devices.borrow(); + tracing::debug!( + "current data offers {:?} {:?}", + data.current.borrow().len(), + data.pending.borrow().len() + ); + let upd = Data::from(data.pending.take()); + drop(data); + tracing::debug!( + "updated data offers {:?} {:?}", + upd.current.borrow().len(), + upd.pending.borrow().len() + ); + m.devices.replace(upd); + } else { + let upd = Data::from(Vec::new()); + m.devices.replace(upd); + } + } + _ => tracing::warn!("clipboard unhandled {:?} event {:?}", i, event), + } + }); + } + + fn initiate(&self, o: Offer) -> Option> { + tracing::debug!("retrieving {:?} {:?}", o.wobj, o.mimetype); + let (fdread, fdwrite) = match nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC) { + Ok(pipe) => pipe, + Err(cause) => { + tracing::error!("clipboard failed to request data {:?}", cause); + return None; + } + }; + + o.wobj.receive(o.mimetype.to_string(), fdwrite); + if let Err(cause) = self.inner.display.flush() { + tracing::error!("clipboard failed to request data {:?}", cause); + return None; + } + + if let Err(cause) = nix::unistd::close(fdwrite) { + tracing::error!("clipboard failed to request data {:?}", cause); + return None; + } + + let mut data = Vec::new(); + let mut io: std::fs::File = unsafe { std::os::unix::io::FromRawFd::from_raw_fd(fdread) }; + let transfered = match io.read_to_end(&mut data) { + Err(cause) => { + tracing::error!("clipboard unable to retrieve pasted content {:?}", cause); + return None; + } + Ok(transfered) => transfered, + }; + + tracing::debug!("transfered {:?} bytes", transfered); + + match transfered { + 0 => None, + _ => Some(data), + } + } + + pub(super) fn receive(&self, mimetype: impl Into) -> Option> { + let mimetype: String = mimetype.into(); + if let Some(offer) = self.inner.devices.borrow().receive(&mimetype) { + return self.initiate(offer); + } + + None + } +} + +/// The system clipboard. +#[derive(Debug, Clone)] +pub struct Clipboard { + inner: Manager, +} + +impl From<&Manager> for Clipboard { + fn from(m: &Manager) -> Self { + Self { inner: m.clone() } + } +} + +impl Clipboard { + const UTF8: &'static str = "text/plain;charset=utf-8"; + const TEXT: &'static str = "text/plain"; + const UTF8_STRING: &'static str = "UTF8_STRING"; + + /// Put a string onto the system clipboard. + pub fn put_string(&mut self, s: impl AsRef) { + let _s = s.as_ref().to_string(); + self.inner.inner.wdsobj.offer(Clipboard::UTF8.to_string()); + } + + /// Put multi-format data on the system clipboard. + pub fn put_formats(&mut self, _formats: &[ClipboardFormat]) { + tracing::warn!("clipboard copy not implemented"); + } + + /// Get a string from the system clipboard, if one is available. + pub fn get_string(&self) -> Option { + vec![Clipboard::UTF8, Clipboard::TEXT, Clipboard::UTF8_STRING] + .iter() + .find_map( + |mimetype| match std::str::from_utf8(&self.inner.receive(*mimetype)?) { + Ok(s) => Some(s.to_string()), + Err(cause) => { + tracing::error!("clipboard unable to retrieve utf8 content {:?}", cause); + None + } + }, + ) + } + + /// Given a list of supported clipboard types, returns the supported type which has + /// highest priority on the system clipboard, or `None` if no types are supported. + pub fn preferred_format(&self, _formats: &[FormatId]) -> Option { + tracing::warn!("clipboard preferred_format not implemented"); + None + } + + /// Return data in a given format, if available. + /// + /// It is recommended that the `fmt` argument be a format returned by + /// [`Clipboard::preferred_format`] + pub fn get_format(&self, format: FormatId) -> Option> { + self.inner.receive(format) + } + + pub fn available_type_names(&self) -> Vec { + tracing::warn!("clipboard available_type_names not implemented"); + Vec::new() + } +} diff --git a/druid-shell/src/backend/wayland/display.rs b/druid-shell/src/backend/wayland/display.rs new file mode 100644 index 0000000000..d83fdca0bf --- /dev/null +++ b/druid-shell/src/backend/wayland/display.rs @@ -0,0 +1,185 @@ +#![allow(clippy::single_match)] +use super::error; +use std::collections::BTreeMap; +use wayland_client as wlc; +use wayland_client::protocol::wl_registry; +use wayland_protocols::xdg_shell::client::xdg_wm_base; + +type GlobalEventConsumer = dyn Fn(&wlc::GlobalEvent, &wlc::Attached, &wlc::DispatchData) + + 'static; + +#[derive(Clone)] +pub struct GlobalEventSubscription { + id: u64, + sub: std::sync::Arc, +} + +impl GlobalEventSubscription { + fn with_id(mut self, id: u64) -> Self { + self.id = id; + self + } +} + +impl GlobalEventSubscription { + fn consume( + &self, + event: &wlc::GlobalEvent, + registry: &wlc::Attached, + ctx: &wlc::DispatchData, + ) { + (self.sub)(event, registry, ctx) + } +} + +impl From for GlobalEventSubscription +where + X: Fn(&wlc::GlobalEvent, &wlc::Attached, &wlc::DispatchData) + 'static, +{ + fn from(closure: X) -> Self { + Self { + id: 0, + sub: std::sync::Arc::new(closure), + } + } +} + +pub trait GlobalEventDispatch { + fn subscribe(&self, sub: impl Into) -> GlobalEventSubscription; + fn release(&self, s: &GlobalEventSubscription); +} + +pub(super) struct Dispatcher { + incr: crate::Counter, + subscriptions: std::cell::RefCell>, +} + +impl Default for Dispatcher { + fn default() -> Self { + Self { + incr: crate::Counter::new(), + subscriptions: std::cell::RefCell::new(BTreeMap::new()), + } + } +} + +impl Dispatcher { + fn consume( + &self, + event: &wlc::GlobalEvent, + registry: &wlc::Attached, + ctx: &wlc::DispatchData, + ) { + for (_, sub) in self.subscriptions.borrow().iter() { + sub.consume(event, registry, ctx); + } + } +} + +impl GlobalEventDispatch for Dispatcher { + fn subscribe(&self, sub: impl Into) -> GlobalEventSubscription { + let sub = sub.into().with_id(self.incr.next()); + self.subscriptions.borrow_mut().insert(sub.id, sub.clone()); + sub + } + + fn release(&self, s: &GlobalEventSubscription) { + self.subscriptions.borrow_mut().remove(&s.id); + } +} + +pub(super) struct Environment { + pub(super) display: wlc::Display, + pub(super) registry: wlc::GlobalManager, + pub(super) xdg_base: wlc::Main, + pub(super) queue: std::rc::Rc>, + dispatcher: std::sync::Arc, +} + +impl GlobalEventDispatch for Environment { + fn subscribe(&self, sub: impl Into) -> GlobalEventSubscription { + self.dispatcher.subscribe(sub) + } + + fn release(&self, s: &GlobalEventSubscription) { + self.dispatcher.release(s) + } +} + +impl GlobalEventDispatch for std::sync::Arc { + fn subscribe(&self, sub: impl Into) -> GlobalEventSubscription { + self.dispatcher.subscribe(sub) + } + + fn release(&self, s: &GlobalEventSubscription) { + self.dispatcher.release(s) + } +} + +pub(super) fn new(dispatcher: Dispatcher) -> Result, error::Error> { + let dispatcher = std::sync::Arc::new(dispatcher); + let d = wlc::Display::connect_to_env()?; + + let mut queue = d.create_event_queue(); + let handle = d.attach(queue.token()); + let registry = wlc::GlobalManager::new_with_cb(&handle, { + let dispatcher = dispatcher.clone(); + move |event, registry, ctx| { + dispatcher.consume(&event, ®istry, &ctx); + } + }); + + // do a round trip to make sure we have all the globals + queue + .sync_roundtrip(&mut (), |_, _, _| unreachable!()) + .map_err(error::Error::fatal)?; + + let xdg_base = registry + .instantiate_exact::(2) + .map_err(|e| error::Error::global("xdg_wm_base", 2, e))?; + + // We do this to make sure wayland knows we're still responsive. + // + // NOTE: This means that clients mustn't hold up the event loop, or else wayland might kill + // your app's connection. Move *everything* to another thread, including e.g. file i/o, + // computation, network, ... This is good practice for all back-ends: it will improve + // responsiveness. + xdg_base.quick_assign(|xdg_base, event, ctx| { + tracing::info!( + "global xdg_base events {:?} {:?} {:?}", + xdg_base, + event, + ctx + ); + match event { + xdg_wm_base::Event::Ping { serial } => xdg_base.pong(serial), + _ => (), + } + }); + + let env = std::sync::Arc::new(Environment { + queue: std::rc::Rc::new(std::cell::RefCell::new(queue)), + display: d, + registry, + xdg_base, + dispatcher, + }); + + Ok(env) +} + +#[allow(unused)] +pub(super) fn print(reg: &wlc::GlobalManager) { + let mut globals_list = reg.list(); + globals_list.sort_by(|(_, name1, version1), (_, name2, version2)| { + name1.cmp(name2).then(version1.cmp(version2)) + }); + + for (id, name, version) in globals_list.into_iter() { + tracing::debug!("{:?}@{:?} - {:?}", name, version, id); + } +} + +pub(super) fn count(reg: &wlc::GlobalManager, i: &str) -> usize { + reg.list().iter().filter(|(_, name, _)| name == i).count() +} diff --git a/druid-shell/src/backend/wayland/error.rs b/druid-shell/src/backend/wayland/error.rs new file mode 100644 index 0000000000..f28147ad63 --- /dev/null +++ b/druid-shell/src/backend/wayland/error.rs @@ -0,0 +1,119 @@ +// Copyright 2019 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! wayland platform errors. + +use std::{error::Error as StdError, fmt, sync::Arc}; +use wayland_client as wl; + +#[derive(Debug, Clone)] +pub enum Error { + /// Error connecting to wayland server. + Connect(Arc), + /// A wayland global either doesn't exist, or doesn't support the version we need. + Global { + name: String, + version: u32, + inner: Arc, + }, + /// An unexpected error occurred. It's not handled by druid-shell/wayland, so you should + /// terminate the app. + Fatal(Arc), + String(ErrorString), + InvalidParent(u32), + /// general error. + Err(Arc), +} + +impl Error { + #[allow(clippy::self_named_constructors)] + pub fn error(e: impl StdError + 'static) -> Self { + Self::Err(Arc::new(e)) + } + + pub fn fatal(e: impl StdError + 'static) -> Self { + Self::Fatal(Arc::new(e)) + } + + pub fn global(name: impl Into, version: u32, inner: wl::GlobalError) -> Self { + Error::Global { + name: name.into(), + version, + inner: Arc::new(inner), + } + } + + pub fn string(s: impl Into) -> Self { + Error::String(ErrorString::from(s)) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self { + Self::Connect(e) => write!(f, "could not connect to the wayland server: {:?}", e), + Self::Global { name, version, .. } => write!( + f, + "a required wayland global ({}@{}) was unavailable", + name, version + ), + Self::Fatal(e) => write!(f, "an unhandled error occurred: {:?}", e), + Self::Err(e) => write!(f, "an unhandled error occurred: {:?}", e), + Self::String(e) => e.fmt(f), + Self::InvalidParent(id) => write!(f, "invalid parent window for popup: {:?}", id), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::Connect(e) => Some(&**e), + Self::Global { inner, .. } => Some(&**inner), + Self::Fatal(e) => Some(&**e), + Self::Err(e) => Some(&**e), + Self::String(e) => Some(e), + Self::InvalidParent(_) => None, + } + } +} + +impl From for Error { + fn from(err: wl::ConnectError) -> Self { + Self::Connect(Arc::new(err)) + } +} + +#[derive(Debug, Clone)] +pub struct ErrorString { + details: String, +} + +impl ErrorString { + pub fn from(s: impl Into) -> Self { + Self { details: s.into() } + } +} + +impl std::fmt::Display for ErrorString { + fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.details) + } +} + +impl std::error::Error for ErrorString { + fn description(&self) -> &str { + &self.details + } +} diff --git a/druid-shell/src/backend/wayland/events.rs b/druid-shell/src/backend/wayland/events.rs new file mode 100644 index 0000000000..71fa3f7a7d --- /dev/null +++ b/druid-shell/src/backend/wayland/events.rs @@ -0,0 +1,155 @@ +//! Multiplexing events. +//! +//! Calloop is a wrapper around `epoll` essentially allowing us to *select* multiple file +//! descriptors. We use it here to select events from a timer and from wayland. +//! +//! Based on `client-toolkit/src/event_loop.rs` in `smithay-client-toolkit` (MIT Licensed). + +use calloop::{ + generic::{Fd, Generic}, + Dispatcher, EventSource, Interest, Mode, +}; +use std::{cell::RefCell, io, rc::Rc}; +use wayland_client::EventQueue; + +use super::{application, window}; + +/// A wrapper around the wayland event queue that calloop knows how to select. +pub(crate) struct WaylandSource { + appdata: std::sync::Arc, + queue: Rc>, + fd: Generic, +} + +impl WaylandSource { + /// Wrap an `EventQueue` as a `WaylandSource`. + pub fn new(appdata: std::sync::Arc) -> WaylandSource { + let queue = appdata.wayland.queue.clone(); + let fd = queue.borrow().display().get_connection_fd(); + WaylandSource { + appdata, + queue, + fd: Generic::from_fd(fd, Interest::READ, Mode::Level), + } + } + + /// Get a dispatcher that we can insert into our event loop. + pub fn into_dispatcher( + self, + ) -> Dispatcher< + Self, + impl FnMut( + window::WindowHandle, + &mut Rc>, + &mut std::sync::Arc, + ) -> io::Result, + > { + Dispatcher::new(self, |_winhandle, queue, appdata| { + queue + .borrow_mut() + .dispatch_pending(appdata, |event, object, _| { + tracing::error!( + "[druid-shell] Encountered an orphan event: {}@{} : {}", + event.interface, + object.as_ref().id(), + event.name + ); + tracing::error!("all events should be handled: please raise an issue"); + }) + }) + } +} + +impl EventSource for WaylandSource { + type Event = window::WindowHandle; + type Metadata = Rc>; + type Ret = io::Result; + + fn process_events( + &mut self, + ready: calloop::Readiness, + token: calloop::Token, + mut callback: F, + ) -> std::io::Result<()> + where + F: FnMut(window::WindowHandle, &mut Rc>) -> Self::Ret, + { + tracing::trace!("processing events invoked {:?} {:?}", ready, token); + + self.appdata.display_flushed.replace(false); + + let winhandle = match self.appdata.acquire_current_window() { + Some(winhandle) => winhandle, + None => { + tracing::error!("unable to acquire current window"); + return Ok(()); + } + }; + + // in case of readiness of the wayland socket we do the following in a loop, until nothing + // more can be read: + loop { + // 1. read events from the socket if any are available + if let Some(guard) = self.queue.borrow().prepare_read() { + // might be None if some other thread read events before us, concurently + if let Err(e) = guard.read_events() { + if e.kind() != io::ErrorKind::WouldBlock { + return Err(e); + } + } + } + tracing::trace!("processing events initiated"); + // 2. dispatch any pending event in the queue + // propagate orphan events to the user + let ret = callback(winhandle.clone(), &mut self.queue); + tracing::trace!("processing events completed {:?}", ret); + match ret { + Ok(0) => { + // no events were dispatched even after reading the socket, + // nothing more to do, stop here + break; + } + Ok(_) => {} + Err(e) => { + // in case of error, forward it and fast-exit + return Err(e); + } + } + } + + tracing::trace!("dispatching completed, flushing"); + // 3. Once dispatching is finished, flush the responses to the compositor + if let Err(e) = self.queue.borrow().display().flush() { + if e.kind() != io::ErrorKind::WouldBlock { + // in case of error, forward it and fast-exit + return Err(e); + } + + // WouldBlock error means the compositor could not process all our messages + // quickly. Either it is slowed down or we are a spammer. + // Should not really happen, if it does we do nothing and will flush again later. + tracing::warn!("unable to flush display: {:?}", e); + } else { + self.appdata.display_flushed.replace(true); + } + + tracing::trace!("event queue completed"); + Ok(()) + } + + fn register(&mut self, poll: &mut calloop::Poll, token: calloop::Token) -> std::io::Result<()> { + self.fd.register(poll, token) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token: calloop::Token, + ) -> std::io::Result<()> { + self.fd.reregister(poll, token) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> std::io::Result<()> { + self.fd.unregister(poll) + } +} diff --git a/druid-shell/src/backend/wayland/keyboard.rs b/druid-shell/src/backend/wayland/keyboard.rs new file mode 100644 index 0000000000..8951811a79 --- /dev/null +++ b/druid-shell/src/backend/wayland/keyboard.rs @@ -0,0 +1,409 @@ +use std::convert::TryInto; +use wayland_client as wlc; +use wayland_client::protocol::wl_keyboard; +use wayland_client::protocol::wl_seat; + +use crate::keyboard_types::KeyState; +use crate::text; +use crate::KeyEvent; +use crate::Modifiers; + +use super::application::ApplicationData; +use super::surfaces::buffers; +use crate::backend::shared::xkb; + +#[allow(unused)] +#[derive(Clone)] +struct CachedKeyPress { + seat: u32, + serial: u32, + timestamp: u32, + key: u32, + repeat: bool, + state: wayland_client::protocol::wl_keyboard::KeyState, + queue: calloop::channel::Sender, +} + +impl CachedKeyPress { + fn repeat(&self) -> Self { + let mut c = self.clone(); + c.repeat = true; + c + } +} + +#[derive(Debug, Clone)] +struct Repeat { + rate: std::time::Duration, + delay: std::time::Duration, +} + +impl Default for Repeat { + fn default() -> Self { + Self { + rate: std::time::Duration::from_millis(40), + delay: std::time::Duration::from_millis(600), + } + } +} + +struct Keyboard { + /// Whether we've currently got keyboard focus. + focused: bool, + repeat: Repeat, + last_key_press: Option, + xkb_context: xkb::Context, + xkb_keymap: std::cell::RefCell>, + xkb_state: std::cell::RefCell>, + xkb_mods: std::cell::Cell, +} + +impl Default for Keyboard { + fn default() -> Self { + Self { + focused: false, + repeat: Repeat::default(), + last_key_press: None, + xkb_context: xkb::Context::new(), + xkb_keymap: std::cell::RefCell::new(None), + xkb_state: std::cell::RefCell::new(None), + xkb_mods: std::cell::Cell::new(Modifiers::empty()), + } + } +} + +impl Keyboard { + fn focused(&mut self, updated: bool) { + self.focused = updated; + } + + fn repeat(&mut self, u: Repeat) { + self.repeat = u; + } + + fn replace_last_key_press(&mut self, u: Option) { + self.last_key_press = u; + } + + fn release_last_key_press(&self, current: &CachedKeyPress) -> Option { + match &self.last_key_press { + None => None, // nothing to do. + Some(last) => { + if last.serial >= current.serial { + return Some(last.clone()); + } + if last.key != current.key { + return Some(last.clone()); + } + None + } + } + } + + fn keystroke<'a>(&'a mut self, keystroke: &'a CachedKeyPress) { + let keystate = match keystroke.state { + wl_keyboard::KeyState::Released => { + self.replace_last_key_press(self.release_last_key_press(keystroke)); + KeyState::Up + } + wl_keyboard::KeyState::Pressed => { + self.replace_last_key_press(Some(keystroke.repeat())); + KeyState::Down + } + _ => panic!("unrecognised key event"), + }; + + let mut event = self + .xkb_state + .borrow_mut() + .as_mut() + .unwrap() + .key_event(keystroke.key, keystate); + event.mods = self.xkb_mods.get(); + event.repeat = keystroke.repeat; + + if let Err(cause) = keystroke.queue.send(event) { + tracing::error!("failed to send druid key event: {:?}", cause); + } + } + + fn consume( + &mut self, + seat: u32, + event: wl_keyboard::Event, + keyqueue: calloop::channel::Sender, + ) { + tracing::trace!("consume {:?} -> {:?}", seat, event); + match event { + wl_keyboard::Event::Keymap { format, fd, size } => { + if !matches!(format, wl_keyboard::KeymapFormat::XkbV1) { + panic!("only xkb keymap supported for now"); + } + + // TODO to test memory ownership we copy the memory. That way we can deallocate it + // and see if we get a segfault. + let keymap_data = unsafe { + buffers::Mmap::from_raw_private( + fd, + size.try_into().unwrap(), + 0, + size.try_into().unwrap(), + ) + .unwrap() + .as_ref() + .to_vec() + }; + + // keymap data is '\0' terminated. + let keymap = self.xkb_context.keymap_from_slice(&keymap_data); + let keymapstate = keymap.state(); + + self.xkb_keymap.replace(Some(keymap)); + self.xkb_state.replace(Some(keymapstate)); + } + wl_keyboard::Event::Enter { .. } => { + self.focused(true); + } + wl_keyboard::Event::Leave { .. } => { + self.focused(false); + } + wl_keyboard::Event::Key { + serial, + time, + state, + key, + } => { + tracing::trace!( + "key stroke registered {:?} {:?} {:?} {:?}", + time, + serial, + key, + state + ); + self.keystroke(&CachedKeyPress { + repeat: false, + seat, + serial, + timestamp: time, + key: key + 8, // TODO: understand the magic 8. + state, + queue: keyqueue, + }) + } + wl_keyboard::Event::Modifiers { .. } => { + self.xkb_mods.replace(event_to_mods(event)); + } + wl_keyboard::Event::RepeatInfo { rate, delay } => { + tracing::trace!("keyboard repeat info received {:?} {:?}", rate, delay); + self.repeat(Repeat { + rate: std::time::Duration::from_millis((1000 / rate) as u64), + delay: std::time::Duration::from_millis(delay as u64), + }); + } + evt => { + tracing::warn!("unimplemented keyboard event: {:?}", evt); + } + } + } +} + +pub(super) struct State { + apptx: calloop::channel::Sender, + apprx: std::cell::RefCell>>, + tx: calloop::channel::Sender<(u32, wl_keyboard::Event, calloop::channel::Sender)>, +} + +impl Default for State { + fn default() -> Self { + let (apptx, apprx) = calloop::channel::channel::(); + let (tx, rx) = calloop::channel::channel::<( + u32, + wl_keyboard::Event, + calloop::channel::Sender, + )>(); + let state = Self { + apptx, + apprx: std::cell::RefCell::new(Some(apprx)), + tx, + }; + + std::thread::spawn(move || { + let mut eventloop: calloop::EventLoop<(calloop::LoopSignal, Keyboard)> = + calloop::EventLoop::try_new() + .expect("failed to initialize the keyboard event loop!"); + let signal = eventloop.get_signal(); + let handle = eventloop.handle(); + let repeat = calloop::timer::Timer::::new().unwrap(); + handle + .insert_source(rx, { + let repeater = repeat.handle(); + move |event, _ignored, state| { + let event = match event { + calloop::channel::Event::Closed => { + tracing::info!("keyboard event loop closed shutting down"); + state.0.stop(); + return; + } + calloop::channel::Event::Msg(keyevent) => keyevent, + }; + state.1.consume(event.0, event.1, event.2); + match &state.1.last_key_press { + None => repeater.cancel_all_timeouts(), + Some(cached) => { + repeater.cancel_all_timeouts(); + repeater.add_timeout(state.1.repeat.delay, cached.clone()); + } + }; + } + }) + .unwrap(); + + // generate repeat keypresses. + handle + .insert_source(repeat, |event, timer, state| { + timer.add_timeout(state.1.repeat.rate, event.clone()); + state.1.keystroke(&event); + }) + .unwrap(); + + tracing::debug!("keyboard event loop initiated"); + eventloop + .run( + std::time::Duration::from_secs(60), + &mut (signal, Keyboard::default()), + |_ignored| { + tracing::trace!("keyboard event loop idle"); + }, + ) + .expect("keyboard event processing failed"); + tracing::debug!("keyboard event loop completed"); + }); + + state + } +} + +struct ModMap(u32, Modifiers); + +impl ModMap { + fn merge(self, m: Modifiers, mods: u32, locked: u32) -> Modifiers { + if self.0 & mods == 0 && self.0 & locked == 0 { + return m; + } + + m | self.1 + } +} + +const MOD_SHIFT: ModMap = ModMap(1, Modifiers::SHIFT); +const MOD_CAP_LOCK: ModMap = ModMap(2, Modifiers::CAPS_LOCK); +const MOD_CTRL: ModMap = ModMap(4, Modifiers::CONTROL); +const MOD_ALT: ModMap = ModMap(8, Modifiers::ALT); +const MOD_NUM_LOCK: ModMap = ModMap(16, Modifiers::NUM_LOCK); +const MOD_META: ModMap = ModMap(64, Modifiers::META); + +pub fn event_to_mods(event: wl_keyboard::Event) -> Modifiers { + match event { + wl_keyboard::Event::Modifiers { + mods_depressed, + mods_locked, + .. + } => { + let mods = Modifiers::empty(); + let mods = MOD_SHIFT.merge(mods, mods_depressed, mods_locked); + let mods = MOD_CAP_LOCK.merge(mods, mods_depressed, mods_locked); + let mods = MOD_CTRL.merge(mods, mods_depressed, mods_locked); + let mods = MOD_ALT.merge(mods, mods_depressed, mods_locked); + let mods = MOD_NUM_LOCK.merge(mods, mods_depressed, mods_locked); + + MOD_META.merge(mods, mods_depressed, mods_locked) + } + _ => Modifiers::empty(), + } +} + +pub struct Manager { + inner: std::sync::Arc, +} + +impl Default for Manager { + fn default() -> Self { + Self { + inner: std::sync::Arc::new(State::default()), + } + } +} + +impl Manager { + pub(super) fn attach( + &self, + id: u32, + seat: wlc::Main, + ) -> wlc::Main { + let keyboard = seat.get_keyboard(); + keyboard.quick_assign({ + let tx = self.inner.tx.clone(); + let queue = self.inner.apptx.clone(); + move |_, event, _| { + if let Err(cause) = tx.send((id, event, queue.clone())) { + tracing::error!("failed to transmit keyboard event {:?}", cause); + }; + } + }); + + keyboard + } + + // TODO turn struct into a calloop event source. + pub(super) fn events<'a>( + &self, + handle: &'a calloop::LoopHandle>, + ) { + let rx = self.inner.apprx.borrow_mut().take().unwrap(); + handle + .insert_source(rx, { + move |evt, _ignored, appdata| { + let evt = match evt { + calloop::channel::Event::Msg(e) => e, + calloop::channel::Event::Closed => { + tracing::info!("keyboard events receiver closed"); + return; + } + }; + + if let Some(winhandle) = appdata.acquire_current_window() { + if let Some(windata) = winhandle.data() { + windata.with_handler({ + let windata = windata.clone(); + let evt = evt; + move |handler| match evt.state { + KeyState::Up => { + handler.key_up(evt.clone()); + tracing::trace!( + "key press event up {:?} {:?}", + evt, + windata.active_text_input.get() + ); + } + KeyState::Down => { + let handled = text::simulate_input( + handler, + windata.active_text_input.get(), + evt.clone(), + ); + tracing::trace!( + "key press event down {:?} {:?} {:?}", + handled, + evt, + windata.active_text_input.get() + ); + } + } + }); + } + } + } + }) + .unwrap(); + } +} diff --git a/druid-shell/src/backend/wayland/menu.rs b/druid-shell/src/backend/wayland/menu.rs new file mode 100644 index 0000000000..b0c6b0a783 --- /dev/null +++ b/druid-shell/src/backend/wayland/menu.rs @@ -0,0 +1,49 @@ +// Copyright 2019 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(unused)] +use super::window::WindowHandle; +use crate::common_util::strip_access_key; +use crate::hotkey::{HotKey, RawMods}; +use crate::keyboard::{KbKey, Modifiers}; + +#[derive(Default, Debug)] +pub struct Menu; + +#[derive(Debug)] +struct MenuItem; + +impl Menu { + pub fn new() -> Menu { + Menu + } + + pub fn new_for_popup() -> Menu { + Menu + } + + pub fn add_dropdown(&mut self, menu: Menu, text: &str, _enabled: bool) {} + + pub fn add_item( + &mut self, + id: u32, + text: &str, + key: Option<&HotKey>, + enabled: bool, + _selected: bool, + ) { + } + + pub fn add_separator(&mut self) {} +} diff --git a/druid-shell/src/backend/wayland/mod.rs b/druid-shell/src/backend/wayland/mod.rs new file mode 100644 index 0000000000..736cc4b608 --- /dev/null +++ b/druid-shell/src/backend/wayland/mod.rs @@ -0,0 +1,41 @@ +// Copyright 2019 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! wayland platform support + +pub mod application; +pub mod clipboard; +mod display; +pub mod error; +mod events; +pub mod keyboard; +pub mod menu; +mod outputs; +pub mod pointers; +pub mod screen; +pub mod surfaces; +pub mod window; + +/// Little enum to make it clearer what some return values mean. +#[derive(Copy, Clone)] +enum Changed { + Changed, + Unchanged, +} + +impl Changed { + fn is_changed(self) -> bool { + matches!(self, Changed::Changed) + } +} diff --git a/druid-shell/src/backend/wayland/outputs/mod.rs b/druid-shell/src/backend/wayland/outputs/mod.rs new file mode 100644 index 0000000000..871c972de8 --- /dev/null +++ b/druid-shell/src/backend/wayland/outputs/mod.rs @@ -0,0 +1,188 @@ +use wayland_client as wlc; +use wayland_client::protocol::wl_output; + +use super::display; +use super::error; +pub mod output; + +#[derive(Debug, Clone)] +#[allow(unused)] +pub enum Event { + Located(Meta), + Removed(Meta), +} + +pub fn auto( + env: &impl display::GlobalEventDispatch, +) -> Result, error::Error> { + tracing::debug!("detecting xdg outputs"); + match output::detect(env) { + Ok(rx) => return Ok(rx), + Err(cause) => tracing::info!("unable to detect xdg outputs {:?}", cause), + } + + Err(error::Error::string("unable to detect display outputs")) +} + +pub(super) fn current() -> Result, error::Error> { + let dispatcher = display::Dispatcher::default(); + let rx = auto(&dispatcher)?; + let env = display::new(dispatcher)?; + let mut cache = std::collections::BTreeMap::new(); + let mut eventloop: calloop::EventLoop<( + calloop::LoopSignal, + &mut std::collections::BTreeMap, + )> = calloop::EventLoop::try_new().expect("failed to initialize the displays event loop!"); + let signal = eventloop.get_signal(); + let handle = eventloop.handle(); + handle + .insert_source(rx, { + move |event, _ignored, (signal, cache)| { + let event = match event { + calloop::channel::Event::Msg(event) => event, + calloop::channel::Event::Closed => return signal.stop(), + }; + + match event { + Event::Located(meta) => { + cache.insert(meta.name.clone(), meta); + } + Event::Removed(meta) => { + cache.remove(&meta.name); + } + } + } + }) + .map_err(error::Error::error)?; + + // do a round trip to flush commands. + let mut queue = env.queue.try_borrow_mut().map_err(error::Error::error)?; + queue + .sync_roundtrip(&mut (), |_, _, _| unreachable!()) + .map_err(error::Error::error)?; + + let expected = display::count(&env.registry, "wl_output"); + let result: std::sync::Arc>> = + std::sync::Arc::new(std::cell::RefCell::new(Vec::new())); + eventloop + .run( + std::time::Duration::from_secs(1), + &mut (signal, &mut cache), + { + let result = result.clone(); + move |(signal, cache)| { + if expected <= cache.len() { + result.replace(cache.values().cloned().collect()); + signal.stop(); + } + + let res = queue + .sync_roundtrip(&mut (), |_, _, _| unreachable!()) + .map_err(error::Error::error); + + if let Err(cause) = res { + tracing::error!("wayland sync failed {:?}", cause); + signal.stop(); + } + } + }, + ) + .map_err(error::Error::error)?; + Ok(result.take()) +} + +pub trait Wayland { + fn consume<'a>( + &'a mut self, + obj: &'a wlc::Main, + event: &'a wl_output::Event, + ); +} + +#[derive(Clone, Debug, Default)] +pub struct Dimensions { + pub width: i32, + pub height: i32, +} + +impl From<(i32, i32)> for Dimensions { + fn from(v: (i32, i32)) -> Self { + Self { + width: v.0, + height: v.1, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct Position { + pub x: i32, + pub y: i32, +} + +impl From<(i32, i32)> for Position { + fn from(v: (i32, i32)) -> Self { + Self { x: v.0, y: v.1 } + } +} + +#[derive(Debug, Default, Clone)] +pub struct Mode { + pub logical: Dimensions, + pub refresh: i32, + pub preferred: bool, +} + +#[derive(Clone, Debug)] +pub struct Meta { + pub gid: u32, + pub name: String, + pub description: String, + pub logical: Dimensions, + pub refresh: i32, + pub physical: Dimensions, + pub subpixel: wl_output::Subpixel, + pub transform: wl_output::Transform, + pub make: String, + pub model: String, + pub scale: f64, + pub enabled: bool, + pub position: Position, +} + +impl Default for Meta { + fn default() -> Self { + Self { + gid: Default::default(), + name: Default::default(), + description: Default::default(), + logical: Default::default(), + refresh: Default::default(), + physical: Default::default(), + position: Default::default(), + subpixel: wl_output::Subpixel::Unknown, + transform: wl_output::Transform::Normal, + make: Default::default(), + model: Default::default(), + scale: Default::default(), + enabled: Default::default(), + } + } +} + +impl Meta { + pub fn normalize(mut self) -> Self { + match self.transform { + wl_output::Transform::Flipped270 | wl_output::Transform::_270 => { + self.logical = Dimensions::from((self.logical.height, self.logical.width)); + self.physical = Dimensions::from((self.physical.height, self.physical.width)); + } + _ => {} + } + self + } + + pub fn id(&self) -> u32 { + self.gid + } +} diff --git a/druid-shell/src/backend/wayland/outputs/output.rs b/druid-shell/src/backend/wayland/outputs/output.rs new file mode 100644 index 0000000000..41f08270f8 --- /dev/null +++ b/druid-shell/src/backend/wayland/outputs/output.rs @@ -0,0 +1,204 @@ +use super::super::display; +use super::super::error; +use super::super::outputs; +use wayland_client as wlc; +use wayland_client::protocol::wl_output; +use wayland_client::protocol::wl_registry; +use wayland_protocols::unstable::xdg_output::v1::client::zxdg_output_manager_v1; +use wayland_protocols::unstable::xdg_output::v1::client::zxdg_output_v1; + +pub fn detect( + env: &impl display::GlobalEventDispatch, +) -> Result, error::Error> { + let (outputstx, outputsrx) = calloop::channel::channel::(); + let xdg_output_manager_id: std::cell::RefCell> = std::cell::RefCell::new(None); + display::GlobalEventDispatch::subscribe(env, { + move |event: &'_ wlc::GlobalEvent, + registry: &'_ wlc::Attached, + _ctx: &'_ wlc::DispatchData| { + match event { + wlc::GlobalEvent::New { + id, + interface, + version, + } => { + let id = *id; + let version = *version; + + if interface.as_str() == "zxdg_output_manager_v1" && version == 3 { + xdg_output_manager_id.replace(Some(id)); + return; + } + + if !(interface.as_str() == "wl_output" && version >= 3) { + return; + } + + let output = registry.bind::(3, id); + let xdgm = (*xdg_output_manager_id.borrow()).map(|xdgm_id| { + registry.bind::(3, xdgm_id) + }); + + let mut meta = Meta::default(); + let mut xdgmeta = XdgMeta::new(); + output.quick_assign({ + let outputstx = outputstx.clone(); + move |output, event, _ctx| { + let mut m = match meta.consume(&output, &event) { + Some(m) => m, + None => return, + }; + + if !xdgmeta.set_xdg_handled() { + if let Some(xdgm) = &xdgm { + let xdg_output = xdgm.get_xdg_output(&output); + xdg_output.quick_assign({ + let mut xdgmeta = xdgmeta.clone(); + move |xdg_output, event, _ctx| { + xdgmeta.consume(&xdg_output, &event); + } + }); + return; + } + } + + xdgmeta.modify(&mut m); + + if let Err(cause) = outputstx.send(outputs::Event::Located(m)) { + tracing::warn!("unable to transmit output {:?}", cause); + } + } + }); + } + wlc::GlobalEvent::Removed { interface, .. } => { + if interface.as_str() != "wl_output" { + return; + } + tracing::info!("output removed event {:?} {:?}", registry, interface); + } + }; + } + }); + + Ok(outputsrx) +} + +#[derive(Debug, Default)] +struct XdgState { + name: String, + description: String, + position: outputs::Position, + logical: outputs::Dimensions, +} + +#[derive(Clone, Debug)] +struct XdgMeta { + handled: bool, + state: std::sync::Arc>, +} + +impl XdgMeta { + fn new() -> Self { + Self { + handled: false, + state: std::sync::Arc::new(std::cell::RefCell::new(XdgState::default())), + } + } + + fn set_xdg_handled(&mut self) -> bool { + let tmp = self.handled; + self.handled = true; + tmp + } + + fn consume( + &mut self, + output: &wlc::Main, + evt: &zxdg_output_v1::Event, + ) { + match evt { + zxdg_output_v1::Event::Name { name } => { + self.state.borrow_mut().name = name.clone(); + } + zxdg_output_v1::Event::Description { description } => { + self.state.borrow_mut().description = description.clone(); + } + zxdg_output_v1::Event::LogicalPosition { x, y } => { + self.state.borrow_mut().position = outputs::Position::from((*x, *y)); + } + zxdg_output_v1::Event::LogicalSize { width, height } => { + self.state.borrow_mut().logical = outputs::Dimensions::from((*width, *height)); + } + _ => tracing::warn!("unused xdg_output_v1 event {:?} {:?}", output, evt), + }; + } + + fn modify(&self, meta: &mut outputs::Meta) { + let state = self.state.borrow(); + meta.name = state.name.clone(); + meta.description = state.description.clone(); + meta.position = state.position.clone(); + meta.logical = state.logical.clone(); + } +} + +#[derive(Default)] +struct Meta { + meta: outputs::Meta, +} + +impl Meta { + /// Incorporate update data from the server for this output. + fn consume( + &mut self, + output: &wlc::Main, + evt: &wl_output::Event, + ) -> Option { + match evt { + wl_output::Event::Geometry { + x, + y, + physical_width, + physical_height, + subpixel, + make, + model, + transform, + } => { + self.meta.position = outputs::Position::from((*x, *y)); + self.meta.physical = outputs::Dimensions::from((*physical_width, *physical_height)); + self.meta.subpixel = *subpixel; + self.meta.make = make.clone(); + self.meta.model = model.clone(); + self.meta.transform = *transform; + None + } + wl_output::Event::Mode { + flags, + width, + height, + refresh, + } => { + if flags.contains(wl_output::Mode::Current) { + self.meta.logical = outputs::Dimensions::from((*width, *height)); + self.meta.refresh = *refresh; + } + + None + } + wl_output::Event::Done => { + self.meta.gid = wlc::Proxy::from(output.detach()).id(); + self.meta.enabled = true; + Some(self.meta.clone()) + } + wl_output::Event::Scale { factor } => { + self.meta.scale = (*factor).into(); + None + } + _ => { + tracing::warn!("unknown output event {:?}", evt); // ignore possible future events + None + } + } + } +} diff --git a/druid-shell/src/backend/wayland/pointers.rs b/druid-shell/src/backend/wayland/pointers.rs new file mode 100644 index 0000000000..7901caa924 --- /dev/null +++ b/druid-shell/src/backend/wayland/pointers.rs @@ -0,0 +1,398 @@ +use std::collections::VecDeque; +use wayland_client::protocol::wl_pointer; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{self as wl}; +use wayland_cursor::CursorImageBuffer; +use wayland_cursor::CursorTheme; + +use crate::keyboard::Modifiers; +use crate::kurbo::{Point, Vec2}; +use crate::mouse; + +use super::application::ApplicationData; + +// Button constants (linux specific) +const BTN_LEFT: u32 = 0x110; +const BTN_RIGHT: u32 = 0x111; +const BTN_MIDDLE: u32 = 0x112; + +// used to keep track of click event counts. +#[derive(Debug, Clone)] +struct ClickDebouncer { + timestamp: std::time::Instant, + count: u8, + previous: mouse::MouseButton, +} + +impl Default for ClickDebouncer { + fn default() -> Self { + Self { + timestamp: std::time::Instant::now(), + count: 1, + previous: mouse::MouseButton::None, + } + } +} + +impl ClickDebouncer { + // this threshold was abritrarily chosen based on experimention. + // there is likely a better default based on research to use. + // during experimentation this allowed one to get to around 4 clicks. + // but likely heavily dependent on the machine. + const THRESHOLD: std::time::Duration = std::time::Duration::from_millis(500); + + fn reset(ts: std::time::Instant, btn: mouse::MouseButton) -> Self { + Self { + timestamp: ts, + count: 1, + previous: btn, + } + } + + fn debounce(&mut self, current: MouseEvtKind) -> MouseEvtKind { + let ts = std::time::Instant::now(); + + // reset counting and button. + if self.timestamp + ClickDebouncer::THRESHOLD < ts { + *self = ClickDebouncer::default(); + } + + match current { + MouseEvtKind::Up(mut evt) if self.previous == evt.button => { + evt.count = self.count; + MouseEvtKind::Up(evt) + } + MouseEvtKind::Down(mut evt) if self.previous == evt.button => { + self.count += 1; + evt.count = self.count; + MouseEvtKind::Down(evt) + } + MouseEvtKind::Down(evt) if self.previous != evt.button => { + *self = ClickDebouncer::reset(ts, evt.button); + MouseEvtKind::Down(evt) + } + MouseEvtKind::Leave => { + *self = ClickDebouncer::reset(ts, mouse::MouseButton::None); + current + } + _ => current, + } + } +} + +/// Collect up mouse events then emit them together on a pointer frame. +pub(crate) struct Pointer { + /// The image surface which contains the cursor image. + pub(crate) cursor_surface: wl::Main, + /// Events that have occurred since the last frame. + pub(crate) queued_events: std::cell::RefCell>, + /// Currently pressed buttons + buttons: std::cell::RefCell, + /// Current position + pos: std::cell::Cell, + wl_pointer: std::cell::RefCell>, + // used to keep track of the current clicking + clickevent: std::cell::RefCell, + /// cursor theme data. + theme: std::cell::RefCell, + /// Cache the current cursor, so we can see if it changed + current_cursor: std::cell::RefCell, +} + +/// Raw wayland pointer events. +#[derive(Debug)] +pub(crate) enum PointerEvent { + /// Mouse moved/entered + Motion { + pointer: wl_pointer::WlPointer, + point: Point, + }, + /// Mouse button pressed/released + Button { + button: u32, + state: wl_pointer::ButtonState, + }, + /// Axis movement + Axis { axis: wl_pointer::Axis, value: f64 }, + /// Mouse left + Leave, +} + +/// An enum that we will convert into the different callbacks. +#[derive(Debug)] +pub(crate) enum MouseEvtKind { + Move(mouse::MouseEvent), + Up(mouse::MouseEvent), + Down(mouse::MouseEvent), + Leave, + Wheel(mouse::MouseEvent), +} + +#[allow(unused)] +impl Pointer { + /// Create a new pointer + pub fn new(theme: CursorTheme, cursor: wl::Main) -> Self { + // ignore all events + cursor.quick_assign(|a1, event, a2| { + tracing::trace!("pointer surface event {:?} {:?} {:?}", a1, event, a2); + }); + + Pointer { + theme: std::cell::RefCell::new(theme), + buttons: std::cell::RefCell::new(mouse::MouseButtons::new()), + pos: std::cell::Cell::new(Point::ZERO), // will get set before we emit any events + queued_events: std::cell::RefCell::new(VecDeque::with_capacity(3)), // should be enough most of the time + cursor_surface: cursor, + wl_pointer: std::cell::RefCell::new(None), + current_cursor: std::cell::RefCell::new(mouse::Cursor::Arrow), + clickevent: std::cell::RefCell::new(ClickDebouncer::default()), + } + } + + pub fn attach(&self, current: wl_pointer::WlPointer) { + tracing::trace!("attaching pointer reference {:?}", current); + self.wl_pointer.replace(Some(current)); + } + + #[inline] + pub fn push(&self, event: PointerEvent) { + self.queued_events.borrow_mut().push_back(event); + } + + #[inline] + pub fn pop(&self) -> Option { + self.queued_events.borrow_mut().pop_front() + } + + #[inline] + pub fn cursor(&self) -> &WlSurface { + &self.cursor_surface + } + + pub fn replace(&self, cursor: &mouse::Cursor) { + let current = self.current_cursor.borrow().clone(); + let cursor = cursor.clone(); + + // Setting a new cursor involves communicating with the server, so don't do it if we + // don't have to. + if current == cursor { + return; + } + + let b = self.wl_pointer.borrow_mut(); + let wl_pointer = match &*b { + None => return, + Some(p) => p, + }; + + tracing::trace!("replacing cursor {:?} -> {:?}", current, cursor); + let buffer = match self.get_cursor_buffer(&cursor) { + None => return, + Some(b) => b, + }; + + self.current_cursor.replace(cursor); + wl_pointer.set_cursor(0, Some(&self.cursor_surface), 0, 0); + self.cursor_surface.attach(Some(&*buffer), 0, 0); + self.cursor_surface.damage_buffer(0, 0, i32::MAX, i32::MAX); + self.cursor_surface.commit(); + } + + fn get_cursor_buffer(&self, cursor: &mouse::Cursor) -> Option { + #[allow(deprecated)] + return match cursor { + mouse::Cursor::Arrow => self.unpack_image_buffer("left_ptr"), + mouse::Cursor::IBeam => self.unpack_image_buffer("xterm"), + mouse::Cursor::Crosshair => self.unpack_image_buffer("cross"), + mouse::Cursor::OpenHand => self.unpack_image_buffer("openhand"), + mouse::Cursor::NotAllowed => self.unpack_image_buffer("X_cursor"), + mouse::Cursor::ResizeLeftRight => self.unpack_image_buffer("row-resize"), + mouse::Cursor::ResizeUpDown => self.unpack_image_buffer("col-resize"), + mouse::Cursor::Pointer => self.unpack_image_buffer("pointer"), + mouse::Cursor::Custom(_) => { + tracing::warn!("custom cursors not implemented"); + self.unpack_image_buffer("left_ptr") + } + }; + } + + // Just use the first image, people using animated cursors have already made bad life + // choices and shouldn't expect it to work. + fn unpack_image_buffer(&self, name: &str) -> Option { + self.theme + .borrow_mut() + .get_cursor(name) + .map(|c| c[c.frame_and_duration(0).frame_index].clone()) + } + + pub(super) fn consume( + appdata: std::sync::Arc, + source: wl_pointer::WlPointer, + event: wl_pointer::Event, + ) { + match event { + wl_pointer::Event::Enter { + surface, + surface_x, + surface_y, + .. + } => { + appdata.pointer.push(PointerEvent::Motion { + point: Point::new(surface_x, surface_y), + pointer: source, + }); + } + wl_pointer::Event::Leave { surface, .. } => { + appdata.pointer.push(PointerEvent::Leave); + } + wl_pointer::Event::Motion { + surface_x, + surface_y, + .. + } => { + appdata.pointer.push(PointerEvent::Motion { + point: Point::new(surface_x, surface_y), + pointer: source, + }); + } + wl_pointer::Event::Button { button, state, .. } => { + appdata.pointer.push(PointerEvent::Button { button, state }); + } + wl_pointer::Event::Axis { axis, value, .. } => { + appdata.pointer.push(PointerEvent::Axis { axis, value }); + } + wl_pointer::Event::Frame => { + let winhandle = match appdata.acquire_current_window().and_then(|w| w.data()) { + Some(w) => w, + None => { + tracing::warn!("dropping mouse events, no window available"); + appdata.pointer.queued_events.borrow_mut().clear(); + return; + } + }; + let mut winhandle = winhandle.handler.borrow_mut(); + + // (re-entrancy) call user code + while let Some(event) = appdata.pointer.dequeue() { + match event { + MouseEvtKind::Move(evt) => winhandle.mouse_move(&evt), + MouseEvtKind::Up(evt) => winhandle.mouse_up(&evt), + MouseEvtKind::Down(evt) => winhandle.mouse_down(&evt), + MouseEvtKind::Wheel(evt) => winhandle.wheel(&evt), + MouseEvtKind::Leave => winhandle.mouse_leave(), + } + } + } + evt => { + log::warn!("Unhandled pointer event: {:?}", evt); + } + } + } + + fn dequeue(&self) -> Option { + use wl_pointer::{Axis, ButtonState}; + // sometimes we need to ignore an event and move on + loop { + let event = self.queued_events.borrow_mut().pop_front()?; + tracing::trace!("mouse event {:?}", event); + match event { + PointerEvent::Motion { pointer, point } => { + self.pos.replace(point); + return Some(MouseEvtKind::Move(mouse::MouseEvent { + pos: point, + buttons: *self.buttons.borrow(), + mods: Modifiers::empty(), + count: 0, + focus: false, + button: mouse::MouseButton::None, + wheel_delta: Vec2::ZERO, + })); + } + PointerEvent::Button { button, state } => { + let button = match linux_to_mouse_button(button) { + // Skip unsupported buttons. + None => { + tracing::debug!("unsupport button click {:?}", button); + continue; + } + Some(b) => b, + }; + let evt = match state { + ButtonState::Pressed => { + self.buttons.borrow_mut().insert(button); + self.clickevent.borrow_mut().debounce(MouseEvtKind::Down( + mouse::MouseEvent { + pos: self.pos.get(), + buttons: *self.buttons.borrow(), + mods: Modifiers::empty(), + count: 1, + focus: false, + button, + wheel_delta: Vec2::ZERO, + }, + )) + } + ButtonState::Released => { + let mut updated = self.buttons.borrow_mut().remove(button); + self.clickevent.borrow_mut().debounce(MouseEvtKind::Up( + mouse::MouseEvent { + pos: self.pos.get(), + buttons: *self.buttons.borrow(), + mods: Modifiers::empty(), + count: 0, + focus: false, + button, + wheel_delta: Vec2::ZERO, + }, + )) + } + _ => { + log::error!("mouse button changed, but not pressed or released"); + continue; + } + }; + return Some(evt); + } + PointerEvent::Axis { axis, value } => { + let wheel_delta = match axis { + Axis::VerticalScroll => Vec2::new(0., value), + Axis::HorizontalScroll => Vec2::new(value, 0.), + _ => { + log::error!("axis direction not vertical or horizontal"); + continue; + } + }; + return Some(MouseEvtKind::Wheel(mouse::MouseEvent { + pos: self.pos.get(), + buttons: *self.buttons.borrow(), + mods: Modifiers::empty(), + count: 0, + focus: false, + button: mouse::MouseButton::None, + wheel_delta, + })); + } + PointerEvent::Leave => { + // The parent will remove us. + return Some(MouseEvtKind::Leave); + } + } + } + } +} + +impl Drop for Pointer { + fn drop(&mut self) { + self.cursor_surface.destroy(); + } +} + +#[inline] +fn linux_to_mouse_button(button: u32) -> Option { + match button { + BTN_LEFT => Some(mouse::MouseButton::Left), + BTN_RIGHT => Some(mouse::MouseButton::Right), + BTN_MIDDLE => Some(mouse::MouseButton::Middle), + _ => None, + } +} diff --git a/druid-shell/src/backend/wayland/screen.rs b/druid-shell/src/backend/wayland/screen.rs new file mode 100644 index 0000000000..93a06ab05c --- /dev/null +++ b/druid-shell/src/backend/wayland/screen.rs @@ -0,0 +1,47 @@ +// Copyright 2020 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! wayland Monitors and Screen information. +use crate::screen::Monitor; + +use super::error; +use super::outputs; + +fn _get_monitors() -> Result, error::Error> { + let metas = outputs::current()?; + let monitors: Vec = metas + .iter() + .map(|m| { + let rect = kurbo::Rect::from_origin_size( + (m.position.x as f64, m.position.y as f64), + (m.logical.width as f64, m.logical.height as f64), + ); + Monitor::new(false, rect, rect) + }) + .collect(); + Ok(monitors) +} + +pub(crate) fn get_monitors() -> Vec { + match _get_monitors() { + Ok(m) => m, + Err(cause) => { + tracing::error!( + "unable to detect monitors, failed to connect to wayland server {:?}", + cause + ); + Vec::new() + } + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/buffers.rs b/druid-shell/src/backend/wayland/surfaces/buffers.rs new file mode 100644 index 0000000000..9cf55039a8 --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/buffers.rs @@ -0,0 +1,646 @@ +use kurbo::{Rect, Size}; +use nix::{ + errno::Errno, + fcntl::OFlag, + sys::{ + mman::{mmap, munmap, shm_open, MapFlags, ProtFlags}, + stat::Mode, + }, + unistd::{close, ftruncate}, +}; +use std::{ + cell::{Cell, RefCell}, + convert::{TryFrom, TryInto}, + fmt, + ops::{Deref, DerefMut}, + os::{raw::c_void, unix::prelude::RawFd}, + ptr::{self, NonNull}, + rc::{Rc, Weak as WeakRc}, + slice, +}; +use wayland_client::{ + self as wl, + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_shm::{self, WlShm}, + wl_shm_pool::WlShmPool, + wl_surface::WlSurface, + }, +}; + +use super::surface; + +/// Number of bytes for a pixel (argb = 4) +pub(super) const PIXEL_WIDTH: i32 = 4; +/// Number of frames we need (2 for double buffering) +pub(super) const NUM_FRAMES: i32 = 2; + +/// A collection of buffers that can change size. +/// +/// This object knows nothing about scaling or events. It just provides buffers to draw into. +pub struct Buffers { + /// Release buffers which are just waiting to be freed. + released: Cell>, + /// The actual buffer objects. + buffers: Cell>, + /// Which buffer is the next to present. Iterates through to `N-1` then wraps. Draw to this + /// buffer + pending: Cell, + /// The physical size of the buffers. + /// + /// This will be different from the buffers' actual size if `recreate_buffers` is true. + // NOTE: This really should support fractional scaling, use unstable protocol. + size: Cell, + /// Do we need to rebuild the framebuffers (size changed). + recreate_buffers: Cell, + /// This flag allows us to check that we only hand out a mutable ref to the buffer data once. + /// Otherwise providing mutable access to the data would be unsafe. + pending_buffer_borrowed: Cell, + + /// Shared memory to allocate buffers in + shm: RefCell, +} + +impl Buffers { + /// Create a new `Buffers` object. + /// + pub fn new(wl_shm: wl::Main, size: RawSize) -> Rc { + assert!(N >= 2, "must be at least 2 buffers"); + Rc::new(Self { + released: Cell::new(Vec::new()), + buffers: Cell::new(None), + pending: Cell::new(0), + size: Cell::new(size), + recreate_buffers: Cell::new(true), + pending_buffer_borrowed: Cell::new(false), + shm: RefCell::new(Shm::new(wl_shm).expect("error allocating shared memory")), + }) + } + + /// Get the physical size of the buffer. + pub fn size(&self) -> RawSize { + self.size.get() + } + + /// Request that the size of the buffer is changed. + pub fn set_size(&self, updated: RawSize) { + assert!(!updated.is_empty(), "window size must not be empty"); + let old = self.size.replace(updated); + self.recreate_buffers.set(old != updated); + } + + /// Request painting the next frame. + /// + /// This calls into user code. To avoid re-entrancy, ensure that we are not already in user + /// code (defer this call if necessary). + /// + /// We will call into `WindowData` to paint the frame, and present it. If no buffers are + /// available we will set a flag, so that when one becomes available we immediately paint and + /// present. This includes if we need to resize. + pub fn request_paint(self: &Rc, window: &surface::Data) { + tracing::trace!( + "request_paint {:?} {:?}", + self.size.get(), + window.get_size() + ); + // if our size is empty there is nothing to do. + if self.size.get().is_empty() { + return; + } + + if self.pending_buffer_borrowed.get() { + panic!("called request_paint during painting"); + } + + // recreate if necessary + self.buffers_recreate(); + + // paint if we have a buffer available. + if self.pending_buffer_released() { + self.paint_unchecked(window); + } + + // attempt to release any unused buffers. + self.buffers_drop_unused(); + } + + /// Paint the next frame, without checking if the buffer is free. + fn paint_unchecked(self: &Rc, window: &surface::Data) { + tracing::trace!("buffer.paint_unchecked"); + let mut buf_data = self.pending_buffer_data().unwrap(); + debug_assert!( + self.pending_buffer_released(), + "buffer in use/not initialized" + ); + + window.paint( + self.size.get(), + &mut *buf_data, + self.recreate_buffers.replace(false), + ); + } + + // attempt to release unused buffers. + fn buffers_drop_unused(&self) { + let mut pool = self.released.take(); + pool.retain(|b| { + if b.in_use.get() { + return true; + } + b.destroy(); + false + }); + self.released.replace(pool); + } + + fn buffers_invalidate(&self) { + if let Some(buffers) = self.buffers.replace(None) { + let mut tmp = self.released.take(); + tmp.append(&mut buffers.to_vec()); + self.released.replace(tmp); + } + } + + /// Destroy the current buffers, resize the shared memory pool if necessary, and create new + /// buffers. + fn buffers_recreate(&self) { + if !self.recreate_buffers.get() { + return; + } + + debug_assert!(!self.pending_buffer_borrowed.get()); + + // move current buffers into the release queue to be cleaned up later. + self.buffers_invalidate(); + + let new_buffer_size = self.size.get().buffer_size(N.try_into().unwrap()); + // This is probably OOM if it fails, but we unwrap to report the underlying error. + self.shm.borrow_mut().extend(new_buffer_size).unwrap(); + + let pool = self.shm.borrow_mut().create_pool(); + self.buffers.set({ + let mut buffers = vec![]; + let size = self.size.get(); + for i in 0..N { + buffers.push(Buffer::create(&pool, i, size.width, size.height)); + } + Some(buffers.try_into().unwrap()) + }); + pool.destroy(); + // Don't unset `recreate_buffers` here. We immediately call paint_unchecked, and need to + // know if buffers were recreated (to invalidate the whole window). + } + + fn with_buffers(&self, f: impl FnOnce(&Option<[Buffer; N]>) -> T) -> T { + let buffers = self.buffers.replace(None); + let out = f(&buffers); + self.buffers.set(buffers); + out + } + + /// Get a ref to the next buffer to draw to. + fn with_pending_buffer(&self, f: impl FnOnce(Option<&Buffer>) -> T) -> T { + self.with_buffers(|buffers| f(buffers.as_ref().map(|buffers| &buffers[self.pending.get()]))) + } + + /// For checking whether the next buffer is free. + fn pending_buffer_released(&self) -> bool { + self.with_pending_buffer(|buf| buf.map(|buf| !buf.in_use.get()).unwrap_or(false)) + } + + /// Get the raw buffer data of the next buffer to draw to. + /// + /// Will return `None` if buffer already borrowed. + fn pending_buffer_data(self: &Rc) -> Option> { + if self.pending_buffer_borrowed.get() { + None + } else { + self.pending_buffer_borrowed.set(true); + let frame_len = self.frame_len(); + // Safety: we make sure the data is only loaned out once. + unsafe { + Some(BufferData { + buffers: Rc::downgrade(self), + mmap: self + .shm + .borrow() + .mmap(frame_len * self.pending.get(), frame_len), + }) + } + } + } + + /// Signal to wayland that the pending buffer is ready to be presented, and switch the next + /// buffer to be the pending one. + pub(crate) fn attach(&self, window: &surface::Data) { + self.with_pending_buffer(|buf| buf.unwrap().attach(&window.wl_surface.borrow())); + self.pending.set((self.pending.get() + 1) % N); + } + + fn frame_len(&self) -> usize { + let size = self.size.get(); + (PIXEL_WIDTH * size.width * size.height) + .try_into() + .expect("integer overflow") + } +} + +/// A wrapper round `WlBuffer` that tracks whether the buffer is released. +/// +/// No allocations on `clone`. +#[derive(Debug, Clone)] +pub struct Buffer { + inner: wl::Main, + in_use: Rc>, +} + +impl Buffer { + /// Create a new buffer using the given backing storage. It is the responsibility of the caller + /// to ensure buffers don't overlap, and the backing storage has enough space. + // Window handle is needed for the callback. + pub fn create(pool: &wl::Main, idx: usize, width: i32, height: i32) -> Self { + let offset = i32::try_from(idx).unwrap() * width * height * PIXEL_WIDTH; + let stride = width * PIXEL_WIDTH; + let inner = pool.create_buffer(offset, width, height, stride, wl_shm::Format::Argb8888); + let in_use = Rc::new(Cell::new(false)); + + inner.quick_assign(with_cloned!(in_use; move |b, event, _dispatchdata| { + tracing::trace!("buffer event: {:?} {:?}", b, event); + match event { + wl_buffer::Event::Release => { + in_use.set(false); + } + _ => tracing::warn!("unhandled wayland buffer event: {:?} {:?}", b, event), + } + })); + + Buffer { inner, in_use } + } + + pub fn attach(&self, wl_surface: &wl::Main) { + if self.in_use.get() { + panic!("attaching an already in-use surface"); + } + self.in_use.set(true); + wl_surface.attach(Some(&self.inner), 0, 0); + } + + pub fn destroy(&self) { + if self.in_use.get() { + panic!("Destroying a buffer while it is in use"); + } + self.inner.destroy(); + } +} + +pub struct BufferData { + buffers: WeakRc>, + mmap: Mmap, +} + +impl Deref for BufferData { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + self.mmap.deref() + } +} + +impl DerefMut for BufferData { + fn deref_mut(&mut self) -> &mut [u8] { + self.mmap.deref_mut() + } +} + +impl Drop for BufferData { + fn drop(&mut self) { + if let Some(buffers) = self.buffers.upgrade() { + buffers.pending_buffer_borrowed.set(false); + } + } +} + +/// RAII wrapper for shm_open (file descriptors for mmap'd shared memory) +/// +/// Designed to work like a vec: to manage extending when necessary. +pub struct Shm { + inner: RawFd, + size: usize, + // a handle on the wayland structure. + wl_shm: wl::Main, +} + +#[allow(unused)] +impl Shm { + /// Create a new shared memory object. Will be empty until resized. + pub fn new(wl_shm: wl::Main) -> Result { + // TODO is this a good way to choose a filename? What should our retry strategy be? + let name = format!("/druid-wl-{}", rand::random::()); + // Open the file we will use for shared memory. + let fd = shm_open( + name.as_str(), + OFlag::O_RDWR | OFlag::O_EXCL | OFlag::O_CREAT, + Mode::S_IRUSR | Mode::S_IWUSR, + )?; + + // The memory is 0-sized until we resize it with `ftruncate`. + let shm = Shm { + inner: fd, + size: 0, + wl_shm, + }; + Ok(shm) + } + + /// Resizes the shared memory pool. + /// + /// This is almost certainly unsafe if the server is using the memory TODO use locking + /// (provided by wayland I think). + pub fn resize(&mut self, new_size: i32) -> Result<(), nix::Error> { + let new_size: usize = new_size.try_into().unwrap(); + if self.size == new_size { + return Ok(()); + } + + // allocate the space (retry on interrupt) + loop { + match ftruncate(self.inner, new_size.try_into().unwrap()) { + Ok(()) => { + self.size = new_size; + return Ok(()); + } + Err(e) if matches!(e.as_errno(), Some(Errno::EINTR)) => { + // continue (try again) + } + Err(e) => { + return Err(e); + } + } + } + } + + /// Like `resize`, but doesn't shrink. + pub fn extend(&mut self, new_size: i32) -> Result<(), nix::Error> { + if self.size < new_size.try_into().unwrap() { + self.resize(new_size) + } else { + Ok(()) + } + } + + pub fn size(&self) -> usize { + self.size + } + + /// Create a `WlShmPool` backed by our memory that will be mmap'd by the server. + pub fn create_pool(&self) -> wl::Main { + self.wl_shm + .create_pool(self.inner, self.size.try_into().unwrap()) + } + + /// A method to make all the data `1` (white). Useful for debugging. + /// + /// Safe only when no frames are in use. + #[allow(unused)] + pub fn fill_white(&mut self) { + unsafe { + let mut buf = self.mmap(0, self.size); + for byte in buf.as_mut() { + *byte = 0xff; + } + } + } + + /// Get access to the shared memory for the given frame. + /// + /// # Safety + /// + /// It's not checked if any other process has access to the memory. Data races may occur if + /// they do. + pub unsafe fn mmap(&self, offset: usize, len: usize) -> Mmap { + Mmap::from_raw(self.inner, self.size, offset, len).unwrap() + } + + /// Closing with error checking + pub fn close(self) -> Result<(), nix::Error> { + close(self.inner) + } +} + +impl Drop for Shm { + fn drop(&mut self) { + // cannot handle errors in drop. + let _ = close(self.inner); + } +} + +pub struct Mmap { + ptr: NonNull, + size: usize, + offset: usize, + len: usize, +} + +impl Mmap { + /// `fd` and `size` are the whole memory you want to map. `offset` and `len` are there to + /// provide extra protection (only giving you access to that part). + /// + /// # Safety + /// + /// Concurrent use of the memory we map to isn't checked. + #[inline] + pub unsafe fn from_raw( + fd: RawFd, + size: usize, + offset: usize, + len: usize, + ) -> Result { + Self::from_raw_inner(fd, size, offset, len, false) + } + + #[inline] + pub unsafe fn from_raw_private( + fd: RawFd, + size: usize, + offset: usize, + len: usize, + ) -> Result { + Self::from_raw_inner(fd, size, offset, len, true) + } + + unsafe fn from_raw_inner( + fd: RawFd, + size: usize, + offset: usize, + len: usize, + private: bool, + ) -> Result { + assert!(offset + len <= size, "{0} + {1} <= {2}", offset, len, size,); + let map_flags = if private { + MapFlags::MAP_PRIVATE + } else { + MapFlags::MAP_SHARED + }; + let ptr = mmap( + ptr::null_mut(), + size, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + map_flags, + fd, + 0, + )?; + Ok(Mmap { + ptr: NonNull::new(ptr).unwrap(), + size, + offset, + len, + }) + } +} + +impl Deref for Mmap { + type Target = [u8]; + fn deref(&self) -> &[u8] { + unsafe { + let start = self.ptr.as_ptr().offset(self.offset.try_into().unwrap()); + slice::from_raw_parts(start as *const u8, self.len) + } + } +} + +impl DerefMut for Mmap { + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { + let start = self.ptr.as_ptr().offset(self.offset.try_into().unwrap()); + slice::from_raw_parts_mut(start as *mut u8, self.len) + } + } +} + +impl Drop for Mmap { + fn drop(&mut self) { + unsafe { + if let Err(e) = munmap(self.ptr.as_ptr(), self.size) { + log::warn!("Error unmapping memory: {}", e); + } + } + } +} +#[derive(Copy, Clone, PartialEq)] +pub struct RawSize { + pub width: i32, + pub height: i32, +} + +impl RawSize { + pub const ZERO: Self = Self { + width: 0, + height: 0, + }; + + /// How many bytes do we need to store a frame of this size (in pixels) + pub fn frame_size(self) -> i32 { + // Check for overflow + assert!(self.width.checked_mul(self.height).unwrap() < i32::MAX / PIXEL_WIDTH); + self.width * self.height * PIXEL_WIDTH + } + + /// Helper function to get the total buffer size we will need for all the frames. + pub fn buffer_size(self, frames: i32) -> i32 { + // Check for overflow + assert!(self.width.checked_mul(self.height).unwrap() < i32::MAX / (PIXEL_WIDTH * frames)); + self.width * self.height * PIXEL_WIDTH * frames + } + + pub fn scale(self, scale: i32) -> Self { + // NOTE no overflow checking atm. + RawSize { + width: self.width * scale, + height: self.height * scale, + } + } + + pub fn to_rect(self) -> RawRect { + RawRect { + x0: 0, + y0: 0, + x1: self.width, + y1: self.height, + } + } + + pub fn area(self) -> i32 { + self.width * self.height + } + + pub fn is_empty(self) -> bool { + self.area() == 0 + } +} + +impl From for RawSize { + fn from(s: Size) -> Self { + let width = s.width as i32; + let height = s.height as i32; + // Sanity check + assert!(width >= 0 && height >= 0); + + RawSize { width, height } + } +} + +impl From for Size { + fn from(s: RawSize) -> Self { + Size::new(s.width as f64, s.height as f64) + } +} + +impl fmt::Debug for RawSize { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}×{}", self.width, self.height) + } +} + +#[derive(Debug)] +pub struct RawRect { + pub x0: i32, + pub y0: i32, + pub x1: i32, + pub y1: i32, +} + +impl RawRect { + pub fn scale(self, scale: i32) -> Self { + // NOTE no overflow checking atm. + RawRect { + x0: self.x0 * scale, + y0: self.y0 * scale, + x1: self.x1 * scale, + y1: self.y1 * scale, + } + } +} + +impl From for RawRect { + fn from(r: Rect) -> Self { + let max = i32::MAX as f64; + assert!(r.x0.abs() < max && r.y0.abs() < max && r.x1.abs() < max && r.y1.abs() < max); + RawRect { + x0: r.x0 as i32, + y0: r.y0 as i32, + x1: r.x1 as i32, + y1: r.y1 as i32, + } + } +} + +impl From for Rect { + fn from(r: RawRect) -> Self { + Rect { + x0: r.x0 as f64, + y0: r.y0 as f64, + x1: r.x1 as f64, + y1: r.y1 as f64, + } + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/idle.rs b/druid-shell/src/backend/wayland/surfaces/idle.rs new file mode 100644 index 0000000000..5772df9a92 --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/idle.rs @@ -0,0 +1,58 @@ +use crate::common_util::IdleCallback; +use crate::window; + +/// This represents different Idle Callback Mechanism +pub(super) enum Kind { + Callback(Box), + Token(window::IdleToken), +} + +impl std::fmt::Debug for Kind { + fn fmt(&self, format: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Kind::Callback(_) => format.debug_struct("Idle(Callback)").finish(), + Kind::Token(token) => format + .debug_struct("Idle(Token)") + .field("token", &token) + .finish(), + } + } +} + +#[derive(Clone)] +pub struct Handle { + pub(super) queue: std::sync::Arc>>, +} + +impl Handle { + /// Add an idle handler, which is called (once) when the message loop + /// is empty. The idle handler will be run from the main UI thread, and + /// won't be scheduled if the associated view has been dropped. + /// + /// Note: the name "idle" suggests that it will be scheduled with a lower + /// priority than other UI events, but that's not necessarily the case. + pub fn add_idle_callback(&self, callback: F) + where + F: FnOnce(&mut dyn window::WinHandler) + Send + 'static, + { + tracing::trace!("add_idle_callback initiated"); + let mut queue = self.queue.lock().unwrap(); + queue.push(Kind::Callback(Box::new(callback))); + } + + pub fn add_idle_token(&self, token: window::IdleToken) { + tracing::trace!("add_idle_token initiated {:?}", token); + let mut queue = self.queue.lock().unwrap(); + queue.push(Kind::Token(token)); + } +} + +pub(crate) fn run(state: &Handle, winhandle: &mut dyn window::WinHandler) { + let queue: Vec<_> = std::mem::take(&mut state.queue.lock().unwrap()); + for item in queue { + match item { + Kind::Callback(it) => it.call(winhandle), + Kind::Token(it) => winhandle.idle(it), + } + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/layershell.rs b/druid-shell/src/backend/wayland/surfaces/layershell.rs new file mode 100644 index 0000000000..3dbc4eb973 --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/layershell.rs @@ -0,0 +1,421 @@ +use wayland_client as wlc; +use wayland_protocols::wlr::unstable::layer_shell::v1::client as layershell; +use wayland_protocols::xdg_shell::client::xdg_surface; + +use crate::kurbo; +use crate::window; + +use super::super::error; +use super::super::outputs; +use super::surface; +use super::Compositor; +use super::CompositorHandle; +use super::Handle; +use super::Outputs; +use super::Popup; + +struct Inner { + config: Config, + wl_surface: std::cell::RefCell, + ls_surface: + std::cell::RefCell>, + requires_initialization: std::cell::RefCell, + available: std::cell::RefCell, +} + +impl Inner { + fn popup<'a>( + &self, + surface: &'a wlc::Main, + pos: &'a wlc::Main, + ) -> wlc::Main { + let popup = surface.get_popup(None, pos); + self.ls_surface.borrow().get_popup(&popup); + popup + } +} + +impl Popup for Inner { + fn surface<'a>( + &self, + surface: &'a wlc::Main, + pos: &'a wlc::Main, + ) -> Result, error::Error> + { + Ok(self.popup(surface, pos)) + } +} + +impl Drop for Inner { + fn drop(&mut self) { + self.ls_surface.borrow().destroy(); + } +} + +impl From for std::sync::Arc { + fn from(s: Inner) -> std::sync::Arc { + std::sync::Arc::::from(s.wl_surface.borrow().clone()) + } +} + +#[derive(Clone, Debug)] +pub struct Margin { + top: i32, + right: i32, + bottom: i32, + left: i32, +} + +impl Default for Margin { + fn default() -> Self { + Margin::from((0, 0, 0, 0)) + } +} + +impl Margin { + pub fn new(m: impl Into) -> Self { + m.into() + } + + pub fn uniform(m: i32) -> Self { + Margin::from((m, m, m, m)) + } +} + +impl From<(i32, i32, i32, i32)> for Margin { + fn from(margins: (i32, i32, i32, i32)) -> Self { + Self { + top: margins.0, + left: margins.1, + bottom: margins.2, + right: margins.3, + } + } +} + +impl From for Margin { + fn from(m: i32) -> Self { + Margin::from((m, m, m, m)) + } +} + +impl From<(i32, i32)> for Margin { + fn from(m: (i32, i32)) -> Self { + Margin::from((m.0, m.1, m.0, m.1)) + } +} + +#[derive(Clone)] +pub struct Config { + pub initial_size: kurbo::Size, + pub layer: layershell::zwlr_layer_shell_v1::Layer, + pub keyboard_interactivity: layershell::zwlr_layer_surface_v1::KeyboardInteractivity, + pub anchor: layershell::zwlr_layer_surface_v1::Anchor, + pub exclusive_zone: i32, + pub margin: Margin, + pub namespace: &'static str, + pub app_id: &'static str, +} + +impl Config { + pub fn keyboard_interactivity( + mut self, + mode: layershell::zwlr_layer_surface_v1::KeyboardInteractivity, + ) -> Self { + self.keyboard_interactivity = mode; + self + } + + pub fn layer(mut self, layer: layershell::zwlr_layer_shell_v1::Layer) -> Self { + self.layer = layer; + self + } + + pub fn anchor(mut self, anchor: layershell::zwlr_layer_surface_v1::Anchor) -> Self { + self.anchor = anchor; + self + } + + pub fn margin(mut self, m: impl Into) -> Self { + self.margin = m.into(); + self + } + + fn apply(&self, surface: &Surface) { + surface.initialize_dimensions(self.initial_size); + let ls = surface.inner.ls_surface.borrow(); + ls.set_exclusive_zone(self.exclusive_zone); + ls.set_anchor(self.anchor); + ls.set_keyboard_interactivity(self.keyboard_interactivity); + ls.set_margin( + self.margin.top, + self.margin.right, + self.margin.bottom, + self.margin.left, + ); + } +} + +impl Default for Config { + fn default() -> Self { + Self { + layer: layershell::zwlr_layer_shell_v1::Layer::Overlay, + initial_size: kurbo::Size::ZERO, + keyboard_interactivity: layershell::zwlr_layer_surface_v1::KeyboardInteractivity::None, + anchor: layershell::zwlr_layer_surface_v1::Anchor::empty(), + exclusive_zone: 0, + margin: Margin::default(), + namespace: "druid", + app_id: "", + } + } +} + +#[derive(Clone)] +pub struct Surface { + inner: std::sync::Arc, +} + +impl Surface { + pub fn new( + c: impl Into, + handler: Box, + config: Config, + ) -> Self { + let compositor = CompositorHandle::new(c); + let wl_surface = surface::Surface::new(compositor.clone(), handler, kurbo::Size::ZERO); + let ls_surface = compositor.zwlr_layershell_v1().get_layer_surface( + &wl_surface.inner.wl_surface.borrow(), + None, + config.layer, + config.namespace.to_string(), + ); + + let handle = Self { + inner: std::sync::Arc::new(Inner { + config, + wl_surface: std::cell::RefCell::new(wl_surface), + ls_surface: std::cell::RefCell::new(ls_surface), + requires_initialization: std::cell::RefCell::new(true), + available: std::cell::RefCell::new(false), + }), + }; + + Surface::initialize(&handle); + handle + } + + pub(crate) fn with_handler T>( + &self, + f: F, + ) -> Option { + std::sync::Arc::::from(self).with_handler(f) + } + + fn initialize_dimensions(&self, dim: kurbo::Size) { + self.inner + .ls_surface + .borrow() + .set_size(dim.width as u32, dim.height as u32); + self.inner + .wl_surface + .borrow() + .inner + .handler + .borrow_mut() + .size(dim); + } + + fn initialize(handle: &Surface) { + if !handle.inner.requires_initialization.replace(false) { + return; + } + + tracing::info!("attempting to initialize layershell"); + + handle.inner.ls_surface.borrow().quick_assign({ + let handle = handle.clone(); + move |a1, event, a2| { + tracing::debug!("consuming event {:?} {:?} {:?}", a1, event, a2); + Surface::consume_layershell_event(&handle, &a1, &event, &a2); + } + }); + + handle.inner.config.apply(handle); + handle.inner.wl_surface.borrow().commit(); + } + + fn consume_layershell_event( + handle: &Surface, + a1: &wlc::Main, + event: &layershell::zwlr_layer_surface_v1::Event, + data: &wlc::DispatchData, + ) { + match *event { + layershell::zwlr_layer_surface_v1::Event::Configure { + serial, + width, + height, + } => { + let mut dim = handle.inner.config.initial_size; + // compositor is deferring to the client for determining the size + // when values are zero. + if width != 0 && height != 0 { + dim = kurbo::Size::new(width as f64, height as f64); + } + + let ls = handle.inner.ls_surface.borrow(); + ls.ack_configure(serial); + ls.set_size(dim.width as u32, dim.height as u32); + handle.inner.wl_surface.borrow().update_dimensions(dim); + handle.inner.wl_surface.borrow().request_paint(); + handle.inner.available.replace(true); + } + layershell::zwlr_layer_surface_v1::Event::Closed => { + handle.inner.ls_surface.borrow().destroy(); + handle.inner.available.replace(false); + handle.inner.requires_initialization.replace(true); + } + _ => tracing::warn!("unimplemented event {:?} {:?} {:?}", a1, event, data), + } + } +} + +impl Outputs for Surface { + fn removed(&self, o: &outputs::Meta) { + self.inner.wl_surface.borrow().removed(o); + } + + fn inserted(&self, o: &outputs::Meta) { + if !*self.inner.requires_initialization.borrow() { + tracing::debug!( + "skipping reinitialization output for layershell {:?} {:?}", + o.id(), + o + ); + return; + } + + tracing::debug!("reinitializing output for layershell {:?} {:?}", o.id(), o); + let sdata = self.inner.wl_surface.borrow().inner.clone(); + let replacedsurface = self + .inner + .wl_surface + .replace(surface::Surface::replace(&sdata)); + let sdata = self.inner.wl_surface.borrow().inner.clone(); + let replacedlayershell = + self.inner + .ls_surface + .replace(sdata.compositor.zwlr_layershell_v1().get_layer_surface( + &self.inner.wl_surface.borrow().inner.wl_surface.borrow(), + None, + self.inner.config.layer, + self.inner.config.namespace.to_string(), + )); + Surface::initialize(self); + + tracing::debug!("replaced surface {:p}", &replacedsurface); + tracing::debug!("current surface {:p}", &self.inner.wl_surface.borrow()); + tracing::debug!("replaced layershell {:p}", &replacedlayershell); + tracing::debug!("current layershell {:p}", &self.inner.ls_surface.borrow()); + } +} + +impl Popup for Surface { + fn surface<'a>( + &self, + popup: &'a wlc::Main, + pos: &'a wlc::Main, + ) -> Result, error::Error> + { + Ok(self.inner.popup(popup, pos)) + } +} + +impl Handle for Surface { + fn get_size(&self) -> kurbo::Size { + return self.inner.wl_surface.borrow().get_size(); + } + + fn set_size(&self, dim: kurbo::Size) { + return self.inner.wl_surface.borrow().set_size(dim); + } + + fn request_anim_frame(&self) { + if *self.inner.available.borrow() { + self.inner.wl_surface.borrow().request_anim_frame() + } + } + + fn invalidate(&self) { + return self.inner.wl_surface.borrow().invalidate(); + } + + fn invalidate_rect(&self, rect: kurbo::Rect) { + return self.inner.wl_surface.borrow().invalidate_rect(rect); + } + + fn remove_text_field(&self, token: crate::TextFieldToken) { + return self.inner.wl_surface.borrow().remove_text_field(token); + } + + fn set_focused_text_field(&self, active_field: Option) { + return self + .inner + .wl_surface + .borrow() + .set_focused_text_field(active_field); + } + + fn get_idle_handle(&self) -> super::idle::Handle { + return self.inner.wl_surface.borrow().get_idle_handle(); + } + + fn get_scale(&self) -> crate::Scale { + return self.inner.wl_surface.borrow().get_scale(); + } + + fn run_idle(&self) { + if *self.inner.available.borrow() { + self.inner.wl_surface.borrow().run_idle(); + } + } + + fn release(&self) { + return self.inner.wl_surface.borrow().release(); + } + + fn data(&self) -> Option> { + return self.inner.wl_surface.borrow().data(); + } +} + +impl From<&Surface> for std::sync::Arc { + fn from(s: &Surface) -> std::sync::Arc { + std::sync::Arc::::from(s.inner.wl_surface.borrow().clone()) + } +} + +impl From for std::sync::Arc { + fn from(s: Surface) -> std::sync::Arc { + std::sync::Arc::::from(s.inner.wl_surface.borrow().clone()) + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Self { + Box::new(s) as Box + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/mod.rs b/druid-shell/src/backend/wayland/surfaces/mod.rs new file mode 100644 index 0000000000..1df1bdf3a9 --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/mod.rs @@ -0,0 +1,177 @@ +use wayland_client::protocol::wl_shm::WlShm; +use wayland_client::{self as wlc, protocol::wl_surface::WlSurface}; +use wayland_protocols::unstable::xdg_decoration::v1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1; +use wayland_protocols::wlr::unstable::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1; +use wayland_protocols::xdg_shell::client::xdg_popup; +use wayland_protocols::xdg_shell::client::xdg_positioner; +use wayland_protocols::xdg_shell::client::xdg_surface; + +use crate::kurbo; +use crate::Scale; +use crate::TextFieldToken; + +use super::error; +use super::outputs; + +pub mod buffers; +pub mod idle; +pub mod layershell; +pub mod popup; +pub mod surface; +pub mod toplevel; + +pub static GLOBAL_ID: crate::Counter = crate::Counter::new(); + +pub trait Compositor { + fn output(&self, id: u32) -> Option; + fn create_surface(&self) -> wlc::Main; + fn shared_mem(&self) -> wlc::Main; + fn get_xdg_surface(&self, surface: &wlc::Main) + -> wlc::Main; + fn get_xdg_positioner(&self) -> wlc::Main; + fn zxdg_decoration_manager_v1(&self) -> wlc::Main; + fn zwlr_layershell_v1(&self) -> wlc::Main; +} + +pub trait Decor { + fn inner_set_title(&self, title: String); +} + +impl dyn Decor { + pub fn set_title(&self, title: impl Into) { + self.inner_set_title(title.into()) + } +} + +pub trait Popup { + fn surface<'a>( + &self, + popup: &'a wlc::Main, + pos: &'a wlc::Main, + ) -> Result, error::Error>; +} + +pub(super) trait Outputs { + fn removed(&self, o: &outputs::Meta); + fn inserted(&self, o: &outputs::Meta); +} + +// handle on given surface. +pub trait Handle { + fn get_size(&self) -> kurbo::Size; + fn set_size(&self, dim: kurbo::Size); + fn request_anim_frame(&self); + fn invalidate(&self); + fn invalidate_rect(&self, rect: kurbo::Rect); + fn remove_text_field(&self, token: TextFieldToken); + fn set_focused_text_field(&self, active_field: Option); + fn get_idle_handle(&self) -> idle::Handle; + fn get_scale(&self) -> Scale; + fn run_idle(&self); + fn release(&self); + fn data(&self) -> Option>; +} + +#[derive(Clone)] +pub struct CompositorHandle { + inner: std::sync::Weak, +} + +impl CompositorHandle { + pub fn new(c: impl Into) -> Self { + c.into() + } + + pub fn direct(c: std::sync::Weak) -> Self { + Self { inner: c } + } + + fn create_surface(&self) -> Option> { + self.inner.upgrade().map(|c| c.create_surface()) + } + + /// Recompute the scale to use (the maximum of all the provided outputs). + fn recompute_scale(&self, outputs: &std::collections::HashSet) -> i32 { + let compositor = match self.inner.upgrade() { + Some(c) => c, + None => panic!("should never recompute scale of window that has been dropped"), + }; + tracing::debug!("computing scale using {:?} outputs", outputs.len()); + let scale = outputs.iter().fold(0, |scale, id| { + tracing::debug!("recomputing scale using output {:?}", id); + match compositor.output(*id) { + None => { + tracing::warn!( + "we still have a reference to an output that's gone away. The output had id {}", + id, + ); + scale + }, + Some(output) => scale.max(output.scale as i32), + } + }); + + match scale { + 0 => { + tracing::warn!("wayland never reported which output we are drawing to"); + 1 + } + scale => scale, + } + } +} + +impl Compositor for CompositorHandle { + fn output(&self, id: u32) -> Option { + match self.inner.upgrade() { + None => None, + Some(c) => c.output(id), + } + } + + fn create_surface(&self) -> wlc::Main { + match self.inner.upgrade() { + None => panic!("unable to acquire underyling compositor to create a surface"), + Some(c) => c.create_surface(), + } + } + + fn shared_mem(&self) -> wlc::Main { + match self.inner.upgrade() { + None => panic!("unable to acquire underyling compositor to acquire shared memory"), + Some(c) => c.shared_mem(), + } + } + + fn get_xdg_positioner(&self) -> wlc::Main { + match self.inner.upgrade() { + None => panic!("unable to acquire underyling compositor to create an xdg positioner"), + Some(c) => c.get_xdg_positioner(), + } + } + + fn get_xdg_surface(&self, s: &wlc::Main) -> wlc::Main { + match self.inner.upgrade() { + None => panic!("unable to acquire underyling compositor to create an xdg surface"), + Some(c) => c.get_xdg_surface(s), + } + } + + fn zxdg_decoration_manager_v1(&self) -> wlc::Main { + match self.inner.upgrade() { + None => { + panic!("unable to acquire underyling compositor to acquire the decoration manager") + } + Some(c) => c.zxdg_decoration_manager_v1(), + } + } + + fn zwlr_layershell_v1(&self) -> wlc::Main { + match self.inner.upgrade() { + None => { + panic!("unable to acquire underyling compositor to acquire the layershell manager") + } + Some(c) => c.zwlr_layershell_v1(), + } + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/popup.rs b/druid-shell/src/backend/wayland/surfaces/popup.rs new file mode 100644 index 0000000000..05090af61e --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/popup.rs @@ -0,0 +1,270 @@ +use wayland_client as wlc; +use wayland_protocols::xdg_shell::client::xdg_popup; +use wayland_protocols::xdg_shell::client::xdg_positioner; +use wayland_protocols::xdg_shell::client::xdg_surface; + +use crate::kurbo; +use crate::window; + +use super::error; +use super::surface; +use super::Compositor; +use super::CompositorHandle; +use super::Handle; +use super::Outputs; +use super::Popup; + +#[allow(unused)] +struct Inner { + wl_surface: surface::Surface, + wl_xdg_surface: wlc::Main, + wl_xdg_popup: wlc::Main, + wl_xdg_pos: wlc::Main, +} + +impl From for std::sync::Arc { + fn from(s: Inner) -> std::sync::Arc { + std::sync::Arc::::from(s.wl_surface) + } +} + +#[derive(Clone, Debug)] +pub struct Config { + pub size: kurbo::Size, + pub offset: kurbo::Point, + pub anchor_rect: (kurbo::Point, kurbo::Size), + pub anchor: xdg_positioner::Anchor, + pub gravity: xdg_positioner::Gravity, + pub constraint_adjustment: xdg_positioner::ConstraintAdjustment, +} + +impl Config { + fn apply(self, c: &CompositorHandle) -> wlc::Main { + tracing::debug!("configuring popup {:?}", self); + let pos = c.get_xdg_positioner(); + + pos.set_size(self.size.width as i32, self.size.height as i32); + pos.set_offset(self.offset.x as i32, self.offset.y as i32); + pos.set_anchor_rect( + self.anchor_rect.0.x as i32, + self.anchor_rect.0.y as i32, + self.anchor_rect.1.width as i32, + self.anchor_rect.1.height as i32, + ); + pos.set_anchor(self.anchor); + pos.set_gravity(self.gravity); + pos.set_constraint_adjustment(self.constraint_adjustment.bits()); + // requires version 3... + // pos.set_reactive(); + + pos + } + + pub fn with_size(mut self, dim: kurbo::Size) -> Self { + self.size = dim; + self + } + + pub fn with_offset(mut self, p: kurbo::Point) -> Self { + self.offset = p; + self + } +} + +impl Default for Config { + fn default() -> Self { + Self { + size: kurbo::Size::new(1., 1.), + anchor: xdg_positioner::Anchor::Bottom, + offset: kurbo::Point::ZERO, + anchor_rect: (kurbo::Point::ZERO, kurbo::Size::from((1., 1.))), + gravity: xdg_positioner::Gravity::BottomLeft, + constraint_adjustment: xdg_positioner::ConstraintAdjustment::all(), + } + } +} + +#[derive(Clone)] +pub struct Surface { + inner: std::sync::Arc, +} + +impl Surface { + pub fn new( + c: impl Into, + handler: Box, + config: Config, + parent: &dyn Popup, + ) -> Result { + let compositor = CompositorHandle::new(c); + let wl_surface = surface::Surface::new(compositor.clone(), handler, kurbo::Size::ZERO); + let wl_xdg_surface = compositor.get_xdg_surface(&wl_surface.inner.wl_surface.borrow()); + + // register to receive xdg_surface events. + wl_xdg_surface.quick_assign({ + let wl_surface = wl_surface.clone(); + move |xdg_surface, event, _| match event { + xdg_surface::Event::Configure { serial } => { + xdg_surface.ack_configure(serial); + let dim = wl_surface.inner.logical_size.get(); + wl_surface.inner.handler.borrow_mut().size(dim); + wl_surface.request_paint(); + } + _ => tracing::warn!("unhandled xdg_surface event {:?}", event), + } + }); + + let wl_xdg_pos = config.apply(&compositor); + wl_xdg_pos.quick_assign(|obj, event, _| { + tracing::debug!("{:?} {:?}", obj, event); + }); + + let wl_xdg_popup = match parent.surface(&wl_xdg_surface, &wl_xdg_pos) { + Ok(p) => p, + Err(cause) => return Err(cause), + }; + wl_xdg_popup.quick_assign({ + let wl_surface = wl_surface.clone(); + move |_xdg_popup, event, _| { + match event { + xdg_popup::Event::Configure { + x, + y, + width, + height, + } => { + tracing::debug!( + "popup configuration ({:?},{:?}) {:?}x{:?}", + x, + y, + width, + height + ); + wl_surface.update_dimensions((width as f64, height as f64)); + } + xdg_popup::Event::PopupDone => { + tracing::debug!("popup done {:?}", event); + match wl_surface.data() { + None => tracing::warn!("missing surface data, cannot close popup"), + Some(data) => { + data.with_handler(|winhandle| { + winhandle.request_close(); + }); + } + }; + } + _ => tracing::warn!("unhandled xdg_popup event configure {:?}", event), + }; + } + }); + + let handle = Self { + inner: std::sync::Arc::new(Inner { + wl_surface, + wl_xdg_surface, + wl_xdg_popup, + wl_xdg_pos, + }), + }; + + handle.commit(); + Ok(handle) + } + + pub(super) fn commit(&self) { + let wl_surface = &self.inner.wl_surface; + wl_surface.commit(); + } + + pub(crate) fn with_handler T>( + &self, + f: F, + ) -> Option { + std::sync::Arc::::from(self).with_handler(f) + } +} + +impl Handle for Surface { + fn get_size(&self) -> kurbo::Size { + self.inner.wl_surface.get_size() + } + + fn set_size(&self, dim: kurbo::Size) { + self.inner.wl_surface.set_size(dim); + } + + fn request_anim_frame(&self) { + self.inner.wl_surface.request_anim_frame() + } + + fn invalidate(&self) { + self.inner.wl_surface.invalidate() + } + + fn invalidate_rect(&self, rect: kurbo::Rect) { + self.inner.wl_surface.invalidate_rect(rect) + } + + fn remove_text_field(&self, token: crate::TextFieldToken) { + self.inner.wl_surface.remove_text_field(token) + } + + fn set_focused_text_field(&self, active_field: Option) { + self.inner.wl_surface.set_focused_text_field(active_field) + } + + fn get_idle_handle(&self) -> super::idle::Handle { + self.inner.wl_surface.get_idle_handle() + } + + fn get_scale(&self) -> crate::Scale { + self.inner.wl_surface.get_scale() + } + + fn run_idle(&self) { + self.inner.wl_surface.run_idle(); + } + + fn release(&self) { + self.inner.wl_surface.release() + } + + fn data(&self) -> Option> { + self.inner.wl_surface.data() + } +} + +impl From<&Surface> for std::sync::Arc { + fn from(s: &Surface) -> std::sync::Arc { + std::sync::Arc::::from(s.inner.wl_surface.clone()) + } +} + +impl Popup for Surface { + fn surface<'a>( + &self, + popup: &'a wlc::Main, + pos: &'a wlc::Main, + ) -> Result, error::Error> + { + Ok(popup.get_popup(Some(&self.inner.wl_xdg_surface), pos)) + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s.inner.wl_surface.clone()) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Self { + Box::new(s) as Box + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/surface.rs b/druid-shell/src/backend/wayland/surfaces/surface.rs new file mode 100644 index 0000000000..48ae8a1f3f --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/surface.rs @@ -0,0 +1,629 @@ +use std::cell::{Cell, RefCell}; +use std::rc::Rc; +use wayland_client as wlc; +use wayland_client::protocol::wl_surface; +use wayland_protocols::xdg_shell::client::xdg_popup; +use wayland_protocols::xdg_shell::client::xdg_positioner; +use wayland_protocols::xdg_shell::client::xdg_surface; + +use crate::kurbo; +use crate::window; +use crate::{piet::Piet, region::Region, scale::Scale, TextFieldToken}; + +use super::super::Changed; + +use super::super::outputs; +use super::buffers; +use super::error; +use super::idle; +use super::Popup; +use super::{Compositor, CompositorHandle, Decor, Handle, Outputs}; + +pub enum DeferredTask { + Paint, + AnimationClear, +} + +#[derive(Clone)] +pub struct Surface { + pub(super) inner: std::sync::Arc, +} + +impl From> for Surface { + fn from(d: std::sync::Arc) -> Self { + Self { inner: d } + } +} + +impl Surface { + pub fn new( + c: impl Into, + handler: Box, + initial_size: kurbo::Size, + ) -> Self { + let compositor = CompositorHandle::new(c); + let wl_surface = match compositor.create_surface() { + None => panic!("unable to create surface"), + Some(v) => v, + }; + + let current = std::sync::Arc::new(Data { + compositor: compositor.clone(), + wl_surface: RefCell::new(wl_surface), + outputs: RefCell::new(std::collections::HashSet::new()), + buffers: buffers::Buffers::new(compositor.shared_mem(), initial_size.into()), + logical_size: Cell::new(initial_size), + scale: Cell::new(1), + anim_frame_requested: Cell::new(false), + handler: RefCell::new(handler), + idle_queue: std::sync::Arc::new(std::sync::Mutex::new(vec![])), + active_text_input: Cell::new(None), + damaged_region: RefCell::new(Region::EMPTY), + deferred_tasks: RefCell::new(std::collections::VecDeque::new()), + }); + + // register to receive wl_surface events. + Surface::initsurface(¤t); + + Self { inner: current } + } + + pub(super) fn request_paint(&self) { + self.inner.buffers.request_paint(&self.inner); + } + + pub(super) fn update_dimensions(&self, dim: impl Into) -> kurbo::Size { + self.inner.update_dimensions(dim) + } + + pub(super) fn resize(&self, dim: kurbo::Size) -> kurbo::Size { + self.inner.resize(dim) + } + + pub(super) fn commit(&self) { + self.inner.wl_surface.borrow().commit() + } + + pub(super) fn replace(current: &std::sync::Arc) -> Surface { + current + .wl_surface + .replace(match current.compositor.create_surface() { + None => panic!("unable to create surface"), + Some(v) => v, + }); + Surface::initsurface(current); + Self { + inner: current.clone(), + } + } + + fn initsurface(current: &std::sync::Arc) { + current.wl_surface.borrow().quick_assign({ + let current = current.clone(); + move |a, event, b| { + tracing::debug!("wl_surface event {:?} {:?} {:?}", a, event, b); + Surface::consume_surface_event(¤t, &a, &event, &b); + } + }); + } + + pub(super) fn consume_surface_event( + current: &std::sync::Arc, + surface: &wlc::Main, + event: &wlc::protocol::wl_surface::Event, + data: &wlc::DispatchData, + ) { + tracing::debug!("wl_surface event {:?} {:?} {:?}", surface, event, data); + match event { + wl_surface::Event::Enter { output } => { + let proxy = wlc::Proxy::from(output.clone()); + current.outputs.borrow_mut().insert(proxy.id()); + } + wl_surface::Event::Leave { output } => { + let proxy = wlc::Proxy::from(output.clone()); + current.outputs.borrow_mut().remove(&proxy.id()); + } + _ => tracing::warn!("unhandled wayland surface event {:?}", event), + } + + let new_scale = current.recompute_scale(); + if current.set_scale(new_scale).is_changed() { + current.wl_surface.borrow().set_buffer_scale(new_scale); + // We also need to change the physical size to match the new scale + current + .buffers + .set_size(buffers::RawSize::from(current.logical_size.get()).scale(new_scale)); + // always repaint, because the scale changed. + current.schedule_deferred_task(DeferredTask::Paint); + } + } +} + +impl Outputs for Surface { + fn removed(&self, o: &outputs::Meta) { + self.inner.outputs.borrow_mut().remove(&o.id()); + } + + fn inserted(&self, _: &outputs::Meta) { + // nothing to do here. + } +} + +impl Handle for Surface { + fn get_size(&self) -> kurbo::Size { + self.inner.get_size() + } + + fn set_size(&self, dim: kurbo::Size) { + self.inner.resize(dim); + } + + fn request_anim_frame(&self) { + self.inner.request_anim_frame() + } + + fn remove_text_field(&self, token: TextFieldToken) { + self.inner.remove_text_field(token) + } + + fn set_focused_text_field(&self, active_field: Option) { + self.inner.set_focused_text_field(active_field) + } + + fn get_idle_handle(&self) -> idle::Handle { + self.inner.get_idle_handle() + } + + fn get_scale(&self) -> Scale { + self.inner.get_scale() + } + + fn invalidate(&self) { + self.inner.invalidate() + } + + fn invalidate_rect(&self, rect: kurbo::Rect) { + self.inner.invalidate_rect(rect) + } + + fn run_idle(&self) { + self.inner.run_idle(); + } + + fn release(&self) { + self.inner.release() + } + + fn data(&self) -> Option> { + Some(Into::into(self)) + } +} + +impl From for std::sync::Arc { + fn from(s: Surface) -> std::sync::Arc { + s.inner + } +} + +impl From<&Surface> for std::sync::Arc { + fn from(s: &Surface) -> std::sync::Arc { + s.inner.clone() + } +} + +pub struct Data { + pub(super) compositor: CompositorHandle, + pub(super) wl_surface: RefCell>, + + /// The outputs that our surface is present on (we should get the first enter event early). + pub(super) outputs: RefCell>, + + /// Buffers in our shared memory. + // Buffers sometimes need to move references to themselves into closures, so must be behind a + // reference counter. + pub(super) buffers: Rc>, + /// The logical size of the next frame. + pub(crate) logical_size: Cell, + /// The scale we are rendering to (defaults to 1) + pub(crate) scale: Cell, + + /// Contains the callbacks from user code. + pub(crate) handler: RefCell>, + pub(crate) active_text_input: Cell>, + + /// Whether we have requested an animation frame. This stops us requesting more than 1. + anim_frame_requested: Cell, + /// Rects of the image that are damaged and need repainting in the logical coordinate space. + /// + /// This lives outside `data` because they can be borrowed concurrently without re-entrancy. + damaged_region: RefCell, + /// Tasks that were requested in user code. + /// + /// These call back into user code, and so should only be run after all user code has returned, + /// to avoid possible re-entrancy. + deferred_tasks: RefCell>, + + idle_queue: std::sync::Arc>>, +} + +impl Data { + #[track_caller] + pub(crate) fn with_handler T>( + &self, + f: F, + ) -> Option { + let ret = self.with_handler_and_dont_check_the_other_borrows(f); + self.run_deferred_tasks(); + ret + } + + #[track_caller] + fn with_handler_and_dont_check_the_other_borrows< + T, + F: FnOnce(&mut dyn window::WinHandler) -> T, + >( + &self, + f: F, + ) -> Option { + match self.handler.try_borrow_mut() { + Ok(mut h) => Some(f(&mut **h)), + Err(_) => { + tracing::error!( + "failed to borrow WinHandler at {}", + std::panic::Location::caller() + ); + None + } + } + } + + pub(super) fn update_dimensions(&self, dim: impl Into) -> kurbo::Size { + let dim = dim.into(); + if self.logical_size.get() != self.resize(dim) { + match self.handler.try_borrow_mut() { + Ok(mut handler) => handler.size(dim), + Err(cause) => tracing::warn!("unhable to borrow handler {:?}", cause), + }; + } + + dim + } + + // client initiated resizing. + pub(super) fn resize(&self, dim: kurbo::Size) -> kurbo::Size { + // The size here is the logical size + let scale = self.scale.get(); + let raw_logical_size = buffers::RawSize { + width: dim.width as i32, + height: dim.height as i32, + }; + let previous_logical_size = self.logical_size.replace(dim); + if previous_logical_size != dim { + self.buffers.set_size(raw_logical_size.scale(scale)); + } + + dim + } + + /// Assert that the physical size = logical size * scale + #[allow(unused)] + fn assert_size(&self) { + assert_eq!( + self.buffers.size(), + buffers::RawSize::from(self.logical_size.get()).scale(self.scale.get()), + "phy {:?} == logic {:?} * {}", + self.buffers.size(), + self.logical_size.get(), + self.scale.get() + ); + } + + /// Recompute the scale to use (the maximum of all the scales for the different outputs this + /// surface is drawn to). + fn recompute_scale(&self) -> i32 { + tracing::debug!("recompute initiated"); + self.compositor.recompute_scale(&self.outputs.borrow()) + } + + /// Sets the scale + /// + /// Up to the caller to make sure `physical_size`, `logical_size` and `scale` are consistent. + fn set_scale(&self, new_scale: i32) -> Changed { + tracing::debug!("set_scale initiated"); + if self.scale.get() != new_scale { + self.scale.set(new_scale); + // (re-entrancy) Report change to client + self.handler + .borrow_mut() + .scale(Scale::new(new_scale as f64, new_scale as f64)); + Changed::Changed + } else { + Changed::Unchanged + } + } + + /// Paint the next frame. + /// + /// The buffers object is responsible for calling this function after we called + /// `request_paint`. + /// + /// - `buf` is what we draw the frame into + /// - `size` is the physical size in pixels we are drawing. + /// - `force` means draw the whole frame, even if it wasn't all invalidated. + pub(super) fn paint(&self, physical_size: buffers::RawSize, buf: &mut [u8], force: bool) { + tracing::trace!( + "paint initiated {:?} - {:?} {:?}", + self.get_size(), + physical_size, + force + ); + + if force { + self.invalidate(); + } else { + let damaged_region = self.damaged_region.borrow_mut(); + for rect in damaged_region.rects() { + // Convert it to physical coordinate space. + let rect = buffers::RawRect::from(*rect).scale(self.scale.get()); + self.wl_surface.borrow().damage_buffer( + rect.x0, + rect.y0, + rect.x1 - rect.x0, + rect.y1 - rect.y0, + ); + } + if damaged_region.is_empty() { + // Nothing to draw, so we can finish here! + return; + } + } + + // create cairo context (safety: we must drop the buffer before we commit the frame) + // TODO: Cairo is native-endian while wayland is little-endian, which is a pain. Currently + // will give incorrect results on big-endian architectures. + // TODO cairo might use a different stride than the width of the format. Since we always + // use argb32 which is 32-bit aligned we should be ok, but strictly speaking cairo might + // choose a wider stride and read past the end of our buffer (UB). Fixing this would + // require a fair bit of effort. + unsafe { + // We're going to lie about the lifetime of our buffer here. This is (I think) ok, + // becuase the Rust wrapper for cairo is overly pessimistic: the buffer only has to + // last as long as the `ImageSurface` (which we know this buffer will). + let buf: &'static mut [u8] = &mut *(buf as *mut _); + let cairo_surface = match cairo::ImageSurface::create_for_data( + buf, + cairo::Format::ARgb32, + physical_size.width, + physical_size.height, + physical_size.width * buffers::PIXEL_WIDTH, + ) { + Ok(s) => s, + Err(cause) => { + tracing::error!("unable to create cairo surface: {:?}", cause); + return; + } + }; + let ctx = match cairo::Context::new(&cairo_surface) { + Ok(ctx) => ctx, + Err(cause) => { + tracing::error!("unable to create cairo context: {:?}", cause); + return; + } + }; + // Apply scaling + let scale = self.scale.get() as f64; + ctx.scale(scale, scale); + + let mut piet = Piet::new(&ctx); + // Actually paint the new frame + let region = self.damaged_region.borrow(); + + // The handler must not be already borrowed. This may mean deferring this call. + self.handler.borrow_mut().paint(&mut piet, &*region); + } + + // reset damage ready for next frame. + self.damaged_region.borrow_mut().clear(); + self.buffers.attach(self); + self.wl_surface.borrow().commit(); + } + + /// Request invalidation of the entire window contents. + fn invalidate(&self) { + tracing::trace!("invalidate initiated"); + // This is one of 2 methods the user can use to schedule a repaint, the other is + // `invalidate_rect`. + let window_rect = self.logical_size.get().to_rect(); + self.damaged_region.borrow_mut().add_rect(window_rect); + self.schedule_deferred_task(DeferredTask::Paint); + } + + /// Request invalidation of one rectangle, which is given in display points relative to the + /// drawing area. + fn invalidate_rect(&self, rect: kurbo::Rect) { + tracing::trace!("invalidate_rect initiated {:?}", rect); + // Quick check to see if we can skip the rect entirely (if it is outside the visible + // screen). + if rect.intersect(self.logical_size.get().to_rect()).is_empty() { + return; + } + /* this would be useful for debugging over-keen invalidation by clients. + println!( + "{:?} {:?}", + rect, + self.with_data(|data| data.logical_size.to_rect()) + ); + */ + self.damaged_region.borrow_mut().add_rect(rect); + self.schedule_deferred_task(DeferredTask::Paint); + } + + pub fn schedule_deferred_task(&self, task: DeferredTask) { + tracing::trace!("scedule_deferred_task initiated"); + self.deferred_tasks.borrow_mut().push_back(task); + } + + pub fn run_deferred_tasks(&self) { + tracing::trace!("run_deferred_tasks initiated"); + while let Some(task) = self.next_deferred_task() { + self.run_deferred_task(task); + } + } + + fn next_deferred_task(&self) -> Option { + self.deferred_tasks.borrow_mut().pop_front() + } + + fn run_deferred_task(&self, task: DeferredTask) { + match task { + DeferredTask::Paint => { + self.buffers.request_paint(self); + } + DeferredTask::AnimationClear => { + self.anim_frame_requested.set(false); + } + } + } + + pub(super) fn get_size(&self) -> kurbo::Size { + // size in pixels, so we must apply scale. + let logical_size = self.logical_size.get(); + let scale = self.scale.get() as f64; + kurbo::Size::new( + logical_size.width as f64 * scale, + logical_size.height as f64 * scale, + ) + } + + pub(super) fn request_anim_frame(&self) { + if self.anim_frame_requested.replace(true) { + return; + } + + let idle = self.get_idle_handle(); + idle.add_idle_callback(move |winhandle| { + winhandle.prepare_paint(); + }); + self.schedule_deferred_task(DeferredTask::AnimationClear); + } + + pub(super) fn remove_text_field(&self, token: TextFieldToken) { + if self.active_text_input.get() == Some(token) { + self.active_text_input.set(None); + } + } + + pub(super) fn set_focused_text_field(&self, active_field: Option) { + self.active_text_input.set(active_field); + } + + pub(super) fn get_idle_handle(&self) -> idle::Handle { + idle::Handle { + queue: self.idle_queue.clone(), + } + } + + pub(super) fn get_scale(&self) -> Scale { + let scale = self.scale.get() as f64; + Scale::new(scale, scale) + } + + pub(super) fn run_idle(&self) { + self.with_handler(|winhandle| { + idle::run(&self.get_idle_handle(), winhandle); + }); + } + + pub(super) fn release(&self) { + self.wl_surface.borrow().destroy(); + } +} + +#[derive(Default)] +pub struct Dead; + +impl From for Box { + fn from(d: Dead) -> Box { + Box::new(d) as Box + } +} + +impl From for Box { + fn from(d: Dead) -> Box { + Box::new(d) as Box + } +} + +impl Decor for Dead { + fn inner_set_title(&self, title: String) { + tracing::warn!("set_title not implemented for this surface: {:?}", title); + } +} + +impl Outputs for Dead { + fn removed(&self, _: &outputs::Meta) {} + + fn inserted(&self, _: &outputs::Meta) {} +} + +impl Popup for Dead { + fn surface<'a>( + &self, + _: &'a wlc::Main, + _: &'a wlc::Main, + ) -> Result, error::Error> { + tracing::warn!("popup invoked on a dead surface"); + Err(error::Error::InvalidParent(0)) + } +} + +impl Handle for Dead { + fn get_size(&self) -> kurbo::Size { + kurbo::Size::ZERO + } + + fn set_size(&self, dim: kurbo::Size) { + tracing::warn!("set_size invoked on a dead surface {:?}", dim); + } + + fn request_anim_frame(&self) { + tracing::warn!("request_anim_frame invoked on a dead surface") + } + + fn remove_text_field(&self, _token: TextFieldToken) { + tracing::warn!("remove_text_field invoked on a dead surface") + } + + fn set_focused_text_field(&self, _active_field: Option) { + tracing::warn!("set_focused_text_field invoked on a dead surface") + } + + fn get_idle_handle(&self) -> idle::Handle { + panic!("get_idle_handle invoked on a dead surface") + } + + fn get_scale(&self) -> Scale { + Scale::new(1., 1.) + } + + fn invalidate(&self) { + tracing::warn!("invalidate invoked on a dead surface") + } + + fn invalidate_rect(&self, _rect: kurbo::Rect) { + tracing::warn!("invalidate_rect invoked on a dead surface") + } + + fn run_idle(&self) { + tracing::warn!("run_idle invoked on a dead surface") + } + + fn release(&self) { + tracing::warn!("release invoked on a dead surface"); + } + + fn data(&self) -> Option> { + tracing::warn!("data invoked on a dead surface"); + None + } +} diff --git a/druid-shell/src/backend/wayland/surfaces/toplevel.rs b/druid-shell/src/backend/wayland/surfaces/toplevel.rs new file mode 100644 index 0000000000..c1ccd77781 --- /dev/null +++ b/druid-shell/src/backend/wayland/surfaces/toplevel.rs @@ -0,0 +1,191 @@ +use wayland_client as wlc; +use wayland_protocols::unstable::xdg_decoration::v1::client::zxdg_toplevel_decoration_v1 as toplevel_decorations; +use wayland_protocols::xdg_shell::client::xdg_surface; +use wayland_protocols::xdg_shell::client::xdg_toplevel; + +use crate::kurbo; +use crate::window; + +use super::error; +use super::surface; +use super::Compositor; +use super::CompositorHandle; +use super::Decor; +use super::Handle; +use super::Outputs; +use super::Popup; + +struct Inner { + wl_surface: surface::Surface, + #[allow(unused)] + pub(super) xdg_surface: wlc::Main, + pub(super) xdg_toplevel: wlc::Main, + #[allow(unused)] + pub(super) zxdg_toplevel_decoration_v1: + wlc::Main, +} + +impl From for std::sync::Arc { + fn from(s: Inner) -> std::sync::Arc { + std::sync::Arc::::from(s.wl_surface) + } +} + +#[derive(Clone)] +pub struct Surface { + inner: std::sync::Arc, +} + +impl Surface { + pub fn new( + c: impl Into, + handler: Box, + initial_size: kurbo::Size, + min_size: Option, + ) -> Self { + let compositor = CompositorHandle::new(c); + let wl_surface = surface::Surface::new(compositor.clone(), handler, kurbo::Size::ZERO); + let xdg_surface = compositor.get_xdg_surface(&wl_surface.inner.wl_surface.borrow()); + let xdg_toplevel = xdg_surface.get_toplevel(); + let zxdg_toplevel_decoration_v1 = compositor + .zxdg_decoration_manager_v1() + .get_toplevel_decoration(&xdg_toplevel); + + // register to receive xdg_surface events. + xdg_surface.quick_assign({ + let wl_surface = wl_surface.clone(); + move |xdg_surface, event, _| { + tracing::trace!("xdg_surface event configure {:?}", event); + match event { + xdg_surface::Event::Configure { serial } => { + xdg_surface.ack_configure(serial); + wl_surface.resize(wl_surface.get_size()); + wl_surface.request_paint(); + } + _ => tracing::warn!("unhandled xdg_surface event {:?}", event), + } + } + }); + xdg_toplevel.quick_assign({ + let wl_surface = wl_surface.clone(); + let mut dim = initial_size; + move |_xdg_toplevel, event, a3| match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => { + tracing::trace!( + "configure event {:?} {:?} {:?} {:?}", + width, + height, + states, + a3 + ); + // compositor is deferring to the client for determining the size + // when values are zero. + if width != 0 && height != 0 { + dim = kurbo::Size::new(width as f64, height as f64); + } + wl_surface.update_dimensions(dim); + } + xdg_toplevel::Event::Close => { + tracing::info!("xdg close event {:?}", event); + wl_surface.inner.handler.borrow_mut().request_close(); + } + _ => tracing::info!("unimplemented event {:?}", event), + } + }); + + zxdg_toplevel_decoration_v1.quick_assign(move |_zxdg_toplevel_decoration_v1, event, _| { + tracing::info!("toplevel decoration unimplemented {:?}", event); + }); + + let inner = Inner { + wl_surface, + xdg_toplevel, + xdg_surface, + zxdg_toplevel_decoration_v1, + }; + + inner + .zxdg_toplevel_decoration_v1 + .set_mode(toplevel_decorations::Mode::ServerSide); + if let Some(size) = min_size { + inner + .xdg_toplevel + .set_min_size(size.width as i32, size.height as i32); + } + + let handle = Self { + inner: std::sync::Arc::new(inner), + }; + + handle.commit(); + handle + } + + pub(crate) fn with_handler T>( + &self, + f: F, + ) -> Option { + std::sync::Arc::::from(self).with_handler(f) + } + + pub(crate) fn commit(&self) { + self.inner.wl_surface.commit(); + } +} + +impl Popup for Surface { + fn surface<'a>( + &self, + popup: &'a wlc::Main, + pos: &'a wlc::Main, + ) -> Result, error::Error> + { + Ok(popup.get_popup(Some(&self.inner.xdg_surface), pos)) + } +} + +impl Decor for Surface { + fn inner_set_title(&self, title: String) { + self.inner.xdg_toplevel.set_title(title); + } +} + +impl From<&Surface> for std::sync::Arc { + fn from(s: &Surface) -> std::sync::Arc { + std::sync::Arc::::from(s.inner.wl_surface.clone()) + } +} + +impl From for std::sync::Arc { + fn from(s: Surface) -> std::sync::Arc { + std::sync::Arc::::from(s.inner.wl_surface.clone()) + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s.inner.wl_surface.clone()) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Box { + Box::new(s.inner.wl_surface.clone()) as Box + } +} + +impl From for Box { + fn from(s: Surface) -> Self { + Box::new(s) as Box + } +} diff --git a/druid-shell/src/backend/wayland/window.rs b/druid-shell/src/backend/wayland/window.rs new file mode 100644 index 0000000000..26946a2b65 --- /dev/null +++ b/druid-shell/src/backend/wayland/window.rs @@ -0,0 +1,613 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(clippy::single_match)] + +use tracing; +use wayland_protocols::xdg_shell::client::xdg_popup; +use wayland_protocols::xdg_shell::client::xdg_positioner; +use wayland_protocols::xdg_shell::client::xdg_surface; + +use super::{ + application::{Application, ApplicationData, Timer}, + error::Error, + menu::Menu, + outputs, surfaces, +}; + +use crate::{ + dialog::FileDialogOptions, + error::Error as ShellError, + kurbo::{Insets, Point, Rect, Size}, + mouse::{Cursor, CursorDesc}, + piet::PietText, + scale::Scale, + text::Event, + window::{self, FileDialogToken, TimerToken, WinHandler, WindowLevel}, + TextFieldToken, +}; + +pub use surfaces::idle::Handle as IdleHandle; + +// holds references to the various components for a window implementation. +struct Inner { + pub(super) id: u64, + pub(super) decor: Box, + pub(super) surface: Box, + pub(super) outputs: Box, + pub(super) popup: Box, + pub(super) appdata: std::sync::Weak, +} + +#[derive(Clone)] +pub struct WindowHandle { + inner: std::sync::Arc, +} + +impl surfaces::Outputs for WindowHandle { + fn removed(&self, o: &outputs::Meta) { + self.inner.outputs.removed(o) + } + + fn inserted(&self, o: &outputs::Meta) { + self.inner.outputs.inserted(o) + } +} + +impl surfaces::Popup for WindowHandle { + fn surface( + &self, + popup: &wayland_client::Main, + pos: &wayland_client::Main, + ) -> Result, Error> { + self.inner.popup.surface(popup, pos) + } +} + +impl WindowHandle { + pub(super) fn new( + outputs: impl Into>, + decor: impl Into>, + surface: impl Into>, + popup: impl Into>, + appdata: impl Into>, + ) -> Self { + Self { + inner: std::sync::Arc::new(Inner { + id: surfaces::GLOBAL_ID.next(), + outputs: outputs.into(), + decor: decor.into(), + surface: surface.into(), + popup: popup.into(), + appdata: appdata.into(), + }), + } + } + + pub fn id(&self) -> u64 { + self.inner.id + } + + pub fn show(&self) { + tracing::debug!("show initiated"); + } + + pub fn resizable(&self, _resizable: bool) { + tracing::warn!("resizable is unimplemented on wayland"); + } + + pub fn show_titlebar(&self, _show_titlebar: bool) { + tracing::warn!("show_titlebar is unimplemented on wayland"); + } + + pub fn set_position(&self, _position: Point) { + tracing::warn!("set_position is unimplemented on wayland"); + } + + pub fn get_position(&self) -> Point { + tracing::warn!("get_position is unimplemented on wayland"); + Point::ZERO + } + + pub fn content_insets(&self) -> Insets { + Insets::from(0.) + } + + pub fn set_size(&self, size: Size) { + self.inner.surface.set_size(size); + } + + pub fn get_size(&self) -> Size { + self.inner.surface.get_size() + } + + pub fn set_window_state(&mut self, _current_state: window::WindowState) { + tracing::warn!("set_window_state is unimplemented on wayland"); + } + + pub fn get_window_state(&self) -> window::WindowState { + tracing::warn!("get_window_state is unimplemented on wayland"); + window::WindowState::Maximized + } + + pub fn handle_titlebar(&self, _val: bool) { + tracing::warn!("handle_titlebar is unimplemented on wayland"); + } + + /// Close the window. + pub fn close(&self) { + if let Some(appdata) = self.inner.appdata.upgrade() { + tracing::trace!( + "closing window initiated {:?}", + appdata.active_surface_id.borrow() + ); + appdata.handles.borrow_mut().remove(&self.id()); + appdata.active_surface_id.borrow_mut().pop_front(); + self.inner.surface.release(); + tracing::trace!( + "closing window completed {:?}", + appdata.active_surface_id.borrow() + ); + } + } + + /// Bring this window to the front of the window stack and give it focus. + pub fn bring_to_front_and_focus(&self) { + tracing::warn!("unimplemented bring_to_front_and_focus initiated"); + } + + /// Request a new paint, but without invalidating anything. + pub fn request_anim_frame(&self) { + self.inner.surface.request_anim_frame(); + } + + /// Request invalidation of the entire window contents. + pub fn invalidate(&self) { + self.inner.surface.invalidate(); + } + + /// Request invalidation of one rectangle, which is given in display points relative to the + /// drawing area. + pub fn invalidate_rect(&self, rect: Rect) { + self.inner.surface.invalidate_rect(rect); + } + + pub fn text(&self) -> PietText { + PietText::new() + } + + pub fn add_text_field(&self) -> TextFieldToken { + TextFieldToken::next() + } + + pub fn remove_text_field(&self, token: TextFieldToken) { + self.inner.surface.remove_text_field(token); + } + + pub fn set_focused_text_field(&self, active_field: Option) { + self.inner.surface.set_focused_text_field(active_field); + } + + pub fn update_text_field(&self, _token: TextFieldToken, _update: Event) { + // noop until we get a real text input implementation + } + + pub fn request_timer(&self, deadline: std::time::Instant) -> TimerToken { + let appdata = match self.inner.appdata.upgrade() { + Some(d) => d, + None => { + tracing::warn!("requested timer on a window that was destroyed"); + return Timer::new(self.id(), deadline).token(); + } + }; + + let now = instant::Instant::now(); + let mut timers = appdata.timers.borrow_mut(); + let sooner = timers + .peek() + .map(|timer| deadline < timer.deadline()) + .unwrap_or(true); + + let timer = Timer::new(self.id(), deadline); + timers.push(timer); + + // It is possible that the deadline has passed since it was set. + let timeout = if deadline < now { + std::time::Duration::ZERO + } else { + deadline - now + }; + + if sooner { + appdata.timer_handle.cancel_all_timeouts(); + appdata.timer_handle.add_timeout(timeout, timer.token()); + } + + timer.token() + } + + pub fn set_cursor(&mut self, cursor: &Cursor) { + if let Some(appdata) = self.inner.appdata.upgrade() { + appdata.set_cursor(cursor); + } + } + + pub fn make_cursor(&self, _desc: &CursorDesc) -> Option { + tracing::warn!("unimplemented make_cursor initiated"); + None + } + + pub fn open_file(&mut self, _options: FileDialogOptions) -> Option { + tracing::warn!("unimplemented open_file"); + None + } + + pub fn save_as(&mut self, _options: FileDialogOptions) -> Option { + tracing::warn!("unimplemented save_as"); + None + } + + /// Get a handle that can be used to schedule an idle task. + pub fn get_idle_handle(&self) -> Option { + Some(self.inner.surface.get_idle_handle()) + } + + /// Get the `Scale` of the window. + pub fn get_scale(&self) -> Result { + Ok(self.inner.surface.get_scale()) + } + + pub fn set_menu(&self, _menu: Menu) { + tracing::warn!("set_menu not implement for wayland"); + } + + pub fn show_context_menu(&self, _menu: Menu, _pos: Point) { + tracing::warn!("show_context_menu not implement for wayland"); + } + + pub fn set_title(&self, title: impl Into) { + self.inner.decor.set_title(title); + } + + pub(super) fn run_idle(&self) { + self.inner.surface.run_idle(); + } + + pub(super) fn data(&self) -> Option> { + self.inner.surface.data() + } +} + +impl std::cmp::PartialEq for WindowHandle { + fn eq(&self, rhs: &Self) -> bool { + self.id() == rhs.id() + } +} + +impl std::default::Default for WindowHandle { + fn default() -> WindowHandle { + WindowHandle { + inner: std::sync::Arc::new(Inner { + id: surfaces::GLOBAL_ID.next(), + outputs: Box::new(surfaces::surface::Dead::default()), + decor: Box::new(surfaces::surface::Dead::default()), + surface: Box::new(surfaces::surface::Dead::default()), + popup: Box::new(surfaces::surface::Dead::default()), + appdata: std::sync::Weak::new(), + }), + } + } +} + +#[derive(Clone, PartialEq)] +pub struct CustomCursor; + +/// Builder abstraction for creating new windows +pub(crate) struct WindowBuilder { + appdata: std::sync::Weak, + handler: Option>, + title: String, + menu: Option, + position: Option, + level: WindowLevel, + state: Option, + // pre-scaled + size: Size, + min_size: Option, + resizable: bool, + show_titlebar: bool, +} + +impl WindowBuilder { + pub fn new(app: Application) -> WindowBuilder { + WindowBuilder { + appdata: std::sync::Arc::downgrade(&app.data), + handler: None, + title: String::new(), + menu: None, + size: Size::new(0.0, 0.0), + position: None, + level: WindowLevel::AppWindow, + state: None, + min_size: None, + resizable: true, + show_titlebar: true, + } + } + + pub fn set_handler(&mut self, handler: Box) { + self.handler = Some(handler); + } + + pub fn set_size(&mut self, size: Size) { + self.size = size; + } + + pub fn set_min_size(&mut self, size: Size) { + self.min_size = Some(size); + } + + pub fn resizable(&mut self, resizable: bool) { + self.resizable = resizable; + } + + pub fn show_titlebar(&mut self, show_titlebar: bool) { + self.show_titlebar = show_titlebar; + } + + pub fn set_transparent(&mut self, _transparent: bool) { + tracing::warn!( + "set_transparent unimplemented for wayland, it allows transparency by default" + ); + } + + pub fn set_position(&mut self, position: Point) { + self.position = Some(position); + } + + pub fn set_level(&mut self, level: WindowLevel) { + self.level = level; + } + + pub fn set_window_state(&mut self, state: window::WindowState) { + self.state = Some(state); + } + + pub fn set_title(&mut self, title: impl Into) { + self.title = title.into(); + } + + pub fn set_menu(&mut self, menu: Menu) { + self.menu = Some(menu); + } + + pub fn build(self) -> Result { + if matches!(self.menu, Some(_)) { + tracing::warn!("menus unimplemented for wayland"); + } + + if let WindowLevel::DropDown(parent) = self.level { + let dim = self.min_size.unwrap_or(Size::ZERO); + let dim = Size::new(dim.width.max(1.), dim.height.max(1.)); + let dim = Size::new( + self.size.width.max(dim.width), + self.size.height.max(dim.height), + ); + + let config = surfaces::popup::Config::default() + .with_size(dim) + .with_offset(Into::into( + self.position.unwrap_or_else(|| Into::into((0., 0.))), + )); + + tracing::debug!("popup {:?}", config); + + return popup::create(&parent.0, &config, self.appdata, self.handler); + } + + let appdata = match self.appdata.upgrade() { + Some(d) => d, + None => return Err(ShellError::ApplicationDropped), + }; + + let handler = self.handler.expect("must set a window handler"); + // compute the initial window size. + let initial_size = appdata.initial_window_size(self.size); + let surface = + surfaces::toplevel::Surface::new(appdata.clone(), handler, initial_size, self.min_size); + + (&surface as &dyn surfaces::Decor).set_title(self.title); + + let handle = WindowHandle::new( + surface.clone(), + surface.clone(), + surface.clone(), + surface.clone(), + self.appdata.clone(), + ); + + if appdata + .handles + .borrow_mut() + .insert(handle.id(), handle.clone()) + .is_some() + { + return Err(ShellError::Platform(Error::string( + "wayland should use a unique id", + ))); + } + + appdata + .active_surface_id + .borrow_mut() + .push_front(handle.id()); + + surface.with_handler({ + let handle = handle.clone(); + move |winhandle| winhandle.connect(&handle.into()) + }); + + Ok(handle) + } +} + +#[allow(unused)] +pub mod layershell { + use crate::error::Error as ShellError; + use crate::window::WinHandler; + + use super::WindowHandle; + use crate::backend::wayland::application::{Application, ApplicationData}; + use crate::backend::wayland::error::Error; + use crate::backend::wayland::surfaces; + + /// Builder abstraction for creating new windows + pub(crate) struct Builder { + appdata: std::sync::Weak, + winhandle: Option>, + pub(crate) config: surfaces::layershell::Config, + } + + impl Builder { + pub fn new(app: Application) -> Builder { + Builder { + appdata: std::sync::Arc::downgrade(&app.data), + config: surfaces::layershell::Config::default(), + winhandle: None, + } + } + + pub fn set_handler(&mut self, handler: Box) { + self.winhandle = Some(handler); + } + + pub fn build(self) -> Result { + let appdata = match self.appdata.upgrade() { + Some(d) => d, + None => return Err(ShellError::ApplicationDropped), + }; + + let winhandle = match self.winhandle { + Some(winhandle) => winhandle, + None => { + return Err(ShellError::Platform(Error::string( + "window handler required", + ))) + } + }; + + // compute the initial window size. + let mut updated = self.config.clone(); + updated.initial_size = appdata.initial_window_size(self.config.initial_size); + + let surface = surfaces::layershell::Surface::new(appdata.clone(), winhandle, updated); + + let handle = WindowHandle::new( + surface.clone(), + surfaces::surface::Dead::default(), + surface.clone(), + surface.clone(), + self.appdata.clone(), + ); + + if appdata + .handles + .borrow_mut() + .insert(handle.id(), handle.clone()) + .is_some() + { + panic!("wayland should use unique object IDs"); + } + appdata + .active_surface_id + .borrow_mut() + .push_front(handle.id()); + + surface.with_handler({ + let handle = handle.clone(); + move |winhandle| winhandle.connect(&handle.into()) + }); + + Ok(handle) + } + } +} + +#[allow(unused)] +pub mod popup { + use crate::error::Error as ShellError; + use crate::window::WinHandler; + + use super::WindowBuilder; + use super::WindowHandle; + use crate::backend::wayland::application::{Application, ApplicationData}; + use crate::backend::wayland::error::Error; + use crate::backend::wayland::surfaces; + + pub(super) fn create( + parent: &WindowHandle, + config: &surfaces::popup::Config, + wappdata: std::sync::Weak, + winhandle: Option>, + ) -> Result { + let appdata = match wappdata.upgrade() { + Some(d) => d, + None => return Err(ShellError::ApplicationDropped), + }; + + let winhandle = match winhandle { + Some(winhandle) => winhandle, + None => { + return Err(ShellError::Platform(Error::string( + "window handler required", + ))) + } + }; + + // compute the initial window size. + let updated = config.clone(); + let surface = + match surfaces::popup::Surface::new(appdata.clone(), winhandle, updated, parent) { + Err(cause) => return Err(ShellError::Platform(cause)), + Ok(s) => s, + }; + + let handle = WindowHandle::new( + surface.clone(), + surfaces::surface::Dead::default(), + surface.clone(), + surface.clone(), + wappdata, + ); + + if appdata + .handles + .borrow_mut() + .insert(handle.id(), handle.clone()) + .is_some() + { + panic!("wayland should use unique object IDs"); + } + appdata + .active_surface_id + .borrow_mut() + .push_front(handle.id()); + + surface.with_handler({ + let handle = handle.clone(); + move |winhandle| winhandle.connect(&handle.into()) + }); + + Ok(handle) + } +} diff --git a/druid-shell/src/backend/x11/application.rs b/druid-shell/src/backend/x11/application.rs index b701195fc8..3c3997b135 100644 --- a/druid-shell/src/backend/x11/application.rs +++ b/druid-shell/src/backend/x11/application.rs @@ -36,8 +36,10 @@ use x11rb::xcb_ffi::XCBConnection; use crate::application::AppHandler; use super::clipboard::Clipboard; +use super::util; use super::window::Window; -use super::{util, xkb}; +use crate::backend::shared::linux; +use crate::backend::shared::xkb; // This creates a `struct WindowAtoms` containing the specified atoms as members (along with some // convenience methods to intern and query those atoms). We use the following atoms: @@ -807,43 +809,7 @@ impl Application { } pub fn get_locale() -> String { - fn locale_env_var(var: &str) -> Option { - match std::env::var(var) { - Ok(s) if s.is_empty() => { - tracing::debug!("locale: ignoring empty env var {}", var); - None - } - Ok(s) => { - tracing::debug!("locale: env var {} found: {:?}", var, &s); - Some(s) - } - Err(std::env::VarError::NotPresent) => { - tracing::debug!("locale: env var {} not found", var); - None - } - Err(std::env::VarError::NotUnicode(_)) => { - tracing::debug!("locale: ignoring invalid unicode env var {}", var); - None - } - } - } - - // from gettext manual - // https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html#Locale-Environment-Variables - let mut locale = locale_env_var("LANGUAGE") - // the LANGUAGE value is priority list seperated by : - // See: https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html#The-LANGUAGE-variable - .and_then(|locale| locale.split(':').next().map(String::from)) - .or_else(|| locale_env_var("LC_ALL")) - .or_else(|| locale_env_var("LC_MESSAGES")) - .or_else(|| locale_env_var("LANG")) - .unwrap_or_else(|| "en-US".to_string()); - - // This is done because the locale parsing library we use expects an unicode locale, but these vars have an ISO locale - if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') { - locale.truncate(idx); - } - locale + linux::env::locale() } pub(crate) fn idle_pipe(&self) -> RawFd { diff --git a/druid-shell/src/backend/x11/mod.rs b/druid-shell/src/backend/x11/mod.rs index b51998b70e..6451f6ac52 100644 --- a/druid-shell/src/backend/x11/mod.rs +++ b/druid-shell/src/backend/x11/mod.rs @@ -32,13 +32,10 @@ #[macro_use] mod util; -mod xkb; pub mod application; pub mod clipboard; pub mod error; -mod keycodes; pub mod menu; pub mod screen; pub mod window; -mod xkbcommon_sys; diff --git a/druid-shell/src/backend/x11/util.rs b/druid-shell/src/backend/x11/util.rs index fb85368a95..714bc4d355 100644 --- a/druid-shell/src/backend/x11/util.rs +++ b/druid-shell/src/backend/x11/util.rs @@ -14,9 +14,7 @@ //! Miscellaneous utility functions for working with X11. -use std::cmp::Ordering; use std::rc::Rc; -use std::time::Instant; use anyhow::{anyhow, Error}; use x11rb::connection::RequestConnection; @@ -26,8 +24,6 @@ use x11rb::protocol::render::{self, ConnectionExt as _}; use x11rb::protocol::xproto::{Screen, Visualid, Visualtype, Window}; use x11rb::xcb_ffi::XCBConnection; -use crate::window::TimerToken; - // See: https://github.com/rtbo/rust-xcb/blob/master/examples/randr_screen_modes.rs pub fn refresh_rate(conn: &Rc, window_id: Window) -> Option { let try_refresh_rate = || -> Result { @@ -153,40 +149,3 @@ macro_rules! log_x11 { } }; } - -/// A timer is a deadline (`std::Time::Instant`) and a `TimerToken`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct Timer { - deadline: Instant, - token: TimerToken, -} - -impl Timer { - pub(crate) fn new(deadline: Instant) -> Self { - let token = TimerToken::next(); - Self { deadline, token } - } - - pub(crate) fn deadline(&self) -> Instant { - self.deadline - } - - pub(crate) fn token(&self) -> TimerToken { - self.token - } -} - -impl Ord for Timer { - /// Ordering is so that earliest deadline sorts first - // "Earliest deadline first" that a std::collections::BinaryHeap will have the earliest timer - // at its head, which is just what is needed for timer management. - fn cmp(&self, other: &Self) -> Ordering { - self.deadline.cmp(&other.deadline).reverse() - } -} - -impl PartialOrd for Timer { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} diff --git a/druid-shell/src/backend/x11/window.rs b/druid-shell/src/backend/x11/window.rs index d0ead09fcc..0201c6917c 100644 --- a/druid-shell/src/backend/x11/window.rs +++ b/druid-shell/src/backend/x11/window.rs @@ -44,6 +44,7 @@ use x11rb::xcb_ffi::XCBConnection; #[cfg(feature = "raw-win-handle")] use raw_window_handle::{unix::XcbHandle, HasRawWindowHandle, RawWindowHandle}; +use crate::backend::shared::Timer; use crate::common_util::IdleCallback; use crate::dialog::FileDialogOptions; use crate::error::Error as ShellError; @@ -61,7 +62,6 @@ use crate::{window, KeyEvent, ScaledArea}; use super::application::Application; use super::menu::Menu; -use super::util::Timer; /// A version of XCB's `xcb_visualtype_t` struct. This was copied from the [example] in x11rb; it /// is used to interoperate with cairo. @@ -566,7 +566,7 @@ pub(crate) struct Window { /// The region that was invalidated since the last time we rendered. invalid: RefCell, /// Timers, sorted by "earliest deadline first" - timer_queue: Mutex>, + timer_queue: Mutex>>, idle_queue: Arc>>, // Writing to this wakes up the event loop, so that it can run idle handlers. idle_pipe: RawFd, @@ -1746,7 +1746,7 @@ impl WindowHandle { pub fn request_timer(&self, deadline: Instant) -> TimerToken { if let Some(w) = self.window.upgrade() { - let timer = Timer::new(deadline); + let timer = Timer::new(deadline, ()); w.timer_queue.lock().unwrap().push(timer); timer.token() } else { diff --git a/druid-shell/src/clipboard.rs b/druid-shell/src/clipboard.rs index 2d18dd8088..4f25a74ebd 100644 --- a/druid-shell/src/clipboard.rs +++ b/druid-shell/src/clipboard.rs @@ -179,6 +179,7 @@ pub type FormatId = &'static str; /// Data coupled with a type identifier. #[derive(Debug, Clone)] +#[cfg_attr(feature = "wayland", allow(dead_code))] #[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub struct ClipboardFormat { pub(crate) identifier: FormatId, diff --git a/druid-shell/src/dialog.rs b/druid-shell/src/dialog.rs index 0d8352b429..ba8d0d0fa4 100644 --- a/druid-shell/src/dialog.rs +++ b/druid-shell/src/dialog.rs @@ -37,7 +37,10 @@ pub struct FileInfo { } /// Type of file dialog. -#[cfg(not(all(any(target_os = "linux", target_os = "openbsd"), feature = "x11")))] +#[cfg(not(any( + all(feature = "x11", any(target_os = "linux", target_os = "openbsd")), + feature = "wayland" +)))] #[derive(Clone, Copy, PartialEq)] pub enum FileDialogType { /// File open dialog. diff --git a/druid-shell/src/error.rs b/druid-shell/src/error.rs index 57093c5b3a..498eeadffc 100644 --- a/druid-shell/src/error.rs +++ b/druid-shell/src/error.rs @@ -24,6 +24,8 @@ use crate::backend::error as backend; pub enum Error { /// The Application instance has already been created. ApplicationAlreadyExists, + /// Tried to use the application after it had been dropped. + ApplicationDropped, /// The window has already been destroyed. WindowDropped, /// Platform specific error. @@ -38,6 +40,12 @@ impl fmt::Display for Error { Error::ApplicationAlreadyExists => { write!(f, "An application instance has already been created.") } + Error::ApplicationDropped => { + write!( + f, + "The application this operation requires has been dropped." + ) + } Error::Platform(err) => fmt::Display::fmt(err, f), Error::WindowDropped => write!(f, "The window has already been destroyed."), Error::Other(s) => write!(f, "{}", s), diff --git a/druid-shell/src/platform/linux.rs b/druid-shell/src/platform/linux.rs index d1f57b591d..55e2dda348 100644 --- a/druid-shell/src/platform/linux.rs +++ b/druid-shell/src/platform/linux.rs @@ -32,5 +32,7 @@ mod test { use super::*; use static_assertions as sa; + // TODO: impl ApplicationExt for wayland + #[cfg(not(feature = "wayland"))] sa::assert_impl_all!(Application: ApplicationExt); } diff --git a/druid-shell/src/util.rs b/druid-shell/src/util.rs index c218bd67cf..873035f506 100644 --- a/druid-shell/src/util.rs +++ b/druid-shell/src/util.rs @@ -118,3 +118,17 @@ macro_rules! borrow_mut { }) }}; } + +/// This is a useful way to clone some values into a `move` closure. Currently only used in the +/// Wayland backend. +#[allow(unused_macros)] +macro_rules! with_cloned { + ($($val:ident),* ; $($rest:tt)*) => { + { + $( + let $val = $val.clone(); + )* + $($rest)* + } + }; +} diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 3b0b7c276c..110c237fcc 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -26,6 +26,8 @@ gtk = ["druid-shell/gtk"] image = ["druid-shell/image"] svg = ["usvg"] x11 = ["druid-shell/x11"] +# **WARNING** not ready for the prime time. Many things don't work yet. +wayland = ["druid-shell/wayland"] crochet = [] serde = ["im/serde", "druid-shell/serde"]