From 2e5a72549282215379e99f58961bf5f5af3a591a Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Sun, 3 May 2020 14:14:33 +0300 Subject: [PATCH 1/4] Fix window closing in X11. --- druid-shell/src/platform/x11/application.rs | 74 ++++-- druid-shell/src/platform/x11/util.rs | 16 +- druid-shell/src/platform/x11/window.rs | 244 ++++++++++++-------- 3 files changed, 216 insertions(+), 118 deletions(-) diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs index b163defbbc..269a2b94a1 100644 --- a/druid-shell/src/platform/x11/application.rs +++ b/druid-shell/src/platform/x11/application.rs @@ -18,6 +18,12 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use xcb::{ + ButtonPressEvent, ButtonReleaseEvent, ClientMessageEvent, Connection, DestroyNotifyEvent, + ExposeEvent, KeyPressEvent, MotionNotifyEvent, BUTTON_PRESS, BUTTON_RELEASE, CLIENT_MESSAGE, + DESTROY_NOTIFY, EXPOSE, KEY_PRESS, MOTION_NOTIFY, +}; + use crate::application::AppHandler; use super::clipboard::Clipboard; @@ -26,18 +32,37 @@ use super::window::Window; #[derive(Clone)] pub(crate) struct Application { - connection: Rc, + /// The connection to the X server. + /// + /// This connection is associated with a single display. + /// The X server might also host other displays. + /// + /// A display is a collection of screens. + connection: Rc, + /// The default screen of the connected display. + /// + /// The connected display may also have additional screens. + /// Moving windows between multiple screens is difficult and there is no support for it. + /// The application would have to create a clone of its window on multiple screens + /// and then fake the visual transfer. + /// + /// In practice multiple physical monitor drawing areas are present on a single screen. + /// This is achieved via various X server extensions (XRandR/Xinerama/TwinView), + /// with XRandR seeming like the best choice. screen_num: i32, // Needs a container when no longer const + /// The mutable `Application` state. state: Rc>, } +/// The mutable `Application` state. struct State { + /// A collection of all the `Application` windows. windows: HashMap>, } impl Application { pub fn new() -> Result { - let (conn, screen_num) = match xcb::Connection::connect_with_xlib_display() { + let (conn, screen_num) = match Connection::connect_with_xlib_display() { Ok(conn) => conn, Err(err) => return Err(Error::ConnectionError(err.to_string())), }; @@ -77,7 +102,7 @@ impl Application { } #[inline] - pub(crate) fn connection(&self) -> &Rc { + pub(crate) fn connection(&self) -> &Rc { &self.connection } @@ -92,8 +117,8 @@ impl Application { if let Some(ev) = self.connection.wait_for_event() { let ev_type = ev.response_type() & !0x80; match ev_type { - xcb::EXPOSE => { - let expose: &xcb::ExposeEvent = unsafe { xcb::cast_event(&ev) }; + EXPOSE => { + let expose = unsafe { xcb::cast_event::(&ev) }; let window_id = expose.window(); if let Some(w) = self.window(window_id) { w.handle_expose(expose); @@ -101,8 +126,8 @@ impl Application { log::warn!("EXPOSE - failed to get window"); } } - xcb::KEY_PRESS => { - let key_press: &xcb::KeyPressEvent = unsafe { xcb::cast_event(&ev) }; + KEY_PRESS => { + let key_press = unsafe { xcb::cast_event::(&ev) }; let window_id = key_press.event(); if let Some(w) = self.window(window_id) { w.handle_key_press(key_press); @@ -110,8 +135,8 @@ impl Application { log::warn!("KEY_PRESS - failed to get window"); } } - xcb::BUTTON_PRESS => { - let button_press: &xcb::ButtonPressEvent = unsafe { xcb::cast_event(&ev) }; + BUTTON_PRESS => { + let button_press = unsafe { xcb::cast_event::(&ev) }; let window_id = button_press.event(); if let Some(w) = self.window(window_id) { w.handle_button_press(button_press); @@ -119,9 +144,8 @@ impl Application { log::warn!("BUTTON_PRESS - failed to get window"); } } - xcb::BUTTON_RELEASE => { - let button_release: &xcb::ButtonReleaseEvent = - unsafe { xcb::cast_event(&ev) }; + BUTTON_RELEASE => { + let button_release = unsafe { xcb::cast_event::(&ev) }; let window_id = button_release.event(); if let Some(w) = self.window(window_id) { w.handle_button_release(button_release); @@ -129,9 +153,8 @@ impl Application { log::warn!("BUTTON_RELEASE - failed to get window"); } } - xcb::MOTION_NOTIFY => { - let motion_notify: &xcb::MotionNotifyEvent = - unsafe { xcb::cast_event(&ev) }; + MOTION_NOTIFY => { + let motion_notify = unsafe { xcb::cast_event::(&ev) }; let window_id = motion_notify.event(); if let Some(w) = self.window(window_id) { w.handle_motion_notify(motion_notify); @@ -139,15 +162,24 @@ impl Application { log::warn!("MOTION_NOTIFY - failed to get window"); } } - xcb::DESTROY_NOTIFY => { - let destroy_notify: &xcb::DestroyNotifyEvent = - unsafe { xcb::cast_event(&ev) }; + CLIENT_MESSAGE => { + let client_message = unsafe { xcb::cast_event::(&ev) }; + let window_id = client_message.window(); + if let Some(w) = self.window(window_id) { + w.handle_client_message(client_message); + } else { + log::warn!("CLIENT_MESSAGE - failed to get window"); + } + } + DESTROY_NOTIFY => { + let destroy_notify = unsafe { xcb::cast_event::(&ev) }; let window_id = destroy_notify.window(); if let Some(w) = self.window(window_id) { w.handle_destroy_notify(destroy_notify); } else { log::warn!("DESTROY_NOTIFY - failed to get window"); } + // Remove our reference to it and allow the Window to be dropped self.remove_window(window_id); } _ => {} @@ -158,6 +190,12 @@ impl Application { pub fn quit(&self) { // TODO(x11/quit): implement Application::quit + // + // We should loop over all the windows and destroy them to execute all the destroy handlers. + // + // Then we should send a custom client message event which would break out of the run loop. + // Events seem to be only associated with windows so we will probably need + // a message-only window that will act as the application level event driver. } pub fn clipboard(&self) -> Clipboard { diff --git a/druid-shell/src/platform/x11/util.rs b/druid-shell/src/platform/x11/util.rs index f0fc169d68..f35441c6b4 100644 --- a/druid-shell/src/platform/x11/util.rs +++ b/druid-shell/src/platform/x11/util.rs @@ -14,10 +14,10 @@ //! Miscellaneous utility functions for working with X11. -use xcb::Window; +use xcb::{Connection, Screen, Visualtype, 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 { +pub fn refresh_rate(conn: &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(); @@ -48,3 +48,15 @@ pub fn refresh_rate(conn: &xcb::Connection, window_id: Window) -> Option { Some(refresh_rate) } + +// Apparently you have to get the visualtype this way :| +pub fn get_visual_from_screen(screen: &Screen<'_>) -> Option { + for depth in screen.allowed_depths() { + for visual in depth.visuals() { + if visual.visual_id() == screen.root_visual() { + return Some(visual); + } + } + } + None +} diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index e8e0e410ad..1cec36a461 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -20,7 +20,13 @@ use std::convert::TryInto; use std::rc::{Rc, Weak}; use cairo::XCBSurface; -use xcb::ffi::XCB_COPY_FROM_PARENT; +use xcb::{ + Atom, Visualtype, ATOM_ATOM, ATOM_STRING, ATOM_WM_NAME, CONFIG_WINDOW_STACK_MODE, + COPY_FROM_PARENT, CURRENT_TIME, CW_BACK_PIXEL, CW_EVENT_MASK, EVENT_MASK_BUTTON_PRESS, + EVENT_MASK_BUTTON_RELEASE, EVENT_MASK_EXPOSURE, EVENT_MASK_KEY_PRESS, EVENT_MASK_KEY_RELEASE, + EVENT_MASK_POINTER_MOTION, EVENT_MASK_STRUCTURE_NOTIFY, INPUT_FOCUS_POINTER_ROOT, + PROP_MODE_REPLACE, STACK_MODE_ABOVE, WINDOW_CLASS_INPUT_OUTPUT, +}; use crate::dialog::{FileDialogOptions, FileInfo}; use crate::keyboard::{KeyEvent, KeyModifiers}; @@ -83,28 +89,99 @@ impl WindowBuilder { // TODO(x11/menus): implement WindowBuilder::set_menu (currently a no-op) } + fn atoms(&self, id: u32) -> Result { + let conn = self.app.connection(); + + let wm_protocols = match xcb::intern_atom(conn, false, "WM_PROTOCOLS").get_reply() { + Ok(reply) => reply.atom(), + Err(err) => { + return Err(Error::Generic(format!( + "failed to get WM_PROTOCOLS: {}", + err + ))) + } + }; + + let wm_delete_window = match xcb::intern_atom(conn, false, "WM_DELETE_WINDOW").get_reply() { + Ok(reply) => reply.atom(), + Err(err) => { + return Err(Error::Generic(format!( + "failed to get WM_DELETE_WINDOW: {}", + err + ))) + } + }; + + let protocols = [wm_delete_window]; + // TODO(x11/errors): Check the response for errors? + xcb::change_property( + conn, + PROP_MODE_REPLACE as u8, + id, + wm_protocols, + ATOM_ATOM, + 32, + &protocols, + ); + + Ok(WindowAtoms { + wm_protocols, + wm_delete_window, + }) + } + + fn cairo_context( + &self, + id: u32, + visual_type: &mut Visualtype, + ) -> Result, Error> { + // Create a draw surface + let conn = self.app.connection(); + 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(id); + let cairo_visual_type = unsafe { + cairo::XCBVisualType::from_raw_none( + &mut visual_type.base as *mut _ as *mut cairo_sys::xcb_visualtype_t, + ) + }; + // TODO(x11/errors): Don't unwrap + let cairo_surface = XCBSurface::create( + &cairo_xcb_connection, + &cairo_drawable, + &cairo_visual_type, + self.size.width as i32, + self.size.height as i32, + ) + .expect("couldn't create a cairo surface"); + Ok(RefCell::new(cairo::Context::new(&cairo_surface))) + } + // TODO(x11/menus): make menus if requested pub fn build(self) -> Result { let conn = self.app.connection(); let screen_num = self.app.screen_num(); - let window_id = conn.generate_id(); + let 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 mut visual_type = util::get_visual_from_screen(&screen).unwrap(); let visual_id = visual_type.visual_id(); let cw_values = [ - (xcb::CW_BACK_PIXEL, screen.white_pixel()), + (CW_BACK_PIXEL, screen.white_pixel()), ( - xcb::CW_EVENT_MASK, - xcb::EVENT_MASK_EXPOSURE - | xcb::EVENT_MASK_STRUCTURE_NOTIFY - | 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, + CW_EVENT_MASK, + EVENT_MASK_EXPOSURE + | EVENT_MASK_STRUCTURE_NOTIFY + | EVENT_MASK_KEY_PRESS + | EVENT_MASK_KEY_RELEASE + | EVENT_MASK_BUTTON_PRESS + | EVENT_MASK_BUTTON_RELEASE + | EVENT_MASK_POINTER_MOTION, ), ]; @@ -113,9 +190,9 @@ impl WindowBuilder { // Connection to the X server conn, // Window depth - XCB_COPY_FROM_PARENT.try_into().unwrap(), + COPY_FROM_PARENT.try_into().unwrap(), // The new window's ID - window_id, + id, // Parent window of this new window // TODO(#468): either `screen.root()` (no parent window) or pass parent here to attach screen.root(), @@ -132,96 +209,64 @@ impl WindowBuilder { // Border width 0, // Window class type - xcb::WINDOW_CLASS_INPUT_OUTPUT as u16, + 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(), - ); + // TODO(x11/errors): Should do proper cleanup (window destruction etc) in case of error + let atoms = self.atoms(id)?; + let cairo_context = self.cairo_context(id, &mut visual_type)?; + // Figure out the refresh rate of the current screen + let refresh_rate = util::refresh_rate(conn, id); + let state = RefCell::new(WindowState { size: self.size }); + let handler = RefCell::new(self.handler.unwrap()); - conn.flush(); + let window = Rc::new(Window { + id, + app: self.app.clone(), + handler, + cairo_context, + refresh_rate, + atoms, + state, + }); + window.set_title(&self.title); - let handler = self.handler.unwrap(); - let window = Rc::new(Window::new(window_id, self.app.clone(), handler, self.size)); - let handle = WindowHandle::new(window_id, Rc::downgrade(&window)); + let handle = WindowHandle::new(id, Rc::downgrade(&window)); window.connect(handle.clone()); - self.app.add_window(window_id, window); + self.app.add_window(id, window); Ok(handle) } } -// X11-specific event handling and window drawing (etc.) +/// An X11 window. pub(crate) struct Window { id: u32, app: Application, handler: RefCell>, cairo_context: RefCell, refresh_rate: Option, + atoms: WindowAtoms, state: RefCell, } +/// All the Atom references the window needs. +struct WindowAtoms { + wm_protocols: Atom, + wm_delete_window: Atom, +} + /// The mutable state of the window. struct WindowState { size: Size, } impl Window { - fn new(id: u32, app: Application, handler: Box, size: Size) -> Window { - // Create a draw surface - let setup = app.connection().get_setup(); - let screen_num = app.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(); - let cairo_xcb_connection = unsafe { - cairo::XCBConnection::from_raw_none( - app.connection().get_raw_conn() as *mut cairo_sys::xcb_connection_t - ) - }; - let cairo_drawable = cairo::XCBDrawable(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 = 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 cairo_context = RefCell::new(cairo::Context::new(&cairo_surface)); - - // Figure out the refresh rate of the current screen - let refresh_rate = util::refresh_rate(app.connection(), id); - - let handler = RefCell::new(handler); - let state = RefCell::new(WindowState { size }); - Window { - id, - app, - handler, - cairo_context, - refresh_rate, - state, - } - } - fn connect(&self, handle: WindowHandle) { let size = self.state.borrow().size; if let Ok(mut handler) = self.handler.try_borrow_mut() { @@ -275,7 +320,7 @@ impl Window { } } - fn request_redraw(&self, rect: Rect) { + fn request_redraw(&self, rect: Rect, flush: bool) { // See: http://rtbo.github.io/rust-xcb/xcb/ffi/xproto/struct.xcb_expose_event_t.html let expose_event = xcb::ExposeEvent::new( self.id, @@ -289,10 +334,12 @@ impl Window { self.app.connection(), false, self.id, - xcb::EVENT_MASK_EXPOSURE, + EVENT_MASK_EXPOSURE, &expose_event, ); - self.app.connection().flush(); + if flush { + self.app.connection().flush(); + } } fn render(&self, invalid_rect: Rect) { @@ -330,7 +377,7 @@ impl Window { let sleep_amount_ms = (1000.0 / self.refresh_rate.unwrap()) as u64; std::thread::sleep(std::time::Duration::from_millis(sleep_amount_ms)); - self.request_redraw(size.to_rect()); + self.request_redraw(size.to_rect(), false); } self.app.connection().flush(); } @@ -342,6 +389,7 @@ impl Window { fn close(&self) { xcb::destroy_window(self.app.connection(), self.id); + self.app.connection().flush(); } /// Set whether the window should be resizable @@ -360,14 +408,15 @@ impl Window { xcb::configure_window( self.app.connection(), self.id, - &[(xcb::CONFIG_WINDOW_STACK_MODE as u16, xcb::STACK_MODE_ABOVE)], + &[(CONFIG_WINDOW_STACK_MODE as u16, STACK_MODE_ABOVE)], ); xcb::set_input_focus( self.app.connection(), - xcb::INPUT_FOCUS_POINTER_ROOT as u8, + INPUT_FOCUS_POINTER_ROOT as u8, self.id, - xcb::CURRENT_TIME, + CURRENT_TIME, ); + self.app.connection().flush(); } fn invalidate(&self) { @@ -378,24 +427,25 @@ impl Window { log::warn!("Window::invalidate - state already borrowed"); } if let Some(size) = size { - self.request_redraw(size.to_rect()); + self.request_redraw(size.to_rect(), true); } } fn invalidate_rect(&self, rect: Rect) { - self.request_redraw(rect); + self.request_redraw(rect, true); } fn set_title(&self, title: &str) { xcb::change_property( self.app.connection(), - xcb::PROP_MODE_REPLACE as u8, + PROP_MODE_REPLACE as u8, self.id, - xcb::ATOM_WM_NAME, - xcb::ATOM_STRING, + ATOM_WM_NAME, + ATOM_STRING, 8, title.as_bytes(), ); + self.app.connection().flush(); } fn set_menu(&self, _menu: Menu) { @@ -512,6 +562,16 @@ impl Window { } } + pub fn handle_client_message(&self, client_message: &xcb::ClientMessageEvent) { + // https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#id2745388 + if client_message.type_() == self.atoms.wm_protocols && client_message.format() == 32 { + let protocol = client_message.data().data32()[0]; + if protocol == self.atoms.wm_delete_window { + self.close(); + } + } + } + pub fn handle_destroy_notify(&self, _destroy_notify: &xcb::DestroyNotifyEvent) { if let Ok(mut handler) = self.handler.try_borrow_mut() { handler.destroy(); @@ -521,18 +581,6 @@ impl Window { } } -// 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(crate) struct IdleHandle; From 489631083f1cd6b062b6f11cde5fbf4de8b5234b Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Sun, 3 May 2020 14:17:36 +0300 Subject: [PATCH 2/4] Add changelog entry. --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff155e543..56c7a5b020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa ### Fixed - GTK: Use the system locale. ([#798] by [@finnerale]) -- GTK: Actually close windows ([#797] by [@finnerale]) +- GTK: Actually close windows. ([#797] by [@finnerale]) - Windows: Respect the minimum window size. ([#727] by [@teddemunnik]) - Windows: Respect resizability. ([#712] by [@teddemunnik]) - `Event::HotChanged(false)` will be emitted when the cursor leaves the window. ([#821] by [@teddemunnik]) @@ -68,7 +68,8 @@ While some features like the clipboard, menus or file dialogs are not yet availa - Windows: Termiate app when all windows have closed. ([#763] by [@xStrom]) - macOS: `Application::quit` now quits the run loop instead of killing the process. ([#763] by [@xStrom]) - macOS/GTK/web: `MouseButton::X1` and `MouseButton::X2` clicks are now recognized. ([#843] by [@xStrom]) -- GTK: Support disabled menu items ([#897] by [@jneem]) +- GTK: Support disabled menu items. ([#897] by [@jneem]) +- X11: Support individual window closing. ([#900] by [@xStrom]) ### Visual @@ -144,6 +145,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa [#894]: https://github.com/xi-editor/druid/pull/894 [#897]: https://github.com/xi-editor/druid/pull/897 [#898]: https://github.com/xi-editor/druid/pull/898 +[#900]: https://github.com/xi-editor/druid/pull/900 ## [0.5.0] - 2020-04-01 From c70831a95884a1a66d59507d32b75602dd11d0f9 Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Sun, 3 May 2020 19:21:56 +0300 Subject: [PATCH 3/4] Add support for Application::quit in X11. --- CHANGELOG.md | 1 + druid-shell/src/platform/x11/application.rs | 259 +++++++++++++++----- druid-shell/src/platform/x11/error.rs | 2 +- druid-shell/src/platform/x11/window.rs | 237 +++++++++++------- druid/examples/multiwin.rs | 8 +- druid/src/win_handler.rs | 2 +- 6 files changed, 366 insertions(+), 143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c7a5b020..a2dd6a230e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa - macOS/GTK/web: `MouseButton::X1` and `MouseButton::X2` clicks are now recognized. ([#843] by [@xStrom]) - GTK: Support disabled menu items. ([#897] by [@jneem]) - X11: Support individual window closing. ([#900] by [@xStrom]) +- X11: Support `Application::quit`. ([#900] by [@xStrom]) ### Visual diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs index 269a2b94a1..5cb1055e67 100644 --- a/druid-shell/src/platform/x11/application.rs +++ b/druid-shell/src/platform/x11/application.rs @@ -16,12 +16,14 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::convert::TryInto; use std::rc::Rc; use xcb::{ ButtonPressEvent, ButtonReleaseEvent, ClientMessageEvent, Connection, DestroyNotifyEvent, ExposeEvent, KeyPressEvent, MotionNotifyEvent, BUTTON_PRESS, BUTTON_RELEASE, CLIENT_MESSAGE, - DESTROY_NOTIFY, EXPOSE, KEY_PRESS, MOTION_NOTIFY, + COPY_FROM_PARENT, CW_EVENT_MASK, DESTROY_NOTIFY, EVENT_MASK_STRUCTURE_NOTIFY, EXPOSE, + KEY_PRESS, MOTION_NOTIFY, WINDOW_CLASS_INPUT_ONLY, }; use crate::application::AppHandler; @@ -50,12 +52,22 @@ pub(crate) struct Application { /// This is achieved via various X server extensions (XRandR/Xinerama/TwinView), /// with XRandR seeming like the best choice. screen_num: i32, // Needs a container when no longer const + /// The X11 window id of this `Application`. + /// + /// This is an input-only non-visual X11 window that is created first during initialization, + /// and it is destroyed last during `Application::finalize_quit`. + /// This window is useful for receiving application level events without any real windows. + /// + /// This is constant for the lifetime of the `Application`. + window_id: u32, /// The mutable `Application` state. state: Rc>, } /// The mutable `Application` state. struct State { + /// Whether `Application::quit` has already been called. + quitting: bool, /// A collection of all the `Application` windows. windows: HashMap>, } @@ -66,38 +78,98 @@ impl Application { Ok(conn) => conn, Err(err) => return Err(Error::ConnectionError(err.to_string())), }; + let connection = Rc::new(conn); + let window_id = Application::create_event_window(&connection, screen_num)?; let state = Rc::new(RefCell::new(State { + quitting: false, windows: HashMap::new(), })); Ok(Application { - connection: Rc::new(conn), + connection, screen_num, + window_id, state, }) } - pub(crate) fn add_window(&self, id: u32, window: Rc) { - if let Ok(mut state) = self.state.try_borrow_mut() { - state.windows.insert(id, window); - } else { - log::warn!("Application::add_window - state already borrowed"); + fn create_event_window(conn: &Rc, screen_num: i32) -> Result { + let id = conn.generate_id(); + let setup = conn.get_setup(); + // TODO(x11/errors): Don't unwrap for screen? + let screen = setup.roots().nth(screen_num as usize).unwrap(); + + let cw_values = [(CW_EVENT_MASK, EVENT_MASK_STRUCTURE_NOTIFY)]; + + // Create the actual window + // TODO(x11/errors): check that this actually succeeds? + xcb::create_window( + // Connection to the X server + conn, + // Window depth + COPY_FROM_PARENT.try_into().unwrap(), + // The new window's ID + id, + // Parent window of this new window + screen.root(), + // X-coordinate of the new window + 0, + // Y-coordinate of the new window + 0, + // Width of the new window + 1, + // Height of the new window + 1, + // Border width + 0, + // Window class type + WINDOW_CLASS_INPUT_ONLY as u16, + // Visual ID + COPY_FROM_PARENT.try_into().unwrap(), + // Window properties mask + &cw_values, + ); + + Ok(id) + } + + pub(crate) fn add_window(&self, id: u32, window: Rc) -> Result<(), Error> { + match self.state.try_borrow_mut() { + Ok(mut state) => { + state.windows.insert(id, window); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Application::add_window state: {}", + err + ))), } } - fn remove_window(&self, id: u32) { - if let Ok(mut state) = self.state.try_borrow_mut() { - state.windows.remove(&id); - } else { - log::warn!("Application::remove_window - state already borrowed"); + /// Remove the specified window from the `Application` and return the number of windows left. + fn remove_window(&self, id: u32) -> Result { + match self.state.try_borrow_mut() { + Ok(mut state) => { + state.windows.remove(&id); + Ok(state.windows.len()) + } + Err(err) => Err(Error::BorrowError(format!( + "Application::remove_window state: {}", + err + ))), } } - fn window(&self, id: u32) -> Option> { - if let Ok(state) = self.state.try_borrow() { - state.windows.get(&id).cloned() - } else { - log::warn!("Application::window - state already borrowed"); - None + fn window(&self, id: u32) -> Result, Error> { + match self.state.try_borrow() { + Ok(state) => state + .windows + .get(&id) + .cloned() + .ok_or_else(|| Error::Generic(format!("No window with id {}", id))), + Err(err) => Err(Error::BorrowError(format!( + "Application::window state: {}", + err + ))), } } @@ -112,75 +184,136 @@ impl Application { } // TODO(x11/events): handle mouse scroll events + #[allow(clippy::cognitive_complexity)] pub fn run(self, _handler: Option>) { loop { if let Some(ev) = self.connection.wait_for_event() { let ev_type = ev.response_type() & !0x80; + // NOTE: When adding handling for any of the following events, + // there must be a check against self.window_id + // to know if the event must be ignored. + // Otherwise there will be a "failed to get window" error. + // + // CIRCULATE_NOTIFY, CONFIGURE_NOTIFY, GRAVITY_NOTIFY + // MAP_NOTIFY, REPARENT_NOTIFY, UNMAP_NOTIFY match ev_type { EXPOSE => { let expose = unsafe { xcb::cast_event::(&ev) }; let window_id = expose.window(); - if let Some(w) = self.window(window_id) { - w.handle_expose(expose); - } else { - log::warn!("EXPOSE - failed to get window"); + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_expose(expose) { + log::error!("EXPOSE - failed to handle: {}", err); + } + } + Err(err) => log::error!("EXPOSE - failed to get window: {}", err), } } KEY_PRESS => { let key_press = unsafe { xcb::cast_event::(&ev) }; let window_id = key_press.event(); - if let Some(w) = self.window(window_id) { - w.handle_key_press(key_press); - } else { - log::warn!("KEY_PRESS - failed to get window"); + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_key_press(key_press) { + log::error!("KEY_PRESS - failed to handle: {}", err); + } + } + Err(err) => log::error!("KEY_PRESS - failed to get window: {}", err), } } BUTTON_PRESS => { let button_press = unsafe { xcb::cast_event::(&ev) }; let window_id = button_press.event(); - if let Some(w) = self.window(window_id) { - w.handle_button_press(button_press); - } else { - log::warn!("BUTTON_PRESS - failed to get window"); + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_button_press(button_press) { + log::error!("BUTTON_PRESS - failed to handle: {}", err); + } + } + Err(err) => log::error!("BUTTON_PRESS - failed to get window: {}", err), } } BUTTON_RELEASE => { let button_release = unsafe { xcb::cast_event::(&ev) }; let window_id = button_release.event(); - if let Some(w) = self.window(window_id) { - w.handle_button_release(button_release); - } else { - log::warn!("BUTTON_RELEASE - failed to get window"); + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_button_release(button_release) { + log::error!("BUTTON_RELEASE - failed to handle: {}", err); + } + } + Err(err) => { + log::error!("BUTTON_RELEASE - failed to get window: {}", err) + } } } MOTION_NOTIFY => { let motion_notify = unsafe { xcb::cast_event::(&ev) }; let window_id = motion_notify.event(); - if let Some(w) = self.window(window_id) { - w.handle_motion_notify(motion_notify); - } else { - log::warn!("MOTION_NOTIFY - failed to get window"); + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_motion_notify(motion_notify) { + log::error!("MOTION_NOTIFY - failed to handle: {}", err); + } + } + Err(err) => { + log::error!("MOTION_NOTIFY - failed to get window: {}", err) + } } } CLIENT_MESSAGE => { let client_message = unsafe { xcb::cast_event::(&ev) }; let window_id = client_message.window(); - if let Some(w) = self.window(window_id) { - w.handle_client_message(client_message); - } else { - log::warn!("CLIENT_MESSAGE - failed to get window"); + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_client_message(client_message) { + log::error!("CLIENT_MESSAGE - failed to handle: {}", err); + } + } + Err(err) => { + log::error!("CLIENT_MESSAGE - failed to get window: {}", err) + } } } DESTROY_NOTIFY => { let destroy_notify = unsafe { xcb::cast_event::(&ev) }; let window_id = destroy_notify.window(); - if let Some(w) = self.window(window_id) { - w.handle_destroy_notify(destroy_notify); - } else { - log::warn!("DESTROY_NOTIFY - failed to get window"); + if window_id == self.window_id { + // The destruction of the Application window means that + // we need to quit the run loop. + break; + } + match self.window(window_id) { + Ok(w) => { + if let Err(err) = w.handle_destroy_notify(destroy_notify) { + log::error!("DESTROY_NOTIFY - failed to handle: {}", err); + } + } + Err(err) => { + log::error!("DESTROY_NOTIFY - failed to get window: {}", err) + } + } + + // Remove our reference to the Window and allow it to be dropped + match self.remove_window(window_id) { + Ok(windows_left) => { + if windows_left == 0 { + // Check if we need to finalize a quit request + if let Ok(state) = self.state.try_borrow() { + if state.quitting { + self.finalize_quit(); + } + } else { + log::error!( + "DESTROY_NOTIFY - failed to check for quit request" + ); + } + } + } + Err(err) => { + log::error!("DESTROY_NOTIFY - failed to remove window: {}", err) + } } - // Remove our reference to it and allow the Window to be dropped - self.remove_window(window_id); } _ => {} } @@ -189,13 +322,29 @@ impl Application { } pub fn quit(&self) { - // TODO(x11/quit): implement Application::quit - // - // We should loop over all the windows and destroy them to execute all the destroy handlers. - // - // Then we should send a custom client message event which would break out of the run loop. - // Events seem to be only associated with windows so we will probably need - // a message-only window that will act as the application level event driver. + if let Ok(mut state) = self.state.try_borrow_mut() { + if !state.quitting { + state.quitting = true; + if state.windows.is_empty() { + // There are no windows left, so we can immediately finalize the quit. + self.finalize_quit(); + } else { + // We need to queue up the destruction of all our windows. + // Failure to do so will lead to resource leaks. + for window in state.windows.values() { + window.destroy(); + } + self.connection.flush(); + } + } + } else { + log::error!("Application state already borrowed"); + } + } + + fn finalize_quit(&self) { + xcb::destroy_window(&self.connection, self.window_id); + self.connection.flush(); } pub fn clipboard(&self) -> Clipboard { diff --git a/druid-shell/src/platform/x11/error.rs b/druid-shell/src/platform/x11/error.rs index 3b3d35597c..43b86a5ed8 100644 --- a/druid-shell/src/platform/x11/error.rs +++ b/druid-shell/src/platform/x11/error.rs @@ -31,7 +31,7 @@ impl fmt::Display for Error { match self { Error::Generic(msg) => write!(f, "Error: {}", msg), Error::ConnectionError(err) => write!(f, "Connection error: {}", err), - Error::BorrowError(msg) => write!(f, "Borrow error: {}", msg), + Error::BorrowError(msg) => write!(f, "Failed to borrow: {}", msg), } } } diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 1cec36a461..4f24cf35d3 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -186,6 +186,7 @@ impl WindowBuilder { ]; // Create the actual window + // TODO(x11/errors): check that this actually succeeds? xcb::create_window( // Connection to the X server conn, @@ -236,9 +237,9 @@ impl WindowBuilder { window.set_title(&self.title); let handle = WindowHandle::new(id, Rc::downgrade(&window)); - window.connect(handle.clone()); + window.connect(handle.clone())?; - self.app.add_window(id, window); + self.app.add_window(id, window)?; Ok(handle) } @@ -267,57 +268,90 @@ struct WindowState { } impl Window { - fn connect(&self, handle: WindowHandle) { - let size = self.state.borrow().size; - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.connect(&handle.into()); - handler.size(size.width as u32, size.height as u32); - } else { - log::warn!("Window::connect - handler already borrowed"); + fn connect(&self, handle: WindowHandle) -> Result<(), Error> { + match self.handler.try_borrow_mut() { + Ok(mut handler) => { + let size = self.size()?; + handler.connect(&handle.into()); + handler.size(size.width as u32, size.height as u32); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Window::connect handler: {}", + err + ))), } } + /// Start the destruction of the window. + /// + /// ### Connection + /// + /// Does not flush the connection. + pub fn destroy(&self) { + xcb::destroy_window(self.app.connection(), self.id); + } + fn cairo_surface(&self) -> Result { match self.cairo_context.try_borrow() { Ok(ctx) => match ctx.get_target().try_into() { Ok(surface) => Ok(surface), Err(err) => Err(Error::Generic(format!( - "try_into in Window::cairo_surface: {}", + "Window::cairo_surface ctx.try_into: {}", err ))), }, - Err(_) => Err(Error::BorrowError( - "cairo context in Window::cairo_surface".into(), - )), + Err(err) => Err(Error::BorrowError(format!( + "Window::cairo_surface cairo_context: {}", + err + ))), } } - fn set_size(&self, size: Size) { + fn size(&self) -> Result { + match self.state.try_borrow() { + Ok(state) => Ok(state.size), + Err(err) => Err(Error::BorrowError(format!("Window::size state: {}", err))), + } + } + + fn set_size(&self, size: Size) -> Result<(), Error> { // TODO(x11/dpi_scaling): detect DPI and scale size - let mut new_size = None; - if let Ok(mut s) = self.state.try_borrow_mut() { - if s.size != size { - s.size = size; - new_size = Some(size); + let new_size = match self.state.try_borrow_mut() { + Ok(mut state) => { + if state.size != size { + state.size = size; + Some(size) + } else { + None + } } - } else { - log::warn!("Window::set_size - state already borrowed"); - } + Err(err) => { + return Err(Error::BorrowError(format!( + "Window::set_size state: {}", + err + ))) + } + }; if let Some(size) = new_size { - match self.cairo_surface() { - Ok(surface) => { - if let Err(err) = surface.set_size(size.width as i32, size.height as i32) { - log::error!("Failed to update cairo surface size to {:?}: {}", size, err); - } - } - Err(err) => log::error!("Failed to get cairo surface: {}", err), + let cairo_surface = self.cairo_surface()?; + if let Err(err) = cairo_surface.set_size(size.width as i32, size.height as i32) { + return Err(Error::Generic(format!( + "Failed to update cairo surface size to {:?}: {}", + size, err + ))); } - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.size(size.width as u32, size.height as u32); - } else { - log::warn!("Window::set_size - handler already borrowed"); + match self.handler.try_borrow_mut() { + Ok(mut handler) => handler.size(size.width as u32, size.height as u32), + Err(err) => { + return Err(Error::BorrowError(format!( + "Window::set_size handler: {}", + err + ))) + } } } + Ok(()) } fn request_redraw(&self, rect: Rect, flush: bool) { @@ -342,12 +376,13 @@ impl Window { } } - fn render(&self, invalid_rect: Rect) { + fn render(&self, invalid_rect: Rect) -> Result<(), Error> { + // TODO(x11/errors): this function should return a proper error // Figure out the window's current size let geometry_cookie = xcb::get_geometry(self.app.connection(), self.id); let reply = geometry_cookie.get_reply().unwrap(); let size = Size::new(reply.width() as f64, reply.height() as f64); - self.set_size(size); + self.set_size(size)?; let mut anim = false; if let Ok(mut cairo_ctx) = self.cairo_context.try_borrow_mut() { @@ -356,14 +391,14 @@ impl Window { if let Ok(mut handler) = self.handler.try_borrow_mut() { anim = handler.paint(&mut piet_ctx, invalid_rect); } else { - log::warn!("Window::render - handler already borrowed"); + log::error!("Window::render - handler already borrowed"); } if let Err(e) = piet_ctx.finish() { log::error!("Window::render - piet finish failed: {}", e); } cairo_ctx.reset_clip(); } else { - log::warn!("Window::render - cairo context already borrowed"); + log::error!("Window::render - cairo context already borrowed"); } if anim && self.refresh_rate.is_some() { @@ -380,6 +415,7 @@ impl Window { self.request_redraw(size.to_rect(), false); } self.app.connection().flush(); + Ok(()) } fn show(&self) { @@ -388,7 +424,7 @@ impl Window { } fn close(&self) { - xcb::destroy_window(self.app.connection(), self.id); + self.destroy(); self.app.connection().flush(); } @@ -420,14 +456,9 @@ impl Window { } fn invalidate(&self) { - let mut size = None; - if let Ok(s) = self.state.try_borrow() { - size = Some(s.size); - } else { - log::warn!("Window::invalidate - state already borrowed"); - } - if let Some(size) = size { - self.request_redraw(size.to_rect(), true); + match self.size() { + Ok(size) => self.request_redraw(size.to_rect(), true), + Err(err) => log::error!("Window::invalidate - failed to get size: {}", err), } } @@ -457,17 +488,17 @@ impl Window { 96.0 } - pub fn handle_expose(&self, expose: &xcb::ExposeEvent) { + pub fn handle_expose(&self, expose: &xcb::ExposeEvent) -> Result<(), Error> { // TODO(x11/dpi_scaling): when dpi scaling is // implemented, it needs to be used here too let rect = Rect::from_origin_size( (expose.x() as f64, expose.y() as f64), (expose.width() as f64, expose.height() as f64), ); - self.render(rect); + self.render(rect) } - pub fn handle_key_press(&self, key_press: &xcb::KeyPressEvent) { + pub fn handle_key_press(&self, key_press: &xcb::KeyPressEvent) -> Result<(), Error> { let key: u32 = key_press.detail() as u32; let key_code: KeyCode = key.into(); let key_event = KeyEvent::new( @@ -486,14 +517,19 @@ impl Window { key_code, key_code, ); - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.key_down(key_event); - } else { - log::warn!("Window::handle_key_press - handler already borrowed"); + match self.handler.try_borrow_mut() { + Ok(mut handler) => { + handler.key_down(key_event); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Window::handle_key_press handle: {}", + err + ))), } } - pub fn handle_button_press(&self, button_press: &xcb::ButtonPressEvent) { + pub fn handle_button_press(&self, button_press: &xcb::ButtonPressEvent) -> Result<(), Error> { let mouse_event = MouseEvent { pos: Point::new(button_press.event_x() as f64, button_press.event_y() as f64), // TODO: Fill with held down buttons @@ -507,14 +543,22 @@ impl Window { count: 1, button: MouseButton::Left, }; - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.mouse_down(&mouse_event); - } else { - log::warn!("Window::handle_button_press - handler already borrowed"); + match self.handler.try_borrow_mut() { + Ok(mut handler) => { + handler.mouse_down(&mouse_event); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Window::handle_button_press handle: {}", + err + ))), } } - pub fn handle_button_release(&self, button_release: &xcb::ButtonReleaseEvent) { + pub fn handle_button_release( + &self, + button_release: &xcb::ButtonReleaseEvent, + ) -> Result<(), Error> { let mouse_event = MouseEvent { pos: Point::new( button_release.event_x() as f64, @@ -531,14 +575,22 @@ impl Window { count: 0, button: MouseButton::Left, }; - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.mouse_up(&mouse_event); - } else { - log::warn!("Window::handle_button_release - handler already borrowed"); + match self.handler.try_borrow_mut() { + Ok(mut handler) => { + handler.mouse_up(&mouse_event); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Window::handle_button_release handle: {}", + err + ))), } } - pub fn handle_motion_notify(&self, motion_notify: &xcb::MotionNotifyEvent) { + pub fn handle_motion_notify( + &self, + motion_notify: &xcb::MotionNotifyEvent, + ) -> Result<(), Error> { let mouse_event = MouseEvent { pos: Point::new( motion_notify.event_x() as f64, @@ -555,14 +607,22 @@ impl Window { count: 0, button: MouseButton::None, }; - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.mouse_move(&mouse_event); - } else { - log::warn!("Window::handle_motion_notify - handler already borrowed"); + match self.handler.try_borrow_mut() { + Ok(mut handler) => { + handler.mouse_move(&mouse_event); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Window::handle_motion_notify handle: {}", + err + ))), } } - pub fn handle_client_message(&self, client_message: &xcb::ClientMessageEvent) { + pub fn handle_client_message( + &self, + client_message: &xcb::ClientMessageEvent, + ) -> Result<(), Error> { // https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#id2745388 if client_message.type_() == self.atoms.wm_protocols && client_message.format() == 32 { let protocol = client_message.data().data32()[0]; @@ -570,13 +630,22 @@ impl Window { self.close(); } } + Ok(()) } - pub fn handle_destroy_notify(&self, _destroy_notify: &xcb::DestroyNotifyEvent) { - if let Ok(mut handler) = self.handler.try_borrow_mut() { - handler.destroy(); - } else { - log::warn!("Window::handle_destroy_notify - handler already borrowed"); + pub fn handle_destroy_notify( + &self, + _destroy_notify: &xcb::DestroyNotifyEvent, + ) -> Result<(), Error> { + match self.handler.try_borrow_mut() { + Ok(mut handler) => { + handler.destroy(); + Ok(()) + } + Err(err) => Err(Error::BorrowError(format!( + "Window::handle_destroy_notify handle: {}", + err + ))), } } } @@ -614,7 +683,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.show(); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -622,7 +691,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.close(); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -630,7 +699,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.resizable(resizable); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -638,7 +707,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.show_titlebar(show_titlebar); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -646,7 +715,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.bring_to_front_and_focus(); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -654,7 +723,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.invalidate(); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -662,7 +731,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.invalidate_rect(rect); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -670,7 +739,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.set_title(title); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -678,7 +747,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.set_menu(menu); } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); } } @@ -726,7 +795,7 @@ impl WindowHandle { if let Some(w) = self.window.upgrade() { w.get_dpi() } else { - log::warn!("Window {} has already been dropped", self.id); + log::error!("Window {} has already been dropped", self.id); 96.0 } } diff --git a/druid/examples/multiwin.rs b/druid/examples/multiwin.rs index 04d756bc9d..86bf749239 100644 --- a/druid/examples/multiwin.rs +++ b/druid/examples/multiwin.rs @@ -17,8 +17,8 @@ use druid::widget::prelude::*; use druid::widget::{Align, BackgroundBrush, Button, Flex, Label, Padding}; use druid::{ - commands as sys_cmds, AppDelegate, AppLauncher, Color, Command, ContextMenu, Data, DelegateCtx, - LocalizedString, MenuDesc, MenuItem, Selector, Target, WindowDesc, WindowId, + commands as sys_cmds, AppDelegate, AppLauncher, Application, Color, Command, ContextMenu, Data, + DelegateCtx, LocalizedString, MenuDesc, MenuItem, Selector, Target, WindowDesc, WindowId, }; use log::info; @@ -77,6 +77,9 @@ fn ui_builder() -> impl Widget { let new_button = Button::::new("New window").on_click(|ctx, _data, _env| { ctx.submit_command(sys_cmds::NEW_FILE, Target::Global); }); + let quit_button = Button::::new("Quit app").on_click(|_ctx, _data, _env| { + Application::global().quit(); + }); let mut col = Flex::column(); col.add_flex_child(Align::centered(Padding::new(5.0, label)), 1.0); @@ -86,6 +89,7 @@ fn ui_builder() -> impl Widget { col.add_flex_child(Align::centered(row), 1.0); let mut row = Flex::row(); row.add_child(Padding::new(5.0, new_button)); + row.add_child(Padding::new(5.0, quit_button)); col.add_flex_child(Align::centered(row), 1.0); Glow::new(col) } diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index 2be1cab7b2..43dc36f05f 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -233,7 +233,7 @@ impl Inner { self.root_menu = win.menu.take(); // If there are even no pending windows, we quit the run loop. if self.windows.count() == 0 { - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", feature = "x11"))] self.app.quit(); } } From 887de64c27c559aa21038778dab55288df5f5fd0 Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Wed, 6 May 2020 01:21:55 +0300 Subject: [PATCH 4/4] Add more X11 docs. --- druid-shell/src/platform/x11/window.rs | 73 +++++++++++++++++--------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 4f24cf35d3..7ea6c73110 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -19,7 +19,7 @@ use std::cell::RefCell; use std::convert::TryInto; use std::rc::{Rc, Weak}; -use cairo::XCBSurface; +use cairo::{XCBConnection, XCBDrawable, XCBSurface, XCBVisualType}; use xcb::{ Atom, Visualtype, ATOM_ATOM, ATOM_STRING, ATOM_WM_NAME, CONFIG_WINDOW_STACK_MODE, COPY_FROM_PARENT, CURRENT_TIME, CW_BACK_PIXEL, CW_EVENT_MASK, EVENT_MASK_BUTTON_PRESS, @@ -89,9 +89,16 @@ impl WindowBuilder { // TODO(x11/menus): implement WindowBuilder::set_menu (currently a no-op) } - fn atoms(&self, id: u32) -> Result { + /// Registers and returns all the atoms that the window will need. + fn atoms(&self, window_id: u32) -> Result { let conn = self.app.connection(); + // WM_PROTOCOLS + // + // List of atoms that identify the communications protocols between + // the client and window manager in which the client is willing to participate. + // + // https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html#wm_protocols_property let wm_protocols = match xcb::intern_atom(conn, false, "WM_PROTOCOLS").get_reply() { Ok(reply) => reply.atom(), Err(err) => { @@ -102,6 +109,15 @@ impl WindowBuilder { } }; + // WM_DELETE_WINDOW + // + // Including this atom in the WM_PROTOCOLS property on each window makes sure that + // if the window manager respects WM_DELETE_WINDOW it will send us the event. + // + // The WM_DELETE_WINDOW event is sent when there is a request to close the window. + // Registering for but ignoring this event means that the window will remain open. + // + // https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html#window_deletion let wm_delete_window = match xcb::intern_atom(conn, false, "WM_DELETE_WINDOW").get_reply() { Ok(reply) => reply.atom(), Err(err) => { @@ -112,12 +128,13 @@ impl WindowBuilder { } }; + // Replace the window's WM_PROTOCOLS with the following. let protocols = [wm_delete_window]; // TODO(x11/errors): Check the response for errors? xcb::change_property( conn, PROP_MODE_REPLACE as u8, - id, + window_id, wm_protocols, ATOM_ATOM, 32, @@ -130,34 +147,36 @@ impl WindowBuilder { }) } - fn cairo_context( + /// Create a new cairo `Context`. + fn create_cairo_context( &self, - id: u32, + window_id: u32, visual_type: &mut Visualtype, ) -> Result, Error> { - // Create a draw surface let conn = self.app.connection(); let cairo_xcb_connection = unsafe { - cairo::XCBConnection::from_raw_none( - conn.get_raw_conn() as *mut cairo_sys::xcb_connection_t - ) + XCBConnection::from_raw_none(conn.get_raw_conn() as *mut cairo_sys::xcb_connection_t) }; - let cairo_drawable = cairo::XCBDrawable(id); + let cairo_drawable = XCBDrawable(window_id); let cairo_visual_type = unsafe { - cairo::XCBVisualType::from_raw_none( + XCBVisualType::from_raw_none( &mut visual_type.base as *mut _ as *mut cairo_sys::xcb_visualtype_t, ) }; - // TODO(x11/errors): Don't unwrap let cairo_surface = XCBSurface::create( &cairo_xcb_connection, &cairo_drawable, &cairo_visual_type, self.size.width as i32, self.size.height as i32, - ) - .expect("couldn't create a cairo surface"); - Ok(RefCell::new(cairo::Context::new(&cairo_surface))) + ); + match cairo_surface { + Ok(cairo_surface) => Ok(RefCell::new(cairo::Context::new(&cairo_surface))), + Err(err) => Err(Error::Generic(format!( + "Failed to create cairo surface: {}", + err + ))), + } } // TODO(x11/menus): make menus if requested @@ -219,7 +238,7 @@ impl WindowBuilder { // TODO(x11/errors): Should do proper cleanup (window destruction etc) in case of error let atoms = self.atoms(id)?; - let cairo_context = self.cairo_context(id, &mut visual_type)?; + let cairo_context = self.create_cairo_context(id, &mut visual_type)?; // Figure out the refresh rate of the current screen let refresh_rate = util::refresh_rate(conn, id); let state = RefCell::new(WindowState { size: self.size }); @@ -354,7 +373,12 @@ impl Window { Ok(()) } - fn request_redraw(&self, rect: Rect, flush: bool) { + /// Tell the X server to mark the specified `rect` as needing redraw. + /// + /// ### Connection + /// + /// Does not flush the connection. + fn request_redraw(&self, rect: Rect) { // See: http://rtbo.github.io/rust-xcb/xcb/ffi/xproto/struct.xcb_expose_event_t.html let expose_event = xcb::ExposeEvent::new( self.id, @@ -371,13 +395,12 @@ impl Window { EVENT_MASK_EXPOSURE, &expose_event, ); - if flush { - self.app.connection().flush(); - } } fn render(&self, invalid_rect: Rect) -> Result<(), Error> { - // TODO(x11/errors): this function should return a proper error + // TODO(x11/errors): this function should return a an error + // instead of panicking or logging if the error isn't recoverable. + // Figure out the window's current size let geometry_cookie = xcb::get_geometry(self.app.connection(), self.id); let reply = geometry_cookie.get_reply().unwrap(); @@ -412,7 +435,7 @@ impl Window { let sleep_amount_ms = (1000.0 / self.refresh_rate.unwrap()) as u64; std::thread::sleep(std::time::Duration::from_millis(sleep_amount_ms)); - self.request_redraw(size.to_rect(), false); + self.request_redraw(size.to_rect()); } self.app.connection().flush(); Ok(()) @@ -457,13 +480,14 @@ impl Window { fn invalidate(&self) { match self.size() { - Ok(size) => self.request_redraw(size.to_rect(), true), + Ok(size) => self.invalidate_rect(size.to_rect()), Err(err) => log::error!("Window::invalidate - failed to get size: {}", err), } } fn invalidate_rect(&self, rect: Rect) { - self.request_redraw(rect, true); + self.request_redraw(rect); + self.app.connection().flush(); } fn set_title(&self, title: &str) { @@ -624,6 +648,7 @@ impl Window { client_message: &xcb::ClientMessageEvent, ) -> Result<(), Error> { // https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#id2745388 + // https://www.x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html#window_deletion if client_message.type_() == self.atoms.wm_protocols && client_message.format() == 32 { let protocol = client_message.data().data32()[0]; if protocol == self.atoms.wm_delete_window {