Skip to content
1 change: 1 addition & 0 deletions api/cpp/cbindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ fn gen_corelib(
.with_src(crate_dir.join("api.rs"))
.with_src(crate_dir.join("model.rs"))
.with_src(crate_dir.join("graphics/image.rs"))
.with_src(crate_dir.join("lengths.rs"))
.with_include("slint_string.h")
.with_after_include(format!(
r#"
Expand Down
32 changes: 22 additions & 10 deletions demos/energy-monitor/ui/desktop_window.slint
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ import { Images } from "images.slint";
import { Theme } from "theme.slint";
import { HeaderAdapter } from "blocks/blocks.slint";
import { Navigation, MenuButton, Menu, Value } from "widgets/widgets.slint";
import { BalanceAdapter, OverviewAdapter, UsageAdapter, WeatherAdapter, MenuPageAdapter, MenuOverviewAdapter, SettingsAdapter } from "pages/pages.slint";
import {
BalanceAdapter,
OverviewAdapter,
UsageAdapter,
WeatherAdapter,
MenuPageAdapter,
MenuOverviewAdapter,
SettingsAdapter,
} from "pages/pages.slint";
import { KioskOverlay } from "blocks/kiosk_overlay.slint";

export { OverviewAdapter, UsageAdapter, Value, WeatherAdapter, MenuPageAdapter, MenuOverviewAdapter, SettingsAdapter,
Expand All @@ -38,32 +46,36 @@ export component MainWindow inherits Window {
}
*/

if root.screen-size == ScreenSize.Mobile : MobileMain {
preferred-width: 100%;
preferred-height: 100%;
if root.screen-size == ScreenSize.Mobile: HorizontalLayout {
padding-left: root.safe-area-inset-left;
padding-top: root.safe-area-inset-top;
padding-right: root.safe-area-inset-right;
padding-bottom: root.safe-area-inset-bottom;
MobileMain {
preferred-width: 100%;
preferred-height: 100%;
}
}

if root.screen-size == ScreenSize.EmbeddedMedium : MidMain {
if root.screen-size == ScreenSize.EmbeddedMedium: MidMain {
preferred-width: 100%;
preferred-height: 100%;
}

if root.screen-size == ScreenSize.EmbeddedSmall : SmallMain {
if root.screen-size == ScreenSize.EmbeddedSmall: SmallMain {
preferred-width: 100%;
preferred-height: 100%;
}

if SettingsAdapter.kiosk-mode-checked : KioskOverlay {}
if SettingsAdapter.kiosk-mode-checked: KioskOverlay { }

pure function get-screen-size() -> ScreenSize {
if (root.width <= root.mobile-break-point && root.width < root.height) {
return ScreenSize.Mobile;
}

if (root.width < root.mid-break-point) {
return ScreenSize.EmbeddedSmall;
}

return ScreenSize.EmbeddedMedium;
}
}
}
20 changes: 20 additions & 0 deletions docs/astro/src/content/docs/reference/window/window.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,23 @@ Whether the window should be borderless/frameless or not.
<SlintProperty propName="title" typeName="string">
The window title that is shown in the title bar.
</SlintProperty>

### safe-area-inset-top
<SlintProperty propName="safe-area-inset-top" typeName="length" propertyVisibility="out">
Some devices, such as mobile phones, allow programs to overlap the system UI. A few examples for this are the notch on iPhones, the window buttons on macOS on windows that extend their content over the titlebar and the system bar on Android. This property exposes the amount of space at the top of the window that can be drawn to but where no interactive elements should be placed.
</SlintProperty>

### safe-area-inset-bottom
<SlintProperty propName="safe-area-inset-bottom" typeName="length" propertyVisibility="out">
Some devices, such as mobile phones, allow programs to overlap the system UI. A few examples for this are the notch on iPhones, the window buttons on macOS on windows that extend their content over the titlebar and the system bar on Android. This property exposes the amount of space at the bottom of the window that can be drawn to but where no interactive elements should be placed.
</SlintProperty>

### safe-area-inset-left
<SlintProperty propName="safe-area-inset-left" typeName="length" propertyVisibility="out">
Some devices, such as mobile phones, allow programs to overlap the system UI. A few examples for this are the notch on iPhones, the window buttons on macOS on windows that extend their content over the titlebar and the system bar on Android. This property exposes the amount of space at the left of the window that can be drawn to but where no interactive elements should be placed.
</SlintProperty>

### safe-area-inset-right
<SlintProperty propName="safe-area-inset-right" typeName="length" propertyVisibility="out">
Some devices, such as mobile phones, allow programs to overlap the system UI. A few examples for this are the notch on iPhones, the window buttons on macOS on windows that extend their content over the titlebar and the system bar on Android. This property exposes the amount of space at the right of the window that can be drawn to but where no interactive elements should be placed.
</SlintProperty>
47 changes: 37 additions & 10 deletions internal/backends/android-activity/androidwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use android_activity::input::{
use android_activity::{InputStatus, MainEvent, PollEvent};
use i_slint_core::api::{LogicalPosition, PhysicalPosition, PhysicalSize, PlatformError, Window};
use i_slint_core::items::ColorScheme;
use i_slint_core::lengths::PhysicalInset;
use i_slint_core::platform::{
Key, PointerEventButton, WindowAdapter, WindowEvent, WindowProperties,
};
Expand Down Expand Up @@ -51,14 +52,10 @@ impl WindowAdapter for AndroidWindowAdapter {
&self.window
}
fn size(&self) -> PhysicalSize {
if self.fullscreen.get() {
self.app.native_window().map_or_else(Default::default, |w| PhysicalSize {
width: w.width() as u32,
height: w.height() as u32,
})
} else {
self.java_helper.get_view_rect().unwrap_or_else(|e| print_jni_error(&self.app, e)).1
}
self.app.native_window().map_or_else(Default::default, |w| PhysicalSize {
width: w.width() as u32,
height: w.height() as u32,
})
}
fn renderer(&self) -> &dyn i_slint_core::platform::Renderer {
&self.renderer
Expand Down Expand Up @@ -166,6 +163,22 @@ impl i_slint_core::window::WindowAdapterInternal for AndroidWindowAdapter {
fn color_scheme(&self) -> ColorScheme {
self.color_scheme.as_ref().get()
}

fn safe_area_inset(&self) -> PhysicalInset {
if self.fullscreen.get() {
Default::default()
} else {
let (offset, size) =
self.java_helper.get_view_rect().unwrap_or_else(|e| print_jni_error(&self.app, e));
let win_size = self.size();
PhysicalInset {
left: offset.x.max(0),
top: offset.y.max(0),
right: win_size.width.saturating_sub(size.width + (offset.x as u32)) as i32,
bottom: win_size.height.saturating_sub(size.height + (offset.y as u32)) as i32,
}
}
}
}

impl AndroidWindowAdapter {
Expand Down Expand Up @@ -261,6 +274,13 @@ impl AndroidWindowAdapter {
self.window.try_dispatch_event(WindowEvent::Resized {
size: self.size().to_logical(scale_factor),
})?;
self.window.try_dispatch_event(WindowEvent::SafeAreaChanged {
inset: self
.internal(i_slint_core::InternalToken)
.map(|internal| internal.safe_area_inset().to_logical(scale_factor))
.unwrap_or_default(),
token: i_slint_core::InternalToken,
})?;
}
}
PollEvent::Main(MainEvent::Destroy) => {
Expand Down Expand Up @@ -446,8 +466,15 @@ impl AndroidWindowAdapter {
self.java_helper.get_view_rect().unwrap_or_else(|e| print_jni_error(&self.app, e))
};

self.window.try_dispatch_event(WindowEvent::Resized {
size: size.to_logical(self.window.scale_factor()),
let scale_factor = self.window.scale_factor();
self.window
.try_dispatch_event(WindowEvent::Resized { size: size.to_logical(scale_factor) })?;
self.window.try_dispatch_event(WindowEvent::SafeAreaChanged {
inset: self
.internal(i_slint_core::InternalToken)
.map(|internal| internal.safe_area_inset().to_logical(scale_factor))
.unwrap_or_default(),
token: i_slint_core::InternalToken,
})?;
self.offset.set(offset);
Ok(())
Expand Down
46 changes: 43 additions & 3 deletions internal/backends/winit/winitwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use euclid::approxeq::ApproxEq;

#[cfg(muda)]
use i_slint_core::api::LogicalPosition;
use i_slint_core::lengths::{PhysicalPx, ScaleFactor};
use i_slint_core::lengths::{LogicalInset, PhysicalPx, ScaleFactor};
use winit::event_loop::ActiveEventLoop;
#[cfg(target_arch = "wasm32")]
use winit::platform::web::WindowExtWebSys;
Expand Down Expand Up @@ -726,8 +726,13 @@ impl WinitWindowAdapter {
self.size.set(physical_size);
self.pending_requested_size.set(None);
let scale_factor = WindowInner::from_pub(self.window()).scale_factor();
self.window().try_dispatch_event(WindowEvent::Resized {
size: physical_size.to_logical(scale_factor),

let size = physical_size.to_logical(scale_factor);
self.window().try_dispatch_event(WindowEvent::Resized { size })?;

self.window().try_dispatch_event(WindowEvent::SafeAreaChanged {
inset: self.safe_area_inset().to_logical(scale_factor),
token: corelib::InternalToken,
})?;

// Workaround fox winit not sync'ing CSS size of the canvas (the size shown on the browser)
Expand Down Expand Up @@ -1169,6 +1174,17 @@ impl WindowAdapter for WinitWindowAdapter {
size: i_slint_core::api::LogicalSize::new(width, height),
})
.unwrap();
self.window()
.try_dispatch_event(WindowEvent::SafeAreaChanged {
inset: LogicalInset::new(
window_item.safe_area_inset_top().get(),
window_item.safe_area_inset_bottom().get(),
window_item.safe_area_inset_left().get(),
window_item.safe_area_inset_right().get(),
),
token: corelib::InternalToken,
})
.unwrap();
}

let m = properties.is_fullscreen();
Expand Down Expand Up @@ -1467,6 +1483,30 @@ impl WindowAdapterInternal for WinitWindowAdapter {
}
Ok(())
}

#[cfg(target_os = "ios")]
fn safe_area_inset(&self) -> i_slint_core::lengths::PhysicalInset {
self.winit_window_or_none
.borrow()
.as_window()
.and_then(|window| {
let outer_position = window.outer_position().ok()?;
let inner_position = window.inner_position().ok()?;
let outer_size = window.outer_size();
let inner_size = window.inner_size();
Some(i_slint_core::lengths::PhysicalInset::new(
inner_position.y - outer_position.y,
outer_size.height as i32
- (inner_size.height as i32)
- (inner_position.y - outer_position.y),
inner_position.x - outer_position.x,
outer_size.width as i32
- (inner_size.width as i32)
- (inner_position.x - outer_position.x),
))
})
.unwrap_or_default()
}
}

impl Drop for WinitWindowAdapter {
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/builtins.slint
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,10 @@ export component ContextMenuArea inherits Empty {
component WindowItem {
in-out property <length> width;
in-out property <length> height;
out property <length> safe-area-inset-top;
out property <length> safe-area-inset-bottom;
out property <length> safe-area-inset-left;
out property <length> safe-area-inset-right;
in property <brush> background; // StyleMetrics.background set in apply_default_properties_from_style
in property <brush> color <=> background;
in property <string> title: "Slint Window";
Expand Down
8 changes: 8 additions & 0 deletions internal/core/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,14 @@ impl Window {
self.0.set_window_item_geometry(size.to_euclid());
self.0.window_adapter().renderer().resize(size.to_physical(self.scale_factor()))?;
}
crate::platform::WindowEvent::SafeAreaChanged { inset, .. } => {
self.0.set_window_item_safe_area(
inset.top(),
inset.bottom(),
inset.left(),
inset.right(),
);
}
crate::platform::WindowEvent::CloseRequested => {
if self.0.request_close() {
self.hide()?;
Expand Down
4 changes: 4 additions & 0 deletions internal/core/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,10 @@ impl Default for PropertyAnimation {
pub struct WindowItem {
pub width: Property<LogicalLength>,
pub height: Property<LogicalLength>,
pub safe_area_inset_top: Property<LogicalLength>,
pub safe_area_inset_bottom: Property<LogicalLength>,
pub safe_area_inset_left: Property<LogicalLength>,
pub safe_area_inset_right: Property<LogicalLength>,
pub background: Property<Brush>,
pub title: Property<SharedString>,
pub no_frame: Property<bool>,
Expand Down
Loading
Loading