Merge pull request #1964 from bungoboingo/feat/multi-window-support

[Feature] 🪟 Multi Window 🪟 .. redux!
This commit is contained in:
Héctor Ramón 2023-12-05 01:03:09 +01:00 committed by GitHub
commit fc285d3e46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 3020 additions and 761 deletions

View file

@ -25,7 +25,6 @@ body:
Before filing an issue... Before filing an issue...
- If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples]. - If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples].
- If you are using `glow`, you need support for OpenGL 2.1+. Please, make sure you can run [the `glow` examples].
If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly! If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly!

View file

@ -49,6 +49,8 @@ web-colors = ["iced_renderer/web-colors"]
webgl = ["iced_renderer/webgl"] webgl = ["iced_renderer/webgl"]
# Enables the syntax `highlighter` module # Enables the syntax `highlighter` module
highlighter = ["iced_highlighter"] highlighter = ["iced_highlighter"]
# Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"]
# Enables the advanced module # Enables the advanced module
advanced = [] advanced = []

View file

@ -45,7 +45,7 @@ The widgets of a _graphical_ user interface produce some primitives that eventua
Currently, there are two different official renderers: Currently, there are two different official renderers:
- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal. - [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal.
- [`iced_glow`] is powered by [`glow`] and supports OpenGL 2.1+ and OpenGL ES 2.0+. - [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported.
Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate. Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate.
@ -54,10 +54,7 @@ The widgets of a graphical user _interface_ are interactive. __Shells__ gather a
Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture]. Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture].
As of now, there are two official shells: As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`].
- [`iced_winit`] implements a shell runtime on top of [`winit`].
- [`iced_glutin`] is similar to [`iced_winit`], but it also deals with [OpenGL context creation].
## The web target ## The web target
The Web platform provides all the abstractions necessary to draw widgets and gather users interactions. The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
@ -91,5 +88,4 @@ Finally, [`iced`] unifies everything into a simple abstraction to create cross-p
[`winit`]: https://github.com/rust-windowing/winit [`winit`]: https://github.com/rust-windowing/winit
[`glutin`]: https://github.com/rust-windowing/glutin [`glutin`]: https://github.com/rust-windowing/glutin
[`dodrio`]: https://github.com/fitzgen/dodrio [`dodrio`]: https://github.com/fitzgen/dodrio
[OpenGL context creation]: https://www.khronos.org/opengl/wiki/Creating_an_OpenGL_Context
[The Elm Architecture]: https://guide.elm-lang.org/architecture/ [The Elm Architecture]: https://guide.elm-lang.org/architecture/

View file

@ -23,5 +23,8 @@ palette.optional = true
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
instant.workspace = true instant.workspace = true
[target.'cfg(windows)'.dependencies]
raw-window-handle.workspace = true
[dev-dependencies] [dev-dependencies]
approx = "0.5" approx = "0.5"

View file

@ -19,7 +19,7 @@ pub enum Event {
Mouse(mouse::Event), Mouse(mouse::Event),
/// A window event /// A window event
Window(window::Event), Window(window::Id, window::Event),
/// A touch event /// A touch event
Touch(touch::Event), Touch(touch::Event),

View file

@ -1,26 +1,34 @@
use crate::Vector; use crate::Vector;
use num_traits::{Float, Num};
use std::fmt;
/// A 2D point. /// A 2D point.
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Point { pub struct Point<T = f32> {
/// The X coordinate. /// The X coordinate.
pub x: f32, pub x: T,
/// The Y coordinate. /// The Y coordinate.
pub y: f32, pub y: T,
} }
impl Point { impl Point {
/// The origin (i.e. a [`Point`] at (0, 0)). /// The origin (i.e. a [`Point`] at (0, 0)).
pub const ORIGIN: Point = Point::new(0.0, 0.0); pub const ORIGIN: Self = Self::new(0.0, 0.0);
}
impl<T: Num> Point<T> {
/// Creates a new [`Point`] with the given coordinates. /// Creates a new [`Point`] with the given coordinates.
pub const fn new(x: f32, y: f32) -> Self { pub const fn new(x: T, y: T) -> Self {
Self { x, y } Self { x, y }
} }
/// Computes the distance to another [`Point`]. /// Computes the distance to another [`Point`].
pub fn distance(&self, to: Point) -> f32 { pub fn distance(&self, to: Self) -> T
where
T: Float,
{
let a = self.x - to.x; let a = self.x - to.x;
let b = self.y - to.y; let b = self.y - to.y;
@ -34,9 +42,9 @@ impl From<[f32; 2]> for Point {
} }
} }
impl From<[u16; 2]> for Point { impl From<[u16; 2]> for Point<u16> {
fn from([x, y]: [u16; 2]) -> Self { fn from([x, y]: [u16; 2]) -> Self {
Point::new(x.into(), y.into()) Point::new(x, y)
} }
} }
@ -46,10 +54,13 @@ impl From<Point> for [f32; 2] {
} }
} }
impl std::ops::Add<Vector> for Point { impl<T> std::ops::Add<Vector<T>> for Point<T>
where
T: std::ops::Add<Output = T>,
{
type Output = Self; type Output = Self;
fn add(self, vector: Vector) -> Self { fn add(self, vector: Vector<T>) -> Self {
Self { Self {
x: self.x + vector.x, x: self.x + vector.x,
y: self.y + vector.y, y: self.y + vector.y,
@ -57,10 +68,13 @@ impl std::ops::Add<Vector> for Point {
} }
} }
impl std::ops::Sub<Vector> for Point { impl<T> std::ops::Sub<Vector<T>> for Point<T>
where
T: std::ops::Sub<Output = T>,
{
type Output = Self; type Output = Self;
fn sub(self, vector: Vector) -> Self { fn sub(self, vector: Vector<T>) -> Self {
Self { Self {
x: self.x - vector.x, x: self.x - vector.x,
y: self.y - vector.y, y: self.y - vector.y,
@ -68,10 +82,22 @@ impl std::ops::Sub<Vector> for Point {
} }
} }
impl std::ops::Sub<Point> for Point { impl<T> std::ops::Sub<Point<T>> for Point<T>
type Output = Vector; where
T: std::ops::Sub<Output = T>,
{
type Output = Vector<T>;
fn sub(self, point: Point) -> Vector { fn sub(self, point: Self) -> Vector<T> {
Vector::new(self.x - point.x, self.y - point.y) Vector::new(self.x - point.x, self.y - point.y)
} }
} }
impl<T> fmt::Display for Point<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}

View file

@ -67,7 +67,7 @@ impl Tree {
} }
} }
/// Reconciliates the children of the tree with the provided list of widgets. /// Reconciles the children of the tree with the provided list of widgets.
pub fn diff_children<'a, Message, Renderer>( pub fn diff_children<'a, Message, Renderer>(
&mut self, &mut self,
new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>], new_children: &[impl Borrow<dyn Widget<Message, Renderer> + 'a>],

View file

@ -1,15 +1,21 @@
//! Build window-based GUI applications. //! Build window-based GUI applications.
pub mod icon; pub mod icon;
pub mod settings;
mod event; mod event;
mod id;
mod level; mod level;
mod mode; mod mode;
mod position;
mod redraw_request; mod redraw_request;
mod user_attention; mod user_attention;
pub use event::Event; pub use event::Event;
pub use icon::Icon; pub use icon::Icon;
pub use id::Id;
pub use level::Level; pub use level::Level;
pub use mode::Mode; pub use mode::Mode;
pub use position::Position;
pub use redraw_request::RedrawRequest; pub use redraw_request::RedrawRequest;
pub use settings::Settings;
pub use user_attention::UserAttention; pub use user_attention::UserAttention;

View file

@ -1,10 +1,27 @@
use crate::time::Instant; use crate::time::Instant;
use crate::{Point, Size};
use std::path::PathBuf; use std::path::PathBuf;
/// A window-related event. /// A window-related event.
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub enum Event { pub enum Event {
/// A window was opened.
Opened {
/// The position of the opened window. This is relative to the top-left corner of the desktop
/// the window is on, including virtual desktops. Refers to window's "inner" position,
/// or the client area, in logical pixels.
///
/// **Note**: Not available in Wayland.
position: Option<Point>,
/// The size of the created window. This is its "inner" size, or the size of the
/// client area, in logical pixels.
size: Size,
},
/// A window was closed.
Closed,
/// A window was moved. /// A window was moved.
Moved { Moved {
/// The new logical x location of the window /// The new logical x location of the window
@ -27,9 +44,6 @@ pub enum Event {
RedrawRequested(Instant), RedrawRequested(Instant),
/// The user has requested for the window to close. /// The user has requested for the window to close.
///
/// Usually, you will want to terminate the execution whenever this event
/// occurs.
CloseRequested, CloseRequested,
/// A window was focused. /// A window was focused.

21
core/src/window/id.rs Normal file
View file

@ -0,0 +1,21 @@
use std::hash::Hash;
use std::sync::atomic::{self, AtomicU64};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
/// The id of the window.
///
/// Internally Iced reserves `window::Id::MAIN` for the first window spawned.
pub struct Id(u64);
static COUNT: AtomicU64 = AtomicU64::new(1);
impl Id {
/// The reserved window [`Id`] for the first window in an Iced application.
pub const MAIN: Self = Id(0);
/// Creates a new unique window [`Id`].
pub fn unique() -> Id {
Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
}
}

View file

@ -1,5 +1,7 @@
use crate::Point;
/// The position of a window in a given screen. /// The position of a window in a given screen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum Position { pub enum Position {
/// The platform-specific default position for a new window. /// The platform-specific default position for a new window.
Default, Default,
@ -12,7 +14,7 @@ pub enum Position {
/// position. So if you have decorations enabled and want the window to be /// position. So if you have decorations enabled and want the window to be
/// at (0, 0) you would have to set the position to /// at (0, 0) you would have to set the position to
/// `(PADDING_X, PADDING_Y)`. /// `(PADDING_X, PADDING_Y)`.
Specific(i32, i32), Specific(Point),
} }
impl Default for Position { impl Default for Position {

View file

@ -1,21 +1,47 @@
//! Configure your windows.
#[cfg(target_os = "windows")]
#[path = "settings/windows.rs"]
mod platform;
#[cfg(target_os = "macos")]
#[path = "settings/macos.rs"]
mod platform;
#[cfg(target_os = "linux")]
#[path = "settings/linux.rs"]
mod platform;
#[cfg(target_arch = "wasm32")]
#[path = "settings/wasm.rs"]
mod platform;
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_arch = "wasm32"
)))]
#[path = "settings/other.rs"]
mod platform;
use crate::window::{Icon, Level, Position}; use crate::window::{Icon, Level, Position};
use crate::Size;
pub use iced_winit::settings::PlatformSpecific; pub use platform::PlatformSpecific;
/// The window settings of an application. /// The window settings of an application.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Settings { pub struct Settings {
/// The initial size of the window. /// The initial logical dimensions of the window.
pub size: (u32, u32), pub size: Size,
/// The initial position of the window. /// The initial position of the window.
pub position: Position, pub position: Position,
/// The minimum size of the window. /// The minimum size of the window.
pub min_size: Option<(u32, u32)>, pub min_size: Option<Size>,
/// The maximum size of the window. /// The maximum size of the window.
pub max_size: Option<(u32, u32)>, pub max_size: Option<Size>,
/// Whether the window should be visible or not. /// Whether the window should be visible or not.
pub visible: bool, pub visible: bool,
@ -37,12 +63,22 @@ pub struct Settings {
/// Platform specific settings. /// Platform specific settings.
pub platform_specific: PlatformSpecific, pub platform_specific: PlatformSpecific,
/// Whether the window will close when the user requests it, e.g. when a user presses the
/// close button.
///
/// This can be useful if you want to have some behavior that executes before the window is
/// actually destroyed. If you disable this, you must manually close the window with the
/// `window::close` command.
///
/// By default this is enabled.
pub exit_on_close_request: bool,
} }
impl Default for Settings { impl Default for Settings {
fn default() -> Settings { fn default() -> Self {
Settings { Self {
size: (1024, 768), size: Size::new(1024.0, 768.0),
position: Position::default(), position: Position::default(),
min_size: None, min_size: None,
max_size: None, max_size: None,
@ -52,25 +88,8 @@ impl Default for Settings {
transparent: false, transparent: false,
level: Level::default(), level: Level::default(),
icon: None, icon: None,
exit_on_close_request: true,
platform_specific: PlatformSpecific::default(), platform_specific: PlatformSpecific::default(),
} }
} }
} }
impl From<Settings> for iced_winit::settings::Window {
fn from(settings: Settings) -> Self {
Self {
size: settings.size,
position: iced_winit::Position::from(settings.position),
min_size: settings.min_size,
max_size: settings.max_size,
visible: settings.visible,
resizable: settings.resizable,
decorations: settings.decorations,
transparent: settings.transparent,
level: settings.level,
icon: settings.icon.map(Icon::into),
platform_specific: settings.platform_specific,
}
}
}

View file

@ -10,7 +10,10 @@ use iced::{
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
Events::run(Settings { Events::run(Settings {
exit_on_close_request: false, window: window::Settings {
exit_on_close_request: false,
..window::Settings::default()
},
..Settings::default() ..Settings::default()
}) })
} }
@ -54,8 +57,9 @@ impl Application for Events {
Command::none() Command::none()
} }
Message::EventOccurred(event) => { Message::EventOccurred(event) => {
if let Event::Window(window::Event::CloseRequested) = event { if let Event::Window(id, window::Event::CloseRequested) = event
window::close() {
window::close(id)
} else { } else {
Command::none() Command::none()
} }
@ -65,7 +69,7 @@ impl Application for Events {
Command::none() Command::none()
} }
Message::Exit => window::close(), Message::Exit => window::close(window::Id::MAIN),
} }
} }

View file

@ -34,7 +34,7 @@ impl Application for Exit {
fn update(&mut self, message: Message) -> Command<Message> { fn update(&mut self, message: Message) -> Command<Message> {
match message { match message {
Message::Confirm => window::close(), Message::Confirm => window::close(window::Id::MAIN),
Message::Exit => { Message::Exit => {
self.show_confirm = true; self.show_confirm = true;

View file

@ -6,13 +6,17 @@ use scene::Scene;
use iced_wgpu::graphics::Viewport; use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Backend, Renderer, Settings}; use iced_wgpu::{wgpu, Backend, Renderer, Settings};
use iced_winit::conversion;
use iced_winit::core::mouse; use iced_winit::core::mouse;
use iced_winit::core::renderer; use iced_winit::core::renderer;
use iced_winit::core::window;
use iced_winit::core::{Color, Font, Pixels, Size}; use iced_winit::core::{Color, Font, Pixels, Size};
use iced_winit::futures;
use iced_winit::runtime::program; use iced_winit::runtime::program;
use iced_winit::runtime::Debug; use iced_winit::runtime::Debug;
use iced_winit::style::Theme; use iced_winit::style::Theme;
use iced_winit::{conversion, futures, winit, Clipboard}; use iced_winit::winit;
use iced_winit::Clipboard;
use winit::{ use winit::{
event::{Event, ModifiersState, WindowEvent}, event::{Event, ModifiersState, WindowEvent},
@ -180,6 +184,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// Map window event to iced event // Map window event to iced event
if let Some(event) = iced_winit::conversion::window_event( if let Some(event) = iced_winit::conversion::window_event(
window::Id::MAIN,
&event, &event,
window.scale_factor(), window.scale_factor(),
modifiers, modifiers,

View file

@ -279,7 +279,7 @@ where
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
if let Event::Window(window::Event::RedrawRequested(now)) = event { if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
state.animation = state.animation.timed_transition( state.animation = state.animation.timed_transition(
self.cycle_duration, self.cycle_duration,
self.rotation_duration, self.rotation_duration,

View file

@ -200,7 +200,7 @@ where
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
if let Event::Window(window::Event::RedrawRequested(now)) = event { if let Event::Window(_, window::Event::RedrawRequested(now)) = event {
*state = state.timed_transition(self.cycle_duration, now); *state = state.timed_transition(self.cycle_duration, now);
shell.request_redraw(RedrawRequest::At( shell.request_redraw(RedrawRequest::At(

View file

@ -0,0 +1,9 @@
[package]
name = "multi_window"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../..", features = ["debug", "multi-window"] }

View file

@ -0,0 +1,215 @@
use iced::event;
use iced::executor;
use iced::multi_window::{self, Application};
use iced::widget::{button, column, container, scrollable, text, text_input};
use iced::window;
use iced::{
Alignment, Command, Element, Length, Point, Settings, Subscription, Theme,
Vector,
};
use std::collections::HashMap;
fn main() -> iced::Result {
Example::run(Settings::default())
}
#[derive(Default)]
struct Example {
windows: HashMap<window::Id, Window>,
next_window_pos: window::Position,
}
#[derive(Debug)]
struct Window {
title: String,
scale_input: String,
current_scale: f64,
theme: Theme,
input_id: iced::widget::text_input::Id,
}
#[derive(Debug, Clone)]
enum Message {
ScaleInputChanged(window::Id, String),
ScaleChanged(window::Id, String),
TitleChanged(window::Id, String),
CloseWindow(window::Id),
WindowOpened(window::Id, Option<Point>),
WindowClosed(window::Id),
NewWindow,
}
impl multi_window::Application for Example {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Example {
windows: HashMap::from([(window::Id::MAIN, Window::new(1))]),
next_window_pos: window::Position::Default,
},
Command::none(),
)
}
fn title(&self, window: window::Id) -> String {
self.windows
.get(&window)
.map(|window| window.title.clone())
.unwrap_or("Example".to_string())
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::ScaleInputChanged(id, scale) => {
let window =
self.windows.get_mut(&id).expect("Window not found!");
window.scale_input = scale;
Command::none()
}
Message::ScaleChanged(id, scale) => {
let window =
self.windows.get_mut(&id).expect("Window not found!");
window.current_scale = scale
.parse::<f64>()
.unwrap_or(window.current_scale)
.clamp(0.5, 5.0);
Command::none()
}
Message::TitleChanged(id, title) => {
let window =
self.windows.get_mut(&id).expect("Window not found.");
window.title = title;
Command::none()
}
Message::CloseWindow(id) => window::close(id),
Message::WindowClosed(id) => {
self.windows.remove(&id);
Command::none()
}
Message::WindowOpened(id, position) => {
if let Some(position) = position {
self.next_window_pos = window::Position::Specific(
position + Vector::new(20.0, 20.0),
);
}
if let Some(window) = self.windows.get(&id) {
text_input::focus(window.input_id.clone())
} else {
Command::none()
}
}
Message::NewWindow => {
let count = self.windows.len() + 1;
let (id, spawn_window) = window::spawn(window::Settings {
position: self.next_window_pos,
exit_on_close_request: count % 2 == 0,
..Default::default()
});
self.windows.insert(id, Window::new(count));
spawn_window
}
}
}
fn view(&self, window: window::Id) -> Element<Message> {
let content = self.windows.get(&window).unwrap().view(window);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
fn theme(&self, window: window::Id) -> Self::Theme {
self.windows.get(&window).unwrap().theme.clone()
}
fn scale_factor(&self, window: window::Id) -> f64 {
self.windows
.get(&window)
.map(|window| window.current_scale)
.unwrap_or(1.0)
}
fn subscription(&self) -> Subscription<Self::Message> {
event::listen_with(|event, _| {
if let iced::Event::Window(id, window_event) = event {
match window_event {
window::Event::CloseRequested => {
Some(Message::CloseWindow(id))
}
window::Event::Opened { position, .. } => {
Some(Message::WindowOpened(id, position))
}
window::Event::Closed => Some(Message::WindowClosed(id)),
_ => None,
}
} else {
None
}
})
}
}
impl Window {
fn new(count: usize) -> Self {
Self {
title: format!("Window_{}", count),
scale_input: "1.0".to_string(),
current_scale: 1.0,
theme: if count % 2 == 0 {
Theme::Light
} else {
Theme::Dark
},
input_id: text_input::Id::unique(),
}
}
fn view(&self, id: window::Id) -> Element<Message> {
let scale_input = column![
text("Window scale factor:"),
text_input("Window Scale", &self.scale_input)
.on_input(move |msg| { Message::ScaleInputChanged(id, msg) })
.on_submit(Message::ScaleChanged(
id,
self.scale_input.to_string()
))
];
let title_input = column![
text("Window title:"),
text_input("Window Title", &self.title)
.on_input(move |msg| { Message::TitleChanged(id, msg) })
.id(self.input_id.clone())
];
let new_window_button =
button(text("New Window")).on_press(Message::NewWindow);
let content = scrollable(
column![scale_input, title_input, new_window_button]
.spacing(50)
.width(Length::Fill)
.align_items(Alignment::Center),
);
container(content).width(200).center_x().into()
}
}

View file

@ -1,8 +1,8 @@
use iced::alignment;
use iced::keyboard::KeyCode; use iced::keyboard::KeyCode;
use iced::theme::{Button, Container}; use iced::theme::{Button, Container};
use iced::widget::{button, column, container, image, row, text, text_input}; use iced::widget::{button, column, container, image, row, text, text_input};
use iced::window::screenshot::{self, Screenshot}; use iced::window::screenshot::{self, Screenshot};
use iced::{alignment, window};
use iced::{ use iced::{
event, executor, keyboard, Alignment, Application, Command, ContentFit, event, executor, keyboard, Alignment, Application, Command, ContentFit,
Element, Event, Length, Rectangle, Renderer, Subscription, Theme, Element, Event, Length, Rectangle, Renderer, Subscription, Theme,
@ -70,7 +70,10 @@ impl Application for Example {
fn update(&mut self, message: Self::Message) -> Command<Self::Message> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
match message { match message {
Message::Screenshot => { Message::Screenshot => {
return iced::window::screenshot(Message::ScreenshotData); return iced::window::screenshot(
window::Id::MAIN,
Message::ScreenshotData,
);
} }
Message::ScreenshotData(screenshot) => { Message::ScreenshotData(screenshot) => {
self.screenshot = Some(screenshot); self.screenshot = Some(screenshot);

View file

@ -114,14 +114,14 @@ impl State {
pub fn new() -> State { pub fn new() -> State {
let now = Instant::now(); let now = Instant::now();
let (width, height) = window::Settings::default().size; let size = window::Settings::default().size;
State { State {
space_cache: canvas::Cache::default(), space_cache: canvas::Cache::default(),
system_cache: canvas::Cache::default(), system_cache: canvas::Cache::default(),
start: now, start: now,
now, now,
stars: Self::generate_stars(width, height), stars: Self::generate_stars(size.width, size.height),
} }
} }
@ -130,7 +130,7 @@ impl State {
self.system_cache.clear(); self.system_cache.clear();
} }
fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { fn generate_stars(width: f32, height: f32) -> Vec<(Point, f32)> {
use rand::Rng; use rand::Rng;
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -139,12 +139,8 @@ impl State {
.map(|_| { .map(|_| {
( (
Point::new( Point::new(
rng.gen_range( rng.gen_range((-width / 2.0)..(width / 2.0)),
(-(width as f32) / 2.0)..(width as f32 / 2.0), rng.gen_range((-height / 2.0)..(height / 2.0)),
),
rng.gen_range(
(-(height as f32) / 2.0)..(height as f32 / 2.0),
),
), ),
rng.gen_range(0.5..1.0), rng.gen_range(0.5..1.0),
) )

View file

@ -539,7 +539,9 @@ mod toast {
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
) -> event::Status { ) -> event::Status {
if let Event::Window(window::Event::RedrawRequested(now)) = &event { if let Event::Window(_, window::Event::RedrawRequested(now)) =
&event
{
let mut next_redraw: Option<window::RedrawRequest> = None; let mut next_redraw: Option<window::RedrawRequest> = None;
self.instants.iter_mut().enumerate().for_each( self.instants.iter_mut().enumerate().for_each(

View file

@ -8,7 +8,7 @@ use iced::widget::{
}; };
use iced::window; use iced::window;
use iced::{Application, Element}; use iced::{Application, Element};
use iced::{Color, Command, Length, Settings, Subscription}; use iced::{Color, Command, Length, Settings, Size, Subscription};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -22,7 +22,7 @@ pub fn main() -> iced::Result {
Todos::run(Settings { Todos::run(Settings {
window: window::Settings { window: window::Settings {
size: (500, 800), size: Size::new(500.0, 800.0),
..window::Settings::default() ..window::Settings::default()
}, },
..Settings::default() ..Settings::default()
@ -54,7 +54,7 @@ enum Message {
FilterChanged(Filter), FilterChanged(Filter),
TaskMessage(usize, TaskMessage), TaskMessage(usize, TaskMessage),
TabPressed { shift: bool }, TabPressed { shift: bool },
ChangeWindowMode(window::Mode), ToggleFullscreen(window::Mode),
} }
impl Application for Todos { impl Application for Todos {
@ -165,8 +165,8 @@ impl Application for Todos {
widget::focus_next() widget::focus_next()
} }
} }
Message::ChangeWindowMode(mode) => { Message::ToggleFullscreen(mode) => {
window::change_mode(mode) window::change_mode(window::Id::MAIN, mode)
} }
_ => Command::none(), _ => Command::none(),
}; };
@ -272,10 +272,10 @@ impl Application for Todos {
shift: modifiers.shift(), shift: modifiers.shift(),
}), }),
(keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => { (keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => {
Some(Message::ChangeWindowMode(window::Mode::Fullscreen)) Some(Message::ToggleFullscreen(window::Mode::Fullscreen))
} }
(keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => { (keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => {
Some(Message::ChangeWindowMode(window::Mode::Windowed)) Some(Message::ToggleFullscreen(window::Mode::Windowed))
} }
_ => None, _ => None,
} }

View file

@ -167,7 +167,7 @@ impl Application for Example {
Event::Mouse(mouse::Event::CursorMoved { position }) => { Event::Mouse(mouse::Event::CursorMoved { position }) => {
Some(Message::MouseMoved(position)) Some(Message::MouseMoved(position))
} }
Event::Window(window::Event::Resized { .. }) => { Event::Window(_, window::Event::Resized { .. }) => {
Some(Message::WindowResized) Some(Message::WindowResized)
} }
_ => None, _ => None,

View file

@ -35,7 +35,7 @@ where
subscription::filter_map( subscription::filter_map(
(EventsWith, f), (EventsWith, f),
move |event, status| match event { move |event, status| match event {
Event::Window(window::Event::RedrawRequested(_)) => None, Event::Window(_, window::Event::RedrawRequested(_)) => None,
_ => f(event, status), _ => f(event, status),
}, },
) )

View file

@ -22,7 +22,10 @@ pub trait Compositor: Sized {
fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Self::Settings, settings: Self::Settings,
compatible_window: Option<&W>, compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error>; ) -> Result<Self, Error>;
/// Creates a [`Self::Renderer`] for the [`Compositor`].
fn create_renderer(&self) -> Self::Renderer;
/// Crates a new [`Surface`] for the given window. /// Crates a new [`Surface`] for the given window.
/// ///

View file

@ -26,7 +26,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Self::Settings, settings: Self::Settings,
compatible_window: Option<&W>, compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> { ) -> Result<Self, Error> {
let candidates = let candidates =
Candidate::list_from_env().unwrap_or(Candidate::default_list()); Candidate::list_from_env().unwrap_or(Candidate::default_list());
@ -34,9 +34,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
for candidate in candidates { for candidate in candidates {
match candidate.build(settings, compatible_window) { match candidate.build(settings, compatible_window) {
Ok((compositor, renderer)) => { Ok(compositor) => return Ok(compositor),
return Ok((compositor, renderer))
}
Err(new_error) => { Err(new_error) => {
error = new_error; error = new_error;
} }
@ -46,6 +44,18 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
Err(error) Err(error)
} }
fn create_renderer(&self) -> Self::Renderer {
match self {
Compositor::TinySkia(compositor) => {
Renderer::TinySkia(compositor.create_renderer())
}
#[cfg(feature = "wgpu")]
Compositor::Wgpu(compositor) => {
Renderer::Wgpu(compositor.create_renderer())
}
}
}
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
&mut self, &mut self,
window: &W, window: &W,
@ -220,24 +230,21 @@ impl Candidate {
self, self,
settings: Settings, settings: Settings,
_compatible_window: Option<&W>, _compatible_window: Option<&W>,
) -> Result<(Compositor<Theme>, Renderer<Theme>), Error> { ) -> Result<Compositor<Theme>, Error> {
match self { match self {
Self::TinySkia => { Self::TinySkia => {
let (compositor, backend) = let compositor = iced_tiny_skia::window::compositor::new(
iced_tiny_skia::window::compositor::new(); iced_tiny_skia::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
},
);
Ok(( Ok(Compositor::TinySkia(compositor))
Compositor::TinySkia(compositor),
Renderer::TinySkia(iced_tiny_skia::Renderer::new(
backend,
settings.default_font,
settings.default_text_size,
)),
))
} }
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
Self::Wgpu => { Self::Wgpu => {
let (compositor, backend) = iced_wgpu::window::compositor::new( let compositor = iced_wgpu::window::compositor::new(
iced_wgpu::Settings { iced_wgpu::Settings {
default_font: settings.default_font, default_font: settings.default_font,
default_text_size: settings.default_text_size, default_text_size: settings.default_text_size,
@ -247,14 +254,7 @@ impl Candidate {
_compatible_window, _compatible_window,
)?; )?;
Ok(( Ok(Compositor::Wgpu(compositor))
Compositor::Wgpu(compositor),
Renderer::Wgpu(iced_wgpu::Renderer::new(
backend,
settings.default_font,
settings.default_text_size,
)),
))
} }
#[cfg(not(feature = "wgpu"))] #[cfg(not(feature = "wgpu"))]
Self::Wgpu => { Self::Wgpu => {

View file

@ -12,6 +12,7 @@ keywords.workspace = true
[features] [features]
debug = [] debug = []
multi-window = []
[dependencies] [dependencies]
iced_core.workspace = true iced_core.workspace = true

View file

@ -84,7 +84,9 @@ impl<T> fmt::Debug for Action<T> {
Self::Clipboard(action) => { Self::Clipboard(action) => {
write!(f, "Action::Clipboard({action:?})") write!(f, "Action::Clipboard({action:?})")
} }
Self::Window(action) => write!(f, "Action::Window({action:?})"), Self::Window(action) => {
write!(f, "Action::Window({action:?})")
}
Self::System(action) => write!(f, "Action::System({action:?})"), Self::System(action) => write!(f, "Action::System({action:?})"),
Self::Widget(_action) => write!(f, "Action::Widget"), Self::Widget(_action) => write!(f, "Action::Widget"),
Self::LoadFont { .. } => write!(f, "Action::LoadFont"), Self::LoadFont { .. } => write!(f, "Action::LoadFont"),

View file

@ -26,6 +26,9 @@ pub mod system;
pub mod user_interface; pub mod user_interface;
pub mod window; pub mod window;
#[cfg(feature = "multi-window")]
pub mod multi_window;
// We disable debug capabilities on release builds unless the `debug` feature // We disable debug capabilities on release builds unless the `debug` feature
// is explicitly enabled. // is explicitly enabled.
#[cfg(feature = "debug")] #[cfg(feature = "debug")]

View file

@ -0,0 +1,6 @@
//! A multi-window application.
pub mod program;
pub mod state;
pub use program::Program;
pub use state::State;

View file

@ -0,0 +1,32 @@
//! Build interactive programs using The Elm Architecture.
use crate::core::text;
use crate::core::window;
use crate::core::{Element, Renderer};
use crate::Command;
/// The core of a user interface for a multi-window application following The Elm Architecture.
pub trait Program: Sized {
/// The graphics backend to use to draw the [`Program`].
type Renderer: Renderer + text::Renderer;
/// The type of __messages__ your [`Program`] will produce.
type Message: std::fmt::Debug + Send;
/// Handles a __message__ and updates the state of the [`Program`].
///
/// This is where you define your __update logic__. All the __messages__,
/// produced by either user interactions or commands, will be handled by
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the
/// background by shells.
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// Returns the widgets to display in the [`Program`] for the `window`.
///
/// These widgets can produce __messages__ based on user interaction.
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, Self::Renderer>;
}

View file

@ -0,0 +1,280 @@
//! The internal state of a multi-window [`Program`].
use crate::core::event::{self, Event};
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::operation::{self, Operation};
use crate::core::{Clipboard, Size};
use crate::user_interface::{self, UserInterface};
use crate::{Command, Debug, Program};
/// The execution state of a multi-window [`Program`]. It leverages caching, event
/// processing, and rendering primitive storage.
#[allow(missing_debug_implementations)]
pub struct State<P>
where
P: Program + 'static,
{
program: P,
caches: Option<Vec<user_interface::Cache>>,
queued_events: Vec<Event>,
queued_messages: Vec<P::Message>,
mouse_interaction: mouse::Interaction,
}
impl<P> State<P>
where
P: Program + 'static,
{
/// Creates a new [`State`] with the provided [`Program`], initializing its
/// primitive with the given logical bounds and renderer.
pub fn new(
program: P,
bounds: Size,
renderer: &mut P::Renderer,
debug: &mut Debug,
) -> Self {
let user_interface = build_user_interface(
&program,
user_interface::Cache::default(),
renderer,
bounds,
debug,
);
let caches = Some(vec![user_interface.into_cache()]);
State {
program,
caches,
queued_events: Vec::new(),
queued_messages: Vec::new(),
mouse_interaction: mouse::Interaction::Idle,
}
}
/// Returns a reference to the [`Program`] of the [`State`].
pub fn program(&self) -> &P {
&self.program
}
/// Queues an event in the [`State`] for processing during an [`update`].
///
/// [`update`]: Self::update
pub fn queue_event(&mut self, event: Event) {
self.queued_events.push(event);
}
/// Queues a message in the [`State`] for processing during an [`update`].
///
/// [`update`]: Self::update
pub fn queue_message(&mut self, message: P::Message) {
self.queued_messages.push(message);
}
/// Returns whether the event queue of the [`State`] is empty or not.
pub fn is_queue_empty(&self) -> bool {
self.queued_events.is_empty() && self.queued_messages.is_empty()
}
/// Returns the current [`mouse::Interaction`] of the [`State`].
pub fn mouse_interaction(&self) -> mouse::Interaction {
self.mouse_interaction
}
/// Processes all the queued events and messages, rebuilding and redrawing
/// the widgets of the linked [`Program`] if necessary.
///
/// Returns a list containing the instances of [`Event`] that were not
/// captured by any widget, and the [`Command`] obtained from [`Program`]
/// after updating it, only if an update was necessary.
pub fn update(
&mut self,
bounds: Size,
cursor: mouse::Cursor,
renderer: &mut P::Renderer,
theme: &<P::Renderer as iced_core::Renderer>::Theme,
style: &renderer::Style,
clipboard: &mut dyn Clipboard,
debug: &mut Debug,
) -> (Vec<Event>, Option<Command<P::Message>>) {
let mut user_interfaces = build_user_interfaces(
&self.program,
self.caches.take().unwrap(),
renderer,
bounds,
debug,
);
debug.event_processing_started();
let mut messages = Vec::new();
let uncaptured_events = user_interfaces.iter_mut().fold(
vec![],
|mut uncaptured_events, ui| {
let (_, event_statuses) = ui.update(
&self.queued_events,
cursor,
renderer,
clipboard,
&mut messages,
);
uncaptured_events.extend(
self.queued_events
.iter()
.zip(event_statuses)
.filter_map(|(event, status)| {
matches!(status, event::Status::Ignored)
.then_some(event)
})
.cloned(),
);
uncaptured_events
},
);
self.queued_events.clear();
messages.append(&mut self.queued_messages);
debug.event_processing_finished();
let commands = if messages.is_empty() {
debug.draw_started();
for ui in &mut user_interfaces {
self.mouse_interaction =
ui.draw(renderer, theme, style, cursor);
}
debug.draw_finished();
self.caches = Some(
user_interfaces
.drain(..)
.map(UserInterface::into_cache)
.collect(),
);
None
} else {
let temp_caches = user_interfaces
.drain(..)
.map(UserInterface::into_cache)
.collect();
drop(user_interfaces);
let commands = Command::batch(messages.into_iter().map(|msg| {
debug.log_message(&msg);
debug.update_started();
let command = self.program.update(msg);
debug.update_finished();
command
}));
let mut user_interfaces = build_user_interfaces(
&self.program,
temp_caches,
renderer,
bounds,
debug,
);
debug.draw_started();
for ui in &mut user_interfaces {
self.mouse_interaction =
ui.draw(renderer, theme, style, cursor);
}
debug.draw_finished();
self.caches = Some(
user_interfaces
.drain(..)
.map(UserInterface::into_cache)
.collect(),
);
Some(commands)
};
(uncaptured_events, commands)
}
/// Applies widget [`Operation`]s to the [`State`].
pub fn operate(
&mut self,
renderer: &mut P::Renderer,
operations: impl Iterator<Item = Box<dyn Operation<P::Message>>>,
bounds: Size,
debug: &mut Debug,
) {
let mut user_interfaces = build_user_interfaces(
&self.program,
self.caches.take().unwrap(),
renderer,
bounds,
debug,
);
for operation in operations {
let mut current_operation = Some(operation);
while let Some(mut operation) = current_operation.take() {
for ui in &mut user_interfaces {
ui.operate(renderer, operation.as_mut());
}
match operation.finish() {
operation::Outcome::None => {}
operation::Outcome::Some(message) => {
self.queued_messages.push(message);
}
operation::Outcome::Chain(next) => {
current_operation = Some(next);
}
};
}
}
self.caches = Some(
user_interfaces
.drain(..)
.map(UserInterface::into_cache)
.collect(),
);
}
}
fn build_user_interfaces<'a, P: Program>(
program: &'a P,
mut caches: Vec<user_interface::Cache>,
renderer: &mut P::Renderer,
size: Size,
debug: &mut Debug,
) -> Vec<UserInterface<'a, P::Message, P::Renderer>> {
caches
.drain(..)
.map(|cache| {
build_user_interface(program, cache, renderer, size, debug)
})
.collect()
}
fn build_user_interface<'a, P: Program>(
program: &'a P,
cache: user_interface::Cache,
renderer: &mut P::Renderer,
size: Size,
debug: &mut Debug,
) -> UserInterface<'a, P::Message, P::Renderer> {
debug.view_started();
let view = program.view();
debug.view_finished();
debug.layout_started();
let user_interface = UserInterface::build(view, size, cache, renderer);
debug.layout_finished();
user_interface
}

View file

@ -8,95 +8,113 @@ pub use screenshot::Screenshot;
use crate::command::{self, Command}; use crate::command::{self, Command};
use crate::core::time::Instant; use crate::core::time::Instant;
use crate::core::window::{Event, Icon, Level, Mode, UserAttention}; use crate::core::window::{
use crate::core::Size; Event, Icon, Id, Level, Mode, Settings, UserAttention,
};
use crate::core::{Point, Size};
use crate::futures::event; use crate::futures::event;
use crate::futures::Subscription; use crate::futures::Subscription;
/// Subscribes to the frames of the window of the running application. /// Subscribes to the frames of the window of the running application.
/// ///
/// The resulting [`Subscription`] will produce items at a rate equal to the /// The resulting [`Subscription`] will produce items at a rate equal to the
/// refresh rate of the window. Note that this rate may be variable, as it is /// refresh rate of the first application window. Note that this rate may be variable, as it is
/// normally managed by the graphics driver and/or the OS. /// normally managed by the graphics driver and/or the OS.
/// ///
/// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// In any case, this [`Subscription`] is useful to smoothly draw application-driven
/// animations without missing any frames. /// animations without missing any frames.
pub fn frames() -> Subscription<Instant> { pub fn frames() -> Subscription<Instant> {
event::listen_raw(|event, _status| match event { event::listen_raw(|event, _status| match event {
iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at), crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at),
_ => None, _ => None,
}) })
} }
/// Closes the current window and exits the application. /// Spawns a new window with the given `settings`.
pub fn close<Message>() -> Command<Message> { ///
Command::single(command::Action::Window(Action::Close)) /// Returns the new window [`Id`] alongside the [`Command`].
pub fn spawn<Message>(settings: Settings) -> (Id, Command<Message>) {
let id = Id::unique();
(
id,
Command::single(command::Action::Window(Action::Spawn(id, settings))),
)
}
/// Closes the window with `id`.
pub fn close<Message>(id: Id) -> Command<Message> {
Command::single(command::Action::Window(Action::Close(id)))
} }
/// Begins dragging the window while the left mouse button is held. /// Begins dragging the window while the left mouse button is held.
pub fn drag<Message>() -> Command<Message> { pub fn drag<Message>(id: Id) -> Command<Message> {
Command::single(command::Action::Window(Action::Drag)) Command::single(command::Action::Window(Action::Drag(id)))
} }
/// Resizes the window to the given logical dimensions. /// Resizes the window to the given logical dimensions.
pub fn resize<Message>(new_size: Size<u32>) -> Command<Message> { pub fn resize<Message>(id: Id, new_size: Size) -> Command<Message> {
Command::single(command::Action::Window(Action::Resize(new_size))) Command::single(command::Action::Window(Action::Resize(id, new_size)))
} }
/// Fetches the current window size in logical dimensions. /// Fetches the window's size in logical dimensions.
pub fn fetch_size<Message>( pub fn fetch_size<Message>(
f: impl FnOnce(Size<u32>) -> Message + 'static, id: Id,
f: impl FnOnce(Size) -> Message + 'static,
) -> Command<Message> { ) -> Command<Message> {
Command::single(command::Action::Window(Action::FetchSize(Box::new(f)))) Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f))))
} }
/// Maximizes the window. /// Maximizes the window.
pub fn maximize<Message>(maximized: bool) -> Command<Message> { pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> {
Command::single(command::Action::Window(Action::Maximize(maximized))) Command::single(command::Action::Window(Action::Maximize(id, maximized)))
} }
/// Minimes the window. /// Minimizes the window.
pub fn minimize<Message>(minimized: bool) -> Command<Message> { pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> {
Command::single(command::Action::Window(Action::Minimize(minimized))) Command::single(command::Action::Window(Action::Minimize(id, minimized)))
} }
/// Moves a window to the given logical coordinates. /// Moves the window to the given logical coordinates.
pub fn move_to<Message>(x: i32, y: i32) -> Command<Message> { pub fn move_to<Message>(id: Id, position: Point) -> Command<Message> {
Command::single(command::Action::Window(Action::Move { x, y })) Command::single(command::Action::Window(Action::Move(id, position)))
} }
/// Changes the [`Mode`] of the window. /// Changes the [`Mode`] of the window.
pub fn change_mode<Message>(mode: Mode) -> Command<Message> { pub fn change_mode<Message>(id: Id, mode: Mode) -> Command<Message> {
Command::single(command::Action::Window(Action::ChangeMode(mode))) Command::single(command::Action::Window(Action::ChangeMode(id, mode)))
} }
/// Fetches the current [`Mode`] of the window. /// Fetches the current [`Mode`] of the window.
pub fn fetch_mode<Message>( pub fn fetch_mode<Message>(
id: Id,
f: impl FnOnce(Mode) -> Message + 'static, f: impl FnOnce(Mode) -> Message + 'static,
) -> Command<Message> { ) -> Command<Message> {
Command::single(command::Action::Window(Action::FetchMode(Box::new(f)))) Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f))))
} }
/// Toggles the window to maximized or back. /// Toggles the window to maximized or back.
pub fn toggle_maximize<Message>() -> Command<Message> { pub fn toggle_maximize<Message>(id: Id) -> Command<Message> {
Command::single(command::Action::Window(Action::ToggleMaximize)) Command::single(command::Action::Window(Action::ToggleMaximize(id)))
} }
/// Toggles the window decorations. /// Toggles the window decorations.
pub fn toggle_decorations<Message>() -> Command<Message> { pub fn toggle_decorations<Message>(id: Id) -> Command<Message> {
Command::single(command::Action::Window(Action::ToggleDecorations)) Command::single(command::Action::Window(Action::ToggleDecorations(id)))
} }
/// Request user attention to the window, this has no effect if the application /// Request user attention to the window. This has no effect if the application
/// is already focused. How requesting for user attention manifests is platform dependent, /// is already focused. How requesting for user attention manifests is platform dependent,
/// see [`UserAttention`] for details. /// see [`UserAttention`] for details.
/// ///
/// Providing `None` will unset the request for user attention. Unsetting the request for /// Providing `None` will unset the request for user attention. Unsetting the request for
/// user attention might not be done automatically by the WM when the window receives input. /// user attention might not be done automatically by the WM when the window receives input.
pub fn request_user_attention<Message>( pub fn request_user_attention<Message>(
id: Id,
user_attention: Option<UserAttention>, user_attention: Option<UserAttention>,
) -> Command<Message> { ) -> Command<Message> {
Command::single(command::Action::Window(Action::RequestUserAttention( Command::single(command::Action::Window(Action::RequestUserAttention(
id,
user_attention, user_attention,
))) )))
} }
@ -107,30 +125,36 @@ pub fn request_user_attention<Message>(
/// This [`Command`] steals input focus from other applications. Do not use this method unless /// This [`Command`] steals input focus from other applications. Do not use this method unless
/// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive
/// user experience. /// user experience.
pub fn gain_focus<Message>() -> Command<Message> { pub fn gain_focus<Message>(id: Id) -> Command<Message> {
Command::single(command::Action::Window(Action::GainFocus)) Command::single(command::Action::Window(Action::GainFocus(id)))
} }
/// Changes the window [`Level`]. /// Changes the window [`Level`].
pub fn change_level<Message>(level: Level) -> Command<Message> { pub fn change_level<Message>(id: Id, level: Level) -> Command<Message> {
Command::single(command::Action::Window(Action::ChangeLevel(level))) Command::single(command::Action::Window(Action::ChangeLevel(id, level)))
} }
/// Fetches an identifier unique to the window. /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is
/// not to be confused with [`Id`].
pub fn fetch_id<Message>( pub fn fetch_id<Message>(
id: Id,
f: impl FnOnce(u64) -> Message + 'static, f: impl FnOnce(u64) -> Message + 'static,
) -> Command<Message> { ) -> Command<Message> {
Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) Command::single(command::Action::Window(Action::FetchId(id, Box::new(f))))
} }
/// Changes the [`Icon`] of the window. /// Changes the [`Icon`] of the window.
pub fn change_icon<Message>(icon: Icon) -> Command<Message> { pub fn change_icon<Message>(id: Id, icon: Icon) -> Command<Message> {
Command::single(command::Action::Window(Action::ChangeIcon(icon))) Command::single(command::Action::Window(Action::ChangeIcon(id, icon)))
} }
/// Captures a [`Screenshot`] from the window. /// Captures a [`Screenshot`] from the window.
pub fn screenshot<Message>( pub fn screenshot<Message>(
id: Id,
f: impl FnOnce(Screenshot) -> Message + Send + 'static, f: impl FnOnce(Screenshot) -> Message + Send + 'static,
) -> Command<Message> { ) -> Command<Message> {
Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) Command::single(command::Action::Window(Action::Screenshot(
id,
Box::new(f),
)))
} }

View file

@ -1,5 +1,5 @@
use crate::core::window::{Icon, Level, Mode, UserAttention}; use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention};
use crate::core::Size; use crate::core::{Point, Size};
use crate::futures::MaybeSend; use crate::futures::MaybeSend;
use crate::window::Screenshot; use crate::window::Screenshot;
@ -7,43 +7,40 @@ use std::fmt;
/// An operation to be performed on some window. /// An operation to be performed on some window.
pub enum Action<T> { pub enum Action<T> {
/// Close the current window and exits the application. /// Spawns a new window with some [`Settings`].
Close, Spawn(Id, Settings),
/// Close the window and exits the application.
Close(Id),
/// Move the window with the left mouse button until the button is /// Move the window with the left mouse button until the button is
/// released. /// released.
/// ///
/// Theres no guarantee that this will work unless the left mouse /// Theres no guarantee that this will work unless the left mouse
/// button was pressed immediately before this function is called. /// button was pressed immediately before this function is called.
Drag, Drag(Id),
/// Resize the window. /// Resize the window to the given logical dimensions.
Resize(Size<u32>), Resize(Id, Size),
/// Fetch the current size of the window. /// Fetch the current logical dimensions of the window.
FetchSize(Box<dyn FnOnce(Size<u32>) -> T + 'static>), FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>),
/// Set the window to maximized or back /// Set the window to maximized or back
Maximize(bool), Maximize(Id, bool),
/// Set the window to minimized or back /// Set the window to minimized or back
Minimize(bool), Minimize(Id, bool),
/// Move the window. /// Move the window to the given logical coordinates.
/// ///
/// Unsupported on Wayland. /// Unsupported on Wayland.
Move { Move(Id, Point),
/// The new logical x location of the window
x: i32,
/// The new logical y location of the window
y: i32,
},
/// Change the [`Mode`] of the window. /// Change the [`Mode`] of the window.
ChangeMode(Mode), ChangeMode(Id, Mode),
/// Fetch the current [`Mode`] of the window. /// Fetch the current [`Mode`] of the window.
FetchMode(Box<dyn FnOnce(Mode) -> T + 'static>), FetchMode(Id, Box<dyn FnOnce(Mode) -> T + 'static>),
/// Toggle the window to maximized or back /// Toggle the window to maximized or back
ToggleMaximize, ToggleMaximize(Id),
/// Toggle whether window has decorations. /// Toggle whether window has decorations.
/// ///
/// ## Platform-specific /// ## Platform-specific
/// - **X11:** Not implemented. /// - **X11:** Not implemented.
/// - **Web:** Unsupported. /// - **Web:** Unsupported.
ToggleDecorations, ToggleDecorations(Id),
/// Request user attention to the window, this has no effect if the application /// Request user attention to the window, this has no effect if the application
/// is already focused. How requesting for user attention manifests is platform dependent, /// is already focused. How requesting for user attention manifests is platform dependent,
/// see [`UserAttention`] for details. /// see [`UserAttention`] for details.
@ -57,7 +54,7 @@ pub enum Action<T> {
/// - **macOS:** `None` has no effect. /// - **macOS:** `None` has no effect.
/// - **X11:** Requests for user attention must be manually cleared. /// - **X11:** Requests for user attention must be manually cleared.
/// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect.
RequestUserAttention(Option<UserAttention>), RequestUserAttention(Id, Option<UserAttention>),
/// Bring the window to the front and sets input focus. Has no effect if the window is /// Bring the window to the front and sets input focus. Has no effect if the window is
/// already in focus, minimized, or not visible. /// already in focus, minimized, or not visible.
/// ///
@ -68,11 +65,11 @@ pub enum Action<T> {
/// ## Platform-specific /// ## Platform-specific
/// ///
/// - **Web / Wayland:** Unsupported. /// - **Web / Wayland:** Unsupported.
GainFocus, GainFocus(Id),
/// Change the window [`Level`]. /// Change the window [`Level`].
ChangeLevel(Level), ChangeLevel(Id, Level),
/// Fetch an identifier unique to the window. /// Fetch the raw identifier unique to the window.
FetchId(Box<dyn FnOnce(u64) -> T + 'static>), FetchId(Id, Box<dyn FnOnce(u64) -> T + 'static>),
/// Change the window [`Icon`]. /// Change the window [`Icon`].
/// ///
/// On Windows and X11, this is typically the small icon in the top-left /// On Windows and X11, this is typically the small icon in the top-left
@ -87,9 +84,9 @@ pub enum Action<T> {
/// ///
/// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That
/// said, it's usually in the same ballpark as on Windows. /// said, it's usually in the same ballpark as on Windows.
ChangeIcon(Icon), ChangeIcon(Id, Icon),
/// Screenshot the viewport of the window. /// Screenshot the viewport of the window.
Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>), Screenshot(Id, Box<dyn FnOnce(Screenshot) -> T + 'static>),
} }
impl<T> Action<T> { impl<T> Action<T> {
@ -102,29 +99,35 @@ impl<T> Action<T> {
T: 'static, T: 'static,
{ {
match self { match self {
Self::Close => Action::Close, Self::Spawn(id, settings) => Action::Spawn(id, settings),
Self::Drag => Action::Drag, Self::Close(id) => Action::Close(id),
Self::Resize(size) => Action::Resize(size), Self::Drag(id) => Action::Drag(id),
Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))), Self::Resize(id, size) => Action::Resize(id, size),
Self::Maximize(maximized) => Action::Maximize(maximized), Self::FetchSize(id, o) => {
Self::Minimize(minimized) => Action::Minimize(minimized), Action::FetchSize(id, Box::new(move |s| f(o(s))))
Self::Move { x, y } => Action::Move { x, y },
Self::ChangeMode(mode) => Action::ChangeMode(mode),
Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))),
Self::ToggleMaximize => Action::ToggleMaximize,
Self::ToggleDecorations => Action::ToggleDecorations,
Self::RequestUserAttention(attention_type) => {
Action::RequestUserAttention(attention_type)
} }
Self::GainFocus => Action::GainFocus, Self::Maximize(id, maximized) => Action::Maximize(id, maximized),
Self::ChangeLevel(level) => Action::ChangeLevel(level), Self::Minimize(id, minimized) => Action::Minimize(id, minimized),
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), Self::Move(id, position) => Action::Move(id, position),
Self::ChangeIcon(icon) => Action::ChangeIcon(icon), Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode),
Self::Screenshot(tag) => { Self::FetchMode(id, o) => {
Action::Screenshot(Box::new(move |screenshot| { Action::FetchMode(id, Box::new(move |s| f(o(s))))
f(tag(screenshot))
}))
} }
Self::ToggleMaximize(id) => Action::ToggleMaximize(id),
Self::ToggleDecorations(id) => Action::ToggleDecorations(id),
Self::RequestUserAttention(id, attention_type) => {
Action::RequestUserAttention(id, attention_type)
}
Self::GainFocus(id) => Action::GainFocus(id),
Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level),
Self::FetchId(id, o) => {
Action::FetchId(id, Box::new(move |s| f(o(s))))
}
Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon),
Self::Screenshot(id, tag) => Action::Screenshot(
id,
Box::new(move |screenshot| f(tag(screenshot))),
),
} }
} }
} }
@ -132,35 +135,46 @@ impl<T> Action<T> {
impl<T> fmt::Debug for Action<T> { impl<T> fmt::Debug for Action<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Close => write!(f, "Action::Close"), Self::Spawn(id, settings) => {
Self::Drag => write!(f, "Action::Drag"), write!(f, "Action::Spawn({id:?}, {settings:?})")
Self::Resize(size) => write!(f, "Action::Resize({size:?})"),
Self::FetchSize(_) => write!(f, "Action::FetchSize"),
Self::Maximize(maximized) => {
write!(f, "Action::Maximize({maximized})")
} }
Self::Minimize(minimized) => { Self::Close(id) => write!(f, "Action::Close({id:?})"),
write!(f, "Action::Minimize({minimized}") Self::Drag(id) => write!(f, "Action::Drag({id:?})"),
Self::Resize(id, size) => {
write!(f, "Action::Resize({id:?}, {size:?})")
} }
Self::Move { x, y } => { Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"),
write!(f, "Action::Move {{ x: {x}, y: {y} }}") Self::Maximize(id, maximized) => {
write!(f, "Action::Maximize({id:?}, {maximized})")
} }
Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"), Self::Minimize(id, minimized) => {
Self::FetchMode(_) => write!(f, "Action::FetchMode"), write!(f, "Action::Minimize({id:?}, {minimized}")
Self::ToggleMaximize => write!(f, "Action::ToggleMaximize"),
Self::ToggleDecorations => write!(f, "Action::ToggleDecorations"),
Self::RequestUserAttention(_) => {
write!(f, "Action::RequestUserAttention")
} }
Self::GainFocus => write!(f, "Action::GainFocus"), Self::Move(id, position) => {
Self::ChangeLevel(level) => { write!(f, "Action::Move({id:?}, {position})")
write!(f, "Action::ChangeLevel({level:?})")
} }
Self::FetchId(_) => write!(f, "Action::FetchId"), Self::ChangeMode(id, mode) => {
Self::ChangeIcon(_icon) => { write!(f, "Action::SetMode({id:?}, {mode:?})")
write!(f, "Action::ChangeIcon(icon)")
} }
Self::Screenshot(_) => write!(f, "Action::Screenshot"), Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"),
Self::ToggleMaximize(id) => {
write!(f, "Action::ToggleMaximize({id:?})")
}
Self::ToggleDecorations(id) => {
write!(f, "Action::ToggleDecorations({id:?})")
}
Self::RequestUserAttention(id, _) => {
write!(f, "Action::RequestUserAttention({id:?})")
}
Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"),
Self::ChangeLevel(id, level) => {
write!(f, "Action::ChangeLevel({id:?}, {level:?})")
}
Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"),
Self::ChangeIcon(id, _icon) => {
write!(f, "Action::ChangeIcon({id:?})")
}
Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"),
} }
} }
} }

View file

@ -182,6 +182,9 @@ pub mod window;
#[cfg(feature = "advanced")] #[cfg(feature = "advanced")]
pub mod advanced; pub mod advanced;
#[cfg(feature = "multi-window")]
pub mod multi_window;
pub use style::theme; pub use style::theme;
pub use crate::core::alignment; pub use crate::core::alignment;

4
src/multi_window.rs Normal file
View file

@ -0,0 +1,4 @@
//! Leverage multi-window support in your application.
mod application;
pub use application::Application;

View file

@ -0,0 +1,245 @@
use crate::style::application::StyleSheet;
use crate::window;
use crate::{Command, Element, Executor, Settings, Subscription};
/// An interactive cross-platform multi-window application.
///
/// This trait is the main entrypoint of Iced. Once implemented, you can run
/// your GUI application by simply calling [`run`](#method.run).
///
/// - On native platforms, it will run in its own windows.
/// - On the web, it will take control of the `<title>` and the `<body>` of the
/// document and display only the contents of the `window::Id::MAIN` window.
///
/// An [`Application`] can execute asynchronous actions by returning a
/// [`Command`] in some of its methods. If you do not intend to perform any
/// background work in your program, the [`Sandbox`] trait offers a simplified
/// interface.
///
/// When using an [`Application`] with the `debug` feature enabled, a debug view
/// can be toggled by pressing `F12`.
///
/// # Examples
/// See the `examples/multi-window` example to see this multi-window `Application` trait in action.
///
/// ## A simple "Hello, world!"
///
/// If you just want to get started, here is a simple [`Application`] that
/// says "Hello, world!":
///
/// ```no_run
/// use iced::{executor, window};
/// use iced::{Command, Element, Settings, Theme};
/// use iced::multi_window::{self, Application};
///
/// pub fn main() -> iced::Result {
/// Hello::run(Settings::default())
/// }
///
/// struct Hello;
///
/// impl multi_window::Application for Hello {
/// type Executor = executor::Default;
/// type Flags = ();
/// type Message = ();
/// type Theme = Theme;
///
/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
/// (Hello, Command::none())
/// }
///
/// fn title(&self, _window: window::Id) -> String {
/// String::from("A cool application")
/// }
///
/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
/// Command::none()
/// }
///
/// fn view(&self, _window: window::Id) -> Element<Self::Message> {
/// "Hello, world!".into()
/// }
/// }
/// ```
///
/// [`Sandbox`]: crate::Sandbox
pub trait Application: Sized {
/// The [`Executor`] that will run commands and subscriptions.
///
/// The [default executor] can be a good starting point!
///
/// [`Executor`]: Self::Executor
/// [default executor]: crate::executor::Default
type Executor: Executor;
/// The type of __messages__ your [`Application`] will produce.
type Message: std::fmt::Debug + Send;
/// The theme of your [`Application`].
type Theme: Default + StyleSheet;
/// The data needed to initialize your [`Application`].
type Flags;
/// Initializes the [`Application`] with the flags provided to
/// [`run`] as part of the [`Settings`].
///
/// Here is where you should return the initial state of your app.
///
/// Additionally, you can return a [`Command`] if you need to perform some
/// async action in the background on startup. This is useful if you want to
/// load state from a file, perform an initial HTTP request, etc.
///
/// [`run`]: Self::run
fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
/// Returns the current title of the `window` of the [`Application`].
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your window when necessary.
fn title(&self, window: window::Id) -> String;
/// Handles a __message__ and updates the state of the [`Application`].
///
/// This is where you define your __update logic__. All the __messages__,
/// produced by either user interactions or commands, will be handled by
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// Returns the widgets to display in the `window` of the [`Application`].
///
/// These widgets can produce __messages__ based on user interaction.
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, crate::Renderer<Self::Theme>>;
/// Returns the current [`Theme`] of the `window` of the [`Application`].
///
/// [`Theme`]: Self::Theme
#[allow(unused_variables)]
fn theme(&self, window: window::Id) -> Self::Theme {
Self::Theme::default()
}
/// Returns the current `Style` of the [`Theme`].
///
/// [`Theme`]: Self::Theme
fn style(&self) -> <Self::Theme as StyleSheet>::Style {
<Self::Theme as StyleSheet>::Style::default()
}
/// Returns the event [`Subscription`] for the current state of the
/// application.
///
/// A [`Subscription`] will be kept alive as long as you keep returning it,
/// and the __messages__ produced will be handled by
/// [`update`](#tymethod.update).
///
/// By default, this method returns an empty [`Subscription`].
fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
/// Returns the scale factor of the `window` of the [`Application`].
///
/// It can be used to dynamically control the size of the UI at runtime
/// (i.e. zooming).
///
/// For instance, a scale factor of `2.0` will make widgets twice as big,
/// while a scale factor of `0.5` will shrink them to half their size.
///
/// By default, it returns `1.0`.
#[allow(unused_variables)]
fn scale_factor(&self, window: window::Id) -> f64 {
1.0
}
/// Runs the multi-window [`Application`].
///
/// On native platforms, this method will take control of the current thread
/// until the [`Application`] exits.
///
/// On the web platform, this method __will NOT return__ unless there is an
/// [`Error`] during startup.
///
/// [`Error`]: crate::Error
fn run(settings: Settings<Self::Flags>) -> crate::Result
where
Self: 'static,
{
#[allow(clippy::needless_update)]
let renderer_settings = crate::renderer::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
antialiasing: if settings.antialiasing {
Some(crate::graphics::Antialiasing::MSAAx4)
} else {
None
},
..crate::renderer::Settings::default()
};
Ok(crate::shell::multi_window::run::<
Instance<Self>,
Self::Executor,
crate::renderer::Compositor<Self::Theme>,
>(settings.into(), renderer_settings)?)
}
}
struct Instance<A: Application>(A);
impl<A> crate::runtime::multi_window::Program for Instance<A>
where
A: Application,
{
type Renderer = crate::Renderer<A::Theme>;
type Message = A::Message;
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
self.0.update(message)
}
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, Self::Renderer> {
self.0.view(window)
}
}
impl<A> crate::shell::multi_window::Application for Instance<A>
where
A: Application,
{
type Flags = A::Flags;
fn new(flags: Self::Flags) -> (Self, Command<A::Message>) {
let (app, command) = A::new(flags);
(Instance(app), command)
}
fn title(&self, window: window::Id) -> String {
self.0.title(window)
}
fn theme(&self, window: window::Id) -> A::Theme {
self.0.theme(window)
}
fn style(&self) -> <A::Theme as StyleSheet>::Style {
self.0.style()
}
fn subscription(&self) -> Subscription<Self::Message> {
self.0.subscription()
}
fn scale_factor(&self, window: window::Id) -> f64 {
self.0.scale_factor(window)
}
}

