diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index 949e2f799b..d3a3304277 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -187,6 +187,12 @@ pub trait WindowExtMacOS { /// Getter for the [`WindowExtMacOS::set_unified_titlebar`]. fn unified_titlebar(&self) -> bool; + + /// Returns the height of the titlebar in logical points. + /// + /// This can be useful when drawing custom UI that needs to align with the + /// system titlebar. + fn titlebar_height(&self) -> f64; } impl WindowExtMacOS for dyn Window + '_ { @@ -297,6 +303,12 @@ impl WindowExtMacOS for dyn Window + '_ { let window = self.cast_ref::().unwrap(); window.maybe_wait_on_main(|w| w.unified_titlebar()) } + + #[inline] + fn titlebar_height(&self) -> f64 { + let window = self.cast_ref::().unwrap(); + window.maybe_wait_on_main(|w| w.titlebar_height()) + } } /// Corresponds to `NSApplicationActivationPolicy`. diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 6fce2bdbff..17fa959681 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -197,7 +197,11 @@ define_class!( fn draw_rect(&self, _rect: NSRect) { trace_scope!("drawRect:"); - self.ivars().app_state.handle_redraw(window_id(&self.window())); + let Some(window) = self.window() else { + return; + }; + + self.ivars().app_state.handle_redraw(window_id(&window)); // This is a direct subclass of NSView, no need to call superclass' drawRect: } @@ -420,7 +424,11 @@ define_class!( } // Send command action to user if they requested it. - let window_id = window_id(&self.window()); + let Some(window) = self.window() else { + return; + }; + + let window_id = window_id(&window); self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| { if let Some(handler) = app.macos_handler() { handler.standard_key_binding( @@ -525,7 +533,9 @@ define_class!( #[unsafe(method(insertTab:))] fn insert_tab(&self, _sender: Option<&AnyObject>) { trace_scope!("insertTab:"); - let window = self.window(); + let Some(window) = self.window() else { + return; + }; if let Some(first_responder) = window.firstResponder() { if *first_responder == ***self { window.selectNextKeyView(Some(self)) @@ -536,7 +546,9 @@ define_class!( #[unsafe(method(insertBackTab:))] fn insert_back_tab(&self, _sender: Option<&AnyObject>) { trace_scope!("insertBackTab:"); - let window = self.window(); + let Some(window) = self.window() else { + return; + }; if let Some(first_responder) = window.firstResponder() { if *first_responder == ***self { window.selectPreviousKeyView(Some(self)) @@ -818,19 +830,23 @@ impl WinitView { this } - fn window(&self) -> Retained { - (**self).window().expect("view must be installed in a window") + fn window(&self) -> Option> { + (**self).window() } fn queue_event(&self, event: WindowEvent) { - let window_id = window_id(&self.window()); + let Some(window) = self.window() else { + return; + }; + + let window_id = window_id(&window); self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| { app.window_event(event_loop, window_id, event); }); } fn scale_factor(&self) -> f64 { - self.window().backingScaleFactor() as f64 + self.window().map(|window| window.backingScaleFactor() as f64).unwrap_or(1.0) } fn is_ime_enabled(&self) -> bool { diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index fe4da61efb..277690474a 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -2029,6 +2029,14 @@ impl WindowExtMacOS for WindowDelegate { window.toolbar().is_some() && window.toolbarStyle() == NSWindowToolbarStyle::Unified } + + fn titlebar_height(&self) -> f64 { + let window = self.window(); + let frame = window.frame(); + let content = window.contentRectForFrameRect(frame); + + (frame.size.height - content.size.height).max(0.0) + } } const DEFAULT_STANDARD_FRAME: NSRect = diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index cfca862806..749b85412a 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -1388,7 +1388,10 @@ pub trait Window: AsAny + Send + Sync + fmt::Debug { /// the title bar. This is useful when implementing custom decorations. /// /// ## Platform-specific - /// **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported. + /// - **Windows:** Supported. + /// - **Wayland:** Supported. + /// - **X11:** Supported on some window managers (via `_GTK_SHOW_WINDOW_MENU`). + /// - **Android / iOS / macOS / Orbital / Web:** Unsupported. /// /// [window menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu fn show_window_menu(&self, position: Position); diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index e5ac78f4dd..93340ab018 100644 --- a/winit-x11/src/atoms.rs +++ b/winit-x11/src/atoms.rs @@ -95,6 +95,7 @@ atom_manager! { None: b"None", // Miscellaneous Atoms + _GTK_SHOW_WINDOW_MENU, _GTK_THEME_VARIANT, _MOTIF_WM_HINTS, _NET_ACTIVE_WINDOW, diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 0277732110..3a0b2bd22d 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -2007,7 +2007,36 @@ impl UnownedWindow { } #[inline] - pub fn show_window_menu(&self, _position: Position) {} + pub fn show_window_menu(&self, position: Position) { + let atoms = self.xconn.atoms(); + let show_menu_message = atoms[_GTK_SHOW_WINDOW_MENU]; + + if !util::hint_is_supported(show_menu_message) { + return; + } + + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + let (window_x, window_y) = self.inner_position_physical(); + let x_root = (i64::from(window_x) + i64::from(x)).max(0) as u32; + let y_root = (i64::from(window_y) + i64::from(y)).max(0) as u32; + + let result = self.xconn.send_client_msg( + self.xwindow, + self.root, + show_menu_message, + Some(xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY), + [util::VIRTUAL_CORE_POINTER as u32, x_root, y_root, 0, 0], + ); + + if let Err(err) = result { + tracing::error!("failed to request window menu: {err}"); + return; + } + + if let Err(err) = self.xconn.flush_requests() { + tracing::error!("failed to flush window menu request: {err}"); + } + } /// Resizes the window while it is being dragged. pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { diff --git a/winit/examples/custom_decorations.rs b/winit/examples/custom_decorations.rs new file mode 100644 index 0000000000..00d3555747 --- /dev/null +++ b/winit/examples/custom_decorations.rs @@ -0,0 +1,347 @@ +//! Demonstrates how to create a titlebar-less window and implement basic custom decorations. +//! +//! The goal is to show the building blocks for "draw your own titlebar" across platforms: +//! - Create a window with `decorations(false)`. +//! - Implement click-to-drag (move) via `Window::drag_window()`. +//! - Implement edge/corner resize via `Window::drag_resize_window()`. +//! - Show the system window menu via `Window::show_window_menu()` where supported. +//! +//! This intentionally avoids any UI toolkits: the window is rendered as a solid background with a +//! darker top bar using `softbuffer`. + +use std::error::Error; + +use ::tracing::{info, warn}; +use cursor_icon::CursorIcon; +use winit::application::ApplicationHandler; +use winit::cursor::Cursor; +use winit::event::{ButtonSource, ElementState, MouseButton, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{Key, NamedKey}; +use winit::window::{ResizeDirection, Window, WindowAttributes, WindowId}; + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +const TITLEBAR_HEIGHT_LOGICAL: f64 = 36.0; +const RESIZE_BORDER_LOGICAL: f64 = 8.0; +const CAPTION_BUTTON_SIZE_LOGICAL: f64 = 14.0; +const CAPTION_BUTTON_GAP_LOGICAL: f64 = 8.0; +const CAPTION_BUTTON_PADDING_LOGICAL: f64 = 10.0; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CaptionButton { + Minimize, + Maximize, + Close, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HitTarget { + None, + Titlebar, + Resize(ResizeDirection), + Button(CaptionButton), +} + +#[derive(Debug)] +struct App { + window: Option>, + decorations: bool, + hit_target: HitTarget, + cursor_icon: CursorIcon, +} + +impl Default for App { + fn default() -> Self { + Self { + window: None, + decorations: false, + hit_target: HitTarget::None, + cursor_icon: CursorIcon::default(), + } + } +} + +impl App { + fn window(&self) -> &dyn Window { + self.window.as_ref().expect("window should be created").as_ref() + } + + fn titlebar_height_px(&self) -> f64 { + TITLEBAR_HEIGHT_LOGICAL * self.window().scale_factor() + } + + fn resize_border_px(&self) -> f64 { + RESIZE_BORDER_LOGICAL * self.window().scale_factor() + } + + fn hit_test_resize( + &self, + position: winit::dpi::PhysicalPosition, + ) -> Option { + let size = self.window().surface_size(); + let width = size.width as f64; + let height = size.height as f64; + + if width <= 0.0 || height <= 0.0 { + return None; + } + + let border = self.resize_border_px().max(1.0); + let x = position.x; + let y = position.y; + + let left = x >= 0.0 && x < border; + let right = x <= width && x > width - border; + let top = y >= 0.0 && y < border; + let bottom = y <= height && y > height - border; + + match (left, right, top, bottom) { + (true, _, true, _) => Some(ResizeDirection::NorthWest), + (_, true, true, _) => Some(ResizeDirection::NorthEast), + (true, _, _, true) => Some(ResizeDirection::SouthWest), + (_, true, _, true) => Some(ResizeDirection::SouthEast), + (true, ..) => Some(ResizeDirection::West), + (_, true, ..) => Some(ResizeDirection::East), + (_, _, true, _) => Some(ResizeDirection::North), + (_, _, _, true) => Some(ResizeDirection::South), + _ => None, + } + } + + fn is_in_titlebar(&self, position: winit::dpi::PhysicalPosition) -> bool { + let y = position.y; + y >= 0.0 && y < self.titlebar_height_px() + } + + fn caption_button_rects(&self) -> [(CaptionButton, fill::Rect); 3] { + let size = self.window().surface_size(); + let width = size.width; + + let scale = self.window().scale_factor(); + let button_size = (CAPTION_BUTTON_SIZE_LOGICAL * scale).round().max(1.0) as u32; + let gap = (CAPTION_BUTTON_GAP_LOGICAL * scale).round().max(0.0) as u32; + let padding = (CAPTION_BUTTON_PADDING_LOGICAL * scale).round().max(0.0) as u32; + + let top_bar_height = self.titlebar_height_px().round().max(1.0) as u32; + let y = (top_bar_height.saturating_sub(button_size)) / 2; + + let cluster_width = button_size.saturating_mul(3).saturating_add(gap.saturating_mul(2)); + let x0 = width.saturating_sub(padding.saturating_add(cluster_width)); + + let rect = |i: u32| fill::Rect { + x: x0.saturating_add(i.saturating_mul(button_size.saturating_add(gap))), + y, + width: button_size, + height: button_size, + }; + + [ + (CaptionButton::Minimize, rect(0)), + (CaptionButton::Maximize, rect(1)), + (CaptionButton::Close, rect(2)), + ] + } + + fn hit_test_caption_buttons( + &self, + position: winit::dpi::PhysicalPosition, + ) -> Option { + for (button, rect) in self.caption_button_rects() { + let x0 = rect.x as f64; + let y0 = rect.y as f64; + let x1 = (rect.x + rect.width) as f64; + let y1 = (rect.y + rect.height) as f64; + if position.x >= x0 && position.x < x1 && position.y >= y0 && position.y < y1 { + return Some(button); + } + } + None + } + + fn hit_test(&self, position: winit::dpi::PhysicalPosition) -> HitTarget { + if let Some(direction) = self.hit_test_resize(position) { + return HitTarget::Resize(direction); + } + + if let Some(button) = self.hit_test_caption_buttons(position) { + return HitTarget::Button(button); + } + + if self.is_in_titlebar(position) { + return HitTarget::Titlebar; + } + + HitTarget::None + } + + fn cursor_for_target(target: HitTarget) -> CursorIcon { + match target { + HitTarget::None => CursorIcon::Default, + HitTarget::Titlebar => CursorIcon::Grab, + HitTarget::Button(_) => CursorIcon::Pointer, + HitTarget::Resize(direction) => match direction { + ResizeDirection::East | ResizeDirection::West => CursorIcon::EwResize, + ResizeDirection::North | ResizeDirection::South => CursorIcon::NsResize, + ResizeDirection::NorthEast | ResizeDirection::SouthWest => CursorIcon::NeswResize, + ResizeDirection::NorthWest | ResizeDirection::SouthEast => CursorIcon::NwseResize, + }, + } + } + + fn set_cursor_icon(&mut self, icon: CursorIcon) { + if icon == self.cursor_icon { + return; + } + self.cursor_icon = icon; + self.window().set_cursor(Cursor::from(icon)); + } +} + +impl ApplicationHandler for App { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + info!("Key bindings:"); + info!(" d: toggle decorations (useful for comparison)"); + info!(" esc: exit"); + info!("Mouse:"); + info!(" left drag on top bar: move window"); + info!(" left drag near edges: resize window (if supported)"); + info!(" right click: show window menu (if supported)"); + + self.decorations = false; + self.hit_target = HitTarget::None; + self.cursor_icon = CursorIcon::Default; + + let window_attributes = WindowAttributes::default() + .with_title("Custom decorations (titlebar-less)") + .with_decorations(self.decorations); + + self.window = match event_loop.create_window(window_attributes) { + Ok(window) => Some(window), + Err(err) => { + eprintln!("error creating window: {err}"); + event_loop.exit(); + return; + }, + }; + + self.window().request_redraw(); + } + + fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + fill::cleanup_window(self.window()); + event_loop.exit(); + }, + WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => { + match event.logical_key.as_ref() { + Key::Named(NamedKey::Escape) => { + fill::cleanup_window(self.window()); + event_loop.exit(); + }, + Key::Character("d") => { + self.decorations = !self.decorations; + info!("decorations: {}", self.decorations); + self.window().set_decorations(self.decorations); + self.window().request_redraw(); + }, + _ => (), + } + }, + WindowEvent::PointerButton { + state: ElementState::Pressed, + button: ButtonSource::Mouse(MouseButton::Left), + position, + .. + } => match self.hit_test(position) { + HitTarget::Resize(direction) => { + if let Err(err) = self.window().drag_resize_window(direction) { + warn!("drag_resize_window({direction:?}) failed: {err:?}"); + } + }, + HitTarget::Titlebar => { + if let Err(err) = self.window().drag_window() { + warn!("drag_window failed: {err:?}"); + } + }, + HitTarget::Button(CaptionButton::Close) => { + fill::cleanup_window(self.window()); + event_loop.exit(); + }, + HitTarget::Button(CaptionButton::Minimize) => { + self.window().set_minimized(true); + }, + HitTarget::Button(CaptionButton::Maximize) => { + let maximized = self.window().is_maximized(); + self.window().set_maximized(!maximized); + }, + HitTarget::None => (), + }, + WindowEvent::PointerButton { + state: ElementState::Pressed, + button: ButtonSource::Mouse(MouseButton::Right), + position, + .. + } => { + self.window().show_window_menu(position.into()); + }, + WindowEvent::PointerMoved { position, .. } => { + let target = self.hit_test(position); + if target != self.hit_target { + self.hit_target = target; + self.set_cursor_icon(Self::cursor_for_target(target)); + self.window().request_redraw(); + } + }, + WindowEvent::SurfaceResized(_) => { + self.window().request_redraw(); + }, + WindowEvent::RedrawRequested => { + let window = self.window(); + window.pre_present_notify(); + + let top_bar_height = self.titlebar_height_px().ceil().max(1.0) as u32; + + let mut rects = Vec::new(); + for (button, rect) in self.caption_button_rects() { + let base: u32 = match button { + CaptionButton::Close => 0xffb8383d_u32, + CaptionButton::Maximize => 0xff2f9e44_u32, + CaptionButton::Minimize => 0xfff08c00_u32, + }; + let color = if self.hit_target == HitTarget::Button(button) { + base.saturating_add(0x00101010) + } else { + base + }; + rects.push((rect, color)); + } + + fill::fill_window_with_top_bar_and_rects( + window, + 0xff1c1c1c, + 0xff2b2b2b, + top_bar_height, + &rects, + ); + }, + _ => (), + } + } +} + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::new()?; + event_loop.run_app(App::default())?; + + Ok(()) +} diff --git a/winit/examples/custom_titlebar.rs b/winit/examples/custom_titlebar.rs new file mode 100644 index 0000000000..0ef3b458be --- /dev/null +++ b/winit/examples/custom_titlebar.rs @@ -0,0 +1,268 @@ +//! Demonstrates basic titlebar customization. +//! +//! This example intentionally keeps rendering simple (a solid fill) and focuses on: +//! - Selecting titlebar-related window attributes at creation time. +//! - Toggling a small set of runtime titlebar properties (where supported). + +use std::error::Error; + +use ::tracing::{info, warn}; +use winit::application::ApplicationHandler; +use winit::event::{ButtonSource, ElementState, KeyEvent, MouseButton, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{Key, NamedKey}; +#[cfg(macos_platform)] +use winit::platform::macos::{WindowAttributesMacOS, WindowExtMacOS}; +#[cfg(web_platform)] +use winit::platform::web::WindowAttributesWeb; +#[cfg(windows_platform)] +use winit::platform::windows::{BackdropType, Color, WindowAttributesWindows, WindowExtWindows}; +use winit::window::{Window, WindowAttributes, WindowId}; + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +#[derive(Debug, Default)] +struct App { + window: Option>, + decorations: bool, + + #[cfg(macos_platform)] + macos_unified_titlebar: bool, + #[cfg(macos_platform)] + macos_shadow: bool, + + #[cfg(windows_platform)] + windows_custom_title_colors: bool, + #[cfg(windows_platform)] + windows_backdrop: BackdropType, +} + +impl App { + fn window(&self) -> &dyn Window { + self.window.as_ref().expect("window should be created").as_ref() + } + + fn request_redraw(&self) { + self.window().request_redraw(); + } +} + +impl ApplicationHandler for App { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + self.decorations = true; + + #[cfg(macos_platform)] + { + self.macos_unified_titlebar = true; + self.macos_shadow = true; + } + + #[cfg(windows_platform)] + { + self.windows_custom_title_colors = true; + self.windows_backdrop = BackdropType::MainWindow; + } + + info!("Key bindings:"); + info!(" d: toggle decorations (all platforms)"); + info!(" right click: show window menu (if supported)"); + #[cfg(macos_platform)] + { + info!(" u: toggle unified titlebar (macOS)"); + info!(" s: toggle window shadow (macOS)"); + info!(" h: print titlebar height (macOS)"); + } + #[cfg(windows_platform)] + { + info!(" c: toggle titlebar colors (Windows 11 Build 22000+)"); + info!(" b: cycle backdrop (Windows 11 Build 22523+)"); + } + info!(" esc: exit"); + + let mut window_attributes = WindowAttributes::default() + .with_title("Titlebar customization (press 'd', 'esc')") + .with_decorations(self.decorations); + + // Platform-specific titlebar configuration. These are applied at window creation time. + #[cfg(macos_platform)] + { + let platform = WindowAttributesMacOS::default() + .with_titlebar_transparent(true) + .with_fullsize_content_view(true) + .with_title_hidden(true) + .with_movable_by_window_background(true) + .with_unified_titlebar(self.macos_unified_titlebar); + window_attributes = window_attributes.with_platform_attributes(Box::new(platform)); + } + + #[cfg(windows_platform)] + { + // Titlebar color customization requires Windows 11 Build 22000+. The calls below may + // no-op on older versions. + let platform = WindowAttributesWindows::default() + .with_title_background_color(Some(Color::from_rgb(0x20, 0x24, 0x2a))) + .with_title_text_color(Color::from_rgb(0xf0, 0xf3, 0xf6)) + .with_border_color(Some(Color::from_rgb(0x57, 0x74, 0x8a))) + .with_system_backdrop(self.windows_backdrop); + window_attributes = window_attributes.with_platform_attributes(Box::new(platform)); + } + + #[cfg(web_platform)] + { + // Make sure the canvas is attached to the DOM. + let platform = WindowAttributesWeb::default().with_append(true); + window_attributes = window_attributes.with_platform_attributes(Box::new(platform)); + } + + let window = match event_loop.create_window(window_attributes) { + Ok(window) => window, + Err(err) => { + eprintln!("error creating window: {err}"); + event_loop.exit(); + return; + }, + }; + + // Apply runtime adjustments that are supported after creation. + #[cfg(macos_platform)] + { + window.set_has_shadow(self.macos_shadow); + window.set_unified_titlebar(self.macos_unified_titlebar); + info!("macOS titlebar height: {:.1}", window.titlebar_height()); + } + #[cfg(windows_platform)] + { + window.set_system_backdrop(self.windows_backdrop); + } + + self.window = Some(window); + self.request_redraw(); + } + + fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) { + match event { + WindowEvent::CloseRequested => { + fill::cleanup_window(self.window()); + event_loop.exit(); + }, + WindowEvent::KeyboardInput { + event: KeyEvent { logical_key: key, state: ElementState::Pressed, .. }, + .. + } => { + match key.as_ref() { + Key::Named(NamedKey::Escape) => { + fill::cleanup_window(self.window()); + event_loop.exit(); + }, + Key::Character("d") => { + self.decorations = !self.decorations; + info!("decorations: {}", self.decorations); + self.window().set_decorations(self.decorations); + self.request_redraw(); + }, + + Key::Character("u") => { + #[cfg(macos_platform)] + { + self.macos_unified_titlebar = !self.macos_unified_titlebar; + info!("macos unified titlebar: {}", self.macos_unified_titlebar); + self.window().set_unified_titlebar(self.macos_unified_titlebar); + } + #[cfg(not(macos_platform))] + warn!("'u' only has an effect on macOS"); + }, + Key::Character("s") => { + #[cfg(macos_platform)] + { + self.macos_shadow = !self.macos_shadow; + info!("macos shadow: {}", self.macos_shadow); + self.window().set_has_shadow(self.macos_shadow); + } + #[cfg(not(macos_platform))] + warn!("'s' only has an effect on macOS"); + }, + Key::Character("h") => { + #[cfg(macos_platform)] + info!("macOS titlebar height: {:.1}", self.window().titlebar_height()); + #[cfg(not(macos_platform))] + warn!("'h' only has an effect on macOS"); + }, + + Key::Character("c") => { + #[cfg(windows_platform)] + { + self.windows_custom_title_colors = !self.windows_custom_title_colors; + info!( + "windows custom title colors: {}", + self.windows_custom_title_colors + ); + + if self.windows_custom_title_colors { + self.window().set_title_background_color(Some(Color::from_rgb( + 0x20, 0x24, 0x2a, + ))); + self.window() + .set_title_text_color(Color::from_rgb(0xf0, 0xf3, 0xf6)); + } else { + self.window().set_title_background_color(None); + self.window().set_title_text_color(Color::SYSTEM_DEFAULT); + } + } + #[cfg(not(windows_platform))] + warn!("'c' only has an effect on Windows"); + }, + Key::Character("b") => { + #[cfg(windows_platform)] + { + self.windows_backdrop = match self.windows_backdrop { + BackdropType::Auto => BackdropType::MainWindow, + BackdropType::MainWindow => BackdropType::TransientWindow, + BackdropType::TransientWindow => BackdropType::TabbedWindow, + BackdropType::TabbedWindow => BackdropType::Auto, + }; + + info!("windows backdrop: {:?}", self.windows_backdrop); + self.window().set_system_backdrop(self.windows_backdrop); + } + #[cfg(not(windows_platform))] + warn!("'b' only has an effect on Windows"); + }, + + _ => (), + }; + }, + WindowEvent::PointerButton { + state: ElementState::Pressed, + button: ButtonSource::Mouse(MouseButton::Right), + position, + .. + } => { + self.window().show_window_menu(position.into()); + }, + WindowEvent::SurfaceResized(_) => { + self.request_redraw(); + }, + WindowEvent::RedrawRequested => { + let window = self.window(); + window.pre_present_notify(); + + // Pick a slightly brighter fill so the titlebar overlay is obvious on macOS. + fill::fill_window_with_color(window, 0xff2a3340); + }, + _ => (), + } + } +} + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + let event_loop = EventLoop::new()?; + event_loop.run_app(App::default())?; + Ok(()) +} diff --git a/winit/examples/util/fill.rs b/winit/examples/util/fill.rs index 0b1a4fb06a..f2aae5d0eb 100644 --- a/winit/examples/util/fill.rs +++ b/winit/examples/util/fill.rs @@ -15,6 +15,10 @@ pub use platform::fill_window; pub use platform::fill_window_with_animated_color; #[allow(unused_imports)] pub use platform::fill_window_with_color; +#[allow(unused_imports)] +pub use platform::fill_window_with_top_bar; +#[allow(unused_imports)] +pub use platform::{Rect, fill_window_with_top_bar_and_rects}; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod platform { @@ -31,6 +35,36 @@ mod platform { use web_time::Instant; use winit::window::{Window, WindowId}; + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct Rect { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, + } + + impl Rect { + pub fn clamp_to(self, width: u32, height: u32) -> Option { + if self.width == 0 || self.height == 0 { + return None; + } + + let x1 = self.x.min(width); + let y1 = self.y.min(height); + let x2 = self.x.saturating_add(self.width).min(width); + let y2 = self.y.saturating_add(self.height).min(height); + + let width = x2.saturating_sub(x1); + let height = y2.saturating_sub(y1); + + if width == 0 || height == 0 { + return None; + } + + Some(Self { x: x1, y: y1, width, height }) + } + } + thread_local! { // NOTE: You should never do things like that, create context and drop it before // you drop the event loop. We do this for brevity to not blow up examples. We use @@ -103,6 +137,101 @@ mod platform { }) } + #[allow(dead_code)] + pub fn fill_window_with_top_bar( + window: &dyn Window, + body_color: u32, + top_bar_color: u32, + top_bar_height: u32, + ) { + GC.with(|gc| { + let size = window.surface_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = + gc.get_or_insert_with(|| GraphicsContext::new(window)).create_surface(window); + + surface.resize(width, height).expect("Failed to resize the softbuffer surface"); + + let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer"); + let width = width.get() as usize; + let height = height.get() as usize; + let top_bar_height = (top_bar_height as usize).min(height); + + let pixels = &mut *buffer; + for y in 0..height { + let color = if y < top_bar_height { top_bar_color } else { body_color }; + let row = &mut pixels[y * width..(y + 1) * width]; + row.fill(color); + } + + buffer.present().expect("Failed to present the softbuffer buffer"); + }) + } + + #[allow(dead_code)] + pub fn fill_window_with_top_bar_and_rects( + window: &dyn Window, + body_color: u32, + top_bar_color: u32, + top_bar_height: u32, + rects: &[(Rect, u32)], + ) { + GC.with(|gc| { + let size = window.surface_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = + gc.get_or_insert_with(|| GraphicsContext::new(window)).create_surface(window); + + surface.resize(width, height).expect("Failed to resize the softbuffer surface"); + + let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer"); + let width = width.get() as usize; + let height = height.get() as usize; + let top_bar_height = (top_bar_height as usize).min(height); + + let pixels = &mut *buffer; + for y in 0..height { + let color = if y < top_bar_height { top_bar_color } else { body_color }; + let row = &mut pixels[y * width..(y + 1) * width]; + row.fill(color); + } + + let surface_width = width as u32; + let surface_height = height as u32; + for &(rect, color) in rects { + let Some(rect) = rect.clamp_to(surface_width, surface_height) else { + continue; + }; + + let x0 = rect.x as usize; + let x1 = (rect.x + rect.width) as usize; + let y0 = rect.y as usize; + let y1 = (rect.y + rect.height) as usize; + + for y in y0..y1 { + let row = &mut pixels[y * width + x0..y * width + x1]; + row.fill(color); + } + } + + buffer.present().expect("Failed to present the softbuffer buffer"); + }) + } + #[allow(dead_code)] pub fn fill_window(window: &dyn Window) { fill_window_with_color(window, 0xff181818); @@ -141,6 +270,35 @@ mod platform { // No-op on mobile platforms. } + #[allow(dead_code)] + pub fn fill_window_with_top_bar( + _window: &dyn winit::window::Window, + _body_color: u32, + _top_bar_color: u32, + _top_bar_height: u32, + ) { + // No-op on mobile platforms. + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + pub struct Rect { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, + } + + #[allow(dead_code)] + pub fn fill_window_with_top_bar_and_rects( + _window: &dyn winit::window::Window, + _body_color: u32, + _top_bar_color: u32, + _top_bar_height: u32, + _rects: &[(Rect, u32)], + ) { + // No-op on mobile platforms. + } + #[allow(dead_code)] pub fn fill_window_with_animated_color( _window: &dyn winit::window::Window,