Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ uv.lock
.python-version

MODULE.bazel.lock

# Ignore Xcode projects created by xcodegen
*.xcodeproj
*.xcworkspace
Info.plist
13 changes: 6 additions & 7 deletions demos/energy-monitor/ios-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ targets:
Energy Monitor:
type: application
platform: iOS
deploymentTarget: "12.0"
deploymentTarget: "16.6"
info:
path: Info.plist
properties:
UILaunchScreen:
- ImageRespectSafeAreaInsets: false
path: Info.plist
properties:
UILaunchScreen:
- ImageRespectSafeAreaInsets: false
sources: []
postCompileScripts:
- script: |
../../scripts/build_for_ios_with_cargo.bash energy-monitor
outputFileLists:
$TARGET_BUILD_DIR/$EXECUTABLE_PATH
outputFileLists: $TARGET_BUILD_DIR/$EXECUTABLE_PATH
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>
31 changes: 24 additions & 7 deletions internal/backends/android-activity/androidwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,24 @@ impl WindowAdapter for AndroidWindowAdapter {
&self.window
}
fn size(&self) -> PhysicalSize {
self.app.native_window().map_or_else(Default::default, |w| PhysicalSize {
width: w.width() as u32,
height: w.height() as u32,
})
}
fn safe_area_inset(&self) -> PhysicalInset {
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,
})
Default::default()
} else {
self.java_helper.get_view_rect().unwrap_or_else(|e| print_jni_error(&self.app, e)).1
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) as u32,
top: offset.y.max(0) as u32,
right: (win_size.width.saturating_sub(size.width + offset.x as u32)),
bottom: (win_size.height.saturating_sub(size.height + offset.y as u32)),
}
}
}
fn renderer(&self) -> &dyn i_slint_core::platform::Renderer {
Expand Down Expand Up @@ -261,6 +272,9 @@ 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.safe_area_inset().to_logical(scale_factor),
})?;
}
}
PollEvent::Main(MainEvent::Destroy) => {
Expand Down Expand Up @@ -446,8 +460,11 @@ 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.safe_area_inset().to_logical(scale_factor),
})?;
self.offset.set(offset);
Ok(())
Expand Down
45 changes: 43 additions & 2 deletions internal/backends/winit/winitwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use euclid::approxeq::ApproxEq;

#[cfg(muda)]
use i_slint_core::api::LogicalPosition;
use i_slint_core::api::{LogicalInset, PhysicalInset};
use i_slint_core::lengths::{PhysicalPx, ScaleFactor};
use winit::event_loop::ActiveEventLoop;
#[cfg(target_arch = "wasm32")]
Expand Down Expand Up @@ -721,8 +722,12 @@ 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),
})?;

// Workaround fox winit not sync'ing CSS size of the canvas (the size shown on the browser)
Expand Down Expand Up @@ -1077,6 +1082,32 @@ impl WindowAdapter for WinitWindowAdapter {
self.size.get()
}

fn safe_area_inset(&self) -> PhysicalInset {
#[cfg(not(target_os = "ios"))]
return Default::default();
#[cfg(target_os = "ios")]
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(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()
}

fn request_redraw(&self) {
if !self.pending_redraw.replace(true) {
self.frame_throttle.request_throttled_redraw();
Expand Down Expand Up @@ -1164,6 +1195,16 @@ 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(),
),
})
.unwrap();
}

let m = properties.is_fullscreen();
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/builtins.slint
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,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
116 changes: 116 additions & 0 deletions internal/core/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This module contains types that are public and re-exported in the slint-rs as we
pub use crate::future::*;
use crate::graphics::{Rgba8Pixel, SharedPixelBuffer};
use crate::input::{KeyEventType, MouseEvent};
use crate::lengths::LogicalLength;
use crate::window::{WindowAdapter, WindowInner};
use alloc::boxed::Box;
use alloc::string::String;
Expand Down Expand Up @@ -249,6 +250,113 @@ fn logical_physical_size() {
assert_eq!(logical.to_physical(2.), phys);
}

/// An inset represented in the coordinate space of logical pixels. That is the thickness
/// of the border between the safe area and the edges of the window.
#[derive(Debug, Default, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LogicalInset {
/// The top inset in logical pixels.
pub top: f32,
/// The bottom in logical pixels.
pub bottom: f32,
/// The left inset in logical pixels.
pub left: f32,
/// The right inset in logical pixels.
pub right: f32,
}

impl LogicalInset {
/// Construct a new logical inset from the given border values, that are assumed to be
/// in the logical coordinate space.
pub const fn new(top: f32, bottom: f32, left: f32, right: f32) -> Self {
Self { top, bottom, left, right }
}

/// Converts the top inset to logical pixels.
#[inline]
pub const fn top(&self) -> LogicalLength {
LogicalLength::new(self.top)
}
/// Converts the bottom inset to logical pixels.
#[inline]
pub const fn bottom(&self) -> LogicalLength {
LogicalLength::new(self.bottom)
}
/// Converts the left inset to logical pixels.
#[inline]
pub const fn left(&self) -> LogicalLength {
LogicalLength::new(self.left)
}
/// Converts the right inset to logical pixels.
#[inline]
pub const fn right(&self) -> LogicalLength {
LogicalLength::new(self.right)
}
}

/// An inset represented in the coordinate space of physical pixels. That is the thickness
/// of the border between the safe area and the edges of the window.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PhysicalInset {
/// The top inset in physical pixels.
pub top: i32,
/// The bottom in physical pixels.
pub bottom: i32,
/// The left inset in physical pixels.
pub left: i32,
/// The right inset in physical pixels.
pub right: i32,
}

impl PhysicalInset {
/// Construct a new logical inset from the given border values, that are assumed to be
/// in the physical coordinate space.
pub const fn new(top: i32, bottom: i32, left: i32, right: i32) -> Self {
Self { top, bottom, left, right }
}

/// Convert a given logical inset to a physical inset by dividing the lengths by the
/// specified scale factor.
#[inline]
pub const fn to_logical(&self, scale_factor: f32) -> LogicalInset {
LogicalInset::new(
self.top_to_logical(scale_factor).0,
self.bottom_to_logical(scale_factor).0,
self.left_to_logical(scale_factor).0,
self.right_to_logical(scale_factor).0,
)
}

/// Convert the top logical inset to a physical inset by dividing the length by the
/// specified scale factor.
#[inline]
pub const fn top_to_logical(&self, scale_factor: f32) -> LogicalLength {
LogicalLength::new(self.top as f32 / scale_factor)
}

/// Convert the bottom logical inset to a physical inset by dividing the length by the
/// specified scale factor.
#[inline]
pub const fn bottom_to_logical(&self, scale_factor: f32) -> LogicalLength {
LogicalLength::new(self.bottom as f32 / scale_factor)
}

#[inline]
/// Convert the left logical inset to a physical inset by dividing the length by the
/// specified scale factor.
pub const fn left_to_logical(&self, scale_factor: f32) -> LogicalLength {
LogicalLength::new(self.left as f32 / scale_factor)
}

/// Convert the right logical inset to a physical inset by dividing the length by the
/// specified scale factor.
#[inline]
pub const fn right_to_logical(&self, scale_factor: f32) -> LogicalLength {
LogicalLength::new(self.right as f32 / scale_factor)
}
}

#[i_slint_core_macros::slint_doc]
/// This enum describes a low-level access to specific graphics APIs used
/// by the renderer.
Expand Down Expand Up @@ -675,6 +783,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
Loading
Loading