View file

@ -46,14 +46,6 @@ pub struct Settings<Flags> {
/// ///
/// [`Canvas`]: crate::widget::Canvas /// [`Canvas`]: crate::widget::Canvas
pub antialiasing: bool, pub antialiasing: bool,
/// Whether the [`Application`] should exit when the user requests the
/// window to close (e.g. the user presses the close button).
///
/// By default, it is enabled.
///
/// [`Application`]: crate::Application
pub exit_on_close_request: bool,
} }
impl<Flags> Settings<Flags> { impl<Flags> Settings<Flags> {
@ -71,7 +63,6 @@ impl<Flags> Settings<Flags> {
default_font: default_settings.default_font, default_font: default_settings.default_font,
default_text_size: default_settings.default_text_size, default_text_size: default_settings.default_text_size,
antialiasing: default_settings.antialiasing, antialiasing: default_settings.antialiasing,
exit_on_close_request: default_settings.exit_on_close_request,
} }
} }
} }
@ -89,7 +80,6 @@ where
default_font: Font::default(), default_font: Font::default(),
default_text_size: Pixels(16.0), default_text_size: Pixels(16.0),
antialiasing: false, antialiasing: false,
exit_on_close_request: true,
} }
} }
} }
@ -98,10 +88,9 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> {
fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> {
iced_winit::Settings { iced_winit::Settings {
id: settings.id, id: settings.id,
window: settings.window.into(), window: settings.window,
flags: settings.flags, flags: settings.flags,
fonts: settings.fonts, fonts: settings.fonts,
exit_on_close_request: settings.exit_on_close_request,
} }
} }
} }

