diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 969d644230..a59361e682 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,40 +78,47 @@ jobs: profile: minimal override: true - - name: cargo clippy (no-svg) + - name: cargo clippy (druid) uses: actions-rs/cargo@v1 with: command: clippy - args: --all -- -D warnings - if: contains(matrix.os, 'ubuntu') != true + args: --manifest-path=druid/Cargo.toml --features=svg,image -- -D warnings - - name: cargo clippy (svg) + - name: cargo clippy (druid-shell) uses: actions-rs/cargo@v1 with: command: clippy - args: --all --all-features -- -D warnings - if: contains(matrix.os, 'ubuntu') + args: --manifest-path=druid-shell/Cargo.toml -- -D warnings + - name: cargo clippy (druid-derive) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --manifest-path=druid-derive/Cargo.toml -- -D warnings - - name: cargo test --all + - name: cargo test (druid) uses: actions-rs/cargo@v1 with: command: test - args: --all - if: contains(matrix.os, 'ubuntu') != true + args: --manifest-path=druid/Cargo.toml --features=svg,image - - name: cargo test --all --all-features + - name: cargo test (druid-shell) uses: actions-rs/cargo@v1 with: command: test - args: --all --all-features - if: contains(matrix.os, 'ubuntu') + args: --manifest-path=druid-shell/Cargo.toml + + - name: cargo test (druid-derive) + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid-shell/Cargo.toml - name: Run rustc -D warnings in druid/ uses: actions-rs/cargo@v1 with: command: rustc - args: --manifest-path=druid/Cargo.toml -- -D warnings + args: --manifest-path=druid/Cargo.toml --features=svg,image -- -D warnings - name: Run rustc -d warnings in druid/druid-shell uses: actions-rs/cargo@v1 @@ -188,3 +195,47 @@ jobs: with: command: doc args: --document-private-items + + # by default on linux we test gtk; to test x11 we need a separate job. + test-stable-x11: + runs-on: ubuntu-latest + name: cargo test stable (x11) + steps: + - uses: actions/checkout@v1 + + - name: install deps + run: | + sudo apt update + sudo apt install libx11-dev libgtk-3-dev + + - name: install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + profile: minimal + override: true + + - name: cargo clippy (x11) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --manifest-path=druid-shell/Cargo.toml --features=x11 -- -D warnings + + - name: cargo test druid-shell + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid-shell/Cargo.toml --features=x11 + + - name: cargo test druid + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=druid/Cargo.toml --features=x11 + + - name: Run rustc -d warnings in druid/druid-shell + uses: actions-rs/cargo@v1 + with: + command: rustc + args: --manifest-path=druid-shell/Cargo.toml --features=x11 -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index 1d320cef39..ffb93f57db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,7 @@ name = "druid-shell" version = "0.5.0" dependencies = [ "cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "cocoa 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", "gdk 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -372,6 +373,7 @@ dependencies = [ "time 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "wio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "xcb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1601,6 +1603,25 @@ dependencies = [ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "x11" +version = "2.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "xcb" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "x11 2.18.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "xi-unicode" version = "0.2.0" @@ -1794,6 +1815,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum wio 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +"checksum x11 2.18.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77ecd092546cb16f25783a5451538e73afc8d32e242648d54f4ae5459ba1e773" +"checksum xcb 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62056f63138b39116f82a540c983cc11f1c90cd70b3d492a70c25eaa50bd22a6" "checksum xi-unicode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7395cdb9d0a6219fa0ea77d08c946adf9c1984c72fcd443ace30365f3daadef7" "checksum xmlparser 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ccb4240203dadf40be2de9369e5c6dec1bf427528115b030baca3334c18362d7" "checksum xmlwriter 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" diff --git a/README.md b/README.md index acedb52026..bcb137b3ac 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,10 @@ should be sufficient. Removing this dependency is on the roadmap. On Linux, druid requires gtk+3; see [gtk-rs dependencies] for installation instructions. +Alternatively, there is an X11 backend available, although it is currently +[missing quite a few features](https://github.com/xi-editor/druid/issues/475). +You can try it out with `--features=x11`. + ## Alternatives Druid is only one of many ongoing [Rust-native GUI experiments]. To mention a diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index 7d7f0fcf48..efd2bc5127 100644 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" [features] use_gtk = ["gtk", "gtk-sys", "gio", "gdk", "gdk-sys", "glib", "glib-sys", "cairo-rs"] +x11 = ["xcb", "cairo-sys-rs"] [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" @@ -25,6 +26,7 @@ cfg-if = "0.1.10" kurbo = "0.5.11" cairo-rs = { version = "0.8.1", default_features = false, optional = true } +cairo-sys-rs = { version = "0.9.2", default_features = false, optional = true } gio = { version = "0.8.1", optional = true } gdk = { version = "0.12.1", optional = true } gdk-sys = { version = "0.9.0", optional = true } @@ -32,6 +34,7 @@ gtk = { version = "0.8.1", optional = true } glib = { version = "0.9.3", optional = true } glib-sys = { version = "0.9.0", optional = true } gtk-sys = { version = "0.9.0", optional = true } +xcb = { version = "0.9.0", features = ["thread", "xlib_xcb", "randr"], optional = true } [target.'cfg(target_os="windows")'.dependencies] wio = "0.2" @@ -45,8 +48,9 @@ cocoa = "0.20.0" objc = "0.2.5" cairo-rs = { version = "0.8.1", default_features = false } +# TODO(x11/dependencies): only use feature "xcb" if using XCB [target.'cfg(target_os="linux")'.dependencies] -cairo-rs = { version = "0.8.1", default_features = false } +cairo-rs = { version = "0.8.1", default_features = false, features = ["xcb"] } gio = "0.8.1" gdk = "0.12.1" gdk-sys = "0.9.0" diff --git a/druid-shell/examples/perftest.rs b/druid-shell/examples/perftest.rs index 0c45553fbc..45fe0cef5a 100644 --- a/druid-shell/examples/perftest.rs +++ b/druid-shell/examples/perftest.rs @@ -121,5 +121,6 @@ fn main() { let window = builder.build().unwrap(); window.show(); + app.run(); } diff --git a/druid-shell/examples/shello.rs b/druid-shell/examples/shello.rs index 3e91b1990f..d1ca52cdbe 100644 --- a/druid-shell/examples/shello.rs +++ b/druid-shell/examples/shello.rs @@ -67,6 +67,7 @@ impl WinHandler for HelloState { let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500); let id = self.handle.request_timer(deadline); println!("keydown: {:?}, timer id = {:?}", event, id); + false } @@ -133,8 +134,9 @@ fn main() { builder.set_handler(Box::new(HelloState::default())); builder.set_title("Hello example"); builder.set_menu(menubar); - let window = builder.build().unwrap(); + let window = builder.build().unwrap(); window.show(); + app.run(); } diff --git a/druid-shell/src/common_util.rs b/druid-shell/src/common_util.rs index 4538bd9ea5..b72e294f24 100644 --- a/druid-shell/src/common_util.rs +++ b/druid-shell/src/common_util.rs @@ -21,6 +21,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; /// Strip the access keys from the menu string. /// /// Changes "E&xit" to "Exit". Actual ampersands are escaped as "&&". +#[cfg(not(feature = "x11"))] #[cfg(any(target_os = "macos", target_os = "linux"))] pub fn strip_access_key(raw_menu_text: &str) -> String { let mut saw_ampersand = false; diff --git a/druid-shell/src/dialog.rs b/druid-shell/src/dialog.rs index 5350c6ff3f..b9c110c945 100644 --- a/druid-shell/src/dialog.rs +++ b/druid-shell/src/dialog.rs @@ -23,6 +23,7 @@ pub struct FileInfo { } /// Type of file dialog. +#[cfg(not(feature = "x11"))] pub enum FileDialogType { /// File open dialog. Open, diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index 22189b556e..342832947f 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -36,6 +36,10 @@ extern crate objc; #[macro_use] extern crate lazy_static; +#[cfg(all(target_os = "linux", feature = "x11"))] +#[macro_use] +extern crate lazy_static; + mod application; mod clipboard; mod common_util; diff --git a/druid-shell/src/platform/mod.rs b/druid-shell/src/platform/mod.rs index 03ed704e51..0ae046dfc5 100644 --- a/druid-shell/src/platform/mod.rs +++ b/druid-shell/src/platform/mod.rs @@ -21,6 +21,9 @@ cfg_if::cfg_if! { } else if #[cfg(all(target_os = "macos", not(feature = "use_gtk")))] { mod mac; pub use mac::*; + } else if #[cfg(all(feature = "x11", target_os = "linux"))] { + mod x11; + pub use x11::*; } else if #[cfg(any(feature = "use_gtk", target_os = "linux"))] { mod gtk; pub use self::gtk::*; diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs new file mode 100644 index 0000000000..f534c7fc3b --- /dev/null +++ b/druid-shell/src/platform/x11/application.rs @@ -0,0 +1,211 @@ +// Copyright 2020 The xi-editor 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. + +//! X11 implementation of features at the application scope. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::Arc; + +use super::clipboard::Clipboard; +use super::window::XWindow; +use crate::application::AppHandler; +use crate::kurbo::Point; +use crate::{KeyCode, KeyModifiers, MouseButton, MouseEvent}; + +struct XcbConnection { + connection: Arc, + screen_num: i32, +} + +lazy_static! { + static ref XCB_CONNECTION: XcbConnection = XcbConnection::new(); +} + +thread_local! { + static WINDOW_MAP: RefCell> = RefCell::new(HashMap::new()); +} + +pub struct Application; + +impl Application { + pub fn new(_handler: Option>) -> Application { + Application + } + + pub(crate) fn add_xwindow(id: u32, xwindow: XWindow) { + WINDOW_MAP.with(|map| map.borrow_mut().insert(id, xwindow)); + } + + pub(crate) fn get_connection() -> Arc { + XCB_CONNECTION.connection_cloned() + } + + pub(crate) fn get_screen_num() -> i32 { + XCB_CONNECTION.screen_num() + } + + // TODO(x11/events): handle mouse scroll events + pub fn run(&mut self) { + let conn = XCB_CONNECTION.connection_cloned(); + loop { + if let Some(ev) = conn.wait_for_event() { + let ev_type = ev.response_type() & !0x80; + // NOTE: I don't think we should be doing this here, but I'm trying to keep + // the code mostly unchanged. My personal feeling is that the best approach + // is to dispatch events to the window as early as possible, that is to say + // I would send the *raw* events to the window and then let the window figure + // out how to handle them. - @cmyr + // + // Can't get which window to send the raw event to without first parsing the event + // and getting the window ID out of it :) - @crsaracco + match ev_type { + xcb::EXPOSE => { + let expose: &xcb::ExposeEvent = unsafe { xcb::cast_event(&ev) }; + let window_id = expose.window(); + WINDOW_MAP.with(|map| { + let mut windows = map.borrow_mut(); + if let Some(w) = windows.get_mut(&window_id) { + w.render(); + } + }) + } + xcb::KEY_PRESS => { + let key_press: &xcb::KeyPressEvent = unsafe { xcb::cast_event(&ev) }; + let key: u32 = key_press.detail() as u32; + let key_code: KeyCode = key.into(); + + let window_id = key_press.event(); + println!("window_id {}", window_id); + WINDOW_MAP.with(|map| { + let mut windows = map.borrow_mut(); + if let Some(w) = windows.get_mut(&window_id) { + w.key_down(key_code); + } + }) + } + xcb::BUTTON_PRESS => { + let button_press: &xcb::ButtonPressEvent = unsafe { xcb::cast_event(&ev) }; + let window_id = button_press.event(); + let mouse_event = MouseEvent { + pos: Point::new( + button_press.event_x() as f64, + button_press.event_y() as f64, + ), + mods: KeyModifiers { + shift: false, + alt: false, + ctrl: false, + meta: false, + }, + count: 0, + button: MouseButton::Left, + }; + WINDOW_MAP.with(|map| { + let mut windows = map.borrow_mut(); + if let Some(w) = windows.get_mut(&window_id) { + w.mouse_down(&mouse_event); + } + }) + } + xcb::BUTTON_RELEASE => { + let button_release: &xcb::ButtonReleaseEvent = + unsafe { xcb::cast_event(&ev) }; + let window_id = button_release.event(); + let mouse_event = MouseEvent { + pos: Point::new( + button_release.event_x() as f64, + button_release.event_y() as f64, + ), + mods: KeyModifiers { + shift: false, + alt: false, + ctrl: false, + meta: false, + }, + count: 0, + button: MouseButton::Left, + }; + WINDOW_MAP.with(|map| { + let mut windows = map.borrow_mut(); + if let Some(w) = windows.get_mut(&window_id) { + w.mouse_up(&mouse_event); + } + }) + } + xcb::MOTION_NOTIFY => { + let mouse_move: &xcb::MotionNotifyEvent = unsafe { xcb::cast_event(&ev) }; + let window_id = mouse_move.event(); + let mouse_event = MouseEvent { + pos: Point::new( + mouse_move.event_x() as f64, + mouse_move.event_y() as f64, + ), + mods: KeyModifiers { + shift: false, + alt: false, + ctrl: false, + meta: false, + }, + count: 0, + button: MouseButton::Left, + }; + WINDOW_MAP.with(|map| { + let mut windows = map.borrow_mut(); + if let Some(w) = windows.get_mut(&window_id) { + w.mouse_move(&mouse_event); + } + }) + } + _ => {} + } + } + } + } + + pub fn quit() { + // No-op. + } + + pub fn clipboard() -> Clipboard { + // TODO(x11/clipboard): implement Application::clipboard + log::warn!("Application::clipboard is currently unimplemented for X11 platforms."); + Clipboard {} + } + + pub fn get_locale() -> String { + // TODO(x11/locales): implement Application::get_locale + log::warn!("Application::get_locale is currently unimplemented for X11 platforms. (defaulting to en-US)"); + "en-US".into() + } +} + +impl XcbConnection { + fn new() -> Self { + let (conn, screen_num) = xcb::Connection::connect_with_xlib_display().unwrap(); + + Self { + connection: Arc::new(conn), + screen_num, + } + } + + fn connection_cloned(&self) -> Arc { + self.connection.clone() + } + + fn screen_num(&self) -> i32 { + self.screen_num + } +} diff --git a/druid-shell/src/platform/x11/clipboard.rs b/druid-shell/src/platform/x11/clipboard.rs new file mode 100644 index 0000000000..637b85b7bb --- /dev/null +++ b/druid-shell/src/platform/x11/clipboard.rs @@ -0,0 +1,56 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Interactions with the system pasteboard on X11. + +use crate::clipboard::{ClipboardFormat, FormatId}; + +#[derive(Debug, Clone, Default)] +pub struct Clipboard; + +impl Clipboard { + pub fn put_string(&mut self, _s: impl AsRef) { + // TODO(x11/clipboard): implement Clipboard::put_string + log::warn!("Clipboard::put_string is currently unimplemented for X11 platforms."); + } + + pub fn put_formats(&mut self, _formats: &[ClipboardFormat]) { + // TODO(x11/clipboard): implement Clipboard::put_formats + log::warn!("Clipboard::put_formats is currently unimplemented for X11 platforms."); + } + + pub fn get_string(&self) -> Option { + // TODO(x11/clipboard): implement Clipboard::get_string + log::warn!("Clipboard::set_string is currently unimplemented for X11 platforms."); + None + } + + pub fn preferred_format(&self, _formats: &[FormatId]) -> Option { + // TODO(x11/clipboard): implement Clipboard::preferred_format + log::warn!("Clipboard::preferred_format is currently unimplemented for X11 platforms."); + None + } + + pub fn get_format(&self, _format: FormatId) -> Option> { + // TODO(x11/clipboard): implement Clipboard::get_format + log::warn!("Clipboard::get_format is currently unimplemented for X11 platforms."); + None + } + + pub fn available_type_names(&self) -> Vec { + // TODO(x11/clipboard): implement Clipboard::available_type_names + log::warn!("Clipboard::available_type_names is currently unimplemented for X11 platforms."); + vec![] + } +} diff --git a/druid-shell/src/platform/x11/error.rs b/druid-shell/src/platform/x11/error.rs new file mode 100644 index 0000000000..e5cb9f695b --- /dev/null +++ b/druid-shell/src/platform/x11/error.rs @@ -0,0 +1,31 @@ +// Copyright 2020 The xi-editor 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. + +//! Errors at the application shell level. + +use std::fmt; + +#[derive(Debug, Clone)] +pub enum Error { + // TODO(x11/errors): enumerate `Error`s for X11 + NoError, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + // TODO(x11/errors): implement Error::fmt + log::warn!("Error::fmt is currently unimplemented for X11 platforms."); + write!(f, "X11 Error") + } +} diff --git a/druid-shell/src/platform/x11/keycodes.rs b/druid-shell/src/platform/x11/keycodes.rs new file mode 100644 index 0000000000..75b4e8ba7d --- /dev/null +++ b/druid-shell/src/platform/x11/keycodes.rs @@ -0,0 +1,334 @@ +// Copyright 2020 The xi-editor 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. + +//! X11 keycode handling. + +use gdk::enums::key::*; + +use crate::keyboard::StrOrChar; +use crate::keycodes::KeyCode; + +pub type RawKeyCode = u32; + +impl From for KeyCode { + #[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] + fn from(raw: u32) -> KeyCode { + match raw { + 9 => KeyCode::Escape, + 67 => KeyCode::F1, + 68 => KeyCode::F2, + 69 => KeyCode::F3, + 70 => KeyCode::F4, + 71 => KeyCode::F5, + 72 => KeyCode::F6, + 73 => KeyCode::F7, + 74 => KeyCode::F8, + 75 => KeyCode::F9, + 76 => KeyCode::F10, + 95 => KeyCode::F11, + 96 => KeyCode::F12, + + 107 => KeyCode::PrintScreen, + 118 => KeyCode::Insert, + 119 => KeyCode::Delete, + 49 => KeyCode::Quote, + + 10 => KeyCode::Numpad1, + 11 => KeyCode::Numpad2, + 12 => KeyCode::Numpad3, + 13 => KeyCode::Numpad4, + 14 => KeyCode::Numpad5, + 15 => KeyCode::Numpad6, + 16 => KeyCode::Numpad7, + 17 => KeyCode::Numpad8, + 18 => KeyCode::Numpad9, + 19 => KeyCode::Numpad0, + + 20 => KeyCode::LeftBracket, + 21 => KeyCode::RightBracket, + 22 => KeyCode::Backspace, + + 23 => KeyCode::Tab, + + 24 => KeyCode::KeyQ, + 25 => KeyCode::KeyW, + 26 => KeyCode::KeyE, + 27 => KeyCode::KeyR, + 28 => KeyCode::KeyT, + 29 => KeyCode::KeyY, + 30 => KeyCode::KeyU, + 31 => KeyCode::KeyI, + 32 => KeyCode::KeyO, + 33 => KeyCode::KeyP, + + 34 => KeyCode::Slash, + 35 => KeyCode::Equals, + 36 => KeyCode::Return, + + 66 => KeyCode::CapsLock, + + 38 => KeyCode::KeyA, + 39 => KeyCode::KeyS, + 40 => KeyCode::KeyD, + 41 => KeyCode::KeyF, + 42 => KeyCode::KeyG, + 43 => KeyCode::KeyH, + 44 => KeyCode::KeyJ, + 45 => KeyCode::KeyK, + 46 => KeyCode::KeyL, + + 47 => KeyCode::Semicolon, + 48 => KeyCode::Comma, + // 49 => KeyCode::Hash, + 50 => KeyCode::LeftShift, + 94 => KeyCode::Backslash, + + 52 => KeyCode::KeyZ, + 53 => KeyCode::KeyX, + 54 => KeyCode::KeyC, + 55 => KeyCode::KeyV, + 56 => KeyCode::KeyB, + 57 => KeyCode::KeyN, + 58 => KeyCode::KeyM, + + 59 => KeyCode::Comma, + 60 => KeyCode::Period, + 61 => KeyCode::NumpadMultiply, + 64 => KeyCode::RightShift, + + 37 => KeyCode::LeftControl, + 133 => KeyCode::LeftMeta, + // TODO(x11/keycodes): case 64 is duplicated; which one is correct? + // What should the other be? + // 64 => KeyCode::LeftAlt, + 65 => KeyCode::Space, + 108 => KeyCode::RightAlt, + 105 => KeyCode::RightControl, + + 111 => KeyCode::ArrowUp, + 113 => KeyCode::ArrowLeft, + 114 => KeyCode::ArrowRight, + 116 => KeyCode::ArrowDown, + + _ => { + log::warn!("Warning: unknown keyval {}", raw); + KeyCode::Unknown(raw) + } + } + } +} + +impl Into for KeyCode { + #[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] + fn into(self) -> StrOrChar { + match self { + KeyCode::Numpad1 => StrOrChar::Char('1'), + KeyCode::Numpad2 => StrOrChar::Char('2'), + KeyCode::Numpad3 => StrOrChar::Char('3'), + KeyCode::Numpad4 => StrOrChar::Char('4'), + KeyCode::Numpad5 => StrOrChar::Char('5'), + KeyCode::Numpad6 => StrOrChar::Char('6'), + KeyCode::Numpad7 => StrOrChar::Char('7'), + KeyCode::Numpad8 => StrOrChar::Char('8'), + KeyCode::Numpad9 => StrOrChar::Char('9'), + KeyCode::Numpad0 => StrOrChar::Char('0'), + + KeyCode::LeftBracket => StrOrChar::Char('['), + KeyCode::RightBracket => StrOrChar::Char(']'), + // KeyCode::Backspace => StrOrChar::Char('0'), + // KeyCode::Tab => StrOrChar::Char('0'), + KeyCode::KeyQ => StrOrChar::Char('q'), + KeyCode::KeyW => StrOrChar::Char('w'), + KeyCode::KeyE => StrOrChar::Char('e'), + KeyCode::KeyR => StrOrChar::Char('r'), + KeyCode::KeyT => StrOrChar::Char('t'), + KeyCode::KeyY => StrOrChar::Char('y'), + KeyCode::KeyU => StrOrChar::Char('u'), + KeyCode::KeyI => StrOrChar::Char('i'), + KeyCode::KeyO => StrOrChar::Char('o'), + KeyCode::KeyP => StrOrChar::Char('p'), + KeyCode::Slash => StrOrChar::Char('/'), + KeyCode::Equals => StrOrChar::Char('='), + // KeyCode::Return => StrOrChar::Char('0'), + // KeyCode::CapsLock => StrOrChar::Char('0'), + KeyCode::KeyA => StrOrChar::Char('a'), + KeyCode::KeyS => StrOrChar::Char('s'), + KeyCode::KeyD => StrOrChar::Char('d'), + KeyCode::KeyF => StrOrChar::Char('f'), + KeyCode::KeyG => StrOrChar::Char('g'), + KeyCode::KeyH => StrOrChar::Char('h'), + KeyCode::KeyJ => StrOrChar::Char('j'), + KeyCode::KeyK => StrOrChar::Char('k'), + KeyCode::KeyL => StrOrChar::Char('l'), + KeyCode::Semicolon => StrOrChar::Char(';'), + // KeyCode::Hash => StrOrChar::Char('#'), + // KeyCode::LeftShift => StrOrChar::Char('0'), + KeyCode::Backslash => StrOrChar::Char('/'), + + KeyCode::KeyZ => StrOrChar::Char('z'), + KeyCode::KeyX => StrOrChar::Char('x'), + KeyCode::KeyC => StrOrChar::Char('c'), + KeyCode::KeyV => StrOrChar::Char('v'), + KeyCode::KeyB => StrOrChar::Char('b'), + KeyCode::KeyN => StrOrChar::Char('n'), + KeyCode::KeyM => StrOrChar::Char('m'), + KeyCode::Comma => StrOrChar::Char(','), + KeyCode::Period => StrOrChar::Char('.'), + KeyCode::NumpadMultiply => StrOrChar::Char('*'), + // KeyCode::RightShift => StrOrChar::Char('0'), + // KeyCode::LeftControl => StrOrChar::Char('0'), + // KeyCode::LeftMeta => StrOrChar::Char('0'), + // KeyCode::LeftAlt => StrOrChar::Char('0'), + KeyCode::Space => StrOrChar::Char(' '), + // KeyCode::RightAlt => StrOrChar::Char('0'), + // KeyCode::RightControl => StrOrChar::Char('0'), + /* + KeyCode::ArrowUp + KeyCode::ArrowLeft + KeyCode::ArrowRight + KeyCode::ArrowDown + */ + _ => { + log::warn!("Warning: unknown keycode str representation {:?}", self); + StrOrChar::Char('?') + } + } + } +} + +impl From for u32 { + #[allow(non_upper_case_globals)] + fn from(src: KeyCode) -> u32 { + match src { + KeyCode::Escape => Escape, + KeyCode::Backtick => grave, + KeyCode::Key0 => _0, + KeyCode::Key1 => _1, + KeyCode::Key2 => _2, + KeyCode::Key3 => _3, + KeyCode::Key4 => _4, + KeyCode::Key5 => _5, + KeyCode::Key6 => _6, + KeyCode::Key7 => _7, + KeyCode::Key8 => _8, + KeyCode::Key9 => _9, + KeyCode::Minus => minus, + KeyCode::Equals => equal, + KeyCode::Backspace => BackSpace, + + KeyCode::Tab => Tab, + KeyCode::KeyQ => q | Q, + KeyCode::KeyW => w | W, + KeyCode::KeyE => e | E, + KeyCode::KeyR => r | R, + KeyCode::KeyT => t | T, + KeyCode::KeyY => y | Y, + KeyCode::KeyU => u | U, + KeyCode::KeyI => i | I, + KeyCode::KeyO => o | O, + KeyCode::KeyP => p | P, + KeyCode::LeftBracket => bracketleft, + KeyCode::RightBracket => bracketright, + KeyCode::Return => Return, + + KeyCode::KeyA => a | A, + KeyCode::KeyS => s | S, + KeyCode::KeyD => d | D, + KeyCode::KeyF => f | F, + KeyCode::KeyG => g | G, + KeyCode::KeyH => h | H, + KeyCode::KeyJ => j | J, + KeyCode::KeyK => k | K, + KeyCode::KeyL => l | L, + KeyCode::Semicolon => semicolon, + KeyCode::Quote => quoteright, + KeyCode::Backslash => backslash, + + KeyCode::KeyZ => z | Z, + KeyCode::KeyX => x | X, + KeyCode::KeyC => c | C, + KeyCode::KeyV => v | V, + KeyCode::KeyB => b | B, + KeyCode::KeyN => n | N, + KeyCode::KeyM => m | M, + KeyCode::Comma => comma, + KeyCode::Period => period, + KeyCode::Slash => slash, + + KeyCode::LeftControl => Control_L, + KeyCode::RightControl => Control_R, + KeyCode::LeftAlt => Alt_L, + KeyCode::RightAlt => Alt_R, + KeyCode::LeftShift => Shift_L, + KeyCode::RightShift => Shift_R, + KeyCode::LeftMeta => Super_L, + KeyCode::RightMeta => Super_R, + + KeyCode::Space => space, + KeyCode::CapsLock => Caps_Lock, + KeyCode::F1 => F1, + KeyCode::F2 => F2, + KeyCode::F3 => F3, + KeyCode::F4 => F4, + KeyCode::F5 => F5, + KeyCode::F6 => F6, + KeyCode::F7 => F7, + KeyCode::F8 => F8, + KeyCode::F9 => F9, + KeyCode::F10 => F10, + KeyCode::F11 => F11, + KeyCode::F12 => F12, + + KeyCode::PrintScreen => Print, + KeyCode::ScrollLock => Scroll_Lock, + // Pause/Break not audio. + KeyCode::Pause => Pause, + + KeyCode::Insert => Insert, + KeyCode::Delete => Delete, + KeyCode::Home => Home, + KeyCode::End => End, + KeyCode::PageUp => Page_Up, + KeyCode::PageDown => Page_Down, + + KeyCode::Numpad0 => KP_0, + KeyCode::Numpad1 => KP_1, + KeyCode::Numpad2 => KP_2, + KeyCode::Numpad3 => KP_3, + KeyCode::Numpad4 => KP_4, + KeyCode::Numpad5 => KP_5, + KeyCode::Numpad6 => KP_6, + KeyCode::Numpad7 => KP_7, + KeyCode::Numpad8 => KP_8, + KeyCode::Numpad9 => KP_9, + + KeyCode::NumpadEquals => KP_Equal, + KeyCode::NumpadSubtract => KP_Subtract, + KeyCode::NumpadAdd => KP_Add, + KeyCode::NumpadDecimal => KP_Decimal, + KeyCode::NumpadMultiply => KP_Multiply, + KeyCode::NumpadDivide => KP_Divide, + KeyCode::NumLock => Num_Lock, + KeyCode::NumpadEnter => KP_Enter, + + KeyCode::ArrowUp => Up, + KeyCode::ArrowDown => Down, + KeyCode::ArrowLeft => Left, + KeyCode::ArrowRight => Right, + + // We don't know what this keycode is, so just return it directly. + KeyCode::Unknown(unknown_keycode) => unknown_keycode, + } + } +} diff --git a/druid-shell/src/platform/x11/menu.rs b/druid-shell/src/platform/x11/menu.rs new file mode 100644 index 0000000000..5db0b0bf49 --- /dev/null +++ b/druid-shell/src/platform/x11/menu.rs @@ -0,0 +1,55 @@ +// Copyright 2020 The xi-editor 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. + +//! X11 menus implementation. + +use crate::hotkey::HotKey; + +pub struct Menu; + +impl Menu { + pub fn new() -> Menu { + // TODO(x11/menus): implement Menu::new (currently a no-op) + log::warn!("Menu::new is currently unimplemented for X11 platforms."); + Menu {} + } + + pub fn new_for_popup() -> Menu { + // TODO(x11/menus): implement Menu::new_for_popup (currently a no-op) + log::warn!("Menu::new_for_popup is currently unimplemented for X11 platforms."); + Menu {} + } + + pub fn add_dropdown(&mut self, mut _menu: Menu, _text: &str, _enabled: bool) { + // TODO(x11/menus): implement Menu::add_dropdown (currently a no-op) + log::warn!("Menu::add_dropdown is currently unimplemented for X11 platforms."); + } + + pub fn add_item( + &mut self, + _id: u32, + _text: &str, + _key: Option<&HotKey>, + _enabled: bool, + _selected: bool, + ) { + // TODO(x11/menus): implement Menu::add_item (currently a no-op) + log::warn!("Menu::add_item is currently unimplemented for X11 platforms."); + } + + pub fn add_separator(&mut self) { + // TODO(x11/menus): implement Menu::add_separator (currently a no-op) + log::warn!("Menu::add_separator is currently unimplemented for X11 platforms."); + } +} diff --git a/druid-shell/src/platform/x11/mod.rs b/druid-shell/src/platform/x11/mod.rs new file mode 100644 index 0000000000..0653918b1a --- /dev/null +++ b/druid-shell/src/platform/x11/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2020 The xi-editor 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. + +//! X11 implementation of druid-shell. + +// TODO(x11/render_improvements): screen is currently flashing when resizing in perftest. +// Might be related to the "sleep scheduler" in XWindow::render()? +// TODO(x11/render_improvements): double-buffering / present strategies / etc? + +pub mod application; +pub mod clipboard; +pub mod error; +pub mod keycodes; +pub mod menu; +pub mod window; + +mod util; diff --git a/druid-shell/src/platform/x11/util.rs b/druid-shell/src/platform/x11/util.rs new file mode 100644 index 0000000000..f0fc169d68 --- /dev/null +++ b/druid-shell/src/platform/x11/util.rs @@ -0,0 +1,50 @@ +// Copyright 2020 The xi-editor 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. + +//! Miscellaneous utility functions for working with X11. + +use xcb::Window; + +// See: https://github.com/rtbo/rust-xcb/blob/master/examples/randr_screen_modes.rs +pub fn refresh_rate(conn: &xcb::Connection, window_id: Window) -> Option { + let cookie = xcb::randr::get_screen_resources(conn, window_id); + let reply = cookie.get_reply().unwrap(); + let mut modes = reply.modes(); + + // TODO(x11/render_improvements): Figure out a more correct way of getting the screen's refresh rate. + // Or maybe we don't even need this function if I figure out a better way to schedule redraws? + // Assuming the first mode is the one we want to use. This is probably a bug on some setups. + // Any better way to find the correct one? + let refresh_rate = modes.next().and_then(|mode_info| { + let flags = mode_info.mode_flags(); + let vtotal = { + let mut val = mode_info.vtotal(); + if (flags & xcb::randr::MODE_FLAG_DOUBLE_SCAN) != 0 { + val *= 2; + } + if (flags & xcb::randr::MODE_FLAG_INTERLACE) != 0 { + val /= 2; + } + val + }; + + if vtotal != 0 && mode_info.htotal() != 0 { + Some((mode_info.dot_clock() as f64) / (vtotal as f64 * mode_info.htotal() as f64)) + } else { + None + } + })?; + + Some(refresh_rate) +} diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs new file mode 100644 index 0000000000..38fe8a4df1 --- /dev/null +++ b/druid-shell/src/platform/x11/window.rs @@ -0,0 +1,445 @@ +// Copyright 2020 The xi-editor 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. + +//! X11 window creation and window management. + +use std::any::Any; +use std::convert::TryInto; + +use xcb::ffi::XCB_COPY_FROM_PARENT; + +use crate::dialog::{FileDialogOptions, FileInfo}; +use crate::keyboard::{KeyEvent, KeyModifiers}; +use crate::keycodes::KeyCode; +use crate::kurbo::{Point, Size}; +use crate::mouse::{Cursor, MouseEvent}; +use crate::piet::{Piet, RenderContext}; +use crate::window::{IdleToken, Text, TimerToken, WinHandler}; + +use super::application::Application; +use super::error::Error; +use super::menu::Menu; +use super::util; + +pub struct WindowBuilder { + handler: Option>, + title: String, + size: Size, + min_size: Size, +} + +impl WindowBuilder { + pub fn new() -> WindowBuilder { + WindowBuilder { + handler: None, + title: String::new(), + size: Size::new(500.0, 400.0), + min_size: Size::new(0.0, 0.0), + } + } + + pub fn set_handler(&mut self, handler: Box) { + self.handler = Some(handler); + } + + pub fn set_size(&mut self, size: Size) { + self.size = size; + } + + pub fn set_min_size(&mut self, min_size: Size) { + log::warn!("WindowBuilder::set_min_size is implemented, but the setting is currently unused for X11 platforms."); + self.min_size = min_size; + } + + pub fn resizable(&mut self, _resizable: bool) { + log::warn!("WindowBuilder::resizable is currently unimplemented for X11 platforms."); + } + + pub fn show_titlebar(&mut self, _show_titlebar: bool) { + log::warn!("WindowBuilder::show_titlebar is currently unimplemented for X11 platforms."); + } + + pub fn set_title>(&mut self, title: S) { + self.title = title.into(); + } + + pub fn set_menu(&mut self, _menu: Menu) { + // TODO(x11/menus): implement WindowBuilder::set_menu (currently a no-op) + } + + // TODO(x11/menus): make menus if requested + pub fn build(self) -> Result { + let conn = Application::get_connection(); + let screen_num = Application::get_screen_num(); + let window_id = conn.generate_id(); + let setup = conn.get_setup(); + // TODO(x11/errors): Don't unwrap for screen or visual_type? + let screen = setup.roots().nth(screen_num as usize).unwrap(); + let visual_type = get_visual_from_screen(&screen).unwrap(); + let visual_id = visual_type.visual_id(); + + let cw_values = [ + (xcb::CW_BACK_PIXEL, screen.white_pixel()), + ( + xcb::CW_EVENT_MASK, + xcb::EVENT_MASK_EXPOSURE + | xcb::EVENT_MASK_KEY_PRESS + | xcb::EVENT_MASK_KEY_RELEASE + | xcb::EVENT_MASK_BUTTON_PRESS + | xcb::EVENT_MASK_BUTTON_RELEASE + | xcb::EVENT_MASK_POINTER_MOTION, + ), + ]; + + // Create the actual window + xcb::create_window( + // Connection to the X server + &conn, + // Window depth + XCB_COPY_FROM_PARENT.try_into().unwrap(), + // The new window's ID + window_id, + // Parent window of this new window + // TODO(#468): either `screen.root()` (no parent window) or pass parent here to attach + screen.root(), + // X-coordinate of the new window + 0, + // Y-coordinate of the new window + 0, + // Width of the new window + // TODO(x11/dpi_scaling): figure out DPI scaling + self.size.width as u16, + // Height of the new window + // TODO(x11/dpi_scaling): figure out DPI scaling + self.size.height as u16, + // Border width + 0, + // Window class type + xcb::WINDOW_CLASS_INPUT_OUTPUT as u16, + // Visual ID + visual_id, + // Window properties mask + &cw_values, + ); + + // Set window title + xcb::change_property( + &conn, + xcb::PROP_MODE_REPLACE as u8, + window_id, + xcb::ATOM_WM_NAME, + xcb::ATOM_STRING, + 8, + self.title.as_bytes(), + ); + + conn.flush(); + + let mut handler = self.handler.unwrap(); + let handle = WindowHandle::new(window_id); + handler.connect(&(handle.clone()).into()); + + let xwindow = XWindow::new(window_id, handler, self.size); + Application::add_xwindow(window_id, xwindow); + + Ok(handle) + } +} + +// X11-specific event handling and window drawing (etc.) +pub(crate) struct XWindow { + window_id: u32, + handler: Box, + refresh_rate: Option, +} + +impl XWindow { + pub fn new(window_id: u32, window_handler: Box, size: Size) -> XWindow { + let conn = Application::get_connection(); + + // Figure out the refresh rate of the current screen + let refresh_rate = util::refresh_rate(&conn, window_id); + let mut xwindow = XWindow { + window_id, + handler: window_handler, + refresh_rate, + }; + // Let the window handler know the size of the window + xwindow.communicate_size(size); + + xwindow + } + + pub fn render(&mut self) { + let conn = Application::get_connection(); + let setup = conn.get_setup(); + let screen_num = Application::get_screen_num(); + // TODO(x11/errors): Don't unwrap for screen or visual_type? + let screen = setup.roots().nth(screen_num as usize).unwrap(); + let mut visual_type = get_visual_from_screen(&screen).unwrap(); + + // Figure out the window's current size + let geometry_cookie = xcb::get_geometry(&conn, self.window_id); + let reply = geometry_cookie.get_reply().unwrap(); + let size = Size::new(reply.width() as f64, reply.height() as f64); + self.communicate_size(size); + + // Create a draw surface + // TODO(x11/render_improvements): We have to re-create this draw surface if the window size changes. + let cairo_xcb_connection = unsafe { + cairo::XCBConnection::from_raw_none( + conn.get_raw_conn() as *mut cairo_sys::xcb_connection_t + ) + }; + let cairo_drawable = cairo::XCBDrawable(self.window_id); + let cairo_visual_type = unsafe { + cairo::XCBVisualType::from_raw_none( + &mut visual_type.base as *mut _ as *mut cairo_sys::xcb_visualtype_t, + ) + }; + let cairo_surface = cairo::XCBSurface::create( + &cairo_xcb_connection, + &cairo_drawable, + &cairo_visual_type, + size.width as i32, + size.height as i32, + ) + .expect("couldn't create a cairo surface"); + let mut cairo_context = cairo::Context::new(&cairo_surface); + + cairo_context.set_source_rgb(0.0, 0.0, 0.0); + cairo_context.paint(); + let mut piet_ctx = Piet::new(&mut cairo_context); + let anim = self.handler.paint(&mut piet_ctx); + if let Err(e) = piet_ctx.finish() { + // TODO(x11/errors): hook up to error or something? + panic!("piet error on render: {:?}", e); + } + + if anim && self.refresh_rate.is_some() { + // TODO(x11/render_improvements): Sleeping is a terrible way to schedule redraws. + // I think I'll end up having to write a redraw scheduler or something. :| + // Doing it this way for now to proof-of-concept it. + let sleep_amount_ms = (1000.0 / self.refresh_rate.unwrap()) as u64; + std::thread::sleep(std::time::Duration::from_millis(sleep_amount_ms)); + + request_redraw(self.window_id); + } + conn.flush(); + } + + pub fn key_down(&mut self, key_code: KeyCode) { + self.handler.key_down(KeyEvent::new( + key_code, + false, + KeyModifiers { + /// Shift. + shift: false, + /// Option on macOS. + alt: false, + /// Control. + ctrl: false, + /// Meta / Windows / Command + meta: false, + }, + key_code, + key_code, + )); + } + + pub fn mouse_down(&mut self, mouse_event: &MouseEvent) { + self.handler.mouse_down(mouse_event); + } + + pub fn mouse_up(&mut self, mouse_event: &MouseEvent) { + self.handler.mouse_up(mouse_event); + } + + pub fn mouse_move(&mut self, mouse_event: &MouseEvent) { + self.handler.mouse_move(mouse_event); + } + + fn communicate_size(&mut self, size: Size) { + // TODO(x11/dpi_scaling): detect DPI and scale size + self.handler.size(size.width as u32, size.height as u32); + } +} + +// Apparently you have to get the visualtype this way :| +fn get_visual_from_screen(screen: &xcb::Screen<'_>) -> Option { + for depth in screen.allowed_depths() { + for visual in depth.visuals() { + if visual.visual_id() == screen.root_visual() { + return Some(visual); + } + } + } + None +} + +#[derive(Clone)] +pub struct IdleHandle; + +impl IdleHandle { + pub fn add_idle_callback(&self, _callback: F) + where + F: FnOnce(&dyn Any) + Send + 'static, + { + // TODO(x11/idle_handles): implement IdleHandle::add_idle_callback + log::warn!("IdleHandle::add_idle_callback is currently unimplemented for X11 platforms."); + } + + pub fn add_idle_token(&self, _token: IdleToken) { + // TODO(x11/idle_handles): implement IdleHandle::add_idle_token + log::warn!("IdleHandle::add_idle_token is currently unimplemented for X11 platforms."); + } +} + +#[derive(Clone, Default)] +pub struct WindowHandle { + window_id: u32, +} + +impl WindowHandle { + fn new(window_id: u32) -> WindowHandle { + WindowHandle { window_id } + } + + pub fn show(&self) { + let conn = Application::get_connection(); + xcb::map_window(conn.as_ref(), self.window_id); + conn.as_ref().flush(); + } + + pub fn close(&self) { + // Hopefully there aren't any references to this window after this function is called. + let conn = Application::get_connection(); + xcb::destroy_window(&conn, self.window_id); + } + + /// Set whether the window should be resizable + pub fn resizable(&self, _resizable: bool) { + log::warn!("WindowHandle::resizeable is currently unimplemented for X11 platforms."); + } + + /// Set whether the window should show titlebar + pub fn show_titlebar(&self, _show_titlebar: bool) { + log::warn!("WindowHandle::show_titlebar is currently unimplemented for X11 platforms."); + } + + /// Bring this window to the front of the window stack and give it focus. + pub fn bring_to_front_and_focus(&self) { + // TODO(x11/misc): Unsure if this does exactly what the doc comment says; need a test case. + let conn = Application::get_connection(); + xcb::configure_window( + &conn, + self.window_id, + &[(xcb::CONFIG_WINDOW_STACK_MODE as u16, xcb::STACK_MODE_ABOVE)], + ); + xcb::set_input_focus( + &conn, + xcb::INPUT_FOCUS_POINTER_ROOT as u8, + self.window_id, + xcb::CURRENT_TIME, + ); + } + + pub fn invalidate(&self) { + request_redraw(self.window_id); + } + + pub fn set_title(&self, title: &str) { + let conn = Application::get_connection(); + xcb::change_property( + &conn, + xcb::PROP_MODE_REPLACE as u8, + self.window_id, + xcb::ATOM_WM_NAME, + xcb::ATOM_STRING, + 8, + title.as_bytes(), + ); + } + + pub fn set_menu(&self, _menu: Menu) { + // TODO(x11/menus): implement WindowHandle::set_menu (currently a no-op) + } + + pub fn text(&self) -> Text { + // I'm not entirely sure what this method is doing here, so here's a Text. + Text::new() + } + + pub fn request_timer(&self, _deadline: std::time::Instant) -> TimerToken { + // TODO(x11/timers): implement WindowHandle::request_timer + // This one might be tricky, since there's not really any timers to hook into in X11. + // Might have to code up our own Timer struct, running in its own thread? + log::warn!("WindowHandle::resizeable is currently unimplemented for X11 platforms."); + TimerToken::INVALID + } + + pub fn set_cursor(&mut self, _cursor: &Cursor) { + // TODO(x11/cursors): implement WindowHandle::set_cursor + log::warn!("WindowHandle::set_cursor is currently unimplemented for X11 platforms."); + } + + pub fn open_file_sync(&mut self, _options: FileDialogOptions) -> Option { + // TODO(x11/file_dialogs): implement WindowHandle::open_file_sync + log::warn!("WindowHandle::open_file_sync is currently unimplemented for X11 platforms."); + None + } + + pub fn save_as_sync(&mut self, _options: FileDialogOptions) -> Option { + // TODO(x11/file_dialogs): implement WindowHandle::save_as_sync + log::warn!("WindowHandle::save_as_sync is currently unimplemented for X11 platforms."); + None + } + + pub fn show_context_menu(&self, _menu: Menu, _pos: Point) { + // TODO(x11/menus): implement WindowHandle::show_context_menu + log::warn!("WindowHandle::show_context_menu is currently unimplemented for X11 platforms."); + } + + pub fn get_idle_handle(&self) -> Option { + // TODO(x11/idle_handles): implement WindowHandle::get_idle_handle + //log::warn!("WindowHandle::get_idle_handle is currently unimplemented for X11 platforms."); + Some(IdleHandle) + } + + pub fn get_dpi(&self) -> f32 { + // TODO(x11/dpi_scaling): figure out DPI scaling + log::warn!("WindowHandle::get_dpi is currently unimplemented for X11 platforms."); + 96.0 + } +} + +fn request_redraw(window_id: u32) { + let conn = Application::get_connection(); + + // TODO(x11/render_improvements): Set x, y, width, and height correctly. + // We redraw the entire surface on an ExposeEvent, so these args currently do nothing. + // See: http://rtbo.github.io/rust-xcb/xcb/ffi/xproto/struct.xcb_expose_event_t.html + let expose_event = xcb::ExposeEvent::new( + window_id, /*x=*/ 0, /*y=*/ 0, /*width=*/ 0, /*height=*/ 0, + /*count=*/ 0, + ); + xcb::send_event( + &conn, + false, + window_id, + xcb::EVENT_MASK_EXPOSURE, + &expose_event, + ); + conn.flush(); +} diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 2c084e76be..1423aad9b6 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -15,6 +15,7 @@ default-target = "x86_64-pc-windows-msvc" [features] use_gtk = ["druid-shell/use_gtk"] +x11 = ["druid-shell/x11"] svg = ["usvg"] [badges] diff --git a/druid/src/app.rs b/druid/src/app.rs index 7f6217959f..656cbbe238 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -130,7 +130,7 @@ impl AppLauncher { } impl WindowDesc { - /// Create a new `WindowDesc`, taking a funciton that will generate the root + /// Create a new `WindowDesc`, taking a function that will generate the root /// [`Widget`] for this window. /// /// It is possible that a `WindowDesc` can be reused to launch multiple windows.