diff --git a/.changes/prevent-window-overflow.md b/.changes/prevent-window-overflow.md new file mode 100644 index 000000000000..fe11587c99e4 --- /dev/null +++ b/.changes/prevent-window-overflow.md @@ -0,0 +1,9 @@ +--- +"tauri": "patch:feat" +"tauri-utils": "patch:feat" +"tauri-runtime": "patch:feat" +"tauri-runtime-wry": "patch:feat" +"@tauri-apps/api": "patch:feat" +--- + +Add `preventOverflow` config option to prevent the window from overflowing the monitor size on creation diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 3229931cf177..69f821490dad 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -317,6 +317,17 @@ ], "format": "double" }, + "preventOverflow": { + "description": "Whether or not to prevent the window from overflowing the workarea\n\n ## Platform-specific\n\n - **iOS / Android:** Unsupported.", + "anyOf": [ + { + "$ref": "#/definitions/PreventOverflowConfig" + }, + { + "type": "null" + } + ] + }, "resizable": { "description": "Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled.", "default": true, @@ -539,6 +550,46 @@ } ] }, + "PreventOverflowConfig": { + "description": "Prevent overflow with a margin", + "anyOf": [ + { + "description": "Enable prevent overflow or not", + "type": "boolean" + }, + { + "description": "Enable prevent overflow with a margin\n so that the window's size + this margin won't overflow the workarea", + "allOf": [ + { + "$ref": "#/definitions/PreventOverflowMargin" + } + ] + } + ] + }, + "PreventOverflowMargin": { + "description": "Enable prevent overflow with a margin\n so that the window's size + this margin won't overflow the workarea", + "type": "object", + "required": [ + "height", + "width" + ], + "properties": { + "width": { + "description": "Horizontal margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "height": { + "description": "Vertical margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Theme": { "description": "System theme.", "oneOf": [ diff --git a/crates/tauri-cli/schema.json b/crates/tauri-cli/schema.json index 613287fc78f8..7a771086ce0f 100644 --- a/crates/tauri-cli/schema.json +++ b/crates/tauri-cli/schema.json @@ -301,6 +301,17 @@ ], "format": "double" }, + "preventOverflow": { + "description": "Whether or not to prevent window overflow", + "anyOf": [ + { + "$ref": "#/definitions/PreventOverflowMarginConfig" + }, + { + "type": "null" + } + ] + }, "resizable": { "description": "Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled.", "default": true, @@ -488,6 +499,46 @@ } ] }, + "PreventOverflowMarginConfig": { + "description": "Prevent overflow with a margin", + "anyOf": [ + { + "description": "Enable prevent overflow or not", + "type": "boolean" + }, + { + "description": "Enable prevent overflow with a margin", + "allOf": [ + { + "$ref": "#/definitions/PreventOverflowMargin" + } + ] + } + ] + }, + "PreventOverflowMargin": { + "description": "Enable prevent overflow with a margin", + "type": "object", + "required": [ + "height", + "width" + ], + "properties": { + "width": { + "description": "Horizontal margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "height": { + "description": "Vertical margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Theme": { "description": "System theme.", "oneOf": [ diff --git a/crates/tauri-cli/tauri.config.schema.json b/crates/tauri-cli/tauri.config.schema.json index 9d28874da38b..564849e9148e 100644 --- a/crates/tauri-cli/tauri.config.schema.json +++ b/crates/tauri-cli/tauri.config.schema.json @@ -301,6 +301,17 @@ ], "format": "double" }, + "preventOverflow": { + "description": "Whether or not to prevent window overflow", + "anyOf": [ + { + "$ref": "#/definitions/PreventOverflowMarginConfig" + }, + { + "type": "null" + } + ] + }, "resizable": { "description": "Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled.", "default": true, @@ -488,6 +499,46 @@ } ] }, + "PreventOverflowMarginConfig": { + "description": "Prevent overflow with a margin", + "anyOf": [ + { + "description": "Enable prevent overflow or not", + "type": "boolean" + }, + { + "description": "Enable prevent overflow with a margin", + "allOf": [ + { + "$ref": "#/definitions/PreventOverflowMargin" + } + ] + } + ] + }, + "PreventOverflowMargin": { + "description": "Enable prevent overflow with a margin", + "type": "object", + "required": [ + "height", + "width" + ], + "properties": { + "width": { + "description": "Horizontal margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "height": { + "description": "Vertical margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Theme": { "description": "System theme.", "oneOf": [ diff --git a/crates/tauri-runtime-wry/Cargo.toml b/crates/tauri-runtime-wry/Cargo.toml index 3a180bc43f4e..ebf0cea2fe27 100644 --- a/crates/tauri-runtime-wry/Cargo.toml +++ b/crates/tauri-runtime-wry/Cargo.toml @@ -56,6 +56,7 @@ objc2-app-kit = { version = "0.2", features = [ "NSResponder", "NSView", "NSWindow", + "NSScreen", "NSGraphics", ] } diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index beaa7f9cb4af..c114ff6163ce 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -13,6 +13,8 @@ )] use http::Request; +#[cfg(desktop)] +use monitor::MonitorExt; use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle}; use tauri_runtime::{ @@ -63,6 +65,8 @@ use tao::{ UserAttentionType as TaoUserAttentionType, }, }; +#[cfg(desktop)] +use tauri_utils::config::PreventOverflowConfig; #[cfg(target_os = "macos")] use tauri_utils::TitleBarStyle; use tauri_utils::{ @@ -125,6 +129,8 @@ use std::{ pub type WebviewId = u32; type IpcHandler = dyn Fn(Request) + 'static; +#[cfg(desktop)] +mod monitor; #[cfg(any( windows, target_os = "linux", @@ -730,6 +736,7 @@ impl From for ProgressBarStateWrapper { pub struct WindowBuilderWrapper { inner: TaoWindowBuilder, center: bool, + prevent_overflow: Option, #[cfg(target_os = "macos")] tabbing_identifier: Option, } @@ -737,7 +744,9 @@ pub struct WindowBuilderWrapper { impl std::fmt::Debug for WindowBuilderWrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut s = f.debug_struct("WindowBuilderWrapper"); - s.field("inner", &self.inner).field("center", &self.center); + s.field("inner", &self.inner) + .field("center", &self.center) + .field("prevent_overflow", &self.prevent_overflow); #[cfg(target_os = "macos")] { s.field("tabbing_identifier", &self.tabbing_identifier); @@ -863,6 +872,15 @@ impl WindowBuilder for WindowBuilderWrapper { if let Some(window_classname) = &config.window_classname { window = window.window_classname(window_classname); } + + if let Some(prevent_overflow) = &config.prevent_overflow { + window = match prevent_overflow { + PreventOverflowConfig::Enable(true) => window.prevent_overflow(), + PreventOverflowConfig::Margin(margin) => window + .prevent_overflow_with_margin(TaoPhysicalSize::new(margin.width, margin.height).into()), + _ => window, + }; + } } window @@ -909,6 +927,31 @@ impl WindowBuilder for WindowBuilderWrapper { self } + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + fn prevent_overflow(mut self) -> Self { + self + .prevent_overflow + .replace(PhysicalSize::new(0, 0).into()); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { + self.prevent_overflow.replace(margin); + self + } + fn resizable(mut self, resizable: bool) -> Self { self.inner = self.inner.with_resizable(resizable); self @@ -3962,7 +4005,7 @@ fn create_window( } #[cfg(desktop)] - if window_builder.center { + if window_builder.prevent_overflow.is_some() || window_builder.center { let monitor = if let Some(window_position) = &window_builder.inner.window.position { event_loop.available_monitors().find(|m| { let monitor_pos = m.position(); @@ -3979,39 +4022,68 @@ fn create_window( } else { event_loop.primary_monitor() }; - if let Some(monitor) = monitor { + let scale_factor = monitor.scale_factor(); let desired_size = window_builder .inner .window .inner_size .unwrap_or_else(|| TaoPhysicalSize::new(800, 600).into()); - let scale_factor = monitor.scale_factor(); - #[allow(unused_mut)] - let mut window_size = window_builder + let mut inner_size = window_builder .inner .window .inner_size_constraints .clamp(desired_size, scale_factor) .to_physical::(scale_factor); + let mut window_size = inner_size; + #[allow(unused_mut)] + // Left and right window shadow counts as part of the window on Windows + // We need to include it when calculating positions, but not size + let mut shadow_width = 0; #[cfg(windows)] - { - if window_builder.inner.window.decorations { - use windows::Win32::UI::WindowsAndMessaging::{AdjustWindowRect, WS_OVERLAPPEDWINDOW}; - let mut rect = windows::Win32::Foundation::RECT::default(); - let result = unsafe { AdjustWindowRect(&mut rect, WS_OVERLAPPEDWINDOW, false) }; - if result.is_ok() { - window_size.width += (rect.right - rect.left) as u32; - // rect.bottom is made out of shadow, and we don't care about it - window_size.height += -rect.top as u32; + if window_builder.inner.window.decorations { + use windows::Win32::UI::WindowsAndMessaging::{AdjustWindowRect, WS_OVERLAPPEDWINDOW}; + let mut rect = windows::Win32::Foundation::RECT::default(); + let result = unsafe { AdjustWindowRect(&mut rect, WS_OVERLAPPEDWINDOW, false) }; + if result.is_ok() { + shadow_width = (rect.right - rect.left) as u32; + // rect.bottom is made out of shadow, and we don't care about it + window_size.height += -rect.top as u32; + } + } + + if let Some(margin) = window_builder.prevent_overflow { + let work_area = monitor.work_area(); + let margin = margin.to_physical::(scale_factor); + let constraint = PhysicalSize::new( + work_area.size.width - margin.width, + work_area.size.height - margin.height, + ); + if window_size.width > constraint.width || window_size.height > constraint.height { + if window_size.width > constraint.width { + inner_size.width = inner_size + .width + .saturating_sub(window_size.width - constraint.width); + window_size.width = constraint.width; + } + if window_size.height > constraint.height { + inner_size.height = inner_size + .height + .saturating_sub(window_size.height - constraint.height); + window_size.height = constraint.height; } + window_builder.inner.window.inner_size = Some(inner_size.into()); } } - let position = window::calculate_window_center_position(window_size, monitor); - let logical_position = position.to_logical::(scale_factor); - window_builder = window_builder.position(logical_position.x, logical_position.y); + + if window_builder.center { + window_size.width += shadow_width; + let position = window::calculate_window_center_position(window_size, monitor); + let logical_position = position.to_logical::(scale_factor); + window_builder = window_builder.position(logical_position.x, logical_position.y); + } } - } + }; let window = window_builder.inner.build(event_loop).unwrap(); diff --git a/crates/tauri-runtime-wry/src/monitor/linux.rs b/crates/tauri-runtime-wry/src/monitor/linux.rs new file mode 100644 index 000000000000..31ac2718d4b9 --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/linux.rs @@ -0,0 +1,20 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::PhysicalRect; +use gtk::prelude::MonitorExt; +use tao::{ + dpi::{PhysicalPosition, PhysicalSize}, + platform::unix::MonitorHandleExtUnix, +}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + let rect = self.gdk_monitor().workarea(); + PhysicalRect { + size: PhysicalSize::new(rect.width() as u32, rect.height() as u32), + position: PhysicalPosition::new(rect.x(), rect.y()), + } + } +} diff --git a/crates/tauri-runtime-wry/src/monitor/macos.rs b/crates/tauri-runtime-wry/src/monitor/macos.rs new file mode 100644 index 000000000000..eef09491860f --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/macos.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::PhysicalRect; +use tao::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + use objc2_app_kit::NSScreen; + use tao::platform::macos::MonitorHandleExtMacOS; + if let Some(ns_screen) = self.ns_screen() { + let ns_screen: &NSScreen = unsafe { &*ns_screen.cast() }; + let rect = ns_screen.visibleFrame(); + let scale_factor = self.scale_factor(); + PhysicalRect { + size: LogicalSize::new(rect.size.width, rect.size.height).to_physical(scale_factor), + position: LogicalPosition::new(rect.origin.x, rect.origin.y).to_physical(scale_factor), + } + } else { + PhysicalRect { + size: self.size(), + position: PhysicalPosition::default(), + } + } + } +} diff --git a/crates/tauri-runtime-wry/src/monitor/mod.rs b/crates/tauri-runtime-wry/src/monitor/mod.rs new file mode 100644 index 000000000000..d986d8693aa1 --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tao::dpi::{PhysicalPosition, PhysicalSize}; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; + +pub struct PhysicalRect { + pub size: PhysicalSize, + pub position: PhysicalPosition, +} + +pub trait MonitorExt { + /// Get the work area of this monitor + /// + /// ## Platform-specific: + /// + /// - **Android / iOS**: Unsupported. + fn work_area(&self) -> PhysicalRect; +} diff --git a/crates/tauri-runtime-wry/src/monitor/windows.rs b/crates/tauri-runtime-wry/src/monitor/windows.rs new file mode 100644 index 000000000000..0fddb6fc4bac --- /dev/null +++ b/crates/tauri-runtime-wry/src/monitor/windows.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use super::PhysicalRect; +use tao::dpi::{PhysicalPosition, PhysicalSize}; + +impl super::MonitorExt for tao::monitor::MonitorHandle { + fn work_area(&self) -> PhysicalRect { + use tao::platform::windows::MonitorHandleExtWindows; + use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO}; + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let status = unsafe { GetMonitorInfoW(HMONITOR(self.hmonitor() as _), &mut monitor_info) }; + if status.as_bool() { + PhysicalRect { + size: PhysicalSize::new( + (monitor_info.rcWork.right - monitor_info.rcWork.left) as u32, + (monitor_info.rcWork.bottom - monitor_info.rcWork.top) as u32, + ), + position: PhysicalPosition::new(monitor_info.rcWork.left, monitor_info.rcWork.top), + } + } else { + PhysicalRect { + size: self.size(), + position: PhysicalPosition::default(), + } + } + } +} diff --git a/crates/tauri-runtime-wry/src/window/mod.rs b/crates/tauri-runtime-wry/src/window/mod.rs index c2b1b448686d..f38888460966 100644 --- a/crates/tauri-runtime-wry/src/window/mod.rs +++ b/crates/tauri-runtime-wry/src/window/mod.rs @@ -61,29 +61,23 @@ pub fn calculate_window_center_position( window_size: tao::dpi::PhysicalSize, target_monitor: tao::monitor::MonitorHandle, ) -> tao::dpi::PhysicalPosition { - #[cfg(windows)] + let monitor_size: tao::dpi::PhysicalSize; + let monitor_position: tao::dpi::PhysicalPosition; + #[cfg(desktop)] { - use ::windows::Win32::Graphics::Gdi::{GetMonitorInfoW, HMONITOR, MONITORINFO}; - use tao::platform::windows::MonitorHandleExtWindows; - - let mut monitor_info = MONITORINFO { - cbSize: std::mem::size_of::() as u32, - ..Default::default() - }; - let hmonitor = target_monitor.hmonitor(); - let status = unsafe { GetMonitorInfoW(HMONITOR(hmonitor as _), &mut monitor_info) }; - if status.into() { - let available_width = monitor_info.rcWork.right - monitor_info.rcWork.left; - let available_height = monitor_info.rcWork.bottom - monitor_info.rcWork.top; - let x = (available_width - window_size.width as i32) / 2 + monitor_info.rcWork.left; - let y = (available_height - window_size.height as i32) / 2 + monitor_info.rcWork.top; - return tao::dpi::PhysicalPosition::new(x, y); - } + use crate::monitor::MonitorExt; + let work_area = target_monitor.work_area(); + monitor_size = work_area.size; + monitor_position = work_area.position; + } + #[cfg(mobile)] + { + monitor_size = target_monitor.size(); + monitor_position = target_monitor.position(); } - let screen_size = target_monitor.size(); - let monitor_pos = target_monitor.position(); - let x = (screen_size.width as i32 - window_size.width as i32) / 2 + monitor_pos.x; - let y = (screen_size.height as i32 - window_size.height as i32) / 2 + monitor_pos.y; - tao::dpi::PhysicalPosition::new(x, y) + tao::dpi::PhysicalPosition::new( + (monitor_size.width - window_size.width) as i32 / 2 + monitor_position.x, + (monitor_size.height - window_size.height) as i32 / 2 + monitor_position.y, + ) } diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs index dc843100da98..13a90e833b8f 100644 --- a/crates/tauri-runtime/src/window.rs +++ b/crates/tauri-runtime/src/window.rs @@ -267,6 +267,23 @@ pub trait WindowBuilder: WindowBuilderBase { #[must_use] fn inner_size_constraints(self, constraints: WindowSizeConstraints) -> Self; + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + fn prevent_overflow(self) -> Self; + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + fn prevent_overflow_with_margin(self, margin: dpi::Size) -> Self; + /// Whether the window is resizable or not. /// When resizable is set to false, native window's maximize button is automatically disabled. #[must_use] diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 3229931cf177..69f821490dad 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -317,6 +317,17 @@ ], "format": "double" }, + "preventOverflow": { + "description": "Whether or not to prevent the window from overflowing the workarea\n\n ## Platform-specific\n\n - **iOS / Android:** Unsupported.", + "anyOf": [ + { + "$ref": "#/definitions/PreventOverflowConfig" + }, + { + "type": "null" + } + ] + }, "resizable": { "description": "Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled.", "default": true, @@ -539,6 +550,46 @@ } ] }, + "PreventOverflowConfig": { + "description": "Prevent overflow with a margin", + "anyOf": [ + { + "description": "Enable prevent overflow or not", + "type": "boolean" + }, + { + "description": "Enable prevent overflow with a margin\n so that the window's size + this margin won't overflow the workarea", + "allOf": [ + { + "$ref": "#/definitions/PreventOverflowMargin" + } + ] + } + ] + }, + "PreventOverflowMargin": { + "description": "Enable prevent overflow with a margin\n so that the window's size + this margin won't overflow the workarea", + "type": "object", + "required": [ + "height", + "width" + ], + "properties": { + "width": { + "description": "Horizontal margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "height": { + "description": "Vertical margin in physical unit", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Theme": { "description": "System theme.", "oneOf": [ diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 3877fefeb305..2162e6441c62 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -1438,6 +1438,30 @@ pub struct WindowEffectsConfig { pub color: Option, } +/// Enable prevent overflow with a margin +/// so that the window's size + this margin won't overflow the workarea +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PreventOverflowMargin { + /// Horizontal margin in physical unit + pub width: u32, + /// Vertical margin in physical unit + pub height: u32, +} + +/// Prevent overflow with a margin +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(untagged)] +pub enum PreventOverflowConfig { + /// Enable prevent overflow or not + Enable(bool), + /// Enable prevent overflow with a margin + /// so that the window's size + this margin won't overflow the workarea + Margin(PreventOverflowMargin), +} + /// The window configuration object. /// /// See more: @@ -1491,6 +1515,13 @@ pub struct WindowConfig { /// The max window height. #[serde(alias = "max-height")] pub max_height: Option, + /// Whether or not to prevent the window from overflowing the workarea + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[serde(alias = "prevent-overflow")] + pub prevent_overflow: Option, /// Whether the window is resizable or not. When resizable is set to false, native window's maximize button is automatically disabled. #[serde(default = "default_true")] pub resizable: bool, @@ -1706,6 +1737,7 @@ impl Default for WindowConfig { min_height: None, max_width: None, max_height: None, + prevent_overflow: None, resizable: true, maximizable: true, minimizable: true, @@ -2955,6 +2987,32 @@ mod build { } } + impl ToTokens for PreventOverflowMargin { + fn to_tokens(&self, tokens: &mut TokenStream) { + let width = self.width; + let height = self.height; + + literal_struct!( + tokens, + ::tauri::utils::config::PreventOverflowMargin, + width, + height + ) + } + } + + impl ToTokens for PreventOverflowConfig { + fn to_tokens(&self, tokens: &mut TokenStream) { + let prefix = quote! { ::tauri::utils::config::PreventOverflowMarginConfig }; + + #[allow(deprecated)] + tokens.append_all(match self { + Self::Enable(enable) => quote! { #prefix::Enable(#enable) }, + Self::Margin(margin) => quote! { #prefix::Margin(#margin) }, + }) + } + } + impl ToTokens for WindowConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let label = str_lit(&self.label); @@ -2971,6 +3029,7 @@ mod build { let min_height = opt_lit(self.min_height.as_ref()); let max_width = opt_lit(self.max_width.as_ref()); let max_height = opt_lit(self.max_height.as_ref()); + let prevent_overflow = opt_lit(self.prevent_overflow.as_ref()); let resizable = self.resizable; let maximizable = self.maximizable; let minimizable = self.minimizable; @@ -3022,6 +3081,7 @@ mod build { min_height, max_width, max_height, + prevent_overflow, resizable, maximizable, minimizable, diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs index 2d45794762ca..a6705b8cfec0 100644 --- a/crates/tauri/src/test/mock_runtime.rs +++ b/crates/tauri/src/test/mock_runtime.rs @@ -348,6 +348,14 @@ impl WindowBuilder for MockWindowBuilder { self } + fn prevent_overflow(self) -> Self { + self + } + + fn prevent_overflow_with_margin(self, margin: tauri_runtime::dpi::Size) -> Self { + self + } + fn resizable(self, resizable: bool) -> Self { self } diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index 901ffb1d9a47..765a304bd6fe 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -413,6 +413,29 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self } + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow(mut self) -> Self { + self.window_builder = self.window_builder.prevent_overflow(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { + self.window_builder = self.window_builder.prevent_overflow_with_margin(margin); + self + } + /// Whether the window is resizable or not. /// When resizable is set to false, native window's maximize button is automatically disabled. #[must_use] diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs index 8330d6aaa733..3ca93916d869 100644 --- a/crates/tauri/src/window/mod.rs +++ b/crates/tauri/src/window/mod.rs @@ -472,6 +472,29 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { self } + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) on creation + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow(mut self) -> Self { + self.window_builder = self.window_builder.prevent_overflow(); + self + } + + /// Prevent the window from overflowing the working area (e.g. monitor size - taskbar size) + /// on creation with a margin + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[must_use] + pub fn prevent_overflow_with_margin(mut self, margin: Size) -> Self { + self.window_builder = self.window_builder.prevent_overflow_with_margin(margin); + self + } + /// Whether the window is resizable or not. /// When resizable is set to false, native window's maximize button is automatically disabled. #[must_use] diff --git a/packages/api/src/window.ts b/packages/api/src/window.ts index 2283be585d3d..06093fa56c99 100644 --- a/packages/api/src/window.ts +++ b/packages/api/src/window.ts @@ -2194,6 +2194,14 @@ interface Effects { color?: Color } +/** + * Minimum margin to work area + */ +interface PreventOverflowMargin { + width: number + height: number +} + /** * Configuration for the window to create. * @@ -2218,6 +2226,14 @@ interface WindowOptions { maxWidth?: number /** The maximum height. Only applies if `maxWidth` is also set. */ maxHeight?: number + /** + * Prevent initial window from getting bigger than the work area + * + * #### Platform-specific + * + * - **iOS / Android:** Unsupported. + */ + preventOverflow?: boolean | PreventOverflowMargin /** Whether the window is resizable or not. */ resizable?: boolean /** Window title. */