View file

@ -1,12 +1,8 @@
//! Configure the window of your application in native platforms. //! Configure the window of your application in native platforms.
mod position;
mod settings;
pub mod icon; pub mod icon;
pub use icon::Icon; pub use icon::Icon;
pub use position::Position;
pub use settings::{PlatformSpecific, Settings};
pub use crate::core::window::*; pub use crate::core::window::*;
pub use crate::runtime::window::*; pub use crate::runtime::window::*;

View file

@ -1,32 +0,0 @@
/// The position of a window in a given screen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Position {
/// The platform-specific default position for a new window.
Default,
/// The window is completely centered on the screen.
Centered,
/// The window is positioned with specific coordinates: `(X, Y)`.
///
/// When the decorations of the window are enabled, Windows 10 will add some
/// invisible padding to the window. This padding gets included in the
/// position. So if you have decorations enabled and want the window to be
/// at (0, 0) you would have to set the position to
/// `(PADDING_X, PADDING_Y)`.
Specific(i32, i32),
}
impl Default for Position {
fn default() -> Self {
Self::Default
}
}
impl From<Position> for iced_winit::Position {
fn from(position: Position) -> Self {
match position {
Position::Default => Self::Default,
Position::Centered => Self::Centered,
Position::Specific(x, y) => Self::Specific(x, y),
}
}
}

