From f6008acf9516ab1ea2aec58c2dc1088a33d32c22 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Sun, 15 Oct 2023 11:42:34 +0100 Subject: [PATCH 01/10] Start to sketch out a v2 API --- Cargo.toml | 3 + v2/Cargo.toml | 9 +++ v2/src/backend/mod.rs | 3 + v2/src/backend/v1.rs | 25 ++++++++ v2/src/handler.rs | 53 ++++++++++++++++ v2/src/lib.rs | 138 ++++++++++++++++++++++++++++++++++++++++++ v2/src/menu.rs | 74 ++++++++++++++++++++++ v2/src/shapes.rs | 9 +++ v2/src/util.rs | 31 ++++++++++ 9 files changed, 345 insertions(+) create mode 100644 v2/Cargo.toml create mode 100644 v2/src/backend/mod.rs create mode 100644 v2/src/backend/v1.rs create mode 100644 v2/src/handler.rs create mode 100644 v2/src/lib.rs create mode 100644 v2/src/menu.rs create mode 100644 v2/src/shapes.rs create mode 100644 v2/src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 5af3875e..3b8c21b0 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,9 @@ categories = [ exclude = ["/.github/"] publish = false # Until it's ready +[workspace] +members = ["v2"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/v2/Cargo.toml b/v2/Cargo.toml new file mode 100644 index 00000000..3a6aca57 --- /dev/null +++ b/v2/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "v2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +glazier = { path = ".." } diff --git a/v2/src/backend/mod.rs b/v2/src/backend/mod.rs new file mode 100644 index 00000000..6c902c41 --- /dev/null +++ b/v2/src/backend/mod.rs @@ -0,0 +1,3 @@ +mod v1; + +pub(crate) use v1::*; diff --git a/v2/src/backend/v1.rs b/v2/src/backend/v1.rs new file mode 100644 index 00000000..13c7ea30 --- /dev/null +++ b/v2/src/backend/v1.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +use crate::{WindowBuilder, WindowId}; + +pub struct Glazier { + app: glazier::Application, + windows: HashMap, +} + +impl Glazier { + pub(crate) fn stop(&self) { + self.app.quit() + } + pub(crate) fn new_window(&mut self, builder: WindowBuilder) -> WindowId { + let bld = glazier::WindowBuilder::new(self.app.clone()); + let bld = bld.title(builder.title); + let bld = bld.resizable(builder.resizable); + let bld = bld.show_titlebar(builder.show_titlebar); + let bld = bld.transparent(builder.transparent); + let window = bld.build().unwrap(); + let id = WindowId::next(); + self.windows.insert(id, window); + id + } +} diff --git a/v2/src/handler.rs b/v2/src/handler.rs new file mode 100644 index 00000000..1edf362f --- /dev/null +++ b/v2/src/handler.rs @@ -0,0 +1,53 @@ +use crate::{Glazier, WindowId}; + +/// The primary trait which must be implemented by your application state +/// +/// This trait consists of handlers for events the platform may provide, +/// or to request details for communication +/// +/// These handlers are the primary expected way for code to be executed +/// on the main thread whilst the application is running. One-off tasks +/// can also be added to the main thread using `GlazierHandle::run_on_main`, +/// which gets access to the +/// +/// # Context +/// +/// Each handler is passed an exclusive reference to the [`Glazier`] as the +/// first non-self parameter. This can be used to control the platform +/// (such as by requesting a change in properties of a window). +// TODO: Is this useful? +//
+// Historical discussion on the use of resource handles +// +// Prior versions of `glazier` (and its precursor `druid-shell`) provided +// value-semantics handles for the key resources of `Glazier` (formerly +// `Application`) and `Window`. This however caused issues with state management +// - namely that implementations of `WindowHandler` needed to store the handle +// associated with the window +// with the event loop. As these handles were neither `Send` nor `Sync`, these +// could only be used within event handler methods, so moving the capabilities +// to a context parameter is a natural progression. +//
+/// +/// Most of the event are also associated with a single window. +/// The methods which are +/// +// Note: Most methods are marked with `#[allow(unused_variables)]` decoration +// for documentation purposes +pub trait PlatformHandler { + /// Called when an app level menu item is selected. + /// + /// This is primarily useful on macOS, where the menu can exist even when + /// + /// In future, this may also be used for selections in tray menus + #[allow(unused_variables)] + fn app_menu_item_selected(&mut self, glz: &mut Glazier, command: u32) {} + + /// Called when a menu item associated with a window is selected. + /// + /// This distinction from [app_menu_item_selected] allows for the same command ids to be reused in multiple windows + /// + /// [app_menu_item_selected]: PlatformHandler::app_menu_item_selected + #[allow(unused_variables)] + fn menu_item_selected(&mut self, glz: &mut Glazier, win: WindowId, command: u32) {} +} diff --git a/v2/src/lib.rs b/v2/src/lib.rs new file mode 100644 index 00000000..1917bcd3 --- /dev/null +++ b/v2/src/lib.rs @@ -0,0 +1,138 @@ +//! Glazier is an operating system integration layer infrastructure layer +//! intended for high quality GUI toolkits in Rust. +//! +//! # Example +//! +//! ```rust,no_run +//! # use v2::{WindowId, GlazierBuilder}; +//! struct UiState { +//! main_window_id: WindowId; +//! } +//! +//! impl UiHandler for UiState { +//! // .. +//! } +//! +//! let mut platform = GlazierBuilder::new(); +//! let main_window_id = platform.build_window(|window_builder| { +//! window_builder.title("Main Window") +//! .logical_size((600., 400.)); +//! }); +//! let state = UiState { +//! main_window_id +//! }; +//! platform.run(Box::new(state), |_| ()); +//! +//! ``` +//! +//! It is agnostic to the +//! choice of drawing, so the client must provide that, but the goal is to +//! abstract over most of the other integration points with the underlying +//! operating system. +//! +//! `glazier` is an abstraction around a given platform UI & application +//! framework. It provides common types, which then defer to a platform-defined +//! implementation. + +use std::num::NonZeroU64; + +use util::Counter; + +mod backend; +mod handler; +pub mod menu; +pub mod shapes; +mod util; + +pub use handler::PlatformHandler; + +/// Manages communication with the platform +/// +/// Created using a `GlazierBuilder` +pub struct Glazier(backend::Glazier); + +pub struct WindowBuilder { + title: String, + // menu: Option, + // size: Size, + // min_size: Option, + // position: Option, + // level: Option, + // window_state: Option, + resizable: bool, + show_titlebar: bool, + transparent: bool, +} + +impl Default for WindowBuilder { + fn default() -> Self { + Self { + title: "Glazier Application Window".to_string(), + resizable: true, + show_titlebar: true, + transparent: false, + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct WindowId(NonZeroU64); + +static WINDOW_ID_COUNTER: Counter = Counter::new(); + +impl WindowId { + pub(crate) fn next() -> Self { + Self(WINDOW_ID_COUNTER.next_nonzero()) + } +} + +impl Glazier { + pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowBuilder)) -> WindowId { + let mut builder_instance = WindowBuilder::default(); + builder(&mut builder_instance); + self.new_window(builder_instance) + } + + pub fn new_window(&mut self, builder: WindowBuilder) -> WindowId { + self.0.new_window(builder) + } + + /// Request that this `Glazier` stop controlling the current thread + /// + /// This should be called after all windows have been closed + pub fn stop(&mut self) { + self.0.stop(); + } +} + +/// Allows configuring a `Glazier` before initialising the system +pub struct GlazierBuilder; + +impl GlazierBuilder { + /// Prepare to interact with the desktop environment + /// + /// This should be called on the main thread for maximum portability. + pub fn new() -> GlazierBuilder { + GlazierBuilder + } + + pub fn build_window(&mut self, builder: impl FnOnce(&mut WindowBuilder)) -> WindowId { + todo!() + } + /// Queues the creation of a new window for when the `Glazier` is created + pub fn new_window(&mut self, builder: WindowBuilder) -> WindowId { + todo!() + } + + /// Start interacting with the platform + /// + /// Start handling events from the platform using `event_handler` + /// + /// `on_init` will be called once the event loop is sufficiently + /// intialized to allow creating + /// + /// ## Notes + /// + /// The event_handler is passed as a box for simplicity + pub fn run(self, event_handler: Box, on_init: impl FnOnce(&mut Glazier)) {} +} diff --git a/v2/src/menu.rs b/v2/src/menu.rs new file mode 100644 index 00000000..737e3b03 --- /dev/null +++ b/v2/src/menu.rs @@ -0,0 +1,74 @@ +use std::borrow::Cow; + +/// An abstract menu object +/// +/// These have value semantics - that is +pub struct MenuBuilder<'a> { + items: Vec>, +} + +impl<'a> MenuBuilder<'a> { + pub fn add_child(&mut self) {} + + pub fn with_child_with_children(&mut self) {} + + pub fn with_child_with_children_builder(&mut self) {} +} + +// Kinds of menu item: +// - Normal (e.g. 'Copy/Paste') +// - Break/seperator (e.g. -------). These can't have children (?) +// - + +pub enum MenuItem { + Ordinary, + Break, +} + +pub enum Command { + Copy, + Paste, + Undo, + Redo, + Custom(u32), +} + +#[non_exhaustive] +pub struct MenuItems<'a> { + pub display: Cow<'a, str>, +} + +pub(crate) struct MenuIterator<'a, 'b> { + items: &'a [MenuMember<'b>], + stack: Vec, +} + +impl<'a, 'b> Iterator for MenuIterator<'a, 'b> { + type Item = &'a MenuItem<'b>; + + fn next(&mut self) -> Option { + let current = self.stack.last().copied()?; + let current_item = &self.items[current.0]; + if let Some(child) = current_item.first_child { + self.stack.push(child); + } else if let Some(sibling) = current_item.next_sibling { + self.stack.pop(); + self.stack.push(sibling); + } else { + self.stack.pop(); + } + Some(¤t_item.item) + } +} + +struct MenuMember<'a> { + item: MenuItem<'a>, + next_sibling: Option, + first_child: Option, +} + +#[derive(Clone, Copy)] +pub struct MenuItemId(usize); + +/// Allows building an application menu which matches platform conventions +pub struct AppMenuBuilder {} diff --git a/v2/src/shapes.rs b/v2/src/shapes.rs new file mode 100644 index 00000000..1be714bf --- /dev/null +++ b/v2/src/shapes.rs @@ -0,0 +1,9 @@ +pub struct LogicalSize { + x: f64, + y: f64, +} + +pub struct PhysicalSize { + x: u32, + y: u32, +} diff --git a/v2/src/util.rs b/v2/src/util.rs new file mode 100644 index 00000000..6e583b83 --- /dev/null +++ b/v2/src/util.rs @@ -0,0 +1,31 @@ +use std::{ + num::NonZeroU64, + sync::atomic::{AtomicU64, Ordering}, +}; + +/// An incrementing counter for generating unique ids. +/// +/// This can be used safely from multiple threads. +/// +/// The counter will overflow if `next()` is called 2^64 - 2 times. +/// If this is possible for your application, and reuse would be undesirable, +/// use something else. +pub struct Counter(AtomicU64); + +impl Counter { + /// Create a new counter. + pub const fn new() -> Counter { + Counter(AtomicU64::new(1)) + } + + /// Return the next value. + pub fn next(&self) -> u64 { + self.0.fetch_add(1, Ordering::Relaxed) + } + + /// Return the next value, as a `NonZeroU64`. + pub fn next_nonzero(&self) -> NonZeroU64 { + // unwrap won't happen because our initial value is 1 and can only be incremented. + NonZeroU64::new(self.0.fetch_add(1, Ordering::Relaxed)).unwrap() + } +} From ba21e92df68b84d57af94db8044ca5f4c7f54501 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 24 Nov 2023 22:55:44 +0100 Subject: [PATCH 02/10] Start to document some considerations around hotkeys --- v2/src/hotkey_notes.rs | 315 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 v2/src/hotkey_notes.rs diff --git a/v2/src/hotkey_notes.rs b/v2/src/hotkey_notes.rs new file mode 100644 index 00000000..8b02e904 --- /dev/null +++ b/v2/src/hotkey_notes.rs @@ -0,0 +1,315 @@ +// 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. + +//! # Keyboard shortcuts +//! +//! Using keyboard shortcuts inherently requires awareness of the user's keyboard layout. +//! This is especially important for displaying, where it is correct to display the key +//! which should be pressed to activate the shortcut. For example, a shortcut based on +//! the z key would be displayed using ζ for a user with a +//! Greek keyboard. +//! +//! +//! ```rust,no_run +//! # use v2::hotkey::KeyboardLayout; +//! struct State { +//! layout: KeyboardLayout, +//! copy_hotkey: HotKey, +//! } +//! impl MyState { +//! fn layout_changed(&mut self, glz: &mut Glazier) { +//! // TODO: Does get_keyboard_layout need to be async? Doesn't on Windows/Linux. +//! // macOS not sure +//! self.layout = glz.get_keyboard_layout(); +//! // TODO: Make an easier way to type this +//! let copy_shortcut = KeyboardShortcut::CharacterBased { +//! modifiers: SysMods::Cmd, +//! primary: Some('C'), +//! alternative_characters: vec![] +//! }; +//! self.copy_hotkey = self.layout.translate(copy_shortcut); +//! } +//! fn key_down(&mut self, glz: &mut Glazier, event: &KeyEvent) { +//! if self.copy_hotkey.matches(&event) { +//! // Copy currently selected item +//! // TODO: Is Copy a native feature? +//! } +//! } +//! } +//! ``` +//! +//! For documentation in this module, Mac style keyboard shortcuts are used to provide +//! consistency in examples. However, for clarity the full names are used, rather than +//! symbols. For Windows/Linux users, the main difference is that instances of 'Command' +//! represent a modifier which is used similarly to Control on Windows. See [SysMods] +//! for more details. +//! +//! This module contains two primary types to describe the same keyboard shortcuts, for different purposes: +//! - A [KeyboardShortcut] is a description of a logical or intended keyboard shortcut, +//! as created by the application developer. For example, this would describe the shortcut +//! for copy as one which is activated when the C key is pressed whilst the command key is +//! held. +//! - A [HotKey] describes which key should be pressed in the current layout to activate the +//! given shortcut. These are printable to be displayed to the user, and key press events +//! can be tested against then. If a match occured, the action of the keyboard shortcut +//! should be performed. In the copy example, printing the Hotkey on a Mac would give +//! ⌘C, and on Windows would give Ctrl+C. +//! +//! Each [KeyboardShortcut] can be converted into a [HotKey] using the [KeyboardLayout] type, +//! which is obtained using the `get_keyboard_layout` method on the [Glazier](crate::Glazier). +//! +//! ## Configurability +//! +//! Reasonable users may have different expectations around their keyboard shortcuts. For example, +//! some users may be using an uncommon keyboard layout (such as [Dvorak]). Because of this, there +//! will be some configuration options you should make available, but these are not yet implemented +// +// TODO: Something like this? +// Glazier exposes an option to use a (US) Qwerty layout for all keyboard shortcuts. The default +// behaviour (where the shortcut is based on the character which would be typed) should be correct +// for most users, but you could expose this as an option in your settings +// for Dvorak users to choose. +// +// +// This option can also be configured using an environment variable, to enable these users to +// configure this across all Glazier applications. The environment variable overwrites the global condition, +// so you should indicate that the setting is disabled when the environment variable is set. +// If the `GLAZIER_USE_US_QWERTY_HOTKEYS` environment variable contains a value of `alpha`, +// or the force_qwerty_fallback function is called on the `KeyboardLayout`, a QWERTY +// layout will be used for all alphabetical hotkeys, even when a different latin keyboard +// layout (such as DVORAK) is enabled. This may be exposed as an option to users.[^qwerty_force] +//! +//! [Dvorak]: https://en.wikipedia.org/wiki/Dvorak_keyboard_layout + +use std::borrow::Borrow; + +use glazier::{keyboard_types::Key, Code, KeyEvent, Modifiers}; + +/// A [`KeyboardShortcut`] contains layout-agnostic instructions for creating a [`HotKey`] for a [`KeyboardLayout`] +pub enum KeyboardShortcut { + /// This kind of shortcut is based on the specific character being typed + /// + /// Additional characters may be provided, to allow for localised shortcuts. + /// For example, for a shortcut used to go to a money related page, you may + /// wish to provide the shortcut ⌘-[Local Currency symbol][^localised_shortcuts]. + /// For that command, you could set `alternative_characters` to `['¥', '₹', '£', '€']`, + /// and set `primary` to `Some('$')` + /// + /// If the specified character cannot be typed on the current keyboard layout, + /// and the character is alphabetical, its location on a QWERTY keyboard will + /// be used for the hotkey instead. For example, if the user is using a Greek + /// keyboard, the shortcut ⌘-C would use the location of C + /// on a US QWERTY keyboard, so the [HotKey] would match ⌘-ψ. + /// It is possible to force-enable this behaviour. See [the module level docs](self#configurability) + /// for more details + /// + /// If the character is not alphabetical, Glazier does not choose a fallback. You are + /// instead expected to raise this to the user, such as by disabling the shortcut, and + /// listing that the shortcut is not available on this machine. + // TODO: Do we want this behaviour: The error value will + // contain what the fallback would be, if possible. This could be exposed as a suggested + // alternative. The extended fallback can be force-enabled using the + // `GLAZIER_FORCE_LAYOUT_AGNOSTIC` environment variable. + /// + /// [^localised_shortcuts]: Whether this kind of shortcut would be idiomatic is a different question + /// + /// [^qwerty_force]: To achieve the equivalent of this feature, some users will have enabled a + /// "hold ctrl to enable QWERTY layout" functionality. This feature however extends this to keyboard + /// shortcuts such as s (as seen on GitHub to go to the search bar), where a modifier is not held down + // TODO: What to do about non-alphabetical + CharacterBased { + /// The modifiers which must be pressed alongside this character + /// + /// The use of the Shift modifier should be avoided for non-alphabetical shortcuts + /// + /// There is, however, one case where this is useful, which is paired modifiers. + /// For example, 2 could be assigned to an action (e.g. activating the second + /// item), and Shift+2 could be assigned to a different related item (e.g. + /// selecting the second item but not activating it) + modifiers: RawMods, + primary: Option, + }, + /// A keyboard shortcut which depends on the exact scancode being provided, i.e. + /// the physical location on the keyboard + /// + /// When creating default keyboard shortcuts, care should be taken to limit the use of + /// this variant to the few specific cases where they are correct. These are: + /// - Where the shortcut is set because of the location of the key. The primary example + /// of this is for games using WASD controls, where `Code::KeyW` would be used for + /// forward, `KeyA` for strafe left, etc. These should generally only be used for + /// alphabetical or numeric keycodes, as these are the only codes with generally + /// consistent key locations. This includese + /// + /// Note that user-provided keyboard shortcuts may use this form as per their own preference. + KeyCodeBased { + // The key which must be pressed + keycode: Code, + modifiers: RawMods, + }, +} + +/// A platform-and-layout specific representation of a hotkey +/// +/// This is the type used for matching hotkeys, and displaying them to a user +pub struct HotKey { + code: Code, + modifiers: Modifiers, + /// The character printed when activating this hotkey + printable: char, +} + +impl HotKey { + /// Returns `true` if this [`KeyEvent`] matches this `HotKey`. + /// + /// [`KeyEvent`]: KeyEvent + pub fn matches(&self, event: impl Borrow) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::META; + let event: &KeyEvent = event.borrow(); + self.modifiers == event.mods & base_mods && self.code == event.code + } +} + +/// A keyboard layout, used to convert [`KeyboardShortcut`]s into [`HotKey`]s +pub struct KeyboardLayout { + force_qwerty_fallback: bool, +} + +/// A platform-agnostic representation of keyboard modifiers, for command handling. +/// +/// This does one thing: it allows specifying hotkeys that use the Command key +/// on macOS, but use the Ctrl key on other platforms. +#[derive(Debug, Clone, Copy)] +pub enum SysMods { + None, + Shift, + /// Command on macOS, and Ctrl on windows/linux/OpenBSD + Cmd, + /// Command + Alt on macOS, Ctrl + Alt on windows/linux/OpenBSD + AltCmd, + /// Command + Shift on macOS, Ctrl + Shift on windows/linux/OpenBSD + CmdShift, + /// Command + Alt + Shift on macOS, Ctrl + Alt + Shift on windows/linux/OpenBSD + AltCmdShift, +} + +//TODO: should something like this just _replace_ keymodifiers? +/// A representation of the active modifier keys. +/// +/// This is intended to be clearer than `Modifiers`, when describing hotkeys. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawMods { + None, + Alt, + Ctrl, + Meta, + Shift, + AltCtrl, + AltMeta, + AltShift, + CtrlShift, + CtrlMeta, + MetaShift, + AltCtrlMeta, + AltCtrlShift, + AltMetaShift, + CtrlMetaShift, + AltCtrlMetaShift, +} + +impl std::cmp::PartialEq for RawMods { + fn eq(&self, other: &Modifiers) -> bool { + let mods: Modifiers = (*self).into(); + mods == *other + } +} + +impl std::cmp::PartialEq for Modifiers { + fn eq(&self, other: &RawMods) -> bool { + other == self + } +} + +impl std::cmp::PartialEq for SysMods { + fn eq(&self, other: &Modifiers) -> bool { + let mods: RawMods = (*self).into(); + mods == *other + } +} + +impl std::cmp::PartialEq for Modifiers { + fn eq(&self, other: &SysMods) -> bool { + let other: RawMods = (*other).into(); + &other == self + } +} + +impl From for Modifiers { + fn from(src: RawMods) -> Modifiers { + let (alt, ctrl, meta, shift) = match src { + RawMods::None => (false, false, false, false), + RawMods::Alt => (true, false, false, false), + RawMods::Ctrl => (false, true, false, false), + RawMods::Meta => (false, false, true, false), + RawMods::Shift => (false, false, false, true), + RawMods::AltCtrl => (true, true, false, false), + RawMods::AltMeta => (true, false, true, false), + RawMods::AltShift => (true, false, false, true), + RawMods::CtrlMeta => (false, true, true, false), + RawMods::CtrlShift => (false, true, false, true), + RawMods::MetaShift => (false, false, true, true), + RawMods::AltCtrlMeta => (true, true, true, false), + RawMods::AltMetaShift => (true, false, true, true), + RawMods::AltCtrlShift => (true, true, false, true), + RawMods::CtrlMetaShift => (false, true, true, true), + RawMods::AltCtrlMetaShift => (true, true, true, true), + }; + let mut mods = Modifiers::empty(); + mods.set(Modifiers::ALT, alt); + mods.set(Modifiers::CONTROL, ctrl); + mods.set(Modifiers::META, meta); + mods.set(Modifiers::SHIFT, shift); + mods + } +} + +// we do this so that HotKey::new can accept `None` as an initial argument. +impl From for Option { + fn from(src: SysMods) -> Option { + Some(src.into()) + } +} + +impl From for RawMods { + fn from(src: SysMods) -> RawMods { + #[cfg(target_os = "macos")] + match src { + SysMods::None => RawMods::None, + SysMods::Shift => RawMods::Shift, + SysMods::Cmd => RawMods::Meta, + SysMods::AltCmd => RawMods::AltMeta, + SysMods::CmdShift => RawMods::MetaShift, + SysMods::AltCmdShift => RawMods::AltMetaShift, + } + #[cfg(not(target_os = "macos"))] + match src { + SysMods::None => RawMods::None, + SysMods::Shift => RawMods::Shift, + SysMods::Cmd => RawMods::Ctrl, + SysMods::AltCmd => RawMods::AltCtrl, + SysMods::CmdShift => RawMods::CtrlShift, + SysMods::AltCmdShift => RawMods::AltCtrlShift, + } + } +} From ba2daa13f9c35a6075d39487cf52f1d0bc4b8862 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 24 Nov 2023 22:56:41 +0100 Subject: [PATCH 03/10] Sketch out the initial API for app handler only --- Cargo.toml | 2 +- v2/src/backend/v1.rs | 130 ++++++++++++++++++++++++++++++++++++++----- v2/src/handler.rs | 10 +++- v2/src/lib.rs | 49 +++++++++------- v2/src/menu.rs | 74 ------------------------ v2/src/shapes.rs | 9 --- v2/src/util.rs | 31 ----------- 7 files changed, 155 insertions(+), 150 deletions(-) delete mode 100644 v2/src/menu.rs delete mode 100644 v2/src/shapes.rs delete mode 100644 v2/src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 3b8c21b0..8068a8c1 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ default-target = "x86_64-pc-windows-msvc" cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] [features] -default = ["x11"] +default = ["x11", "wayland"] x11 = ["ashpd", "bindgen", "futures", "nix", "pkg-config", "x11rb"] wayland = [ # Required for XKBCommon diff --git a/v2/src/backend/v1.rs b/v2/src/backend/v1.rs index 13c7ea30..e7d39791 100644 --- a/v2/src/backend/v1.rs +++ b/v2/src/backend/v1.rs @@ -1,25 +1,127 @@ -use std::collections::HashMap; +use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc}; -use crate::{WindowBuilder, WindowId}; +use glazier::{AppHandler, Application, Error, WinHandler}; -pub struct Glazier { +use crate::{Glazier, PlatformHandler, WindowBuilder, WindowId}; + +pub fn launch( + handler: Box, + on_init: impl FnOnce(Glazier), +) -> Result<(), Error> { + let app = Application::new()?; + // Current glazier's design forces + let state = Rc::new(RefCell::new(V1SharedState { + glz: GlazierState { + app: app.clone(), + windows: Default::default(), + operations: Default::default(), + }, + handler, + })); + with_glz(&state, |_, glz| on_init(glz)); + let handler = V1AppHandler { state }; + app.run(Some(Box::new(handler))); + Ok(()) +} +pub type GlazierImpl<'a> = &'a mut GlazierState; + +struct V1AppHandler { + state: Rc>, +} + +impl AppHandler for V1AppHandler { + fn command(&mut self, id: u32) { + with_glz(&self.state, |handler, glz| { + handler.app_menu_item_selected(glz, id) + }); + } +} + +struct V1WindowHandler { + state: Rc>, + window: WindowId, +} + +impl WinHandler for V1WindowHandler { + fn connect(&mut self, _: &glazier::WindowHandle) { + // No need to do anything here? + } + + fn prepare_paint(&mut self) {} + + fn paint(&mut self, invalid: &glazier::Region) {} + + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } +} + +struct V1SharedState { + glz: GlazierState, + handler: Box, +} + +pub(crate) struct GlazierState { app: glazier::Application, windows: HashMap, + operations: Vec, } -impl Glazier { - pub(crate) fn stop(&self) { +fn with_glz( + outer_state: &Rc>, + f: impl FnOnce(&mut Box, Glazier) -> R, +) -> R { + let mut state = outer_state.borrow_mut(); + let state = &mut *state; + let glz = Glazier(&mut state.glz, PhantomData); + let res = f(&mut state.handler, glz); + let mut create_window_failures = Vec::<(WindowId, Error)>::new(); + for op in state.glz.operations.drain(..) { + match op { + Command::NewWindow(builder) => { + // let state = outer_state.clone(); + let bld = glazier::WindowBuilder::new(state.glz.app.clone()); + let bld = bld + .title(builder.title) + .resizable(builder.resizable) + .show_titlebar(builder.show_titlebar) + .transparent(builder.transparent); + let id = builder.id.unwrap_or_else(WindowId::next); + let window_id = builder.id.unwrap_or_else(WindowId::next); + let bld = bld.handler(Box::new(V1WindowHandler { + state: outer_state.clone(), + window: window_id, + })); + let window = bld.build(); + + match window { + Ok(window) => { + state.glz.windows.insert(id, window); + } + Err(e) => create_window_failures.push((window_id, e)), + } + } + } + } + for (win, error) in create_window_failures.drain(..) { + let glz = Glazier(&mut state.glz, PhantomData); + state.handler.creating_window_failed(glz, win, error) + } + res +} +pub enum Command { + NewWindow(WindowBuilder), +} + +impl GlazierState { + pub(crate) fn stop(&mut self) { self.app.quit() } - pub(crate) fn new_window(&mut self, builder: WindowBuilder) -> WindowId { - let bld = glazier::WindowBuilder::new(self.app.clone()); - let bld = bld.title(builder.title); - let bld = bld.resizable(builder.resizable); - let bld = bld.show_titlebar(builder.show_titlebar); - let bld = bld.transparent(builder.transparent); - let window = bld.build().unwrap(); - let id = WindowId::next(); - self.windows.insert(id, window); + + pub(crate) fn new_window(&mut self, mut builder: WindowBuilder) -> WindowId { + let id = builder.id.unwrap_or_else(WindowId::next); + builder.id = Some(id); + self.operations.push(Command::NewWindow(builder)); id } } diff --git a/v2/src/handler.rs b/v2/src/handler.rs index 1edf362f..737df422 100644 --- a/v2/src/handler.rs +++ b/v2/src/handler.rs @@ -1,3 +1,5 @@ +use glazier::Error; + use crate::{Glazier, WindowId}; /// The primary trait which must be implemented by your application state @@ -41,7 +43,7 @@ pub trait PlatformHandler { /// /// In future, this may also be used for selections in tray menus #[allow(unused_variables)] - fn app_menu_item_selected(&mut self, glz: &mut Glazier, command: u32) {} + fn app_menu_item_selected(&mut self, glz: Glazier, command: u32) {} /// Called when a menu item associated with a window is selected. /// @@ -49,5 +51,9 @@ pub trait PlatformHandler { /// /// [app_menu_item_selected]: PlatformHandler::app_menu_item_selected #[allow(unused_variables)] - fn menu_item_selected(&mut self, glz: &mut Glazier, win: WindowId, command: u32) {} + fn menu_item_selected(&mut self, glz: Glazier, win: WindowId, command: u32) {} + + fn creating_window_failed(&mut self, glz: Glazier, win: WindowId, error: Error) { + todo!("Failed to create window {win:?}. Error: {error:?}"); + } } diff --git a/v2/src/lib.rs b/v2/src/lib.rs index 1917bcd3..91dfc78d 100644 --- a/v2/src/lib.rs +++ b/v2/src/lib.rs @@ -34,22 +34,18 @@ //! framework. It provides common types, which then defer to a platform-defined //! implementation. -use std::num::NonZeroU64; - -use util::Counter; +use std::{marker::PhantomData, num::NonZeroU64}; mod backend; mod handler; -pub mod menu; -pub mod shapes; -mod util; +use glazier::Counter; pub use handler::PlatformHandler; /// Manages communication with the platform /// /// Created using a `GlazierBuilder` -pub struct Glazier(backend::Glazier); +pub struct Glazier<'a>(backend::GlazierImpl<'a>, PhantomData<&'a mut ()>); pub struct WindowBuilder { title: String, @@ -62,6 +58,7 @@ pub struct WindowBuilder { resizable: bool, show_titlebar: bool, transparent: bool, + id: Option, } impl Default for WindowBuilder { @@ -71,11 +68,12 @@ impl Default for WindowBuilder { resizable: true, show_titlebar: true, transparent: false, + id: None, } } } -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct WindowId(NonZeroU64); static WINDOW_ID_COUNTER: Counter = Counter::new(); @@ -86,7 +84,7 @@ impl WindowId { } } -impl Glazier { +impl Glazier<'_> { pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowBuilder)) -> WindowId { let mut builder_instance = WindowBuilder::default(); builder(&mut builder_instance); @@ -106,33 +104,46 @@ impl Glazier { } /// Allows configuring a `Glazier` before initialising the system -pub struct GlazierBuilder; +pub struct GlazierBuilder { + windows: Vec, +} impl GlazierBuilder { /// Prepare to interact with the desktop environment - /// - /// This should be called on the main thread for maximum portability. pub fn new() -> GlazierBuilder { - GlazierBuilder + GlazierBuilder { windows: vec![] } } pub fn build_window(&mut self, builder: impl FnOnce(&mut WindowBuilder)) -> WindowId { - todo!() + let mut builder_instance = WindowBuilder::default(); + builder(&mut builder_instance); + self.new_window(builder_instance) } /// Queues the creation of a new window for when the `Glazier` is created - pub fn new_window(&mut self, builder: WindowBuilder) -> WindowId { - todo!() + pub fn new_window(&mut self, mut builder: WindowBuilder) -> WindowId { + let id = builder.id.get_or_insert_with(WindowId::next).clone(); + self.windows.push(builder); + id } /// Start interacting with the platform /// - /// Start handling events from the platform using `event_handler` + /// Start handling events from the platform using `event_handler`. + /// This should be called on the main thread for maximum portability. /// /// `on_init` will be called once the event loop is sufficiently - /// intialized to allow creating + /// intialized to allow creating resources at that time. This will + /// be after the other properties of this builder are applied (such as queued windows). /// /// ## Notes /// /// The event_handler is passed as a box for simplicity - pub fn run(self, event_handler: Box, on_init: impl FnOnce(&mut Glazier)) {} + /// + pub fn launch(self, event_handler: Box, on_init: impl FnOnce(Glazier)) { + backend::launch(event_handler, move |glz| { + on_init(glz); + }) + // TODO: Proper error handling + .unwrap() + } } diff --git a/v2/src/menu.rs b/v2/src/menu.rs deleted file mode 100644 index 737e3b03..00000000 --- a/v2/src/menu.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::borrow::Cow; - -/// An abstract menu object -/// -/// These have value semantics - that is -pub struct MenuBuilder<'a> { - items: Vec>, -} - -impl<'a> MenuBuilder<'a> { - pub fn add_child(&mut self) {} - - pub fn with_child_with_children(&mut self) {} - - pub fn with_child_with_children_builder(&mut self) {} -} - -// Kinds of menu item: -// - Normal (e.g. 'Copy/Paste') -// - Break/seperator (e.g. -------). These can't have children (?) -// - - -pub enum MenuItem { - Ordinary, - Break, -} - -pub enum Command { - Copy, - Paste, - Undo, - Redo, - Custom(u32), -} - -#[non_exhaustive] -pub struct MenuItems<'a> { - pub display: Cow<'a, str>, -} - -pub(crate) struct MenuIterator<'a, 'b> { - items: &'a [MenuMember<'b>], - stack: Vec, -} - -impl<'a, 'b> Iterator for MenuIterator<'a, 'b> { - type Item = &'a MenuItem<'b>; - - fn next(&mut self) -> Option { - let current = self.stack.last().copied()?; - let current_item = &self.items[current.0]; - if let Some(child) = current_item.first_child { - self.stack.push(child); - } else if let Some(sibling) = current_item.next_sibling { - self.stack.pop(); - self.stack.push(sibling); - } else { - self.stack.pop(); - } - Some(¤t_item.item) - } -} - -struct MenuMember<'a> { - item: MenuItem<'a>, - next_sibling: Option, - first_child: Option, -} - -#[derive(Clone, Copy)] -pub struct MenuItemId(usize); - -/// Allows building an application menu which matches platform conventions -pub struct AppMenuBuilder {} diff --git a/v2/src/shapes.rs b/v2/src/shapes.rs deleted file mode 100644 index 1be714bf..00000000 --- a/v2/src/shapes.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub struct LogicalSize { - x: f64, - y: f64, -} - -pub struct PhysicalSize { - x: u32, - y: u32, -} diff --git a/v2/src/util.rs b/v2/src/util.rs deleted file mode 100644 index 6e583b83..00000000 --- a/v2/src/util.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::{ - num::NonZeroU64, - sync::atomic::{AtomicU64, Ordering}, -}; - -/// An incrementing counter for generating unique ids. -/// -/// This can be used safely from multiple threads. -/// -/// The counter will overflow if `next()` is called 2^64 - 2 times. -/// If this is possible for your application, and reuse would be undesirable, -/// use something else. -pub struct Counter(AtomicU64); - -impl Counter { - /// Create a new counter. - pub const fn new() -> Counter { - Counter(AtomicU64::new(1)) - } - - /// Return the next value. - pub fn next(&self) -> u64 { - self.0.fetch_add(1, Ordering::Relaxed) - } - - /// Return the next value, as a `NonZeroU64`. - pub fn next_nonzero(&self) -> NonZeroU64 { - // unwrap won't happen because our initial value is 1 and can only be incremented. - NonZeroU64::new(self.0.fetch_add(1, Ordering::Relaxed)).unwrap() - } -} From 918e8e36579f2e2af4553da64db99cf74a16eff7 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:14:57 +0000 Subject: [PATCH 04/10] Begin porting the Wayland backend to the v2 API --- v2/Cargo.toml | 56 +- v2/build.rs | 68 ++ v2/src/backend/mod.rs | 5 +- v2/src/backend/shared/keyboard.rs | 256 +++++ v2/src/backend/shared/linux/env.rs | 42 + v2/src/backend/shared/linux/mod.rs | 2 + v2/src/backend/shared/mod.rs | 35 + v2/src/backend/shared/timer.rs | 44 + v2/src/backend/shared/xkb.rs | 73 ++ v2/src/backend/shared/xkb/keycodes.rs | 252 +++++ v2/src/backend/shared/xkb/xkb_api.rs | 714 ++++++++++++++ v2/src/backend/shared/xkb/xkbcommon_sys.rs | 4 + v2/src/backend/v1.rs | 23 +- v2/src/backend/wayland/.README.md | 3 + v2/src/backend/wayland/error.rs | 59 ++ v2/src/backend/wayland/input/keyboard.rs | 316 +++++++ v2/src/backend/wayland/input/keyboard/mmap.rs | 91 ++ v2/src/backend/wayland/input/mod.rs | 602 ++++++++++++ v2/src/backend/wayland/input/repeat.rs | 1 + v2/src/backend/wayland/input/text_input.rs | 425 +++++++++ v2/src/backend/wayland/mod.rs | 135 +++ v2/src/backend/wayland/run_loop.rs | 171 ++++ v2/src/backend/wayland/screen.rs | 24 + v2/src/backend/wayland/window.rs | 873 ++++++++++++++++++ v2/src/handler.rs | 44 +- v2/src/lib.rs | 83 +- v2/src/window.rs | 60 ++ 27 files changed, 4391 insertions(+), 70 deletions(-) create mode 100644 v2/build.rs create mode 100644 v2/src/backend/shared/keyboard.rs create mode 100644 v2/src/backend/shared/linux/env.rs create mode 100644 v2/src/backend/shared/linux/mod.rs create mode 100644 v2/src/backend/shared/mod.rs create mode 100644 v2/src/backend/shared/timer.rs create mode 100644 v2/src/backend/shared/xkb.rs create mode 100644 v2/src/backend/shared/xkb/keycodes.rs create mode 100644 v2/src/backend/shared/xkb/xkb_api.rs create mode 100644 v2/src/backend/shared/xkb/xkbcommon_sys.rs create mode 100644 v2/src/backend/wayland/.README.md create mode 100644 v2/src/backend/wayland/error.rs create mode 100644 v2/src/backend/wayland/input/keyboard.rs create mode 100644 v2/src/backend/wayland/input/keyboard/mmap.rs create mode 100644 v2/src/backend/wayland/input/mod.rs create mode 100644 v2/src/backend/wayland/input/repeat.rs create mode 100644 v2/src/backend/wayland/input/text_input.rs create mode 100644 v2/src/backend/wayland/mod.rs create mode 100644 v2/src/backend/wayland/run_loop.rs create mode 100644 v2/src/backend/wayland/screen.rs create mode 100644 v2/src/backend/wayland/window.rs create mode 100644 v2/src/window.rs diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 3a6aca57..95ae14df 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -5,5 +5,59 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["wayland"] +wayland = [ + # Required for XKBCommon + "pkg-config", + "bindgen", + "nix", + "smithay-client-toolkit", + "wayland-backend", +] + + [dependencies] -glazier = { path = ".." } +glazier = { path = "../" } +cfg-if = "1.0.0" + +tracing = { version = "0.1.22", features = ["log"] } +raw-window-handle = { version = "0.5.0", default_features = false } + +keyboard-types = { version = "0.7", default_features = false } +instant = { version = "0.1.6", features = ["wasm-bindgen"] } + +[target.'cfg(any(target_os = "freebsd", target_os="linux", target_os="openbsd"))'.dependencies] +ashpd = { version = "0.5", optional = true } +futures = { version = "0.3.24", optional = true, features = ["executor"] } + +nix = { version = "0.25.0", optional = true } + +x11rb = { version = "0.12", features = [ + "allow-unsafe-code", + "present", + "render", + "randr", + "xfixes", + "xkb", + "resource_manager", + "cursor", + "xinput", +], optional = true } + +rand = { version = "0.8.0", optional = true } +log = { version = "0.4.14", optional = true } + +smithay-client-toolkit = { version = "0.17.0", optional = true, default-features = false, features = [ + # Don't use the built-in xkb handling + "calloop", +] } +# Wayland dependencies +# Needed for supporting RawWindowHandle +wayland-backend = { version = "0.1.0", default_features = false, features = [ + "client_system", +], optional = true } + +[target.'cfg(any(target_os = "freebsd", target_os="linux", target_os="openbsd"))'.build-dependencies] +bindgen = { version = "0.66", optional = true } +pkg-config = { version = "0.3.25", optional = true } diff --git a/v2/build.rs b/v2/build.rs new file mode 100644 index 00000000..f6845d55 --- /dev/null +++ b/v2/build.rs @@ -0,0 +1,68 @@ +#[cfg(not(all( + any(feature = "x11", feature = "wayland"), + any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") +)))] +fn main() {} + +#[cfg(all( + any(feature = "x11", feature = "wayland"), + any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") +))] +fn main() { + use pkg_config::probe_library; + use std::env; + use std::path::PathBuf; + + let xkbcommon = 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", &header) + .clang_args( + xkbcommon + .include_paths + .iter() + .filter_map(|path| path.to_str().map(|s| format!("-I{s}"))), + ) + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .prepend_enum_name(false) + .size_t_is_usize(true) + .allowlist_function("xkb_.*") + .allowlist_type("xkb_.*") + .allowlist_var("XKB_.*") + .allowlist_type("xcb_connection_t") + // this needs var args + .blocklist_function("xkb_context_set_log_fn") + // we use FILE from libc + .blocklist_type("FILE") + .blocklist_type("va_list") + .default_enum_style(bindgen::EnumVariation::NewType { + is_bitfield: true, + is_global: false, + }) + .generate() + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/xkbcommon.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("xkbcommon_sys.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/v2/src/backend/mod.rs b/v2/src/backend/mod.rs index 6c902c41..24da0d7f 100644 --- a/v2/src/backend/mod.rs +++ b/v2/src/backend/mod.rs @@ -1,3 +1,4 @@ -mod v1; +mod shared; +mod wayland; -pub(crate) use v1::*; +pub use wayland::*; diff --git a/v2/src/backend/shared/keyboard.rs b/v2/src/backend/shared/keyboard.rs new file mode 100644 index 00000000..63972d7f --- /dev/null +++ b/v2/src/backend/shared/keyboard.rs @@ -0,0 +1,256 @@ +// 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. + +//! Keyboard logic that is shared by more than one backend. + +#[allow(unused)] +use keyboard_types::{Code, Location}; + +#[cfg(any( + all( + any(feature = "x11", feature = "wayland"), + any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") + ), + target_os = "macos" +))] +/// Map key code to location. +/// +/// The logic for this is adapted from InitKeyEvent in TextInputHandler (in the Mozilla +/// mac port). +/// +/// Note: in the original, this is based on kVK constants, but since we don't have those +/// readily available, we use the mapping to code (which should be effectively lossless). +pub fn code_to_location(code: Code) -> Location { + match code { + Code::MetaLeft | Code::ShiftLeft | Code::AltLeft | Code::ControlLeft => Location::Left, + Code::MetaRight | Code::ShiftRight | Code::AltRight | Code::ControlRight => Location::Right, + Code::Numpad0 + | Code::Numpad1 + | Code::Numpad2 + | Code::Numpad3 + | Code::Numpad4 + | Code::Numpad5 + | Code::Numpad6 + | Code::Numpad7 + | Code::Numpad8 + | Code::Numpad9 + | Code::NumpadAdd + | Code::NumpadComma + | Code::NumpadDecimal + | Code::NumpadDivide + | Code::NumpadEnter + | Code::NumpadEqual + | Code::NumpadMultiply + | Code::NumpadSubtract => Location::Numpad, + _ => Location::Standard, + } +} + +#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"))] +/// Map hardware keycode to code. +/// +/// In theory, the hardware keycode is device dependent, but in +/// practice it's probably pretty reliable. +/// +/// The logic is based on NativeKeyToDOMCodeName.h in Mozilla. +pub fn hardware_keycode_to_code(hw_keycode: u16) -> Code { + match hw_keycode { + 0x0009 => Code::Escape, + 0x000A => Code::Digit1, + 0x000B => Code::Digit2, + 0x000C => Code::Digit3, + 0x000D => Code::Digit4, + 0x000E => Code::Digit5, + 0x000F => Code::Digit6, + 0x0010 => Code::Digit7, + 0x0011 => Code::Digit8, + 0x0012 => Code::Digit9, + 0x0013 => Code::Digit0, + 0x0014 => Code::Minus, + 0x0015 => Code::Equal, + 0x0016 => Code::Backspace, + 0x0017 => Code::Tab, + 0x0018 => Code::KeyQ, + 0x0019 => Code::KeyW, + 0x001A => Code::KeyE, + 0x001B => Code::KeyR, + 0x001C => Code::KeyT, + 0x001D => Code::KeyY, + 0x001E => Code::KeyU, + 0x001F => Code::KeyI, + 0x0020 => Code::KeyO, + 0x0021 => Code::KeyP, + 0x0022 => Code::BracketLeft, + 0x0023 => Code::BracketRight, + 0x0024 => Code::Enter, + 0x0025 => Code::ControlLeft, + 0x0026 => Code::KeyA, + 0x0027 => Code::KeyS, + 0x0028 => Code::KeyD, + 0x0029 => Code::KeyF, + 0x002A => Code::KeyG, + 0x002B => Code::KeyH, + 0x002C => Code::KeyJ, + 0x002D => Code::KeyK, + 0x002E => Code::KeyL, + 0x002F => Code::Semicolon, + 0x0030 => Code::Quote, + 0x0031 => Code::Backquote, + 0x0032 => Code::ShiftLeft, + 0x0033 => Code::Backslash, + 0x0034 => Code::KeyZ, + 0x0035 => Code::KeyX, + 0x0036 => Code::KeyC, + 0x0037 => Code::KeyV, + 0x0038 => Code::KeyB, + 0x0039 => Code::KeyN, + 0x003A => Code::KeyM, + 0x003B => Code::Comma, + 0x003C => Code::Period, + 0x003D => Code::Slash, + 0x003E => Code::ShiftRight, + 0x003F => Code::NumpadMultiply, + 0x0040 => Code::AltLeft, + 0x0041 => Code::Space, + 0x0042 => Code::CapsLock, + 0x0043 => Code::F1, + 0x0044 => Code::F2, + 0x0045 => Code::F3, + 0x0046 => Code::F4, + 0x0047 => Code::F5, + 0x0048 => Code::F6, + 0x0049 => Code::F7, + 0x004A => Code::F8, + 0x004B => Code::F9, + 0x004C => Code::F10, + 0x004D => Code::NumLock, + 0x004E => Code::ScrollLock, + 0x004F => Code::Numpad7, + 0x0050 => Code::Numpad8, + 0x0051 => Code::Numpad9, + 0x0052 => Code::NumpadSubtract, + 0x0053 => Code::Numpad4, + 0x0054 => Code::Numpad5, + 0x0055 => Code::Numpad6, + 0x0056 => Code::NumpadAdd, + 0x0057 => Code::Numpad1, + 0x0058 => Code::Numpad2, + 0x0059 => Code::Numpad3, + 0x005A => Code::Numpad0, + 0x005B => Code::NumpadDecimal, + 0x005D => Code::Lang5, + 0x005E => Code::IntlBackslash, + 0x005F => Code::F11, + 0x0060 => Code::F12, + 0x0061 => Code::IntlRo, + 0x0062 => Code::Lang3, + 0x0063 => Code::Lang4, + 0x0064 => Code::Convert, + 0x0065 => Code::KanaMode, + 0x0066 => Code::NonConvert, + 0x0068 => Code::NumpadEnter, + 0x0069 => Code::ControlRight, + 0x006A => Code::NumpadDivide, + 0x006B => Code::PrintScreen, + 0x006C => Code::AltRight, + 0x006E => Code::Home, + 0x006F => Code::ArrowUp, + 0x0070 => Code::PageUp, + 0x0071 => Code::ArrowLeft, + 0x0072 => Code::ArrowRight, + 0x0073 => Code::End, + 0x0074 => Code::ArrowDown, + 0x0075 => Code::PageDown, + 0x0076 => Code::Insert, + 0x0077 => Code::Delete, + 0x0079 => Code::AudioVolumeMute, + 0x007A => Code::AudioVolumeDown, + 0x007B => Code::AudioVolumeUp, + 0x007C => Code::Power, + 0x007D => Code::NumpadEqual, + 0x007F => Code::Pause, + 0x0080 => Code::ShowAllWindows, + 0x0081 => Code::NumpadComma, + 0x0082 => Code::Lang1, + 0x0083 => Code::Lang2, + 0x0084 => Code::IntlYen, + 0x0085 => Code::MetaLeft, + 0x0086 => Code::MetaRight, + 0x0087 => Code::ContextMenu, + 0x0088 => Code::BrowserStop, + 0x0089 => Code::Again, + 0x008A => Code::Props, + 0x008B => Code::Undo, + 0x008C => Code::Select, + 0x008D => Code::Copy, + 0x008E => Code::Open, + 0x008F => Code::Paste, + 0x0090 => Code::Find, + 0x0091 => Code::Cut, + 0x0092 => Code::Help, + 0x0094 => Code::LaunchApp2, + 0x0096 => Code::Sleep, + 0x0097 => Code::WakeUp, + 0x0098 => Code::LaunchApp1, + // key to right of volume controls on T430s produces 0x9C + // but no documentation of what it should map to :/ + 0x00A3 => Code::LaunchMail, + 0x00A4 => Code::BrowserFavorites, + 0x00A6 => Code::BrowserBack, + 0x00A7 => Code::BrowserForward, + 0x00A9 => Code::Eject, + 0x00AB => Code::MediaTrackNext, + 0x00AC => Code::MediaPlayPause, + 0x00AD => Code::MediaTrackPrevious, + 0x00AE => Code::MediaStop, + 0x00AF => Code::MediaRecord, + 0x00B0 => Code::MediaRewind, + 0x00B3 => Code::MediaSelect, + 0x00B4 => Code::BrowserHome, + 0x00B5 => Code::BrowserRefresh, + 0x00BB => Code::NumpadParenLeft, + 0x00BC => Code::NumpadParenRight, + 0x00BF => Code::F13, + 0x00C0 => Code::F14, + 0x00C1 => Code::F15, + 0x00C2 => Code::F16, + 0x00C3 => Code::F17, + 0x00C4 => Code::F18, + 0x00C5 => Code::F19, + 0x00C6 => Code::F20, + 0x00C7 => Code::F21, + 0x00C8 => Code::F22, + 0x00C9 => Code::F23, + 0x00CA => Code::F24, + 0x00D1 => Code::MediaPause, + 0x00D7 => Code::MediaPlay, + 0x00D8 => Code::MediaFastForward, + 0x00E1 => Code::BrowserSearch, + 0x00E8 => Code::BrightnessDown, + 0x00E9 => Code::BrightnessUp, + 0x00EB => Code::DisplayToggleIntExt, + 0x00EF => Code::MailSend, + 0x00F0 => Code::MailReply, + 0x00F1 => Code::MailForward, + 0x0100 => Code::MicrophoneMuteToggle, + 0x017C => Code::ZoomToggle, + 0x024B => Code::LaunchControlPanel, + 0x024C => Code::SelectTask, + 0x024D => Code::LaunchScreenSaver, + 0x024F => Code::LaunchAssistant, + 0x0250 => Code::KeyboardLayoutSelect, + 0x0281 => Code::PrivacyScreenToggle, + _ => Code::Unidentified, + } +} diff --git a/v2/src/backend/shared/linux/env.rs b/v2/src/backend/shared/linux/env.rs new file mode 100644 index 00000000..59c7be20 --- /dev/null +++ b/v2/src/backend/shared/linux/env.rs @@ -0,0 +1,42 @@ +pub fn locale() -> String { + let mut locale = iso_locale(); + // This is done because the locale parsing library we use (TODO - do we?) 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 +} + +pub fn iso_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 + locale_env_var("LANGUAGE") + // the LANGUAGE value is priority list separated 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()) +} diff --git a/v2/src/backend/shared/linux/mod.rs b/v2/src/backend/shared/linux/mod.rs new file mode 100644 index 00000000..d40b15d9 --- /dev/null +++ b/v2/src/backend/shared/linux/mod.rs @@ -0,0 +1,2 @@ +// environment based utilities +pub mod env; diff --git a/v2/src/backend/shared/mod.rs b/v2/src/backend/shared/mod.rs new file mode 100644 index 00000000..e3960601 --- /dev/null +++ b/v2/src/backend/shared/mod.rs @@ -0,0 +1,35 @@ +// 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. + +//! Logic that is shared by more than one backend. + +cfg_if::cfg_if! { + if #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "linux", target_os = "openbsd"))] { + mod keyboard; + pub use keyboard::*; + } +} +cfg_if::cfg_if! { + if #[cfg(all(any(target_os = "freebsd", target_os = "linux"), any(feature = "x11", feature = "wayland")))] { + pub(crate) mod xkb; + pub(crate) mod linux; + } +} +cfg_if::cfg_if! { + if #[cfg(all(any(target_os = "freebsd", target_os = "linux"), any(feature = "x11")))] { + // TODO: This might also be used in Wayland, but we don't implement timers there yet + mod timer; + pub(crate) use timer::*; + } +} diff --git a/v2/src/backend/shared/timer.rs b/v2/src/backend/shared/timer.rs new file mode 100644 index 00000000..0d22589e --- /dev/null +++ b/v2/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/v2/src/backend/shared/xkb.rs b/v2/src/backend/shared/xkb.rs new file mode 100644 index 00000000..14d0a864 --- /dev/null +++ b/v2/src/backend/shared/xkb.rs @@ -0,0 +1,73 @@ +mod xkb_api; +pub use xkb_api::*; + +use glazier::{ + text::{simulate_compose, InputHandler}, + KeyEvent, +}; + +mod keycodes; +mod xkbcommon_sys; + +pub enum KeyboardHandled { + UpdatedReleasingCompose, + UpdatedClaimingCompose, + UpdatedRetainingCompose, + UpdatedNoCompose, + NoUpdate, +} + +/// Handle the results of a single keypress event +/// +/// `handler` must be a mutable lock, and must not be *owned* +/// by any other input methods. In particular, the composition region +/// must: +/// - Be `None` for the first call to this function +/// - Be `None` if [`KeyEventsState::cancel_composing`] +/// was called since the previous call to this function, as would occur +/// if another IME claimed the input field, or the focused field was changed +/// - Be the range previously set by the previous call to this function in all other cases +/// +/// If a different input method exists on the backend, it *must* +/// be removed from the input handler before calling this method +/// +/// Note that this does assume that if IME is in some sense *active*, +/// it consumes all keypresses. This is a correct assumption on Wayland[^consumes], +/// and we don't currently intend to implement X11 input methods ourselves. +/// +/// [^consumes]: The text input spec doesn't actually make this guarantee, but +/// it also provides no mechanism to mark a keypress as "pre-handled", so +/// in practice all implementations (probably) have to do so +pub fn xkb_simulate_input( + xkb_state: &mut KeyEventsState, + keysym: KeySym, + event: &KeyEvent, + // To handle composition, we have chosen to require being inside a text field + // This does mean that we don't get composition outside of a text field + // but that's expected, as there is no suitable `handler` method for that + // case. We get the same behaviour on macOS (?) + + // TODO: There are a few cases where this input lock doesn't need to be mutable (or exist at all) + // e.g. primarily for e.g. pressing control and other modifier keys + // It would require a significant rearchitecture to make it possible to not acquire the lock in + // that case, and this is only a minor inefficiency, but it's better to be sure + handler: &mut dyn InputHandler, +) -> KeyboardHandled { + let compose_result = xkb_state.compose_key_down(event, keysym); + let result_if_update_occurs = match compose_result { + glazier::text::CompositionResult::NoComposition => KeyboardHandled::UpdatedNoCompose, + glazier::text::CompositionResult::Cancelled(_) + | glazier::text::CompositionResult::Finished(_) => KeyboardHandled::UpdatedReleasingCompose, + glazier::text::CompositionResult::Updated { just_started, .. } if just_started => { + KeyboardHandled::UpdatedClaimingCompose + } + glazier::text::CompositionResult::Updated { .. } => { + KeyboardHandled::UpdatedRetainingCompose + } + }; + if simulate_compose(handler, event, compose_result) { + result_if_update_occurs + } else { + KeyboardHandled::NoUpdate + } +} diff --git a/v2/src/backend/shared/xkb/keycodes.rs b/v2/src/backend/shared/xkb/keycodes.rs new file mode 100644 index 00000000..3c6188d8 --- /dev/null +++ b/v2/src/backend/shared/xkb/keycodes.rs @@ -0,0 +1,252 @@ +#![allow(non_upper_case_globals)] + +use keyboard_types::Key; + +use super::{xkbcommon_sys::*, ComposeFeedSym}; + +/// Map from an xkb_common key code to a key, if possible. +pub fn map_key(keysym: u32) -> Key { + use Key::*; + match keysym { + 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, + XKB_KEY_Return | XKB_KEY_KP_Enter => Enter, + XKB_KEY_Linefeed => Enter, + XKB_KEY_Pause => Pause, + XKB_KEY_Scroll_Lock => ScrollLock, + XKB_KEY_Escape => Escape, + XKB_KEY_Multi_key => Compose, + XKB_KEY_Kanji => KanjiMode, + XKB_KEY_Muhenkan => NonConvert, + XKB_KEY_Henkan_Mode => Convert, + XKB_KEY_Romaji => Romaji, + XKB_KEY_Hiragana => Hiragana, + XKB_KEY_Katakana => Katakana, + XKB_KEY_Hiragana_Katakana => HiraganaKatakana, + XKB_KEY_Zenkaku => Zenkaku, + XKB_KEY_Hankaku => Hankaku, + XKB_KEY_Zenkaku_Hankaku => ZenkakuHankaku, + XKB_KEY_Kana_Lock => KanaMode, + XKB_KEY_Eisu_Shift | XKB_KEY_Eisu_toggle => Alphanumeric, + XKB_KEY_Hangul => HangulMode, + XKB_KEY_Hangul_Hanja => HanjaMode, + XKB_KEY_Codeinput => CodeInput, + XKB_KEY_SingleCandidate => SingleCandidate, + XKB_KEY_MultipleCandidate => AllCandidates, + XKB_KEY_PreviousCandidate => PreviousCandidate, + XKB_KEY_Home | XKB_KEY_KP_Home => Home, + XKB_KEY_Left | XKB_KEY_KP_Left => ArrowLeft, + XKB_KEY_Up | XKB_KEY_KP_Up => ArrowUp, + XKB_KEY_Right | XKB_KEY_KP_Right => ArrowRight, + XKB_KEY_Down | XKB_KEY_KP_Down => ArrowDown, + XKB_KEY_Prior | XKB_KEY_KP_Prior => PageUp, + XKB_KEY_Next | XKB_KEY_KP_Next | XKB_KEY_XF86ScrollDown => PageDown, + XKB_KEY_End | XKB_KEY_KP_End | XKB_KEY_XF86ScrollUp => End, + XKB_KEY_Select => Select, + // Treat Print/PrintScreen as PrintScreen https://crbug.com/683097. + XKB_KEY_Print | XKB_KEY_3270_PrintScreen => PrintScreen, + XKB_KEY_Execute => Execute, + XKB_KEY_Insert | XKB_KEY_KP_Insert => Insert, + XKB_KEY_Undo => Undo, + XKB_KEY_Redo => Redo, + XKB_KEY_Menu => ContextMenu, + XKB_KEY_Find => Find, + XKB_KEY_Cancel => Cancel, + XKB_KEY_Help => Help, + XKB_KEY_Break | XKB_KEY_3270_Attn => Attn, + XKB_KEY_Mode_switch => ModeChange, + XKB_KEY_Num_Lock => NumLock, + XKB_KEY_F1 | XKB_KEY_KP_F1 => F1, + XKB_KEY_F2 | XKB_KEY_KP_F2 => F2, + XKB_KEY_F3 | XKB_KEY_KP_F3 => F3, + XKB_KEY_F4 | XKB_KEY_KP_F4 => F4, + XKB_KEY_F5 => F5, + XKB_KEY_F6 => F6, + XKB_KEY_F7 => F7, + XKB_KEY_F8 => F8, + XKB_KEY_F9 => F9, + XKB_KEY_F10 => F10, + XKB_KEY_F11 => F11, + XKB_KEY_F12 => F12, + XKB_KEY_XF86Tools | XKB_KEY_F13 => F13, + XKB_KEY_F14 | XKB_KEY_XF86Launch5 => F14, + XKB_KEY_F15 | XKB_KEY_XF86Launch6 => F15, + XKB_KEY_F16 | XKB_KEY_XF86Launch7 => F16, + XKB_KEY_F17 | XKB_KEY_XF86Launch8 => F17, + XKB_KEY_F18 | XKB_KEY_XF86Launch9 => F18, + XKB_KEY_F19 => F19, + XKB_KEY_F20 => F20, + XKB_KEY_F21 => F21, + XKB_KEY_F22 => F22, + XKB_KEY_F23 => F23, + XKB_KEY_F24 => F24, + XKB_KEY_F25 => F25, + XKB_KEY_F26 => F26, + XKB_KEY_F27 => F27, + XKB_KEY_F28 => F28, + XKB_KEY_F29 => F29, + XKB_KEY_F30 => F30, + XKB_KEY_F31 => F31, + XKB_KEY_F32 => F32, + XKB_KEY_F33 => F33, + XKB_KEY_F34 => F34, + XKB_KEY_F35 => F35, + // not available in keyboard-types + // XKB_KEY_XF86Calculator => LaunchCalculator, + // XKB_KEY_XF86MyComputer | XKB_KEY_XF86Explorer => LaunchMyComputer, + // XKB_KEY_ISO_Level3_Latch => AltGraphLatch, + // XKB_KEY_ISO_Level5_Shift => ShiftLevel5, + XKB_KEY_Shift_L | XKB_KEY_Shift_R => Shift, + XKB_KEY_Control_L | XKB_KEY_Control_R => Control, + XKB_KEY_Caps_Lock => CapsLock, + XKB_KEY_Meta_L | XKB_KEY_Meta_R => Meta, + XKB_KEY_Alt_L | XKB_KEY_Alt_R => Alt, + XKB_KEY_Super_L | XKB_KEY_Super_R => Meta, + XKB_KEY_Hyper_L | XKB_KEY_Hyper_R => Hyper, + XKB_KEY_Delete => Delete, + XKB_KEY_SunProps => Props, + XKB_KEY_XF86Next_VMode => VideoModeNext, + XKB_KEY_XF86MonBrightnessUp => BrightnessUp, + XKB_KEY_XF86MonBrightnessDown => BrightnessDown, + XKB_KEY_XF86Standby | XKB_KEY_XF86Sleep | XKB_KEY_XF86Suspend => Standby, + XKB_KEY_XF86AudioLowerVolume => AudioVolumeDown, + XKB_KEY_XF86AudioMute => AudioVolumeMute, + XKB_KEY_XF86AudioRaiseVolume => AudioVolumeUp, + XKB_KEY_XF86AudioPlay => MediaPlayPause, + XKB_KEY_XF86AudioStop => MediaStop, + XKB_KEY_XF86AudioPrev => MediaTrackPrevious, + XKB_KEY_XF86AudioNext => MediaTrackNext, + XKB_KEY_XF86HomePage => BrowserHome, + XKB_KEY_XF86Mail => LaunchMail, + XKB_KEY_XF86Search => BrowserSearch, + XKB_KEY_XF86AudioRecord => MediaRecord, + XKB_KEY_XF86Calendar => LaunchCalendar, + XKB_KEY_XF86Back => BrowserBack, + XKB_KEY_XF86Forward => BrowserForward, + XKB_KEY_XF86Stop => BrowserStop, + XKB_KEY_XF86Refresh | XKB_KEY_XF86Reload => BrowserRefresh, + XKB_KEY_XF86PowerOff => PowerOff, + XKB_KEY_XF86WakeUp => WakeUp, + XKB_KEY_XF86Eject => Eject, + XKB_KEY_XF86ScreenSaver => LaunchScreenSaver, + XKB_KEY_XF86WWW => LaunchWebBrowser, + XKB_KEY_XF86Favorites => BrowserFavorites, + XKB_KEY_XF86AudioPause => MediaPause, + XKB_KEY_XF86AudioMedia | XKB_KEY_XF86Music => LaunchMusicPlayer, + XKB_KEY_XF86AudioRewind => MediaRewind, + XKB_KEY_XF86CD | XKB_KEY_XF86Video => LaunchMediaPlayer, + XKB_KEY_XF86Close => Close, + XKB_KEY_XF86Copy | XKB_KEY_SunCopy => Copy, + XKB_KEY_XF86Cut | XKB_KEY_SunCut => Cut, + XKB_KEY_XF86Display => DisplaySwap, + XKB_KEY_XF86Excel => LaunchSpreadsheet, + XKB_KEY_XF86LogOff => LogOff, + XKB_KEY_XF86New => New, + XKB_KEY_XF86Open | XKB_KEY_SunOpen => Open, + XKB_KEY_XF86Paste | XKB_KEY_SunPaste => Paste, + XKB_KEY_XF86Reply => MailReply, + XKB_KEY_XF86Save => Save, + XKB_KEY_XF86Send => MailSend, + XKB_KEY_XF86Spell => SpellCheck, + XKB_KEY_XF86SplitScreen => SplitScreenToggle, + XKB_KEY_XF86Word | XKB_KEY_XF86OfficeHome => LaunchWordProcessor, + XKB_KEY_XF86ZoomIn => ZoomIn, + XKB_KEY_XF86ZoomOut => ZoomOut, + XKB_KEY_XF86WebCam => LaunchWebCam, + XKB_KEY_XF86MailForward => MailForward, + XKB_KEY_XF86AudioForward => MediaFastForward, + XKB_KEY_XF86AudioRandomPlay => RandomToggle, + XKB_KEY_XF86Subtitle => Subtitle, + XKB_KEY_XF86Hibernate => Hibernate, + XKB_KEY_3270_EraseEOF => EraseEof, + XKB_KEY_3270_Play => Play, + XKB_KEY_3270_ExSelect => ExSel, + XKB_KEY_3270_CursorSelect => CrSel, + XKB_KEY_ISO_Level3_Shift => AltGraph, + XKB_KEY_ISO_Next_Group => GroupNext, + XKB_KEY_ISO_Prev_Group => GroupPrevious, + XKB_KEY_ISO_First_Group => GroupFirst, + XKB_KEY_ISO_Last_Group => GroupLast, + XKB_KEY_dead_grave + | XKB_KEY_dead_acute + | XKB_KEY_dead_circumflex + | XKB_KEY_dead_tilde + | XKB_KEY_dead_macron + | XKB_KEY_dead_breve + | XKB_KEY_dead_abovedot + | XKB_KEY_dead_diaeresis + | XKB_KEY_dead_abovering + | XKB_KEY_dead_doubleacute + | XKB_KEY_dead_caron + | XKB_KEY_dead_cedilla + | XKB_KEY_dead_ogonek + | XKB_KEY_dead_iota + | XKB_KEY_dead_voiced_sound + | XKB_KEY_dead_semivoiced_sound + | XKB_KEY_dead_belowdot + | XKB_KEY_dead_hook + | XKB_KEY_dead_horn + | XKB_KEY_dead_stroke + | XKB_KEY_dead_abovecomma + | XKB_KEY_dead_abovereversedcomma + | XKB_KEY_dead_doublegrave + | XKB_KEY_dead_belowring + | XKB_KEY_dead_belowmacron + | XKB_KEY_dead_belowcircumflex + | XKB_KEY_dead_belowtilde + | XKB_KEY_dead_belowbreve + | XKB_KEY_dead_belowdiaeresis + | XKB_KEY_dead_invertedbreve + | XKB_KEY_dead_belowcomma + | XKB_KEY_dead_currency + | XKB_KEY_dead_greek => Dead, + _ => Unidentified, + } +} + +pub(super) fn map_for_compose(keysym: u32) -> Option { + use ComposeFeedSym::*; + let sym = match keysym { + XKB_KEY_dead_grave => DeadGrave, + XKB_KEY_dead_acute => DeadAcute, + XKB_KEY_dead_circumflex => DeadCircumflex, + XKB_KEY_dead_tilde => DeadTilde, + XKB_KEY_dead_macron => DeadMacron, + XKB_KEY_dead_breve => DeadBreve, + XKB_KEY_dead_abovedot => DeadAbovedot, + XKB_KEY_dead_diaeresis => DeadDiaeresis, + XKB_KEY_dead_abovering => DeadAbovering, + XKB_KEY_dead_doubleacute => DeadDoubleacute, + XKB_KEY_dead_caron => DeadCaron, + XKB_KEY_dead_cedilla => DeadCedilla, + XKB_KEY_dead_ogonek => DeadOgonek, + XKB_KEY_dead_iota => DeadIota, + XKB_KEY_dead_voiced_sound => DeadVoicedSound, + XKB_KEY_dead_semivoiced_sound => DeadSemivoicedSound, + XKB_KEY_dead_belowdot => DeadBelowdot, + XKB_KEY_dead_hook => DeadHook, + XKB_KEY_dead_horn => DeadHorn, + XKB_KEY_dead_stroke => DeadStroke, + XKB_KEY_dead_abovecomma => DeadAbovecomma, + XKB_KEY_dead_abovereversedcomma => DeadAbovereversedcomma, + XKB_KEY_dead_doublegrave => DeadDoublegrave, + XKB_KEY_dead_belowring => DeadBelowring, + XKB_KEY_dead_belowmacron => DeadBelowmacron, + XKB_KEY_dead_belowcircumflex => DeadBelowcircumflex, + XKB_KEY_dead_belowtilde => DeadBelowtilde, + XKB_KEY_dead_belowbreve => DeadBelowbreve, + XKB_KEY_dead_belowdiaeresis => DeadBelowdiaeresis, + XKB_KEY_dead_invertedbreve => DeadInvertedbreve, + XKB_KEY_dead_belowcomma => DeadBelowcomma, + XKB_KEY_dead_currency => DeadCurrency, + XKB_KEY_dead_greek => DeadGreek, + XKB_KEY_Multi_key => Compose, + _ => return None, + }; + Some(sym) +} + +pub fn is_backspace(keysym: u32) -> bool { + keysym == XKB_KEY_BackSpace +} diff --git a/v2/src/backend/shared/xkb/xkb_api.rs b/v2/src/backend/shared/xkb/xkb_api.rs new file mode 100644 index 00000000..9ef0c696 --- /dev/null +++ b/v2/src/backend/shared/xkb/xkb_api.rs @@ -0,0 +1,714 @@ +// Copyright 2021 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. + +//! A minimal wrapper around Xkb for our use. + +use super::{keycodes, xkbcommon_sys::*}; +use crate::backend::shared::{code_to_location, hardware_keycode_to_code, linux}; +use glazier::{text::CompositionResult, KeyEvent, KeyState, Modifiers}; +use keyboard_types::{Code, Key}; +use std::{convert::TryFrom, ffi::CString}; +use std::{os::raw::c_char, ptr::NonNull}; + +#[cfg(feature = "x11")] +use x11rb::xcb_ffi::XCBConnection; + +use super::keycodes::{is_backspace, map_for_compose}; + +#[cfg(feature = "x11")] +pub struct DeviceId(pub std::os::raw::c_int); + +/// A global xkb context object. +/// +/// Reference counted under the hood. +// Assume this isn't threadsafe unless proved otherwise. (e.g. don't implement Send/Sync) +// Safety: Is a valid xkb_context +pub struct Context(*mut xkb_context); + +impl Context { + /// Create a new xkb context. + /// + /// The returned object is lightweight and clones will point at the same context internally. + pub fn new() -> Self { + // Safety: No given preconditions + let ctx = unsafe { xkb_context_new(xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; + if ctx.is_null() { + // No failure conditions are enumerated, so this should be impossible + panic!("Could not create an xkbcommon Context"); + } + // Safety: xkb_context_new returns a valid + Self(ctx) + } + + #[cfg(feature = "x11")] + pub fn core_keyboard_device_id(&self, conn: &XCBConnection) -> Option { + let id = unsafe { + xkb_x11_get_core_keyboard_device_id( + conn.get_raw_xcb_connection() as *mut xcb_connection_t + ) + }; + if id != -1 { + Some(DeviceId(id)) + } else { + None + } + } + + #[cfg(feature = "x11")] + pub fn keymap_from_x11_device( + &self, + conn: &XCBConnection, + device: &DeviceId, + ) -> Option { + let key_map = unsafe { + xkb_x11_keymap_new_from_device( + self.0, + conn.get_raw_xcb_connection() as *mut xcb_connection_t, + device.0, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + if key_map.is_null() { + return None; + } + Some(Keymap(key_map)) + } + + #[cfg(feature = "x11")] + pub fn state_from_x11_keymap( + &mut self, + keymap: &Keymap, + conn: &XCBConnection, + device: &DeviceId, + ) -> Option { + let state = unsafe { + xkb_x11_state_new_from_device( + keymap.0, + conn.get_raw_xcb_connection() as *mut xcb_connection_t, + device.0, + ) + }; + if state.is_null() { + return None; + } + Some(self.keyboard_state(keymap, state)) + } + + #[cfg(feature = "wayland")] + pub fn state_from_keymap(&mut self, keymap: &Keymap) -> Option { + let state = unsafe { xkb_state_new(keymap.0) }; + if state.is_null() { + return None; + } + Some(self.keyboard_state(keymap, state)) + } + /// 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::XKB_KEYMAP_FORMAT_TEXT_V1, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + assert!(!keymap.is_null()); + Keymap(keymap) + } + } + + fn keyboard_state(&mut self, keymap: &Keymap, state: *mut xkb_state) -> KeyEventsState { + let keymap = keymap.0; + // let mod_count = unsafe { xkb_keymap_num_mods(keymap) }; + // for idx in 0..mod_count { + // let name = unsafe { xkb_keymap_mod_get_name(keymap, idx) }; + // let str = unsafe { CStr::from_ptr(name) }; + // println!("{:?}", str); + // } + let mod_idx = |str: &'static [u8]| unsafe { + xkb_keymap_mod_get_index(keymap, str.as_ptr() as *mut c_char) + }; + KeyEventsState { + mods_state: state, + mod_indices: ModsIndices { + control: mod_idx(XKB_MOD_NAME_CTRL), + shift: mod_idx(XKB_MOD_NAME_SHIFT), + alt: mod_idx(XKB_MOD_NAME_ALT), + super_: mod_idx(XKB_MOD_NAME_LOGO), + caps_lock: mod_idx(XKB_MOD_NAME_CAPS), + num_lock: mod_idx(XKB_MOD_NAME_NUM), + }, + active_mods: Modifiers::empty(), + compose_state: self.compose_state(), + is_composing: false, + compose_sequence: vec![], + compose_string: String::with_capacity(16), + previous_was_compose: false, + } + } + fn compose_state(&mut self) -> Option> { + let locale = linux::env::iso_locale(); + let locale = CString::new(locale).unwrap(); + // Safety: Self is a valid context + // Locale is a C string, which (although it isn't documented as such), we have to assume is the preconditon + let table = unsafe { + xkb_compose_table_new_from_locale( + self.0, + locale.as_ptr(), + xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; + if table.is_null() { + return None; + } + let state = unsafe { + xkb_compose_state_new(table, xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS) + }; + unsafe { xkb_compose_table_unref(table) }; + NonNull::new(state) + } +} + +impl Drop for Context { + fn drop(&mut self) { + unsafe { + xkb_context_unref(self.0); + } + } +} + +pub struct Keymap(*mut xkb_keymap); + +impl Keymap { + #[cfg(feature = "wayland")] + /// Whether the given key should repeat + pub fn repeats(&mut self, scancode: u32) -> bool { + unsafe { xkb_keymap_key_repeats(self.0, scancode) == 1 } + } +} + +impl Drop for Keymap { + fn drop(&mut self) { + unsafe { + xkb_keymap_unref(self.0); + } + } +} + +pub struct KeyEventsState { + mods_state: *mut xkb_state, + mod_indices: ModsIndices, + compose_state: Option>, + active_mods: Modifiers, + is_composing: bool, + compose_sequence: Vec, + compose_string: String, + previous_was_compose: bool, +} + +#[derive(Clone, Copy, Debug)] +pub struct ModsIndices { + control: xkb_mod_index_t, + shift: xkb_mod_index_t, + alt: xkb_mod_index_t, + super_: xkb_mod_index_t, + caps_lock: xkb_mod_index_t, + num_lock: xkb_mod_index_t, +} + +#[derive(Clone, Copy, Debug)] +pub struct ActiveModifiers { + pub base_mods: xkb_mod_mask_t, + pub latched_mods: xkb_mod_mask_t, + pub locked_mods: xkb_mod_mask_t, + pub base_layout: xkb_layout_index_t, + pub latched_layout: xkb_layout_index_t, + pub locked_layout: xkb_layout_index_t, +} + +#[derive(Copy, Clone)] +/// An opaque representation of a KeySym, to make APIs less error prone +pub struct KeySym(xkb_keysym_t); + +impl KeyEventsState { + /// Stop the active composition. + /// This should happen if the text field changes, or the selection within the text field changes + /// or the IME is activated + pub fn cancel_composing(&mut self) -> bool { + let was_composing = self.is_composing; + self.is_composing = false; + if let Some(state) = self.compose_state { + unsafe { xkb_compose_state_reset(state.as_ptr()) } + } + was_composing + } + + pub fn update_xkb_state(&mut self, mods: ActiveModifiers) { + unsafe { + xkb_state_update_mask( + self.mods_state, + mods.base_mods, + mods.latched_mods, + mods.locked_mods, + mods.base_layout, + mods.latched_layout, + mods.locked_layout, + ); + let mut mods = Modifiers::empty(); + for (idx, mod_) in [ + (self.mod_indices.control, Modifiers::CONTROL), + (self.mod_indices.shift, Modifiers::SHIFT), + (self.mod_indices.super_, Modifiers::SUPER), + (self.mod_indices.alt, Modifiers::ALT), + (self.mod_indices.caps_lock, Modifiers::CAPS_LOCK), + (self.mod_indices.num_lock, Modifiers::NUM_LOCK), + ] { + if xkb_state_mod_index_is_active( + self.mods_state, + idx, + xkb_state_component::XKB_STATE_MODS_EFFECTIVE, + ) != 0 + { + mods |= mod_; + } + } + self.active_mods = mods; + }; + } + + /// For an explanation of how our compose/dead key handling operates, see + /// the documentation of [`crate::text::simulate_compose`] + /// + /// This method calculates the key event which is passed to the `key_down` handler. + /// This is step "0" if that process + pub fn key_event( + &mut self, + scancode: u32, + keysym: KeySym, + state: KeyState, + repeat: bool, + ) -> KeyEvent { + // TODO: This shouldn't be repeated + let code = u16::try_from(scancode) + .map(hardware_keycode_to_code) + .unwrap_or(Code::Unidentified); + // TODO this is lazy - really should use xkb i.e. augment the get_logical_key method. + // TODO: How? + let location = code_to_location(code); + let key = Self::get_logical_key(keysym); + + let mut event = KeyEvent::default(); + + event.state = state; + event.key = key; + event.code = code; + event.location = location; + event.mods = self.active_mods; + event.repeat = repeat; + event.is_composing = self.is_composing; + event + } + + /// Alert the composition pipeline of a new key down event + /// + /// Should only be called if we're currently in a text input field. + /// This will calculate: + /// - Whether composition is active + /// - If so, what the new composition range displayed to + /// the user should be (and if it changed) + /// - If composition finished, what the inserted string should be + /// - Otherwise, does nothing + pub(crate) fn compose_key_down<'a>( + &'a mut self, + event: &KeyEvent, + keysym: KeySym, + ) -> CompositionResult<'a> { + let Some(compose_state) = self.compose_state else { + assert!(!self.is_composing); + // If we couldn't make a compose map, there's nothing to do + return CompositionResult::NoComposition; + }; + // If we were going to do any custom compose kinds, here would be the place to inject them + // E.g. for unicode characters as in GTK + if self.is_composing && is_backspace(keysym.0) { + return self.compose_handle_backspace(compose_state); + } + let feed_result = unsafe { xkb_compose_state_feed(compose_state.as_ptr(), keysym.0) }; + if feed_result == xkb_compose_feed_result::XKB_COMPOSE_FEED_IGNORED { + return CompositionResult::NoComposition; + } + + debug_assert_eq!( + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED, + feed_result + ); + + let status = unsafe { xkb_compose_state_get_status(compose_state.as_ptr()) }; + match status { + xkb_compose_status::XKB_COMPOSE_COMPOSING => { + let just_started = !self.is_composing; + if just_started { + self.compose_string.clear(); + self.compose_sequence.clear(); + self.previous_was_compose = false; + self.is_composing = true; + } + if self.previous_was_compose { + let _popped = self.compose_string.pop(); + debug_assert_eq!(_popped, Some('·')); + self.previous_was_compose = false; + } + Self::append_key_to_compose( + &mut self.compose_string, + keysym, + true, + &mut self.previous_was_compose, + Some(&event.key), + ); + self.compose_sequence.push(keysym); + CompositionResult::Updated { + text: &self.compose_string, + just_started, + } + } + xkb_compose_status::XKB_COMPOSE_COMPOSED => { + self.compose_string.clear(); + self.is_composing = false; + let result_keysym = + unsafe { xkb_compose_state_get_one_sym(compose_state.as_ptr()) }; + if result_keysym != 0 { + let result = Self::key_get_char(KeySym(result_keysym)); + if let Some(chr) = result { + self.compose_string.push(chr); + return CompositionResult::Finished(&self.compose_string); + } else { + tracing::warn!("Got a keysym without a unicode representation from xkb_compose_state_get_one_sym"); + } + } + // Ideally we'd have followed the happy path above, where composition results in + // a single unicode codepoint. But unfortunately, we need to use xkb_compose_state_get_utf8, + // which is a C API dealing with strings, and so is incredibly awkward. + // To handle this API, we need to pass in a buffer + // So as to minimise allocations, first we try with an array which should definitely be big enough + // The type of this buffer can safely be u8, as c_char is u8 on all platforms (supported by Rust) + if false { + // We assert that u8 and c_char are the same size for the casts below + let _test_valid = std::mem::transmute::; + } + let mut stack_buffer: [u8; 32] = Default::default(); + let capacity = stack_buffer.len(); + // Safety: We properly report the number of available elements to libxkbcommon + // Safety: We assume that libxkbcommon is somewhat sane, and therefore doesn't write + // uninitialised elements into the passed in buffer, and that + // "The number of bytes required for the string" is the number of bytes in the string + // The current implementation falls back to snprintf, which does make these guarantees, + // so we just hope for the best + let result_string_len = unsafe { + xkb_compose_state_get_utf8( + compose_state.as_ptr(), + stack_buffer.as_mut_ptr().cast(), + capacity, + ) + }; + if result_string_len < 0 { + // xkbcommon documents no case where this would be the case + // peeking into the implementation, this could occur if snprint has + // "encoding errors". This is just a safety valve + unreachable!(); + } + // The number of items needed in the buffer, as reported by + // xkb_compose_state_get_utf8. This excludes the null byte, + // but room is needed for the null byte + let non_null_bytes = result_string_len as usize; + // Truncation has occured if the needed size is greater than or equal to the capacity + if non_null_bytes < capacity { + let from_utf = std::str::from_utf8(&stack_buffer[..result_string_len as usize]) + .expect("libxkbcommon should have given valid utf8"); + self.compose_string.clear(); + self.compose_string.push_str(from_utf); + } else { + // Re-use the compose_string buffer for this, to avoid allocating on each compose + let mut buffer = std::mem::take(&mut self.compose_string).into_bytes(); + // The buffer is already empty, reserve space for the needed items and the null byte + buffer.reserve(non_null_bytes + 1); + let new_result_size = unsafe { + xkb_compose_state_get_utf8( + compose_state.as_ptr(), + buffer.as_mut_ptr().cast(), + non_null_bytes + 1, + ) + }; + assert_eq!(new_result_size, result_string_len); + // Safety: We assume/know that xkb_compose_state_get_utf8 wrote new_result_size items + // which we know is greater than 0. Note that we exclude the null byte here + unsafe { buffer.set_len(non_null_bytes) }; + let result = String::from_utf8(buffer) + .expect("libxkbcommon should have given valid utf8"); + self.compose_string = result; + } + CompositionResult::Finished(&self.compose_string) + } + xkb_compose_status::XKB_COMPOSE_CANCELLED => { + CompositionResult::Cancelled(self.cancelled_string()) + } + xkb_compose_status::XKB_COMPOSE_NOTHING => { + assert!(!self.is_composing); + // This is technically out-of-spec. xkbcommon documents that xkb_compose_state_get_status + // returns ..._ACCEPTED when "The keysym started, advanced or cancelled a sequence" + // which isn't the case when we're in "nothing". However, we have to work with the + // actually implemented version, which sends accepted even when the keysym didn't start + // a sequence + CompositionResult::NoComposition + } + _ => unreachable!(), + } + } + + pub fn cancelled_string(&mut self) -> &str { + // Clearing the compose string and other state isn't needed, + // as it is cleared at the start of the next composition + self.is_composing = false; + if self.previous_was_compose { + self.compose_string.pop(); + } + &self.compose_string + } + + fn compose_handle_backspace( + &mut self, + compose_state: NonNull, + ) -> CompositionResult<'_> { + if let Some(state) = self.compose_state { + unsafe { xkb_compose_state_reset(state.as_ptr()) } + } + self.compose_sequence.pop(); + if self.compose_sequence.is_empty() { + self.is_composing = false; + // This is not cancelled, but finished, because cancelled would replay the backspace a second time + return CompositionResult::Finished(""); + } + let compose_sequence = std::mem::take(&mut self.compose_sequence); + let mut compose_string = std::mem::take(&mut self.compose_string); + compose_string.clear(); + let last_index = compose_sequence.len() - 1; + let mut last_is_compose = false; + for (i, keysym) in compose_sequence.iter().cloned().enumerate() { + Self::append_key_to_compose( + &mut compose_string, + keysym, + i == last_index, + &mut last_is_compose, + None, + ); + let feed_result = unsafe { xkb_compose_state_feed(compose_state.as_ptr(), keysym.0) }; + debug_assert_eq!( + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED, + feed_result, + "Should only be storing accepted feed results" + ); + } + self.compose_sequence = compose_sequence; + self.previous_was_compose = last_is_compose; + self.compose_string = compose_string; + CompositionResult::Updated { + text: &self.compose_string, + just_started: false, + } + } + + fn append_key_to_compose( + compose_string: &mut String, + keysym: KeySym, + is_last: bool, + last_is_compose: &mut bool, + key: Option<&Key>, + ) { + if let Some(special) = map_for_compose(keysym.0) { + special.append_to(compose_string, is_last, last_is_compose); + return; + } + let key_temp; + let key = if let Some(key) = key { + key + } else { + key_temp = Self::get_logical_key(keysym); + &key_temp + }; + match key { + Key::Character(it) => compose_string.push_str(it), + it => { + tracing::warn!( + ?it, + "got unexpected key as a non-cancelling part of a compose" + ) + // Do nothing for other keys. This should generally be unreachable anyway + } + } + } + + fn get_logical_key(keysym: KeySym) -> Key { + let mut key = keycodes::map_key(keysym.0); + if matches!(key, Key::Unidentified) { + if let Some(chr) = Self::key_get_char(keysym) { + // TODO `keyboard_types` forces us to return a String, but it would be nicer if we could stay + // on the stack, especially since we know all results will only contain 1 unicode codepoint + key = Key::Character(String::from(chr)); + } + } + key + } + + /// Get the single (opaque) KeySym the given scan + pub fn get_one_sym(&mut self, scancode: u32) -> KeySym { + // TODO: We should use xkb_state_key_get_syms here (returning &'keymap [*const xkb_keysym_t]) + // but that is complicated slightly by the fact that we'd need to implement our own + // capitalisation transform + KeySym(unsafe { xkb_state_key_get_one_sym(self.mods_state, scancode) }) + } + + /// Get the string representation of a key. + fn key_get_char(keysym: KeySym) -> Option { + // We convert the keysym to a string directly, rather than using the XKB state function + // because (experimentally) [UI Events Keyboard Events](https://www.w3.org/TR/uievents-key/#key-attribute-value) + // use the symbol rather than the x11 string (which includes the ctrl KeySym transformation) + // If we used the KeySym transformation, it would not be possible to use keyboard shortcuts containing the + // control key, for example + let chr = unsafe { xkb_keysym_to_utf32(keysym.0) }; + if chr == 0 { + // There is no unicode representation of this symbol + return None; + } + let chr = char::from_u32(chr).expect("xkb should give valid UTF-32 char"); + Some(chr) + } +} + +impl Drop for KeyEventsState { + fn drop(&mut self) { + unsafe { + xkb_state_unref(self.mods_state); + if let Some(compose) = self.compose_state { + xkb_compose_state_unref(compose.as_ptr()); + } + } + } +} + +/// A keysym which gets special printing in our compose handling +pub(super) enum ComposeFeedSym { + DeadGrave, + DeadAcute, + DeadCircumflex, + DeadTilde, + DeadMacron, + DeadBreve, + DeadAbovedot, + DeadDiaeresis, + DeadAbovering, + DeadDoubleacute, + DeadCaron, + DeadCedilla, + DeadOgonek, + DeadIota, + DeadVoicedSound, + DeadSemivoicedSound, + DeadBelowdot, + DeadHook, + DeadHorn, + DeadStroke, + DeadAbovecomma, + DeadAbovereversedcomma, + DeadDoublegrave, + DeadBelowring, + DeadBelowmacron, + DeadBelowcircumflex, + DeadBelowtilde, + DeadBelowbreve, + DeadBelowdiaeresis, + DeadInvertedbreve, + DeadBelowcomma, + DeadCurrency, + DeadGreek, + + Compose, +} + +impl ComposeFeedSym { + fn append_to(self, string: &mut String, is_last: bool, last_is_compose: &mut bool) { + let char = match self { + ComposeFeedSym::Compose => { + if is_last { + *last_is_compose = true; + '·' + } else { + return; + } + } + ComposeFeedSym::DeadVoicedSound => '゛', + ComposeFeedSym::DeadTilde => '~', // asciitilde # TILDE + ComposeFeedSym::DeadAcute => '´', // acute # ACUTE ACCENT + ComposeFeedSym::DeadGrave => '`', // grave # GRAVE ACCENT + ComposeFeedSym::DeadCircumflex => '^', // asciicircum # CIRCUMFLEX ACCENT + ComposeFeedSym::DeadAbovering => '°', // degree # DEGREE SIGN + ComposeFeedSym::DeadMacron => '¯', // macron # MACRON + ComposeFeedSym::DeadBreve => '˘', // breve # BREVE + ComposeFeedSym::DeadAbovedot => '˙', // abovedot # DOT ABOVE + ComposeFeedSym::DeadDiaeresis => '¨', // diaeresis # DIAERESIS + ComposeFeedSym::DeadDoubleacute => '˝', // U2dd # DOUBLE ACUTE ACCENT + ComposeFeedSym::DeadCaron => 'ˇ', // caron # CARON + ComposeFeedSym::DeadCedilla => '¸', // cedilla # CEDILLA + ComposeFeedSym::DeadOgonek => '˛', // ogonek # OGONEK + ComposeFeedSym::DeadIota => 'ͺ', // U37a # GREEK YPOGEGRAMMENI + ComposeFeedSym::DeadBelowcomma => ',', // comma # COMMA + ComposeFeedSym::DeadCurrency => '¤', // currency # CURRENCY SIGN + ComposeFeedSym::DeadGreek => 'µ', // U00B5 # MICRO SIGN + ComposeFeedSym::DeadStroke => '/', // slash # SOLIDUS + ComposeFeedSym::DeadSemivoicedSound => '゜', + // These two dead keys appear to not be used in any + // of the default compose keymaps, and their names aren't clear what they represent + // Since these are only display versions, we just use acute and grave accents again, + // as these seem to describe those + ComposeFeedSym::DeadAbovecomma => '´', + ComposeFeedSym::DeadAbovereversedcomma => '`', + ComposeFeedSym::DeadBelowring => '˳', + ComposeFeedSym::DeadBelowmacron => 'ˍ', + ComposeFeedSym::DeadBelowcircumflex => '‸', + ComposeFeedSym::DeadBelowtilde => '˷', + // There is no non-combining dot below, so we use the combining version with a circle + ComposeFeedSym::DeadBelowdot => return string.push_str("◌̣"), //U0323 # COMBINING DOT BELOW + // There is no non-combining hook above + ComposeFeedSym::DeadHook => return string.push_str("◌̉"), //U0309 # COMBINING HOOK ABOVE + // There is no non-combining horn + ComposeFeedSym::DeadHorn => return string.push_str("◌̛"), //U031B # COMBINING HORN + // There is no non-combining double grave + ComposeFeedSym::DeadDoublegrave => return string.push_str("◌̏"), + // There is no non-combining breve below + ComposeFeedSym::DeadBelowbreve => return string.push_str("◌̮"), + // There is no non-combining diaeresis below + ComposeFeedSym::DeadBelowdiaeresis => return string.push_str("◌̤"), + // There is no non-combining inverted breve + ComposeFeedSym::DeadInvertedbreve => return string.push_str("◌̑"), + }; + string.push(char); + } +} diff --git a/v2/src/backend/shared/xkb/xkbcommon_sys.rs b/v2/src/backend/shared/xkb/xkbcommon_sys.rs new file mode 100644 index 00000000..9ba9b4cb --- /dev/null +++ b/v2/src/backend/shared/xkb/xkbcommon_sys.rs @@ -0,0 +1,4 @@ +#![allow(unused, non_upper_case_globals, non_camel_case_types, non_snake_case)] + +use nix::libc::FILE; +include!(concat!(env!("OUT_DIR"), "/xkbcommon_sys.rs")); diff --git a/v2/src/backend/v1.rs b/v2/src/backend/v1.rs index e7d39791..2a1f406f 100644 --- a/v2/src/backend/v1.rs +++ b/v2/src/backend/v1.rs @@ -42,14 +42,31 @@ struct V1WindowHandler { window: WindowId, } +impl V1WindowHandler { + fn with_glz( + &mut self, + f: impl FnOnce(&mut Box, Glazier, WindowId) -> R, + ) -> R { + with_glz(&self.state, |handler, glz| f(handler, glz, self.window)) + } +} + impl WinHandler for V1WindowHandler { fn connect(&mut self, _: &glazier::WindowHandle) { - // No need to do anything here? + self.with_glz(|handler, glz, win| handler.surface_available(glz, win)) + } + + fn prepare_paint(&mut self) { + self.with_glz(|handler, glz, win| handler.prepare_paint(glz, win)) } - fn prepare_paint(&mut self) {} + fn paint(&mut self, invalid: &glazier::Region) { + self.with_glz(|handler, glz, win| handler.paint(glz, win, invalid)) + } - fn paint(&mut self, invalid: &glazier::Region) {} + fn command(&mut self, id: u32) { + self.with_glz(|handler, glz, win| handler.menu_item_selected(glz, win, id)) + } fn as_any(&mut self) -> &mut dyn std::any::Any { self diff --git a/v2/src/backend/wayland/.README.md b/v2/src/backend/wayland/.README.md new file mode 100644 index 00000000..8cbb7baa --- /dev/null +++ b/v2/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/v2/src/backend/wayland/error.rs b/v2/src/backend/wayland/error.rs new file mode 100644 index 00000000..8e76435f --- /dev/null +++ b/v2/src/backend/wayland/error.rs @@ -0,0 +1,59 @@ +// 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 errors + +use std::fmt; + +use smithay_client_toolkit::reexports::{ + calloop, + client::{globals::BindError, ConnectError}, +}; + +#[derive(Debug)] +pub enum Error { + Connect(ConnectError), + Bind(BindError), + Calloop(calloop::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self { + Error::Connect(e) => write!(f, "could not connect to the wayland server: {e:}"), + Error::Bind(e) => write!(f, "could not bind a wayland global: {e:}"), + Error::Calloop(e) => write!(f, "calloop failed: {e:}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(value: ConnectError) -> Self { + Self::Connect(value) + } +} + +impl From for Error { + fn from(value: BindError) -> Self { + Self::Bind(value) + } +} + +impl From for Error { + fn from(value: calloop::Error) -> Self { + Self::Calloop(value) + } +} diff --git a/v2/src/backend/wayland/input/keyboard.rs b/v2/src/backend/wayland/input/keyboard.rs new file mode 100644 index 00000000..b231f5a2 --- /dev/null +++ b/v2/src/backend/wayland/input/keyboard.rs @@ -0,0 +1,316 @@ +use std::os::fd::AsRawFd; + +use crate::backend::{ + shared::xkb::{ActiveModifiers, KeyEventsState, Keymap}, + wayland::{window::WindowId, WaylandPlatform}, +}; + +use super::{input_state, SeatInfo, SeatName, WaylandState}; +use instant::Duration; +use keyboard_types::KeyState; +use smithay_client_toolkit::reexports::{ + calloop::{ + timer::{TimeoutAction, Timer}, + LoopHandle, RegistrationToken, + }, + client::{ + protocol::{ + wl_keyboard::{self, KeymapFormat}, + wl_seat, + }, + Connection, Dispatch, Proxy, QueueHandle, WEnum, + }, +}; + +mod mmap; + +/// The rate at which a pressed key is repeated. +/// +/// Taken from smithay-client-toolkit's xkbcommon feature +#[derive(Debug, Clone, Copy)] +pub enum RepeatInfo { + /// Keys will be repeated at the specified rate and delay. + Repeat { + /// The time between each repetition + rate: Duration, + + /// Delay (in milliseconds) between a key press and the start of repetition. + delay: u32, + }, + + /// Keys should not be repeated. + Disable, +} + +/// The seat identifier of this keyboard +struct KeyboardUserData(SeatName); + +pub(super) struct KeyboardState { + pub(super) xkb_state: Option<(KeyEventsState, Keymap)>, + keyboard: wl_keyboard::WlKeyboard, + + repeat_settings: RepeatInfo, + // The token, and scancode which is currently being repeated + repeat_details: Option<(RegistrationToken, u32)>, +} + +impl KeyboardState { + pub(super) fn new( + qh: &QueueHandle, + name: SeatName, + seat: wl_seat::WlSeat, + ) -> Self { + KeyboardState { + xkb_state: None, + keyboard: seat.get_keyboard(qh, KeyboardUserData(name)), + repeat_settings: RepeatInfo::Disable, + repeat_details: None, + } + } +} + +impl WaylandState { + fn keyboard(&mut self, data: &KeyboardUserData) -> &mut KeyboardState { + keyboard(&mut self.input_states, data) + } + /// Stop receiving events for the given keyboard + fn delete_keyboard(&mut self, data: &KeyboardUserData) { + let it = self.input_state(data.0); + it.destroy_keyboard(); + } +} + +fn keyboard<'a>(seats: &'a mut [SeatInfo], data: &KeyboardUserData) -> &'a mut KeyboardState { + input_state(seats, data.0).keyboard_state.as_mut().expect( + "KeyboardUserData is only constructed when a new keyboard is created, so state exists", + ) +} + +impl Drop for KeyboardState { + fn drop(&mut self) { + self.keyboard.release() + } +} + +impl Dispatch for WaylandPlatform { + fn event( + state: &mut Self, + proxy: &wl_keyboard::WlKeyboard, + event: ::Event, + data: &KeyboardUserData, + _: &Connection, + _: &QueueHandle, + ) { + match event { + wl_keyboard::Event::Keymap { format, fd, size } => match format { + WEnum::Value(KeymapFormat::XkbV1) => { + tracing::info!("Received new keymap"); + let contents = unsafe { + mmap::Mmap::from_raw_private( + fd.as_raw_fd(), + size.try_into().unwrap(), + 0, + size.try_into().unwrap(), + ) + .unwrap() + .as_ref() + .to_vec() + }; + let context = &mut state.xkb_context; + // keymap data is '\0' terminated. + let keymap = context.keymap_from_slice(&contents); + let keymapstate = context.state_from_keymap(&keymap).unwrap(); + + let keyboard = state.keyboard(data); + keyboard.xkb_state = Some((keymapstate, keymap)); + } + WEnum::Value(KeymapFormat::NoKeymap) => { + // TODO: What's the expected behaviour here? Is this just for embedded devices? + tracing::error!( + keyboard = ?proxy, + "the server asked that no keymap be used, but Glazier requires one", + ); + tracing::info!(keyboard = ?proxy, + "stopping receiving events from keyboard with no keymap"); + state.delete_keyboard(data); + } + WEnum::Value(it) => { + // Ideally we'd get a compilation failure here, but such are the limits of non_exhaustive + tracing::error!( + issues_url = "https://github.com/linebender/glazier/issues", + "keymap format {it:?} was added to Wayland, but Glazier does not yet support it. Please report this on GitHub"); + tracing::info!(keyboard = ?proxy, + "stopping receiving events from keyboard with unknown keymap format"); + state.delete_keyboard(data); + } + WEnum::Unknown(it) => { + tracing::error!( + keyboard = ?proxy, + format = it, + issues_url = "https://github.com/linebender/glazier/issues", + "the server asked that a keymap in format ({it}) be used, but smithay-client-toolkit cannot interpret this. Please report this on GitHub", + ); + tracing::info!(keyboard = ?proxy, + "stopping receiving events from keyboard with unknown keymap format"); + state.delete_keyboard(data); + } + }, + wl_keyboard::Event::Enter { + serial: _, + surface, + // TODO: How should we handle `keys`? + keys: _, + } => { + let state = &mut **state; + let seat = input_state(&mut state.input_states, data.0); + seat.window_focus_enter(&mut state.windows, WindowId::of_surface(&surface)); + } + wl_keyboard::Event::Leave { .. } => { + let state = &mut **state; + let seat = input_state(&mut state.input_states, data.0); + seat.window_focus_leave(&mut state.windows); + if let Some(keyboard_state) = seat.keyboard_state.as_mut() { + if let Some((token, _)) = keyboard_state.repeat_details.take() { + state.loop_handle.remove(token); + } + } + } + wl_keyboard::Event::Modifiers { + serial: _, + mods_depressed, + mods_latched, + mods_locked, + group, + } => { + let keyboard = state.keyboard(data); + let Some(xkb_state) = keyboard.xkb_state.as_mut() else { + tracing::error!(keyboard = ?proxy, "got Modifiers event before keymap"); + return; + }; + xkb_state.0.update_xkb_state(ActiveModifiers { + base_mods: mods_depressed, + latched_mods: mods_latched, + locked_mods: mods_locked, + // See https://gitlab.gnome.org/GNOME/gtk/-/blob/cffa45d5ff97b3b6107bb9d563a84a529014342a/gdk/wayland/gdkdevice-wayland.c#L2163-2177 + base_layout: group, + latched_layout: 0, + locked_layout: 0, + }) + } + wl_keyboard::Event::Key { + serial: _, + time: _, // TODO: Report the time of the event to the keyboard + key, + state: key_state, + } => { + // Need to add 8 as per wayland spec + // See https://wayland.app/protocols/wayland#wl_keyboard:enum:keymap_format:entry:xkb_v1 + let scancode = key + 8; + + let state = &mut **state; + let seat = input_state(&mut state.input_states, data.0); + + let key_state = match key_state { + WEnum::Value(wl_keyboard::KeyState::Pressed) => KeyState::Down, + WEnum::Value(wl_keyboard::KeyState::Released) => KeyState::Up, + WEnum::Value(_) => unreachable!("non_exhaustive enum extended"), + WEnum::Unknown(_) => unreachable!(), + }; + + seat.handle_key_event(scancode, key_state, false, &mut state.windows); + let keyboard_info = seat.keyboard_state.as_mut().unwrap(); + match keyboard_info.repeat_settings { + RepeatInfo::Repeat { delay, .. } => { + handle_repeat( + key_state, + keyboard_info, + scancode, + &mut state.loop_handle, + delay, + data, + ); + } + RepeatInfo::Disable => {} + } + } + wl_keyboard::Event::RepeatInfo { rate, delay } => { + let keyboard = state.keyboard(data); + if rate != 0 { + let rate: u32 = rate + .try_into() + .expect("Negative rate is invalid in wayland protocol"); + let delay: u32 = delay + .try_into() + .expect("Negative delay is invalid in wayland protocol"); + // The new rate is instantly recorded, as the running repeat (if there is one) + // will pick this up + keyboard.repeat_settings = RepeatInfo::Repeat { + // We confirmed non-zero and positive above + rate: Duration::from_secs_f64(1f64 / rate as f64), + delay, + } + } else { + keyboard.repeat_settings = RepeatInfo::Disable; + if let Some((token, _)) = keyboard.repeat_details { + state.loop_handle.remove(token); + } + } + } + _ => todo!(), + } + } +} + +fn handle_repeat( + key_state: KeyState, + keyboard_info: &mut KeyboardState, + scancode: u32, + loop_handle: &mut LoopHandle<'_, WaylandPlatform>, + delay: u32, + data: &KeyboardUserData, +) { + match &key_state { + KeyState::Down => { + let (_, xkb_keymap) = keyboard_info.xkb_state.as_mut().unwrap(); + if xkb_keymap.repeats(scancode) { + // Start repeating. Exact choice of repeating behaviour varies - see + // discussion in [#glazier > Key Repeat Behaviour](https://xi.zulipchat.com/#narrow/stream/351333-glazier/topic/Key.20repeat.20behaviour) + // We currently choose to repeat based on scancode - this is the behaviour of Chromium apps + if let Some((existing, _)) = keyboard_info.repeat_details.take() { + loop_handle.remove(existing); + } + // Ideally, we'd produce the deadline based on the `time` parameter + // However, it's not clear how to convert that into a Rust instant - it has "undefined base" + let timer = Timer::from_duration(Duration::from_millis(delay.into())); + let seat = data.0; + let token = loop_handle.insert_source(timer, move |deadline, _, state| { + let state = &mut **state; + let seat = input_state(&mut state.input_states, seat); + seat.handle_key_event( + scancode, + KeyState::Down, + true, + &mut state.windows, + ); + let keyboard_info = seat.keyboard_state.as_mut().unwrap(); + let RepeatInfo::Repeat { rate, .. } = keyboard_info.repeat_settings else { + tracing::error!("During repeat, found that repeating was disabled. Calloop Timer didn't unregister in time (?)"); + return TimeoutAction::Drop; + }; + // We use the instant of the deadline + rate rather than a Instant::now to ensure consistency, + // even with a really inaccurate implementation of timers + TimeoutAction::ToInstant(deadline + rate) + }).expect("Can insert into loop"); + keyboard_info.repeat_details = Some((token, scancode)); + } + } + KeyState::Up => { + if let Some((token, old_code)) = keyboard_info.repeat_details { + if old_code == scancode { + keyboard_info.repeat_details.take(); + loop_handle.remove(token); + } + } + } + } +} diff --git a/v2/src/backend/wayland/input/keyboard/mmap.rs b/v2/src/backend/wayland/input/keyboard/mmap.rs new file mode 100644 index 00000000..671a8db7 --- /dev/null +++ b/v2/src/backend/wayland/input/keyboard/mmap.rs @@ -0,0 +1,91 @@ +//! Temporary implementation of memory mapping, to allow testing keyboard interaction +use nix::sys::mman::{mmap, munmap, MapFlags, ProtFlags}; +use std::{ + convert::TryInto, + ops::{Deref, DerefMut}, + os::{raw::c_void, unix::prelude::RawFd}, + ptr::{self, NonNull}, + slice, +}; +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_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, "{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) { + tracing::warn!("Error unmapping memory: {}", e); + } + } + } +} diff --git a/v2/src/backend/wayland/input/mod.rs b/v2/src/backend/wayland/input/mod.rs new file mode 100644 index 00000000..5ed06310 --- /dev/null +++ b/v2/src/backend/wayland/input/mod.rs @@ -0,0 +1,602 @@ +use std::{ + cell::Cell, + collections::HashMap, + rc::{Rc, Weak}, +}; + +use glazier::{text::InputHandler, Counter, TextFieldToken, WinHandler}; + +use crate::backend::shared::xkb::{xkb_simulate_input, KeyboardHandled}; + +use self::{keyboard::KeyboardState, text_input::InputState}; + +use super::{ + window::{WaylandWindowState, WindowId}, + WaylandPlatform, WaylandState, +}; + +use keyboard_types::KeyState; +use smithay_client_toolkit::{ + delegate_seat, + reexports::{ + client::{protocol::wl_seat, Connection, QueueHandle}, + protocols::wp::text_input::zv3::client::zwp_text_input_v3, + }, + seat::SeatHandler, +}; + +mod keyboard; +mod text_input; + +pub(super) use text_input::TextInputManagerData; + +#[derive(Debug)] +pub(in crate::backend::wayland) struct TextFieldChange; + +impl TextFieldChange { + pub(in crate::backend::wayland) fn apply( + self, + seat: &mut SeatInfo, + windows: &mut Windows, + window: &WindowId, + ) { + if seat.keyboard_focused.as_ref() != Some(window) { + // This event is not for the + return; + } + let Some(mut handler) = handler(windows, window) else { + return; + }; + seat.update_active_text_input(&mut handler, false, true); + } +} + +/// The state we need to store about each seat +/// Each wayland seat may have a single: +/// - Keyboard input +/// - Pointer +/// - Touch +/// Plus: +/// - Text input +/// +/// These are stored in a vector because we expect nearly all +/// programs to only encounter a single seat, so we don't need the overhead of a HashMap. +/// +/// However, there's little harm in supporting multiple seats, so we may as well do so +/// +/// The SeatInfo is also the only system which can edit text input fields. +/// +/// Both the keyboard and text input want to handle text fields, so the seat handles ownership of this. +/// In particular, either the keyboard, or the text input system can "own" the input handling for the +/// focused text field, but not both. The main thing this impacts is whose state must be reset when the +/// other makes this claim. +/// +/// This state is stored in the window properties +pub(super) struct SeatInfo { + id: SeatName, + seat: wl_seat::WlSeat, + keyboard_state: Option, + input_state: Option, + keyboard_focused: Option, + + text_field_owner: TextFieldOwner, +} + +#[derive(Copy, Clone)] +enum TextFieldOwner { + Keyboard, + TextInput, + Neither, +} + +/// The type used to store the set of active windows +type Windows = HashMap; + +/// The properties maintained about the text input fields. +/// These are owned by the Window, as they may be modified at any time +/// +/// We should be extremely careful around the use of `active_text_field` and +/// `next_text_field`, because these could become None any time user code has control +/// (during `WinHandler::release_input_lock` is an especially sneaky case we need to watch out +/// for). This is because the corresponding text input could be removed by calling the window method, +/// so we need to not access the input field in that case. +/// +/// Conceptually, `active_text_field` is the same thing as +/// +/// Because of this shared nature, for simplicity we choose to store the properties for each window +/// in a `Rc>`, known as +/// +/// The contents of this struct are opaque to applications. +#[derive(Clone, Copy)] +pub(in crate::backend::wayland) struct TextInputProperties { + pub active_text_field: Option, + pub next_text_field: Option, + /// Whether the contents of `active_text_field` are different + /// to what they previously were. This is only set to true by the application + pub active_text_field_updated: bool, + pub active_text_layout_changed: bool, +} + +pub(in crate::backend::wayland) type TextInputCell = Rc>; +pub(in crate::backend::wayland) type WeakTextInputCell = Weak>; + +struct FutureInputLock<'a> { + handler: &'a mut dyn WinHandler, + token: TextFieldToken, + // Whether the field has been updated by the application since the last + // execution. This effectively means that it isn't meaningful for the IME to + // edit the contents or selection any more + field_updated: bool, +} + +impl SeatInfo { + fn window_focus_enter(&mut self, windows: &mut Windows, new_window: WindowId) { + // We either had no focus, or were already focused on this window + debug_assert!(!self + .keyboard_focused + .as_ref() + .is_some_and(|it| it != &new_window)); + if self.keyboard_focused.is_none() { + let Some(window) = windows.get_mut(&new_window) else { + return; + }; + if let Some(input_state) = self.input_state.as_mut() { + // We accepted the existing pre-edit on unfocus, and so text input being + // active doesn't make sense + // However, at that time the window was unfocused, so the disable request + // then was not respected. + // Because of this, we disable again upon refocus + input_state.remove_field(); + } + window.set_input_seat(self.id); + let mut handler = window_handler(window); + handler.0.got_focus(); + self.keyboard_focused = Some(new_window); + self.update_active_text_input(&mut handler, true, true); + } + } + + // Called once the window has been deleted + pub(super) fn window_deleted(&mut self, windows: &mut Windows) { + self.window_focus_leave(windows) + } + + fn window_focus_leave(&mut self, windows: &mut Windows) { + if let Some(old_focus) = self.keyboard_focused.take() { + let window = windows.get_mut(&old_focus); + if let Some(window) = window { + window.remove_input_seat(self.id); + let TextFieldDetails(handler, props) = window_handler(window); + handler.lost_focus(); + let props = props.get(); + self.force_release_preedit(props.active_text_field.map(|it| FutureInputLock { + handler, + token: it, + field_updated: props.active_text_field_updated, + })); + } else { + // The window might have been dropped, such that there is no previous handler + // However, we need to update our state + self.force_release_preedit(None); + } + } + } + + fn update_active_text_input( + &mut self, + TextFieldDetails(handler, props_cell): &mut TextFieldDetails, + mut force: bool, + should_update_text_input: bool, + ) { + let handler = &mut **handler; + let mut props = props_cell.get(); + loop { + let focus_changed; + { + let previous = props.active_text_field; + focus_changed = props.next_text_field != previous; + if focus_changed { + self.force_release_preedit(previous.map(|it| FutureInputLock { + handler, + token: it, + field_updated: props.active_text_field_updated, + })); + props.active_text_field_updated = true; + // release_input might have called into application code, which might in turn have called a + // text field updating window method. Because of that, we synchronise which field will be active now + props = props_cell.get(); + props.active_text_field = props.next_text_field; + props_cell.set(props); + } + } + if props.active_text_field_updated || force { + force = false; + if !focus_changed { + self.force_release_preedit(props.active_text_field.map(|it| FutureInputLock { + handler, + token: it, + field_updated: true, + })); + props = props_cell.get(); + } + // The pre-edit is definitely invalid at this point + props.active_text_field_updated = false; + props.active_text_layout_changed = false; + props_cell.set(props); + if let Some(field) = props.active_text_field { + if should_update_text_input { + if let Some(input_state) = self.input_state.as_mut() { + // In force_release_preedit, which has definitely been called, we + // might have cleared the field and disabled the text input, if it had any state + // See the comment there for explanation + input_state.set_field_if_needed(field); + + let mut ime = handler.acquire_input_lock(field, false); + input_state + .sync_state(&mut *ime, zwp_text_input_v3::ChangeCause::Other); + handler.release_input_lock(field); + props = props_cell.get(); + } + } + } + // We need to continue the loop here, because the application may have changed the focused field + // (although this seems rather unlikely) + } else if props.active_text_layout_changed { + props.active_text_layout_changed = false; + if let Some(field) = props.active_text_field { + if should_update_text_input { + if let Some(input_state) = self.input_state.as_mut() { + let mut ime = handler.acquire_input_lock(field, false); + input_state.sync_cursor_rectangle(&mut *ime); + handler.release_input_lock(field); + props = props_cell.get(); + } + } + } + } else { + // If there were no other updates from the application, then we can finish the loop + break; + } + } + } + + /// One of the cases in which the active preedit doesn't make sense anymore. + /// This can happen if: + /// 1. The selected field becomes a different field + /// 2. The window loses keyboard (and therefore text input) focus + /// 3. The selected field no longer exists + /// 4. The selected field's content was updated by the application, + /// e.g. selecting a different place with the mouse. Note that this + /// doesn't include layout changes, which leave the preedit as valid + /// + /// This leaves the text_input IME in a disabled state, so it should be re-enabled + /// if there is still a text field present + fn force_release_preedit( + &mut self, + // The field which we were previously focused on + // If that field no longer exists (which could be because it was removed, or because it) + field: Option, + ) { + match self.text_field_owner { + TextFieldOwner::Keyboard => { + let keyboard_state = self + .keyboard_state + .as_mut() + .expect("Keyboard can only claim compose if available"); + let xkb_state = keyboard_state + .xkb_state + .as_mut() + .expect("Keyboard can only claim if keymap available"); + let cancelled = xkb_state.0.cancel_composing(); + // This would be an implementation error in Glazier, so OK to panic + assert!( + cancelled, + "If the keyboard has claimed the input, it must be composing" + ); + if let Some(FutureInputLock { + handler, + token, + field_updated, + }) = field + { + let mut ime = handler.acquire_input_lock(token, true); + if field_updated { + // If the application updated the active field, the best we can do is to + // clear the region + ime.set_composition_range(None); + } else { + let range = ime.composition_range().expect( + "If we were still composing, there will be a composition range", + ); + // If we (for example) lost focus, we want to leave the cancellation string + ime.replace_range(range, xkb_state.0.cancelled_string()); + } + handler.release_input_lock(token); + } + } + TextFieldOwner::TextInput => { + if let Some(FutureInputLock { handler, token, .. }) = field { + // The Wayland text input interface does not permit the IME to respond to an input + // becoming unfocused. + let mut ime = handler.acquire_input_lock(token, true); + // An alternative here would be to reset the composition region to the empty string + // However, we choose not to do that for reasons discussed below + ime.set_composition_range(None); + handler.release_input_lock(token); + } + } + TextFieldOwner::Neither => { + // If there is no preedit text, we don't need to reset the preedit text + } + } + if let Some(ime_state) = self.input_state.as_mut() { + // The design of our IME interface gives no opportunity for the IME to proactively + // intercept e.g. a click event to reset the preedit content. + // Because of these conditions, we are forced into one of two choices. If you are typing e.g. + // `this is a [test]` (where test is the preedit text), then click at ` i|s `, we can either get + // the result `this i[test]s a test`, or `this i|s a test`. + // We would like to choose the latter, where the pre-edit text is not repeated. + // At least on GNOME, this is not possible - GNOME does not respect the application's + // request to cease text input under any circumstances. + // Given these contraints, the best possible implementation on GNOME + // would be `this i[test]s a`, which is implemented by GTK apps. However, + // this doesn't work due to our method of reporting updates from the application. + ime_state.remove_field(); + } + // Release ownership of the field + self.text_field_owner = TextFieldOwner::Neither; + } + + /// Stop receiving events for the keyboard of this seat + fn destroy_keyboard(&mut self) { + self.keyboard_state = None; + + if matches!(self.text_field_owner, TextFieldOwner::Keyboard) { + self.text_field_owner = TextFieldOwner::Neither; + // TODO: Reset the active text field? + // self.force_release_preedit(Some(..)); + } + } + + pub fn handle_key_event( + &mut self, + scancode: u32, + key_state: KeyState, + is_repeat: bool, + windows: &mut Windows, + ) { + let Some(window) = self.keyboard_focused.as_ref() else { + return; + }; + let keyboard = self + .keyboard_state + .as_mut() + // TODO: If the keyboard is removed from the seat whilst repeating, + // this might not be true. Although at that point, repeat should be cancelled anyway, so should be fine + .expect("Will have a keyboard if handling text input"); + let xkb_state = &mut keyboard + .xkb_state + .as_mut() + .expect("Has xkb state by the time keyboard events are arriving") + .0; + let keysym = xkb_state.get_one_sym(scancode); + let event = xkb_state.key_event(scancode, keysym, key_state, is_repeat); + + let Some(mut handler) = handler(windows, window) else { + return; + }; + match key_state { + KeyState::Down => { + if handler.0.key_down(&event) { + return; + } + let update_can_do_nothing = matches!( + self.text_field_owner, + TextFieldOwner::Keyboard | TextFieldOwner::Neither + ); + // TODO: It's possible that some text input implementations would + // pass certain keys (through to us - not for text input purposes) + // For example, a + self.update_active_text_input(&mut handler, !update_can_do_nothing, false); + let keyboard = self + .keyboard_state + .as_mut() + .expect("Will have a keyboard if handling text input"); + + let Some(field) = handler.1.get().active_text_field else { + return; + }; + let handler = handler.0; + let mut ime = handler.acquire_input_lock(field, true); + let result = xkb_simulate_input( + &mut keyboard + .xkb_state + .as_mut() + .expect("Has xkb state by the time keyboard events are arriving") + .0, + keysym, + &event, + &mut *ime, + ); + if let Some(ime_state) = self.input_state.as_mut() { + // In theory, this sync could be skipped if we got exactly KeyboardHandled::NoUpdate + // However, that is incorrect in the case where `update_active_text_input` would have + // made a change which we skipped with should_update_text_input: false + ime_state.sync_state(&mut *ime, zwp_text_input_v3::ChangeCause::Other) + } + handler.release_input_lock(field); + match result { + KeyboardHandled::UpdatedReleasingCompose => { + debug_assert!(matches!(self.text_field_owner, TextFieldOwner::Keyboard)); + self.text_field_owner = TextFieldOwner::Neither; + } + KeyboardHandled::UpdatedClaimingCompose => { + debug_assert!(matches!(self.text_field_owner, TextFieldOwner::Neither)); + self.text_field_owner = TextFieldOwner::Keyboard; + } + KeyboardHandled::UpdatedRetainingCompose => { + debug_assert!(matches!(self.text_field_owner, TextFieldOwner::Keyboard)); + } + KeyboardHandled::UpdatedNoCompose => { + debug_assert!(matches!(self.text_field_owner, TextFieldOwner::Neither)); + } + KeyboardHandled::NoUpdate => {} + } + } + KeyState::Up => handler.0.key_up(&event), + }; + } + + fn prepare_for_ime( + &mut self, + windows: &mut Windows, + op: impl FnOnce(&mut InputState, Box) -> bool, + ) { + let Some(window) = self.keyboard_focused.as_ref() else { + return; + }; + let Some(mut handler) = handler(windows, window) else { + return; + }; + let update_can_do_nothing = matches!( + self.text_field_owner, + TextFieldOwner::TextInput | TextFieldOwner::Neither + ); + self.update_active_text_input(&mut handler, !update_can_do_nothing, false); + let Some(field) = handler.1.get().active_text_field else { + return; + }; + let handler = handler.0; + let ime = handler.acquire_input_lock(field, true); + let has_preedit = op(self.input_state.as_mut().unwrap(), ime); + if has_preedit { + self.text_field_owner = TextFieldOwner::TextInput; + } else { + self.text_field_owner = TextFieldOwner::Neither; + } + handler.release_input_lock(field); + } +} + +struct TextFieldDetails<'a>(&'a mut dyn WinHandler, TextInputCell); + +/// Get the text input information for the given window +fn handler<'a>(windows: &'a mut Windows, window: &WindowId) -> Option> { + let window = &mut *windows.get_mut(window)?; + Some(window_handler(window)) +} + +fn window_handler(window: &mut WaylandWindowState) -> TextFieldDetails { + todo!() +} + +/// Identifier for a seat +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(super) struct SeatName(u64); + +static SEAT_COUNTER: Counter = Counter::new(); + +impl WaylandState { + /// Access the state for the seat with the given name + fn input_state(&mut self, name: SeatName) -> &mut SeatInfo { + input_state(&mut self.input_states, name) + } + + #[track_caller] + fn info_of_seat(&mut self, seat: &wl_seat::WlSeat) -> &mut SeatInfo { + self.input_states + .iter_mut() + .find(|it| &it.seat == seat) + .expect("Glazier: Internal error, accessed deleted seat") + } + + // fn seat_ref(&self, name: SeatName) -> &SeatInfo; +} + +pub(super) fn input_state(seats: &mut [SeatInfo], name: SeatName) -> &mut SeatInfo { + seats + .iter_mut() + .find(|it| it.id == name) + .expect("Glazier: Internal error, accessed deleted seat") +} + +impl WaylandPlatform { + fn handle_new_seat(&mut self, seat: wl_seat::WlSeat) { + let id = SeatName(SEAT_COUNTER.next()); + let new_info = SeatInfo { + id, + seat, + keyboard_state: None, + input_state: None, + keyboard_focused: None, + text_field_owner: TextFieldOwner::Neither, + }; + let idx = self.input_states.len(); + self.input_states.push(new_info); + let state = &mut **self; + let input = &mut state.input_states[idx]; + input.input_state = state + .text_input + .as_ref() + .map(|text_input| InputState::new(text_input, &input.seat, &state.wayland_queue, id)); + } + + pub(super) fn initial_seats(&mut self) { + for seat in self.seats.seats() { + self.handle_new_seat(seat) + } + } +} + +impl SeatHandler for WaylandPlatform { + fn seat_state(&mut self) -> &mut smithay_client_toolkit::seat::SeatState { + &mut self.seats + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, seat: wl_seat::WlSeat) { + self.handle_new_seat(seat); + } + + fn new_capability( + &mut self, + _: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: smithay_client_toolkit::seat::Capability, + ) { + let seat_info = self.info_of_seat(&seat); + + match capability { + smithay_client_toolkit::seat::Capability::Keyboard => { + let state = KeyboardState::new(qh, seat_info.id, seat); + seat_info.keyboard_state = Some(state); + } + smithay_client_toolkit::seat::Capability::Pointer => {} + smithay_client_toolkit::seat::Capability::Touch => {} + it => tracing::warn!(?seat, "Unknown seat capability {it}"), + } + } + + fn remove_capability( + &mut self, + _: &Connection, + _: &QueueHandle, + seat: wl_seat::WlSeat, + capability: smithay_client_toolkit::seat::Capability, + ) { + let state = self.info_of_seat(&seat); + match capability { + smithay_client_toolkit::seat::Capability::Keyboard => state.destroy_keyboard(), + smithay_client_toolkit::seat::Capability::Pointer => {} + smithay_client_toolkit::seat::Capability::Touch => {} + it => tracing::info!(?seat, "Removed unknown seat capability {it}"), + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, seat: wl_seat::WlSeat) { + // Keep every other seat + self.input_states.retain(|it| it.seat != seat) + } +} + +delegate_seat!(WaylandPlatform); diff --git a/v2/src/backend/wayland/input/repeat.rs b/v2/src/backend/wayland/input/repeat.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/v2/src/backend/wayland/input/repeat.rs @@ -0,0 +1 @@ + diff --git a/v2/src/backend/wayland/input/text_input.rs b/v2/src/backend/wayland/input/text_input.rs new file mode 100644 index 00000000..f9b13081 --- /dev/null +++ b/v2/src/backend/wayland/input/text_input.rs @@ -0,0 +1,425 @@ +use smithay_client_toolkit::reexports::{ + client::{protocol::wl_seat, Dispatch, QueueHandle}, + protocols::wp::text_input::zv3::client::{ + zwp_text_input_manager_v3::ZwpTextInputManagerV3, + zwp_text_input_v3::{self, ZwpTextInputV3}, + }, +}; + +use glazier::{ + text::{Affinity, InputHandler, Selection}, + TextFieldToken, +}; + +use crate::backend::{ + wayland::{WaylandPlatform, WaylandState}, + window::WindowId, +}; + +use super::{input_state, SeatInfo, SeatName}; + +struct InputUserData(SeatName); + +pub(super) struct InputState { + text_input: ZwpTextInputV3, + + commit_count: u32, + + // Wayland requires that we store all the state sent in requests, + // and then apply them in the `done` message + // These are the versions of these values which 'we' own - not the + // versions passed to the input field + // These will be applied in done, unless reset + commit_string: Option, + preedit_string: Option, + delete_surrounding_before: u32, + delete_surrounding_after: u32, + /// The new positions of the cursor. + /// Begin and end are unclear - we presume begin is anchor and end is + /// active + new_cursor_begin: i32, + new_cursor_end: i32, + needs_to_own_preedit: bool, + + // The bookkeeping state + /// Used for sanity checking - the token we believe we're operating on, + /// which this bookkeeping state is relative to + token: Option, + /// The position in the input lock buffer (of token) where the compositor + /// believes the buffer to start from. See [set_surrounding_text], which shows + /// that Wayland wants only a subset of the full text ("additional characters") + /// but not the full buffer. + /// Will be None if we didn't send a buffer this time (because the selection was too large) + /// This is relevant if the IME asks for the cursor's position to be set, as + /// that is meaningless if we never sent a selection + /// + /// [set_surrounding_text]: https://wayland.app/protocols/text-input-unstable-v3#zwp_text_input_v3:request:set_surrounding_text + buffer_start: Option, +} + +impl InputState { + pub(super) fn new( + manager: &ZwpTextInputManagerV3, + seat: &wl_seat::WlSeat, + qh: &QueueHandle, + seat_name: SeatName, + ) -> Self { + InputState { + text_input: manager.get_text_input(seat, qh, InputUserData(seat_name)), + commit_count: 0, + + delete_surrounding_after: 0, + delete_surrounding_before: 0, + commit_string: None, + preedit_string: None, + new_cursor_begin: 0, + new_cursor_end: 0, + needs_to_own_preedit: false, + + buffer_start: None, + token: None, + } + } + + /// Move between different text inputs + /// + /// Used alongside the enable request, or in response to the leave event + fn reset(&mut self) { + self.commit_string = None; + self.preedit_string = None; + self.delete_surrounding_before = 0; + self.delete_surrounding_after = 0; + self.new_cursor_begin = 0; + self.new_cursor_end = 0; + self.buffer_start = None; + } + + pub(super) fn set_field_if_needed(&mut self, token: TextFieldToken) { + if self.token.is_none() { + self.reset(); + self.token = Some(token); + + self.text_input.enable(); + tracing::warn!("enabling text input"); + } else { + debug_assert!(self.token == Some(token)) + } + } + + pub(super) fn remove_field(&mut self) { + tracing::warn!("disabling text input"); + self.token = None; + self.text_input.disable(); + self.commit(); + } + + pub(super) fn sync_state( + &mut self, + handler: &mut dyn InputHandler, + cause: zwp_text_input_v3::ChangeCause, + ) { + tracing::trace!("Sending Text Input state to Wayland compositor"); + // input_state.text_input.set_content_type(); + let selection = handler.selection(); + + let selection_range = selection.range(); + // TODO: Confirm these affinities. I suspect all combinations of choices are wrong here, but oh well + let start_line = handler.line_range(selection_range.start, Affinity::Upstream); + let end_line = handler.line_range(selection_range.end, Affinity::Downstream); + let mut complete_range = start_line.start..end_line.end; + self.buffer_start = None; + 'can_set_surrounding_text: { + // Wayland strings cannot be longer than 4000 bytes + // Give some margin for error + if complete_range.len() > 3800 { + // Best effort attempt here? + if selection_range.len() > 3800 { + // If the selection range is too big, the protocol seems not to support this + // Just don't send it then + // Luckily, the set_surrounding_text isn't needed, and + // pre-edit text will soon be deleted + break 'can_set_surrounding_text; + } + let find_boundary = |mut it| { + let mut iterations = 0; + loop { + if handler.is_char_boundary(it) { + break it; + } + if iterations > 10 { + panic!("is_char_boundary implemented incorrectly"); + } + it += 1; + iterations += 1; + } + }; + // TODO: Consider alternative strategies here. + // For example, chromium bytes 2000 characters either side of the center of selection_range + + // 🤷 this is probably "additional characters" + complete_range = find_boundary((selection_range.start - 50).max(start_line.start)) + ..find_boundary((selection_range.end + 50).min(end_line.end)); + } + let start_range; + let end_range; + let mut final_selection = selection; + if let Some(excluded_range) = handler.composition_range() { + // The API isn't clear on what should happen if the selection is changed (e.g. by the mouse) + // whilst an edit is ongoing. Because of this, we choose to commit the pre-edit text when this happens + // (i.e. Event::SelectionChanged). This does mean that the potentially inconsistent pre-edit + // text is inserted into the text, but in my mind this is better than alternatives. + // Because of this behaviour, if pre-edit text has been sent to the client, we know that the selection is empty + // (because it either was replaced by the pre-edit text, or was) + + // However, upon testing to validate this approach, it was discovered that GNOME doesn't implement their + // Wayland text input API properly, as it does nothing with the value from the set_text_change_cause request + // Because of this, as well as the commit, the IME follows the new input. + if excluded_range.contains(&final_selection.active) { + final_selection.active = excluded_range.start; + } + if excluded_range.contains(&final_selection.anchor) { + final_selection.anchor = excluded_range.start; + } + start_range = complete_range.start..excluded_range.start; + end_range = excluded_range.end..complete_range.end; + } else { + start_range = complete_range.clone(); + end_range = 0..0; + } + let mut text = handler.slice(start_range.clone()).into_owned(); + if !end_range.is_empty() { + text.push_str(&handler.slice(end_range.clone())); + } + // The point which all results known by the buffer are available + let buffer_start = complete_range.start; + self.text_input.set_surrounding_text( + text, + (final_selection.active - buffer_start) as i32, + (final_selection.anchor - buffer_start) as i32, + ); + self.buffer_start = Some(buffer_start); + } + + self.sync_cursor_rectangle_inner(selection, selection_range, start_line, end_line, handler); + + // We always set a text change cause to make sure + self.text_input.set_text_change_cause(cause); + + self.commit(); + } + + pub(super) fn sync_cursor_rectangle(&mut self, handler: &mut dyn InputHandler) { + let selection = handler.selection(); + let selection_range = selection.range(); + self.sync_cursor_rectangle_inner( + selection, + selection_range.clone(), + handler.line_range(selection_range.start, Affinity::Upstream), + handler.line_range(selection_range.end, Affinity::Downstream), + handler, + ); + // We don't set the change cause because the "text, cursor or anchor" positions haven't changed + // self.text_input + // .set_text_change_cause(zwp_text_input_v3::ChangeCause::Other); + self.commit(); + } + + fn sync_cursor_rectangle_inner( + &mut self, + selection: Selection, + selection_range: std::ops::Range, + start_line: std::ops::Range, + end_line: std::ops::Range, + handler: &mut dyn InputHandler, + ) { + // TODO: Is this valid? + let active_line = if selection.active == selection_range.start { + end_line.start..selection.active + } else { + selection.active..start_line.end + }; + self.sync_cursor_line(handler, active_line); + } + + fn sync_cursor_line( + &mut self, + handler: &mut dyn InputHandler, + active_line: std::ops::Range, + ) { + let range = handler.slice_bounding_box(active_line); + if let Some(range) = range { + let x = range.min_x(); + let y = range.min_y(); + self.text_input.set_cursor_rectangle( + x as i32, + y as i32, + (range.max_x() - x) as i32, + (range.max_y() - y) as i32, + ); + }; + } + + fn commit(&mut self) { + self.commit_count += 1; + self.text_input.commit(); + } + + fn done(&mut self, handler: &mut dyn InputHandler) -> bool { + // The application must proceed by evaluating the changes in the following order: + let pre_edit_range = handler.composition_range(); + let mut selection = handler.selection(); + let mut has_preedit = false; + // 1. Replace existing preedit string with the cursor. + if let Some(range) = pre_edit_range { + selection.active = range.start; + selection.anchor = range.start; + + handler.replace_range(range, ""); + } + // 2. Delete requested surrounding text. + if self.delete_surrounding_before > 0 || self.delete_surrounding_after > 0 { + // The spec is unclear on how this should be handled when there is a cursor range. + // The relevant verbiage is "current cursor index" + let delete_range = (selection.active - self.delete_surrounding_before as usize) + ..(selection.active + self.delete_surrounding_after as usize); + if delete_range.contains(&selection.anchor) { + selection.anchor = delete_range.start; + } + selection.active = delete_range.start; + + handler.replace_range(delete_range, ""); + } + // 3. Insert commit string with the cursor at its end. + if let Some(commit) = self.commit_string.take() { + handler.replace_range(selection.range(), &commit); + selection = handler.selection(); + } + // 4. Calculate surrounding text to send. + // We skip this step, because we compute it in sync_state. + // 5. Insert new preedit text in cursor position. + if let Some(preedit) = self.preedit_string.take() { + let range = selection.range(); + + let selection_start = range.start; + handler.replace_range(range, &preedit); + handler.set_composition_range(Some(selection_start..(selection_start + preedit.len()))); + let selection_start = selection_start as i32; + // 6. Place cursor inside preedit text. + handler.set_selection(Selection::new( + (selection_start + self.new_cursor_begin) as usize, + (selection_start + self.new_cursor_end) as usize, + )); + has_preedit = true; + } else { + handler.set_composition_range(None); + } + selection = handler.selection(); + // TODO: Confirm this affinity + let active_line = handler.line_range(selection.active, Affinity::Upstream); + self.sync_cursor_line(handler, active_line); + has_preedit + } +} + +impl WaylandState { + fn text_input(&mut self, data: &InputUserData) -> &mut InputState { + text_input(&mut self.input_states, data) + } +} + +fn text_input<'a>(seats: &'a mut [SeatInfo], data: &InputUserData) -> &'a mut InputState { + seat_text_input(seats, data.0) +} +fn seat_text_input(seats: &mut [SeatInfo], data: SeatName) -> &mut InputState { + input_state(seats, data) + .input_state + .as_mut() + .expect("Text Input only obtained for seats with text input") +} + +impl Dispatch for WaylandPlatform { + fn event( + platform: &mut Self, + _proxy: &ZwpTextInputV3, + event: ::Event, + data: &InputUserData, + _conn: &smithay_client_toolkit::reexports::client::Connection, + _qhandle: &QueueHandle, + ) { + let state = &mut **platform; + tracing::trace!("Handling text input event"); + match event { + zwp_text_input_v3::Event::Enter { surface } => { + let window_id = WindowId::of_surface(&surface); + let seat = input_state(&mut state.input_states, data.0); + seat.window_focus_enter(&mut state.windows, window_id); + } + zwp_text_input_v3::Event::Leave { .. } => { + let seat = input_state(&mut state.input_states, data.0); + seat.window_focus_leave(&mut state.windows); + } + zwp_text_input_v3::Event::PreeditString { + text, + cursor_begin, + cursor_end, + } => { + let input_state = state.text_input(data); + input_state.preedit_string = text; + input_state.new_cursor_begin = cursor_begin; + input_state.new_cursor_end = cursor_end; + input_state.needs_to_own_preedit = true; + } + zwp_text_input_v3::Event::CommitString { text } => { + if text.is_none() { + tracing::info!("got CommitString event with null string") + } + let input_state = state.text_input(data); + input_state.commit_string = text; + input_state.needs_to_own_preedit = true; + } + zwp_text_input_v3::Event::DeleteSurroundingText { + after_length, + before_length, + } => { + let input_state = state.text_input(data); + input_state.delete_surrounding_after = after_length; + input_state.delete_surrounding_before = before_length; + input_state.needs_to_own_preedit = true; + } + zwp_text_input_v3::Event::Done { serial } => { + let input_state = input_state(&mut state.input_states, data.0); + let text_input = input_state.input_state.as_mut().unwrap(); + if text_input.needs_to_own_preedit { + // We need an input lock from input_state - call force_remove_preedit if the owner is conflicting + // TODO: Something here isn't right - force_remove_preedit might change the content of the field + // if it cancels a composition, which means that the IME input isn't what you want + text_input.needs_to_own_preedit = false; + input_state.prepare_for_ime(&mut state.windows, |this, mut ime| { + let res = this.done(&mut *ime); + if serial == this.commit_count { + this.sync_state(&mut *ime, zwp_text_input_v3::ChangeCause::InputMethod); + this.needs_to_own_preedit = false; + } + res + }); + } + } + _ => todo!(), + } + } +} + +pub(crate) struct TextInputManagerData; + +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + _: &ZwpTextInputManagerV3, + event: ::Event, + _: &TextInputManagerData, + _: &smithay_client_toolkit::reexports::client::Connection, + _: &smithay_client_toolkit::reexports::client::QueueHandle, + ) { + tracing::error!(?event, "unexpected zwp_text_input_manager_v3 event"); + } +} diff --git a/v2/src/backend/wayland/mod.rs b/v2/src/backend/wayland/mod.rs new file mode 100644 index 00000000..8a3207f5 --- /dev/null +++ b/v2/src/backend/wayland/mod.rs @@ -0,0 +1,135 @@ +// 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 + +use std::{ + collections::{HashMap, VecDeque}, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; + +use smithay_client_toolkit::{ + compositor::CompositorState, + delegate_registry, + output::OutputState, + reexports::{ + calloop::{self, LoopHandle, LoopSignal}, + client::QueueHandle, + protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3, + }, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::SeatState, + shell::xdg::XdgShell, +}; + +use crate::{handler::PlatformHandler, Glazier}; + +use self::{ + input::SeatInfo, + window::{WaylandWindowState, WindowAction, WindowId}, +}; + +use super::shared::xkb::Context; + +pub mod error; +mod input; +mod run_loop; +mod screen; +pub mod window; + +pub use run_loop::launch; + +pub(crate) type GlazierImpl<'a> = &'a mut WaylandState; + +/// The main state type of the event loop. Implements dispatching for all supported +/// wayland events +struct WaylandPlatform { + // Drop the handler as early as possible, in case there are any Wgpu surfaces owned by it + pub handler: Box, + pub state: WaylandState, +} + +pub(crate) struct WaylandState { + pub(self) windows: HashMap, + + pub(self) registry_state: RegistryState, + + pub(self) output_state: OutputState, + // TODO: Do we need to keep this around + // It is unused because(?) wgpu creates the surfaces through RawDisplayHandle(?) + pub(self) _compositor_state: CompositorState, + // Is used: Keep the XdgShell alive, which is a Weak in all Handles + pub(self) _xdg_shell_state: XdgShell, + pub(self) wayland_queue: QueueHandle, + + pub(self) loop_signal: LoopSignal, + // Used for timers and keyboard repeating - not yet implemented + pub(self) loop_handle: LoopHandle<'static, WaylandPlatform>, + + pub(self) seats: SeatState, + pub(self) input_states: Vec, + pub(self) xkb_context: Context, + pub(self) text_input: Option, + + pub(self) idle_actions: Vec, + pub(self) actions: VecDeque, + pub(self) loop_sender: calloop::channel::Sender, +} + +delegate_registry!(WaylandPlatform); + +impl ProvidesRegistryState for WaylandPlatform { + fn registry(&mut self) -> &mut RegistryState { + &mut self.state.registry_state + } + registry_handlers![OutputState, SeatState]; +} + +impl Deref for WaylandPlatform { + type Target = WaylandState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} +impl DerefMut for WaylandPlatform { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.state + } +} + +impl WaylandPlatform { + fn with_glz( + &mut self, + with_handler: impl FnOnce(&mut dyn PlatformHandler, Glazier) -> R, + ) -> R { + with_handler(&mut *self.handler, Glazier(&mut self.state, PhantomData)) + // TODO: Is now the time to drain the events? + } +} + +enum ActiveAction { + /// A callback which will be run on the event loop + /// This should *only* directly call a user callback + Window(WindowId, WindowAction), +} + +enum IdleAction { + Callback(LoopCallback), + Token(glazier::IdleToken), +} + +type LoopCallback = Box; diff --git a/v2/src/backend/wayland/run_loop.rs b/v2/src/backend/wayland/run_loop.rs new file mode 100644 index 00000000..f0acc964 --- /dev/null +++ b/v2/src/backend/wayland/run_loop.rs @@ -0,0 +1,171 @@ +// 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 std::{ + collections::{HashMap, VecDeque}, + rc::Rc, +}; + +use smithay_client_toolkit::{ + compositor::CompositorState, + output::OutputState, + reexports::{ + calloop::{channel, EventLoop}, + client::{ + globals::{registry_queue_init, BindError}, + Connection, WaylandSource, + }, + }, + registry::RegistryState, + seat::SeatState, + shell::xdg::XdgShell, +}; + +use super::{error::Error, IdleAction, LoopCallback, WaylandState}; +use crate::{ + backend::{ + shared::xkb::Context, + wayland::{input::TextInputManagerData, WaylandPlatform}, + }, + Glazier, PlatformHandler, +}; + +pub fn launch( + handler: Box, + on_init: impl FnOnce(Glazier), +) -> Result<(), Error> { + tracing::info!("wayland application initiated"); + + let conn = Connection::connect_to_env()?; + let (globals, event_queue) = registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); + let mut event_loop: EventLoop = EventLoop::try_new()?; + let loop_handle = event_loop.handle(); + let loop_signal = event_loop.get_signal(); + let (loop_sender, loop_source) = channel::channel::(); + + loop_handle + .insert_source(loop_source, |event, _, platform| { + match event { + channel::Event::Msg(msg) => { + msg(platform) + } + channel::Event::Closed => { + let _ = &platform.loop_sender; + unreachable!( + "The value `platform.loop_sender` has been dropped, except we have a reference to it" + ) + } // ? + } + }) + .unwrap(); + + WaylandSource::new(event_queue) + .unwrap() + .insert(loop_handle.clone()) + .unwrap(); + + let compositor_state: CompositorState = CompositorState::bind(&globals, &qh)?; + + let shell = XdgShell::bind(&globals, &qh)?; + let text_input_global = globals.bind(&qh, 1..=1, TextInputManagerData).map_or_else( + |err| match err { + e @ BindError::UnsupportedVersion => Err(e), + BindError::NotPresent => Ok(None), + }, + |it| Ok(Some(it)), + )?; + + let state = WaylandState { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + _compositor_state: compositor_state, + _xdg_shell_state: shell, + windows: HashMap::new(), + wayland_queue: qh.clone(), + loop_signal: loop_signal.clone(), + input_states: vec![], + seats: SeatState::new(&globals, &qh), + xkb_context: Context::new(), + text_input: text_input_global, + loop_handle: loop_handle.clone(), + + actions: VecDeque::new(), + idle_actions: Vec::new(), + loop_sender, + }; + let mut platform = WaylandPlatform { handler, state }; + platform.initial_seats(); + + tracing::info!("wayland event loop initiated"); + platform.with_glz(|_, glz| on_init(glz)); + event_loop + .run(None, &mut platform, |platform| { + let mut idle_actions = std::mem::take(&mut platform.idle_actions); + for action in idle_actions.drain(..) { + match action { + IdleAction::Callback(cb) => cb(platform), + IdleAction::Token(token) => { + platform.with_glz(|handler, glz| handler.idle(glz, token)) + } + } + } + if platform.idle_actions.is_empty() { + // Re-use the allocation if possible + platform.idle_actions = idle_actions; + } else { + tracing::info!( + "A new idle request was added during an idle callback. This may be an error" + ); + } + }) + .expect("Shouldn't error in event loop"); + Ok(()) +} + +impl WaylandState { + pub(crate) fn stop(&mut self) { + self.loop_signal.stop() + } + + pub(crate) fn handle(&mut self) -> LoopHandle { + LoopHandle { + loop_sender: self.loop_sender.clone(), + } + } +} + +#[derive(Clone)] +pub struct LoopHandle { + loop_sender: channel::Sender, +} + +impl LoopHandle { + pub fn run_on_main(&self, callback: F) + where + F: FnOnce(&mut dyn PlatformHandler, Glazier) + Send + 'static, + { + match self + .loop_sender + .send(Box::new(|plat| plat.with_glz(callback))) + { + Ok(()) => (), + Err(err) => { + tracing::warn!("Sending idle event loop failed: {err:?}") + } + }; + } +} diff --git a/v2/src/backend/wayland/screen.rs b/v2/src/backend/wayland/screen.rs new file mode 100644 index 00000000..30c3761b --- /dev/null +++ b/v2/src/backend/wayland/screen.rs @@ -0,0 +1,24 @@ +use smithay_client_toolkit::{ + delegate_output, + output::{OutputHandler, OutputState}, + reexports::client::{protocol::wl_output::WlOutput, Connection, QueueHandle}, +}; + +use super::WaylandPlatform; + +delegate_output!(WaylandPlatform); + +impl OutputHandler for WaylandPlatform { + fn output_state(&mut self) -> &mut OutputState { + &mut self.state.output_state + } + + fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle, _output: WlOutput) { + // TODO: Tell the app about these? + } + + fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle, _output: WlOutput) {} + + fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle, _output: WlOutput) { + } +} diff --git a/v2/src/backend/wayland/window.rs b/v2/src/backend/wayland/window.rs new file mode 100644 index 00000000..687b0451 --- /dev/null +++ b/v2/src/backend/wayland/window.rs @@ -0,0 +1,873 @@ +// +// 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 std::cell::{Cell, RefCell}; +use std::os::raw::c_void; +use std::rc::{Rc, Weak}; +use std::sync::mpsc::{self, Sender}; + +use smithay_client_toolkit::compositor::CompositorHandler; +use smithay_client_toolkit::reexports::calloop::{channel, LoopHandle}; +use smithay_client_toolkit::reexports::client::protocol::wl_compositor::WlCompositor; +use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface; +use smithay_client_toolkit::reexports::client::{protocol, Connection, Proxy, QueueHandle}; +use smithay_client_toolkit::shell::xdg::window::{ + Window, WindowConfigure, WindowDecorations, WindowHandler, +}; +use smithay_client_toolkit::shell::xdg::XdgShell; +use smithay_client_toolkit::shell::WaylandSurface; +use smithay_client_toolkit::{delegate_compositor, delegate_xdg_shell, delegate_xdg_window}; +use tracing; +use wayland_backend::client::ObjectId; + +use crate::PlatformHandler; + +use super::input::{ + input_state, SeatName, TextFieldChange, TextInputCell, TextInputProperties, WeakTextInputCell, +}; +use super::{ActiveAction, IdleAction, WaylandPlatform, WaylandState}; + +use glazier::{ + kurbo::{Point, Rect, Size}, + Error as ShellError, IdleToken, Region, Scalable, Scale, WinHandler, WindowLevel, +}; + +#[derive(Clone)] +pub struct WindowHandle { + idle_sender: Sender, + loop_sender: channel::Sender, + properties: Weak>, + text: WeakTextInputCell, + // Safety: Points to a wl_display instance + raw_display_handle: Option<*mut c_void>, +} + +// impl WindowHandle { +// fn id(&self) -> WindowId { +// let props = self.properties(); +// let props = props.borrow(); +// WindowId::new(&props.wayland_window) +// } + +// fn defer(&self, action: WindowAction) { +// self.loop_sender +// .send(ActiveAction::Window(self.id(), action)) +// .expect("Running on a window should only occur whilst application is active") +// } + +// fn properties(&self) -> Rc> { +// self.properties.upgrade().unwrap() +// } + +// pub fn show(&self) { +// tracing::debug!("show initiated"); +// let props = self.properties(); +// let props = props.borrow(); +// // TODO: Is this valid? +// props.wayland_window.commit(); +// } + +// pub fn resizable(&self, _resizable: bool) { +// tracing::warn!("resizable is unimplemented on wayland"); +// // TODO: If we are using fallback decorations, we should be able to disable +// // dragging based resizing +// } + +// pub fn show_titlebar(&self, show_titlebar: bool) { +// tracing::info!("show_titlebar is implemented on a best-effort basis on wayland"); +// // TODO: Track this into the fallback decorations when we add those +// let props = self.properties(); +// let props = props.borrow(); +// if show_titlebar { +// props +// .wayland_window +// .request_decoration_mode(Some(DecorationMode::Server)) +// } else { +// props +// .wayland_window +// .request_decoration_mode(Some(DecorationMode::Client)) +// } +// } + +// pub fn set_position(&self, _position: Point) { +// tracing::warn!("set_position is unimplemented on wayland"); +// // TODO: Use the KDE plasma extensions for this if available +// // TODO: Use xdg_positioner if this is a child window +// } + +// pub fn get_position(&self) -> Point { +// tracing::warn!("get_position is unimplemented on wayland"); +// Point::ZERO +// } + +// pub fn content_insets(&self) -> Insets { +// // I *think* wayland surfaces don't care about content insets +// // That is, all decorations (to confirm: even client side?) are 'outsets' +// Insets::from(0.) +// } + +// pub fn set_size(&self, size: Size) { +// let props = self.properties(); +// props.borrow_mut().requested_size = Some(size); + +// // We don't need to tell the server about changing the size - so long as the size of the surface gets changed properly +// // So, all we need to do is to tell the handler about this change (after caching it here) +// // We must defer this, because we're probably in the handler +// self.defer(WindowAction::ResizeRequested); +// } + +// pub fn get_size(&self) -> Size { +// let props = self.properties(); +// let props = props.borrow(); +// props.current_size +// } + +// pub fn set_window_state(&mut self, state: glazier::WindowState) { +// let props = self.properties(); +// let props = props.borrow(); +// match state { +// glazier::WindowState::Maximized => props.wayland_window.set_maximized(), +// glazier::WindowState::Minimized => props.wayland_window.set_minimized(), +// // TODO: I don't think we can do much better than this - we can't unset being minimised +// glazier::WindowState::Restored => props.wayland_window.unset_maximized(), +// } +// } + +// pub fn get_window_state(&self) -> glazier::WindowState { +// // We can know if we're maximised or restored, but not if minimised +// tracing::warn!("get_window_state is unimplemented on wayland"); +// glazier::WindowState::Maximized +// } + +// pub fn handle_titlebar(&self, _val: bool) { +// tracing::warn!("handle_titlebar is unimplemented on wayland"); +// } + +// /// Close the window. +// pub fn close(&self) { +// self.defer(WindowAction::Close) +// } + +// /// 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) { +// let props = self.properties(); +// let mut props = props.borrow_mut(); +// props.will_repaint = true; +// if !props.pending_frame_callback { +// drop(props); +// self.defer(WindowAction::AnimationRequested); +// } +// } + +// /// Request invalidation of the entire window contents. +// pub fn invalidate(&self) { +// self.request_anim_frame(); +// } + +// /// Request invalidation of one rectangle, which is given in display points relative to the +// /// drawing area. +// pub fn invalidate_rect(&self, _rect: Rect) { +// todo!() +// } + +// pub fn add_text_field(&self) -> TextFieldToken { +// TextFieldToken::next() +// } + +// pub fn remove_text_field(&self, token: TextFieldToken) { +// let props_cell = self.text.upgrade().unwrap(); +// let mut props = props_cell.get(); +// let mut updated = false; +// if props.active_text_field.is_some_and(|it| it == token) { +// props.active_text_field = None; +// props.active_text_field_updated = true; +// updated = true; +// } +// if props.next_text_field.is_some_and(|it| it == token) { +// props.next_text_field = None; +// updated = true; +// } + +// if updated { +// props_cell.set(props); + +// self.defer(WindowAction::TextField(TextFieldChange)); +// } +// } + +// pub fn set_focused_text_field(&self, active_field: Option) { +// let props_cell = self.text.upgrade().unwrap(); +// let mut props = props_cell.get(); +// props.next_text_field = active_field; +// props_cell.set(props); + +// self.defer(WindowAction::TextField(TextFieldChange)); +// } + +// pub fn update_text_field(&self, token: TextFieldToken, update: Event) { +// let props_cell = self.text.upgrade().unwrap(); +// let mut props = props_cell.get(); +// if props.active_text_field.is_some_and(|it| it == token) { +// match update { +// Event::LayoutChanged => props.active_text_layout_changed = true, +// Event::SelectionChanged | Event::Reset => props.active_text_field_updated = true, +// _ => {} +// } +// props_cell.set(props); +// self.defer(WindowAction::TextField(TextFieldChange)); +// } +// } + +// pub fn request_timer(&self, deadline: std::time::Instant) -> TimerToken { +// let props = self.properties(); +// let props = props.borrow(); +// let window_id = WindowId::new(&props.wayland_window); +// let token = TimerToken::next(); +// props +// .loop_handle +// .insert_source( +// Timer::from_deadline(deadline), +// move |_deadline, _, state| { +// let window = state.windows.get_mut(&window_id); +// if let Some(window) = window { +// window.handler.timer(token); +// } +// // In theory, we could get the `timer` request to give us a new deadline +// TimeoutAction::Drop +// }, +// ) +// .expect("adding a Timer to the calloop event loop is infallible"); +// token +// } + +// pub fn set_cursor(&mut self, _cursor: &Cursor) { +// tracing::warn!("unimplemented set_cursor called") +// } + +// 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(IdleHandle { +// idle_sender: self.idle_sender.clone(), +// window: self.id(), +// }) +// } + +// /// Get the `Scale` of the window. +// pub fn get_scale(&self) -> Result { +// let props = self.properties(); +// let props = props.borrow(); +// Ok(props.current_scale) +// } + +// pub fn set_title(&self, title: &str) { +// let props = self.properties(); +// let props = props.borrow(); +// props.wayland_window.set_title(title) +// } + +// #[cfg(feature = "accesskit")] +// pub fn update_accesskit_if_active( +// &self, +// _update_factory: impl FnOnce() -> accesskit::TreeUpdate, +// ) { +// // AccessKit doesn't yet support this backend. +// } +// } + +impl PartialEq for WindowHandle { + fn eq(&self, rhs: &Self) -> bool { + self.properties.ptr_eq(&rhs.properties) + } +} + +impl Eq for WindowHandle {} + +impl Default for WindowHandle { + fn default() -> WindowHandle { + // Make fake channels, to work around WindowHandle being default + let (idle_sender, _) = mpsc::channel(); + let (loop_sender, _) = channel::channel(); + // TODO: Why is this Default? + WindowHandle { + properties: Weak::new(), + raw_display_handle: None, + idle_sender, + loop_sender, + text: Weak::default(), + } + } +} + +// TODO: Port +// unsafe impl HasRawWindowHandle for WindowHandle { +// fn raw_window_handle(&self) -> RawWindowHandle { +// let mut handle = WaylandWindowHandle::empty(); +// let props = self.properties(); +// handle.surface = props.borrow().wayland_window.wl_surface().id().as_ptr() as *mut _; +// RawWindowHandle::Wayland(handle) +// } +// } + +// unsafe impl HasRawDisplayHandle for WindowHandle { +// fn raw_display_handle(&self) -> RawDisplayHandle { +// let mut handle = WaylandDisplayHandle::empty(); +// handle.display = self +// .raw_display_handle +// .expect("Window can only be created with a valid display pointer"); +// RawDisplayHandle::Wayland(handle) +// } +// } + +#[derive(Clone)] +pub struct IdleHandle { + window: WindowId, + idle_sender: Sender, +} + +impl IdleHandle { + pub fn add_idle_callback(&self, callback: F) + where + F: FnOnce(&mut dyn PlatformHandler) + Send + 'static, + { + self.add_idle_state_callback(|state| callback(&mut *state.handler)) + } + + fn add_idle_state_callback(&self, callback: F) + where + F: FnOnce(&mut WaylandPlatform) + Send + 'static, + { + let window = self.window.clone(); + match self + .idle_sender + .send(IdleAction::Callback(Box::new(callback))) + { + Ok(()) => (), + Err(err) => { + tracing::warn!("Added idle callback for invalid application: {err:?}") + } + }; + } + + pub fn add_idle_token(&self, token: IdleToken) { + match self.idle_sender.send(IdleAction::Token(token)) { + Ok(()) => (), + Err(err) => tracing::warn!("Requested idle on invalid application: {err:?}"), + } + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct CustomCursor; + +/// Builder abstraction for creating new windows +pub(crate) struct WindowBuilder { + handler: Option>, + title: String, + position: Option, + level: WindowLevel, + state: Option, + // pre-scaled + size: Option, + min_size: Option, + resizable: bool, + show_titlebar: bool, + compositor: WlCompositor, + wayland_queue: QueueHandle, + loop_handle: LoopHandle<'static, WaylandPlatform>, + xdg_state: Weak, + idle_sender: Sender, + loop_sender: channel::Sender, + raw_display_handle: *mut c_void, +} + +impl WindowBuilder { + pub fn handler(mut self, handler: Box) -> Self { + self.handler = Some(handler); + self + } + + pub fn size(mut self, size: Size) -> Self { + self.size = Some(size); + self + } + + pub fn min_size(mut self, size: Size) -> Self { + self.min_size = Some(size); + self + } + + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + pub fn show_titlebar(mut self, show_titlebar: bool) -> Self { + self.show_titlebar = show_titlebar; + self + } + + pub fn transparent(self, _transparent: bool) -> Self { + tracing::info!( + "WindowBuilder::transparent is unimplemented for Wayland, it allows transparency by default" + ); + self + } + + pub fn position(mut self, position: Point) -> Self { + self.position = Some(position); + self + } + + pub fn level(mut self, level: WindowLevel) -> Self { + self.level = level; + self + } + + pub fn window_state(mut self, state: glazier::WindowState) -> Self { + self.state = Some(state); + self + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } + + pub fn build(self) -> Result { + let surface = self + .compositor + .create_surface(&self.wayland_queue, Default::default()); + let xdg_shell = self + .xdg_state + .upgrade() + .expect("Can only build whilst event loop hasn't ended"); + let wayland_window = xdg_shell.create_window( + surface, + // Request server decorations, because we don't yet do client decorations properly + WindowDecorations::RequestServer, + &self.wayland_queue, + ); + wayland_window.set_title(self.title); + // TODO: Pass this down + wayland_window.set_app_id("org.linebender.glazier.user_app"); + // TODO: Convert properly, set all properties + // wayland_window.set_min_size(self.min_size); + let window_id = WindowId::new(&wayland_window); + let properties = WindowProperties { + configure: None, + requested_size: self.size, + // This is just used as the default sizes, as we don't call `size` until the requested size is used + current_size: Size::new(600., 800.), + current_scale: Scale::new(1., 1.), // TODO: NaN? - these values should (must?) not be used + wayland_window, + wayland_queue: self.wayland_queue, + loop_handle: self.loop_handle, + will_repaint: false, + pending_frame_callback: false, + configured: false, + }; + let properties_strong = Rc::new(RefCell::new(properties)); + + let properties = Rc::downgrade(&properties_strong); + let text = Rc::new(Cell::new(TextInputProperties { + active_text_field: None, + next_text_field: None, + active_text_field_updated: false, + active_text_layout_changed: false, + })); + let handle = WindowHandle { + idle_sender: self.idle_sender, + loop_sender: self.loop_sender.clone(), + raw_display_handle: Some(self.raw_display_handle), + properties, + text: Rc::downgrade(&text), + }; + // TODO: When should Window::commit be called? This feels fragile + self.loop_sender + .send(ActiveAction::Window( + window_id, + WindowAction::Create(WaylandWindowState { + properties: properties_strong, + text_input_seat: None, + text, + handle: Some(handle.clone()), + }), + )) + .expect("Event loop should still be valid"); + + Ok(handle) + } +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +// TODO: According to https://github.com/linebender/druid/pull/2033, this should not be +// synced with the ID of the surface +pub(super) struct WindowId(ObjectId); + +impl WindowId { + pub fn new(surface: &impl WaylandSurface) -> Self { + Self::of_surface(surface.wl_surface()) + } + pub fn of_surface(surface: &WlSurface) -> Self { + Self(surface.id()) + } +} + +/// The state associated with each window, stored in [`WaylandState`] +pub(super) struct WaylandWindowState { + // TODO: This refcell is too strong - most of the fields can just be Cells + properties: Rc>, + text_input_seat: Option, + pub text: TextInputCell, + // The handle which will be used to access this window + // Will be passed to the `connect` handler in initial configure + // Cheap to clone, but kept in an option to track whether + // `connect` has been sent + handle: Option, +} + +struct WindowProperties { + // Requested size is used in configure, if it's supported + requested_size: Option, + + configure: Option, + // The dimensions of the surface we reported to the handler, and so report in get_size() + // Wayland gives strong deference to the application on surface size + // so, for example an application using wgpu could have the surface configured to be a different size + current_size: Size, + current_scale: Scale, + // The underlying wayland Window + // The way to close this Window is to drop the handle + // We make this the only handle, so we can definitely drop it + wayland_window: Window, + wayland_queue: QueueHandle, + loop_handle: LoopHandle<'static, WaylandPlatform>, + + /// Wayland requires frame (throttling) callbacks be requested *before* running commit. + /// However, user code controls when commit is called (generally through wgpu's + /// `present` in `paint`). + /// To allow using the frame throttling hints properly we: + /// - Always request a throttling hint before `paint`ing + /// - Only action that hint if a request_anim_frame (or equivalent) was called + /// - If there is no running hint, manually run this process when calling request_anim_frame + will_repaint: bool, + /// Whether a `frame` callback has been skipped + /// If this is false, and painting is requested, we need to manually run our own painting + pending_frame_callback: bool, + // We can't draw before being configured + configured: bool, +} + +impl WindowProperties { + /// Calculate the size that this window should be, given the current configuration + /// Called in response to a configure event or a resize being requested + /// + /// Returns the size which should be passed to [`WinHandler::size`]. + /// This is also set as self.current_size + fn calculate_size(&mut self) -> Size { + // We consume the requested size, as that is considered to be a one-shot affair + // Without doing so, the window would never be resizable + // + // TODO: Is this what we want? + let configure = self.configure.as_ref().unwrap(); + let requested_size = self.requested_size.take(); + if let Some(requested_size) = requested_size { + if !configure.is_maximized() && !configure.is_resizing() { + let requested_size_absolute = requested_size.to_px(self.current_scale); + if let Some((x, y)) = configure.suggested_bounds { + if requested_size_absolute.width < x as f64 + && requested_size_absolute.height < y as f64 + { + self.current_size = requested_size; + return self.current_size; + } + } else { + self.current_size = requested_size; + return self.current_size; + } + } + } + let current_size_absolute = self.current_size.to_dp(self.current_scale); + let new_width = configure + .new_size + .0 + .map_or(current_size_absolute.width, |it| it.get() as f64); + let new_height = configure + .new_size + .1 + .map_or(current_size_absolute.height, |it| it.get() as f64); + let new_size_absolute = Size { + height: new_height, + width: new_width, + }; + + self.current_size = new_size_absolute.to_dp(self.current_scale); + self.current_size + } +} + +/// The context do_paint is called in +enum PaintContext { + /// Painting occurs during a `frame` callback and finished, we know that there are no more frame callbacks + Frame, + Requested, + Configure, +} + +impl WaylandWindowState { + fn do_paint(&mut self, force: bool, context: PaintContext) { + { + let mut props = self.properties.borrow_mut(); + if matches!(context, PaintContext::Frame) { + props.pending_frame_callback = false; + } + if matches!(context, PaintContext::Requested) && props.pending_frame_callback && !force + { + // We'll handle this in the frame callback, when that occurs. + // This ensures throttling is respected + // This also prevents a hang on startup, although the reason for that occuring isn't clear + return; + } + if !props.configured || (!props.will_repaint && !force) { + return; + } + props.will_repaint = false; + // If there is not a frame callback in flight, we request it here + // This branch could be skipped e.g. on `configure`, which ignores frame throttling hints and + // always paints eagerly, even if there is a frame callback running + // TODO: Is that the semantics we want? + if !props.pending_frame_callback { + props.pending_frame_callback = true; + let surface = props.wayland_window.wl_surface(); + surface.frame(&props.wayland_queue.clone(), surface.clone()); + } + } + todo!("HANDLER"); + // self.prepare_paint(); + // TODO: Apply invalid properly + // When forcing, should mark the entire region as damaged + let mut region = Region::EMPTY; + { + let props = self.properties.borrow(); + let size = props.current_size.to_dp(props.current_scale); + region.add_rect(Rect { + x0: 0.0, + y0: 0.0, + x1: size.width, + y1: size.height, + }); + } + todo!("HANDLER"); + // self.handler.paint(®ion); + } + + pub(super) fn set_input_seat(&mut self, seat: SeatName) { + assert!(self.text_input_seat.is_none()); + self.text_input_seat = Some(seat); + } + pub(super) fn remove_input_seat(&mut self, seat: SeatName) { + assert_eq!(self.text_input_seat, Some(seat)); + self.text_input_seat = None; + } +} + +delegate_xdg_shell!(WaylandPlatform); +delegate_xdg_window!(WaylandPlatform); + +delegate_compositor!(WaylandPlatform); + +impl CompositorHandler for WaylandPlatform { + fn scale_factor_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &protocol::wl_surface::WlSurface, + // TODO: Support the fractional-scaling extension instead + // This requires an update in client-toolkit and wayland-protocols + new_factor: i32, + ) { + let window = self.windows.get_mut(&WindowId::of_surface(surface)); + let window = window.expect("Should only get events for real windows"); + let factor = f64::from(new_factor); + let scale = Scale::new(factor, factor); + let new_size; + { + let mut props = window.properties.borrow_mut(); + // TODO: Effectively, we need to re-evaluate the size calculation + // That means we need to cache the WindowConfigure or (mostly) equivalent + let cur_size_raw: Size = props.current_size.to_px(props.current_scale); + new_size = cur_size_raw.to_dp(scale); + props.current_scale = scale; + props.current_size = new_size; + // avoid locking the properties into user code + } + todo!("HANDLER"); + // window.handler.scale(scale); + // window.handler.size(new_size); + // TODO: Do we repaint here? + } + + fn frame( + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &protocol::wl_surface::WlSurface, + _time: u32, + ) { + let Some(window) = self.windows.get_mut(&WindowId::of_surface(surface)) else { + return; + }; + window.do_paint(false, PaintContext::Frame); + } +} + +impl WindowHandler for WaylandPlatform { + fn request_close( + &mut self, + _: &Connection, + _: &QueueHandle, + wl_window: &smithay_client_toolkit::shell::xdg::window::Window, + ) { + let Some(window) = self.state.windows.get_mut(&WindowId::new(wl_window)) else { + return; + }; + todo!("HANDLER"); + // window.handler.request_close(); + } + + fn configure( + &mut self, + _: &Connection, + _: &QueueHandle, + window: &smithay_client_toolkit::shell::xdg::window::Window, + configure: smithay_client_toolkit::shell::xdg::window::WindowConfigure, + _: u32, + ) { + let window = if let Some(window) = self.windows.get_mut(&WindowId::new(window)) { + window + } else { + // Using let else here breaks formatting with rustfmt + tracing::warn!("Received configure event for unknown window"); + return; + }; + if let Some(handle) = window.handle.take() { + // TODO: Handle it + } + // TODO: Actually use the suggestions from requested_size + let display_size; + { + let mut props = window.properties.borrow_mut(); + props.configure = Some(configure); + display_size = props.calculate_size(); + props.configured = true; + }; + // window.handler.size(display_size); + todo!("HANDLER"); + window.do_paint(true, PaintContext::Configure); + } +} + +pub(super) enum WindowAction { + /// Change the window size, based on `requested_size` + /// + /// `requested_size` must be set before this is called + ResizeRequested, + /// Close the Window + Close, + Create(WaylandWindowState), + AnimationRequested, + TextField(TextFieldChange), +} + +impl WindowAction { + pub(super) fn run(self, state: &mut WaylandState, window_id: WindowId) { + match self { + WindowAction::ResizeRequested => { + let Some(window) = state.windows.get_mut(&window_id) else { + return; + }; + let size = { + let mut props = window.properties.borrow_mut(); + props.calculate_size() + }; + // TODO: Ensure we follow the rules laid out by the compositor in `configure` + todo!("HANDLER"); + // window.handler.size(size); + // Force repainting now that the size has changed. + // TODO: Should this only happen if the size is actually different? + window.do_paint(true, PaintContext::Requested); + } + WindowAction::Close => { + // Remove the window from tracking + { + let Some(win) = state.windows.remove(&window_id) else { + tracing::error!("Tried to close the same window twice"); + return; + }; + if let Some(seat) = win.text_input_seat { + let seat = input_state(&mut state.input_states, seat); + seat.window_deleted(&mut state.windows); + } + } + // We will drop the proper wayland window later when we Drop window.props + if state.windows.is_empty() { + state.loop_signal.stop(); + } + } + WindowAction::Create(win_state) => { + state.windows.insert(window_id, win_state); + } + WindowAction::AnimationRequested => { + let Some(window) = state.windows.get_mut(&window_id) else { + return; + }; + window.do_paint(false, PaintContext::Requested); + } + WindowAction::TextField(change) => { + let Some(props) = state.windows.get_mut(&window_id) else { + return; + }; + let Some(seat) = props.text_input_seat else { + return; + }; + change.apply( + input_state(&mut state.input_states, seat), + &mut state.windows, + &window_id, + ); + } + } + } +} diff --git a/v2/src/handler.rs b/v2/src/handler.rs index 737df422..819a9943 100644 --- a/v2/src/handler.rs +++ b/v2/src/handler.rs @@ -1,4 +1,4 @@ -use glazier::Error; +use glazier::{Error, IdleToken, Region}; use crate::{Glazier, WindowId}; @@ -34,8 +34,8 @@ use crate::{Glazier, WindowId}; /// Most of the event are also associated with a single window. /// The methods which are /// -// Note: Most methods are marked with `#[allow(unused_variables)]` decoration -// for documentation purposes +// Methods have the `#[allow(unused_variables)]` attribute to allow for meaningful +// parameter names in optional methods which don't use that parameter pub trait PlatformHandler { /// Called when an app level menu item is selected. /// @@ -43,7 +43,9 @@ pub trait PlatformHandler { /// /// In future, this may also be used for selections in tray menus #[allow(unused_variables)] - fn app_menu_item_selected(&mut self, glz: Glazier, command: u32) {} + fn app_menu_item_selected(&mut self, glz: Glazier, command: u32) { + // TODO: Warn? If you have set a command, it seems reasonable to complain if you don't handle it? + } /// Called when a menu item associated with a window is selected. /// @@ -53,7 +55,39 @@ pub trait PlatformHandler { #[allow(unused_variables)] fn menu_item_selected(&mut self, glz: Glazier, win: WindowId, command: u32) {} + /// A surface can now be created for window `win`. + /// + /// This surface can accessed using [`Glazier::window_handle`] on `glz` + fn surface_available(&mut self, glz: Glazier, win: WindowId); + + // /// The surface associated with `win` is no longer active. In particular, + // /// you may not interact with that window *after* returning from this callback. + // /// + // /// This will only be called after [`surface_available`], but there is no + // /// guarantee that an intermediate [`paint`] will occur. + // /// + // /// [`surface_available`]: PlatformHandler::surface_available + // /// [`paint`]: PlatformHandler::paint + // fn surface_invalidated(&mut self, glz: Glazier, win: WindowId); + + /// Request the handler to prepare to paint the window contents. In particular, if there are + /// any regions that need to be repainted on the next call to `paint`, the handler should + /// invalidate those regions by calling [`WindowHandle::invalidate_rect`] or + /// [`WindowHandle::invalidate`]. + fn prepare_paint(&mut self, glz: Glazier, win: WindowId); + + /// Request the handler to paint the window contents. `invalid` is the region in [display + /// points](crate::Scale) that needs to be repainted; painting outside the invalid region + /// might have no effect. + fn paint(&mut self, glz: Glazier, win: WindowId, invalid: &Region); + + /// Creating a window failed + #[allow(unused_variables)] fn creating_window_failed(&mut self, glz: Glazier, win: WindowId, error: Error) { - todo!("Failed to create window {win:?}. Error: {error:?}"); + panic!("Failed to create window {win:?}. Error: {error:?}"); + } + + fn idle(&mut self, glz: Glazier, token: IdleToken) { + panic!("Your requested idle, but didn't implement PlatformHandler::idle") } } diff --git a/v2/src/lib.rs b/v2/src/lib.rs index 91dfc78d..ef71e69e 100644 --- a/v2/src/lib.rs +++ b/v2/src/lib.rs @@ -34,65 +34,29 @@ //! framework. It provides common types, which then defer to a platform-defined //! implementation. -use std::{marker::PhantomData, num::NonZeroU64}; +use std::marker::PhantomData; mod backend; mod handler; +mod window; -use glazier::Counter; pub use handler::PlatformHandler; +pub use window::{WindowDescription, WindowId}; -/// Manages communication with the platform -/// -/// Created using a `GlazierBuilder` +/// A short-lived handle for communication with the platform, +/// which is available whilst an event handler is being called pub struct Glazier<'a>(backend::GlazierImpl<'a>, PhantomData<&'a mut ()>); -pub struct WindowBuilder { - title: String, - // menu: Option, - // size: Size, - // min_size: Option, - // position: Option, - // level: Option, - // window_state: Option, - resizable: bool, - show_titlebar: bool, - transparent: bool, - id: Option, -} - -impl Default for WindowBuilder { - fn default() -> Self { - Self { - title: "Glazier Application Window".to_string(), - resizable: true, - show_titlebar: true, - transparent: false, - id: None, - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct WindowId(NonZeroU64); - -static WINDOW_ID_COUNTER: Counter = Counter::new(); - -impl WindowId { - pub(crate) fn next() -> Self { - Self(WINDOW_ID_COUNTER.next_nonzero()) - } -} - impl Glazier<'_> { - pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowBuilder)) -> WindowId { - let mut builder_instance = WindowBuilder::default(); + pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowDescription)) -> WindowId { + let mut builder_instance = WindowDescription::default(); builder(&mut builder_instance); self.new_window(builder_instance) } - pub fn new_window(&mut self, builder: WindowBuilder) -> WindowId { - self.0.new_window(builder) + pub fn new_window(&mut self, builder: WindowDescription) -> WindowId { + todo!(); + // self.0.new_window(builder) } /// Request that this `Glazier` stop controlling the current thread @@ -101,11 +65,15 @@ impl Glazier<'_> { pub fn stop(&mut self) { self.0.stop(); } + + // pub fn window_handle(&mut self, window: WindowId) -> NativeWindowHandle { + // NativeWindowHandle(self.0.window_handle()) + // } } /// Allows configuring a `Glazier` before initialising the system pub struct GlazierBuilder { - windows: Vec, + windows: Vec, } impl GlazierBuilder { @@ -114,18 +82,6 @@ impl GlazierBuilder { GlazierBuilder { windows: vec![] } } - pub fn build_window(&mut self, builder: impl FnOnce(&mut WindowBuilder)) -> WindowId { - let mut builder_instance = WindowBuilder::default(); - builder(&mut builder_instance); - self.new_window(builder_instance) - } - /// Queues the creation of a new window for when the `Glazier` is created - pub fn new_window(&mut self, mut builder: WindowBuilder) -> WindowId { - let id = builder.id.get_or_insert_with(WindowId::next).clone(); - self.windows.push(builder); - id - } - /// Start interacting with the platform /// /// Start handling events from the platform using `event_handler`. @@ -146,4 +102,13 @@ impl GlazierBuilder { // TODO: Proper error handling .unwrap() } + + /// Queues the creation of a new window for when the `Glazier` is created + pub fn new_window(&mut self, mut builder: WindowDescription) -> WindowId { + // TODO: Should the id be part of the descriptor? + // I don't see the harm in allowing early created ids, and it may allow greater flexibility + let id = builder.assign_id(); + self.windows.push(builder); + id + } } diff --git a/v2/src/window.rs b/v2/src/window.rs new file mode 100644 index 00000000..5f993daf --- /dev/null +++ b/v2/src/window.rs @@ -0,0 +1,60 @@ +use std::num::NonZeroU64; + +use glazier::Counter; + +pub struct WindowDescription { + pub title: String, + // menu: Option, + // size: Size, + // min_size: Option, + // position: Option, + // level: Option, + // window_state: Option, + pub resizable: bool, + pub show_titlebar: bool, + pub transparent: bool, + // TODO: Should the id live in the builder, + // and/or should these be Glazier specific? + // That would allow using the struct initialisation syntax (i.e. ..Default::default), + // which is tempting + pub(crate) id: Option, +} + +impl WindowDescription { + pub fn new(title: impl Into) -> Self { + WindowDescription { + title: title.into(), + resizable: true, + show_titlebar: true, + transparent: false, + id: None, + } + } + + pub fn id(&self) -> Option { + self.id + } + + pub fn assign_id(&mut self) -> WindowId { + *self.id.get_or_insert_with(WindowId::next) + } +} + +impl Default for WindowDescription { + fn default() -> Self { + Self::new("Glazier Application Window") + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct WindowId(NonZeroU64); + +static WINDOW_ID_COUNTER: Counter = Counter::new(); + +impl WindowId { + pub(crate) fn next() -> Self { + Self(WINDOW_ID_COUNTER.next_nonzero()) + } +} + +// pub struct NativeWindowHandle(backend::NativeWindowHandle); From 5a181385336dcb4e027d2ec6a2fbed0259001cfa Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 22 Dec 2023 21:45:30 +0000 Subject: [PATCH 05/10] Some more API porting work --- v2/Cargo.toml | 7 +- v2/src/backend/shared/xkb.rs | 40 +- v2/src/backend/shared/xkb/xkb_api.rs | 5 +- v2/src/backend/wayland/input/mod.rs | 25 +- v2/src/backend/wayland/input/text_input.rs | 14 +- v2/src/backend/wayland/mod.rs | 15 +- v2/src/backend/wayland/run_loop.rs | 25 +- v2/src/backend/wayland/window.rs | 195 ++---- v2/src/glazier.rs | 53 ++ v2/src/handler.rs | 33 +- v2/src/keyboard/events.rs | 88 +++ v2/src/keyboard/hotkey.rs | 243 +++++++ v2/src/keyboard/mod.rs | 7 + v2/src/lib.rs | 193 ++++-- v2/src/text/mod.rs | 767 +++++++++++++++++++++ v2/src/text/simulate.rs | 283 ++++++++ v2/src/util.rs | 47 ++ v2/src/window.rs | 131 +++- v2/src/window/region.rs | 113 +++ v2/src/window/scale.rs | 297 ++++++++ 20 files changed, 2304 insertions(+), 277 deletions(-) create mode 100644 v2/src/glazier.rs create mode 100644 v2/src/keyboard/events.rs create mode 100644 v2/src/keyboard/hotkey.rs create mode 100644 v2/src/keyboard/mod.rs create mode 100644 v2/src/text/mod.rs create mode 100644 v2/src/text/simulate.rs create mode 100644 v2/src/util.rs create mode 100644 v2/src/window/region.rs create mode 100644 v2/src/window/scale.rs diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 95ae14df..004044ea 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -18,7 +18,8 @@ wayland = [ [dependencies] -glazier = { path = "../" } +kurbo = "0.9.0" +# glazier = { path = "../" } cfg-if = "1.0.0" tracing = { version = "0.1.22", features = ["log"] } @@ -26,6 +27,10 @@ raw-window-handle = { version = "0.5.0", default_features = false } keyboard-types = { version = "0.7", default_features = false } instant = { version = "0.1.6", features = ["wasm-bindgen"] } +thiserror = "1.0.51" + +[dev-dependencies] +static_assertions = "1.1.0" [target.'cfg(any(target_os = "freebsd", target_os="linux", target_os="openbsd"))'.dependencies] ashpd = { version = "0.5", optional = true } diff --git a/v2/src/backend/shared/xkb.rs b/v2/src/backend/shared/xkb.rs index 14d0a864..c367b0dc 100644 --- a/v2/src/backend/shared/xkb.rs +++ b/v2/src/backend/shared/xkb.rs @@ -1,10 +1,7 @@ mod xkb_api; pub use xkb_api::*; -use glazier::{ - text::{simulate_compose, InputHandler}, - KeyEvent, -}; +use crate::{keyboard::KeyEvent, text::InputHandler}; mod keycodes; mod xkbcommon_sys; @@ -53,21 +50,22 @@ pub fn xkb_simulate_input( // that case, and this is only a minor inefficiency, but it's better to be sure handler: &mut dyn InputHandler, ) -> KeyboardHandled { - let compose_result = xkb_state.compose_key_down(event, keysym); - let result_if_update_occurs = match compose_result { - glazier::text::CompositionResult::NoComposition => KeyboardHandled::UpdatedNoCompose, - glazier::text::CompositionResult::Cancelled(_) - | glazier::text::CompositionResult::Finished(_) => KeyboardHandled::UpdatedReleasingCompose, - glazier::text::CompositionResult::Updated { just_started, .. } if just_started => { - KeyboardHandled::UpdatedClaimingCompose - } - glazier::text::CompositionResult::Updated { .. } => { - KeyboardHandled::UpdatedRetainingCompose - } - }; - if simulate_compose(handler, event, compose_result) { - result_if_update_occurs - } else { - KeyboardHandled::NoUpdate - } + todo!(); + // let compose_result = xkb_state.compose_key_down(event, keysym); + // let result_if_update_occurs = match compose_result { + // glazier::text::CompositionResult::NoComposition => KeyboardHandled::UpdatedNoCompose, + // glazier::text::CompositionResult::Cancelled(_) + // | glazier::text::CompositionResult::Finished(_) => KeyboardHandled::UpdatedReleasingCompose, + // glazier::text::CompositionResult::Updated { just_started, .. } if just_started => { + // KeyboardHandled::UpdatedClaimingCompose + // } + // glazier::text::CompositionResult::Updated { .. } => { + // KeyboardHandled::UpdatedRetainingCompose + // } + // }; + // if simulate_compose(handler, event, compose_result) { + // result_if_update_occurs + // } else { + // KeyboardHandled::NoUpdate + // } } diff --git a/v2/src/backend/shared/xkb/xkb_api.rs b/v2/src/backend/shared/xkb/xkb_api.rs index 9ef0c696..e8feab29 100644 --- a/v2/src/backend/shared/xkb/xkb_api.rs +++ b/v2/src/backend/shared/xkb/xkb_api.rs @@ -16,8 +16,9 @@ use super::{keycodes, xkbcommon_sys::*}; use crate::backend::shared::{code_to_location, hardware_keycode_to_code, linux}; -use glazier::{text::CompositionResult, KeyEvent, KeyState, Modifiers}; -use keyboard_types::{Code, Key}; +use crate::keyboard::KeyEvent; +use crate::text::simulate::CompositionResult; +use keyboard_types::{Code, Key, KeyState, Modifiers}; use std::{convert::TryFrom, ffi::CString}; use std::{os::raw::c_char, ptr::NonNull}; diff --git a/v2/src/backend/wayland/input/mod.rs b/v2/src/backend/wayland/input/mod.rs index 5ed06310..7d6d0438 100644 --- a/v2/src/backend/wayland/input/mod.rs +++ b/v2/src/backend/wayland/input/mod.rs @@ -4,9 +4,11 @@ use std::{ rc::{Rc, Weak}, }; -use glazier::{text::InputHandler, Counter, TextFieldToken, WinHandler}; - -use crate::backend::shared::xkb::{xkb_simulate_input, KeyboardHandled}; +use crate::{ + backend::shared::xkb::{xkb_simulate_input, KeyboardHandled}, + text::{InputHandler, TextFieldToken}, + util::Counter, +}; use self::{keyboard::KeyboardState, text_input::InputState}; @@ -120,8 +122,7 @@ pub(in crate::backend::wayland) struct TextInputProperties { pub(in crate::backend::wayland) type TextInputCell = Rc>; pub(in crate::backend::wayland) type WeakTextInputCell = Weak>; -struct FutureInputLock<'a> { - handler: &'a mut dyn WinHandler, +struct FutureInputLock { token: TextFieldToken, // Whether the field has been updated by the application since the last // execution. This effectively means that it isn't meaningful for the IME to @@ -166,11 +167,10 @@ impl SeatInfo { let window = windows.get_mut(&old_focus); if let Some(window) = window { window.remove_input_seat(self.id); - let TextFieldDetails(handler, props) = window_handler(window); + let TextFieldDetails(props) = window_handler(window); handler.lost_focus(); let props = props.get(); self.force_release_preedit(props.active_text_field.map(|it| FutureInputLock { - handler, token: it, field_updated: props.active_text_field_updated, })); @@ -184,7 +184,7 @@ impl SeatInfo { fn update_active_text_input( &mut self, - TextFieldDetails(handler, props_cell): &mut TextFieldDetails, + TextFieldDetails(props_cell): &mut TextFieldDetails, mut force: bool, should_update_text_input: bool, ) { @@ -197,7 +197,6 @@ impl SeatInfo { focus_changed = props.next_text_field != previous; if focus_changed { self.force_release_preedit(previous.map(|it| FutureInputLock { - handler, token: it, field_updated: props.active_text_field_updated, })); @@ -213,7 +212,6 @@ impl SeatInfo { force = false; if !focus_changed { self.force_release_preedit(props.active_text_field.map(|it| FutureInputLock { - handler, token: it, field_updated: true, })); @@ -294,7 +292,6 @@ impl SeatInfo { "If the keyboard has claimed the input, it must be composing" ); if let Some(FutureInputLock { - handler, token, field_updated, }) = field @@ -315,7 +312,7 @@ impl SeatInfo { } } TextFieldOwner::TextInput => { - if let Some(FutureInputLock { handler, token, .. }) = field { + if let Some(FutureInputLock { token, .. }) = field { // The Wayland text input interface does not permit the IME to respond to an input // becoming unfocused. let mut ime = handler.acquire_input_lock(token, true); @@ -478,10 +475,10 @@ impl SeatInfo { } } -struct TextFieldDetails<'a>(&'a mut dyn WinHandler, TextInputCell); +struct TextFieldDetails(TextInputCell); /// Get the text input information for the given window -fn handler<'a>(windows: &'a mut Windows, window: &WindowId) -> Option> { +fn handler(windows: &mut Windows, window: &WindowId) -> Option { let window = &mut *windows.get_mut(window)?; Some(window_handler(window)) } diff --git a/v2/src/backend/wayland/input/text_input.rs b/v2/src/backend/wayland/input/text_input.rs index f9b13081..edc37dda 100644 --- a/v2/src/backend/wayland/input/text_input.rs +++ b/v2/src/backend/wayland/input/text_input.rs @@ -6,14 +6,12 @@ use smithay_client_toolkit::reexports::{ }, }; -use glazier::{ - text::{Affinity, InputHandler, Selection}, - TextFieldToken, -}; - -use crate::backend::{ - wayland::{WaylandPlatform, WaylandState}, - window::WindowId, +use crate::{ + backend::{ + wayland::{WaylandPlatform, WaylandState}, + window::WindowId, + }, + text::{Affinity, InputHandler, Selection, TextFieldToken}, }; use super::{input_state, SeatInfo, SeatName}; diff --git a/v2/src/backend/wayland/mod.rs b/v2/src/backend/wayland/mod.rs index 8a3207f5..10653ce9 100644 --- a/v2/src/backend/wayland/mod.rs +++ b/v2/src/backend/wayland/mod.rs @@ -15,6 +15,7 @@ //! wayland platform support use std::{ + any::TypeId, collections::{HashMap, VecDeque}, marker::PhantomData, ops::{Deref, DerefMut}, @@ -35,7 +36,7 @@ use smithay_client_toolkit::{ shell::xdg::XdgShell, }; -use crate::{handler::PlatformHandler, Glazier}; +use crate::{handler::PlatformHandler, window::IdleToken, Glazier}; use self::{ input::SeatInfo, @@ -50,7 +51,9 @@ mod run_loop; mod screen; pub mod window; -pub use run_loop::launch; +pub use window::BackendWindowCreationError; + +pub use run_loop::{launch, LoopHandle as LoopHandle2}; pub(crate) type GlazierImpl<'a> = &'a mut WaylandState; @@ -70,9 +73,9 @@ pub(crate) struct WaylandState { pub(self) output_state: OutputState, // TODO: Do we need to keep this around // It is unused because(?) wgpu creates the surfaces through RawDisplayHandle(?) - pub(self) _compositor_state: CompositorState, + pub(self) compositor_state: CompositorState, // Is used: Keep the XdgShell alive, which is a Weak in all Handles - pub(self) _xdg_shell_state: XdgShell, + pub(self) xdg_shell_state: XdgShell, pub(self) wayland_queue: QueueHandle, pub(self) loop_signal: LoopSignal, @@ -87,6 +90,8 @@ pub(crate) struct WaylandState { pub(self) idle_actions: Vec, pub(self) actions: VecDeque, pub(self) loop_sender: calloop::channel::Sender, + + pub(self) handler_type: TypeId, } delegate_registry!(WaylandPlatform); @@ -129,7 +134,7 @@ enum ActiveAction { enum IdleAction { Callback(LoopCallback), - Token(glazier::IdleToken), + Token(IdleToken), } type LoopCallback = Box; diff --git a/v2/src/backend/wayland/run_loop.rs b/v2/src/backend/wayland/run_loop.rs index f0acc964..b6fcf125 100644 --- a/v2/src/backend/wayland/run_loop.rs +++ b/v2/src/backend/wayland/run_loop.rs @@ -15,8 +15,8 @@ #![allow(clippy::single_match)] use std::{ + any::TypeId, collections::{HashMap, VecDeque}, - rc::Rc, }; use smithay_client_toolkit::{ @@ -44,8 +44,8 @@ use crate::{ }; pub fn launch( - handler: Box, - on_init: impl FnOnce(Glazier), + mut handler: Box, + on_init: impl FnOnce(&mut dyn PlatformHandler, Glazier), ) -> Result<(), Error> { tracing::info!("wayland application initiated"); @@ -92,8 +92,8 @@ pub fn launch( let state = WaylandState { registry_state: RegistryState::new(&globals), output_state: OutputState::new(&globals, &qh), - _compositor_state: compositor_state, - _xdg_shell_state: shell, + compositor_state, + xdg_shell_state: shell, windows: HashMap::new(), wayland_queue: qh.clone(), loop_signal: loop_signal.clone(), @@ -106,12 +106,13 @@ pub fn launch( actions: VecDeque::new(), idle_actions: Vec::new(), loop_sender, + handler_type: handler.as_any().type_id(), }; let mut platform = WaylandPlatform { handler, state }; platform.initial_seats(); tracing::info!("wayland event loop initiated"); - platform.with_glz(|_, glz| on_init(glz)); + platform.with_glz(|handler, glz| on_init(handler, glz)); event_loop .run(None, &mut platform, |platform| { let mut idle_actions = std::mem::take(&mut platform.idle_actions); @@ -141,7 +142,14 @@ impl WaylandState { self.loop_signal.stop() } - pub(crate) fn handle(&mut self) -> LoopHandle { + pub(crate) fn raw_handle(&mut self) -> LoopHandle { + LoopHandle { + loop_sender: self.loop_sender.clone(), + } + } + + pub(crate) fn typed_handle(&mut self, handler_type: TypeId) -> LoopHandle { + assert_eq!(self.handler_type, handler_type); LoopHandle { loop_sender: self.loop_sender.clone(), } @@ -164,7 +172,8 @@ impl LoopHandle { { Ok(()) => (), Err(err) => { - tracing::warn!("Sending idle event loop failed: {err:?}") + tracing::warn!("Sending to event loop failed: {err:?}") + // TODO: Return an error here? } }; } diff --git a/v2/src/backend/wayland/window.rs b/v2/src/backend/wayland/window.rs index 687b0451..673a4a1e 100644 --- a/v2/src/backend/wayland/window.rs +++ b/v2/src/backend/wayland/window.rs @@ -11,39 +11,33 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(clippy::single_match)] - -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::os::raw::c_void; -use std::rc::{Rc, Weak}; +use std::rc::Weak; use std::sync::mpsc::{self, Sender}; +use kurbo_0_9::{Rect, Size}; use smithay_client_toolkit::compositor::CompositorHandler; use smithay_client_toolkit::reexports::calloop::{channel, LoopHandle}; -use smithay_client_toolkit::reexports::client::protocol::wl_compositor::WlCompositor; use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface; use smithay_client_toolkit::reexports::client::{protocol, Connection, Proxy, QueueHandle}; use smithay_client_toolkit::shell::xdg::window::{ Window, WindowConfigure, WindowDecorations, WindowHandler, }; -use smithay_client_toolkit::shell::xdg::XdgShell; use smithay_client_toolkit::shell::WaylandSurface; use smithay_client_toolkit::{delegate_compositor, delegate_xdg_shell, delegate_xdg_window}; +use thiserror::Error; use tracing; use wayland_backend::client::ObjectId; -use crate::PlatformHandler; +use crate::window::{IdleToken, Region, Scalable, Scale}; +use crate::{PlatformHandler, WindowDescription}; use super::input::{ - input_state, SeatName, TextFieldChange, TextInputCell, TextInputProperties, WeakTextInputCell, + input_state, SeatName, TextFieldChange, TextInputProperties, WeakTextInputCell, }; use super::{ActiveAction, IdleAction, WaylandPlatform, WaylandState}; -use glazier::{ - kurbo::{Point, Rect, Size}, - Error as ShellError, IdleToken, Region, Scalable, Scale, WinHandler, WindowLevel, -}; - #[derive(Clone)] pub struct WindowHandle { idle_sender: Sender, @@ -54,6 +48,9 @@ pub struct WindowHandle { raw_display_handle: Option<*mut c_void>, } +#[derive(Error, Debug)] +pub enum BackendWindowCreationError {} + // impl WindowHandle { // fn id(&self) -> WindowId { // let props = self.properties(); @@ -391,103 +388,24 @@ impl IdleHandle { #[derive(Clone, PartialEq, Eq)] pub struct CustomCursor; -/// Builder abstraction for creating new windows -pub(crate) struct WindowBuilder { - handler: Option>, - title: String, - position: Option, - level: WindowLevel, - state: Option, - // pre-scaled - size: Option, - min_size: Option, - resizable: bool, - show_titlebar: bool, - compositor: WlCompositor, - wayland_queue: QueueHandle, - loop_handle: LoopHandle<'static, WaylandPlatform>, - xdg_state: Weak, - idle_sender: Sender, - loop_sender: channel::Sender, - raw_display_handle: *mut c_void, -} - -impl WindowBuilder { - pub fn handler(mut self, handler: Box) -> Self { - self.handler = Some(handler); - self - } - - pub fn size(mut self, size: Size) -> Self { - self.size = Some(size); - self - } - - pub fn min_size(mut self, size: Size) -> Self { - self.min_size = Some(size); - self - } - - pub fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; - self - } - - pub fn show_titlebar(mut self, show_titlebar: bool) -> Self { - self.show_titlebar = show_titlebar; - self - } - - pub fn transparent(self, _transparent: bool) -> Self { - tracing::info!( - "WindowBuilder::transparent is unimplemented for Wayland, it allows transparency by default" - ); - self - } - - pub fn position(mut self, position: Point) -> Self { - self.position = Some(position); - self - } - - pub fn level(mut self, level: WindowLevel) -> Self { - self.level = level; - self - } - - pub fn window_state(mut self, state: glazier::WindowState) -> Self { - self.state = Some(state); - self - } - - pub fn title(mut self, title: impl Into) -> Self { - self.title = title.into(); - self - } - - pub fn build(self) -> Result { - let surface = self - .compositor - .create_surface(&self.wayland_queue, Default::default()); - let xdg_shell = self - .xdg_state - .upgrade() - .expect("Can only build whilst event loop hasn't ended"); - let wayland_window = xdg_shell.create_window( +impl WaylandState { + pub(crate) fn new_window(&mut self, mut desc: WindowDescription) -> crate::WindowId { + let window_id = desc.assign_id(); + let surface = self.compositor_state.create_surface(&self.wayland_queue); + let wayland_window = self.xdg_shell_state.create_window( surface, // Request server decorations, because we don't yet do client decorations properly WindowDecorations::RequestServer, &self.wayland_queue, ); - wayland_window.set_title(self.title); + wayland_window.set_title(desc.title); // TODO: Pass this down wayland_window.set_app_id("org.linebender.glazier.user_app"); // TODO: Convert properly, set all properties // wayland_window.set_min_size(self.min_size); - let window_id = WindowId::new(&wayland_window); let properties = WindowProperties { configure: None, - requested_size: self.size, + requested_size: None, // This is just used as the default sizes, as we don't call `size` until the requested size is used current_size: Size::new(600., 800.), current_scale: Scale::new(1., 1.), // TODO: NaN? - these values should (must?) not be used @@ -498,44 +416,29 @@ impl WindowBuilder { pending_frame_callback: false, configured: false, }; - let properties_strong = Rc::new(RefCell::new(properties)); - let properties = Rc::downgrade(&properties_strong); - let text = Rc::new(Cell::new(TextInputProperties { + let text = TextInputProperties { active_text_field: None, next_text_field: None, active_text_field_updated: false, active_text_layout_changed: false, - })); - let handle = WindowHandle { - idle_sender: self.idle_sender, - loop_sender: self.loop_sender.clone(), - raw_display_handle: Some(self.raw_display_handle), - properties, - text: Rc::downgrade(&text), }; - // TODO: When should Window::commit be called? This feels fragile - self.loop_sender - .send(ActiveAction::Window( - window_id, - WindowAction::Create(WaylandWindowState { - properties: properties_strong, - text_input_seat: None, - text, - handle: Some(handle.clone()), - }), - )) - .expect("Event loop should still be valid"); - - Ok(handle) + self.windows.insert( + WindowId::of_surface(&surface), + WaylandWindowState { + properties, + text_input_seat: None, + text, + }, + ); + window_id } } - #[derive(Clone, PartialEq, Eq, Hash, Debug)] // TODO: According to https://github.com/linebender/druid/pull/2033, this should not be // synced with the ID of the surface -pub(super) struct WindowId(ObjectId); +pub(super) struct WindowId(ObjectId); impl WindowId { pub fn new(surface: &impl WaylandSurface) -> Self { Self::of_surface(surface.wl_surface()) @@ -548,14 +451,9 @@ impl WindowId { /// The state associated with each window, stored in [`WaylandState`] pub(super) struct WaylandWindowState { // TODO: This refcell is too strong - most of the fields can just be Cells - properties: Rc>, + properties: WindowProperties, text_input_seat: Option, - pub text: TextInputCell, - // The handle which will be used to access this window - // Will be passed to the `connect` handler in initial configure - // Cheap to clone, but kept in an option to track whether - // `connect` has been sent - handle: Option, + pub text: TextInputProperties, } struct WindowProperties { @@ -649,7 +547,7 @@ enum PaintContext { impl WaylandWindowState { fn do_paint(&mut self, force: bool, context: PaintContext) { { - let mut props = self.properties.borrow_mut(); + let mut props = self.properties; if matches!(context, PaintContext::Frame) { props.pending_frame_callback = false; } @@ -680,7 +578,7 @@ impl WaylandWindowState { // When forcing, should mark the entire region as damaged let mut region = Region::EMPTY; { - let props = self.properties.borrow(); + let props = self.properties; let size = props.current_size.to_dp(props.current_scale); region.add_rect(Rect { x0: 0.0, @@ -722,20 +620,21 @@ impl CompositorHandler for WaylandPlatform { let window = window.expect("Should only get events for real windows"); let factor = f64::from(new_factor); let scale = Scale::new(factor, factor); - let new_size; - { - let mut props = window.properties.borrow_mut(); - // TODO: Effectively, we need to re-evaluate the size calculation - // That means we need to cache the WindowConfigure or (mostly) equivalent - let cur_size_raw: Size = props.current_size.to_px(props.current_scale); - new_size = cur_size_raw.to_dp(scale); - props.current_scale = scale; - props.current_size = new_size; - // avoid locking the properties into user code - } - todo!("HANDLER"); - // window.handler.scale(scale); - // window.handler.size(new_size); + + let mut props = window.properties; + // TODO: Effectively, we need to re-evaluate the size calculation + // That means we need to cache the WindowConfigure or (mostly) equivalent + let cur_size_raw: Size = props.current_size.to_px(props.current_scale); + let new_size = cur_size_raw.to_dp(scale); + props.current_scale = scale; + props.current_size = new_size; + self.with_glz(move |handler, glz| { + let new_size = new_size; + let new_scale = scale; + todo!("Handle ") + // handler.scale(glz, windowscale); + // handler.size(new_size); + }); // TODO: Do we repaint here? } diff --git a/v2/src/glazier.rs b/v2/src/glazier.rs new file mode 100644 index 00000000..5e7da96d --- /dev/null +++ b/v2/src/glazier.rs @@ -0,0 +1,53 @@ +use std::{any::TypeId, marker::PhantomData}; + +use crate::{backend, LoopHandle, PlatformHandler, RawLoopHandle, WindowDescription, WindowId}; + +/// A short-lived handle for communication with the platform, +/// which is available whilst an event handler is being called +// TODO: Assert ¬Send, ¬Sync +pub struct Glazier<'a>( + pub(crate) backend::GlazierImpl<'a>, + pub(crate) PhantomData<&'a mut ()>, +); + +impl Glazier<'_> { + pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowDescription)) -> WindowId { + let mut builder_instance = WindowDescription::default(); + builder(&mut builder_instance); + self.new_window(builder_instance) + } + + pub fn new_window(&mut self, desc: WindowDescription) -> WindowId { + self.0.new_window(desc) + } + + /// Request that this Glazier stop controlling the current thread + /// + /// This should be called after all windows have been closed + pub fn stop(&mut self) { + self.0.stop(); + } + + /// Get a handle that can be used to schedule tasks on the application loop. + /// + /// # Panics + /// + /// If `H` is not the type of the [PlatformHandler] this Glazier was [launch]ed + /// using + /// + /// [launch]: crate::GlazierBuilder::launch + pub fn handle(&mut self) -> LoopHandle { + let ty_id = TypeId::of::(); + LoopHandle(RawLoopHandle(self.0.typed_handle(ty_id)), PhantomData) + } + + /// Get a handle that can be used to schedule tasks on an application loop + /// with any implementor of [PlatformHandler]. + pub fn raw_handle(&mut self) -> RawLoopHandle { + RawLoopHandle(self.0.raw_handle()) + } + + // pub fn window_handle(&mut self, window: WindowId) -> NativeWindowHandle { + // NativeWindowHandle(self.0.window_handle()) + // } +} diff --git a/v2/src/handler.rs b/v2/src/handler.rs index 819a9943..6bf43633 100644 --- a/v2/src/handler.rs +++ b/v2/src/handler.rs @@ -1,6 +1,9 @@ -use glazier::{Error, IdleToken, Region}; +use std::any::Any; -use crate::{Glazier, WindowId}; +use crate::{ + window::{IdleToken, Region, WindowCreationError}, + Glazier, WindowId, +}; /// The primary trait which must be implemented by your application state /// @@ -36,7 +39,7 @@ use crate::{Glazier, WindowId}; /// // Methods have the `#[allow(unused_variables)]` attribute to allow for meaningful // parameter names in optional methods which don't use that parameter -pub trait PlatformHandler { +pub trait PlatformHandler: Any { /// Called when an app level menu item is selected. /// /// This is primarily useful on macOS, where the menu can exist even when @@ -58,6 +61,7 @@ pub trait PlatformHandler { /// A surface can now be created for window `win`. /// /// This surface can accessed using [`Glazier::window_handle`] on `glz` + // TODO: Pass in size/scale(!?) fn surface_available(&mut self, glz: Glazier, win: WindowId); // /// The surface associated with `win` is no longer active. In particular, @@ -72,9 +76,10 @@ pub trait PlatformHandler { /// Request the handler to prepare to paint the window contents. In particular, if there are /// any regions that need to be repainted on the next call to `paint`, the handler should - /// invalidate those regions by calling [`WindowHandle::invalidate_rect`] or - /// [`WindowHandle::invalidate`]. - fn prepare_paint(&mut self, glz: Glazier, win: WindowId); + /// invalidate those regions by calling [`Glazier::invalidate_rect`] or + /// [`Glazier::invalidate`]. + #[allow(unused_variables)] + fn prepare_paint(&mut self, glz: Glazier, win: WindowId) {} /// Request the handler to paint the window contents. `invalid` is the region in [display /// points](crate::Scale) that needs to be repainted; painting outside the invalid region @@ -83,11 +88,23 @@ pub trait PlatformHandler { /// Creating a window failed #[allow(unused_variables)] - fn creating_window_failed(&mut self, glz: Glazier, win: WindowId, error: Error) { + fn creating_window_failed(&mut self, glz: Glazier, win: WindowId, error: WindowCreationError) { panic!("Failed to create window {win:?}. Error: {error:?}"); } + #[allow(unused_variables)] fn idle(&mut self, glz: Glazier, token: IdleToken) { - panic!("Your requested idle, but didn't implement PlatformHandler::idle") + panic!("You requested idle, but didn't implement PlatformHandler::idle") } + + /// Get a reference to `self`. Used by [crate::LoopHandle::run_on_main]. + /// The implementation should be `self`, that is: + /// ```rust + /// # use core::any::Any; + /// fn as_any(&mut self) -> &mut dyn Any { + /// self + /// } + /// ``` + // N.B. Implemented by users, so don't rely upon for safety + fn as_any(&mut self) -> &mut dyn Any; } diff --git a/v2/src/keyboard/events.rs b/v2/src/keyboard/events.rs new file mode 100644 index 00000000..4ad01cae --- /dev/null +++ b/v2/src/keyboard/events.rs @@ -0,0 +1,88 @@ +// 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. + +//! Keyboard types. + +use keyboard_types::{Code, KeyState, Location, Modifiers}; + +/// The meaning (mapped value) of a keypress. +pub type KbKey = keyboard_types::Key; + +/// Information about a keyboard event. +/// +/// Note that this type is similar to [`KeyboardEvent`] in keyboard-types, +/// but has a few small differences for convenience. +/// +/// [`KeyboardEvent`]: keyboard_types::KeyboardEvent +#[non_exhaustive] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct KeyEvent { + /// Whether the key is pressed or released. + pub state: KeyState, + /// Logical key value. + pub key: KbKey, + /// Physical key position. + pub code: Code, + /// Location for keys with multiple instances on common keyboards. + pub location: Location, + /// Flags for pressed modifier keys. + pub mods: Modifiers, + /// True if the key is currently auto-repeated. + pub repeat: bool, + /// Events with this flag should be ignored in a text editor + /// and instead composition events should be used. + pub is_composing: bool, +} + +/// A convenience trait for creating Key objects. +/// +/// This trait is implemented by [`KbKey`] itself and also strings, which are +/// converted into the `Character` variant. It is defined this way and not +/// using the standard `Into` mechanism because `KbKey` is a type in an external +/// crate. +/// +/// [`KbKey`]: KbKey +pub trait IntoKey { + fn into_key(self) -> KbKey; +} + +impl KeyEvent { + #[doc(hidden)] + /// Create a key event for testing purposes. + pub fn for_test(mods: impl Into, key: impl IntoKey) -> KeyEvent { + let mods = mods.into(); + let key = key.into_key(); + KeyEvent { + key, + code: Code::Unidentified, + location: Location::Standard, + state: KeyState::Down, + mods, + is_composing: false, + repeat: false, + } + } +} + +impl IntoKey for KbKey { + fn into_key(self) -> KbKey { + self + } +} + +impl IntoKey for &str { + fn into_key(self) -> KbKey { + KbKey::Character(self.into()) + } +} diff --git a/v2/src/keyboard/hotkey.rs b/v2/src/keyboard/hotkey.rs new file mode 100644 index 00000000..ed579aa8 --- /dev/null +++ b/v2/src/keyboard/hotkey.rs @@ -0,0 +1,243 @@ +// 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. + +//! Hotkeys and helpers for parsing keyboard shortcuts. + +use std::borrow::Borrow; + +use tracing::warn; + +use crate::keyboard::{IntoKey, KbKey, KeyEvent, Modifiers}; + +// TODO: fix docstring + +/// A description of a keyboard shortcut. +/// +/// This type is only intended to be used to describe shortcuts, +/// and recognize them when they arrive. +/// +/// # Examples +/// +/// [`SysMods`] matches the Command key on macOS and Ctrl elsewhere: +/// +/// ``` +/// use glazier::{HotKey, KbKey, KeyEvent, RawMods, SysMods}; +/// +/// let hotkey = HotKey::new(SysMods::Cmd, "a"); +/// +/// #[cfg(target_os = "macos")] +/// assert!(hotkey.matches(KeyEvent::for_test(RawMods::Meta, "a"))); +/// +/// #[cfg(target_os = "windows")] +/// assert!(hotkey.matches(KeyEvent::for_test(RawMods::Ctrl, "a"))); +/// ``` +/// +/// `None` matches only the key without modifiers: +/// +/// ``` +/// use glazier::{HotKey, KbKey, KeyEvent, RawMods, SysMods}; +/// +/// let hotkey = HotKey::new(None, KbKey::ArrowLeft); +/// +/// assert!(hotkey.matches(KeyEvent::for_test(RawMods::None, KbKey::ArrowLeft))); +/// assert!(!hotkey.matches(KeyEvent::for_test(RawMods::Ctrl, KbKey::ArrowLeft))); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HotKey { + pub(crate) mods: RawMods, + pub(crate) key: KbKey, +} + +impl HotKey { + /// Create a new hotkey. + /// + /// The first argument describes the keyboard modifiers. This can be `None`, + /// or an instance of either [`SysMods`], or [`RawMods`]. [`SysMods`] unify the + /// 'Command' key on macOS with the 'Ctrl' key on other platforms. + /// + /// The second argument describes the non-modifier key. This can be either + /// a `&str` or a [`KbKey`]; the former is merely a convenient + /// shorthand for `KbKey::Character()`. + /// + /// # Examples + /// ``` + /// use glazier::{HotKey, KbKey, RawMods, SysMods}; + /// + /// let select_all = HotKey::new(SysMods::Cmd, "a"); + /// let esc = HotKey::new(None, KbKey::Escape); + /// let macos_fullscreen = HotKey::new(RawMods::CtrlMeta, "f"); + /// ``` + /// + /// [`Key`]: keyboard_types::Key + pub fn new(mods: impl Into>, key: impl IntoKey) -> Self { + HotKey { + mods: mods.into().unwrap_or(RawMods::None), + key: key.into_key(), + } + .warn_if_needed() + } + + //TODO: figure out if we need to be normalizing case or something? + fn warn_if_needed(self) -> Self { + if let KbKey::Character(s) = &self.key { + let km: Modifiers = self.mods.into(); + if km.shift() && s.chars().any(|c| c.is_lowercase()) { + warn!( + "warning: HotKey {:?} includes shift, but text is lowercase. \ + Text is matched literally; this may cause problems.", + &self + ); + } + } + self + } + + /// Returns `true` if this [`KeyEvent`] matches this `HotKey`. + /// + /// [`KeyEvent`]: KeyEvent + pub fn matches(&self, event: impl Borrow) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::META; + let event = event.borrow(); + self.mods == event.mods & base_mods && self.key == event.key + } +} + +/// A platform-agnostic representation of keyboard modifiers, for command handling. +/// +/// This does one thing: it allows specifying hotkeys that use the Command key +/// on macOS, but use the Ctrl key on other platforms. +#[derive(Debug, Clone, Copy)] +pub enum SysMods { + None, + Shift, + /// Command on macOS, and Ctrl on windows/linux/OpenBSD + Cmd, + /// Command + Alt on macOS, Ctrl + Alt on windows/linux/OpenBSD + AltCmd, + /// Command + Shift on macOS, Ctrl + Shift on windows/linux/OpenBSD + CmdShift, + /// Command + Alt + Shift on macOS, Ctrl + Alt + Shift on windows/linux/OpenBSD + AltCmdShift, +} + +//TODO: should something like this just _replace_ keymodifiers? +/// A representation of the active modifier keys. +/// +/// This is intended to be clearer than `Modifiers`, when describing hotkeys. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawMods { + None, + Alt, + Ctrl, + Meta, + Shift, + AltCtrl, + AltMeta, + AltShift, + CtrlShift, + CtrlMeta, + MetaShift, + AltCtrlMeta, + AltCtrlShift, + AltMetaShift, + CtrlMetaShift, + AltCtrlMetaShift, +} + +impl std::cmp::PartialEq for RawMods { + fn eq(&self, other: &Modifiers) -> bool { + let mods: Modifiers = (*self).into(); + mods == *other + } +} + +impl std::cmp::PartialEq for Modifiers { + fn eq(&self, other: &RawMods) -> bool { + other == self + } +} + +impl std::cmp::PartialEq for SysMods { + fn eq(&self, other: &Modifiers) -> bool { + let mods: RawMods = (*self).into(); + mods == *other + } +} + +impl std::cmp::PartialEq for Modifiers { + fn eq(&self, other: &SysMods) -> bool { + let other: RawMods = (*other).into(); + &other == self + } +} + +impl From for Modifiers { + fn from(src: RawMods) -> Modifiers { + let (alt, ctrl, meta, shift) = match src { + RawMods::None => (false, false, false, false), + RawMods::Alt => (true, false, false, false), + RawMods::Ctrl => (false, true, false, false), + RawMods::Meta => (false, false, true, false), + RawMods::Shift => (false, false, false, true), + RawMods::AltCtrl => (true, true, false, false), + RawMods::AltMeta => (true, false, true, false), + RawMods::AltShift => (true, false, false, true), + RawMods::CtrlMeta => (false, true, true, false), + RawMods::CtrlShift => (false, true, false, true), + RawMods::MetaShift => (false, false, true, true), + RawMods::AltCtrlMeta => (true, true, true, false), + RawMods::AltMetaShift => (true, false, true, true), + RawMods::AltCtrlShift => (true, true, false, true), + RawMods::CtrlMetaShift => (false, true, true, true), + RawMods::AltCtrlMetaShift => (true, true, true, true), + }; + let mut mods = Modifiers::empty(); + mods.set(Modifiers::ALT, alt); + mods.set(Modifiers::CONTROL, ctrl); + mods.set(Modifiers::META, meta); + mods.set(Modifiers::SHIFT, shift); + mods + } +} + +// we do this so that HotKey::new can accept `None` as an initial argument. +impl From for Option { + fn from(src: SysMods) -> Option { + Some(src.into()) + } +} + +impl From for RawMods { + fn from(src: SysMods) -> RawMods { + #[cfg(target_os = "macos")] + match src { + SysMods::None => RawMods::None, + SysMods::Shift => RawMods::Shift, + SysMods::Cmd => RawMods::Meta, + SysMods::AltCmd => RawMods::AltMeta, + SysMods::CmdShift => RawMods::MetaShift, + SysMods::AltCmdShift => RawMods::AltMetaShift, + } + #[cfg(not(target_os = "macos"))] + match src { + SysMods::None => RawMods::None, + SysMods::Shift => RawMods::Shift, + SysMods::Cmd => RawMods::Ctrl, + SysMods::AltCmd => RawMods::AltCtrl, + SysMods::CmdShift => RawMods::CtrlShift, + SysMods::AltCmdShift => RawMods::AltCtrlShift, + } + } +} diff --git a/v2/src/keyboard/mod.rs b/v2/src/keyboard/mod.rs new file mode 100644 index 00000000..e88e3297 --- /dev/null +++ b/v2/src/keyboard/mod.rs @@ -0,0 +1,7 @@ +mod events; +mod hotkey; + +pub use events::*; +pub use hotkey::*; + +pub use keyboard_types::{self, Code, KeyState, Location, Modifiers}; diff --git a/v2/src/lib.rs b/v2/src/lib.rs index ef71e69e..49bfc9dd 100644 --- a/v2/src/lib.rs +++ b/v2/src/lib.rs @@ -4,24 +4,32 @@ //! # Example //! //! ```rust,no_run -//! # use v2::{WindowId, GlazierBuilder}; +//! # use v2::{WindowId, GlazierBuilder, PlatformHandler, WindowDescription}; +//! # use core::any::Any; +//! # struct Surface; //! struct UiState { -//! main_window_id: WindowId; +//! main_window_id: WindowId, +//! main_window_surface: Option, //! } //! -//! impl UiHandler for UiState { +//! impl PlatformHandler for UiState { +//! fn as_any(&mut self)-> { self } //! // .. //! } //! //! let mut platform = GlazierBuilder::new(); -//! let main_window_id = platform.build_window(|window_builder| { -//! window_builder.title("Main Window") -//! .logical_size((600., 400.)); -//! }); +//! let mut main_window = WindowDescription { +//! # /* +//! logical_size: (600., 400.).into(), +//! # */ +//! ..WindowDescription::new("Main Window") +//! }; +//! let main_window_id = platform.new_window(main_window); //! let state = UiState { -//! main_window_id +//! main_window_id, +//! main_window_surface: None //! }; -//! platform.run(Box::new(state), |_| ()); +//! platform.launch(Box::new(state), |_| ()); //! //! ``` //! @@ -34,42 +42,24 @@ //! framework. It provides common types, which then defer to a platform-defined //! implementation. -use std::marker::PhantomData; - -mod backend; -mod handler; -mod window; - -pub use handler::PlatformHandler; -pub use window::{WindowDescription, WindowId}; +use std::{any::Any, marker::PhantomData, ops::Deref}; -/// A short-lived handle for communication with the platform, -/// which is available whilst an event handler is being called -pub struct Glazier<'a>(backend::GlazierImpl<'a>, PhantomData<&'a mut ()>); +pub mod keyboard; +pub mod text; +pub mod window; -impl Glazier<'_> { - pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowDescription)) -> WindowId { - let mut builder_instance = WindowDescription::default(); - builder(&mut builder_instance); - self.new_window(builder_instance) - } +extern crate kurbo as kurbo_0_9; - pub fn new_window(&mut self, builder: WindowDescription) -> WindowId { - todo!(); - // self.0.new_window(builder) - } +mod glazier; +pub use glazier::Glazier; +mod handler; +pub use handler::PlatformHandler; - /// Request that this `Glazier` stop controlling the current thread - /// - /// This should be called after all windows have been closed - pub fn stop(&mut self) { - self.0.stop(); - } +mod util; +pub(crate) use util::*; - // pub fn window_handle(&mut self, window: WindowId) -> NativeWindowHandle { - // NativeWindowHandle(self.0.window_handle()) - // } -} +pub(crate) mod backend; +use window::{WindowDescription, WindowId}; /// Allows configuring a `Glazier` before initialising the system pub struct GlazierBuilder { @@ -84,20 +74,56 @@ impl GlazierBuilder { /// Start interacting with the platform /// - /// Start handling events from the platform using `event_handler`. /// This should be called on the main thread for maximum portability. + /// Any events from the platform will be handled using `event_handler`. + /// + /// See also [GlazierBuilder::launch_then] for a variant which supports + /// an additional pause point. This is useful for obtaining a [LoopHandle] + /// + /// # Notes + /// + /// The event_handler is passed as a box as our backends are not generic + pub fn launch(self, event_handler: impl PlatformHandler) { + self.launch_then(event_handler, |_, _| ()); + } + + /// Start interacting with the platform, then run a one-time callback /// /// `on_init` will be called once the event loop is sufficiently /// intialized to allow creating resources at that time. This will /// be after the other properties of this builder are applied (such as queued windows). + pub fn launch_then( + self, + event_handler: H, + on_init: impl FnOnce(&mut H, Glazier), + ) { + self.launch_then_dyn(Box::new(event_handler), |plat, glz| { + let handler = plat.as_any().downcast_mut().unwrap_or_else(|| { + panic!( + "`Glazier::as_any` is implemented incorrectly for {}. Its body should only contain `self`", + std::any::type_name::() + ) + }); + on_init(handler, glz); + }) + } + + /// Start interacting with the platform, then run a one-time callback /// - /// ## Notes - /// - /// The event_handler is passed as a box for simplicity - /// - pub fn launch(self, event_handler: Box, on_init: impl FnOnce(Glazier)) { - backend::launch(event_handler, move |glz| { - on_init(glz); + /// `on_init` will be called once the event loop is sufficiently + /// intialized to allow creating resources at that time. This will + /// be after the other properties of this builder are applied (such as queued windows). + pub fn launch_then_dyn( + self, + event_handler: Box, + on_init: impl FnOnce(&mut dyn PlatformHandler, Glazier), + ) { + let Self { mut windows } = self; + backend::launch(event_handler, |plat, mut glz| { + for desc in windows.drain(..) { + glz.new_window(desc); + } + on_init(plat, glz); }) // TODO: Proper error handling .unwrap() @@ -112,3 +138,74 @@ impl GlazierBuilder { id } } + +/// A handle that can enqueue tasks on the application loop, from any thread +#[derive(Clone)] +pub struct RawLoopHandle(backend::LoopHandle2); + +impl RawLoopHandle { + /// Run `callback` on the loop this handle was created for. + /// `callback` will be provided with a reference to the [`PlatformHandler`] + /// provided during [`launch`], and a Glazier for the loop. + /// + /// If the loop is no longer running, no guarantees are currently provided. + /// + /// [PlatformHandler::as_any] can be used to access the underlying type. + /// Note that if you use this, you should prefer to get a [`LoopHandle`] using + /// [Glazier::handle], then use [`LoopHandle::run_on_main`], to front-load + /// any error handling. This type and method may be preferred if the loop may + /// have been launched with varied platform handler types + /// + /// [`launch`]: GlazierBuilder::launch + // TODO: Return an error for this case + pub fn run_on_main_raw(&self, callback: F) + where + F: FnOnce(&mut dyn PlatformHandler, Glazier) + Send + 'static, + { + self.0.run_on_main(callback); + } +} + +/// A handle that can enqueue tasks on the application loop, from any thread +#[derive(Clone)] +pub struct LoopHandle(RawLoopHandle, PhantomData); + +impl LoopHandle { + /// Run `callback` on the loop this handle was created for, with exclusive + /// access to your [PlatformHandler], and a [`Glazier`] for the loop. + /// + /// If the loop is no longer running, this callback may be not executed + /// on the loop. + /// + /// [`launch`]: GlazierBuilder::launch + pub fn run_on_main(&self, callback: F) + where + F: FnOnce(&mut H, Glazier) + Send + 'static, + { + self.0 + .0 + .run_on_main(|handler, glz| match handler.as_any().downcast_mut() { + Some(handler) => callback(handler, glz), + None => unreachable!("We ensured that the "), + }); + } +} + +impl Deref for LoopHandle { + type Target = RawLoopHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + // We need to be consistent with `Sync` across all backends. + // Being `Sync` confers no additional abilities, as `LoopHandle: Clone`, + // but does have ergonomics improvements + static_assertions::assert_impl_all!(LoopHandle>: Send, Sync); + static_assertions::assert_impl_all!(RawLoopHandle: Send, Sync); +} diff --git a/v2/src/text/mod.rs b/v2/src/text/mod.rs new file mode 100644 index 00000000..5dabde28 --- /dev/null +++ b/v2/src/text/mod.rs @@ -0,0 +1,767 @@ +// 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. + +//! Types and functions for cross-platform text input. +//! +//! Text input is a notoriously difficult problem. Unlike many other aspects of +//! user interfaces, text input can not be correctly modeled using discrete +//! events passed from the platform to the application. For example, many mobile +//! phones implement autocorrect: when the user presses the spacebar, the +//! platform peeks at the word directly behind the caret, and potentially +//! replaces it if it's misspelled. This means the platform needs to know the +//! contents of a text field. On other devices, the platform may need to draw an +//! emoji window under the caret, or look up the on-screen locations of letters +//! for crossing out with a stylus, both of which require fetching on-screen +//! coordinates from the application. +//! +//! This is all to say: text editing is a bidirectional conversation between the +//! application and the platform. The application, when the platform asks for +//! it, serves up text field coordinates and content. The platform looks at +//! this information and combines it with input from keyboards (physical or +//! onscreen), voice, styluses, the user's language settings, and then sends +//! edit commands to the application. +//! +//! Many platforms have an additional complication: this input fusion often +//! happens in a different process from your application. If we don't +//! specifically account for this fact, we might get race conditions! In the +//! autocorrect example, if I sloppily type "meoow" and press space, the +//! platform might issue edits to "delete backwards one word and insert meow". +//! However, if I concurrently click somewhere else in the document to move the +//! caret, this will replace some *other* word with "meow", and leave the +//! "meoow" disappointingly present. To mitigate this problem, we use locks, +//! represented by the `InputHandler` trait. +//! +//! ## Lifecycle of a Text Input +//! +//! 1. The user clicks a link or switches tabs, and the window content now +//! contains a new text field. The application registers this new field by +//! calling `WindowHandle::add_text_field`, and gets a `TextFieldToken` that +//! represents this new field. +//! 2. The user clicks on that text field, focusing it. The application lets the +//! platform know by calling `WindowHandle::set_focused_text_field` with that +//! field's `TextFieldToken`. +//! 3. The user presses a key on the keyboard. The platform first calls +//! `WinHandler::key_down`. If this method returns `true`, the application +//! has indicated the keypress was captured, and we skip the remaining steps. +//! 4. If `key_down` returned `false`, glazier forwards the key event to the +//! platform's text input system +//! 5. The platform, in response to either this key event or some other user +//! action, determines it's time for some text input. It calls +//! `WinHandler::text_input` to acquire a lock on the text field's state. +//! The application returns an `InputHandler` object corresponding to the +//! requested text field. To prevent race conditions, your application may +//! not make any changes +//! to the text field's state until the platform drops the `InputHandler`. +//! 6. The platform calls various `InputHandler` methods to inspect and edit the +//! text field's state. Later, usually within a few milliseconds, the +//! platform drops the `InputHandler`, allowing the application to once again +//! make changes to the text field's state. These commands might be "insert +//! `q`" for a smartphone user tapping on their virtual keyboard, or +//! "move the caret one word left" for a user pressing the left arrow key +//! while holding control. +//! 7. Eventually, after many keypresses cause steps 3–6 to repeat, the user +//! unfocuses the text field. The application indicates this to the platform +//! by calling `set_focused_text_field`. Note that even though focus has +//! shifted away from our text field, the platform may still send edits to it +//! by calling `WinHandler::text_input`. +//! 8. At some point, the user clicks a link or switches a tab, and the text +//! field is no longer present in the window. The application calls +//! `WindowHandle::remove_text_field`, and the platform may no longer call +//! `WinHandler::text_input` to make changes to it. +//! +//! The application also has a series of steps it follows if it wants to make +//! its own changes to the text field's state: +//! +//! 1. The application determines it would like to make a change to the text +//! field; perhaps the user has scrolled and and the text field has changed +//! its visible location on screen, or perhaps the user has clicked to move +//! the caret to a new location. +//! 2. The application first checks to see if there's an outstanding +//! `InputHandler` lock for this text field; if so, it waits until the last +//! `InputHandler` is dropped before continuing. +//! 3. The application then makes the change to the text input. If the change +//! would affect state visible from an `InputHandler`, the application must +//! notify the platform via `WinHandler::update_text_field`. +//! +//! ## Supported Platforms +//! +//! Currently, `glazier` text input is fully implemented on macOS. Our goal +//! is to have full support for all glazier targets, but for now, +//! `InputHandler` calls are simulated from keypresses on other platforms, which +//! doesn't allow for IME input, dead keys, etc. + +use crate::kurbo_0_9::{Point, Rect}; +use crate::util::Counter; +use std::borrow::Cow; +use std::ops::Range; + +pub(crate) mod simulate; + +/// An event representing an application-initiated change in [`InputHandler`] +/// state. +/// +/// When we change state that may have previously been retrieved from an +/// [`InputHandler`], we notify the platform so that it can invalidate any +/// data if necessary. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Event { + /// Indicates the value returned by `InputHandler::selection` may have changed. + SelectionChanged, + + /// Indicates the values returned by one or more of these methods may have changed: + /// - `InputHandler::hit_test_point` + /// - `InputHandler::line_range` + /// - `InputHandler::bounding_box` + /// - `InputHandler::slice_bounding_box` + LayoutChanged, + + /// Indicates any value returned from any `InputHandler` method may have changed. + Reset, +} + +/// A range of selected text, or a caret. +/// +/// A caret is the blinking vertical bar where text is to be inserted. We +/// represent it as a selection with zero length, where `anchor == active`. +/// Indices are always expressed in UTF-8 bytes, and must be between 0 and the +/// document length, inclusive. +/// +/// As an example, if the input caret is at the start of the document `hello +/// world`, we would expect both `anchor` and `active` to be `0`. If the user +/// holds shift and presses the right arrow key five times, we would expect the +/// word `hello` to be selected, the `anchor` to still be `0`, and the `active` +/// to now be `5`. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[non_exhaustive] +pub struct Selection { + /// The 'anchor' end of the selection. + /// + /// This is the end of the selection that stays unchanged while holding + /// shift and pressing the arrow keys. + pub anchor: usize, + /// The 'active' end of the selection. + /// + /// This is the end of the selection that moves while holding shift and + /// pressing the arrow keys. + pub active: usize, + /// The saved horizontal position, during vertical movement. + /// + /// This should not be set by the IME; it will be tracked and handled by + /// the text field. + pub h_pos: Option, +} + +#[allow(clippy::len_without_is_empty)] +impl Selection { + /// Create a new `Selection` with the provided `anchor` and `active` positions. + /// + /// Both positions refer to UTF-8 byte indices in some text. + /// + /// If your selection is a caret, you can use [`Selection::caret`] instead. + pub fn new(anchor: usize, active: usize) -> Selection { + Selection { + anchor, + active, + h_pos: None, + } + } + + /// Create a new caret (zero-length selection) at the provided UTF-8 byte index. + /// + /// `index` must be a grapheme cluster boundary. + pub fn caret(index: usize) -> Selection { + Selection { + anchor: index, + active: index, + h_pos: None, + } + } + + /// Construct a new selection from this selection, with the provided h_pos. + /// + /// # Note + /// + /// `h_pos` is used to track the *pixel* location of the cursor when moving + /// vertically; lines may have available cursor positions at different + /// positions, and arrowing down and then back up should always result + /// in a cursor at the original starting location; doing this correctly + /// requires tracking this state. + /// + /// You *probably* don't need to use this, unless you are implementing a new + /// text field, or otherwise implementing vertical cursor motion, in which + /// case you will want to set this during vertical motion if it is not + /// already set. + pub fn with_h_pos(mut self, h_pos: Option) -> Self { + self.h_pos = h_pos; + self + } + + /// Create a new selection that is guaranteed to be valid for the provided + /// text. + #[must_use = "constrained constructs a new Selection"] + pub fn constrained(mut self, s: &str) -> Self { + let s_len = s.len(); + self.anchor = self.anchor.min(s_len); + self.active = self.active.min(s_len); + while !s.is_char_boundary(self.anchor) { + self.anchor += 1; + } + while !s.is_char_boundary(self.active) { + self.active += 1; + } + self + } + + /// Return the position of the upstream end of the selection. + /// + /// This is end with the lesser byte index. + /// + /// Because of bidirectional text, this is not necessarily "left". + pub fn min(&self) -> usize { + usize::min(self.anchor, self.active) + } + + /// Return the position of the downstream end of the selection. + /// + /// This is the end with the greater byte index. + /// + /// Because of bidirectional text, this is not necessarily "right". + pub fn max(&self) -> usize { + usize::max(self.anchor, self.active) + } + + /// The sequential range of the document represented by this selection. + /// + /// This is the range that would be replaced if text were inserted at this + /// selection. + pub fn range(&self) -> Range { + self.min()..self.max() + } + + /// The length, in bytes of the selected region. + /// + /// If the selection is a caret, this is `0`. + pub fn len(&self) -> usize { + if self.anchor > self.active { + self.anchor - self.active + } else { + self.active - self.anchor + } + } + + /// Returns `true` if the selection's length is `0`. + pub fn is_caret(&self) -> bool { + self.len() == 0 + } +} + +/// A lock on a text field that allows the platform to retrieve state and make +/// edits. +/// +/// This trait is implemented by the application or UI framework. The platform +/// acquires this lock temporarily to apply edits corresponding to some user +/// input. So long as the `InputHandler` has not been dropped, the only changes +/// to the document state must come from calls to `InputHandler`. +/// +/// Some methods require a mutable lock, as indicated when acquiring the lock +/// with `WinHandler::text_input`. If a mutable method is called on a immutable +/// lock, `InputHandler` may panic. +/// +/// All ranges, lengths, and indices are specified in UTF-8 code units, unless +/// specified otherwise. +pub trait InputHandler { + /// The document's current [`Selection`]. + /// + /// If the selection is a vertical caret bar, then `range.start == range.end`. + /// Both `selection.anchor` and `selection.active` must be less than or + /// equal to the value returned from `InputHandler::len()`, and must land on + /// a extended grapheme cluster boundary in the document. + fn selection(&self) -> Selection; + + /// Set the document's selection. + /// + /// If the selection is a vertical caret bar, then `range.start == range.end`. + /// Both `selection.anchor` and `selection.active` must be less + /// than or equal to the value returned from `InputHandler::len()`. + /// + /// Properties of the `Selection` *other* than `anchor` and `active` may + /// be ignored by the handler. + /// + /// The `set_selection` implementation should round up (downstream) both + /// `selection.anchor` and `selection.active` to the nearest extended + /// grapheme cluster boundary. + /// + /// Requires a mutable lock. + fn set_selection(&mut self, selection: Selection); + + /// The current composition region. + /// + /// This should be `Some` only if the IME is currently active, in which + /// case it represents the range of text that may be modified by the IME. + /// + /// Both `range.start` and `range.end` must be less than or equal + /// to the value returned from `InputHandler::len()`, and must land on a + /// extended grapheme cluster boundary in the document. + fn composition_range(&self) -> Option>; + + /// Set the composition region. + /// + /// If this is `Some` it means that the IME is currently active for this + /// region of the document. If it is `None` it means that the IME is not + /// currently active. + /// + /// Both `range.start` and `range.end` must be less than or equal to the + /// value returned from `InputHandler::len()`. + /// + /// The `set_selection` implementation should round up (downstream) both + /// `range.start` and `range.end` to the nearest extended grapheme cluster + /// boundary. + /// + /// Requires a mutable lock. + fn set_composition_range(&mut self, range: Option>); + + /// Check if the provided index is the first byte of a UTF-8 code point + /// sequence, or is the end of the document. + /// + /// Equivalent in functionality to [`str::is_char_boundary`]. + fn is_char_boundary(&self, i: usize) -> bool; + + /// The length of the document in UTF-8 code units. + fn len(&self) -> usize; + + /// Returns `true` if the length of the document is `0`. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the subslice of the document represented by `range`. + /// + /// # Panics + /// + /// Panics if the start or end of the range do not fall on a code point + /// boundary. + fn slice(&self, range: Range) -> Cow; + + /// Returns the number of UTF-16 code units in the provided UTF-8 range. + /// + /// Converts the document into UTF-8, looks up the range specified by + /// `utf8_range` (in UTF-8 code units), reencodes that substring into + /// UTF-16, and then returns the number of UTF-16 code units in that + /// substring. + /// + /// This is automatically implemented, but you can override this if you have + /// some faster system to determine string length. + /// + /// # Panics + /// + /// Panics if the start or end of the range do not fall on a code point + /// boundary. + fn utf8_to_utf16(&self, utf8_range: Range) -> usize { + self.slice(utf8_range).encode_utf16().count() + } + + /// Returns the number of UTF-8 code units in the provided UTF-16 range. + /// + /// Converts the document into UTF-16, looks up the range specified by + /// `utf16_range` (in UTF-16 code units), reencodes that substring into + /// UTF-8, and then returns the number of UTF-8 code units in that + /// substring. + /// + /// This is automatically implemented, but you can override this if you have + /// some faster system to determine string length. + fn utf16_to_utf8(&self, utf16_range: Range) -> usize { + if utf16_range.is_empty() { + return 0; + } + let doc_range = 0..self.len(); + let text = self.slice(doc_range); + //FIXME: we can do this without allocating; there's an impl in piet + let utf16: Vec = text + .encode_utf16() + .skip(utf16_range.start) + .take(utf16_range.end) + .collect(); + String::from_utf16_lossy(&utf16).len() + } + + /// Replaces a range of the text document with `text`. + /// + /// This method also sets the composition range to `None`, and updates the + /// selection: + /// + /// - If both the selection's anchor and active are `< range.start`, then + /// nothing is updated. - If both the selection's anchor and active are `> + /// range.end`, then subtract `range.len()` from both, and add `text.len()`. + /// - If neither of the previous two conditions are true, then set both + /// anchor and active to `range.start + text.len()`. + /// + /// After the above update, if we increase each end of the selection if + /// necessary to put it on a grapheme cluster boundary. + /// + /// Requires a mutable lock. + /// + /// # Panics + /// + /// Panics if either end of the range does not fall on a code point + /// boundary. + fn replace_range(&mut self, range: Range, text: &str); + + /// Given a `Point`, determine the corresponding text position. + fn hit_test_point(&self, point: Point) -> HitTestPoint; + + /// Returns the range, in UTF-8 code units, of the line (soft- or hard-wrapped) + /// containing the byte specified by `index`. + fn line_range(&self, index: usize, affinity: Affinity) -> Range; + + /// Returns the bounding box, in window coordinates, of the visible text + /// document. + /// + /// For instance, a text box's bounding box would be the rectangle + /// of the border surrounding it, even if the text box is empty. If the + /// text document is completely offscreen, return `None`. + fn bounding_box(&self) -> Option; + + /// Returns the bounding box, in window coordinates, of the range of text specified by `range`. + /// + /// Ranges will always be equal to or a subrange of some line range returned + /// by `InputHandler::line_range`. If a range spans multiple lines, + /// `slice_bounding_box` may panic. + fn slice_bounding_box(&self, range: Range) -> Option; + + /// Applies an [`Action`] to the text field. + /// + /// Requires a mutable lock. + fn handle_action(&mut self, action: Action); +} + +/// Indicates a movement that transforms a particular text position in a +/// document. +/// +/// These movements transform only single indices — not selections. +/// +/// You'll note that a lot of these operations are idempotent, but you can get +/// around this by first sending a `Grapheme` movement. If for instance, you +/// want a `ParagraphStart` that is not idempotent, you can first send +/// `Movement::Grapheme(Direction::Upstream)`, and then follow it with +/// `ParagraphStart`. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Movement { + /// A movement that stops when it reaches an extended grapheme cluster boundary. + /// + /// This movement is achieved on most systems by pressing the left and right + /// arrow keys. For more information on grapheme clusters, see + /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries). + Grapheme(Direction), + /// A movement that stops when it reaches a word boundary. + /// + /// This movement is achieved on most systems by pressing the left and right + /// arrow keys while holding control. For more information on words, see + /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries). + Word(Direction), + /// A movement that stops when it reaches a soft line break. + /// + /// This movement is achieved on macOS by pressing the left and right arrow + /// keys while holding command. `Line` should be idempotent: if the + /// position is already at the end of a soft-wrapped line, this movement + /// should never push it onto another soft-wrapped line. + /// + /// In order to implement this properly, your text positions should remember + /// their affinity. + Line(Direction), + /// An upstream movement that stops when it reaches a hard line break. + /// + /// `ParagraphStart` should be idempotent: if the position is already at the + /// start of a hard-wrapped line, this movement should never push it onto + /// the previous line. + ParagraphStart, + /// A downstream movement that stops when it reaches a hard line break. + /// + /// `ParagraphEnd` should be idempotent: if the position is already at the + /// end of a hard-wrapped line, this movement should never push it onto the + /// next line. + ParagraphEnd, + /// A vertical movement, see `VerticalMovement` for more details. + Vertical(VerticalMovement), +} + +/// Indicates a horizontal direction in the text. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Direction { + /// The direction visually to the left. + /// + /// This may be byte-wise forwards or backwards in the document, depending + /// on the text direction around the position being moved. + Left, + /// The direction visually to the right. + /// + /// This may be byte-wise forwards or backwards in the document, depending + /// on the text direction around the position being moved. + Right, + /// Byte-wise backwards in the document. + /// + /// In a left-to-right context, this value is the same as `Left`. + Upstream, + /// Byte-wise forwards in the document. + /// + /// In a left-to-right context, this value is the same as `Right`. + Downstream, +} + +impl Direction { + /// Returns `true` if this direction is byte-wise backwards for + /// the provided [`WritingDirection`]. + /// + /// The provided direction *must not be* `WritingDirection::Natural`. + pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool { + assert!( + !matches!(direction, WritingDirection::Natural), + "writing direction must be resolved" + ); + match self { + Direction::Upstream => true, + Direction::Downstream => false, + Direction::Left => matches!(direction, WritingDirection::LeftToRight), + Direction::Right => matches!(direction, WritingDirection::RightToLeft), + } + } +} + +/// Distinguishes between two visually distinct locations with the same byte +/// index. +/// +/// Sometimes, a byte location in a document has two visual locations. For +/// example, the end of a soft-wrapped line and the start of the subsequent line +/// have different visual locations (and we want to be able to place an input +/// caret in either place!) but the same byte-wise location. This also shows up +/// in bidirectional text contexts. Affinity allows us to disambiguate between +/// these two visual locations. +pub enum Affinity { + Upstream, + Downstream, +} + +/// Indicates a horizontal direction for writing text. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum WritingDirection { + LeftToRight, + RightToLeft, + /// Indicates writing direction should be automatically detected based on + /// the text contents. + Natural, +} + +/// Indicates a vertical movement in a text document. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum VerticalMovement { + LineUp, + LineDown, + PageUp, + PageDown, + DocumentStart, + DocumentEnd, +} + +/// A special text editing command sent from the platform to the application. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Action { + /// Moves the selection. + /// + /// Before moving, if the active and the anchor of the selection are not at + /// the same position (it's a non-caret selection), then: + /// + /// 1. First set both active and anchor to the same position: the + /// selection's upstream index if `Movement` is an upstream movement, or + /// the downstream index if `Movement` is a downstream movement. + /// + /// 2. If `Movement` is `Grapheme`, then stop. Otherwise, apply the + /// `Movement` as per the usual rules. + Move(Movement), + + /// Moves just the selection's active edge. + /// + /// Equivalent to holding shift while performing movements or clicks on most + /// operating systems. + MoveSelecting(Movement), + + /// Select the entire document. + SelectAll, + + /// Expands the selection to the entire soft-wrapped line. + /// + /// If multiple lines are already selected, expands the selection to + /// encompass all soft-wrapped lines that intersected with the prior + /// selection. If the selection is a caret is on a soft line break, uses + /// the affinity of the caret to determine which of the two lines to select. + /// `SelectLine` should be idempotent: it should never expand onto adjacent + /// lines. + SelectLine, + + /// Expands the selection to the entire hard-wrapped line. + /// + /// If multiple lines are already selected, expands the selection to + /// encompass all hard-wrapped lines that intersected with the prior + /// selection. `SelectParagraph` should be idempotent: it should never + /// expand onto adjacent lines. + SelectParagraph, + + /// Expands the selection to the entire word. + /// + /// If multiple words are already selected, expands the selection to + /// encompass all words that intersected with the prior selection. If the + /// selection is a caret is on a word boundary, selects the word downstream + /// of the caret. `SelectWord` should be idempotent: it should never expand + /// onto adjacent words. + /// + /// For more information on what these so-called "words" are, see + /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries). + SelectWord, + + /// Deletes some text. + /// + /// If some text is already selected, `Movement` is ignored, and the + /// selection is deleted. If the selection's anchor is the same as the + /// active, then first apply `MoveSelecting(Movement)` and then delete the + /// resulting selection. + Delete(Movement), + + /// Delete backwards, potentially breaking graphemes. + /// + /// A special kind of backspace that, instead of deleting the entire + /// grapheme upstream of the caret, may in some cases and character sets + /// delete a subset of that grapheme's code points. + DecomposingBackspace, + + /// Maps the characters in the selection to uppercase. + /// + /// For more information on case mapping, see the + /// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7) + UppercaseSelection, + + /// Maps the characters in the selection to lowercase. + /// + /// For more information on case mapping, see the + /// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7) + LowercaseSelection, + + /// Maps the characters in the selection to titlecase. + /// + /// When calculating whether a character is at the beginning of a word, you + /// may have to peek outside the selection to other characters in the document. + /// + /// For more information on case mapping, see the + /// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7) + TitlecaseSelection, + + /// Inserts a newline character into the document. + InsertNewLine { + /// If `true`, then always insert a newline, even if normally you + /// would run a keyboard shortcut attached to the return key, like + /// sending a message or activating autocomplete. + /// + /// On macOS, this is triggered by pressing option-return. + ignore_hotkey: bool, + /// Either `U+000A`, `U+2029`, or `U+2028`. For instance, on macOS, control-enter inserts `U+2028`. + //FIXME: what about windows? + newline_type: char, + }, + + /// Inserts a tab character into the document. + InsertTab { + /// If `true`, then always insert a tab, even if normally you would run + /// a keyboard shortcut attached to the return key, like indenting a + /// line or activating autocomplete. + /// + /// On macOS, this is triggered by pressing option-tab. + ignore_hotkey: bool, + }, + + /// Indicates the reverse of inserting tab; corresponds to shift-tab on most + /// operating systems. + InsertBacktab, + + InsertSingleQuoteIgnoringSmartQuotes, + InsertDoubleQuoteIgnoringSmartQuotes, + + /// Scrolls the text field without modifying the selection. + Scroll(VerticalMovement), + + /// Centers the selection vertically in the text field. + /// + /// The average of the anchor's y and the active's y should be exactly + /// halfway down the field. If the selection is taller than the text + /// field's visible height, then instead scrolls the minimum distance such + /// that the text field is completely vertically filled by the selection. + ScrollToSelection, + + /// Sets the writing direction of the selected text or caret. + SetSelectionWritingDirection(WritingDirection), + + /// Sets the writing direction of all paragraphs that partially or fully + /// intersect with the selection or caret. + SetParagraphWritingDirection(WritingDirection), + + /// Cancels the current window or operation. + /// + /// Triggered on most operating systems with escape. + Cancel, +} + +/// Result of hit testing a point in a block of text. +/// +/// This type is returned by [`InputHandler::hit_test_point`]. +#[derive(Debug, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct HitTestPoint { + /// The index representing the grapheme boundary closest to the `Point`. + pub idx: usize, + /// Whether or not the point was inside the bounds of the layout object. + /// + /// A click outside the layout object will still resolve to a position in the + /// text; for instance a click to the right edge of a line will resolve to the + /// end of that line, and a click below the last line will resolve to a + /// position in that line. + pub is_inside: bool, +} + +impl HitTestPoint { + pub fn new(idx: usize, is_inside: bool) -> Self { + Self { idx, is_inside } + } +} + +/// Uniquely identifies a text input field inside a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)] +pub struct TextFieldToken(u64); + +impl TextFieldToken { + /// Create a new token; this should for the most part be called only by platform code. + pub fn next() -> TextFieldToken { + static TEXT_FIELD_COUNTER: Counter = Counter::new(); + TextFieldToken(TEXT_FIELD_COUNTER.next()) + } + + /// Create a new token from a raw value. + pub const fn from_raw(id: u64) -> TextFieldToken { + TextFieldToken(id) + } + + /// Get the raw value for a token. + pub const fn into_raw(self) -> u64 { + self.0 + } +} diff --git a/v2/src/text/simulate.rs b/v2/src/text/simulate.rs new file mode 100644 index 00000000..546ceee4 --- /dev/null +++ b/v2/src/text/simulate.rs @@ -0,0 +1,283 @@ +use crate::keyboard::{KbKey, KeyEvent}; + +use super::{ + Action, Direction, InputHandler, Movement, Selection, TextFieldToken, VerticalMovement, +}; + +/// Implements the "application facing" side of composition and dead keys. +/// +/// Returns how the text input field was modified. +/// +/// When using Wayland and X11, dead keys and compose are implemented as +/// a transformation which converts a sequence of keypresses into a resulting +/// string. For example, pressing the dead key corresponding to grave accent +/// (dead_grave), then a letter (say `a`) yields that letter with a grave +/// accent (`à`) as a single character, if available. +/// +/// The UX implemented in QT in this case does not provide any feedback +/// that composition is ongoing (this matches the behaviour on Windows). +/// However, GTK displays the current composition sequence with underlines +/// (this matches the behaviour on macOS). For example, pressing the +/// dead_grave gives an underlined \` (grave accent) until another character +/// is entered (or the composition is otherwise cancelled). +/// +/// We choose to emulate that behaviour for applications using Glazier on Wayland +/// and X11. Upon a keypress, the keypress is converted into its KeyEvent (ignoring +/// composition, but properly setting `is_composing`). Then, if the handler does +/// not handle[^handling] that key press, and there is a text input[^input_present], this process kicks in: +/// +/// - The key press is given to the composition. If composition was not ongoing or started by this +/// key press, simulate input is called as normal +/// - Otherwise, the character of this key press is inserted into the document, expanding the +/// composition region to cover it, with a few exceptions: +/// When this character is a dead key, the corresponding "alive" key is inserted instead +/// (as there is no unicode value to represent the dead key). +/// When this character is the compose key, a · is inserted instead. Then, when the next character +/// is inserted, this overwrites the ·. +/// When this character is the backspace key, the previous item in the composition sequence is removed, +/// then the sequence is replayed from the beginning +/// - If the keypress finished the composition, the current composition region is overwritten and +/// replaced with the composition result. +/// In this case, the previous step can be skipped (as it would have been unobservable). +/// - If the keypress cancelled the composition, the composition region is reset (but the sequence is not removed[^sequence_removed]) +/// The new keypress then `simulate_input`s as normal[^new_keypress] +/// +/// If the text input box has changed, we also cancel the current composition. This would include selecting +/// a different input box and selecting a different place in the +/// +/// Please note that, at the time of writing, Gnome uses the input method editor for composition, +/// rather than the xkb compose handling. We implement support for this on Wayland, +/// so when using Gnome we get this behaviour "for free". +/// +/// Bringing this same behaviour to Windows has not been investigated, but +/// would be welcome. +/// +/// Some more reading includes , +/// but note that this incorrectly asserts that "The MacOS and Linux operating systems +/// use input methods to process dead keys". This *is* true of Gnome, but not of KDE. +/// This is also inconsistent with the section around +/// +/// in which "the keystroke MUST be ignored", but if `ê` has been produced, the +/// key press has been taken into account. We choose to follow the latter behaviour, +/// i.e. report a `Dead` then `e`, rather than a `Dead` then `ê`. +/// +/// [^handling]: Is 'handling' that key press ever correct (once composition has begun)? +/// See also the last paragraph of the main text +/// +/// [^input_present]: The correct choice of what to do outside of text input is not completely +/// clear. The case where this matters would be for keyboard shortcuts, e.g. `alt + é`. But +/// that +/// +/// [^sequence_removed]: Another option would be to remove the sequence entirely. GTK +/// implements that behaviour for compose sequences, but not dead key sequences. +/// +/// [^new_keypress]: The correct behaviour here is a little bit unclear. In GTK, if the +/// keypress is (for example), a right arrow, it gets ignored. But if it's a character, +/// it gets inserted. I believe this to be an order of operations issue - i.e. if we're composing, +/// the keypress gets consumed by the input method, but then it turns out to cancel the input, +/// so the processing doesn't have the context of the other "keybindings". +#[allow(dead_code)] +pub(crate) fn simulate_compose( + input_handler: &mut dyn InputHandler, + event: &KeyEvent, + composition: CompositionResult, +) -> bool { + match composition { + CompositionResult::NoComposition => simulate_single_input(event, input_handler), + CompositionResult::Cancelled(text) => { + let range = input_handler.composition_range().unwrap(); + input_handler.replace_range(range, text); + simulate_single_input(event, input_handler); + true + } + CompositionResult::Updated { text, just_started } => { + let range = if just_started { + input_handler.selection().range() + } else { + input_handler.composition_range().unwrap() + }; + let start = range.start; + input_handler.replace_range(range, text); + input_handler.set_composition_range(Some(start..(start + text.len()))); + true + } + CompositionResult::Finished(text) => { + let range = input_handler + .composition_range() + .expect("Composition should only finish if it were ongoing"); + input_handler.replace_range(range, text); + true + } + } +} + +#[allow(dead_code)] +pub enum CompositionResult<'a> { + /// Composition had no effect, either because composition remained + /// non-ongoing, or the key was an ignored modifier + NoComposition, + Cancelled(&'a str), + Updated { + text: &'a str, + just_started: bool, + }, + Finished(&'a str), +} + +#[allow(dead_code)] +/// Simulates `InputHandler` calls on `handler` for a given keypress `event`. +/// +/// This circumvents the platform, and so can't work with important features +/// like input method editors! However, it's necessary while we build up our +/// input support on various platforms, which takes a lot of time. We want +/// applications to start building on the new `InputHandler` interface +/// immediately, with a hopefully seamless upgrade process as we implement IME +/// input on more platforms. +pub(crate) fn simulate_input(token: Option, event: KeyEvent) -> bool { + // if handler.key_down(&event) { + // return true; + // } + + let token = match token { + Some(v) => v, + None => return false, + }; + // let mut input_handler = handler.acquire_input_lock(token, true); + let mut input_handler: Box = todo!(); + let change_occured = simulate_single_input(&event, &mut *input_handler); + // handler.release_input_lock(token); + change_occured +} + +/// Simulate the effect of a single keypress on the +#[allow(dead_code)] +pub(crate) fn simulate_single_input( + event: &KeyEvent, + input_handler: &mut dyn InputHandler, +) -> bool { + match &event.key { + KbKey::Character(c) if !event.mods.ctrl() && !event.mods.meta() && !event.mods.alt() => { + let selection = input_handler.selection(); + input_handler.replace_range(selection.range(), c); + let new_caret_index = selection.min() + c.len(); + input_handler.set_selection(Selection::caret(new_caret_index)); + } + KbKey::ArrowLeft => { + let movement = if event.mods.ctrl() { + Movement::Word(Direction::Left) + } else { + Movement::Grapheme(Direction::Left) + }; + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::ArrowRight => { + let movement = if event.mods.ctrl() { + Movement::Word(Direction::Right) + } else { + Movement::Grapheme(Direction::Right) + }; + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::ArrowUp => { + let movement = Movement::Vertical(VerticalMovement::LineUp); + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::ArrowDown => { + let movement = Movement::Vertical(VerticalMovement::LineDown); + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::Backspace => { + let movement = if event.mods.ctrl() { + Movement::Word(Direction::Upstream) + } else { + Movement::Grapheme(Direction::Upstream) + }; + input_handler.handle_action(Action::Delete(movement)); + } + KbKey::Delete => { + let movement = if event.mods.ctrl() { + Movement::Word(Direction::Downstream) + } else { + Movement::Grapheme(Direction::Downstream) + }; + input_handler.handle_action(Action::Delete(movement)); + } + KbKey::Enter => { + // I'm sorry windows, you'll get IME soon. + input_handler.handle_action(Action::InsertNewLine { + ignore_hotkey: false, + newline_type: '\n', + }); + } + KbKey::Tab => { + let action = if event.mods.shift() { + Action::InsertBacktab + } else { + Action::InsertTab { + ignore_hotkey: false, + } + }; + input_handler.handle_action(action); + } + KbKey::Home => { + let movement = if event.mods.ctrl() { + Movement::Vertical(VerticalMovement::DocumentStart) + } else { + Movement::Line(Direction::Upstream) + }; + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::End => { + let movement = if event.mods.ctrl() { + Movement::Vertical(VerticalMovement::DocumentEnd) + } else { + Movement::Line(Direction::Downstream) + }; + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::PageUp => { + let movement = Movement::Vertical(VerticalMovement::PageUp); + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + KbKey::PageDown => { + let movement = Movement::Vertical(VerticalMovement::PageDown); + if event.mods.shift() { + input_handler.handle_action(Action::MoveSelecting(movement)); + } else { + input_handler.handle_action(Action::Move(movement)); + } + } + _ => { + return false; + } + } + true +} diff --git a/v2/src/util.rs b/v2/src/util.rs new file mode 100644 index 00000000..a11082eb --- /dev/null +++ b/v2/src/util.rs @@ -0,0 +1,47 @@ +use std::{ + num::NonZeroU64, + sync::atomic::{AtomicU64, Ordering}, +}; + +/// An thread-safe incrementing counter for generating unique ids. +/// +/// The counter wraps on overflow overflow. If the [new] constructor +/// If this is possible for your application, and reuse would be undesirable, +/// use something else. +/// +/// [new]: Counter::new +pub struct Counter(pub AtomicU64); + +impl Counter { + /// Create a new counter. + pub const fn new() -> Counter { + Counter(AtomicU64::new(1)) + } + + /// Creates a new counter with a given starting value. + pub const fn new_with_initial_value(init: u64) -> Counter { + Counter(AtomicU64::new(init)) + } + + pub const fn to_raw(self) -> AtomicU64 { + self.0 + } + + /// Return the next value. + /// + /// This wraps on overflow + pub fn next(&self) -> u64 { + self.0.fetch_add(1, Ordering::Relaxed) + } + + /// Return the next value, as a `NonZeroU64`. + /// + /// If the next value would be zero, the counter is incremented again + /// to get the next value + pub fn next_nonzero(&self) -> NonZeroU64 { + // If we increment and wrap reach zero, try again. + // It is implausible that another 2^64-1 calls would be made between + // the two, so we can safely unwrap + NonZeroU64::new(self.next()).unwrap_or_else(|| NonZeroU64::new(self.next()).unwrap()) + } +} diff --git a/v2/src/window.rs b/v2/src/window.rs index 5f993daf..9d64ab3e 100644 --- a/v2/src/window.rs +++ b/v2/src/window.rs @@ -1,7 +1,28 @@ -use std::num::NonZeroU64; +use std::{fmt, num::NonZeroU64}; -use glazier::Counter; +use crate::Counter; +mod region; +mod scale; +pub use region::*; +pub use scale::*; +use thiserror::Error; + +/// The properties which will be used when creating a window +/// +/// # Usage +/// +/// ```rust,no_run +/// # use v2::WindowDescription; +/// # let glz: v2::Glazier = todo!(); +/// let mut my_window = WindowDescription { +/// show_titlebar: false, +/// ..WindowDescription::new("Application Name") +/// }; +/// let my_window_id = my_window.assign_id(); +/// glz.new_window(my_window); +/// ``` +#[derive(Debug)] pub struct WindowDescription { pub title: String, // menu: Option, @@ -13,14 +34,21 @@ pub struct WindowDescription { pub resizable: bool, pub show_titlebar: bool, pub transparent: bool, - // TODO: Should the id live in the builder, - // and/or should these be Glazier specific? - // That would allow using the struct initialisation syntax (i.e. ..Default::default), - // which is tempting - pub(crate) id: Option, + /// The identifier the window created from this descriptor will be assigned. + /// + /// In most cases you should leave this as `None`. If you do need access + /// to the id of the window, the helper method [assign_id] can be used to + /// obtain it + /// + /// The type [NewWindowId] is used here to disallow multiple windows to + /// have the same id + /// + /// [assign_id]: WindowDescription::assign_id + pub id: Option, } impl WindowDescription { + /// Create a new WindowDescription with the given title pub fn new(title: impl Into) -> Self { WindowDescription { title: title.into(), @@ -31,12 +59,11 @@ impl WindowDescription { } } - pub fn id(&self) -> Option { - self.id - } - + /// Get the id which will be used for this window when it is created. + /// + /// This may create a new identifier, if there wasn't one previously assigned pub fn assign_id(&mut self) -> WindowId { - *self.id.get_or_insert_with(WindowId::next) + self.id.get_or_insert_with(NewWindowId::next).id() } } @@ -46,15 +73,91 @@ impl Default for WindowDescription { } } +// No use comparing, as they are all unique. Copy/Clone would break guarantees +// Default could be interesting, but there's little point - we choose to keep +// it explicit where ids are being generated +#[derive(Debug)] +/// A guaranteed unique [WindowId] +pub struct NewWindowId(pub(self) WindowId); + +impl NewWindowId { + /// Get the actual WindowId + pub fn id(&self) -> WindowId { + self.0 + } + pub fn next() -> Self { + Self(WindowId::next()) + } +} + +/// The unique identifier of a platform window +/// +/// This is passed to the methods of your [PlatformHandler], allowing +/// them to identify which window they refer to. +/// If you have multiple windows, you can obtain the id of each window +/// as you create them using [WindowDescription::assign_id] +/// +/// [PlatformHandler]: crate::PlatformHandler +/// [Glazier]: crate::Glazier #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct WindowId(NonZeroU64); -static WINDOW_ID_COUNTER: Counter = Counter::new(); - impl WindowId { pub(crate) fn next() -> Self { + static WINDOW_ID_COUNTER: Counter = Counter::new(); Self(WINDOW_ID_COUNTER.next_nonzero()) } } // pub struct NativeWindowHandle(backend::NativeWindowHandle); + +/// A token that uniquely identifies a idle schedule. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)] +pub struct IdleToken(usize); + +impl IdleToken { + /// Create a new `IdleToken` with the given raw `usize` id. + pub const fn new(raw: usize) -> IdleToken { + IdleToken(raw) + } +} + +#[derive(Error, Debug)] +pub enum WindowCreationError { + #[error(transparent)] + Backend(crate::backend::BackendWindowCreationError), +} + +/// Levels in the window system - Z order for display purposes. +/// Describes the purpose of a window and should be mapped appropriately to match platform +/// conventions. +#[derive(Clone, PartialEq, Eq)] +pub enum WindowLevel { + /// A top level app window. + AppWindow, + /// A window that should stay above app windows - like a tooltip + Tooltip(WindowId), + /// A user interface element such as a dropdown menu or combo box + DropDown(WindowId), + /// A modal dialog + Modal(WindowId), +} + +impl fmt::Debug for WindowLevel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + WindowLevel::AppWindow => write!(f, "AppWindow"), + WindowLevel::Tooltip(_) => write!(f, "Tooltip"), + WindowLevel::DropDown(_) => write!(f, "DropDown"), + WindowLevel::Modal(_) => write!(f, "Modal"), + } + } +} + +/// Contains the different states a Window can be in. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowState { + Maximized, + Minimized, + Restored, +} diff --git a/v2/src/window/region.rs b/v2/src/window/region.rs new file mode 100644 index 00000000..706f6b66 --- /dev/null +++ b/v2/src/window/region.rs @@ -0,0 +1,113 @@ +use kurbo::{BezPath, Rect, Shape, Vec2}; + +/// A union of rectangles, useful for describing an area that needs to be repainted. +#[derive(Clone, Debug)] +pub struct Region { + rects: Vec, +} + +impl Region { + /// The empty region. + pub const EMPTY: Region = Region { rects: Vec::new() }; + + /// Returns the collection of rectangles making up this region. + #[inline] + pub fn rects(&self) -> &[Rect] { + &self.rects + } + + /// Adds a rectangle to this region. + pub fn add_rect(&mut self, rect: Rect) { + if rect.area() > 0.0 { + self.rects.push(rect); + } + } + + /// Replaces this region with a single rectangle. + pub fn set_rect(&mut self, rect: Rect) { + self.clear(); + self.add_rect(rect); + } + + /// Sets this region to the empty region. + pub fn clear(&mut self) { + self.rects.clear(); + } + + /// Returns a rectangle containing this region. + pub fn bounding_box(&self) -> Rect { + if self.rects.is_empty() { + Rect::ZERO + } else { + self.rects[1..] + .iter() + .fold(self.rects[0], |r, s| r.union(*s)) + } + } + + #[deprecated(since = "0.7.0", note = "Use bounding_box() instead")] + // this existed on the previous Region type, and I've bumped into it + // a couple times while updating + pub fn to_rect(&self) -> Rect { + self.bounding_box() + } + + /// Returns `true` if this region has a non-empty intersection with the given rectangle. + pub fn intersects(&self, rect: Rect) -> bool { + self.rects.iter().any(|r| r.intersect(rect).area() > 0.0) + } + + /// Returns `true` if this region is empty. + pub fn is_empty(&self) -> bool { + // Note that we only ever add non-empty rects to self.rects. + self.rects.is_empty() + } + + /// Converts into a Bezier path. Note that this just gives the concatenation of the rectangle + /// paths, which is not the smartest possible thing. Also, it's not the right answer for an + /// even/odd fill rule. + pub fn to_bez_path(&self) -> BezPath { + let mut ret = BezPath::new(); + for rect in self.rects() { + // Rect ignores the tolerance. + ret.extend(rect.path_elements(0.0)); + } + ret + } + + /// Modifies this region by including everything in the other region. + pub fn union_with(&mut self, other: &Region) { + self.rects.extend_from_slice(&other.rects); + } + + /// Modifies this region by intersecting it with the given rectangle. + pub fn intersect_with(&mut self, rect: Rect) { + // TODO: this would be a good use of the nightly drain_filter function, if it stabilizes + for r in &mut self.rects { + *r = r.intersect(rect); + } + self.rects.retain(|r| r.area() > 0.0) + } +} + +impl std::ops::AddAssign for Region { + fn add_assign(&mut self, rhs: Vec2) { + for r in &mut self.rects { + *r = *r + rhs; + } + } +} + +impl std::ops::SubAssign for Region { + fn sub_assign(&mut self, rhs: Vec2) { + for r in &mut self.rects { + *r = *r - rhs; + } + } +} + +impl From for Region { + fn from(rect: Rect) -> Region { + Region { rects: vec![rect] } + } +} diff --git a/v2/src/window/scale.rs b/v2/src/window/scale.rs new file mode 100644 index 00000000..2381532f --- /dev/null +++ b/v2/src/window/scale.rs @@ -0,0 +1,297 @@ +// 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. + +//! Resolution scale related helpers. + +use crate::kurbo_0_9::{Insets, Line, Point, Rect, Size, Vec2}; + +/// Coordinate scaling between pixels and display points. +/// +/// This holds the platform scale factors. +/// +/// ## Pixels and Display Points +/// +/// A pixel (**px**) represents the smallest controllable area of color on the platform. +/// A display point (**dp**) is a resolution independent logical unit. +/// When developing your application you should primarily be thinking in display points. +/// These display points will be automatically converted into pixels under the hood. +/// One pixel is equal to one display point when the platform scale factor is `1.0`. +/// +/// Read more about pixels and display points [in the Druid book]. +/// +/// ## Converting with `Scale` +/// +/// To translate coordinates between pixels and display points you should use one of the +/// helper conversion methods of `Scale` or for manual conversion [`Scale::x()`] / [`Scale::y()`]. +/// +/// `Scale` is designed for responsive applications, including responding to platform scale changes. +/// The platform scale can change quickly, e.g. when moving a window from one monitor to another. +/// +/// A copy of `Scale` will be stale as soon as the platform scale changes. +/// +/// [in the Druid book]: https://linebender.org/druid/07_resolution_independence.html +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Scale { + /// The scale factor on the x axis. + x: f64, + /// The scale factor on the y axis. + y: f64, +} + +/// A specific area scaling state. +/// +/// This holds the platform area size in pixels and the logical area size in display points. +/// +/// The platform area size in pixels tends to be limited to integers and `ScaledArea` works +/// under that assumption. +/// +/// The logical area size in display points is an unrounded conversion, which means that it is +/// often not limited to integers. This allows for accurate calculations of +/// the platform area pixel boundaries from the logical area using a [`Scale`]. +/// +/// Even though the logical area size can be fractional, the integer boundaries of that logical area +/// will still match up with the platform area pixel boundaries as often as the scale factor allows. +/// +/// A copy of `ScaledArea` will be stale as soon as the platform area size changes. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct ScaledArea { + /// The size of the scaled area in display points. + size_dp: Size, + /// The size of the scaled area in pixels. + size_px: Size, +} + +/// The `Scalable` trait describes how coordinates should be translated +/// from display points into pixels and vice versa using a [`Scale`]. +pub trait Scalable { + /// Converts the scalable item from display points into pixels, + /// using the x axis scale factor for coordinates on the x axis + /// and the y axis scale factor for coordinates on the y axis. + fn to_px(&self, scale: Scale) -> Self; + + /// Converts the scalable item from pixels into display points, + /// using the x axis scale factor for coordinates on the x axis + /// and the y axis scale factor for coordinates on the y axis. + fn to_dp(&self, scale: Scale) -> Self; +} + +impl Default for Scale { + fn default() -> Scale { + Scale { x: 1.0, y: 1.0 } + } +} + +impl Scale { + /// Create a new `Scale` based on the specified axis factors. + /// + /// Units: none (scale relative to "standard" scale) + pub fn new(x: f64, y: f64) -> Scale { + Scale { x, y } + } + + /// Returns the x axis scale factor. + #[inline] + pub fn x(self) -> f64 { + self.x + } + + /// Returns the y axis scale factor. + #[inline] + pub fn y(self) -> f64 { + self.y + } + + /// Converts from pixels into display points, using the x axis scale factor. + #[inline] + pub fn px_to_dp_x>(self, x: T) -> f64 { + x.into() / self.x + } + + /// Converts from pixels into display points, using the y axis scale factor. + #[inline] + pub fn px_to_dp_y>(self, y: T) -> f64 { + y.into() / self.y + } + + /// Converts from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + pub fn px_to_dp_xy>(self, x: T, y: T) -> (f64, f64) { + (x.into() / self.x, y.into() / self.y) + } +} + +impl Scalable for Vec2 { + /// Converts a `Vec2` from display points into pixels, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_px(&self, scale: Scale) -> Vec2 { + Vec2::new(self.x * scale.x, self.y * scale.y) + } + + /// Converts a `Vec2` from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_dp(&self, scale: Scale) -> Vec2 { + Vec2::new(self.x / scale.x, self.y / scale.y) + } +} + +impl Scalable for Point { + /// Converts a `Point` from display points into pixels, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_px(&self, scale: Scale) -> Point { + Point::new(self.x * scale.x, self.y * scale.y) + } + + /// Converts a `Point` from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_dp(&self, scale: Scale) -> Point { + Point::new(self.x / scale.x, self.y / scale.y) + } +} + +impl Scalable for Line { + /// Converts a `Line` from display points into pixels, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_px(&self, scale: Scale) -> Line { + Line::new(self.p0.to_px(scale), self.p1.to_px(scale)) + } + + /// Converts a `Line` from pixels into display points, + /// using the x axis scale factor for `x` and the y axis scale factor for `y`. + #[inline] + fn to_dp(&self, scale: Scale) -> Line { + Line::new(self.p0.to_dp(scale), self.p1.to_dp(scale)) + } +} + +impl Scalable for Size { + /// Converts a `Size` from display points into pixels, + /// using the x axis scale factor for `width` + /// and the y axis scale factor for `height`. + #[inline] + fn to_px(&self, scale: Scale) -> Size { + Size::new(self.width * scale.x, self.height * scale.y) + } + + /// Converts a `Size` from pixels into points, + /// using the x axis scale factor for `width` + /// and the y axis scale factor for `height`. + #[inline] + fn to_dp(&self, scale: Scale) -> Size { + Size::new(self.width / scale.x, self.height / scale.y) + } +} + +impl Scalable for Rect { + /// Converts a `Rect` from display points into pixels, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_px(&self, scale: Scale) -> Rect { + Rect::new( + self.x0 * scale.x, + self.y0 * scale.y, + self.x1 * scale.x, + self.y1 * scale.y, + ) + } + + /// Converts a `Rect` from pixels into display points, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_dp(&self, scale: Scale) -> Rect { + Rect::new( + self.x0 / scale.x, + self.y0 / scale.y, + self.x1 / scale.x, + self.y1 / scale.y, + ) + } +} + +impl Scalable for Insets { + /// Converts `Insets` from display points into pixels, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_px(&self, scale: Scale) -> Insets { + Insets::new( + self.x0 * scale.x, + self.y0 * scale.y, + self.x1 * scale.x, + self.y1 * scale.y, + ) + } + + /// Converts `Insets` from pixels into display points, + /// using the x axis scale factor for `x0` and `x1` + /// and the y axis scale factor for `y0` and `y1`. + #[inline] + fn to_dp(&self, scale: Scale) -> Insets { + Insets::new( + self.x0 / scale.x, + self.y0 / scale.y, + self.x1 / scale.x, + self.y1 / scale.y, + ) + } +} + +impl Default for ScaledArea { + fn default() -> ScaledArea { + ScaledArea { + size_dp: Size::ZERO, + size_px: Size::ZERO, + } + } +} + +impl ScaledArea { + /// Create a new scaled area from pixels. + pub fn from_px>(size: T, scale: Scale) -> ScaledArea { + let size_px = size.into(); + let size_dp = size_px.to_dp(scale); + ScaledArea { size_dp, size_px } + } + + /// Create a new scaled area from display points. + /// + /// The calculated size in pixels is rounded away from zero to integers. + /// That means that the scaled area size in display points isn't always the same + /// as the `size` given to this function. To find out the new size in points use + /// [`ScaledArea::size_dp()`]. + pub fn from_dp>(size: T, scale: Scale) -> ScaledArea { + let size_px = size.into().to_px(scale).expand(); + let size_dp = size_px.to_dp(scale); + ScaledArea { size_dp, size_px } + } + + /// Returns the scaled area size in display points. + #[inline] + pub fn size_dp(&self) -> Size { + self.size_dp + } + + /// Returns the scaled area size in pixels. + #[inline] + pub fn size_px(&self) -> Size { + self.size_px + } +} From 56cc6ee2178b2dffb38884304278cf2bed2bd4b5 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Sat, 23 Dec 2023 21:40:14 +0000 Subject: [PATCH 06/10] Start to realign how windows are stored --- v2/src/backend/wayland/mod.rs | 54 ++++++++++++++++++++------- v2/src/backend/wayland/run_loop.rs | 3 +- v2/src/backend/wayland/window.rs | 60 ++---------------------------- v2/src/window.rs | 5 ++- 4 files changed, 50 insertions(+), 72 deletions(-) diff --git a/v2/src/backend/wayland/mod.rs b/v2/src/backend/wayland/mod.rs index 10653ce9..bb29a684 100644 --- a/v2/src/backend/wayland/mod.rs +++ b/v2/src/backend/wayland/mod.rs @@ -16,7 +16,7 @@ use std::{ any::TypeId, - collections::{HashMap, VecDeque}, + collections::{BTreeMap, HashMap, VecDeque}, marker::PhantomData, ops::{Deref, DerefMut}, }; @@ -27,7 +27,7 @@ use smithay_client_toolkit::{ output::OutputState, reexports::{ calloop::{self, LoopHandle, LoopSignal}, - client::QueueHandle, + client::{protocol::wl_surface::WlSurface, QueueHandle}, protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3, }, registry::{ProvidesRegistryState, RegistryState}, @@ -36,11 +36,15 @@ use smithay_client_toolkit::{ shell::xdg::XdgShell, }; -use crate::{handler::PlatformHandler, window::IdleToken, Glazier}; +use crate::{ + handler::PlatformHandler, + window::{IdleToken, WindowId}, + Glazier, +}; use self::{ input::SeatInfo, - window::{WaylandWindowState, WindowAction, WindowId}, + window::{WaylandWindowState, WindowAction}, }; use super::shared::xkb::Context; @@ -66,32 +70,54 @@ struct WaylandPlatform { } pub(crate) struct WaylandState { - pub(self) windows: HashMap, - - pub(self) registry_state: RegistryState, + /// The type of the user's [PlatformHandler]. Used to allow + /// [Glazier::handle] to have eager error handling + pub(self) handler_type: TypeId, + /// Monitors, not currently used pub(self) output_state: OutputState, - // TODO: Do we need to keep this around - // It is unused because(?) wgpu creates the surfaces through RawDisplayHandle(?) + + // Windowing + /// The properties we maintain about each window + pub(self) windows: BTreeMap, + /// A map from `Surface` to Window. This allows the surface + /// for a window to change, which may be required + /// (see https://github.com/linebender/druid/pull/2033) + pub(self) surface_to_window: HashMap, + + /// The compositor, used to create surfaces and regions pub(self) compositor_state: CompositorState, - // Is used: Keep the XdgShell alive, which is a Weak in all Handles + /// The XdgShell, used to create desktop windows pub(self) xdg_shell_state: XdgShell, + + /// The queue used to communicate with the platform pub(self) wayland_queue: QueueHandle, + /// Used to stop the event loop pub(self) loop_signal: LoopSignal, - // Used for timers and keyboard repeating - not yet implemented + /// Used to add new items into the loop. Primarily used for timers and keyboard repeats pub(self) loop_handle: LoopHandle<'static, WaylandPlatform>, + // Input. Wayland splits input into seats, and doesn't provide much + // help in implementing cases where there are multiple of these + /// The sctk manager for seats pub(self) seats: SeatState, + /// The data pub(self) input_states: Vec, - pub(self) xkb_context: Context, + /// Global used for IME. Optional because the compositor might not implement text input pub(self) text_input: Option, + /// The xkb context object + pub(self) xkb_context: Context, + /// The actions which the application has requested to occur on the next opportunity pub(self) idle_actions: Vec, + /// Actions which the application has requested to happen, but which require access to the handler pub(self) actions: VecDeque, + /// The sender used to access the event loop from other threads pub(self) loop_sender: calloop::channel::Sender, - pub(self) handler_type: TypeId, + // Other wayland state + pub(self) registry_state: RegistryState, } delegate_registry!(WaylandPlatform); @@ -122,7 +148,7 @@ impl WaylandPlatform { with_handler: impl FnOnce(&mut dyn PlatformHandler, Glazier) -> R, ) -> R { with_handler(&mut *self.handler, Glazier(&mut self.state, PhantomData)) - // TODO: Is now the time to drain the events? + // TODO: Is now the time to drain `self.actions`? } } diff --git a/v2/src/backend/wayland/run_loop.rs b/v2/src/backend/wayland/run_loop.rs index b6fcf125..c021288c 100644 --- a/v2/src/backend/wayland/run_loop.rs +++ b/v2/src/backend/wayland/run_loop.rs @@ -90,11 +90,12 @@ pub fn launch( )?; let state = WaylandState { + windows: Default::default(), + surface_to_window: HashMap::new(), registry_state: RegistryState::new(&globals), output_state: OutputState::new(&globals, &qh), compositor_state, xdg_shell_state: shell, - windows: HashMap::new(), wayland_queue: qh.clone(), loop_signal: loop_signal.clone(), input_states: vec![], diff --git a/v2/src/backend/wayland/window.rs b/v2/src/backend/wayland/window.rs index 673a4a1e..741609a1 100644 --- a/v2/src/backend/wayland/window.rs +++ b/v2/src/backend/wayland/window.rs @@ -347,44 +347,6 @@ impl Default for WindowHandle { // } // } -#[derive(Clone)] -pub struct IdleHandle { - window: WindowId, - idle_sender: Sender, -} - -impl IdleHandle { - pub fn add_idle_callback(&self, callback: F) - where - F: FnOnce(&mut dyn PlatformHandler) + Send + 'static, - { - self.add_idle_state_callback(|state| callback(&mut *state.handler)) - } - - fn add_idle_state_callback(&self, callback: F) - where - F: FnOnce(&mut WaylandPlatform) + Send + 'static, - { - let window = self.window.clone(); - match self - .idle_sender - .send(IdleAction::Callback(Box::new(callback))) - { - Ok(()) => (), - Err(err) => { - tracing::warn!("Added idle callback for invalid application: {err:?}") - } - }; - } - - pub fn add_idle_token(&self, token: IdleToken) { - match self.idle_sender.send(IdleAction::Token(token)) { - Ok(()) => (), - Err(err) => tracing::warn!("Requested idle on invalid application: {err:?}"), - } - } -} - #[derive(Clone, PartialEq, Eq)] pub struct CustomCursor; @@ -423,8 +385,9 @@ impl WaylandState { active_text_field_updated: false, active_text_layout_changed: false, }; + self.surface_to_window.insert(surface, window_id); self.windows.insert( - WindowId::of_surface(&surface), + window_id, WaylandWindowState { properties, text_input_seat: None, @@ -434,19 +397,6 @@ impl WaylandState { window_id } } -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -// TODO: According to https://github.com/linebender/druid/pull/2033, this should not be -// synced with the ID of the surface - -pub(super) struct WindowId(ObjectId); -impl WindowId { - pub fn new(surface: &impl WaylandSurface) -> Self { - Self::of_surface(surface.wl_surface()) - } - pub fn of_surface(surface: &WlSurface) -> Self { - Self(surface.id()) - } -} /// The state associated with each window, stored in [`WaylandState`] pub(super) struct WaylandWindowState { @@ -681,18 +631,16 @@ impl WindowHandler for WaylandPlatform { tracing::warn!("Received configure event for unknown window"); return; }; - if let Some(handle) = window.handle.take() { - // TODO: Handle it - } // TODO: Actually use the suggestions from requested_size let display_size; { - let mut props = window.properties.borrow_mut(); + let mut props = window.properties; props.configure = Some(configure); display_size = props.calculate_size(); props.configured = true; }; // window.handler.size(display_size); + // self.with_glz(|plat, glz| plat.surface_available(glz, win)); todo!("HANDLER"); window.do_paint(true, PaintContext::Configure); } diff --git a/v2/src/window.rs b/v2/src/window.rs index 9d64ab3e..873eef6c 100644 --- a/v2/src/window.rs +++ b/v2/src/window.rs @@ -99,7 +99,7 @@ impl NewWindowId { /// /// [PlatformHandler]: crate::PlatformHandler /// [Glazier]: crate::Glazier -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct WindowId(NonZeroU64); impl WindowId { @@ -107,6 +107,9 @@ impl WindowId { static WINDOW_ID_COUNTER: Counter = Counter::new(); Self(WINDOW_ID_COUNTER.next_nonzero()) } + pub fn as_raw(self) -> NonZeroU64 { + self.0 + } } // pub struct NativeWindowHandle(backend::NativeWindowHandle); From 5c7cac020b5c6d34f165e3e7dcae45d75781124f Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:04:33 +0000 Subject: [PATCH 07/10] Make changes to the wayland backend --- v2/src/backend/wayland/mod.rs | 13 +++++------- v2/src/backend/wayland/run_loop.rs | 33 ++++++++++++------------------ v2/src/backend/wayland/window.rs | 31 +++++++++++++++++++++------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/v2/src/backend/wayland/mod.rs b/v2/src/backend/wayland/mod.rs index bb29a684..64f107fc 100644 --- a/v2/src/backend/wayland/mod.rs +++ b/v2/src/backend/wayland/mod.rs @@ -32,7 +32,6 @@ use smithay_client_toolkit::{ }, registry::{ProvidesRegistryState, RegistryState}, registry_handlers, - seat::SeatState, shell::xdg::XdgShell, }; @@ -42,10 +41,8 @@ use crate::{ Glazier, }; -use self::{ - input::SeatInfo, - window::{WaylandWindowState, WindowAction}, -}; +// input::SeatInfo, +use self::window::{WaylandWindowState, WindowAction}; use super::shared::xkb::Context; @@ -101,9 +98,9 @@ pub(crate) struct WaylandState { // Input. Wayland splits input into seats, and doesn't provide much // help in implementing cases where there are multiple of these /// The sctk manager for seats - pub(self) seats: SeatState, + // pub(self) seats: SeatState, /// The data - pub(self) input_states: Vec, + // pub(self) input_states: Vec, /// Global used for IME. Optional because the compositor might not implement text input pub(self) text_input: Option, /// The xkb context object @@ -126,7 +123,7 @@ impl ProvidesRegistryState for WaylandPlatform { fn registry(&mut self) -> &mut RegistryState { &mut self.state.registry_state } - registry_handlers![OutputState, SeatState]; + registry_handlers![OutputState]; } impl Deref for WaylandPlatform { diff --git a/v2/src/backend/wayland/run_loop.rs b/v2/src/backend/wayland/run_loop.rs index c021288c..11338de5 100644 --- a/v2/src/backend/wayland/run_loop.rs +++ b/v2/src/backend/wayland/run_loop.rs @@ -24,22 +24,15 @@ use smithay_client_toolkit::{ output::OutputState, reexports::{ calloop::{channel, EventLoop}, - client::{ - globals::{registry_queue_init, BindError}, - Connection, WaylandSource, - }, + client::{globals::registry_queue_init, Connection, WaylandSource}, }, registry::RegistryState, - seat::SeatState, shell::xdg::XdgShell, }; use super::{error::Error, IdleAction, LoopCallback, WaylandState}; use crate::{ - backend::{ - shared::xkb::Context, - wayland::{input::TextInputManagerData, WaylandPlatform}, - }, + backend::{shared::xkb::Context, wayland::WaylandPlatform}, Glazier, PlatformHandler, }; @@ -81,13 +74,13 @@ pub fn launch( let compositor_state: CompositorState = CompositorState::bind(&globals, &qh)?; let shell = XdgShell::bind(&globals, &qh)?; - let text_input_global = globals.bind(&qh, 1..=1, TextInputManagerData).map_or_else( - |err| match err { - e @ BindError::UnsupportedVersion => Err(e), - BindError::NotPresent => Ok(None), - }, - |it| Ok(Some(it)), - )?; + // let text_input_global = globals.bind(&qh, 1..=1, TextInputManagerData).map_or_else( + // |err| match err { + // e @ BindError::UnsupportedVersion => Err(e), + // BindError::NotPresent => Ok(None), + // }, + // |it| Ok(Some(it)), + // )?; let state = WaylandState { windows: Default::default(), @@ -98,10 +91,10 @@ pub fn launch( xdg_shell_state: shell, wayland_queue: qh.clone(), loop_signal: loop_signal.clone(), - input_states: vec![], - seats: SeatState::new(&globals, &qh), + // input_states: vec![], + // seats: SeatState::new(&globals, &qh), xkb_context: Context::new(), - text_input: text_input_global, + text_input: None, loop_handle: loop_handle.clone(), actions: VecDeque::new(), @@ -110,7 +103,7 @@ pub fn launch( handler_type: handler.as_any().type_id(), }; let mut platform = WaylandPlatform { handler, state }; - platform.initial_seats(); + // platform.initial_seats(); tracing::info!("wayland event loop initiated"); platform.with_glz(|handler, glz| on_init(handler, glz)); diff --git a/v2/src/backend/wayland/window.rs b/v2/src/backend/wayland/window.rs index 741609a1..8823be46 100644 --- a/v2/src/backend/wayland/window.rs +++ b/v2/src/backend/wayland/window.rs @@ -27,7 +27,7 @@ use smithay_client_toolkit::shell::xdg::window::{ use smithay_client_toolkit::shell::WaylandSurface; use smithay_client_toolkit::{delegate_compositor, delegate_xdg_shell, delegate_xdg_window}; use thiserror::Error; -use tracing; +use tracing::{self, warn}; use wayland_backend::client::ObjectId; use crate::window::{IdleToken, Region, Scalable, Scale}; @@ -566,8 +566,16 @@ impl CompositorHandler for WaylandPlatform { // This requires an update in client-toolkit and wayland-protocols new_factor: i32, ) { - let window = self.windows.get_mut(&WindowId::of_surface(surface)); - let window = window.expect("Should only get events for real windows"); + let Some(window_id) = self.surface_to_window.get(surface) else { + warn!( + "Got surface scale factor change (to {new_factor}) for unknown surface {surface:?}" + ); + return; + }; + let window = self + .windows + .get_mut(window_id) + .expect("Should only get events for non-dropped windows"); let factor = f64::from(new_factor); let scale = Scale::new(factor, factor); @@ -595,9 +603,14 @@ impl CompositorHandler for WaylandPlatform { surface: &protocol::wl_surface::WlSurface, _time: u32, ) { - let Some(window) = self.windows.get_mut(&WindowId::of_surface(surface)) else { + let Some(window_id) = self.surface_to_window.get(surface) else { + warn!("Got repaint for unknown surface {surface:?}"); return; }; + let window = self + .windows + .get_mut(window_id) + .expect("Should only get events for non-dropped windows"); window.do_paint(false, PaintContext::Frame); } } @@ -609,11 +622,15 @@ impl WindowHandler for WaylandPlatform { _: &QueueHandle, wl_window: &smithay_client_toolkit::shell::xdg::window::Window, ) { - let Some(window) = self.state.windows.get_mut(&WindowId::new(wl_window)) else { + let Some(window_id) = self.surface_to_window.get(wl_window.wl_surface()) else { + warn!("Got request close for unknown window {wl_window:?}"); return; }; - todo!("HANDLER"); - // window.handler.request_close(); + let window = self + .windows + .get_mut(window_id) + .expect("Should only get events for non-dropped windows"); + self.with_glz(|handler, glz| handler.window); } fn configure( From 411b8b73ffb92784812a68e3ef426fe99aca316a Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:04:47 +0000 Subject: [PATCH 08/10] Start a new wayland backend from scratch --- v2/Cargo.toml | 12 +- v2/src/backend/mod.rs | 4 +- v2/src/backend/new_wayland/error.rs | 59 ++++ v2/src/backend/new_wayland/mod.rs | 140 +++++++++ v2/src/backend/new_wayland/outputs.rs | 374 +++++++++++++++++++++++++ v2/src/backend/new_wayland/run_loop.rs | 170 +++++++++++ v2/src/glazier.rs | 33 ++- v2/src/handler.rs | 6 + v2/src/lib.rs | 1 + v2/src/main.rs | 28 ++ v2/src/monitor.rs | 101 +++++++ 11 files changed, 910 insertions(+), 18 deletions(-) create mode 100644 v2/src/backend/new_wayland/error.rs create mode 100644 v2/src/backend/new_wayland/mod.rs create mode 100644 v2/src/backend/new_wayland/outputs.rs create mode 100644 v2/src/backend/new_wayland/run_loop.rs create mode 100644 v2/src/main.rs create mode 100644 v2/src/monitor.rs diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 004044ea..7fa5b80d 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -18,7 +18,7 @@ wayland = [ [dependencies] -kurbo = "0.9.0" +kurbo = "0.10.4" # glazier = { path = "../" } cfg-if = "1.0.0" @@ -33,12 +33,12 @@ thiserror = "1.0.51" static_assertions = "1.1.0" [target.'cfg(any(target_os = "freebsd", target_os="linux", target_os="openbsd"))'.dependencies] -ashpd = { version = "0.5", optional = true } +ashpd = { version = "0.6.7", optional = true } futures = { version = "0.3.24", optional = true, features = ["executor"] } -nix = { version = "0.25.0", optional = true } +nix = { version = "0.27.0", optional = true } -x11rb = { version = "0.12", features = [ +x11rb = { version = "0.13", features = [ "allow-unsafe-code", "present", "render", @@ -53,13 +53,13 @@ x11rb = { version = "0.12", features = [ rand = { version = "0.8.0", optional = true } log = { version = "0.4.14", optional = true } -smithay-client-toolkit = { version = "0.17.0", optional = true, default-features = false, features = [ +smithay-client-toolkit = { version = "0.18.0", optional = true, default-features = false, features = [ # Don't use the built-in xkb handling "calloop", ] } # Wayland dependencies # Needed for supporting RawWindowHandle -wayland-backend = { version = "0.1.0", default_features = false, features = [ +wayland-backend = { version = "0.3.2", default_features = false, features = [ "client_system", ], optional = true } diff --git a/v2/src/backend/mod.rs b/v2/src/backend/mod.rs index 24da0d7f..352d268d 100644 --- a/v2/src/backend/mod.rs +++ b/v2/src/backend/mod.rs @@ -1,4 +1,4 @@ +mod new_wayland; mod shared; -mod wayland; -pub use wayland::*; +pub use new_wayland::*; diff --git a/v2/src/backend/new_wayland/error.rs b/v2/src/backend/new_wayland/error.rs new file mode 100644 index 00000000..8e76435f --- /dev/null +++ b/v2/src/backend/new_wayland/error.rs @@ -0,0 +1,59 @@ +// 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 errors + +use std::fmt; + +use smithay_client_toolkit::reexports::{ + calloop, + client::{globals::BindError, ConnectError}, +}; + +#[derive(Debug)] +pub enum Error { + Connect(ConnectError), + Bind(BindError), + Calloop(calloop::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self { + Error::Connect(e) => write!(f, "could not connect to the wayland server: {e:}"), + Error::Bind(e) => write!(f, "could not bind a wayland global: {e:}"), + Error::Calloop(e) => write!(f, "calloop failed: {e:}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(value: ConnectError) -> Self { + Self::Connect(value) + } +} + +impl From for Error { + fn from(value: BindError) -> Self { + Self::Bind(value) + } +} + +impl From for Error { + fn from(value: calloop::Error) -> Self { + Self::Calloop(value) + } +} diff --git a/v2/src/backend/new_wayland/mod.rs b/v2/src/backend/new_wayland/mod.rs new file mode 100644 index 00000000..43cbcde5 --- /dev/null +++ b/v2/src/backend/new_wayland/mod.rs @@ -0,0 +1,140 @@ +// 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 + +use std::{any::TypeId, collections::HashMap, marker::PhantomData}; + +use smithay_client_toolkit::{ + compositor::CompositorState, + delegate_registry, + reexports::{ + calloop::{self, LoopHandle, LoopSignal}, + client::{protocol::wl_surface::WlSurface, QueueHandle}, + protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3, + }, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + shell::xdg::XdgShell, +}; +use thiserror::Error; + +use crate::{ + handler::PlatformHandler, + window::{IdleToken, WindowId}, + Glazier, +}; + +use self::outputs::Outputs; + +use super::shared::xkb::Context; + +pub mod error; +mod outputs; +mod run_loop; + +#[derive(Error, Debug)] +pub enum BackendWindowCreationError {} + +pub use run_loop::{launch, LoopHandle as LoopHandle2}; + +pub(crate) type GlazierImpl<'a> = &'a mut WaylandState; + +/// The main state type of the event loop. Implements dispatching for all supported +/// wayland events +struct WaylandPlatform { + // Drop the handler as early as possible, in case there are any Wgpu surfaces owned by it + pub handler: Box, + pub state: WaylandState, +} + +pub(crate) struct WaylandState { + /// The type of the user's [PlatformHandler]. Used to allow + /// [Glazier::handle] to have eager error handling + pub(self) handler_type: TypeId, + + /// Monitors, not currently used + pub(self) monitors: Outputs, + + // Windowing + /// The properties we maintain about each window + // pub(self) windows: BTreeMap, + + /// A map from `Surface` to Window. This allows the surface + /// for a window to change, which may be required + /// (see https://github.com/linebender/druid/pull/2033) + pub(self) surface_to_window: HashMap, + + /// The compositor, used to create surfaces and regions + pub(self) compositor_state: CompositorState, + /// The XdgShell, used to create desktop windows + pub(self) xdg_shell_state: XdgShell, + + /// The queue used to communicate with the platform + pub(self) wayland_queue: QueueHandle, + + /// Used to stop the event loop + pub(self) loop_signal: LoopSignal, + /// Used to add new items into the loop. Primarily used for timers and keyboard repeats + pub(self) loop_handle: LoopHandle<'static, WaylandPlatform>, + + // Input. Wayland splits input into seats, and doesn't provide much + // help in implementing cases where there are multiple of these + /// The sctk manager for seats + // pub(self) seats: SeatState, + /// The data + // pub(self) input_states: Vec, + /// Global used for IME. Optional because the compositor might not implement text input + pub(self) text_input: Option, + /// The xkb context object + pub(self) xkb_context: Context, + + /// The actions which the application has requested to occur on the next opportunity + pub(self) idle_actions: Vec, + /// Actions which the application has requested to happen, but which require access to the handler + // pub(self) actions: VecDeque, + /// The sender used to access the event loop from other threads + pub(self) loop_sender: calloop::channel::Sender, + + // Other wayland state + pub(self) registry_state: RegistryState, +} + +delegate_registry!(WaylandPlatform); + +impl ProvidesRegistryState for WaylandPlatform { + fn registry(&mut self) -> &mut RegistryState { + &mut self.state.registry_state + } + registry_handlers![Outputs]; +} + +// We *could* implement `Deref` for `WaylandPlatform`, but +// that causes borrow checking issues, because the borrow checker doesn't know +// that the derefs don't make unrelated fields alias in a horrible but safe way. +// To enable greater consistency, we therefore force using `plat.state` + +impl WaylandPlatform { + fn with_glz(&mut self, f: impl FnOnce(&mut dyn PlatformHandler, Glazier) -> R) -> R { + f(&mut *self.handler, Glazier(&mut self.state, PhantomData)) + // TODO: Is now the time to drain `self.actions`? + } +} + +enum IdleAction { + Callback(LoopCallback), + Token(IdleToken), +} + +type LoopCallback = Box; diff --git a/v2/src/backend/new_wayland/outputs.rs b/v2/src/backend/new_wayland/outputs.rs new file mode 100644 index 00000000..927b7ea1 --- /dev/null +++ b/v2/src/backend/new_wayland/outputs.rs @@ -0,0 +1,374 @@ +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + ops::RangeInclusive, +}; + +use kurbo_0_9::{Point, Size}; +use smithay_client_toolkit::{ + output::Mode, + reexports::{ + client::{ + globals::BindError, + protocol::wl_output::{self, Event as WlOutputEvent, Subpixel, Transform, WlOutput}, + Dispatch, Proxy, QueueHandle, + }, + protocols::xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, + zxdg_output_v1::{Event as XdgOutputEvent, ZxdgOutputV1}, + }, + }, + registry::{RegistryHandler, RegistryState}, +}; +use wayland_backend::protocol::WEnum; + +use crate::{monitor::MonitorId, window::WindowId}; + +use super::WaylandPlatform; + +pub(super) struct Outputs { + xdg_manager: Option, + outputs: BTreeMap, + output_to_monitor: HashMap, + global_name_to_monitor: BTreeMap, +} + +pub(super) struct OutputInfoForWindow { + pub monitor: MonitorId, + /// The integer scale factor of this output + /// + /// N.B. Wayland (before wl_compositor v6) forces us to guess which scale + /// makes the most sense. We choose to provide the *highest* relevant scale, + /// as there is no further guidance available. I.e., if the window is between + /// two monitors, one with scale 1, one with scale 2, we give scale 2. + /// Note that this has a performance cost, but we avoid doing this for good + /// compositors which *actually tell us what they want* + /// + /// In most cases, we will use the fractional scale protocol, which avoids + /// this concern. Any compositors not implementing that protocol should + pub scale: i32, +} + +impl Outputs { + pub(super) fn window_entered_output( + &mut self, + output: &WlOutput, + window: WindowId, + ) -> Option { + let Some(monitor) = self.output_to_monitor.get(output).copied() else { + tracing::warn!("Got window enter for unknown monitor. This is probably because"); + return None; + }; + let output = self + .outputs + .get_mut(&monitor) + .expect("If we've been added to `output_to_monitor`, we're definitely in `outputs`"); + output.windows.insert(window); + Some(monitor) + } + + /// ### Panics + /// If any of the monitors weren't associated with this Outputs, for some reason + pub(super) fn max_integer_scale(&mut self, monitors: &[MonitorId]) -> i32 { + // TODO: Also return the subpixel and transform data (if all the same, give + // that value, otherwise normal/unknown) + monitors + .iter() + .map(|it| { + self.outputs.get(it).expect( + "Monitor id should have only been available through setting up an output, and correctly removed if the output was deleted", + ) + }) + .flat_map(|it| &it.info) + .map(|it| it.scale_factor) + .reduce(|acc, other| acc.max(other)) + .unwrap_or(1) + } + + pub(super) fn bind(registry: &RegistryState, qh: &QueueHandle) -> Outputs { + let mut ids = Vec::new(); + + // All known compositors implement version 4, which moves the `name` from xdg into core wayland + // For simplicity of implementation, we therefore only support this + let initial_outputs: Result, _> = + registry.bind_all(qh, XDG_OUTPUT_VERSIONS, |name| { + let monitor = MonitorId::next(); + ids.push((name, monitor)); + OutputUserData { monitor } + }); + let initial_outputs = match initial_outputs { + Ok(it) => it, + Err(BindError::UnsupportedVersion) => { + tracing::warn!("Your compositor doesn't support wl_output version 4. Monitor information may not be provided"); + Vec::new() + } + Err(BindError::NotPresent) => { + unreachable!("The behaviour of bind_all has changed to return `NotPresent` when the value is present"); + } + }; + + // We choose to support only version 3, as this is the first version supporting the atomic updates + // Most compositors we care about implement this, and we don't require this to function + let xdg_manager: Option = + match registry.bind_one(qh, 3..=3, OutputManagerData) { + Ok(it) => Some(it), + Err(BindError::UnsupportedVersion) => { + tracing::warn!("Your compositor does not support XdgOutputManager"); + None + } + Err(BindError::NotPresent) => None, + }; + let mut outputs = Outputs { + xdg_manager, + outputs: BTreeMap::new(), + output_to_monitor: HashMap::new(), + global_name_to_monitor: BTreeMap::new(), + }; + for ((name, monitor), output) in ids.iter().zip(initial_outputs) { + outputs.setup(qh, *monitor, output, *name); + } + outputs + } +} + +/// The (non-deprecated) fields of a wayland - i.e. a display +#[derive(Clone)] +struct OutputInfo { + subpixel: Subpixel, + transform: Transform, + scale_factor: i32, + mode: Mode, + logical_position: Point, + logical_size: Size, + name: String, + description: String, +} + +struct OutputData { + /// The name of the global the WlOutput is + output: WlOutput, + info: Option, + pending: OutputInfo, + windows: BTreeSet, + xdg_output: Option, +} + +impl Outputs { + fn setup( + &mut self, + qh: &QueueHandle, + monitor: MonitorId, + output: WlOutput, + name: u32, + ) { + let xdg_output = self + .xdg_manager + .as_mut() + .map(|xdg_manager| xdg_manager.get_xdg_output(&output, qh, OutputUserData { monitor })); + self.global_name_to_monitor.insert(name, monitor); + self.output_to_monitor.insert(output.clone(), monitor); + let output = OutputData { + output, + info: None, + pending: OutputInfo { + subpixel: Subpixel::Unknown, + transform: Transform::Normal, + scale_factor: 1, + mode: Mode { + dimensions: (0, 0), + refresh_rate: 0, + current: false, + preferred: false, + }, + logical_position: (0., 0.).into(), + logical_size: (0., 0.).into(), + name: String::new(), + description: String::new(), + }, + windows: BTreeSet::new(), + xdg_output, + }; + self.outputs.insert(monitor, output); + } +} + +const XDG_OUTPUT_VERSIONS: RangeInclusive = 4..=4; + +struct OutputUserData { + monitor: MonitorId, +} + +impl Dispatch for WaylandPlatform { + fn event( + plat: &mut Self, + _: &WlOutput, + event: WlOutputEvent, + data: &OutputUserData, + _: &smithay_client_toolkit::reexports::client::Connection, + _: &smithay_client_toolkit::reexports::client::QueueHandle, + ) { + let Some(info) = plat.state.monitors.outputs.get_mut(&data.monitor) else { + tracing::error!("Unknown monitor bound to result"); + return; + }; + match event { + WlOutputEvent::Geometry { + subpixel, + transform, + physical_width, + physical_height, + x: _x, + y: _y, + make: _make, + model: _model, + } => { + match subpixel { + WEnum::Value(subpixel) => info.pending.subpixel = subpixel, + WEnum::Unknown(e) => { + tracing::warn!("Unknown subpixel layout: {e:?}"); + } + } + match transform { + WEnum::Value(transform) => info.pending.transform = transform, + WEnum::Unknown(e) => { + tracing::warn!("Unknown transform: {e:?}"); + } + } + } + WlOutputEvent::Mode { + flags, + width, + height, + refresh, + } => { + // Mode is *exceedingly* poorly specified. As far as I can tell, this is the best behaviour we can have + match flags { + WEnum::Value(flags) => { + let preferred = flags.contains(wl_output::Mode::Preferred); + let current = flags.contains(wl_output::Mode::Current); + if current { + info.pending.mode = Mode { + dimensions: (width, height), + refresh_rate: refresh, + current, + preferred, + }; + } + } + WEnum::Unknown(e) => tracing::info!("Unknown mode flag: {e}"), + } + } + WlOutputEvent::Done => { + let (scale_factor_changed, new) = match &info.info { + None => (true, true), + Some(old_info) => (old_info.scale_factor != info.pending.scale_factor, false), + }; + info.info = Some(info.pending.clone()); + if scale_factor_changed { + // TODO: Report an updated scale factor to each associated window + for window in &info.windows {} + } + if new { + // TODO: Report the updated monitor to the handler? + } else { + } + } + WlOutputEvent::Scale { factor } => info.pending.scale_factor = factor, + WlOutputEvent::Name { name } => info.pending.name = name, + WlOutputEvent::Description { description } => info.pending.description = description, + _ => todo!(), + } + } +} + +struct OutputManagerData; + +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + _: &ZxdgOutputManagerV1, + event: ::Event, + _: &OutputManagerData, + _: &smithay_client_toolkit::reexports::client::Connection, + _: &QueueHandle, + ) { + match event { + _ => unreachable!("There are no events for the output manager"), + } + } +} + +impl Dispatch for WaylandPlatform { + fn event( + plat: &mut Self, + _: &ZxdgOutputV1, + event: ::Event, + data: &OutputUserData, + _: &smithay_client_toolkit::reexports::client::Connection, + _: &smithay_client_toolkit::reexports::client::QueueHandle, + ) { + let Some(info) = plat.state.monitors.outputs.get_mut(&data.monitor) else { + tracing::error!("Unknown monitor bound to result"); + return; + }; + match event { + XdgOutputEvent::LogicalPosition { x, y } => { + info.pending.logical_position = (x as f64, y as f64).into() + } + XdgOutputEvent::LogicalSize { width, height } => { + info.pending.logical_size = (width as f64, height as f64).into() + } + //These events are deprecated, so we don't use them + XdgOutputEvent::Done + | XdgOutputEvent::Name { .. } + | XdgOutputEvent::Description { .. } => {} + _ => todo!(), + } + } +} + +impl RegistryHandler for Outputs { + fn new_global( + plat: &mut WaylandPlatform, + _: &smithay_client_toolkit::reexports::client::Connection, + qh: &QueueHandle, + name: u32, + interface: &str, + _: u32, + ) { + if interface == WlOutput::interface().name { + let monitor = MonitorId::next(); + let output = match plat.state.registry_state.bind_specific( + qh, + name, + XDG_OUTPUT_VERSIONS, + OutputUserData { monitor }, + ) { + Ok(output) => output, + Err(e) => { + tracing::warn!("Couldn't bind new output because:\n\t{e}"); + return; + } + }; + plat.state.monitors.setup(qh, monitor, output, name); + } + } + + fn remove_global( + plat: &mut WaylandPlatform, + conn: &smithay_client_toolkit::reexports::client::Connection, + qh: &QueueHandle, + name: u32, + interface: &str, + ) { + if interface == WlOutput::interface().name { + let monitor = plat.state.monitors.global_name_to_monitor.remove(&name); + if let Some(monitor) = monitor { + let output = plat.state.monitors.outputs.remove(&monitor).unwrap(); + for window in output.windows { + // Notify that they've left the output, i.e. that they should re-calculate their buffer scale + } + let _ = plat.state.monitors.output_to_monitor.remove(&output.output); + } + } + } +} diff --git a/v2/src/backend/new_wayland/run_loop.rs b/v2/src/backend/new_wayland/run_loop.rs new file mode 100644 index 00000000..b645d76a --- /dev/null +++ b/v2/src/backend/new_wayland/run_loop.rs @@ -0,0 +1,170 @@ +// 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 std::{any::TypeId, collections::HashMap}; + +use smithay_client_toolkit::{ + compositor::CompositorState, + reexports::{ + calloop::{channel, EventLoop}, + calloop_wayland_source::WaylandSource, + client::{globals::registry_queue_init, Connection}, + }, + registry::RegistryState, +}; + +use super::{error::Error, IdleAction, LoopCallback, WaylandPlatform, WaylandState}; +use crate::{ + backend::{new_wayland::outputs::Outputs, shared::xkb::Context}, + Glazier, PlatformHandler, +}; + +pub fn launch( + mut handler: Box, + on_init: impl FnOnce(&mut dyn PlatformHandler, Glazier), +) -> Result<(), Error> { + tracing::info!("wayland application initiated"); + + let conn = Connection::connect_to_env()?; + let (globals, event_queue) = registry_queue_init::(&conn).unwrap(); + let qh = event_queue.handle(); + let mut event_loop: EventLoop = EventLoop::try_new()?; + let loop_handle = event_loop.handle(); + let loop_signal = event_loop.get_signal(); + let (loop_sender, loop_source) = channel::channel::(); + + loop_handle + .insert_source(loop_source, |event, _, platform| { + match event { + channel::Event::Msg(msg) => { + msg(platform) + } + channel::Event::Closed => { + let _ = &platform.state.loop_sender; + unreachable!( + "The value `platform.loop_sender` has been dropped, except we have a reference to it" + ) + } // ? + } + }) + .unwrap(); + + WaylandSource::new(conn, event_queue) + .insert(loop_handle.clone()) + .unwrap(); + + let registry_state = RegistryState::new(&globals); + let monitors = Outputs::bind(®istry_state, &qh); + + let compositor_state: CompositorState = todo!(); + + let shell = todo!(); + // let text_input_global = globals.bind(&qh, 1..=1, TextInputManagerData).map_or_else( + // |err| match err { + // e @ BindError::UnsupportedVersion => Err(e), + // BindError::NotPresent => Ok(None), + // }, + // |it| Ok(Some(it)), + // )?; + + let state = WaylandState { + monitors, + surface_to_window: HashMap::new(), + registry_state, + // output_state: OutputState::new(&globals, &qh), + compositor_state, + xdg_shell_state: shell, + wayland_queue: qh.clone(), + loop_signal: loop_signal.clone(), + // input_states: vec![], + // seats: SeatState::new(&globals, &qh), + xkb_context: Context::new(), + text_input: None, + loop_handle: loop_handle.clone(), + + idle_actions: Vec::new(), + loop_sender, + handler_type: handler.as_any().type_id(), + }; + let mut plat = WaylandPlatform { handler, state }; + + tracing::info!("wayland event loop initiated"); + plat.with_glz(|handler, glz| on_init(handler, glz)); + let idle_handler = |plat: &mut WaylandPlatform| { + let mut idle_actions = std::mem::take(&mut plat.state.idle_actions); + for action in idle_actions.drain(..) { + match action { + IdleAction::Callback(cb) => cb(plat), + IdleAction::Token(token) => plat.with_glz(|handler, glz| handler.idle(glz, token)), + } + } + if plat.state.idle_actions.is_empty() { + // Re-use the allocation if possible + plat.state.idle_actions = idle_actions; + } else { + tracing::info!( + "A new idle request was added during an idle callback. This may be an error" + ); + } + }; + + event_loop + .run(None, &mut plat, idle_handler) + .expect("Shouldn't error in event loop"); + Ok(()) +} + +impl WaylandState { + pub(crate) fn stop(&mut self) { + self.loop_signal.stop() + } + + pub(crate) fn raw_handle(&mut self) -> LoopHandle { + LoopHandle { + loop_sender: self.loop_sender.clone(), + } + } + + pub(crate) fn typed_handle(&mut self, handler_type: TypeId) -> LoopHandle { + assert_eq!(self.handler_type, handler_type); + LoopHandle { + loop_sender: self.loop_sender.clone(), + } + } +} + +#[derive(Clone)] +pub struct LoopHandle { + loop_sender: channel::Sender, +} + +impl LoopHandle { + pub fn run_on_main(&self, callback: F) + where + F: FnOnce(&mut dyn PlatformHandler, Glazier) + Send + 'static, + { + match self + .loop_sender + .send(Box::new(|plat| plat.with_glz(callback))) + { + Ok(()) => (), + Err(err) => { + tracing::warn!("Sending to event loop failed: {err:?}") + // TODO: Return an error here? + } + }; + } +} diff --git a/v2/src/glazier.rs b/v2/src/glazier.rs index 5e7da96d..9674838c 100644 --- a/v2/src/glazier.rs +++ b/v2/src/glazier.rs @@ -10,17 +10,8 @@ pub struct Glazier<'a>( pub(crate) PhantomData<&'a mut ()>, ); +/// General control of the [Glazier] impl Glazier<'_> { - pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowDescription)) -> WindowId { - let mut builder_instance = WindowDescription::default(); - builder(&mut builder_instance); - self.new_window(builder_instance) - } - - pub fn new_window(&mut self, desc: WindowDescription) -> WindowId { - self.0.new_window(desc) - } - /// Request that this Glazier stop controlling the current thread /// /// This should be called after all windows have been closed @@ -51,3 +42,25 @@ impl Glazier<'_> { // NativeWindowHandle(self.0.window_handle()) // } } + +/// Window lifecycle management +impl Glazier<'_> { + pub fn build_new_window(&mut self, builder: impl FnOnce(&mut WindowDescription)) -> WindowId { + let mut builder_instance = WindowDescription::default(); + builder(&mut builder_instance); + self.new_window(builder_instance) + } + + pub fn new_window(&mut self, mut desc: WindowDescription) -> WindowId { + tracing::trace!("Will create window"); + desc.assign_id() + // self.0.new_window(desc) + } + + pub fn close_window(&mut self, win: WindowId) { + tracing::trace!("Will close window {win:?}"); + } +} + +/// Window State/Appearance management +impl Glazier<'_> {} diff --git a/v2/src/handler.rs b/v2/src/handler.rs index 6bf43633..3adaac24 100644 --- a/v2/src/handler.rs +++ b/v2/src/handler.rs @@ -58,10 +58,16 @@ pub trait PlatformHandler: Any { #[allow(unused_variables)] fn menu_item_selected(&mut self, glz: Glazier, win: WindowId, command: u32) {} + fn window_close_requested(&mut self, mut glz: Glazier, win: WindowId /* , reason: () */) { + glz.close_window(win); + } + /// A surface can now be created for window `win`. /// /// This surface can accessed using [`Glazier::window_handle`] on `glz` // TODO: Pass in size/scale(!?) + // TODO: Would it be reasonable to make this a linear type - i.e. force a return of the value in + // surface_invalidated fn surface_available(&mut self, glz: Glazier, win: WindowId); // /// The surface associated with `win` is no longer active. In particular, diff --git a/v2/src/lib.rs b/v2/src/lib.rs index 49bfc9dd..33d2be14 100644 --- a/v2/src/lib.rs +++ b/v2/src/lib.rs @@ -45,6 +45,7 @@ use std::{any::Any, marker::PhantomData, ops::Deref}; pub mod keyboard; +pub mod monitor; pub mod text; pub mod window; diff --git a/v2/src/main.rs b/v2/src/main.rs new file mode 100644 index 00000000..3b2f98c1 --- /dev/null +++ b/v2/src/main.rs @@ -0,0 +1,28 @@ +use v2::{ + window::{WindowDescription, WindowId}, + *, +}; + +fn main() { + let mut plat = GlazierBuilder::new(); + let my_window = plat.new_window(WindowDescription { + ..WindowDescription::new("Testing App For Glazier v2") + }); + plat.launch(EventHandler { + main_window_id: my_window, + }) +} + +struct EventHandler { + main_window_id: WindowId, +} + +impl PlatformHandler for EventHandler { + fn surface_available(&mut self, glz: Glazier, win: WindowId) {} + + fn paint(&mut self, glz: Glazier, win: WindowId, invalid: &window::Region) {} + + fn as_any(&mut self) -> &mut dyn std::any::Any { + self + } +} diff --git a/v2/src/monitor.rs b/v2/src/monitor.rs new file mode 100644 index 00000000..6aacac98 --- /dev/null +++ b/v2/src/monitor.rs @@ -0,0 +1,101 @@ +// 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. + +//! Module to get information about monitors + +use kurbo_0_9::Rect; +use std::fmt; +use std::fmt::Display; +use std::num::NonZeroU64; + +use crate::util::Counter; + +/// Monitor struct containing data about a monitor on the system +/// +/// Use [`Screen::get_monitors()`] to return a `Vec` of all the monitors on the system +#[derive(Clone, Debug, PartialEq)] +pub struct Monitor { + primary: bool, + rect: Rect, + // TODO: Work area, cross_platform + // https://developer.apple.com/documentation/appkit/nsscreen/1388369-visibleframe + // https://developer.gnome.org/gdk3/stable/GdkMonitor.html#gdk-monitor-get-workarea + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo + // Unsure about x11 + work_rect: Rect, +} + +impl Monitor { + #[allow(dead_code)] + pub(crate) fn new(primary: bool, rect: Rect, work_rect: Rect) -> Self { + Monitor { + primary, + rect, + work_rect, + } + } + /// Returns true if the monitor is the primary monitor. + /// The primary monitor has its origin at (0, 0) in virtual screen coordinates. + pub fn is_primary(&self) -> bool { + self.primary + } + /// Returns the monitor rectangle in virtual screen coordinates. + pub fn virtual_rect(&self) -> Rect { + self.rect + } + + /// Returns the monitor working rectangle in virtual screen coordinates. + /// The working rectangle excludes certain things like the dock and menubar on mac, + /// and the taskbar on windows. + pub fn virtual_work_rect(&self) -> Rect { + self.work_rect + } +} + +impl Display for Monitor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.primary { + write!(f, "Primary ")?; + } else { + write!(f, "Secondary ")?; + } + write!( + f, + "({}, {})({}, {})", + self.rect.x0, self.rect.x1, self.rect.y0, self.rect.y1 + )?; + Ok(()) + } +} + +/// The unique identifier of a monitor +/// +/// This is passed to the methods of your [PlatformHandler], allowing +/// them to identify which monitor they refer to (such as when a +/// monitor's properties are changed). +/// +/// [PlatformHandler]: crate::PlatformHandler +/// [Glazier]: crate::Glazier +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] +pub struct MonitorId(NonZeroU64); + +impl MonitorId { + pub(crate) fn next() -> Self { + static MONITOR_ID_COUNTER: Counter = Counter::new(); + Self(MONITOR_ID_COUNTER.next_nonzero()) + } + pub fn as_raw(self) -> NonZeroU64 { + self.0 + } +} From 5ff03029e460f8bdd97724e94004251dbd1563cd Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 29 Dec 2023 19:02:12 +0000 Subject: [PATCH 09/10] Continue making the new implementation happen --- v2/src/backend/new_wayland/error.rs | 1 + v2/src/backend/new_wayland/mod.rs | 81 ++-- v2/src/backend/new_wayland/outputs.rs | 121 +++--- v2/src/backend/new_wayland/run_loop.rs | 58 ++- v2/src/backend/new_wayland/windows.rs | 406 +++++++++++++++++++++ v2/src/backend/shared/keyboard.rs | 4 +- v2/src/backend/shared/linux/env.rs | 4 +- v2/src/backend/shared/linux/mod.rs | 2 +- v2/src/backend/shared/mod.rs | 2 +- v2/src/backend/shared/xkb/xkb_api.rs | 46 ++- v2/src/backend/shared/xkb/xkbcommon_sys.rs | 8 +- v2/src/glazier.rs | 5 +- v2/src/lib.rs | 1 + v2/src/window.rs | 14 +- 14 files changed, 597 insertions(+), 156 deletions(-) create mode 100644 v2/src/backend/new_wayland/windows.rs diff --git a/v2/src/backend/new_wayland/error.rs b/v2/src/backend/new_wayland/error.rs index 8e76435f..1d3def25 100644 --- a/v2/src/backend/new_wayland/error.rs +++ b/v2/src/backend/new_wayland/error.rs @@ -21,6 +21,7 @@ use smithay_client_toolkit::reexports::{ client::{globals::BindError, ConnectError}, }; +// TODO: Work out error handling #[derive(Debug)] pub enum Error { Connect(ConnectError), diff --git a/v2/src/backend/new_wayland/mod.rs b/v2/src/backend/new_wayland/mod.rs index 43cbcde5..707b4961 100644 --- a/v2/src/backend/new_wayland/mod.rs +++ b/v2/src/backend/new_wayland/mod.rs @@ -14,40 +14,34 @@ //! wayland platform support -use std::{any::TypeId, collections::HashMap, marker::PhantomData}; +use std::{any::TypeId, fmt::Debug, marker::PhantomData}; use smithay_client_toolkit::{ - compositor::CompositorState, delegate_registry, reexports::{ calloop::{self, LoopHandle, LoopSignal}, - client::{protocol::wl_surface::WlSurface, QueueHandle}, + client::{Proxy, QueueHandle}, protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3, }, registry::{ProvidesRegistryState, RegistryState}, registry_handlers, - shell::xdg::XdgShell, }; use thiserror::Error; -use crate::{ - handler::PlatformHandler, - window::{IdleToken, WindowId}, - Glazier, -}; - -use self::outputs::Outputs; - +use self::{outputs::Outputs, windows::Windowing}; use super::shared::xkb::Context; -pub mod error; +use crate::{handler::PlatformHandler, window::IdleToken, Glazier}; + +pub(crate) mod error; mod outputs; mod run_loop; +mod windows; #[derive(Error, Debug)] pub enum BackendWindowCreationError {} -pub use run_loop::{launch, LoopHandle as LoopHandle2}; +pub(crate) use run_loop::{launch, LoopHandle as LoopHandle2}; pub(crate) type GlazierImpl<'a> = &'a mut WaylandState; @@ -55,40 +49,39 @@ pub(crate) type GlazierImpl<'a> = &'a mut WaylandState; /// wayland events struct WaylandPlatform { // Drop the handler as early as possible, in case there are any Wgpu surfaces owned by it - pub handler: Box, - pub state: WaylandState, + pub(self) handler: Box, + pub(self) state: WaylandState, } pub(crate) struct WaylandState { + // Meta /// The type of the user's [PlatformHandler]. Used to allow /// [Glazier::handle] to have eager error handling pub(self) handler_type: TypeId, - /// Monitors, not currently used - pub(self) monitors: Outputs, - - // Windowing - /// The properties we maintain about each window - // pub(self) windows: BTreeMap, - - /// A map from `Surface` to Window. This allows the surface - /// for a window to change, which may be required - /// (see https://github.com/linebender/druid/pull/2033) - pub(self) surface_to_window: HashMap, - - /// The compositor, used to create surfaces and regions - pub(self) compositor_state: CompositorState, - /// The XdgShell, used to create desktop windows - pub(self) xdg_shell_state: XdgShell, - + // Event loop management /// The queue used to communicate with the platform pub(self) wayland_queue: QueueHandle, - /// Used to stop the event loop pub(self) loop_signal: LoopSignal, /// Used to add new items into the loop. Primarily used for timers and keyboard repeats pub(self) loop_handle: LoopHandle<'static, WaylandPlatform>, + // Callbacks and other delayed actions + /// The actions which the application has requested to occur on the next opportunity + pub(self) idle_actions: Vec, + /// Actions which the application has requested to happen, but which require access to the handler + // pub(self) actions: VecDeque, + /// The sender used to access the event loop from other threads + pub(self) loop_sender: calloop::channel::Sender, + + // Subsytem state + /// Monitors, not currently used + pub(self) monitors: Outputs, + + // State of the windowing subsystem + pub(self) windows: Windowing, + // Input. Wayland splits input into seats, and doesn't provide much // help in implementing cases where there are multiple of these /// The sctk manager for seats @@ -99,14 +92,6 @@ pub(crate) struct WaylandState { pub(self) text_input: Option, /// The xkb context object pub(self) xkb_context: Context, - - /// The actions which the application has requested to occur on the next opportunity - pub(self) idle_actions: Vec, - /// Actions which the application has requested to happen, but which require access to the handler - // pub(self) actions: VecDeque, - /// The sender used to access the event loop from other threads - pub(self) loop_sender: calloop::channel::Sender, - // Other wayland state pub(self) registry_state: RegistryState, } @@ -138,3 +123,15 @@ enum IdleAction { } type LoopCallback = Box; + +fn on_unknown_event(proxy: &P, event: P::Event) +where + P::Event: Debug, +{ + let name = P::interface().name; + tracing::warn!( + proxy = ?proxy, + event = ?event, + issues_url = "https://github.com/linebender/glazier/issues", + "Got an unknown event for interface {name}, got event: {event:?}. Please report this to Glazier on GitHub"); +} diff --git a/v2/src/backend/new_wayland/outputs.rs b/v2/src/backend/new_wayland/outputs.rs index 927b7ea1..c01db18e 100644 --- a/v2/src/backend/new_wayland/outputs.rs +++ b/v2/src/backend/new_wayland/outputs.rs @@ -9,12 +9,12 @@ use smithay_client_toolkit::{ reexports::{ client::{ globals::BindError, - protocol::wl_output::{self, Event as WlOutputEvent, Subpixel, Transform, WlOutput}, + protocol::wl_output::{self, Subpixel, Transform, WlOutput}, Dispatch, Proxy, QueueHandle, }, protocols::xdg::xdg_output::zv1::client::{ - zxdg_output_manager_v1::ZxdgOutputManagerV1, - zxdg_output_v1::{Event as XdgOutputEvent, ZxdgOutputV1}, + zxdg_output_manager_v1::{self, ZxdgOutputManagerV1}, + zxdg_output_v1::{self, ZxdgOutputV1}, }, }, registry::{RegistryHandler, RegistryState}, @@ -23,7 +23,7 @@ use wayland_backend::protocol::WEnum; use crate::{monitor::MonitorId, window::WindowId}; -use super::WaylandPlatform; +use super::{on_unknown_event, WaylandPlatform}; pub(super) struct Outputs { xdg_manager: Option, @@ -32,22 +32,6 @@ pub(super) struct Outputs { global_name_to_monitor: BTreeMap, } -pub(super) struct OutputInfoForWindow { - pub monitor: MonitorId, - /// The integer scale factor of this output - /// - /// N.B. Wayland (before wl_compositor v6) forces us to guess which scale - /// makes the most sense. We choose to provide the *highest* relevant scale, - /// as there is no further guidance available. I.e., if the window is between - /// two monitors, one with scale 1, one with scale 2, we give scale 2. - /// Note that this has a performance cost, but we avoid doing this for good - /// compositors which *actually tell us what they want* - /// - /// In most cases, we will use the fractional scale protocol, which avoids - /// this concern. Any compositors not implementing that protocol should - pub scale: i32, -} - impl Outputs { pub(super) fn window_entered_output( &mut self, @@ -55,7 +39,7 @@ impl Outputs { window: WindowId, ) -> Option { let Some(monitor) = self.output_to_monitor.get(output).copied() else { - tracing::warn!("Got window enter for unknown monitor. This is probably because"); + tracing::error!("Got window enter for unknown output. This may lead to this window getting an incorrect scale factor"); return None; }; let output = self @@ -66,15 +50,47 @@ impl Outputs { Some(monitor) } + pub(super) fn window_left_output( + &mut self, + output: &WlOutput, + window: WindowId, + ) -> Option { + let Some(monitor) = self.output_to_monitor.get(output).copied() else { + tracing::error!("Got window enter for unknown output. This may lead to this window getting an incorrect scale factor"); + return None; + }; + let output = self + .outputs + .get_mut(&monitor) + .expect("If we've been added to `output_to_monitor`, we're definitely in `outputs`"); + output.windows.remove(&window); + Some(monitor) + } + + /// Get the (integer) scale associated with a set of monitors + /// + /// N.B. Wayland (before wl_compositor v6) forces us to guess which scale + /// makes the most sense. We choose to provide the *highest* relevant scale, + /// as there is no further guidance available. I.e., if the window is between + /// two monitors, one with scale 1, one with scale 2, we give scale 2. + /// Note that this has a performance cost, but we avoid doing this for + /// compositors which *actually tell us what they want* + /// + /// In most cases, we will use the fractional scale protocol, which avoids + /// this concern. Any compositors not implementing that protocol should do + /// so. + /// /// ### Panics - /// If any of the monitors weren't associated with this Outputs, for some reason - pub(super) fn max_integer_scale(&mut self, monitors: &[MonitorId]) -> i32 { + /// If any of the monitors weren't associated with this `Outputs` + pub(super) fn max_fallback_integer_scale( + &mut self, + monitors: impl Iterator, + ) -> i32 { // TODO: Also return the subpixel and transform data (if all the same, give // that value, otherwise normal/unknown) monitors - .iter() .map(|it| { - self.outputs.get(it).expect( + self.outputs.get(&it).expect( "Monitor id should have only been available through setting up an output, and correctly removed if the output was deleted", ) }) @@ -117,6 +133,10 @@ impl Outputs { } Err(BindError::NotPresent) => None, }; + // TODO: Maybe bind https://wayland.app/protocols/kde-primary-output-v1 ? + // We need to check that this is one of the values made available to us, without using + // https://wayland.app/protocols/kde-outputdevice (as those are not associated with + // wayland outputs) let mut outputs = Outputs { xdg_manager, outputs: BTreeMap::new(), @@ -139,6 +159,7 @@ struct OutputInfo { mode: Mode, logical_position: Point, logical_size: Size, + physical_size: Size, name: String, description: String, } @@ -179,10 +200,11 @@ impl Outputs { current: false, preferred: false, }, - logical_position: (0., 0.).into(), - logical_size: (0., 0.).into(), + logical_position: Point::ZERO, + logical_size: Size::ZERO, name: String::new(), description: String::new(), + physical_size: Size::ZERO, }, windows: BTreeSet::new(), xdg_output, @@ -200,8 +222,8 @@ struct OutputUserData { impl Dispatch for WaylandPlatform { fn event( plat: &mut Self, - _: &WlOutput, - event: WlOutputEvent, + proxy: &WlOutput, + event: wl_output::Event, data: &OutputUserData, _: &smithay_client_toolkit::reexports::client::Connection, _: &smithay_client_toolkit::reexports::client::QueueHandle, @@ -211,7 +233,7 @@ impl Dispatch for WaylandPlatform { return; }; match event { - WlOutputEvent::Geometry { + wl_output::Event::Geometry { subpixel, transform, physical_width, @@ -233,8 +255,9 @@ impl Dispatch for WaylandPlatform { tracing::warn!("Unknown transform: {e:?}"); } } + info.pending.physical_size = (physical_width as f64, physical_height as f64).into(); } - WlOutputEvent::Mode { + wl_output::Event::Mode { flags, width, height, @@ -257,7 +280,7 @@ impl Dispatch for WaylandPlatform { WEnum::Unknown(e) => tracing::info!("Unknown mode flag: {e}"), } } - WlOutputEvent::Done => { + wl_output::Event::Done => { let (scale_factor_changed, new) = match &info.info { None => (true, true), Some(old_info) => (old_info.scale_factor != info.pending.scale_factor, false), @@ -272,10 +295,10 @@ impl Dispatch for WaylandPlatform { } else { } } - WlOutputEvent::Scale { factor } => info.pending.scale_factor = factor, - WlOutputEvent::Name { name } => info.pending.name = name, - WlOutputEvent::Description { description } => info.pending.description = description, - _ => todo!(), + wl_output::Event::Scale { factor } => info.pending.scale_factor = factor, + wl_output::Event::Name { name } => info.pending.name = name, + wl_output::Event::Description { description } => info.pending.description = description, + event => on_unknown_event(proxy, event), } } } @@ -285,14 +308,14 @@ struct OutputManagerData; impl Dispatch for WaylandPlatform { fn event( _: &mut Self, - _: &ZxdgOutputManagerV1, - event: ::Event, + proxy: &ZxdgOutputManagerV1, + event: zxdg_output_manager_v1::Event, _: &OutputManagerData, _: &smithay_client_toolkit::reexports::client::Connection, _: &QueueHandle, ) { match event { - _ => unreachable!("There are no events for the output manager"), + event => on_unknown_event(proxy, event), } } } @@ -300,8 +323,8 @@ impl Dispatch for WaylandPlatform { impl Dispatch for WaylandPlatform { fn event( plat: &mut Self, - _: &ZxdgOutputV1, - event: ::Event, + proxy: &ZxdgOutputV1, + event: zxdg_output_v1::Event, data: &OutputUserData, _: &smithay_client_toolkit::reexports::client::Connection, _: &smithay_client_toolkit::reexports::client::QueueHandle, @@ -311,17 +334,17 @@ impl Dispatch for WaylandPlatform { return; }; match event { - XdgOutputEvent::LogicalPosition { x, y } => { + zxdg_output_v1::Event::LogicalPosition { x, y } => { info.pending.logical_position = (x as f64, y as f64).into() } - XdgOutputEvent::LogicalSize { width, height } => { + zxdg_output_v1::Event::LogicalSize { width, height } => { info.pending.logical_size = (width as f64, height as f64).into() } //These events are deprecated, so we don't use them - XdgOutputEvent::Done - | XdgOutputEvent::Name { .. } - | XdgOutputEvent::Description { .. } => {} - _ => todo!(), + zxdg_output_v1::Event::Done + | zxdg_output_v1::Event::Name { .. } + | zxdg_output_v1::Event::Description { .. } => {} + event => on_unknown_event(proxy, event), } } } @@ -355,8 +378,8 @@ impl RegistryHandler for Outputs { fn remove_global( plat: &mut WaylandPlatform, - conn: &smithay_client_toolkit::reexports::client::Connection, - qh: &QueueHandle, + _: &smithay_client_toolkit::reexports::client::Connection, + _: &QueueHandle, name: u32, interface: &str, ) { diff --git a/v2/src/backend/new_wayland/run_loop.rs b/v2/src/backend/new_wayland/run_loop.rs index b645d76a..96f4de8c 100644 --- a/v2/src/backend/new_wayland/run_loop.rs +++ b/v2/src/backend/new_wayland/run_loop.rs @@ -14,10 +14,9 @@ #![allow(clippy::single_match)] -use std::{any::TypeId, collections::HashMap}; +use std::any::TypeId; use smithay_client_toolkit::{ - compositor::CompositorState, reexports::{ calloop::{channel, EventLoop}, calloop_wayland_source::WaylandSource, @@ -28,7 +27,10 @@ use smithay_client_toolkit::{ use super::{error::Error, IdleAction, LoopCallback, WaylandPlatform, WaylandState}; use crate::{ - backend::{new_wayland::outputs::Outputs, shared::xkb::Context}, + backend::{ + new_wayland::{outputs::Outputs, windows::Windowing}, + shared::xkb::Context, + }, Glazier, PlatformHandler, }; @@ -46,21 +48,18 @@ pub fn launch( let loop_signal = event_loop.get_signal(); let (loop_sender, loop_source) = channel::channel::(); + // work around https://github.com/rust-lang/rustfmt/issues/3863 + const MESSAGE: &str = + "The value `platform.loop_sender` has been dropped, except we have a reference to it"; loop_handle - .insert_source(loop_source, |event, _, platform| { - match event { - channel::Event::Msg(msg) => { - msg(platform) - } - channel::Event::Closed => { - let _ = &platform.state.loop_sender; - unreachable!( - "The value `platform.loop_sender` has been dropped, except we have a reference to it" - ) - } // ? + .insert_source(loop_source, |event, _, platform| match event { + channel::Event::Msg(msg) => msg(platform), + channel::Event::Closed => { + let _ = &platform.state.loop_sender; + unreachable!("{MESSAGE}") } }) - .unwrap(); + .map_err(|it| it.error)?; WaylandSource::new(conn, event_queue) .insert(loop_handle.clone()) @@ -68,10 +67,8 @@ pub fn launch( let registry_state = RegistryState::new(&globals); let monitors = Outputs::bind(®istry_state, &qh); + let windows = Windowing::bind(®istry_state, &qh)?; - let compositor_state: CompositorState = todo!(); - - let shell = todo!(); // let text_input_global = globals.bind(&qh, 1..=1, TextInputManagerData).map_or_else( // |err| match err { // e @ BindError::UnsupportedVersion => Err(e), @@ -81,23 +78,22 @@ pub fn launch( // )?; let state = WaylandState { - monitors, - surface_to_window: HashMap::new(), - registry_state, - // output_state: OutputState::new(&globals, &qh), - compositor_state, - xdg_shell_state: shell, + handler_type: handler.as_any().type_id(), + wayland_queue: qh.clone(), loop_signal: loop_signal.clone(), - // input_states: vec![], - // seats: SeatState::new(&globals, &qh), - xkb_context: Context::new(), - text_input: None, loop_handle: loop_handle.clone(), idle_actions: Vec::new(), loop_sender, - handler_type: handler.as_any().type_id(), + + monitors, + windows, + + text_input: None, + xkb_context: Context::new(), + + registry_state, }; let mut plat = WaylandPlatform { handler, state }; @@ -121,9 +117,7 @@ pub fn launch( } }; - event_loop - .run(None, &mut plat, idle_handler) - .expect("Shouldn't error in event loop"); + event_loop.run(None, &mut plat, idle_handler)?; Ok(()) } diff --git a/v2/src/backend/new_wayland/windows.rs b/v2/src/backend/new_wayland/windows.rs new file mode 100644 index 00000000..ca0b9ad3 --- /dev/null +++ b/v2/src/backend/new_wayland/windows.rs @@ -0,0 +1,406 @@ +use std::collections::{BTreeMap, HashMap}; + +use smithay_client_toolkit::{ + reexports::{ + client::{ + globals::BindError, + protocol::{ + wl_compositor::{self, WlCompositor}, + wl_surface::{self, WlSurface}, + }, + Connection, Dispatch, Proxy, QueueHandle, + }, + protocols::{ + wp::{ + fractional_scale::v1::client::{ + wp_fractional_scale_manager_v1::{self, WpFractionalScaleManagerV1}, + wp_fractional_scale_v1::{self, WpFractionalScaleV1}, + }, + viewporter::client::wp_viewporter::{self, WpViewporter}, + }, + xdg::shell::client::{ + xdg_surface::{self, XdgSurface}, + xdg_toplevel::{self, XdgToplevel}, + xdg_wm_base::{self, XdgWmBase}, + }, + }, + }, + registry::RegistryState, +}; + +use crate::{ + monitor::MonitorId, + window::{Scale, WindowDescription, WindowId, WindowLevel}, +}; + +use super::{on_unknown_event, WaylandPlatform, WaylandState}; + +pub(super) struct Windowing { + compositor: WlCompositor, + xdg: XdgWmBase, + fractional_scale: Option, + viewporter: Option, + + windows: BTreeMap, + surface_to_window: HashMap, +} + +impl Windowing { + pub(super) fn bind( + registry: &RegistryState, + qh: &QueueHandle, + ) -> Result { + // All compositors we expect to need to support allow at least version 5 + let compositor = registry.bind_one(qh, 5..=6, ())?; + // Sway is supposedly still on v2? + let xdg = registry.bind_one(qh, 2..=6, ())?; + let fractional_scale = registry.bind_one(qh, 1..=1, ()).ok(); + let viewporter = registry.bind_one(qh, 1..=1, ()).ok(); + + Ok(Self { + compositor, + xdg, + fractional_scale, + viewporter, + surface_to_window: Default::default(), + windows: Default::default(), + }) + } +} + +struct SurfaceUserData(WindowId); + +impl WaylandState { + pub(crate) fn new_window(&mut self, mut desc: WindowDescription) -> WindowId { + let window_id = desc.assign_id(); + let WindowDescription { + title, + resizable, + show_titlebar, // TODO: Handling titlebars is tricky on wayland, we need to work out the right API + transparent: _, // Meaningless on wayland? + id: _, // Already used + app_id, + size, + min_size, + level, + } = desc; + if level != WindowLevel::AppWindow { + tracing::error!("The Wayland backend doesn't yet support {level:?} windows"); + } + + let qh = &self.wayland_queue; + let windows = &mut self.windows; + + let surface = windows + .compositor + .create_surface(qh, SurfaceUserData(window_id)); + let xdg_surface = windows + .xdg + .get_xdg_surface(&surface, qh, SurfaceUserData(window_id)); + let toplevel = xdg_surface.get_toplevel(qh, SurfaceUserData(window_id)); + let fractional_scale = windows + .fractional_scale + .as_ref() + .map(|it| it.get_fractional_scale(&surface, qh, SurfaceUserData(window_id))); + + toplevel.set_title(title); + if let Some(app_id) = app_id { + toplevel.set_app_id(app_id); + } + + surface.commit(); + + windows.surface_to_window.insert(surface.clone(), window_id); + windows.windows.insert( + window_id, + PerWindowState { + surface, + xdg_surface, + toplevel, + _show_titlebar: show_titlebar, + resizable, + initial_configure_complete: false, + requested_scale: ScaleSource::Fallback(1), + + fractional_scale, + monitors: Vec::new(), + applied_scale: Scale::default(), + }, + ); + window_id + } +} + +const FRACTIONAL_DENOMINATOR: i32 = 120; + +#[derive(Copy, Clone, Debug)] +enum ScaleSource { + /// Stored as a multiple of 120ths of the 'actual' scale. + /// + /// This avoids doing floating point comparisons + Fractional(i32), + Buffer(i32), + Fallback(i32), +} + +impl ScaleSource { + fn equal(&self, other: &Self) -> bool { + self.normalise() == other.normalise() + } + + fn normalise(&self) -> i32 { + match self { + ScaleSource::Fractional(v) => *v, + ScaleSource::Buffer(s) | ScaleSource::Fallback(s) => s * FRACTIONAL_DENOMINATOR, + } + } + + fn as_scale(&self) -> Scale { + let factor = match *self { + ScaleSource::Fractional(num) => (num as f64) / (FRACTIONAL_DENOMINATOR as f64), + ScaleSource::Buffer(s) => s as f64, + ScaleSource::Fallback(s) => s as f64, + }; + Scale::new(factor, factor) + } + + fn needs_fallback(&self) -> bool { + match self { + ScaleSource::Fractional(_) | ScaleSource::Buffer(_) => false, + ScaleSource::Fallback(_) => true, + } + } + + fn better(old: &Self, new: &Self) -> Self { + match (old, new) { + (_, new @ ScaleSource::Fractional(_)) => *new, + (old @ ScaleSource::Fractional(_), _) => *old, + (_, new @ ScaleSource::Buffer(_)) => *new, + (old @ ScaleSource::Buffer(_), _) => *old, + (ScaleSource::Fallback(_), ScaleSource::Fallback(_)) => { + unreachable!() + } + } + } +} + +struct PerWindowState { + // Wayland properties + // Dropped before `xdg_surface` + toplevel: XdgToplevel, + // Dropped before `surface` + xdg_surface: XdgSurface, + // Dropped before `surface` + fractional_scale: Option, + + surface: WlSurface, + + // Configuration + _show_titlebar: bool, + resizable: bool, + applied_scale: Scale, + + // State + monitors: Vec, + initial_configure_complete: bool, + requested_scale: ScaleSource, +} + +impl Dispatch for WaylandPlatform { + fn event( + plat: &mut Self, + proxy: &WlSurface, + event: wl_surface::Event, + data: &SurfaceUserData, + _: &Connection, + _: &QueueHandle, + ) { + let Some(this) = plat.state.windows.windows.get_mut(&data.0) else { + tracing::error!(?event, "Got unexpected event after deleting a window"); + return; + }; + match event { + wl_surface::Event::Enter { output } => { + let new_monitor = plat.state.monitors.window_entered_output(&output, data.0); + if let Some(monitor) = new_monitor { + this.monitors.push(monitor); + let new_scale = did_fallback_scale_change(this, &mut plat.state.monitors); + } else { + tracing::warn!( + ?output, + "Got window surface leave with previously unknown output" + ); + } + } + wl_surface::Event::Leave { output } => { + let removed_monitor = plat.state.monitors.window_left_output(&output, data.0); + if let Some(monitor) = removed_monitor { + // Keep only the items which aren't this monitor, i.e. remove this item + // TODO: swap_remove? + // We expect this array to be + let existing_len = this.monitors.len(); + this.monitors.retain(|item| item != &monitor); + if this.monitors.len() == existing_len { + tracing::warn!( + ?output, + "Got window surface leave without corresponding enter being recorded" + ); + return; + } + let new_scale = did_fallback_scale_change(this, &mut plat.state.monitors); + } + + // TODO: Recalculate scale if we never got preferred scale through another means + } + wl_surface::Event::PreferredBufferScale { factor } => todo!(), + wl_surface::Event::PreferredBufferTransform { transform } => todo!(), + + event => on_unknown_event(proxy, event), + } + } +} + +fn did_fallback_scale_change( + this: &mut PerWindowState, + outputs: &mut super::outputs::Outputs, +) -> Option { + if this.requested_scale.needs_fallback() && this.initial_configure_complete { + let new_factor = ScaleSource::Fractional( + outputs.max_fallback_integer_scale(this.monitors.iter().copied()), + ); + let was_same = new_factor.equal(&this.requested_scale); + this.requested_scale = new_factor; + return Some(new_factor.as_scale()); + } + return None; +} + +impl Dispatch for WaylandPlatform { + fn event( + plat: &mut Self, + proxy: &XdgToplevel, + event: xdg_toplevel::Event, + data: &SurfaceUserData, + conn: &Connection, + qhandle: &QueueHandle, + ) { + let Some(this) = plat.state.windows.windows.get_mut(&data.0) else { + tracing::error!(?event, "Got unexpected event after deleting a window"); + return; + }; + match event { + xdg_toplevel::Event::Configure { + width, + height, + states, + } => todo!(), + xdg_toplevel::Event::Close => todo!(), + xdg_toplevel::Event::ConfigureBounds { width, height } => todo!(), + xdg_toplevel::Event::WmCapabilities { capabilities } => todo!(), + event => on_unknown_event(proxy, event), + } + } +} + +impl Dispatch for WaylandPlatform { + fn event( + plat: &mut Self, + proxy: &XdgSurface, + event: xdg_surface::Event, + data: &SurfaceUserData, + conn: &Connection, + qhandle: &QueueHandle, + ) { + let Some(this) = plat.state.windows.windows.get_mut(&data.0) else { + tracing::error!(?event, "Got unexpected event after deleting a window"); + return; + }; + match event { + xdg_surface::Event::Configure { serial } => todo!(), + event => on_unknown_event(proxy, event), + } + } +} + +impl Dispatch for WaylandPlatform { + fn event( + plat: &mut Self, + proxy: &WpFractionalScaleV1, + event: wp_fractional_scale_v1::Event, + data: &SurfaceUserData, + _: &Connection, + _: &QueueHandle, + ) { + let Some(this) = plat.state.windows.windows.get_mut(&data.0) else { + tracing::error!(?event, "Got unexpected event after deleting a window"); + return; + }; + match event { + wp_fractional_scale_v1::Event::PreferredScale { scale } => todo!(), + event => on_unknown_event(proxy, event), + } + } +} + +// Simple but necessary implementations +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + proxy: &XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + xdg_wm_base::Event::Ping { serial } => proxy.pong(serial), + event => on_unknown_event(proxy, event), + } + } +} + +// No-op implementations +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + proxy: &WlCompositor, + event: wl_compositor::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + event => on_unknown_event(proxy, event), + } + } +} + +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + proxy: &WpFractionalScaleManagerV1, + event: wp_fractional_scale_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + event => on_unknown_event(proxy, event), + } + } +} + +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + proxy: &WpViewporter, + event: wp_viewporter::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + event => on_unknown_event(proxy, event), + } + } +} diff --git a/v2/src/backend/shared/keyboard.rs b/v2/src/backend/shared/keyboard.rs index 63972d7f..59364e53 100644 --- a/v2/src/backend/shared/keyboard.rs +++ b/v2/src/backend/shared/keyboard.rs @@ -31,7 +31,7 @@ use keyboard_types::{Code, Location}; /// /// Note: in the original, this is based on kVK constants, but since we don't have those /// readily available, we use the mapping to code (which should be effectively lossless). -pub fn code_to_location(code: Code) -> Location { +pub(in crate::backend) fn code_to_location(code: Code) -> Location { match code { Code::MetaLeft | Code::ShiftLeft | Code::AltLeft | Code::ControlLeft => Location::Left, Code::MetaRight | Code::ShiftRight | Code::AltRight | Code::ControlRight => Location::Right, @@ -64,7 +64,7 @@ pub fn code_to_location(code: Code) -> Location { /// practice it's probably pretty reliable. /// /// The logic is based on NativeKeyToDOMCodeName.h in Mozilla. -pub fn hardware_keycode_to_code(hw_keycode: u16) -> Code { +pub(in crate::backend) fn hardware_keycode_to_code(hw_keycode: u16) -> Code { match hw_keycode { 0x0009 => Code::Escape, 0x000A => Code::Digit1, diff --git a/v2/src/backend/shared/linux/env.rs b/v2/src/backend/shared/linux/env.rs index 59c7be20..c3aa1a7c 100644 --- a/v2/src/backend/shared/linux/env.rs +++ b/v2/src/backend/shared/linux/env.rs @@ -1,4 +1,4 @@ -pub fn locale() -> String { +pub(in crate::backend) fn locale() -> String { let mut locale = iso_locale(); // This is done because the locale parsing library we use (TODO - do we?) expects an unicode locale, but these vars have an ISO locale if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') { @@ -7,7 +7,7 @@ pub fn locale() -> String { locale } -pub fn iso_locale() -> String { +pub(in crate::backend) fn iso_locale() -> String { fn locale_env_var(var: &str) -> Option { match std::env::var(var) { Ok(s) if s.is_empty() => { diff --git a/v2/src/backend/shared/linux/mod.rs b/v2/src/backend/shared/linux/mod.rs index d40b15d9..77cdf2c5 100644 --- a/v2/src/backend/shared/linux/mod.rs +++ b/v2/src/backend/shared/linux/mod.rs @@ -1,2 +1,2 @@ // environment based utilities -pub mod env; +pub(in crate::backend) mod env; diff --git a/v2/src/backend/shared/mod.rs b/v2/src/backend/shared/mod.rs index e3960601..032f8951 100644 --- a/v2/src/backend/shared/mod.rs +++ b/v2/src/backend/shared/mod.rs @@ -17,7 +17,7 @@ cfg_if::cfg_if! { if #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "linux", target_os = "openbsd"))] { mod keyboard; - pub use keyboard::*; + pub(in crate::backend) use keyboard::*; } } cfg_if::cfg_if! { diff --git a/v2/src/backend/shared/xkb/xkb_api.rs b/v2/src/backend/shared/xkb/xkb_api.rs index e8feab29..cbcf7983 100644 --- a/v2/src/backend/shared/xkb/xkb_api.rs +++ b/v2/src/backend/shared/xkb/xkb_api.rs @@ -28,20 +28,20 @@ use x11rb::xcb_ffi::XCBConnection; use super::keycodes::{is_backspace, map_for_compose}; #[cfg(feature = "x11")] -pub struct DeviceId(pub std::os::raw::c_int); +pub(in crate::backend) struct DeviceId(pub std::os::raw::c_int); /// A global xkb context object. /// /// Reference counted under the hood. // Assume this isn't threadsafe unless proved otherwise. (e.g. don't implement Send/Sync) // Safety: Is a valid xkb_context -pub struct Context(*mut xkb_context); +pub(in crate::backend) struct Context(*mut xkb_context); impl Context { /// Create a new xkb context. /// /// The returned object is lightweight and clones will point at the same context internally. - pub fn new() -> Self { + pub(in crate::backend) fn new() -> Self { // Safety: No given preconditions let ctx = unsafe { xkb_context_new(xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; if ctx.is_null() { @@ -53,7 +53,10 @@ impl Context { } #[cfg(feature = "x11")] - pub fn core_keyboard_device_id(&self, conn: &XCBConnection) -> Option { + pub(in crate::backend) fn core_keyboard_device_id( + &self, + conn: &XCBConnection, + ) -> Option { let id = unsafe { xkb_x11_get_core_keyboard_device_id( conn.get_raw_xcb_connection() as *mut xcb_connection_t @@ -67,7 +70,7 @@ impl Context { } #[cfg(feature = "x11")] - pub fn keymap_from_x11_device( + pub(in crate::backend) fn keymap_from_x11_device( &self, conn: &XCBConnection, device: &DeviceId, @@ -87,7 +90,7 @@ impl Context { } #[cfg(feature = "x11")] - pub fn state_from_x11_keymap( + pub(in crate::backend) fn state_from_x11_keymap( &mut self, keymap: &Keymap, conn: &XCBConnection, @@ -107,7 +110,10 @@ impl Context { } #[cfg(feature = "wayland")] - pub fn state_from_keymap(&mut self, keymap: &Keymap) -> Option { + pub(in crate::backend) fn state_from_keymap( + &mut self, + keymap: &Keymap, + ) -> Option { let state = unsafe { xkb_state_new(keymap.0) }; if state.is_null() { return None; @@ -118,7 +124,7 @@ impl Context { /// /// Uses `xkb_keymap_new_from_buffer` under the hood. #[cfg(feature = "wayland")] - pub fn keymap_from_slice(&self, buffer: &[u8]) -> Keymap { + pub(in crate::backend) 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. @@ -198,12 +204,12 @@ impl Drop for Context { } } -pub struct Keymap(*mut xkb_keymap); +pub(in crate::backend) struct Keymap(*mut xkb_keymap); impl Keymap { #[cfg(feature = "wayland")] /// Whether the given key should repeat - pub fn repeats(&mut self, scancode: u32) -> bool { + pub(in crate::backend) fn repeats(&mut self, scancode: u32) -> bool { unsafe { xkb_keymap_key_repeats(self.0, scancode) == 1 } } } @@ -216,7 +222,7 @@ impl Drop for Keymap { } } -pub struct KeyEventsState { +pub(in crate::backend) struct KeyEventsState { mods_state: *mut xkb_state, mod_indices: ModsIndices, compose_state: Option>, @@ -228,7 +234,7 @@ pub struct KeyEventsState { } #[derive(Clone, Copy, Debug)] -pub struct ModsIndices { +pub(in crate::backend) struct ModsIndices { control: xkb_mod_index_t, shift: xkb_mod_index_t, alt: xkb_mod_index_t, @@ -238,7 +244,7 @@ pub struct ModsIndices { } #[derive(Clone, Copy, Debug)] -pub struct ActiveModifiers { +pub(in crate::backend) struct ActiveModifiers { pub base_mods: xkb_mod_mask_t, pub latched_mods: xkb_mod_mask_t, pub locked_mods: xkb_mod_mask_t, @@ -249,13 +255,13 @@ pub struct ActiveModifiers { #[derive(Copy, Clone)] /// An opaque representation of a KeySym, to make APIs less error prone -pub struct KeySym(xkb_keysym_t); +pub(in crate::backend) struct KeySym(xkb_keysym_t); impl KeyEventsState { /// Stop the active composition. /// This should happen if the text field changes, or the selection within the text field changes /// or the IME is activated - pub fn cancel_composing(&mut self) -> bool { + pub(in crate::backend) fn cancel_composing(&mut self) -> bool { let was_composing = self.is_composing; self.is_composing = false; if let Some(state) = self.compose_state { @@ -264,7 +270,7 @@ impl KeyEventsState { was_composing } - pub fn update_xkb_state(&mut self, mods: ActiveModifiers) { + pub(in crate::backend) fn update_xkb_state(&mut self, mods: ActiveModifiers) { unsafe { xkb_state_update_mask( self.mods_state, @@ -302,7 +308,7 @@ impl KeyEventsState { /// /// This method calculates the key event which is passed to the `key_down` handler. /// This is step "0" if that process - pub fn key_event( + pub(in crate::backend) fn key_event( &mut self, scancode: u32, keysym: KeySym, @@ -339,7 +345,7 @@ impl KeyEventsState { /// the user should be (and if it changed) /// - If composition finished, what the inserted string should be /// - Otherwise, does nothing - pub(crate) fn compose_key_down<'a>( + pub(in crate::backend) fn compose_key_down<'a>( &'a mut self, event: &KeyEvent, keysym: KeySym, @@ -485,7 +491,7 @@ impl KeyEventsState { } } - pub fn cancelled_string(&mut self) -> &str { + pub(in crate::backend) fn cancelled_string(&mut self) -> &str { // Clearing the compose string and other state isn't needed, // as it is cleared at the start of the next composition self.is_composing = false; @@ -580,7 +586,7 @@ impl KeyEventsState { } /// Get the single (opaque) KeySym the given scan - pub fn get_one_sym(&mut self, scancode: u32) -> KeySym { + pub(in crate::backend) fn get_one_sym(&mut self, scancode: u32) -> KeySym { // TODO: We should use xkb_state_key_get_syms here (returning &'keymap [*const xkb_keysym_t]) // but that is complicated slightly by the fact that we'd need to implement our own // capitalisation transform diff --git a/v2/src/backend/shared/xkb/xkbcommon_sys.rs b/v2/src/backend/shared/xkb/xkbcommon_sys.rs index 9ba9b4cb..3add1770 100644 --- a/v2/src/backend/shared/xkb/xkbcommon_sys.rs +++ b/v2/src/backend/shared/xkb/xkbcommon_sys.rs @@ -1,4 +1,10 @@ -#![allow(unused, non_upper_case_globals, non_camel_case_types, non_snake_case)] +#![allow( + unused, + non_upper_case_globals, + non_camel_case_types, + non_snake_case, + unreachable_pub +)] use nix::libc::FILE; include!(concat!(env!("OUT_DIR"), "/xkbcommon_sys.rs")); diff --git a/v2/src/glazier.rs b/v2/src/glazier.rs index 9674838c..8180e449 100644 --- a/v2/src/glazier.rs +++ b/v2/src/glazier.rs @@ -51,10 +51,9 @@ impl Glazier<'_> { self.new_window(builder_instance) } - pub fn new_window(&mut self, mut desc: WindowDescription) -> WindowId { + pub fn new_window(&mut self, desc: WindowDescription) -> WindowId { tracing::trace!("Will create window"); - desc.assign_id() - // self.0.new_window(desc) + self.0.new_window(desc) } pub fn close_window(&mut self, win: WindowId) { diff --git a/v2/src/lib.rs b/v2/src/lib.rs index 33d2be14..1af52ba8 100644 --- a/v2/src/lib.rs +++ b/v2/src/lib.rs @@ -41,6 +41,7 @@ //! `glazier` is an abstraction around a given platform UI & application //! framework. It provides common types, which then defer to a platform-defined //! implementation. +#![warn(unreachable_pub)] use std::{any::Any, marker::PhantomData, ops::Deref}; diff --git a/v2/src/window.rs b/v2/src/window.rs index 873eef6c..15deefe5 100644 --- a/v2/src/window.rs +++ b/v2/src/window.rs @@ -4,6 +4,7 @@ use crate::Counter; mod region; mod scale; +use kurbo_0_9::Size; pub use region::*; pub use scale::*; use thiserror::Error; @@ -23,17 +24,20 @@ use thiserror::Error; /// glz.new_window(my_window); /// ``` #[derive(Debug)] +#[forbid(clippy::partial_pub_fields)] pub struct WindowDescription { pub title: String, // menu: Option, - // size: Size, - // min_size: Option, + pub size: Size, + pub min_size: Option, // position: Option, - // level: Option, + pub level: WindowLevel, // window_state: Option, pub resizable: bool, pub show_titlebar: bool, pub transparent: bool, + /// The application id which will be used on Linux + pub app_id: Option, /// The identifier the window created from this descriptor will be assigned. /// /// In most cases you should leave this as `None`. If you do need access @@ -52,9 +56,13 @@ impl WindowDescription { pub fn new(title: impl Into) -> Self { WindowDescription { title: title.into(), + level: WindowLevel::AppWindow, + min_size: None, + size: Size::new(600., 800.), resizable: true, show_titlebar: true, transparent: false, + app_id: None, id: None, } } From a8c0b2f70f7b20fd5cad75e15e41e68d42392ad3 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:26:23 +0100 Subject: [PATCH 10/10] Stash un-looked-at changes --- v2/src/backend/mod.rs | 2 +- v2/src/backend/new_wayland/run_loop.rs | 6 +- v2/src/backend/new_wayland/windows.rs | 340 +++++++++++++++++++++---- v2/src/glazier.rs | 12 +- v2/src/handler.rs | 7 +- v2/src/window.rs | 7 +- 6 files changed, 322 insertions(+), 52 deletions(-) diff --git a/v2/src/backend/mod.rs b/v2/src/backend/mod.rs index 352d268d..b38d9b79 100644 --- a/v2/src/backend/mod.rs +++ b/v2/src/backend/mod.rs @@ -1,4 +1,4 @@ mod new_wayland; mod shared; -pub use new_wayland::*; +pub(crate) use new_wayland::*; diff --git a/v2/src/backend/new_wayland/run_loop.rs b/v2/src/backend/new_wayland/run_loop.rs index 96f4de8c..083ba717 100644 --- a/v2/src/backend/new_wayland/run_loop.rs +++ b/v2/src/backend/new_wayland/run_loop.rs @@ -34,7 +34,7 @@ use crate::{ Glazier, PlatformHandler, }; -pub fn launch( +pub(crate) fn launch( mut handler: Box, on_init: impl FnOnce(&mut dyn PlatformHandler, Glazier), ) -> Result<(), Error> { @@ -141,12 +141,12 @@ impl WaylandState { } #[derive(Clone)] -pub struct LoopHandle { +pub(crate) struct LoopHandle { loop_sender: channel::Sender, } impl LoopHandle { - pub fn run_on_main(&self, callback: F) + pub(crate) fn run_on_main(&self, callback: F) where F: FnOnce(&mut dyn PlatformHandler, Glazier) + Send + 'static, { diff --git a/v2/src/backend/new_wayland/windows.rs b/v2/src/backend/new_wayland/windows.rs index ca0b9ad3..4974feb1 100644 --- a/v2/src/backend/new_wayland/windows.rs +++ b/v2/src/backend/new_wayland/windows.rs @@ -5,11 +5,13 @@ use smithay_client_toolkit::{ client::{ globals::BindError, protocol::{ + wl_callback::{self, WlCallback}, wl_compositor::{self, WlCompositor}, wl_surface::{self, WlSurface}, }, Connection, Dispatch, Proxy, QueueHandle, }, + csd_frame::WindowManagerCapabilities, protocols::{ wp::{ fractional_scale::v1::client::{ @@ -18,10 +20,13 @@ use smithay_client_toolkit::{ }, viewporter::client::wp_viewporter::{self, WpViewporter}, }, - xdg::shell::client::{ - xdg_surface::{self, XdgSurface}, - xdg_toplevel::{self, XdgToplevel}, - xdg_wm_base::{self, XdgWmBase}, + xdg::{ + decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, + shell::client::{ + xdg_surface::{self, XdgSurface}, + xdg_toplevel::{self, WmCapabilities, XdgToplevel}, + xdg_wm_base::{self, XdgWmBase}, + }, }, }, }, @@ -40,6 +45,7 @@ pub(super) struct Windowing { xdg: XdgWmBase, fractional_scale: Option, viewporter: Option, + decoration_manager: Option, windows: BTreeMap, surface_to_window: HashMap, @@ -56,12 +62,14 @@ impl Windowing { let xdg = registry.bind_one(qh, 2..=6, ())?; let fractional_scale = registry.bind_one(qh, 1..=1, ()).ok(); let viewporter = registry.bind_one(qh, 1..=1, ()).ok(); + let decoration_manager = registry.bind_one(qh, 1..=1, ()).ok(); Ok(Self { compositor, xdg, fractional_scale, viewporter, + decoration_manager, surface_to_window: Default::default(), windows: Default::default(), }) @@ -75,14 +83,15 @@ impl WaylandState { let window_id = desc.assign_id(); let WindowDescription { title, + initial_size, + min_size, + level, + // Meaningless on wayland? resizable, show_titlebar, // TODO: Handling titlebars is tricky on wayland, we need to work out the right API - transparent: _, // Meaningless on wayland? - id: _, // Already used + transparent: _, app_id, - size, - min_size, - level, + id: _, } = desc; if level != WindowLevel::AppWindow { tracing::error!("The Wayland backend doesn't yet support {level:?} windows"); @@ -107,6 +116,14 @@ impl WaylandState { if let Some(app_id) = app_id { toplevel.set_app_id(app_id); } + if let Some(min_size) = min_size { + if min_size.is_finite() && min_size.width > 0. && min_size.height > 0. { + toplevel.set_min_size(min_size.width as i32, min_size.height as i32) + } else { + todo!("Couldn't apply invalid min_size: {min_size:?}"); + } + } + // Do the first, empty, commit surface.commit(); @@ -114,17 +131,23 @@ impl WaylandState { windows.windows.insert( window_id, PerWindowState { - surface, - xdg_surface, toplevel, + xdg_surface, + fractional_scale, + surface, _show_titlebar: show_titlebar, + resizable, - initial_configure_complete: false, - requested_scale: ScaleSource::Fallback(1), + applied_scale: Scale::default(), + app_requested_scale: None, - fractional_scale, monitors: Vec::new(), - applied_scale: Scale::default(), + initial_configure_complete: false, + platform_requested_scale: None, + is_closing: false, + + pending_frame_callback: false, + will_repaint: false, }, ); window_id @@ -138,7 +161,7 @@ enum ScaleSource { /// Stored as a multiple of 120ths of the 'actual' scale. /// /// This avoids doing floating point comparisons - Fractional(i32), + Fractional(u32), Buffer(i32), Fallback(i32), } @@ -150,7 +173,9 @@ impl ScaleSource { fn normalise(&self) -> i32 { match self { - ScaleSource::Fractional(v) => *v, + ScaleSource::Fractional(v) => (*v) + .try_into() + .expect("Fractional scale should be sensible"), ScaleSource::Buffer(s) | ScaleSource::Fallback(s) => s * FRACTIONAL_DENOMINATOR, } } @@ -197,13 +222,93 @@ struct PerWindowState { // Configuration _show_titlebar: bool, + /// Whether the window should be resizable. + /// This has a few consequences resizable: bool, + /// The currently active `Scale` applied_scale: Scale, + /// The scale requested by the app. Used to + app_requested_scale: Option, - // State + // # State + /// The monitors this window is located within monitors: Vec, + + // Drawing + /// Whether a `frame` callback is currently active + /// + /// ## Context + /// Wayland requires frame (repainting) callbacks be requested *before* running commit. + /// However, user code controls when commit is called (generally through calling + /// wgpu's `present`). Generally, this would mean we would need to know whether the hint + /// needed to be requested *before* drawing the previous frame, which isn't ideal. + /// Instead, we follow this procedure: + /// - Always request a throttling hint before `paint`ing + /// - Only `paint` in response to that hint *if* the app requested to be redrawn + /// - `paint` in response to an app request to redraw *only* if there is no running hint + pending_frame_callback: bool, + /// Whether an (app launched) repaint request will be sent when the latest + will_repaint: bool, + /// We can't draw until the initial configure is complete initial_configure_complete: bool, - requested_scale: ScaleSource, + + platform_requested_scale: Option, + is_closing: bool, +} + +/// The context do_paint is called in +enum PaintContext { + /// Painting occurs during a `frame` callback and finished, we know that there are no more frame callbacks + Frame, + /// We're actioning a repaint request, when there was a callback + Requested, + /// We're painting in response to a configure event + Configure, +} + +impl WaylandPlatform { + /// Request that the application paint the window + fn do_paint(&mut self, win: WindowId, context: PaintContext, force: bool) { + let this = self + .state + .windows + .windows + .get_mut(&win) + .expect("Window present in do_paint"); + if matches!(context, PaintContext::Frame) { + this.pending_frame_callback = false; + } + if matches!(context, PaintContext::Requested) && this.pending_frame_callback && !force { + // We'll handle this in the frame callback, when that occurs. + // This ensures throttling is respected + // This also prevents a hang on startup, although the reason for that occuring isn't clear + return; + } + if !this.initial_configure_complete || (!this.will_repaint && !force) { + return; + } + this.will_repaint = false; + // If there is not a frame callback in flight, we request it here + // This branch could be skipped e.g. on `configure`, which ignores frame throttling hints and + // always paints eagerly, even if there is a frame callback running + // TODO: Is that the semantics we want? + if !this.pending_frame_callback { + this.pending_frame_callback = true; + this.surface + .frame(&self.state.wayland_queue, FrameCallbackData(win)); + } + } +} + +impl WaylandState { + pub(crate) fn set_window_scale(&mut self, win: WindowId, scale: Scale) { + let Some(this) = self.windows.windows.get_mut(&win) else { + tracing::error!("Called `set_window_scale` on an unknown/deleted window {win:?}"); + return; + }; + this.app_requested_scale = Some(scale); + // TODO: Request repaint? + } } impl Dispatch for WaylandPlatform { @@ -219,12 +324,20 @@ impl Dispatch for WaylandPlatform { tracing::error!(?event, "Got unexpected event after deleting a window"); return; }; + if this.is_closing { + return; + } match event { wl_surface::Event::Enter { output } => { let new_monitor = plat.state.monitors.window_entered_output(&output, data.0); if let Some(monitor) = new_monitor { this.monitors.push(monitor); let new_scale = did_fallback_scale_change(this, &mut plat.state.monitors); + if let Some(new_scale) = new_scale { + plat.with_glz(|handler, glz| { + handler.platform_proposed_scale(glz, data.0, new_scale); + }); + } } else { tracing::warn!( ?output, @@ -235,10 +348,8 @@ impl Dispatch for WaylandPlatform { wl_surface::Event::Leave { output } => { let removed_monitor = plat.state.monitors.window_left_output(&output, data.0); if let Some(monitor) = removed_monitor { - // Keep only the items which aren't this monitor, i.e. remove this item - // TODO: swap_remove? - // We expect this array to be let existing_len = this.monitors.len(); + // Keep only the items which aren't this monitor, i.e. remove this item this.monitors.retain(|item| item != &monitor); if this.monitors.len() == existing_len { tracing::warn!( @@ -248,13 +359,35 @@ impl Dispatch for WaylandPlatform { return; } let new_scale = did_fallback_scale_change(this, &mut plat.state.monitors); + if let Some(new_scale) = new_scale { + plat.with_glz(|handler, glz| { + handler.platform_proposed_scale(glz, data.0, new_scale); + }); + } } - - // TODO: Recalculate scale if we never got preferred scale through another means } - wl_surface::Event::PreferredBufferScale { factor } => todo!(), - wl_surface::Event::PreferredBufferTransform { transform } => todo!(), + wl_surface::Event::PreferredBufferScale { factor } => { + let source = ScaleSource::Buffer(factor); + let proposed_scale = if let Some(existing) = this.platform_requested_scale { + let new = ScaleSource::better(&existing, &source); + let had_same_value = existing.equal(&new); + if had_same_value { + return; + } + new + } else { + *this.platform_requested_scale.insert(source) + }; + let proposed_scale = proposed_scale.as_scale(); + plat.with_glz(|handler, glz| { + handler.platform_proposed_scale(glz, data.0, proposed_scale); + }); + } + wl_surface::Event::PreferredBufferTransform { transform } => { + // TODO: Do we want to abstract over this? + tracing::info!("Platform suggested a transform {transform:?}"); + } event => on_unknown_event(proxy, event), } } @@ -264,14 +397,20 @@ fn did_fallback_scale_change( this: &mut PerWindowState, outputs: &mut super::outputs::Outputs, ) -> Option { - if this.requested_scale.needs_fallback() && this.initial_configure_complete { - let new_factor = ScaleSource::Fractional( - outputs.max_fallback_integer_scale(this.monitors.iter().copied()), - ); - let was_same = new_factor.equal(&this.requested_scale); - this.requested_scale = new_factor; - return Some(new_factor.as_scale()); + // If we don't have an existing, that means we didn't request the fallback yet + if let Some(existing) = this.platform_requested_scale { + if existing.needs_fallback() { + let new_factor = ScaleSource::Fallback( + outputs.max_fallback_integer_scale(this.monitors.iter().copied()), + ); + let was_same = new_factor.equal(&existing); + this.platform_requested_scale = Some(new_factor); + if !was_same { + return Some(new_factor.as_scale()); + } + } } + return None; } @@ -281,22 +420,50 @@ impl Dispatch for WaylandPlatform { proxy: &XdgToplevel, event: xdg_toplevel::Event, data: &SurfaceUserData, - conn: &Connection, - qhandle: &QueueHandle, + _: &Connection, + _: &QueueHandle, ) { let Some(this) = plat.state.windows.windows.get_mut(&data.0) else { tracing::error!(?event, "Got unexpected event after deleting a window"); return; }; + if this.is_closing { + return; + } match event { xdg_toplevel::Event::Configure { width, height, states, - } => todo!(), - xdg_toplevel::Event::Close => todo!(), - xdg_toplevel::Event::ConfigureBounds { width, height } => todo!(), - xdg_toplevel::Event::WmCapabilities { capabilities } => todo!(), + } => { + + // Test + } + xdg_toplevel::Event::Close => {} + xdg_toplevel::Event::ConfigureBounds { width, height } => {} + xdg_toplevel::Event::WmCapabilities { capabilities } => { + // Adapted from Smithay Client Toolkit + let capabilities = capabilities + .chunks_exact(4) + .flat_map(TryInto::<[u8; 4]>::try_into) + .map(u32::from_ne_bytes) + .map(|val| WmCapabilities::try_from(val).map_err(|()| val)) + .fold(WindowManagerCapabilities::empty(), |acc, capability| { + acc | match capability { + Ok(WmCapabilities::WindowMenu) => { + WindowManagerCapabilities::WINDOW_MENU + } + Ok(WmCapabilities::Maximize) => WindowManagerCapabilities::MAXIMIZE, + Ok(WmCapabilities::Fullscreen) => WindowManagerCapabilities::FULLSCREEN, + Ok(WmCapabilities::Minimize) => WindowManagerCapabilities::MINIMIZE, + Ok(_) => return acc, + Err(v) => { + tracing::warn!(?proxy, "Unrecognised window capability {v}"); + return acc; + } + } + }); + } event => on_unknown_event(proxy, event), } } @@ -308,15 +475,45 @@ impl Dispatch for WaylandPlatform { proxy: &XdgSurface, event: xdg_surface::Event, data: &SurfaceUserData, - conn: &Connection, - qhandle: &QueueHandle, + _: &Connection, + _: &QueueHandle, ) { let Some(this) = plat.state.windows.windows.get_mut(&data.0) else { tracing::error!(?event, "Got unexpected event after deleting a window"); return; }; + if this.is_closing { + return; + } match event { - xdg_surface::Event::Configure { serial } => todo!(), + xdg_surface::Event::Configure { serial } => { + if !this.initial_configure_complete { + // TODO: What does this mean? + this.initial_configure_complete = true; + } + if this.platform_requested_scale.is_none() { + // We need to use the fallback, so do that + let new_factor = ScaleSource::Fallback( + plat.state + .monitors + .max_fallback_integer_scale(this.monitors.iter().copied()), + ); + this.platform_requested_scale = Some(new_factor); + plat.with_glz(|handler, glz| { + handler.platform_proposed_scale(glz, data.0, new_factor.as_scale()) + }); + } + let this = plat + .state + .windows + .windows + .get_mut(&data.0) + .expect("User's handler can't delete window"); + if this.is_closing { + return; + } + this.xdg_surface.ack_configure(serial); + } event => on_unknown_event(proxy, event), } } @@ -335,8 +532,50 @@ impl Dispatch for WaylandPlatform { tracing::error!(?event, "Got unexpected event after deleting a window"); return; }; + if this.is_closing { + return; + } + match event { + wp_fractional_scale_v1::Event::PreferredScale { scale } => { + let source = ScaleSource::Fractional(scale); + let proposed_scale = if let Some(existing) = this.platform_requested_scale { + let new = ScaleSource::better(&existing, &source); + let had_same_value = existing.equal(&new); + if had_same_value { + return; + } + new + } else { + *this.platform_requested_scale.insert(source) + }; + let proposed_scale = proposed_scale.as_scale(); + + plat.with_glz(|handler, glz| { + handler.platform_proposed_scale(glz, data.0, proposed_scale); + }); + } + event => on_unknown_event(proxy, event), + } + } +} + +struct FrameCallbackData(WindowId); + +impl Dispatch for WaylandPlatform { + fn event( + state: &mut Self, + proxy: &WlCallback, + event: wl_callback::Event, + data: &FrameCallbackData, + _: &Connection, + _: &QueueHandle, + ) { match event { - wp_fractional_scale_v1::Event::PreferredScale { scale } => todo!(), + wl_callback::Event::Done { + callback_data: _current_time, + } => { + state.do_paint(data.0, PaintContext::Frame, false); + } event => on_unknown_event(proxy, event), } } @@ -404,3 +643,18 @@ impl Dispatch for WaylandPlatform { } } } + +impl Dispatch for WaylandPlatform { + fn event( + _: &mut Self, + proxy: &ZxdgDecorationManagerV1, + event: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + _ => on_unknown_event(proxy, event), + } + } +} diff --git a/v2/src/glazier.rs b/v2/src/glazier.rs index 8180e449..38f603e8 100644 --- a/v2/src/glazier.rs +++ b/v2/src/glazier.rs @@ -1,6 +1,8 @@ use std::{any::TypeId, marker::PhantomData}; -use crate::{backend, LoopHandle, PlatformHandler, RawLoopHandle, WindowDescription, WindowId}; +use crate::{ + backend, window::Scale, LoopHandle, PlatformHandler, RawLoopHandle, WindowDescription, WindowId, +}; /// A short-lived handle for communication with the platform, /// which is available whilst an event handler is being called @@ -62,4 +64,10 @@ impl Glazier<'_> { } /// Window State/Appearance management -impl Glazier<'_> {} +impl Glazier<'_> { + /// Set the scale which will be used for this Window. In most cases, this should be the + #[track_caller] + pub fn set_window_scale(&mut self, win: WindowId, scale: Scale) { + self.0.set_window_scale(win, scale); + } +} diff --git a/v2/src/handler.rs b/v2/src/handler.rs index 3adaac24..9b4db606 100644 --- a/v2/src/handler.rs +++ b/v2/src/handler.rs @@ -1,7 +1,7 @@ use std::any::Any; use crate::{ - window::{IdleToken, Region, WindowCreationError}, + window::{IdleToken, Region, Scale, WindowCreationError}, Glazier, WindowId, }; @@ -80,6 +80,11 @@ pub trait PlatformHandler: Any { // /// [`paint`]: PlatformHandler::paint // fn surface_invalidated(&mut self, glz: Glazier, win: WindowId); + /// The platform proposed a new scale factor. + fn platform_proposed_scale(&mut self, mut glz: Glazier, win: WindowId, scale: Scale) { + glz.set_window_scale(win, scale) + } + /// Request the handler to prepare to paint the window contents. In particular, if there are /// any regions that need to be repainted on the next call to `paint`, the handler should /// invalidate those regions by calling [`Glazier::invalidate_rect`] or diff --git a/v2/src/window.rs b/v2/src/window.rs index 15deefe5..3b585fbd 100644 --- a/v2/src/window.rs +++ b/v2/src/window.rs @@ -28,7 +28,10 @@ use thiserror::Error; pub struct WindowDescription { pub title: String, // menu: Option, - pub size: Size, + // TODO: We need to be extremely careful around sizes, to avoid + // mixing up logical and physical sizes, and to make it easy for + // our users + pub initial_size: Size, pub min_size: Option, // position: Option, pub level: WindowLevel, @@ -58,7 +61,7 @@ impl WindowDescription { title: title.into(), level: WindowLevel::AppWindow, min_size: None, - size: Size::new(600., 800.), + initial_size: Size::new(600., 800.), resizable: true, show_titlebar: true, transparent: false,