View file

@ -8,6 +8,7 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use std::marker::PhantomData; use std::marker::PhantomData;
pub struct Compositor<Theme> { pub struct Compositor<Theme> {
settings: Settings,
_theme: PhantomData<Theme>, _theme: PhantomData<Theme>,
} }
@ -27,17 +28,16 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Self::Settings, settings: Self::Settings,
_compatible_window: Option<&W>, _compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> { ) -> Result<Self, Error> {
let (compositor, backend) = new(); Ok(new(settings))
}
Ok(( fn create_renderer(&self) -> Self::Renderer {
compositor, Renderer::new(
Renderer::new( Backend::new(),
backend, self.settings.default_font,
settings.default_font, self.settings.default_text_size,
settings.default_text_size, )
),
))
} }
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(
@ -121,13 +121,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
} }
} }
pub fn new<Theme>() -> (Compositor<Theme>, Backend) { pub fn new<Theme>(settings: Settings) -> Compositor<Theme> {
( Compositor {
Compositor { settings,
_theme: PhantomData, _theme: PhantomData,
}, }
Backend::new(),
)
} }
pub fn present<T: AsRef<str>>( pub fn present<T: AsRef<str>>(

View file

@ -139,16 +139,14 @@ impl<Theme> Compositor<Theme> {
pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Settings, settings: Settings,
compatible_window: Option<&W>, compatible_window: Option<&W>,
) -> Result<(Compositor<Theme>, Backend), Error> { ) -> Result<Compositor<Theme>, Error> {
let compositor = futures::executor::block_on(Compositor::request( let compositor = futures::executor::block_on(Compositor::request(
settings, settings,
compatible_window, compatible_window,
)) ))
.ok_or(Error::GraphicsAdapterNotFound)?; .ok_or(Error::GraphicsAdapterNotFound)?;
let backend = compositor.create_backend(); Ok(compositor)
Ok((compositor, backend))
} }
/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. /// Presents the given primitives with the given [`Compositor`] and [`Backend`].
@ -214,17 +212,16 @@ impl<Theme> graphics::Compositor for Compositor<Theme> {
fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Self::Settings, settings: Self::Settings,
compatible_window: Option<&W>, compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> { ) -> Result<Self, Error> {
let (compositor, backend) = new(settings, compatible_window)?; new(settings, compatible_window)
}
Ok(( fn create_renderer(&self) -> Self::Renderer {
compositor, Renderer::new(
Renderer::new( self.create_backend(),
backend, self.settings.default_font,
settings.default_font, self.settings.default_text_size,
settings.default_text_size, )
),
))
} }
fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>(

View file

@ -109,7 +109,7 @@ where
Some(Event::Keyboard(keyboard_event)) Some(Event::Keyboard(keyboard_event))
} }
core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)),
core::Event::Window(window::Event::RedrawRequested(instant)) => { core::Event::Window(_, window::Event::RedrawRequested(instant)) => {
Some(Event::RedrawRequested(instant)) Some(Event::RedrawRequested(instant))
} }
_ => None, _ => None,

View file

@ -1003,14 +1003,14 @@ where
state.keyboard_modifiers = modifiers; state.keyboard_modifiers = modifiers;
} }
Event::Window(window::Event::Unfocused) => { Event::Window(_, window::Event::Unfocused) => {
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { if let Some(focus) = &mut state.is_focused {
focus.is_window_focused = false; focus.is_window_focused = false;
} }
} }
Event::Window(window::Event::Focused) => { Event::Window(_, window::Event::Focused) => {
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { if let Some(focus) = &mut state.is_focused {
@ -1020,7 +1020,7 @@ where
shell.request_redraw(window::RedrawRequest::NextFrame); shell.request_redraw(window::RedrawRequest::NextFrame);
} }
} }
Event::Window(window::Event::RedrawRequested(now)) => { Event::Window(_, window::Event::RedrawRequested(now)) => {
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { if let Some(focus) = &mut state.is_focused {

View file

@ -19,6 +19,7 @@ x11 = ["winit/x11"]
wayland = ["winit/wayland"] wayland = ["winit/wayland"]
wayland-dlopen = ["winit/wayland-dlopen"] wayland-dlopen = ["winit/wayland-dlopen"]
wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
multi-window = ["iced_runtime/multi-window"]
[dependencies] [dependencies]
iced_graphics.workspace = true iced_graphics.workspace = true
@ -26,7 +27,6 @@ iced_runtime.workspace = true
iced_style.workspace = true iced_style.workspace = true
log.workspace = true log.workspace = true
raw-window-handle.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
window_clipboard.workspace = true window_clipboard.workspace = true

View file

@ -1,6 +1,4 @@
//! Create interactive, native cross-platform applications. //! Create interactive, native cross-platform applications.
#[cfg(feature = "trace")]
mod profiler;
mod state; mod state;
pub use state::State; pub use state::State;
@ -27,11 +25,6 @@ use futures::channel::mpsc;
use std::mem::ManuallyDrop; use std::mem::ManuallyDrop;
#[cfg(feature = "trace")]
pub use profiler::Profiler;
#[cfg(feature = "trace")]
use tracing::{info_span, instrument::Instrument};
/// An interactive, native cross-platform application. /// An interactive, native cross-platform application.
/// ///
/// This trait is the main entrypoint of Iced. Once implemented, you can run /// This trait is the main entrypoint of Iced. Once implemented, you can run
@ -119,15 +112,9 @@ where
use futures::Future; use futures::Future;
use winit::event_loop::EventLoopBuilder; use winit::event_loop::EventLoopBuilder;
#[cfg(feature = "trace")]
let _guard = Profiler::init();
let mut debug = Debug::new(); let mut debug = Debug::new();
debug.startup_started(); debug.startup_started();
#[cfg(feature = "trace")]
let _ = info_span!("Application", "RUN").entered();
let event_loop = EventLoopBuilder::with_user_event().build(); let event_loop = EventLoopBuilder::with_user_event().build();
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
@ -148,14 +135,15 @@ where
let target = settings.window.platform_specific.target.clone(); let target = settings.window.platform_specific.target.clone();
let should_be_visible = settings.window.visible; let should_be_visible = settings.window.visible;
let builder = settings let exit_on_close_request = settings.window.exit_on_close_request;
.window
.into_builder( let builder = conversion::window_settings(
&application.title(), settings.window,
event_loop.primary_monitor(), &application.title(),
settings.id, event_loop.primary_monitor(),
) settings.id,
.with_visible(false); )
.with_visible(false);
log::debug!("Window builder: {builder:#?}"); log::debug!("Window builder: {builder:#?}");
@ -193,8 +181,8 @@ where
}; };
} }
let (compositor, mut renderer) = let compositor = C::new(compositor_settings, Some(&window))?;
C::new(compositor_settings, Some(&window))?; let mut renderer = compositor.create_renderer();
for font in settings.fonts { for font in settings.fonts {
use crate::core::text::Renderer; use crate::core::text::Renderer;
@ -205,28 +193,20 @@ where
let (mut event_sender, event_receiver) = mpsc::unbounded(); let (mut event_sender, event_receiver) = mpsc::unbounded();
let (control_sender, mut control_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded();
let mut instance = Box::pin({ let mut instance = Box::pin(run_instance::<A, E, C>(
let run_instance = run_instance::<A, E, C>( application,
application, compositor,
compositor, renderer,
renderer, runtime,
runtime, proxy,
proxy, debug,
debug, event_receiver,
event_receiver, control_sender,
control_sender, init_command,
init_command, window,
window, should_be_visible,
should_be_visible, exit_on_close_request,
settings.exit_on_close_request, ));
);
#[cfg(feature = "trace")]
let run_instance =
run_instance.instrument(info_span!("Application", "LOOP"));
run_instance
});
let mut context = task::Context::from_waker(task::noop_waker_ref()); let mut context = task::Context::from_waker(task::noop_waker_ref());
@ -426,6 +406,7 @@ async fn run_instance<A, E, C>(
// Then, we can use the `interface_state` here to decide if a redraw // Then, we can use the `interface_state` here to decide if a redraw
// is needed right away, or simply wait until a specific time. // is needed right away, or simply wait until a specific time.
let redraw_event = Event::Window( let redraw_event = Event::Window(
window::Id::MAIN,
window::Event::RedrawRequested(Instant::now()), window::Event::RedrawRequested(Instant::now()),
); );
@ -488,9 +469,6 @@ async fn run_instance<A, E, C>(
messages.push(message); messages.push(message);
} }
event::Event::RedrawRequested(_) => { event::Event::RedrawRequested(_) => {
#[cfg(feature = "trace")]
let _ = info_span!("Application", "FRAME").entered();
let physical_size = state.physical_size(); let physical_size = state.physical_size();
if physical_size.width == 0 || physical_size.height == 0 { if physical_size.width == 0 || physical_size.height == 0 {
@ -578,6 +556,7 @@ async fn run_instance<A, E, C>(
state.update(&window, &window_event, &mut debug); state.update(&window, &window_event, &mut debug);
if let Some(event) = conversion::window_event( if let Some(event) = conversion::window_event(
window::Id::MAIN,
&window_event, &window_event,
state.scale_factor(), state.scale_factor(),
state.modifiers(), state.modifiers(),
@ -629,24 +608,12 @@ pub fn build_user_interface<'a, A: Application>(
where where
<A::Renderer as core::Renderer>::Theme: StyleSheet, <A::Renderer as core::Renderer>::Theme: StyleSheet,
{ {
#[cfg(feature = "trace")]
let view_span = info_span!("Application", "VIEW").entered();
debug.view_started(); debug.view_started();
let view = application.view(); let view = application.view();
#[cfg(feature = "trace")]
let _ = view_span.exit();
debug.view_finished(); debug.view_finished();
#[cfg(feature = "trace")]
let layout_span = info_span!("Application", "LAYOUT").entered();
debug.layout_started(); debug.layout_started();
let user_interface = UserInterface::build(view, size, cache, renderer); let user_interface = UserInterface::build(view, size, cache, renderer);
#[cfg(feature = "trace")]
let _ = layout_span.exit();
debug.layout_finished(); debug.layout_finished();
user_interface user_interface
@ -673,16 +640,10 @@ pub fn update<A: Application, C, E: Executor>(
<A::Renderer as core::Renderer>::Theme: StyleSheet, <A::Renderer as core::Renderer>::Theme: StyleSheet,
{ {
for message in messages.drain(..) { for message in messages.drain(..) {
#[cfg(feature = "trace")]
let update_span = info_span!("Application", "UPDATE").entered();
debug.log_message(&message); debug.log_message(&message);
debug.update_started(); debug.update_started();
let command = runtime.enter(|| application.update(message)); let command = runtime.enter(|| application.update(message));
#[cfg(feature = "trace")]
let _ = update_span.exit();
debug.update_finished(); debug.update_finished();
run_command( run_command(
@ -752,20 +713,27 @@ pub fn run_command<A, C, E>(
} }
}, },
command::Action::Window(action) => match action { command::Action::Window(action) => match action {
window::Action::Close => { window::Action::Close(_id) => {
*should_exit = true; *should_exit = true;
} }
window::Action::Drag => { window::Action::Drag(_id) => {
let _res = window.drag_window(); let _res = window.drag_window();
} }
window::Action::Resize(size) => { window::Action::Spawn { .. } => {
log::warn!(
"Spawning a window is only available with \
multi-window applications."
);
}
window::Action::Resize(_id, size) => {
window.set_inner_size(winit::dpi::LogicalSize { window.set_inner_size(winit::dpi::LogicalSize {
width: size.width, width: size.width,
height: size.height, height: size.height,
}); });
} }
window::Action::FetchSize(callback) => { window::Action::FetchSize(_id, callback) => {
let size = window.inner_size(); let size =
window.inner_size().to_logical(window.scale_factor());
proxy proxy
.send_event(callback(Size::new( .send_event(callback(Size::new(
@ -774,29 +742,29 @@ pub fn run_command<A, C, E>(
))) )))
.expect("Send message to event loop"); .expect("Send message to event loop");
} }
window::Action::Maximize(maximized) => { window::Action::Maximize(_id, maximized) => {
window.set_maximized(maximized); window.set_maximized(maximized);
} }
window::Action::Minimize(minimized) => { window::Action::Minimize(_id, minimized) => {
window.set_minimized(minimized); window.set_minimized(minimized);
} }
window::Action::Move { x, y } => { window::Action::Move(_id, position) => {
window.set_outer_position(winit::dpi::LogicalPosition { window.set_outer_position(winit::dpi::LogicalPosition {
x, x: position.x,
y, y: position.y,
}); });
} }
window::Action::ChangeMode(mode) => { window::Action::ChangeMode(_id, mode) => {
window.set_visible(conversion::visible(mode)); window.set_visible(conversion::visible(mode));
window.set_fullscreen(conversion::fullscreen( window.set_fullscreen(conversion::fullscreen(
window.current_monitor(), window.current_monitor(),
mode, mode,
)); ));
} }
window::Action::ChangeIcon(icon) => { window::Action::ChangeIcon(_id, icon) => {
window.set_window_icon(conversion::icon(icon)); window.set_window_icon(conversion::icon(icon));
} }
window::Action::FetchMode(tag) => { window::Action::FetchMode(_id, tag) => {
let mode = if window.is_visible().unwrap_or(true) { let mode = if window.is_visible().unwrap_or(true) {
conversion::mode(window.fullscreen()) conversion::mode(window.fullscreen())
} else { } else {
@ -807,29 +775,29 @@ pub fn run_command<A, C, E>(
.send_event(tag(mode)) .send_event(tag(mode))
.expect("Send message to event loop"); .expect("Send message to event loop");
} }
window::Action::ToggleMaximize => { window::Action::ToggleMaximize(_id) => {
window.set_maximized(!window.is_maximized()); window.set_maximized(!window.is_maximized());
} }
window::Action::ToggleDecorations => { window::Action::ToggleDecorations(_id) => {
window.set_decorations(!window.is_decorated()); window.set_decorations(!window.is_decorated());
} }
window::Action::RequestUserAttention(user_attention) => { window::Action::RequestUserAttention(_id, user_attention) => {
window.request_user_attention( window.request_user_attention(
user_attention.map(conversion::user_attention), user_attention.map(conversion::user_attention),
); );
} }
window::Action::GainFocus => { window::Action::GainFocus(_id) => {
window.focus_window(); window.focus_window();
} }
window::Action::ChangeLevel(level) => { window::Action::ChangeLevel(_id, level) => {
window.set_window_level(conversion::window_level(level)); window.set_window_level(conversion::window_level(level));
} }
window::Action::FetchId(tag) => { window::Action::FetchId(_id, tag) => {
proxy proxy
.send_event(tag(window.id().into())) .send_event(tag(window.id().into()))
.expect("Send message to event loop"); .expect("Send message to event loop");
} }
window::Action::Screenshot(tag) => { window::Action::Screenshot(_id, tag) => {
let bytes = compositor.screenshot( let bytes = compositor.screenshot(
renderer, renderer,
surface, surface,

View file

@ -1,101 +0,0 @@
//! A simple profiler for Iced.
use std::ffi::OsStr;
use std::path::Path;
use std::time::Duration;
use tracing_subscriber::prelude::*;
use tracing_subscriber::Registry;
#[cfg(feature = "chrome-trace")]
use {
tracing_chrome::FlushGuard,
tracing_subscriber::fmt::{format::DefaultFields, FormattedFields},
};
/// Profiler state. This will likely need to be updated or reworked when adding new tracing backends.
#[allow(missing_debug_implementations)]
pub struct Profiler {
#[cfg(feature = "chrome-trace")]
/// [`FlushGuard`] must not be dropped until the application scope is dropped for accurate tracing.
_guard: FlushGuard,
}
impl Profiler {
/// Initializes the [`Profiler`].
pub fn init() -> Self {
// Registry stores the spans & generates unique span IDs
let subscriber = Registry::default();
let default_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let curr_exe = std::env::current_exe()
.unwrap_or_else(|_| default_path.to_path_buf());
let out_dir = curr_exe.parent().unwrap_or(default_path).join("traces");
#[cfg(feature = "chrome-trace")]
let (chrome_layer, guard) = {
let mut layer = tracing_chrome::ChromeLayerBuilder::new();
// Optional configurable env var: CHROME_TRACE_FILE=/path/to/trace_file/file.json,
// for uploading to chrome://tracing (old) or ui.perfetto.dev (new).
if let Ok(path) = std::env::var("CHROME_TRACE_FILE") {
layer = layer.file(path);
} else if std::fs::create_dir_all(&out_dir).is_ok() {
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_millis(0))
.as_millis();
let curr_exe_name = curr_exe
.file_name()
.unwrap_or_else(|| OsStr::new("trace"))
.to_str()
.unwrap_or("trace");
let path =
out_dir.join(format!("{curr_exe_name}_trace_{time}.json"));
layer = layer.file(path);
} else {
layer = layer.file(env!("CARGO_MANIFEST_DIR"))
}
let (chrome_layer, guard) = layer
.name_fn(Box::new(|event_or_span| match event_or_span {
tracing_chrome::EventOrSpan::Event(event) => {
event.metadata().name().into()
}
tracing_chrome::EventOrSpan::Span(span) => {
if let Some(fields) = span
.extensions()
.get::<FormattedFields<DefaultFields>>()
{
format!(
"{}: {}",
span.metadata().name(),
fields.fields.as_str()
)
} else {
span.metadata().name().into()
}
}
}))
.build();
(chrome_layer, guard)
};
let fmt_layer = tracing_subscriber::fmt::Layer::default();
let subscriber = subscriber.with(fmt_layer);
#[cfg(feature = "chrome-trace")]
let subscriber = subscriber.with(chrome_layer);
// create dispatcher which will forward span events to the subscriber
// this can only be set once or will panic
tracing::subscriber::set_global_default(subscriber)
.expect("Tracer could not set the global default subscriber.");
Profiler {
#[cfg(feature = "chrome-trace")]
_guard: guard,
}
}
}

View file

@ -6,11 +6,128 @@ use crate::core::keyboard;
use crate::core::mouse; use crate::core::mouse;
use crate::core::touch; use crate::core::touch;
use crate::core::window; use crate::core::window;
use crate::core::{Event, Point}; use crate::core::{Event, Point, Size};
use crate::Position;
/// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`.
pub fn window_settings(
settings: window::Settings,
title: &str,
primary_monitor: Option<winit::monitor::MonitorHandle>,
_id: Option<String>,
) -> winit::window::WindowBuilder {
let mut window_builder = winit::window::WindowBuilder::new();
window_builder = window_builder
.with_title(title)
.with_inner_size(winit::dpi::LogicalSize {
width: settings.size.width,
height: settings.size.height,
})
.with_resizable(settings.resizable)
.with_enabled_buttons(if settings.resizable {
winit::window::WindowButtons::all()
} else {
winit::window::WindowButtons::CLOSE
| winit::window::WindowButtons::MINIMIZE
})
.with_decorations(settings.decorations)
.with_transparent(settings.transparent)
.with_window_icon(settings.icon.and_then(icon))
.with_window_level(window_level(settings.level))
.with_visible(settings.visible);
if let Some(position) =
position(primary_monitor.as_ref(), settings.size, settings.position)
{
window_builder = window_builder.with_position(position);
}
if let Some(min_size) = settings.min_size {
window_builder =
window_builder.with_min_inner_size(winit::dpi::LogicalSize {
width: min_size.width,
height: min_size.height,
});
}
if let Some(max_size) = settings.max_size {
window_builder =
window_builder.with_max_inner_size(winit::dpi::LogicalSize {
width: max_size.width,
height: max_size.height,
});
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
{
// `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do
// exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here.
use ::winit::platform::wayland::WindowBuilderExtWayland;
if let Some(id) = _id {
window_builder = window_builder.with_name(id.clone(), id);
}
}
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowBuilderExtWindows;
#[allow(unsafe_code)]
unsafe {
window_builder = window_builder
.with_parent_window(settings.platform_specific.parent);
}
window_builder = window_builder
.with_drag_and_drop(settings.platform_specific.drag_and_drop);
}
#[cfg(target_os = "macos")]
{
use winit::platform::macos::WindowBuilderExtMacOS;
window_builder = window_builder
.with_title_hidden(settings.platform_specific.title_hidden)
.with_titlebar_transparent(
settings.platform_specific.titlebar_transparent,
)
.with_fullsize_content_view(
settings.platform_specific.fullsize_content_view,
);
}
#[cfg(target_os = "linux")]
{
#[cfg(feature = "x11")]
{
use winit::platform::x11::WindowBuilderExtX11;
window_builder = window_builder.with_name(
&settings.platform_specific.application_id,
&settings.platform_specific.application_id,
);
}
#[cfg(feature = "wayland")]
{
use winit::platform::wayland::WindowBuilderExtWayland;
window_builder = window_builder.with_name(
&settings.platform_specific.application_id,
&settings.platform_specific.application_id,
);
}
}
window_builder
}
/// Converts a winit window event into an iced event. /// Converts a winit window event into an iced event.
pub fn window_event( pub fn window_event(
id: window::Id,
event: &winit::event::WindowEvent<'_>, event: &winit::event::WindowEvent<'_>,
scale_factor: f64, scale_factor: f64,
modifiers: winit::event::ModifiersState, modifiers: winit::event::ModifiersState,
@ -21,21 +138,27 @@ pub fn window_event(
WindowEvent::Resized(new_size) => { WindowEvent::Resized(new_size) => {
let logical_size = new_size.to_logical(scale_factor); let logical_size = new_size.to_logical(scale_factor);
Some(Event::Window(window::Event::Resized { Some(Event::Window(
width: logical_size.width, id,
height: logical_size.height, window::Event::Resized {
})) width: logical_size.width,
height: logical_size.height,
},
))
} }
WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { WindowEvent::ScaleFactorChanged { new_inner_size, .. } => {
let logical_size = new_inner_size.to_logical(scale_factor); let logical_size = new_inner_size.to_logical(scale_factor);
Some(Event::Window(window::Event::Resized { Some(Event::Window(
width: logical_size.width, id,
height: logical_size.height, window::Event::Resized {
})) width: logical_size.width,
height: logical_size.height,
},
))
} }
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
Some(Event::Window(window::Event::CloseRequested)) Some(Event::Window(id, window::Event::CloseRequested))
} }
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
let position = position.to_logical::<f64>(scale_factor); let position = position.to_logical::<f64>(scale_factor);
@ -113,19 +236,22 @@ pub fn window_event(
WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard(
keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)),
)), )),
WindowEvent::Focused(focused) => Some(Event::Window(if *focused { WindowEvent::Focused(focused) => Some(Event::Window(
window::Event::Focused id,
} else { if *focused {
window::Event::Unfocused window::Event::Focused
})), } else {
window::Event::Unfocused
},
)),
WindowEvent::HoveredFile(path) => { WindowEvent::HoveredFile(path) => {
Some(Event::Window(window::Event::FileHovered(path.clone()))) Some(Event::Window(id, window::Event::FileHovered(path.clone())))
} }
WindowEvent::DroppedFile(path) => { WindowEvent::DroppedFile(path) => {
Some(Event::Window(window::Event::FileDropped(path.clone()))) Some(Event::Window(id, window::Event::FileDropped(path.clone())))
} }
WindowEvent::HoveredFileCancelled => { WindowEvent::HoveredFileCancelled => {
Some(Event::Window(window::Event::FilesHoveredLeft)) Some(Event::Window(id, window::Event::FilesHoveredLeft))
} }
WindowEvent::Touch(touch) => { WindowEvent::Touch(touch) => {
Some(Event::Touch(touch_event(*touch, scale_factor))) Some(Event::Touch(touch_event(*touch, scale_factor)))
@ -134,7 +260,7 @@ pub fn window_event(
let winit::dpi::LogicalPosition { x, y } = let winit::dpi::LogicalPosition { x, y } =
position.to_logical(scale_factor); position.to_logical(scale_factor);
Some(Event::Window(window::Event::Moved { x, y })) Some(Event::Window(id, window::Event::Moved { x, y }))
} }
_ => None, _ => None,
} }
@ -153,23 +279,23 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel {
} }
} }
/// Converts a [`Position`] to a [`winit`] logical position for a given monitor. /// Converts a [`window::Position`] to a [`winit`] logical position for a given monitor.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`winit`]: https://github.com/rust-windowing/winit
pub fn position( pub fn position(
monitor: Option<&winit::monitor::MonitorHandle>, monitor: Option<&winit::monitor::MonitorHandle>,
(width, height): (u32, u32), size: Size,
position: Position, position: window::Position,
) -> Option<winit::dpi::Position> { ) -> Option<winit::dpi::Position> {
match position { match position {
Position::Default => None, window::Position::Default => None,
Position::Specific(x, y) => { window::Position::Specific(position) => {
Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition {
x: f64::from(x), x: f64::from(position.x),
y: f64::from(y), y: f64::from(position.y),
})) }))
} }
Position::Centered => { window::Position::Centered => {
if let Some(monitor) = monitor { if let Some(monitor) = monitor {
let start = monitor.position(); let start = monitor.position();
@ -178,8 +304,8 @@ pub fn position(
let centered: winit::dpi::PhysicalPosition<i32> = let centered: winit::dpi::PhysicalPosition<i32> =
winit::dpi::LogicalPosition { winit::dpi::LogicalPosition {
x: (resolution.width - f64::from(width)) / 2.0, x: (resolution.width - f64::from(size.width)) / 2.0,
y: (resolution.height - f64::from(height)) / 2.0, y: (resolution.height - f64::from(size.height)) / 2.0,
} }
.to_physical(monitor.scale_factor()); .to_physical(monitor.scale_factor());

View file

@ -33,6 +33,9 @@ pub use iced_runtime::futures;
pub use iced_style as style; pub use iced_style as style;
pub use winit; pub use winit;
#[cfg(feature = "multi-window")]
pub mod multi_window;
#[cfg(feature = "application")] #[cfg(feature = "application")]
pub mod application; pub mod application;
pub mod clipboard; pub mod clipboard;
@ -43,17 +46,11 @@ pub mod settings;
pub mod system; pub mod system;
mod error; mod error;
mod position;
mod proxy; mod proxy;
#[cfg(feature = "application")] #[cfg(feature = "application")]
pub use application::Application; pub use application::Application;
#[cfg(feature = "trace")]
pub use application::Profiler;
pub use clipboard::Clipboard; pub use clipboard::Clipboard;
pub use error::Error; pub use error::Error;
pub use position::Position;
pub use proxy::Proxy; pub use proxy::Proxy;
pub use settings::Settings; pub use settings::Settings;
pub use iced_graphics::Viewport;

1212
winit/src/multi_window.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,240 @@
use crate::conversion;
use crate::core;
use crate::core::{mouse, window};
use crate::core::{Color, Size};
use crate::graphics::Viewport;
use crate::multi_window::Application;
use crate::style::application;
use std::fmt::{Debug, Formatter};
use iced_style::application::StyleSheet;
use winit::event::{Touch, WindowEvent};
use winit::window::Window;
/// The state of a multi-windowed [`Application`].
pub struct State<A: Application>
where
<A::Renderer as core::Renderer>::Theme: application::StyleSheet,
{
title: String,
scale_factor: f64,
viewport: Viewport,
viewport_version: u64,
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
modifiers: winit::event::ModifiersState,
theme: <A::Renderer as core::Renderer>::Theme,
appearance: application::Appearance,
}
impl<A: Application> Debug for State<A>
where
<A::Renderer as core::Renderer>::Theme: application::StyleSheet,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("multi_window::State")
.field("title", &self.title)
.field("scale_factor", &self.scale_factor)
.field("viewport", &self.viewport)
.field("viewport_version", &self.viewport_version)
.field("cursor_position", &self.cursor_position)
.field("appearance", &self.appearance)
.finish()
}
}
impl<A: Application> State<A>
where
<A::Renderer as core::Renderer>::Theme: application::StyleSheet,
{
/// Creates a new [`State`] for the provided [`Application`]'s `window`.
pub fn new(
application: &A,
window_id: window::Id,
window: &Window,
) -> Self {
let title = application.title(window_id);
let scale_factor = application.scale_factor(window_id);
let theme = application.theme(window_id);
let appearance = theme.appearance(&application.style());
let viewport = {
let physical_size = window.inner_size();
Viewport::with_physical_size(
Size::new(physical_size.width, physical_size.height),
window.scale_factor() * scale_factor,
)
};
Self {
title,
scale_factor,
viewport,
viewport_version: 0,
cursor_position: None,
modifiers: winit::event::ModifiersState::default(),
theme,
appearance,
}
}
/// Returns the current [`Viewport`] of the [`State`].
pub fn viewport(&self) -> &Viewport {
&self.viewport
}
/// Returns the version of the [`Viewport`] of the [`State`].
///
/// The version is incremented every time the [`Viewport`] changes.
pub fn viewport_version(&self) -> u64 {
self.viewport_version
}
/// Returns the physical [`Size`] of the [`Viewport`] of the [`State`].
pub fn physical_size(&self) -> Size<u32> {
self.viewport.physical_size()
}
/// Returns the logical [`Size`] of the [`Viewport`] of the [`State`].
pub fn logical_size(&self) -> Size<f32> {
self.viewport.logical_size()
}
/// Returns the current scale factor of the [`Viewport`] of the [`State`].
pub fn scale_factor(&self) -> f64 {
self.viewport.scale_factor()
}
/// Returns the current cursor position of the [`State`].
pub fn cursor(&self) -> mouse::Cursor {
self.cursor_position
.map(|cursor_position| {
conversion::cursor_position(
cursor_position,
self.viewport.scale_factor(),
)
})
.map(mouse::Cursor::Available)
.unwrap_or(mouse::Cursor::Unavailable)
}
/// Returns the current keyboard modifiers of the [`State`].
pub fn modifiers(&self) -> winit::event::ModifiersState {
self.modifiers
}
/// Returns the current theme of the [`State`].
pub fn theme(&self) -> &<A::Renderer as core::Renderer>::Theme {
&self.theme
}
/// Returns the current background [`Color`] of the [`State`].
pub fn background_color(&self) -> Color {
self.appearance.background_color
}
/// Returns the current text [`Color`] of the [`State`].
pub fn text_color(&self) -> Color {
self.appearance.text_color
}
/// Processes the provided window event and updates the [`State`] accordingly.
pub fn update(
&mut self,
window: &Window,
event: &WindowEvent<'_>,
_debug: &mut crate::runtime::Debug,
) {
match event {
WindowEvent::Resized(new_size) => {
let size = Size::new(new_size.width, new_size.height);
self.viewport = Viewport::with_physical_size(
size,
window.scale_factor() * self.scale_factor,
);
self.viewport_version = self.viewport_version.wrapping_add(1);
}
WindowEvent::ScaleFactorChanged {
scale_factor: new_scale_factor,
new_inner_size,
} => {
let size =
Size::new(new_inner_size.width, new_inner_size.height);
self.viewport = Viewport::with_physical_size(
size,
new_scale_factor * self.scale_factor,
);
self.viewport_version = self.viewport_version.wrapping_add(1);
}
WindowEvent::CursorMoved { position, .. }
| WindowEvent::Touch(Touch {
location: position, ..
}) => {
self.cursor_position = Some(*position);
}
WindowEvent::CursorLeft { .. } => {
self.cursor_position = None;
}
WindowEvent::ModifiersChanged(new_modifiers) => {
self.modifiers = *new_modifiers;
}
#[cfg(feature = "debug")]
WindowEvent::KeyboardInput {
input:
winit::event::KeyboardInput {
virtual_keycode: Some(winit::event::VirtualKeyCode::F12),
state: winit::event::ElementState::Pressed,
..
},
..
} => _debug.toggle(),
_ => {}
}
}
/// Synchronizes the [`State`] with its [`Application`] and its respective
/// window.
///
/// Normally, an [`Application`] should be synchronized with its [`State`]
/// and window after calling [`State::update`].
pub fn synchronize(
&mut self,
application: &A,
window_id: window::Id,
window: &Window,
) {
// Update window title
let new_title = application.title(window_id);
if self.title != new_title {
window.set_title(&new_title);
self.title = new_title;
}
// Update scale factor and size
let new_scale_factor = application.scale_factor(window_id);
let new_size = window.inner_size();
let current_size = self.viewport.physical_size();
if self.scale_factor != new_scale_factor
|| (current_size.width, current_size.height)
!= (new_size.width, new_size.height)
{
self.viewport = Viewport::with_physical_size(
Size::new(new_size.width, new_size.height),
window.scale_factor() * new_scale_factor,
);
self.viewport_version = self.viewport_version.wrapping_add(1);
self.scale_factor = new_scale_factor;
}
// Update theme and appearance
self.theme = application.theme(window_id);
self.appearance = self.theme.appearance(&application.style());
}
}

View file

@ -0,0 +1,156 @@
use crate::core::mouse;
use crate::core::window::Id;
use crate::core::{Point, Size};
use crate::graphics::Compositor;
use crate::multi_window::{Application, State};
use crate::style::application::StyleSheet;
use std::collections::BTreeMap;
use winit::monitor::MonitorHandle;
#[allow(missing_debug_implementations)]
pub struct WindowManager<A: Application, C: Compositor>
where
<A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
C: Compositor<Renderer = A::Renderer>,
{
aliases: BTreeMap<winit::window::WindowId, Id>,
entries: BTreeMap<Id, Window<A, C>>,
}
impl<A, C> WindowManager<A, C>
where
A: Application,
C: Compositor<Renderer = A::Renderer>,
<A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
{
pub fn new() -> Self {
Self {
aliases: BTreeMap::new(),
entries: BTreeMap::new(),
}
}
pub fn insert(
&mut self,
id: Id,
window: winit::window::Window,
application: &A,
compositor: &mut C,
exit_on_close_request: bool,
) -> &mut Window<A, C> {
let state = State::new(application, id, &window);
let viewport_version = state.viewport_version();
let physical_size = state.physical_size();
let surface = compositor.create_surface(
&window,
physical_size.width,
physical_size.height,
);
let renderer = compositor.create_renderer();
let _ = self.aliases.insert(window.id(), id);
let _ = self.entries.insert(
id,
Window {
raw: window,
state,
viewport_version,
exit_on_close_request,
surface,
renderer,
mouse_interaction: mouse::Interaction::Idle,
},
);
self.entries
.get_mut(&id)
.expect("Get window that was just inserted")
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter_mut(
&mut self,
) -> impl Iterator<Item = (Id, &mut Window<A, C>)> {
self.entries.iter_mut().map(|(k, v)| (*k, v))
}
pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<A, C>> {
self.entries.get_mut(&id)
}
pub fn get_mut_alias(
&mut self,
id: winit::window::WindowId,
) -> Option<(Id, &mut Window<A, C>)> {
let id = self.aliases.get(&id).copied()?;
Some((id, self.get_mut(id)?))
}
pub fn last_monitor(&self) -> Option<MonitorHandle> {
self.entries.values().last()?.raw.current_monitor()
}
pub fn remove(&mut self, id: Id) -> Option<Window<A, C>> {
let window = self.entries.remove(&id)?;
let _ = self.aliases.remove(&window.raw.id());
Some(window)
}
}
impl<A, C> Default for WindowManager<A, C>
where
A: Application,
C: Compositor<Renderer = A::Renderer>,
<A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
{
fn default() -> Self {
Self::new()
}
}
#[allow(missing_debug_implementations)]
pub struct Window<A, C>
where
A: Application,
C: Compositor<Renderer = A::Renderer>,
<A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
{
pub raw: winit::window::Window,
pub state: State<A>,
pub viewport_version: u64,
pub exit_on_close_request: bool,
pub mouse_interaction: mouse::Interaction,
pub surface: C::Surface,
pub renderer: A::Renderer,
}
impl<A, C> Window<A, C>
where
A: Application,
C: Compositor<Renderer = A::Renderer>,
<A::Renderer as crate::core::Renderer>::Theme: StyleSheet,
{
pub fn position(&self) -> Option<Point> {
self.raw
.inner_position()
.ok()
.map(|position| position.to_logical(self.raw.scale_factor()))
.map(|position| Point {
x: position.x,
y: position.y,
})
}
pub fn size(&self) -> Size {
let size = self.raw.inner_size().to_logical(self.raw.scale_factor());
Size::new(size.width, size.height)
}
}

View file

@ -1,40 +1,7 @@
//! Configure your application. //! Configure your application.
#[cfg(target_os = "windows")] use crate::core::window;
#[path = "settings/windows.rs"]
mod platform;
#[cfg(target_os = "macos")]
#[path = "settings/macos.rs"]
mod platform;
#[cfg(target_os = "linux")]
#[path = "settings/linux.rs"]
mod platform;
#[cfg(target_arch = "wasm32")]
#[path = "settings/wasm.rs"]
mod platform;
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_arch = "wasm32"
)))]
#[path = "settings/other.rs"]
mod platform;
pub use platform::PlatformSpecific;
use crate::conversion;
use crate::core::window::{Icon, Level};
use crate::Position;
use winit::monitor::MonitorHandle;
use winit::window::WindowBuilder;
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt;
/// The settings of an application. /// The settings of an application.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -45,8 +12,8 @@ pub struct Settings<Flags> {
/// communicate with it through the windowing system. /// communicate with it through the windowing system.
pub id: Option<String>, pub id: Option<String>,
/// The [`Window`] settings. /// The [`window::Settings`].
pub window: Window, pub window: window::Settings,
/// The data needed to initialize an [`Application`]. /// The data needed to initialize an [`Application`].
/// ///
@ -55,197 +22,4 @@ pub struct Settings<Flags> {
/// The fonts to load on boot. /// The fonts to load on boot.
pub fonts: Vec<Cow<'static, [u8]>>, pub fonts: Vec<Cow<'static, [u8]>>,
/// Whether the [`Application`] should exit when the user requests the
/// window to close (e.g. the user presses the close button).
///
/// [`Application`]: crate::Application
pub exit_on_close_request: bool,
}
/// The window settings of an application.
#[derive(Clone)]
pub struct Window {
/// The size of the window.
pub size: (u32, u32),
/// The position of the window.
pub position: Position,
/// The minimum size of the window.
pub min_size: Option<(u32, u32)>,
/// The maximum size of the window.
pub max_size: Option<(u32, u32)>,
/// Whether the window should be visible or not.
pub visible: bool,
/// Whether the window should be resizable or not.
pub resizable: bool,
/// Whether the window should have a border, a title bar, etc.
pub decorations: bool,
/// Whether the window should be transparent.
pub transparent: bool,
/// The window [`Level`].
pub level: Level,
/// The window icon, which is also usually used in the taskbar
pub icon: Option<Icon>,
/// Platform specific settings.
pub platform_specific: platform::PlatformSpecific,
}
impl fmt::Debug for Window {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Window")
.field("size", &self.size)
.field("position", &self.position)
.field("min_size", &self.min_size)
.field("max_size", &self.max_size)
.field("visible", &self.visible)
.field("resizable", &self.resizable)
.field("decorations", &self.decorations)
.field("transparent", &self.transparent)
.field("level", &self.level)
.field("icon", &self.icon.is_some())
.field("platform_specific", &self.platform_specific)
.finish()
}
}
impl Window {
/// Converts the window settings into a `WindowBuilder` from `winit`.
pub fn into_builder(
self,
title: &str,
primary_monitor: Option<MonitorHandle>,
_id: Option<String>,
) -> WindowBuilder {
let mut window_builder = WindowBuilder::new();
let (width, height) = self.size;
window_builder = window_builder
.with_title(title)
.with_inner_size(winit::dpi::LogicalSize { width, height })
.with_resizable(self.resizable)
.with_enabled_buttons(if self.resizable {
winit::window::WindowButtons::all()
} else {
winit::window::WindowButtons::CLOSE
| winit::window::WindowButtons::MINIMIZE
})
.with_decorations(self.decorations)
.with_transparent(self.transparent)
.with_window_icon(self.icon.and_then(conversion::icon))
.with_window_level(conversion::window_level(self.level))
.with_visible(self.visible);
if let Some(position) = conversion::position(
primary_monitor.as_ref(),
self.size,
self.position,
) {
window_builder = window_builder.with_position(position);
}
if let Some((width, height)) = self.min_size {
window_builder = window_builder
.with_min_inner_size(winit::dpi::LogicalSize { width, height });
}
if let Some((width, height)) = self.max_size {
window_builder = window_builder
.with_max_inner_size(winit::dpi::LogicalSize { width, height });
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
{
// `with_name` is available on both `WindowBuilderExtWayland` and `WindowBuilderExtX11` and they do
// exactly the same thing. We arbitrarily choose `WindowBuilderExtWayland` here.
use ::winit::platform::wayland::WindowBuilderExtWayland;
if let Some(id) = _id {
window_builder = window_builder.with_name(id.clone(), id);
}
}
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowBuilderExtWindows;
#[allow(unsafe_code)]
unsafe {
window_builder = window_builder
.with_parent_window(self.platform_specific.parent);
}
window_builder = window_builder
.with_drag_and_drop(self.platform_specific.drag_and_drop);
}
#[cfg(target_os = "macos")]
{
use winit::platform::macos::WindowBuilderExtMacOS;
window_builder = window_builder
.with_title_hidden(self.platform_specific.title_hidden)
.with_titlebar_transparent(
self.platform_specific.titlebar_transparent,
)
.with_fullsize_content_view(
self.platform_specific.fullsize_content_view,
);
}
#[cfg(target_os = "linux")]
{
#[cfg(feature = "x11")]
{
use winit::platform::x11::WindowBuilderExtX11;
window_builder = window_builder.with_name(
&self.platform_specific.application_id,
&self.platform_specific.application_id,
);
}
#[cfg(feature = "wayland")]
{
use winit::platform::wayland::WindowBuilderExtWayland;
window_builder = window_builder.with_name(
&self.platform_specific.application_id,
&self.platform_specific.application_id,
);
}
}
window_builder
}
}
impl Default for Window {
fn default() -> Window {
Window {
size: (1024, 768),
position: Position::default(),
min_size: None,
max_size: None,
visible: true,
resizable: true,
decorations: true,
transparent: false,
level: Level::default(),
icon: None,
platform_specific: PlatformSpecific::default(),
}
}
} }