From 529589d7fe9278858e3f251b559a1118598a8250 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 1 Apr 2022 17:16:15 -0300 Subject: Introduce `multi_window` from `pure` --- Cargo.toml | 2 + src/lib.rs | 3 + src/multi_window.rs | 4 + src/multi_window/application.rs | 196 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 src/multi_window.rs create mode 100644 src/multi_window/application.rs diff --git a/Cargo.toml b/Cargo.toml index 681aae5e..adcdb79f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ chrome-trace = [ "iced_wgpu?/tracing", "iced_glow?/tracing", ] +# Enables experimental multi-window support +multi_window = [] [badges] maintenance = { status = "actively-developed" } diff --git a/src/lib.rs b/src/lib.rs index a0e31be4..6cda8c41 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,6 +181,9 @@ pub mod touch; pub mod widget; pub mod window; +#[cfg(feature = "multi_window")] +pub mod multi_window; + #[cfg(all(not(feature = "glow"), feature = "wgpu"))] use iced_winit as runtime; diff --git a/src/multi_window.rs b/src/multi_window.rs new file mode 100644 index 00000000..5b7a00b4 --- /dev/null +++ b/src/multi_window.rs @@ -0,0 +1,4 @@ +//! Leverage multi-window support in your application. +mod application; + +pub use application::Application; diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs new file mode 100644 index 00000000..db41d54a --- /dev/null +++ b/src/multi_window/application.rs @@ -0,0 +1,196 @@ +use crate::{Command, Element, Executor, Settings, Subscription}; + +pub use iced_native::application::{Appearance, StyleSheet}; + +/// A pure version of [`Application`]. +/// +/// Unlike the impure version, the `view` method of this trait takes an +/// immutable reference to `self` and returns a pure [`Element`]. +/// +/// [`Application`]: crate::Application +/// [`Element`]: pure::Element +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); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self) -> 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; + + /// Returns the current [`Theme`] of the [`Application`]. + /// + /// [`Theme`]: Self::Theme + fn theme(&self) -> Self::Theme { + Self::Theme::default() + } + + /// Returns the current [`Style`] of the [`Theme`]. + /// + /// [`Style`]: ::Style + /// [`Theme`]: Self::Theme + fn style(&self) -> ::Style { + ::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 { + Subscription::none() + } + + /// Returns the widgets to display in the [`Application`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> Element<'_, Self::Message, crate::Renderer>; + + /// Returns the scale factor 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`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } + + /// Runs the [`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) -> 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, + text_multithreading: settings.text_multithreading, + antialiasing: if settings.antialiasing { + Some(crate::renderer::settings::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::renderer::Settings::from_env() + }; + + Ok(crate::runtime::application::run::< + Instance, + Self::Executor, + crate::renderer::window::Compositor, + >(settings.into(), renderer_settings)?) + } +} + +struct Instance(A); + +impl iced_winit::Program for Instance +where + A: Application, +{ + type Renderer = crate::Renderer; + type Message = A::Message; + + fn update(&mut self, message: Self::Message) -> Command { + self.0.update(message) + } + + fn view(&self) -> Element<'_, Self::Message, Self::Renderer> { + self.0.view() + } +} + +impl crate::runtime::Application for Instance +where + A: Application, +{ + type Flags = A::Flags; + + fn new(flags: Self::Flags) -> (Self, Command) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self) -> String { + self.0.title() + } + + fn theme(&self) -> A::Theme { + self.0.theme() + } + + fn style(&self) -> ::Style { + self.0.style() + } + + fn subscription(&self) -> Subscription { + self.0.subscription() + } + + fn scale_factor(&self) -> f64 { + self.0.scale_factor() + } + + fn should_exit(&self) -> bool { + self.0.should_exit() + } +} -- cgit From 287306e1ebec610c31e37782320fe00d20a6c9ac Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 1 Apr 2022 17:27:19 -0300 Subject: Introduce `multi_window` in `iced_winit` --- Cargo.toml | 2 +- winit/Cargo.toml | 1 + winit/src/lib.rs | 3 + winit/src/multi_window.rs | 742 ++++++++++++++++++++++++++++++++++++++++ winit/src/multi_window/state.rs | 212 ++++++++++++ 5 files changed, 959 insertions(+), 1 deletion(-) create mode 100644 winit/src/multi_window.rs create mode 100644 winit/src/multi_window/state.rs diff --git a/Cargo.toml b/Cargo.toml index adcdb79f..41f5af2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ chrome-trace = [ "iced_glow?/tracing", ] # Enables experimental multi-window support -multi_window = [] +multi_window = ["iced_winit/multi_window"] [badges] maintenance = { status = "actively-developed" } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 94aaa2ca..2152e7da 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -16,6 +16,7 @@ chrome-trace = ["trace", "tracing-chrome"] debug = ["iced_native/debug"] system = ["sysinfo"] application = [] +multi_window = [] [dependencies] window_clipboard = "0.2" diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 06674109..9b3c0a02 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -35,6 +35,9 @@ pub use iced_native::*; pub use winit; +#[cfg(feature = "multi_window")] +pub mod multi_window; + #[cfg(feature = "application")] pub mod application; pub mod clipboard; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs new file mode 100644 index 00000000..4ea7383a --- /dev/null +++ b/winit/src/multi_window.rs @@ -0,0 +1,742 @@ +//! Create interactive, native cross-platform applications. +mod state; + +pub use state::State; + +use crate::clipboard::{self, Clipboard}; +use crate::conversion; +use crate::mouse; +use crate::renderer; +use crate::widget::operation; +use crate::{ + Command, Debug, Error, Executor, Proxy, Runtime, Settings, Size, + Subscription, +}; + +use iced_futures::futures; +use iced_futures::futures::channel::mpsc; +use iced_graphics::compositor; +use iced_graphics::window; +use iced_native::program::Program; +use iced_native::user_interface::{self, UserInterface}; + +pub use iced_native::application::{Appearance, StyleSheet}; + +use std::mem::ManuallyDrop; + +/// An interactive, native cross-platform application. +/// +/// This trait is the main entrypoint of Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`]. It will run in +/// its own window. +/// +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Application: Program +where + ::Theme: 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. + fn new(flags: Self::Flags) -> (Self, Command); + + /// Returns the current title of the [`Application`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self) -> String; + + /// Returns the current [`Theme`] of the [`Application`]. + fn theme(&self) -> ::Theme; + + /// Returns the [`Style`] variation of the [`Theme`]. + fn style( + &self, + ) -> <::Theme as StyleSheet>::Style { + Default::default() + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Returns the scale factor 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`. + fn scale_factor(&self) -> f64 { + 1.0 + } + + /// Returns whether the [`Application`] should be terminated. + /// + /// By default, it returns `false`. + fn should_exit(&self) -> bool { + false + } +} + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run( + settings: Settings, + compositor_settings: C::Settings, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor + 'static, + ::Theme: StyleSheet, +{ + use futures::task; + use futures::Future; + use winit::event_loop::EventLoopBuilder; + + let mut debug = Debug::new(); + debug.startup_started(); + + let event_loop = EventLoopBuilder::with_user_event().build(); + let proxy = event_loop.create_proxy(); + + let runtime = { + let proxy = Proxy::new(event_loop.create_proxy()); + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + + Runtime::new(executor, proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let builder = settings.window.into_builder( + &application.title(), + event_loop.primary_monitor(), + settings.id, + ); + + log::info!("Window builder: {:#?}", builder); + + let window = builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window.canvas(); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let _ = body + .append_child(&canvas) + .expect("Append canvas to HTML body"); + } + + let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + + let (mut sender, receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + receiver, + init_command, + window, + settings.exit_on_close_request, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + platform::run(event_loop, move |event, _, control_flow| { + use winit::event_loop::ControlFlow; + + if let ControlFlow::ExitWithCode(_) = control_flow { + return; + } + + let event = match event { + winit::event::Event::WindowEvent { + event: + winit::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + }, + window_id, + } => Some(winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::Resized(*new_inner_size), + window_id, + }), + _ => event.to_static(), + }; + + if let Some(event) = event { + sender.start_send(event).expect("Send event"); + + let poll = instance.as_mut().poll(&mut context); + + *control_flow = match poll { + task::Poll::Pending => ControlFlow::Wait, + task::Poll::Ready(_) => ControlFlow::Exit, + }; + } + }) +} + +async fn run_instance( + mut application: A, + mut compositor: C, + mut renderer: A::Renderer, + mut runtime: Runtime, A::Message>, + mut proxy: winit::event_loop::EventLoopProxy, + mut debug: Debug, + mut receiver: mpsc::UnboundedReceiver>, + init_command: Command, + window: winit::window::Window, + exit_on_close_request: bool, +) where + A: Application + 'static, + E: Executor + 'static, + C: window::Compositor + 'static, + ::Theme: StyleSheet, +{ + use iced_futures::futures::stream::StreamExt; + use winit::event; + + let mut clipboard = Clipboard::connect(&window); + let mut cache = user_interface::Cache::default(); + let mut surface = compositor.create_surface(&window); + + let mut state = State::new(&application, &window); + let mut viewport_version = state.viewport_version(); + + let physical_size = state.physical_size(); + + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); + + run_command( + &application, + &mut cache, + &state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &window, + || compositor.fetch_information(), + ); + runtime.track(application.subscription()); + + let mut user_interface = ManuallyDrop::new(build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + )); + + let mut mouse_interaction = mouse::Interaction::default(); + let mut events = Vec::new(); + let mut messages = Vec::new(); + + debug.startup_finished(); + + while let Some(event) = receiver.next().await { + match event { + event::Event::MainEventsCleared => { + if events.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + + let (interface_state, statuses) = user_interface.update( + &events, + state.cursor_position(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + + debug.event_processing_finished(); + + for event in events.drain(..).zip(statuses.into_iter()) { + runtime.broadcast(event); + } + + if !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated, + ) + { + let mut cache = + ManuallyDrop::into_inner(user_interface).into_cache(); + + // Update application + update( + &mut application, + &mut cache, + &state, + &mut renderer, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &mut messages, + &window, + || compositor.fetch_information(), + ); + + // Update window + state.synchronize(&application, &window); + + let should_exit = application.should_exit(); + + user_interface = ManuallyDrop::new(build_user_interface( + &application, + cache, + &mut renderer, + state.logical_size(), + &mut debug, + )); + + if should_exit { + break; + } + } + + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); + debug.draw_finished(); + + if new_mouse_interaction != mouse_interaction { + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); + + mouse_interaction = new_mouse_interaction; + } + + window.request_redraw(); + } + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use iced_native::event; + + events.push(iced_native::Event::PlatformSpecific( + event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( + url, + )), + )); + } + event::Event::UserEvent(message) => { + messages.push(message); + } + event::Event::RedrawRequested(_) => { + let physical_size = state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } + + debug.render_started(); + let current_viewport_version = state.viewport_version(); + + if viewport_version != current_viewport_version { + let logical_size = state.logical_size(); + + debug.layout_started(); + user_interface = ManuallyDrop::new( + ManuallyDrop::into_inner(user_interface) + .relayout(logical_size, &mut renderer), + ); + debug.layout_finished(); + + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); + + if new_mouse_interaction != mouse_interaction { + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); + + mouse_interaction = new_mouse_interaction; + } + debug.draw_finished(); + + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); + + viewport_version = current_viewport_version; + } + + match compositor.present( + &mut renderer, + &mut surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ) { + Ok(()) => { + debug.render_finished(); + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + Err(error) => match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + _ => { + debug.render_finished(); + + // Try rendering again next frame. + window.request_redraw(); + } + }, + } + } + event::Event::WindowEvent { + event: window_event, + .. + } => { + if requests_exit(&window_event, state.modifiers()) + && exit_on_close_request + { + break; + } + + state.update(&window, &window_event, &mut debug); + + if let Some(event) = conversion::window_event( + &window_event, + state.scale_factor(), + state.modifiers(), + ) { + events.push(event); + } + } + _ => {} + } + } + + // Manually drop the user interface + drop(ManuallyDrop::into_inner(user_interface)); +} + +/// Returns true if the provided event should cause an [`Application`] to +/// exit. +pub fn requests_exit( + event: &winit::event::WindowEvent<'_>, + _modifiers: winit::event::ModifiersState, +) -> bool { + use winit::event::WindowEvent; + + match event { + WindowEvent::CloseRequested => true, + #[cfg(target_os = "macos")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if _modifiers.logo() => true, + _ => false, + } +} + +/// Builds a [`UserInterface`] for the provided [`Application`], logging +/// [`struct@Debug`] information accordingly. +pub fn build_user_interface<'a, A: Application>( + application: &'a A, + cache: user_interface::Cache, + renderer: &mut A::Renderer, + size: Size, + debug: &mut Debug, +) -> UserInterface<'a, A::Message, A::Renderer> +where + ::Theme: StyleSheet, +{ + debug.view_started(); + let view = application.view(); + debug.view_finished(); + + debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + debug.layout_finished(); + + user_interface +} + +/// Updates an [`Application`] by feeding it the provided messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`]. +pub fn update( + application: &mut A, + cache: &mut user_interface::Cache, + state: &State, + renderer: &mut A::Renderer, + runtime: &mut Runtime, A::Message>, + clipboard: &mut Clipboard, + proxy: &mut winit::event_loop::EventLoopProxy, + debug: &mut Debug, + messages: &mut Vec, + window: &winit::window::Window, + graphics_info: impl FnOnce() -> compositor::Information + Copy, +) where + ::Theme: StyleSheet, +{ + for message in messages.drain(..) { + debug.log_message(&message); + + debug.update_started(); + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + run_command( + application, + cache, + state, + renderer, + command, + runtime, + clipboard, + proxy, + debug, + window, + graphics_info, + ); + } + + let subscription = application.subscription(); + runtime.track(subscription); +} + +/// Runs the actions of a [`Command`]. +pub fn run_command( + application: &A, + cache: &mut user_interface::Cache, + state: &State, + renderer: &mut A::Renderer, + command: Command, + runtime: &mut Runtime, A::Message>, + clipboard: &mut Clipboard, + proxy: &mut winit::event_loop::EventLoopProxy, + debug: &mut Debug, + window: &winit::window::Window, + _graphics_info: impl FnOnce() -> compositor::Information + Copy, +) where + A: Application, + E: Executor, + ::Theme: StyleSheet, +{ + use iced_native::command; + use iced_native::system; + use iced_native::window; + + for action in command.actions() { + match action { + command::Action::Future(future) => { + runtime.spawn(future); + } + command::Action::Clipboard(action) => match action { + clipboard::Action::Read(tag) => { + let message = tag(clipboard.read()); + + proxy + .send_event(message) + .expect("Send message to event loop"); + } + clipboard::Action::Write(contents) => { + clipboard.write(contents); + } + }, + command::Action::Window(action) => match action { + window::Action::Resize { width, height } => { + window.set_inner_size(winit::dpi::LogicalSize { + width, + height, + }); + } + window::Action::Move { x, y } => { + window.set_outer_position(winit::dpi::LogicalPosition { + x, + y, + }); + } + window::Action::SetMode(mode) => { + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.primary_monitor(), + mode, + )); + } + window::Action::FetchMode(tag) => { + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + window::Mode::Hidden + }; + + proxy + .send_event(tag(mode)) + .expect("Send message to event loop"); + } + }, + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "system")] + { + let graphics_info = _graphics_info(); + let proxy = proxy.clone(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let message = _tag(information); + + proxy + .send_event(message) + .expect("Send message to event loop") + }); + } + } + }, + command::Action::Widget(action) => { + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(action.into_operation()); + + let mut user_interface = build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + debug, + ); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(message) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + } + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +mod platform { + pub fn run( + mut event_loop: winit::event_loop::EventLoop, + event_handler: F, + ) -> Result<(), super::Error> + where + F: 'static + + FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), + { + use winit::platform::run_return::EventLoopExtRunReturn; + + let _ = event_loop.run_return(event_handler); + + Ok(()) + } +} + +#[cfg(target_arch = "wasm32")] +mod platform { + pub fn run( + event_loop: winit::event_loop::EventLoop, + event_handler: F, + ) -> ! + where + F: 'static + + FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), + { + event_loop.run(event_handler) + } +} diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs new file mode 100644 index 00000000..2d120ca1 --- /dev/null +++ b/winit/src/multi_window/state.rs @@ -0,0 +1,212 @@ +use crate::application::{self, StyleSheet as _}; +use crate::conversion; +use crate::multi_window::Application; +use crate::{Color, Debug, Point, Size, Viewport}; + +use std::marker::PhantomData; +use winit::event::{Touch, WindowEvent}; +use winit::window::Window; + +/// The state of a windowed [`Application`]. +#[allow(missing_debug_implementations)] +pub struct State +where + ::Theme: application::StyleSheet, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_version: usize, + cursor_position: winit::dpi::PhysicalPosition, + modifiers: winit::event::ModifiersState, + theme: ::Theme, + appearance: application::Appearance, + application: PhantomData, +} + +impl State +where + ::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`] and window. + pub fn new(application: &A, window: &Window) -> Self { + let title = application.title(); + let scale_factor = application.scale_factor(); + let theme = application.theme(); + 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, + // TODO: Encode cursor availability in the type-system + cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), + modifiers: winit::event::ModifiersState::default(), + theme, + appearance, + application: PhantomData, + } + } + + /// 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) -> usize { + self.viewport_version + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size { + 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_position(&self) -> Point { + conversion::cursor_position( + self.cursor_position, + self.viewport.scale_factor(), + ) + } + + /// 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) -> &::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 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 = *position; + } + WindowEvent::CursorLeft { .. } => { + // TODO: Encode cursor availability in the type-system + self.cursor_position = + winit::dpi::PhysicalPosition::new(-1.0, -1.0); + } + 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 [`Application::update`]. + /// + /// [`Application::update`]: crate::Program::update + pub fn synchronize(&mut self, application: &A, window: &Window) { + // Update window title + let new_title = application.title(); + + if self.title != new_title { + window.set_title(&new_title); + + self.title = new_title; + } + + // Update scale factor + let new_scale_factor = application.scale_factor(); + + if self.scale_factor != new_scale_factor { + let size = window.inner_size(); + + self.viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor() * new_scale_factor, + ); + + self.scale_factor = new_scale_factor; + } + + // Update theme and appearance + self.theme = application.theme(); + self.appearance = self.theme.appearance(application.style()); + } +} -- cgit From b896e41c6e03f1447419194ce41d15fb0db39d96 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 1 Apr 2022 17:39:08 -0300 Subject: Unify `Application` and `Program` Instead of creating a separate `multi_window::Program`, the new `multi_window::Application` unifies both traits --- src/multi_window/application.rs | 28 +++++++++++----------------- winit/src/multi_window.rs | 28 ++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index db41d54a..fa0c15b1 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -132,7 +132,7 @@ pub trait Application: Sized { ..crate::renderer::Settings::from_env() }; - Ok(crate::runtime::application::run::< + Ok(crate::runtime::multi_window::run::< Instance, Self::Executor, crate::renderer::window::Compositor, @@ -142,28 +142,14 @@ pub trait Application: Sized { struct Instance(A); -impl iced_winit::Program for Instance +impl crate::runtime::multi_window::Application for Instance where A: Application, { + type Flags = A::Flags; type Renderer = crate::Renderer; type Message = A::Message; - fn update(&mut self, message: Self::Message) -> Command { - self.0.update(message) - } - - fn view(&self) -> Element<'_, Self::Message, Self::Renderer> { - self.0.view() - } -} - -impl crate::runtime::Application for Instance -where - A: Application, -{ - type Flags = A::Flags; - fn new(flags: Self::Flags) -> (Self, Command) { let (app, command) = A::new(flags); @@ -174,6 +160,14 @@ where self.0.title() } + fn update(&mut self, message: Self::Message) -> Command { + self.0.update(message) + } + + fn view(&self) -> Element<'_, Self::Message, Self::Renderer> { + self.0.view() + } + fn theme(&self) -> A::Theme { self.0.theme() } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 4ea7383a..b519e471 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -9,15 +9,14 @@ use crate::mouse; use crate::renderer; use crate::widget::operation; use crate::{ - Command, Debug, Error, Executor, Proxy, Runtime, Settings, Size, - Subscription, + Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, + Settings, Size, Subscription, }; use iced_futures::futures; use iced_futures::futures::channel::mpsc; use iced_graphics::compositor; use iced_graphics::window; -use iced_native::program::Program; use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; @@ -35,13 +34,34 @@ use std::mem::ManuallyDrop; /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. -pub trait Application: Program +pub trait Application: Sized where ::Theme: StyleSheet, { /// The data needed to initialize your [`Application`]. type Flags; + /// The graphics backend to use to draw the [`Program`]. + type Renderer: 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; + + /// Returns the widgets to display in the [`Program`]. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view(&self) -> Element<'_, Self::Message, Self::Renderer>; + /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// -- cgit From 12538d3c5be08f2109f1dc61936ceb2fda0fc5c6 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 15 Jun 2022 16:46:37 -0300 Subject: Use map of windows internally --- winit/src/multi_window.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index b519e471..8a967207 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -21,6 +21,7 @@ use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; +use std::collections::HashMap; use std::mem::ManuallyDrop; /// An interactive, native cross-platform application. @@ -169,6 +170,10 @@ where .build(&event_loop) .map_err(Error::WindowCreationFailed)?; + let windows: HashMap = + HashMap::from([(0usize, window)]); + let window = windows.values().next().expect("No window found"); + #[cfg(target_arch = "wasm32")] { use winit::platform::web::WindowExtWebSys; @@ -197,7 +202,7 @@ where debug, receiver, init_command, - window, + windows, settings.exit_on_close_request, )); @@ -247,7 +252,7 @@ async fn run_instance( mut debug: Debug, mut receiver: mpsc::UnboundedReceiver>, init_command: Command, - window: winit::window::Window, + windows: HashMap, exit_on_close_request: bool, ) where A: Application + 'static, @@ -258,11 +263,12 @@ async fn run_instance( use iced_futures::futures::stream::StreamExt; use winit::event; - let mut clipboard = Clipboard::connect(&window); + let window = windows.values().next().expect("No window found"); + let mut clipboard = Clipboard::connect(window); let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface(&window); - let mut state = State::new(&application, &window); + let mut state = State::new(&application, window); let mut viewport_version = state.viewport_version(); let physical_size = state.physical_size(); @@ -283,7 +289,7 @@ async fn run_instance( &mut clipboard, &mut proxy, &mut debug, - &window, + &windows, || compositor.fetch_information(), ); runtime.track(application.subscription()); @@ -345,12 +351,12 @@ async fn run_instance( &mut proxy, &mut debug, &mut messages, - &window, + &windows, || compositor.fetch_information(), ); // Update window - state.synchronize(&application, &window); + state.synchronize(&application, window); let should_exit = application.should_exit(); @@ -487,7 +493,7 @@ async fn run_instance( break; } - state.update(&window, &window_event, &mut debug); + state.update(window, &window_event, &mut debug); if let Some(event) = conversion::window_event( &window_event, @@ -564,7 +570,7 @@ pub fn update( proxy: &mut winit::event_loop::EventLoopProxy, debug: &mut Debug, messages: &mut Vec, - window: &winit::window::Window, + windows: &HashMap, graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where ::Theme: StyleSheet, @@ -586,7 +592,7 @@ pub fn update( clipboard, proxy, debug, - window, + windows, graphics_info, ); } @@ -606,7 +612,7 @@ pub fn run_command( clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy, debug: &mut Debug, - window: &winit::window::Window, + windows: &HashMap, _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where A: Application, @@ -617,6 +623,8 @@ pub fn run_command( use iced_native::system; use iced_native::window; + let window = windows.values().next().expect("No window found"); + for action in command.actions() { match action { command::Action::Future(future) => { -- cgit From 5919325d9b9ebf86f7358059201e6fc1af2412d8 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 15 Jun 2022 20:01:50 -0300 Subject: Internally wrap `Message` with a `Event` enum --- winit/src/multi_window.rs | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 8a967207..61984b93 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -13,8 +13,8 @@ use crate::{ Settings, Size, Subscription, }; -use iced_futures::futures; use iced_futures::futures::channel::mpsc; +use iced_futures::futures::{self, FutureExt}; use iced_graphics::compositor; use iced_graphics::window; use iced_native::user_interface::{self, UserInterface}; @@ -24,6 +24,16 @@ pub use iced_native::application::{Appearance, StyleSheet}; use std::collections::HashMap; use std::mem::ManuallyDrop; +/// TODO(derezzedex) +// This is the an wrapper around the `Application::Message` associate type +// to allows the `shell` to create internal messages, while still having +// the current user specified custom messages. +#[derive(Debug, Clone)] +pub enum Event { + /// An [`Application`] generated message + Application(Message), +} + /// An interactive, native cross-platform application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -247,10 +257,12 @@ async fn run_instance( mut application: A, mut compositor: C, mut renderer: A::Renderer, - mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut runtime: Runtime>, Event>, + mut proxy: winit::event_loop::EventLoopProxy>, mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver>, + mut receiver: mpsc::UnboundedReceiver< + winit::event::Event<'_, Event>, + >, init_command: Command, windows: HashMap, exit_on_close_request: bool, @@ -292,7 +304,7 @@ async fn run_instance( &windows, || compositor.fetch_information(), ); - runtime.track(application.subscription()); + runtime.track(application.subscription().map(Event::Application)); let mut user_interface = ManuallyDrop::new(build_user_interface( &application, @@ -406,6 +418,7 @@ async fn run_instance( )); } event::Event::UserEvent(message) => { + let Event::Application(message) = message; messages.push(message); } event::Event::RedrawRequested(_) => { @@ -565,9 +578,9 @@ pub fn update( cache: &mut user_interface::Cache, state: &State, renderer: &mut A::Renderer, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, windows: &HashMap, @@ -597,7 +610,7 @@ pub fn update( ); } - let subscription = application.subscription(); + let subscription = application.subscription().map(Event::Application); runtime.track(subscription); } @@ -608,9 +621,9 @@ pub fn run_command( state: &State, renderer: &mut A::Renderer, command: Command, - runtime: &mut Runtime, A::Message>, + runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, windows: &HashMap, _graphics_info: impl FnOnce() -> compositor::Information + Copy, @@ -628,14 +641,14 @@ pub fn run_command( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(future); + runtime.spawn(Box::pin(future.map(Event::Application))); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { let message = tag(clipboard.read()); proxy - .send_event(message) + .send_event(Event::Application(message)) .expect("Send message to event loop"); } clipboard::Action::Write(contents) => { @@ -670,7 +683,7 @@ pub fn run_command( }; proxy - .send_event(tag(mode)) + .send_event(Event::Application(tag(mode))) .expect("Send message to event loop"); } }, @@ -688,7 +701,7 @@ pub fn run_command( let message = _tag(information); proxy - .send_event(message) + .send_event(Event::Application(message)) .expect("Send message to event loop") }); } @@ -713,7 +726,7 @@ pub fn run_command( operation::Outcome::None => {} operation::Outcome::Some(message) => { proxy - .send_event(message) + .send_event(Event::Application(message)) .expect("Send message to event loop"); } operation::Outcome::Chain(next) => { -- cgit From 00d6baf861ba57984a341283823e9fea3c262130 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 15 Jun 2022 20:10:15 -0300 Subject: fix: temporalily remove the unsafe pointer `HWND` --- winit/src/settings.rs | 6 +++--- winit/src/settings/windows.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 9bbdef5c..94d243a7 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -154,9 +154,9 @@ impl Window { { use winit::platform::windows::WindowBuilderExtWindows; - if let Some(parent) = self.platform_specific.parent { - window_builder = window_builder.with_parent_window(parent); - } + // if let Some(parent) = self.platform_specific.parent { + // window_builder = window_builder.with_parent_window(parent); + // } window_builder = window_builder .with_drag_and_drop(self.platform_specific.drag_and_drop); diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs index ff03a9c5..0891ec2c 100644 --- a/winit/src/settings/windows.rs +++ b/winit/src/settings/windows.rs @@ -4,7 +4,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { /// Parent window - pub parent: Option, + // pub parent: Option, /// Drag and drop support pub drag_and_drop: bool, @@ -13,7 +13,7 @@ pub struct PlatformSpecific { impl Default for PlatformSpecific { fn default() -> Self { Self { - parent: None, + // parent: None, drag_and_drop: true, } } -- cgit From 8fdd5ee8b60e551088d4a18fb1d58b6c3e62ba7d Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 15 Jun 2022 20:38:51 -0300 Subject: Synchronize window list with `windows` method --- winit/src/multi_window.rs | 72 +++++++++++++++++++++++++++++++++-------- winit/src/multi_window/state.rs | 22 +++++++++++-- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 61984b93..900ee92a 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -7,6 +7,7 @@ use crate::clipboard::{self, Clipboard}; use crate::conversion; use crate::mouse; use crate::renderer; +use crate::settings; use crate::widget::operation; use crate::{ Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, @@ -28,10 +29,17 @@ use std::mem::ManuallyDrop; // This is the an wrapper around the `Application::Message` associate type // to allows the `shell` to create internal messages, while still having // the current user specified custom messages. -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum Event { /// An [`Application`] generated message Application(Message), + + /// TODO(derezzedex) + // Create a wrapper variant of `window::Event` type instead + // (maybe we should also allow users to listen/react to those internal messages?) + NewWindow(usize, settings::Window), + /// TODO(derezzedex) + WindowCreated(usize, winit::window::Window), } /// An interactive, native cross-platform application. @@ -218,7 +226,7 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); - platform::run(event_loop, move |event, _, control_flow| { + platform::run(event_loop, move |event, event_loop, control_flow| { use winit::event_loop::ControlFlow; if let ControlFlow::ExitWithCode(_) = control_flow { @@ -237,6 +245,21 @@ where event: winit::event::WindowEvent::Resized(*new_inner_size), window_id, }), + winit::event::Event::UserEvent(Event::NewWindow(id, settings)) => { + // TODO(derezzedex) + let window = settings + .into_builder( + "fix window title", + event_loop.primary_monitor(), + None, + ) + .build(event_loop) + .expect("Failed to build window"); + + Some(winit::event::Event::UserEvent(Event::WindowCreated( + id, window, + ))) + } _ => event.to_static(), }; @@ -264,7 +287,7 @@ async fn run_instance( winit::event::Event<'_, Event>, >, init_command: Command, - windows: HashMap, + mut windows: HashMap, exit_on_close_request: bool, ) where A: Application + 'static, @@ -275,12 +298,18 @@ async fn run_instance( use iced_futures::futures::stream::StreamExt; use winit::event; - let window = windows.values().next().expect("No window found"); - let mut clipboard = Clipboard::connect(window); + // TODO(derezzedex) + let mut clipboard = + Clipboard::connect(windows.values().next().expect("No window found")); let mut cache = user_interface::Cache::default(); - let mut surface = compositor.create_surface(&window); + let mut surface = compositor + .create_surface(&windows.values().next().expect("No window found")); - let mut state = State::new(&application, window); + // TODO(derezzedex) + let mut state = State::new( + &application, + windows.values().next().expect("No window found"), + ); let mut viewport_version = state.viewport_version(); let physical_size = state.physical_size(); @@ -368,7 +397,7 @@ async fn run_instance( ); // Update window - state.synchronize(&application, window); + state.synchronize(&application, &windows, &proxy); let should_exit = application.should_exit(); @@ -396,6 +425,8 @@ async fn run_instance( ); debug.draw_finished(); + // TODO(derezzedex) + let window = windows.values().next().expect("No window found"); if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( new_mouse_interaction, @@ -417,10 +448,15 @@ async fn run_instance( )), )); } - event::Event::UserEvent(message) => { - let Event::Application(message) = message; - messages.push(message); - } + event::Event::UserEvent(event) => match event { + Event::Application(message) => { + messages.push(message); + } + Event::WindowCreated(id, window) => { + let _ = windows.insert(id, window); + } + Event::NewWindow(_, _) => unreachable!(), + }, event::Event::RedrawRequested(_) => { let physical_size = state.physical_size(); @@ -451,6 +487,9 @@ async fn run_instance( state.cursor_position(), ); + // TODO(derezzedex) + let window = + windows.values().next().expect("No window found"); if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( new_mouse_interaction, @@ -491,7 +530,12 @@ async fn run_instance( debug.render_finished(); // Try rendering again next frame. - window.request_redraw(); + // TODO(derezzedex) + windows + .values() + .next() + .expect("No window found") + .request_redraw(); } }, } @@ -506,6 +550,8 @@ async fn run_instance( break; } + // TODO(derezzedex) + let window = windows.values().next().expect("No window found"); state.update(window, &window_event, &mut debug); if let Some(event) = conversion::window_event( diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2d120ca1..009a3698 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -1,10 +1,12 @@ use crate::application::{self, StyleSheet as _}; use crate::conversion; -use crate::multi_window::Application; +use crate::multi_window::{Application, Event}; use crate::{Color, Debug, Point, Size, Viewport}; +use std::collections::HashMap; use std::marker::PhantomData; use winit::event::{Touch, WindowEvent}; +use winit::event_loop::EventLoopProxy; use winit::window::Window; /// The state of a windowed [`Application`]. @@ -181,7 +183,23 @@ where /// and window after calling [`Application::update`]. /// /// [`Application::update`]: crate::Program::update - pub fn synchronize(&mut self, application: &A, window: &Window) { + pub fn synchronize( + &mut self, + application: &A, + windows: &HashMap, + proxy: &EventLoopProxy>, + ) { + let new_windows = application.windows(); + for (id, settings) in new_windows { + if !windows.contains_key(&id) { + proxy + .send_event(Event::NewWindow(id, settings)) + .expect("Failed to send message"); + } + } + + let window = windows.values().next().expect("No window found"); + // Update window title let new_title = application.title(); -- cgit From ec56c0686df1a200e37af951a3a8eca562c32a5c Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 21 Jun 2022 15:59:45 -0300 Subject: Introduce opaque `window::Id` type --- native/src/window.rs | 2 ++ native/src/window/id.rs | 16 ++++++++++++++++ src/multi_window/application.rs | 14 ++++++++++++++ winit/src/multi_window.rs | 23 +++++++++++++---------- winit/src/multi_window/state.rs | 3 ++- winit/src/window.rs | 2 +- 6 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 native/src/window/id.rs diff --git a/native/src/window.rs b/native/src/window.rs index 1b97e655..dc9e2d66 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,10 +1,12 @@ //! Build window-based GUI applications. mod action; mod event; +mod id; mod mode; mod user_attention; pub use action::Action; pub use event::Event; +pub use id::Id; pub use mode::Mode; pub use user_attention::UserAttention; diff --git a/native/src/window/id.rs b/native/src/window/id.rs new file mode 100644 index 00000000..56496aaa --- /dev/null +++ b/native/src/window/id.rs @@ -0,0 +1,16 @@ +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +#[derive(Debug, PartialEq, Eq, Hash)] +/// TODO(derezzedex) +pub struct Id(u64); + +impl Id { + /// TODO(derezzedex) + pub fn new(id: impl Hash) -> Id { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + + Id(hasher.finish()) + } +} diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index fa0c15b1..6b3f4676 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -1,3 +1,4 @@ +use crate::window; use crate::{Command, Element, Executor, Settings, Subscription}; pub use iced_native::application::{Appearance, StyleSheet}; @@ -45,6 +46,9 @@ pub trait Application: Sized { /// title of your application when necessary. fn title(&self) -> String; + /// TODO(derezzedex) + fn windows(&self) -> Vec<(window::Id, window::Settings)>; + /// Handles a __message__ and updates the state of the [`Application`]. /// /// This is where you define your __update logic__. All the __messages__, @@ -160,6 +164,16 @@ where self.0.title() } + fn windows(&self) -> Vec<(window::Id, iced_winit::settings::Window)> { + self.0 + .windows() + .into_iter() + .map(|(id, settings)| { + (id, iced_winit::settings::Window::from(settings)) + }) + .collect() + } + fn update(&mut self, message: Self::Message) -> Command { self.0.update(message) } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 900ee92a..14be4de3 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -9,6 +9,7 @@ use crate::mouse; use crate::renderer; use crate::settings; use crate::widget::operation; +use crate::window; use crate::{ Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, Settings, Size, Subscription, @@ -17,7 +18,6 @@ use crate::{ use iced_futures::futures::channel::mpsc; use iced_futures::futures::{self, FutureExt}; use iced_graphics::compositor; -use iced_graphics::window; use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; @@ -37,9 +37,9 @@ pub enum Event { /// TODO(derezzedex) // Create a wrapper variant of `window::Event` type instead // (maybe we should also allow users to listen/react to those internal messages?) - NewWindow(usize, settings::Window), + NewWindow(window::Id, settings::Window), /// TODO(derezzedex) - WindowCreated(usize, winit::window::Window), + WindowCreated(window::Id, winit::window::Window), } /// An interactive, native cross-platform application. @@ -66,6 +66,9 @@ where /// The type of __messages__ your [`Program`] will produce. type Message: std::fmt::Debug + Send; + /// TODO(derezzedex) + fn windows(&self) -> Vec<(window::Id, settings::Window)>; + /// Handles a __message__ and updates the state of the [`Program`]. /// /// This is where you define your __update logic__. All the __messages__, @@ -150,7 +153,7 @@ pub fn run( where A: Application + 'static, E: Executor + 'static, - C: window::Compositor + 'static, + C: iced_graphics::window::Compositor + 'static, ::Theme: StyleSheet, { use futures::task; @@ -188,8 +191,8 @@ where .build(&event_loop) .map_err(Error::WindowCreationFailed)?; - let windows: HashMap = - HashMap::from([(0usize, window)]); + let windows: HashMap = + HashMap::from([(window::Id::new(0usize), window)]); let window = windows.values().next().expect("No window found"); #[cfg(target_arch = "wasm32")] @@ -287,12 +290,12 @@ async fn run_instance( winit::event::Event<'_, Event>, >, init_command: Command, - mut windows: HashMap, + mut windows: HashMap, exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, - C: window::Compositor + 'static, + C: iced_graphics::window::Compositor + 'static, ::Theme: StyleSheet, { use iced_futures::futures::stream::StreamExt; @@ -629,7 +632,7 @@ pub fn update( proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, - windows: &HashMap, + windows: &HashMap, graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where ::Theme: StyleSheet, @@ -671,7 +674,7 @@ pub fn run_command( clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, - windows: &HashMap, + windows: &HashMap, _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where A: Application, diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 009a3698..dd2d25ce 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -1,6 +1,7 @@ use crate::application::{self, StyleSheet as _}; use crate::conversion; use crate::multi_window::{Application, Event}; +use crate::window; use crate::{Color, Debug, Point, Size, Viewport}; use std::collections::HashMap; @@ -186,7 +187,7 @@ where pub fn synchronize( &mut self, application: &A, - windows: &HashMap, + windows: &HashMap, proxy: &EventLoopProxy>, ) { let new_windows = application.windows(); diff --git a/winit/src/window.rs b/winit/src/window.rs index 89db3262..f2c7037a 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -2,7 +2,7 @@ use crate::command::{self, Command}; use iced_native::window; -pub use window::{Event, Mode, UserAttention}; +pub use window::{Id, Event, Mode, UserAttention}; /// Closes the current window and exits the application. pub fn close() -> Command { -- cgit From 64e21535c7e5df9a1ff94b9b9036b6ae5b5c82b0 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 28 Jun 2022 14:27:06 -0300 Subject: Fix `multi_window` example --- examples/multi_window/src/main.rs | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/multi_window/src/main.rs diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs new file mode 100644 index 00000000..0ba6a591 --- /dev/null +++ b/examples/multi_window/src/main.rs @@ -0,0 +1,58 @@ +use iced::multi_window::Application; +use iced::pure::{button, column, text, Element}; +use iced::{window, Alignment, Command, Settings}; + +pub fn main() -> iced::Result { + Counter::run(Settings::default()) +} + +struct Counter { + value: i32, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + IncrementPressed, + DecrementPressed, +} + +impl Application for Counter { + type Flags = (); + type Executor = iced::executor::Default; + type Message = Message; + + fn new(_flags: ()) -> (Self, Command) { + (Self { value: 0 }, Command::none()) + } + + fn title(&self) -> String { + String::from("MultiWindow - Iced") + } + + fn windows(&self) -> Vec<(window::Id, iced::window::Settings)> { + todo!() + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::IncrementPressed => { + self.value += 1; + } + Message::DecrementPressed => { + self.value -= 1; + } + } + + Command::none() + } + + fn view(&self) -> Element { + column() + .padding(20) + .align_items(Alignment::Center) + .push(button("Increment").on_press(Message::IncrementPressed)) + .push(text(self.value.to_string()).size(50)) + .push(button("Decrement").on_press(Message::DecrementPressed)) + .into() + } +} -- cgit From 97914daaab477ce47a8329f07958332b5caa4ed0 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 12 Jul 2022 10:26:16 -0300 Subject: what is this --- native/src/window/id.rs | 2 +- winit/src/multi_window.rs | 421 ++++++++++++++++++++++++++-------------- winit/src/multi_window/state.rs | 16 +- 3 files changed, 285 insertions(+), 154 deletions(-) diff --git a/native/src/window/id.rs b/native/src/window/id.rs index 56496aaa..0ba355af 100644 --- a/native/src/window/id.rs +++ b/native/src/window/id.rs @@ -1,7 +1,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// TODO(derezzedex) pub struct Id(u64); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 14be4de3..82ee30ed 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -301,50 +301,63 @@ async fn run_instance( use iced_futures::futures::stream::StreamExt; use winit::event; - // TODO(derezzedex) let mut clipboard = Clipboard::connect(windows.values().next().expect("No window found")); let mut cache = user_interface::Cache::default(); - let mut surface = compositor - .create_surface(&windows.values().next().expect("No window found")); + let mut window_ids: HashMap<_, _> = windows + .iter() + .map(|(&id, window)| (window.id(), id)) + .collect(); - // TODO(derezzedex) - let mut state = State::new( - &application, - windows.values().next().expect("No window found"), - ); - let mut viewport_version = state.viewport_version(); + let mut states = HashMap::new(); + let mut interfaces = ManuallyDrop::new(HashMap::new()); - let physical_size = state.physical_size(); + for (&id, window) in windows.keys().zip(windows.values()) { + let mut surface = compositor.create_surface(window); - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); + let state = State::new(&application, window); - run_command( - &application, - &mut cache, - &state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &windows, - || compositor.fetch_information(), - ); - runtime.track(application.subscription().map(Event::Application)); + let physical_size = state.physical_size(); - let mut user_interface = ManuallyDrop::new(build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - )); + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); + + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + ); + + let window_state: WindowState = WindowState { surface, state }; + + let _ = states.insert(id, window_state); + let _ = interfaces.insert(id, user_interface); + } + + { + // TODO(derezzedex) + let window_state = states.values().next().expect("No state found"); + + run_command( + &application, + &mut cache, + &window_state.state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &windows, + || compositor.fetch_information(), + ); + } + runtime.track(application.subscription().map(Event::Application)); let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); @@ -352,93 +365,118 @@ async fn run_instance( debug.startup_finished(); - while let Some(event) = receiver.next().await { + 'main: while let Some(event) = receiver.next().await { match event { event::Event::MainEventsCleared => { - if events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); + dbg!(states.keys().collect::>()); + for id in states.keys().copied().collect::>() { + let cursor_position = + states.get(&id).unwrap().state.cursor_position(); + let window = windows.get(&id).unwrap(); + + if events.is_empty() && messages.is_empty() { + continue; + } - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor_position(), - &mut renderer, - &mut clipboard, - &mut messages, - ); + debug.event_processing_started(); + + let (interface_state, statuses) = { + let user_interface = interfaces.get_mut(&id).unwrap(); + user_interface.update( + &events, + cursor_position, + &mut renderer, + &mut clipboard, + &mut messages, + ) + }; - debug.event_processing_finished(); + debug.event_processing_finished(); - for event in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event); - } + // TODO(derezzedex): only drain events for this window + for event in events.drain(..).zip(statuses.into_iter()) { + runtime.broadcast(event); + } - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated, - ) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - update( - &mut application, - &mut cache, - &state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &mut messages, - &windows, - || compositor.fetch_information(), - ); + if !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated, + ) + { + let state = &mut states.get_mut(&id).unwrap().state; + let pure_states: HashMap<_, _> = + ManuallyDrop::into_inner(interfaces) + .drain() + .map( + |(id, interface): ( + window::Id, + UserInterface<'_, _, _>, + )| { + (id, interface.into_cache()) + }, + ) + .collect(); + + // Update application + update( + &mut application, + &mut cache, + state, + &mut renderer, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &mut messages, + &windows, + || compositor.fetch_information(), + ); + + // Update window + state.synchronize(&application, &windows, &proxy); + + let should_exit = application.should_exit(); + + interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + )); - // Update window - state.synchronize(&application, &windows, &proxy); + if should_exit { + break 'main; + } + } - let should_exit = application.should_exit(); + debug.draw_started(); + let new_mouse_interaction = { + let user_interface = interfaces.get_mut(&id).unwrap(); + let state = &states.get(&id).unwrap().state; + + user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ) + }; + debug.draw_finished(); - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); + if new_mouse_interaction != mouse_interaction { + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); - if should_exit { - break; + mouse_interaction = new_mouse_interaction; } - } - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); - debug.draw_finished(); - - // TODO(derezzedex) - let window = windows.values().next().expect("No window found"); - if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - mouse_interaction = new_mouse_interaction; + window.request_redraw(); } - - window.request_redraw(); } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), @@ -456,43 +494,81 @@ async fn run_instance( messages.push(message); } Event::WindowCreated(id, window) => { + let mut surface = compositor.create_surface(&window); + + let state = State::new(&application, &window); + + let physical_size = state.physical_size(); + + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); + + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + ); + + let window_state: WindowState = + WindowState { surface, state }; + + let _ = states.insert(id, window_state); + let _ = interfaces.insert(id, user_interface); + let _ = window_ids.insert(window.id(), id); let _ = windows.insert(id, window); } Event::NewWindow(_, _) => unreachable!(), }, - event::Event::RedrawRequested(_) => { - let physical_size = state.physical_size(); + event::Event::RedrawRequested(id) => { + let window_state = window_ids + .get(&id) + .and_then(|id| states.get_mut(id)) + .unwrap(); + + let mut user_interface = window_ids + .get(&id) + .and_then(|id| interfaces.remove(id)) + .unwrap(); + + let physical_size = window_state.state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { continue; } debug.render_started(); - let current_viewport_version = state.viewport_version(); - if viewport_version != current_viewport_version { - let logical_size = state.logical_size(); + if window_state.state.viewport_changed() { + let logical_size = window_state.state.logical_size(); debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); + user_interface = + user_interface.relayout(logical_size, &mut renderer); debug.layout_finished(); debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); + let new_mouse_interaction = { + let state = &window_state.state; + + user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ) + }; - // TODO(derezzedex) - let window = - windows.values().next().expect("No window found"); + let window = window_ids + .get(&id) + .and_then(|id| windows.get(id)) + .unwrap(); if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( new_mouse_interaction, @@ -502,20 +578,21 @@ async fn run_instance( } debug.draw_finished(); + let _ = interfaces + .insert(*window_ids.get(&id).unwrap(), user_interface); + compositor.configure_surface( - &mut surface, + &mut window_state.surface, physical_size.width, physical_size.height, ); - - viewport_version = current_viewport_version; } match compositor.present( &mut renderer, - &mut surface, - state.viewport(), - state.background_color(), + &mut window_state.surface, + window_state.state.viewport(), + window_state.state.background_color(), &debug.overlay(), ) { Ok(()) => { @@ -545,22 +622,30 @@ async fn run_instance( } event::Event::WindowEvent { event: window_event, - .. + window_id, } => { - if requests_exit(&window_event, state.modifiers()) + // dbg!(window_id); + let window = window_ids + .get(&window_id) + .and_then(|id| windows.get(id)) + .unwrap(); + let window_state = window_ids + .get(&window_id) + .and_then(|id| states.get_mut(id)) + .unwrap(); + + if requests_exit(&window_event, window_state.state.modifiers()) && exit_on_close_request { break; } - // TODO(derezzedex) - let window = windows.values().next().expect("No window found"); - state.update(window, &window_event, &mut debug); + window_state.state.update(window, &window_event, &mut debug); if let Some(event) = conversion::window_event( &window_event, - state.scale_factor(), - state.modifiers(), + window_state.state.scale_factor(), + window_state.state.modifiers(), ) { events.push(event); } @@ -570,7 +655,7 @@ async fn run_instance( } // Manually drop the user interface - drop(ManuallyDrop::into_inner(user_interface)); + // drop(ManuallyDrop::into_inner(user_interface)); } /// Returns true if the provided event should cause an [`Application`] to @@ -791,6 +876,54 @@ pub fn run_command( } } +struct WindowState +where + A: Application, + C: iced_graphics::window::Compositor, + ::Theme: StyleSheet, +{ + surface: ::Surface, + state: State, +} + +fn build_user_interfaces<'a, A, C>( + application: &'a A, + renderer: &mut A::Renderer, + debug: &mut Debug, + states: &HashMap>, + mut pure_states: HashMap, +) -> HashMap< + window::Id, + UserInterface< + 'a, + ::Message, + ::Renderer, + >, +> +where + A: Application + 'static, + C: iced_graphics::window::Compositor + 'static, + ::Theme: StyleSheet, +{ + let mut interfaces = HashMap::new(); + + for (id, pure_state) in pure_states.drain() { + let state = &states.get(&id).unwrap().state; + + let user_interface = build_user_interface( + application, + pure_state, + renderer, + state.logical_size(), + debug, + ); + + let _ = interfaces.insert(id, user_interface); + } + + interfaces +} + #[cfg(not(target_arch = "wasm32"))] mod platform { pub fn run( diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index dd2d25ce..d22de961 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -19,7 +19,7 @@ where title: String, scale_factor: f64, viewport: Viewport, - viewport_version: usize, + viewport_changed: bool, cursor_position: winit::dpi::PhysicalPosition, modifiers: winit::event::ModifiersState, theme: ::Theme, @@ -51,7 +51,7 @@ where title, scale_factor, viewport, - viewport_version: 0, + viewport_changed: false, // TODO: Encode cursor availability in the type-system cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), modifiers: winit::event::ModifiersState::default(), @@ -66,11 +66,9 @@ where &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) -> usize { - self.viewport_version + /// TODO(derezzedex) + pub fn viewport_changed(&self) -> bool { + self.viewport_changed } /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. @@ -133,7 +131,7 @@ where window.scale_factor() * self.scale_factor, ); - self.viewport_version = self.viewport_version.wrapping_add(1); + self.viewport_changed = true; } WindowEvent::ScaleFactorChanged { scale_factor: new_scale_factor, @@ -147,7 +145,7 @@ where new_scale_factor * self.scale_factor, ); - self.viewport_version = self.viewport_version.wrapping_add(1); + self.viewport_changed = true; } WindowEvent::CursorMoved { position, .. } | WindowEvent::Touch(Touch { -- cgit From 8f53df560e1bde33e874977e5115cd0f9301640d Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 14 Jul 2022 10:13:37 -0300 Subject: fix: temporarily add `window::Id` to events internaly --- winit/src/multi_window.rs | 53 +++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 82ee30ed..6c2dc888 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -368,13 +368,27 @@ async fn run_instance( 'main: while let Some(event) = receiver.next().await { match event { event::Event::MainEventsCleared => { - dbg!(states.keys().collect::>()); for id in states.keys().copied().collect::>() { + let (filtered, remaining): (Vec<_>, Vec<_>) = events + .iter() + .cloned() + .partition( + |(window_id, _event): &( + Option, + iced_native::event::Event, + )| { + *window_id == Some(id) || *window_id == None + }, + ); + + events.retain(|el| remaining.contains(el)); + let filtered: Vec<_> = filtered.into_iter().map(|(_id, event)| event.clone()).collect(); + let cursor_position = states.get(&id).unwrap().state.cursor_position(); let window = windows.get(&id).unwrap(); - if events.is_empty() && messages.is_empty() { + if filtered.is_empty() && messages.is_empty() { continue; } @@ -383,7 +397,7 @@ async fn run_instance( let (interface_state, statuses) = { let user_interface = interfaces.get_mut(&id).unwrap(); user_interface.update( - &events, + &filtered, cursor_position, &mut renderer, &mut clipboard, @@ -393,11 +407,11 @@ async fn run_instance( debug.event_processing_finished(); - // TODO(derezzedex): only drain events for this window - for event in events.drain(..).zip(statuses.into_iter()) { + for event in filtered.into_iter().zip(statuses.into_iter()) { runtime.broadcast(event); } + // TODO(derezzedex): Should we redraw every window? We can't know what changed. if !messages.is_empty() || matches!( interface_state, @@ -475,18 +489,22 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - window.request_redraw(); + for window in windows.values(){ + window.request_redraw(); + } } } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), )) => { use iced_native::event; - - events.push(iced_native::Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), + events.push(( + None, + iced_native::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ), )); } event::Event::UserEvent(event) => match event { @@ -529,12 +547,6 @@ async fn run_instance( .get(&id) .and_then(|id| states.get_mut(id)) .unwrap(); - - let mut user_interface = window_ids - .get(&id) - .and_then(|id| interfaces.remove(id)) - .unwrap(); - let physical_size = window_state.state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -544,6 +556,11 @@ async fn run_instance( debug.render_started(); if window_state.state.viewport_changed() { + let mut user_interface = window_ids + .get(&id) + .and_then(|id| interfaces.remove(id)) + .unwrap(); + let logical_size = window_state.state.logical_size(); debug.layout_started(); @@ -647,7 +664,7 @@ async fn run_instance( window_state.state.scale_factor(), window_state.state.modifiers(), ) { - events.push(event); + events.push((window_ids.get(&window_id).cloned(), event)); } } _ => {} -- cgit From 01bad4f89654d65b0d6a65a8df99c387cbadf7fe Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 14 Jul 2022 10:37:33 -0300 Subject: duplicate `pane_grid` example to `multi_window` --- examples/multi_window/Cargo.toml | 12 ++ examples/multi_window/src/main.rs | 370 +++++++++++++++++++++++++++++++++++--- 2 files changed, 355 insertions(+), 27 deletions(-) create mode 100644 examples/multi_window/Cargo.toml diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml new file mode 100644 index 00000000..9c3d0f21 --- /dev/null +++ b/examples/multi_window/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "multi_window" +version = "0.1.0" +authors = ["Richard Custodio "] +edition = "2021" +publish = false +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +iced = { path = "../..", features = ["debug", "multi_window"] } +iced_native = { path = "../../native" } +iced_lazy = { path = "../../lazy" } \ No newline at end of file diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 0ba6a591..ae8fa22b 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,58 +1,374 @@ -use iced::multi_window::Application; -use iced::pure::{button, column, text, Element}; -use iced::{window, Alignment, Command, Settings}; +use iced::alignment::{self, Alignment}; +use iced::executor; +use iced::keyboard; +use iced::theme::{self, Theme}; +use iced::widget::pane_grid::{self, PaneGrid}; +use iced::widget::{button, column, container, row, scrollable, text}; +use iced::{ + Application, Color, Command, Element, Length, Settings, Size, Subscription, +}; +use iced_lazy::responsive; +use iced_native::{event, subscription, Event}; pub fn main() -> iced::Result { - Counter::run(Settings::default()) + Example::run(Settings::default()) } -struct Counter { - value: i32, +struct Example { + panes: pane_grid::State, + panes_created: usize, + focus: Option, } #[derive(Debug, Clone, Copy)] enum Message { - IncrementPressed, - DecrementPressed, + Split(pane_grid::Axis, pane_grid::Pane), + SplitFocused(pane_grid::Axis), + FocusAdjacent(pane_grid::Direction), + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + Resized(pane_grid::ResizeEvent), + TogglePin(pane_grid::Pane), + Close(pane_grid::Pane), + CloseFocused, } -impl Application for Counter { - type Flags = (); - type Executor = iced::executor::Default; +impl Application for Example { type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); fn new(_flags: ()) -> (Self, Command) { - (Self { value: 0 }, Command::none()) - } + let (panes, _) = pane_grid::State::new(Pane::new(0)); - fn title(&self) -> String { - String::from("MultiWindow - Iced") + ( + Example { + panes, + panes_created: 1, + focus: None, + }, + Command::none(), + ) } - fn windows(&self) -> Vec<(window::Id, iced::window::Settings)> { - todo!() + fn title(&self) -> String { + String::from("Pane grid - Iced") } fn update(&mut self, message: Message) -> Command { match message { - Message::IncrementPressed => { - self.value += 1; + Message::Split(axis, pane) => { + let result = self.panes.split( + axis, + &pane, + Pane::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + Message::SplitFocused(axis) => { + if let Some(pane) = self.focus { + let result = self.panes.split( + axis, + &pane, + Pane::new(self.panes_created), + ); + + if let Some((pane, _)) = result { + self.focus = Some(pane); + } + + self.panes_created += 1; + } + } + Message::FocusAdjacent(direction) => { + if let Some(pane) = self.focus { + if let Some(adjacent) = + self.panes.adjacent(&pane, direction) + { + self.focus = Some(adjacent); + } + } + } + Message::Clicked(pane) => { + self.focus = Some(pane); + } + Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { + self.panes.resize(&split, ratio); + } + Message::Dragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + self.panes.swap(&pane, &target); } - Message::DecrementPressed => { - self.value -= 1; + Message::Dragged(_) => {} + Message::TogglePin(pane) => { + if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane) + { + *is_pinned = !*is_pinned; + } + } + Message::Close(pane) => { + if let Some((_, sibling)) = self.panes.close(&pane) { + self.focus = Some(sibling); + } + } + Message::CloseFocused => { + if let Some(pane) = self.focus { + if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane) + { + if !is_pinned { + if let Some((_, sibling)) = self.panes.close(&pane) + { + self.focus = Some(sibling); + } + } + } + } } } Command::none() } + fn subscription(&self) -> Subscription { + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.command() => handle_hotkey(key_code), + _ => None, + } + }) + } + fn view(&self) -> Element { - column() - .padding(20) - .align_items(Alignment::Center) - .push(button("Increment").on_press(Message::IncrementPressed)) - .push(text(self.value.to_string()).size(50)) - .push(button("Decrement").on_press(Message::DecrementPressed)) + let focus = self.focus; + let total_panes = self.panes.len(); + + let pane_grid = PaneGrid::new(&self.panes, |id, pane| { + let is_focused = focus == Some(id); + + let pin_button = button( + text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), + ) + .on_press(Message::TogglePin(id)) + .padding(3); + + let title = row![ + pin_button, + "Pane", + text(pane.id.to_string()).style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }), + ] + .spacing(5); + + let title_bar = pane_grid::TitleBar::new(title) + .controls(view_controls(id, total_panes, pane.is_pinned)) + .padding(10) + .style(if is_focused { + style::title_bar_focused + } else { + style::title_bar_active + }); + + pane_grid::Content::new(responsive(move |size| { + view_content(id, total_panes, pane.is_pinned, size) + })) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); + + container(pane_grid) + .width(Length::Fill) + .height(Length::Fill) + .padding(10) .into() } } + +const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0xC7 as f32 / 255.0, + 0xC7 as f32 / 255.0, +); +const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { + use keyboard::KeyCode; + use pane_grid::{Axis, Direction}; + + let direction = match key_code { + KeyCode::Up => Some(Direction::Up), + KeyCode::Down => Some(Direction::Down), + KeyCode::Left => Some(Direction::Left), + KeyCode::Right => Some(Direction::Right), + _ => None, + }; + + match key_code { + KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), + KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), + KeyCode::W => Some(Message::CloseFocused), + _ => direction.map(Message::FocusAdjacent), + } +} + +struct Pane { + id: usize, + pub is_pinned: bool, +} + +impl Pane { + fn new(id: usize) -> Self { + Self { + id, + is_pinned: false, + } + } +} + +fn view_content<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + size: Size, +) -> Element<'a, Message> { + let button = |label, message| { + button( + text(label) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .size(16), + ) + .width(Length::Fill) + .padding(8) + .on_press(message) + }; + + let mut controls = column![ + button( + "Split horizontally", + Message::Split(pane_grid::Axis::Horizontal, pane), + ), + button( + "Split vertically", + Message::Split(pane_grid::Axis::Vertical, pane), + ) + ] + .spacing(5) + .max_width(150); + + if total_panes > 1 && !is_pinned { + controls = controls.push( + button("Close", Message::Close(pane)) + .style(theme::Button::Destructive), + ); + } + + let content = column![ + text(format!("{}x{}", size.width, size.height)).size(24), + controls, + ] + .width(Length::Fill) + .spacing(10) + .align_items(Alignment::Center); + + container(scrollable(content)) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() +} + +fn view_controls<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, +) -> Element<'a, Message> { + let mut button = button(text("Close").size(14)) + .style(theme::Button::Destructive) + .padding(3); + + if total_panes > 1 && !is_pinned { + button = button.on_press(Message::Close(pane)); + } + + button.into() +} + +mod style { + use iced::widget::container; + use iced::Theme; + + pub fn title_bar_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() + } + } + + pub fn title_bar_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + text_color: Some(palette.primary.strong.text), + background: Some(palette.primary.strong.color.into()), + ..Default::default() + } + } + + pub fn pane_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.background.strong.color, + ..Default::default() + } + } + + pub fn pane_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.primary.strong.color, + ..Default::default() + } + } +} -- cgit From 2fe58e12619186eb3755491db2bdaf02de297afb Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 21 Jul 2022 09:52:32 -0300 Subject: add `window::Id` to `view` --- src/multi_window/application.rs | 12 +++++++++--- winit/src/multi_window.rs | 35 ++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 6b3f4676..e849bf2b 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -88,7 +88,10 @@ pub trait Application: Sized { /// Returns the widgets to display in the [`Application`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, crate::Renderer>; + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, crate::Renderer>; /// Returns the scale factor of the [`Application`]. /// @@ -178,8 +181,11 @@ where self.0.update(message) } - fn view(&self) -> Element<'_, Self::Message, Self::Renderer> { - self.0.view() + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Renderer> { + self.0.view(window) } fn theme(&self) -> A::Theme { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 6c2dc888..dc00d737 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -82,7 +82,10 @@ where /// Returns the widgets to display in the [`Program`]. /// /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Renderer>; + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Renderer>; /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. @@ -331,6 +334,7 @@ async fn run_instance( &mut renderer, state.logical_size(), &mut debug, + id, ); let window_state: WindowState = WindowState { surface, state }; @@ -354,6 +358,7 @@ async fn run_instance( &mut proxy, &mut debug, &windows, + &window_ids, || compositor.fetch_information(), ); } @@ -369,10 +374,8 @@ async fn run_instance( match event { event::Event::MainEventsCleared => { for id in states.keys().copied().collect::>() { - let (filtered, remaining): (Vec<_>, Vec<_>) = events - .iter() - .cloned() - .partition( + let (filtered, remaining): (Vec<_>, Vec<_>) = + events.iter().cloned().partition( |(window_id, _event): &( Option, iced_native::event::Event, @@ -382,7 +385,10 @@ async fn run_instance( ); events.retain(|el| remaining.contains(el)); - let filtered: Vec<_> = filtered.into_iter().map(|(_id, event)| event.clone()).collect(); + let filtered: Vec<_> = filtered + .into_iter() + .map(|(_id, event)| event) + .collect(); let cursor_position = states.get(&id).unwrap().state.cursor_position(); @@ -407,7 +413,8 @@ async fn run_instance( debug.event_processing_finished(); - for event in filtered.into_iter().zip(statuses.into_iter()) { + for event in filtered.into_iter().zip(statuses.into_iter()) + { runtime.broadcast(event); } @@ -444,6 +451,7 @@ async fn run_instance( &mut debug, &mut messages, &windows, + &window_ids, || compositor.fetch_information(), ); @@ -489,7 +497,7 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - for window in windows.values(){ + for window in windows.values() { window.request_redraw(); } } @@ -530,6 +538,7 @@ async fn run_instance( &mut renderer, state.logical_size(), &mut debug, + id, ); let window_state: WindowState = @@ -707,12 +716,13 @@ pub fn build_user_interface<'a, A: Application>( renderer: &mut A::Renderer, size: Size, debug: &mut Debug, + id: window::Id, ) -> UserInterface<'a, A::Message, A::Renderer> where ::Theme: StyleSheet, { debug.view_started(); - let view = application.view(); + let view = application.view(id); debug.view_finished(); debug.layout_started(); @@ -735,6 +745,7 @@ pub fn update( debug: &mut Debug, messages: &mut Vec, windows: &HashMap, + window_ids: &HashMap, graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where ::Theme: StyleSheet, @@ -757,6 +768,7 @@ pub fn update( proxy, debug, windows, + window_ids, graphics_info, ); } @@ -777,6 +789,7 @@ pub fn run_command( proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, windows: &HashMap, + window_ids: &HashMap, _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where A: Application, @@ -787,7 +800,9 @@ pub fn run_command( use iced_native::system; use iced_native::window; + // TODO(derezzedex) let window = windows.values().next().expect("No window found"); + let id = *window_ids.get(&window.id()).unwrap(); for action in command.actions() { match action { @@ -868,6 +883,7 @@ pub fn run_command( renderer, state.logical_size(), debug, + id, ); while let Some(mut operation) = current_operation.take() { @@ -933,6 +949,7 @@ where renderer, state.logical_size(), debug, + id, ); let _ = interfaces.insert(id, user_interface); -- cgit From 3d901d5f1f8e496651a6f9881fec92bc8998d910 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 21 Jul 2022 09:52:55 -0300 Subject: create multi-windowed `pane_grid` example --- examples/multi_window/src/main.rs | 379 ++++++++++++++++++++++++++++---------- 1 file changed, 278 insertions(+), 101 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index ae8fa22b..4ad92adb 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,36 +1,55 @@ use iced::alignment::{self, Alignment}; use iced::executor; use iced::keyboard; +use iced::multi_window::Application; use iced::theme::{self, Theme}; use iced::widget::pane_grid::{self, PaneGrid}; -use iced::widget::{button, column, container, row, scrollable, text}; -use iced::{ - Application, Color, Command, Element, Length, Settings, Size, Subscription, +use iced::widget::{ + button, column, container, pick_list, row, scrollable, text, text_input, }; +use iced::window; +use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; use iced_lazy::responsive; use iced_native::{event, subscription, Event}; +use std::collections::HashMap; + pub fn main() -> iced::Result { Example::run(Settings::default()) } struct Example { - panes: pane_grid::State, + windows: HashMap, panes_created: usize, + _focused: window::Id, +} + +struct Window { + title: String, + panes: pane_grid::State, focus: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum Message { + Window(window::Id, WindowMessage), +} + +#[derive(Debug, Clone)] +enum WindowMessage { Split(pane_grid::Axis, pane_grid::Pane), SplitFocused(pane_grid::Axis), FocusAdjacent(pane_grid::Direction), Clicked(pane_grid::Pane), Dragged(pane_grid::DragEvent), + PopOut(pane_grid::Pane), Resized(pane_grid::ResizeEvent), + TitleChanged(String), + ToggleMoving(pane_grid::Pane), TogglePin(pane_grid::Pane), Close(pane_grid::Pane), CloseFocused, + SelectedWindow(pane_grid::Pane, SelectableWindow), } impl Application for Example { @@ -40,93 +59,158 @@ impl Application for Example { type Flags = (); fn new(_flags: ()) -> (Self, Command) { - let (panes, _) = pane_grid::State::new(Pane::new(0)); + let (panes, _) = + pane_grid::State::new(Pane::new(0, pane_grid::Axis::Horizontal)); + let window = Window { + panes, + focus: None, + title: String::from("Default window"), + }; ( Example { - panes, + windows: HashMap::from([(window::Id::new(0usize), window)]), panes_created: 1, - focus: None, + _focused: window::Id::new(0usize), }, Command::none(), ) } fn title(&self) -> String { - String::from("Pane grid - Iced") + String::from("Multi windowed pane grid - Iced") } fn update(&mut self, message: Message) -> Command { + let Message::Window(id, message) = message; match message { - Message::Split(axis, pane) => { - let result = self.panes.split( + WindowMessage::Split(axis, pane) => { + let window = self.windows.get_mut(&id).unwrap(); + let result = window.panes.split( axis, &pane, - Pane::new(self.panes_created), + Pane::new(self.panes_created, axis), ); if let Some((pane, _)) = result { - self.focus = Some(pane); + window.focus = Some(pane); } self.panes_created += 1; } - Message::SplitFocused(axis) => { - if let Some(pane) = self.focus { - let result = self.panes.split( + WindowMessage::SplitFocused(axis) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + let result = window.panes.split( axis, &pane, - Pane::new(self.panes_created), + Pane::new(self.panes_created, axis), ); if let Some((pane, _)) = result { - self.focus = Some(pane); + window.focus = Some(pane); } self.panes_created += 1; } } - Message::FocusAdjacent(direction) => { - if let Some(pane) = self.focus { + WindowMessage::FocusAdjacent(direction) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { if let Some(adjacent) = - self.panes.adjacent(&pane, direction) + window.panes.adjacent(&pane, direction) { - self.focus = Some(adjacent); + window.focus = Some(adjacent); } } } - Message::Clicked(pane) => { - self.focus = Some(pane); + WindowMessage::Clicked(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + window.focus = Some(pane); } - Message::Resized(pane_grid::ResizeEvent { split, ratio }) => { - self.panes.resize(&split, ratio); + WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { + let window = self.windows.get_mut(&id).unwrap(); + window.panes.resize(&split, ratio); } - Message::Dragged(pane_grid::DragEvent::Dropped { + WindowMessage::SelectedWindow(pane, selected) => { + let window = self.windows.get_mut(&id).unwrap(); + let (mut pane, _) = window.panes.close(&pane).unwrap(); + pane.is_moving = false; + + if let Some(window) = self.windows.get_mut(&selected.0) { + let (&first_pane, _) = window.panes.iter().next().unwrap(); + let result = + window.panes.split(pane.axis, &first_pane, pane); + + if let Some((pane, _)) = result { + window.focus = Some(pane); + } + + self.panes_created += 1; + } + } + WindowMessage::ToggleMoving(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.panes.get_mut(&pane) { + pane.is_moving = !pane.is_moving; + } + } + WindowMessage::TitleChanged(title) => { + let window = self.windows.get_mut(&id).unwrap(); + window.title = title; + } + WindowMessage::PopOut(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some((popped, sibling)) = window.panes.close(&pane) { + window.focus = Some(sibling); + + let (panes, _) = pane_grid::State::new(popped); + let window = Window { + panes, + focus: None, + title: format!("New window ({})", self.windows.len()), + }; + + self.windows + .insert(window::Id::new(self.windows.len()), window); + } + } + WindowMessage::Dragged(pane_grid::DragEvent::Dropped { pane, target, }) => { - self.panes.swap(&pane, &target); + let window = self.windows.get_mut(&id).unwrap(); + window.panes.swap(&pane, &target); } - Message::Dragged(_) => {} - Message::TogglePin(pane) => { - if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane) + // WindowMessage::Dragged(pane_grid::DragEvent::Picked { pane }) => { + // println!("Picked {pane:?}"); + // } + WindowMessage::Dragged(_) => {} + WindowMessage::TogglePin(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(Pane { is_pinned, .. }) = + window.panes.get_mut(&pane) { *is_pinned = !*is_pinned; } } - Message::Close(pane) => { - if let Some((_, sibling)) = self.panes.close(&pane) { - self.focus = Some(sibling); + WindowMessage::Close(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some((_, sibling)) = window.panes.close(&pane) { + window.focus = Some(sibling); } } - Message::CloseFocused => { - if let Some(pane) = self.focus { - if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane) + WindowMessage::CloseFocused => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + if let Some(Pane { is_pinned, .. }) = + window.panes.get(&pane) { if !is_pinned { - if let Some((_, sibling)) = self.panes.close(&pane) + if let Some((_, sibling)) = + window.panes.close(&pane) { - self.focus = Some(sibling); + window.focus = Some(sibling); } } } @@ -147,66 +231,106 @@ impl Application for Example { Event::Keyboard(keyboard::Event::KeyPressed { modifiers, key_code, - }) if modifiers.command() => handle_hotkey(key_code), + }) if modifiers.command() => { + handle_hotkey(key_code).map(|message| { + Message::Window(window::Id::new(0usize), message) + }) + } // TODO(derezzedex) _ => None, } }) } - fn view(&self) -> Element { - let focus = self.focus; - let total_panes = self.panes.len(); - - let pane_grid = PaneGrid::new(&self.panes, |id, pane| { - let is_focused = focus == Some(id); - - let pin_button = button( - text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), - ) - .on_press(Message::TogglePin(id)) - .padding(3); + fn windows(&self) -> Vec<(window::Id, iced::window::Settings)> { + self.windows + .iter() + .map(|(&id, _window)| (id, iced::window::Settings::default())) + .collect() + } - let title = row![ - pin_button, - "Pane", - text(pane.id.to_string()).style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }), + fn view(&self, window_id: window::Id) -> Element { + if let Some(window) = self.windows.get(&window_id) { + let focus = window.focus; + let total_panes = window.panes.len(); + + let window_controls = row![ + text_input( + "Window title", + &window.title, + WindowMessage::TitleChanged, + ), + button(text("Apply")).style(theme::Button::Primary), ] - .spacing(5); - - let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls(id, total_panes, pane.is_pinned)) - .padding(10) + .spacing(5) + .align_items(Alignment::Center); + + let pane_grid = PaneGrid::new(&window.panes, |id, pane| { + let is_focused = focus == Some(id); + + let pin_button = button( + text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), + ) + .on_press(WindowMessage::TogglePin(id)) + .padding(3); + + let title = row![ + pin_button, + "Pane", + text(pane.id.to_string()).style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }), + ] + .spacing(5); + + let title_bar = pane_grid::TitleBar::new(title) + .controls(view_controls( + id, + total_panes, + pane.is_pinned, + pane.is_moving, + &window.title, + window_id, + &self.windows, + )) + .padding(10) + .style(if is_focused { + style::title_bar_focused + } else { + style::title_bar_active + }); + + pane_grid::Content::new(responsive(move |size| { + view_content(id, total_panes, pane.is_pinned, size) + })) + .title_bar(title_bar) .style(if is_focused { - style::title_bar_focused + style::pane_focused } else { - style::title_bar_active - }); - - pane_grid::Content::new(responsive(move |size| { - view_content(id, total_panes, pane.is_pinned, size) - })) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active + style::pane_active + }) }) - }) - .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .on_click(Message::Clicked) - .on_drag(Message::Dragged) - .on_resize(10, Message::Resized); - - container(pane_grid) .width(Length::Fill) .height(Length::Fill) - .padding(10) + .spacing(10) + .on_click(WindowMessage::Clicked) + .on_drag(WindowMessage::Dragged) + .on_resize(10, WindowMessage::Resized); + + let content: Element<_> = column![window_controls, pane_grid] + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .into(); + + return content + .map(move |message| Message::Window(window_id, message)); + } + + container(text("This shouldn't be possible!").size(20)) + .center_x() + .center_y() .into() } } @@ -222,7 +346,7 @@ const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( 0x47 as f32 / 255.0, ); -fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { use keyboard::KeyCode; use pane_grid::{Axis, Direction}; @@ -235,23 +359,44 @@ fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { }; match key_code { - KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), - KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), - KeyCode::W => Some(Message::CloseFocused), - _ => direction.map(Message::FocusAdjacent), + KeyCode::V => Some(WindowMessage::SplitFocused(Axis::Vertical)), + KeyCode::H => Some(WindowMessage::SplitFocused(Axis::Horizontal)), + KeyCode::W => Some(WindowMessage::CloseFocused), + _ => direction.map(WindowMessage::FocusAdjacent), + } +} + +#[derive(Debug, Clone)] +struct SelectableWindow(window::Id, String); + +impl PartialEq for SelectableWindow { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for SelectableWindow {} + +impl std::fmt::Display for SelectableWindow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.1.fmt(f) } } struct Pane { id: usize, + pub axis: pane_grid::Axis, pub is_pinned: bool, + pub is_moving: bool, } impl Pane { - fn new(id: usize) -> Self { + fn new(id: usize, axis: pane_grid::Axis) -> Self { Self { id, + axis, is_pinned: false, + is_moving: false, } } } @@ -261,7 +406,7 @@ fn view_content<'a>( total_panes: usize, is_pinned: bool, size: Size, -) -> Element<'a, Message> { +) -> Element<'a, WindowMessage> { let button = |label, message| { button( text(label) @@ -277,11 +422,11 @@ fn view_content<'a>( let mut controls = column![ button( "Split horizontally", - Message::Split(pane_grid::Axis::Horizontal, pane), + WindowMessage::Split(pane_grid::Axis::Horizontal, pane), ), button( "Split vertically", - Message::Split(pane_grid::Axis::Vertical, pane), + WindowMessage::Split(pane_grid::Axis::Vertical, pane), ) ] .spacing(5) @@ -289,7 +434,7 @@ fn view_content<'a>( if total_panes > 1 && !is_pinned { controls = controls.push( - button("Close", Message::Close(pane)) + button("Close", WindowMessage::Close(pane)) .style(theme::Button::Destructive), ); } @@ -314,16 +459,48 @@ fn view_controls<'a>( pane: pane_grid::Pane, total_panes: usize, is_pinned: bool, -) -> Element<'a, Message> { - let mut button = button(text("Close").size(14)) + is_moving: bool, + window_title: &'a str, + window_id: window::Id, + windows: &HashMap, +) -> Element<'a, WindowMessage> { + let window_selector = { + let options: Vec<_> = windows + .iter() + .map(|(id, window)| SelectableWindow(*id, window.title.clone())) + .collect(); + pick_list( + options, + Some(SelectableWindow(window_id, window_title.to_string())), + move |window| WindowMessage::SelectedWindow(pane, window), + ) + }; + + let mut move_to = button(text("Move to").size(14)).padding(3); + + let mut pop_out = button(text("Pop Out").size(14)).padding(3); + + let mut close = button(text("Close").size(14)) .style(theme::Button::Destructive) .padding(3); if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); + close = close.on_press(WindowMessage::Close(pane)); + pop_out = pop_out.on_press(WindowMessage::PopOut(pane)); + } + + if windows.len() > 1 && total_panes > 1 && !is_pinned { + move_to = move_to.on_press(WindowMessage::ToggleMoving(pane)); + } + + let mut content = row![].spacing(10); + if is_moving { + content = content.push(pop_out).push(window_selector).push(close); + } else { + content = content.push(pop_out).push(move_to).push(close); } - button.into() + content.into() } mod style { -- cgit From 35331d0a41a53b8ff5c642b8274c7377ae6c6182 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 26 Jul 2022 16:46:12 -0300 Subject: Allow closing the window from user code --- examples/multi_window/src/main.rs | 9 ++++- native/src/window/id.rs | 2 +- winit/src/multi_window.rs | 77 +++++++++++++++++++++++++++++---------- winit/src/multi_window/state.rs | 11 ++++++ 4 files changed, 76 insertions(+), 23 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 4ad92adb..ca137d48 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -50,6 +50,7 @@ enum WindowMessage { Close(pane_grid::Pane), CloseFocused, SelectedWindow(pane_grid::Pane, SelectableWindow), + CloseWindow, } impl Application for Example { @@ -128,6 +129,9 @@ impl Application for Example { let window = self.windows.get_mut(&id).unwrap(); window.focus = Some(pane); } + WindowMessage::CloseWindow => { + let _ = self.windows.remove(&id); + } WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { let window = self.windows.get_mut(&id).unwrap(); window.panes.resize(&split, ratio); @@ -145,8 +149,6 @@ impl Application for Example { if let Some((pane, _)) = result { window.focus = Some(pane); } - - self.panes_created += 1; } } WindowMessage::ToggleMoving(pane) => { @@ -260,6 +262,9 @@ impl Application for Example { WindowMessage::TitleChanged, ), button(text("Apply")).style(theme::Button::Primary), + button(text("Close")) + .on_press(WindowMessage::CloseWindow) + .style(theme::Button::Destructive), ] .spacing(5) .align_items(Alignment::Center); diff --git a/native/src/window/id.rs b/native/src/window/id.rs index 0ba355af..059cf4e7 100644 --- a/native/src/window/id.rs +++ b/native/src/window/id.rs @@ -1,7 +1,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] /// TODO(derezzedex) pub struct Id(u64); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index dc00d737..3c720a69 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -39,6 +39,8 @@ pub enum Event { // (maybe we should also allow users to listen/react to those internal messages?) NewWindow(window::Id, settings::Window), /// TODO(derezzedex) + CloseWindow(window::Id), + /// TODO(derezzedex) WindowCreated(window::Id, winit::window::Window), } @@ -549,6 +551,27 @@ async fn run_instance( let _ = window_ids.insert(window.id(), id); let _ = windows.insert(id, window); } + Event::CloseWindow(id) => { + // TODO(derezzedex): log errors + if let Some(window) = windows.get(&id) { + if window_ids.remove(&window.id()).is_none() { + println!("Failed to remove from `window_ids`!"); + } + } + if states.remove(&id).is_none() { + println!("Failed to remove from `states`!") + } + if interfaces.remove(&id).is_none() { + println!("Failed to remove from `interfaces`!"); + } + if windows.remove(&id).is_none() { + println!("Failed to remove from `windows`!") + } + + if windows.is_empty() { + break 'main; + } + } Event::NewWindow(_, _) => unreachable!(), }, event::Event::RedrawRequested(id) => { @@ -651,29 +674,43 @@ async fn run_instance( window_id, } => { // dbg!(window_id); - let window = window_ids - .get(&window_id) - .and_then(|id| windows.get(id)) - .unwrap(); - let window_state = window_ids - .get(&window_id) - .and_then(|id| states.get_mut(id)) - .unwrap(); - - if requests_exit(&window_event, window_state.state.modifiers()) - && exit_on_close_request + if let Some(window) = + window_ids.get(&window_id).and_then(|id| windows.get(id)) { - break; - } + if let Some(window_state) = window_ids + .get(&window_id) + .and_then(|id| states.get_mut(id)) + { + if requests_exit( + &window_event, + window_state.state.modifiers(), + ) && exit_on_close_request + { + break; + } - window_state.state.update(window, &window_event, &mut debug); + window_state.state.update( + window, + &window_event, + &mut debug, + ); - if let Some(event) = conversion::window_event( - &window_event, - window_state.state.scale_factor(), - window_state.state.modifiers(), - ) { - events.push((window_ids.get(&window_id).cloned(), event)); + if let Some(event) = conversion::window_event( + &window_event, + window_state.state.scale_factor(), + window_state.state.modifiers(), + ) { + events.push(( + window_ids.get(&window_id).cloned(), + event, + )); + } + } else { + // TODO(derezzedex): log error + } + } else { + // TODO(derezzedex): log error + // println!("{:?}: {:?}", window_id, window_event); } } _ => {} diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index d22de961..ae353e3b 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -189,6 +189,17 @@ where proxy: &EventLoopProxy>, ) { let new_windows = application.windows(); + + // Check for windows to close + for window_id in windows.keys() { + if !new_windows.iter().any(|(id, _)| id == window_id) { + proxy + .send_event(Event::CloseWindow(*window_id)) + .expect("Failed to send message"); + } + } + + // Check for windows to spawn for (id, settings) in new_windows { if !windows.contains_key(&id) { proxy -- cgit From dc86bd03733969033df7389c3d21e78ecc6291bb Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 27 Jul 2022 15:37:48 -0300 Subject: Introduce `close_requested` for `multi-window` --- examples/multi_window/src/main.rs | 4 ++++ src/multi_window/application.rs | 7 +++++++ winit/src/multi_window.rs | 15 +++++++++++---- winit/src/settings.rs | 4 ++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index ca137d48..88ddf46f 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -250,6 +250,10 @@ impl Application for Example { .collect() } + fn close_requested(&self, window: window::Id) -> Self::Message { + Message::Window(window, WindowMessage::CloseWindow) + } + fn view(&self, window_id: window::Id) -> Element { if let Some(window) = self.windows.get(&window_id) { let focus = window.focus; diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index e849bf2b..df45ca1e 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -113,6 +113,9 @@ pub trait Application: Sized { false } + /// TODO(derezzedex) + fn close_requested(&self, window: window::Id) -> Self::Message; + /// Runs the [`Application`]. /// /// On native platforms, this method will take control of the current thread @@ -207,4 +210,8 @@ where fn should_exit(&self) -> bool { self.0.should_exit() } + + fn close_requested(&self, window: window::Id) -> Self::Message { + self.0.close_requested(window) + } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 3c720a69..6fbedc55 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -147,6 +147,9 @@ where fn should_exit(&self) -> bool { false } + + /// TODO(derezzedex) + fn close_requested(&self, window: window::Id) -> Self::Message; } /// Runs an [`Application`] with an executor, compositor, and the provided @@ -296,7 +299,7 @@ async fn run_instance( >, init_command: Command, mut windows: HashMap, - exit_on_close_request: bool, + _exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, @@ -684,9 +687,13 @@ async fn run_instance( if requests_exit( &window_event, window_state.state.modifiers(), - ) && exit_on_close_request - { - break; + ) { + if let Some(id) = + window_ids.get(&window_id).cloned() + { + let message = application.close_requested(id); + messages.push(message); + } } window_state.state.update( diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 94d243a7..ea0ba361 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -46,6 +46,10 @@ pub struct Settings { /// Whether the [`Application`] should exit when the user requests the /// window to close (e.g. the user presses the close button). /// + /// NOTE: This is not used for `multi-window`, instead check [`Application::close_requested`]. + /// + /// [`close_requested`]: crate::multi_window::Application::close_requested + /// /// [`Application`]: crate::Application pub exit_on_close_request: bool, -- cgit From 7f35256573db789fa6552c2cfd8aa16dde2a1a4d Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 15 Sep 2022 05:02:18 -0300 Subject: Split `Surface` and `Window` --- winit/src/multi_window.rs | 82 ++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 47 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 6fbedc55..3e7fecd0 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -318,6 +318,7 @@ async fn run_instance( .collect(); let mut states = HashMap::new(); + let mut surfaces = HashMap::new(); let mut interfaces = ManuallyDrop::new(HashMap::new()); for (&id, window) in windows.keys().zip(windows.values()) { @@ -342,20 +343,19 @@ async fn run_instance( id, ); - let window_state: WindowState = WindowState { surface, state }; - - let _ = states.insert(id, window_state); + let _ = states.insert(id, state); + let _ = surfaces.insert(id, surface); let _ = interfaces.insert(id, user_interface); } { // TODO(derezzedex) - let window_state = states.values().next().expect("No state found"); + let state = states.values().next().expect("No state found"); run_command( &application, &mut cache, - &window_state.state, + state, &mut renderer, init_command, &mut runtime, @@ -396,7 +396,7 @@ async fn run_instance( .collect(); let cursor_position = - states.get(&id).unwrap().state.cursor_position(); + states.get(&id).unwrap().cursor_position(); let window = windows.get(&id).unwrap(); if filtered.is_empty() && messages.is_empty() { @@ -430,7 +430,7 @@ async fn run_instance( user_interface::State::Outdated, ) { - let state = &mut states.get_mut(&id).unwrap().state; + let state = &mut states.get_mut(&id).unwrap(); let pure_states: HashMap<_, _> = ManuallyDrop::into_inner(interfaces) .drain() @@ -481,7 +481,7 @@ async fn run_instance( debug.draw_started(); let new_mouse_interaction = { let user_interface = interfaces.get_mut(&id).unwrap(); - let state = &states.get(&id).unwrap().state; + let state = states.get(&id).unwrap(); user_interface.draw( &mut renderer, @@ -546,10 +546,8 @@ async fn run_instance( id, ); - let window_state: WindowState = - WindowState { surface, state }; - - let _ = states.insert(id, window_state); + let _ = states.insert(id, state); + let _ = surfaces.insert(id, surface); let _ = interfaces.insert(id, user_interface); let _ = window_ids.insert(window.id(), id); let _ = windows.insert(id, window); @@ -570,6 +568,9 @@ async fn run_instance( if windows.remove(&id).is_none() { println!("Failed to remove from `windows`!") } + if surfaces.remove(&id).is_none() { + println!("Failed to remove from `surfaces`!") + } if windows.is_empty() { break 'main; @@ -578,11 +579,15 @@ async fn run_instance( Event::NewWindow(_, _) => unreachable!(), }, event::Event::RedrawRequested(id) => { - let window_state = window_ids + let state = window_ids .get(&id) .and_then(|id| states.get_mut(id)) .unwrap(); - let physical_size = window_state.state.physical_size(); + let surface = window_ids + .get(&id) + .and_then(|id| surfaces.get_mut(id)) + .unwrap(); + let physical_size = state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { continue; @@ -590,13 +595,13 @@ async fn run_instance( debug.render_started(); - if window_state.state.viewport_changed() { + if state.viewport_changed() { let mut user_interface = window_ids .get(&id) .and_then(|id| interfaces.remove(id)) .unwrap(); - let logical_size = window_state.state.logical_size(); + let logical_size = state.logical_size(); debug.layout_started(); user_interface = @@ -605,7 +610,7 @@ async fn run_instance( debug.draw_started(); let new_mouse_interaction = { - let state = &window_state.state; + let state = &state; user_interface.draw( &mut renderer, @@ -634,7 +639,7 @@ async fn run_instance( .insert(*window_ids.get(&id).unwrap(), user_interface); compositor.configure_surface( - &mut window_state.surface, + surface, physical_size.width, physical_size.height, ); @@ -642,9 +647,9 @@ async fn run_instance( match compositor.present( &mut renderer, - &mut window_state.surface, - window_state.state.viewport(), - window_state.state.background_color(), + surface, + state.viewport(), + state.background_color(), &debug.overlay(), ) { Ok(()) => { @@ -680,14 +685,11 @@ async fn run_instance( if let Some(window) = window_ids.get(&window_id).and_then(|id| windows.get(id)) { - if let Some(window_state) = window_ids + if let Some(state) = window_ids .get(&window_id) .and_then(|id| states.get_mut(id)) { - if requests_exit( - &window_event, - window_state.state.modifiers(), - ) { + if requests_exit(&window_event, state.modifiers()) { if let Some(id) = window_ids.get(&window_id).cloned() { @@ -696,16 +698,12 @@ async fn run_instance( } } - window_state.state.update( - window, - &window_event, - &mut debug, - ); + state.update(window, &window_event, &mut debug); if let Some(event) = conversion::window_event( &window_event, - window_state.state.scale_factor(), - window_state.state.modifiers(), + state.scale_factor(), + state.modifiers(), ) { events.push(( window_ids.get(&window_id).cloned(), @@ -953,21 +951,12 @@ pub fn run_command( } } -struct WindowState -where - A: Application, - C: iced_graphics::window::Compositor, - ::Theme: StyleSheet, -{ - surface: ::Surface, - state: State, -} - -fn build_user_interfaces<'a, A, C>( +/// TODO(derezzedex) +pub fn build_user_interfaces<'a, A>( application: &'a A, renderer: &mut A::Renderer, debug: &mut Debug, - states: &HashMap>, + states: &HashMap>, mut pure_states: HashMap, ) -> HashMap< window::Id, @@ -979,13 +968,12 @@ fn build_user_interfaces<'a, A, C>( > where A: Application + 'static, - C: iced_graphics::window::Compositor + 'static, ::Theme: StyleSheet, { let mut interfaces = HashMap::new(); for (id, pure_state) in pure_states.drain() { - let state = &states.get(&id).unwrap().state; + let state = &states.get(&id).unwrap(); let user_interface = build_user_interface( application, -- cgit From 974cc6b6f55178976b0ace626ba03bdd88cde5e0 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 19 Sep 2022 16:01:50 -0300 Subject: Introduce `multi_window` to `iced_glutin` --- Cargo.toml | 2 +- glutin/Cargo.toml | 1 + glutin/src/lib.rs | 3 +++ glutin/src/multi_window.rs | 21 +++++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 glutin/src/multi_window.rs diff --git a/Cargo.toml b/Cargo.toml index 41f5af2f..36465a29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ chrome-trace = [ "iced_glow?/tracing", ] # Enables experimental multi-window support -multi_window = ["iced_winit/multi_window"] +multi_window = ["iced_winit/multi_window", "iced_glutin/multi_window"] [badges] maintenance = { status = "actively-developed" } diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 304170cd..2960a0bd 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -14,6 +14,7 @@ categories = ["gui"] trace = ["iced_winit/trace"] debug = ["iced_winit/debug"] system = ["iced_winit/system"] +multi_window = ["iced_winit/multi_window"] [dependencies.log] version = "0.4" diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs index 33afd664..45d6cb5b 100644 --- a/glutin/src/lib.rs +++ b/glutin/src/lib.rs @@ -29,5 +29,8 @@ pub use iced_winit::*; pub mod application; +#[cfg(feature = "multi_window")] +pub mod multi_window; + #[doc(no_inline)] pub use application::Application; diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs new file mode 100644 index 00000000..46d00d81 --- /dev/null +++ b/glutin/src/multi_window.rs @@ -0,0 +1,21 @@ +//! Create interactive, native cross-platform applications. +use crate::{Error, Executor}; + +pub use iced_winit::multi_window::{Application, StyleSheet}; + +use iced_winit::Settings; + +/// Runs an [`Application`] with an executor, compositor, and the provided +/// settings. +pub fn run( + _settings: Settings, + _compositor_settings: C::Settings, +) -> Result<(), Error> +where + A: Application + 'static, + E: Executor + 'static, + C: iced_graphics::window::GLCompositor + 'static, + ::Theme: StyleSheet, +{ + unimplemented!("iced_glutin not implemented!") +} -- cgit From 0ad53a3d5c7b5fb5785a64102ee1ad7df9a5fb2b Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 19 Sep 2022 20:59:37 -0300 Subject: add `window::Id` to `Event` and `Action` --- examples/events/src/main.rs | 2 +- examples/integration_opengl/src/main.rs | 2 + examples/integration_wgpu/src/main.rs | 5 ++- glutin/src/application.rs | 1 + native/src/command/action.rs | 8 ++-- native/src/event.rs | 2 +- native/src/window/id.rs | 3 ++ winit/src/application.rs | 3 +- winit/src/conversion.rs | 46 ++++++++++++-------- winit/src/multi_window.rs | 77 +++++++++++++++------------------ winit/src/window.rs | 30 ++++++++----- 11 files changed, 100 insertions(+), 79 deletions(-) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 234e1423..e9709377 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -52,7 +52,7 @@ impl Application for Events { } } Message::EventOccurred(event) => { - if let Event::Window(window::Event::CloseRequested) = event { + if let Event::Window(_, window::Event::CloseRequested) = event { self.should_exit = true; } } diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs index f161c8a0..56470190 100644 --- a/examples/integration_opengl/src/main.rs +++ b/examples/integration_opengl/src/main.rs @@ -13,6 +13,7 @@ use iced_glow::{Backend, Renderer, Settings, Viewport}; use iced_glutin::conversion; use iced_glutin::glutin; use iced_glutin::renderer; +use iced_glutin::window; use iced_glutin::{program, Clipboard, Color, Debug, Size}; pub fn main() { @@ -107,6 +108,7 @@ pub fn main() { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, &event, windowed_context.window().scale_factor(), modifiers, diff --git a/examples/integration_wgpu/src/main.rs b/examples/integration_wgpu/src/main.rs index 70f9a48b..219573ea 100644 --- a/examples/integration_wgpu/src/main.rs +++ b/examples/integration_wgpu/src/main.rs @@ -6,8 +6,8 @@ use scene::Scene; use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; use iced_winit::{ - conversion, futures, program, renderer, winit, Clipboard, Color, Debug, - Size, + conversion, futures, program, renderer, window, winit, Clipboard, Color, + Debug, Size, }; use winit::{ @@ -169,6 +169,7 @@ pub fn main() { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, &event, window.scale_factor(), modifiers, diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 1464bb2d..108d7ffa 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -435,6 +435,7 @@ async fn run_instance( state.update(context.window(), &window_event, &mut debug); if let Some(event) = conversion::window_event( + crate::window::Id::MAIN, &window_event, state.scale_factor(), state.modifiers(), diff --git a/native/src/command/action.rs b/native/src/command/action.rs index a6954f8f..924f95e6 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -20,7 +20,7 @@ pub enum Action { Clipboard(clipboard::Action), /// Run a window action. - Window(window::Action), + Window(window::Id, window::Action), /// Run a system action. System(system::Action), @@ -46,7 +46,7 @@ impl Action { match self { Self::Future(future) => Action::Future(Box::pin(future.map(f))), Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(window) => Action::Window(window.map(f)), + Self::Window(id, window) => Action::Window(id, window.map(f)), Self::System(system) => Action::System(system.map(f)), Self::Widget(widget) => Action::Widget(widget.map(f)), } @@ -60,7 +60,9 @@ impl fmt::Debug for Action { Self::Clipboard(action) => { write!(f, "Action::Clipboard({:?})", action) } - Self::Window(action) => write!(f, "Action::Window({:?})", action), + Self::Window(id, action) => { + write!(f, "Action::Window({:?}, {:?})", id, action) + } Self::System(action) => write!(f, "Action::System({:?})", action), Self::Widget(_action) => write!(f, "Action::Widget"), } diff --git a/native/src/event.rs b/native/src/event.rs index bcfaf891..eb826399 100644 --- a/native/src/event.rs +++ b/native/src/event.rs @@ -19,7 +19,7 @@ pub enum Event { Mouse(mouse::Event), /// A window event - Window(window::Event), + Window(window::Id, window::Event), /// A touch event Touch(touch::Event), diff --git a/native/src/window/id.rs b/native/src/window/id.rs index 059cf4e7..5060e162 100644 --- a/native/src/window/id.rs +++ b/native/src/window/id.rs @@ -6,6 +6,9 @@ use std::hash::{Hash, Hasher}; pub struct Id(u64); impl Id { + /// TODO(derezzedex): maybe change `u64` to an enum `Type::{Single, Multi(u64)}` + pub const MAIN: Self = Id(0); + /// TODO(derezzedex) pub fn new(id: impl Hash) -> Id { let mut hasher = DefaultHasher::new(); diff --git a/winit/src/application.rs b/winit/src/application.rs index 74c73815..4486f5d9 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -502,6 +502,7 @@ async fn run_instance( state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( + crate::window::Id::MAIN, &window_event, state.scale_factor(), state.modifiers(), @@ -667,7 +668,7 @@ pub fn run_command( clipboard.write(contents); } }, - command::Action::Window(action) => match action { + command::Action::Window(_id, action) => match action { window::Action::Close => { *should_exit = true; } diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 1418e346..6c809d19 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -10,6 +10,7 @@ use crate::{Event, Point, Position}; /// Converts a winit window event into an iced event. pub fn window_event( + id: window::Id, event: &winit::event::WindowEvent<'_>, scale_factor: f64, modifiers: winit::event::ModifiersState, @@ -20,21 +21,27 @@ pub fn window_event( WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { let logical_size = new_inner_size.to_logical(scale_factor); - Some(Event::Window(window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - })) + Some(Event::Window( + id, + window::Event::Resized { + width: logical_size.width, + height: logical_size.height, + }, + )) } WindowEvent::CloseRequested => { - Some(Event::Window(window::Event::CloseRequested)) + Some(Event::Window(id, window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::(scale_factor); @@ -112,19 +119,22 @@ pub fn window_event( WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), )), - WindowEvent::Focused(focused) => Some(Event::Window(if *focused { - window::Event::Focused - } else { - window::Event::Unfocused - })), + WindowEvent::Focused(focused) => Some(Event::Window( + id, + if *focused { + window::Event::Focused + } else { + window::Event::Unfocused + }, + )), WindowEvent::HoveredFile(path) => { - Some(Event::Window(window::Event::FileHovered(path.clone()))) + Some(Event::Window(id, window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(window::Event::FileDropped(path.clone()))) + Some(Event::Window(id, window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(window::Event::FilesHoveredLeft)) + Some(Event::Window(id, window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(*touch, scale_factor))) @@ -133,7 +143,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(window::Event::Moved { x, y })) + Some(Event::Window(id, window::Event::Moved { x, y })) } _ => None, } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 3e7fecd0..9f46b88d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -363,7 +363,6 @@ async fn run_instance( &mut proxy, &mut debug, &windows, - &window_ids, || compositor.fetch_information(), ); } @@ -456,7 +455,6 @@ async fn run_instance( &mut debug, &mut messages, &windows, - &window_ids, || compositor.fetch_information(), ); @@ -701,6 +699,7 @@ async fn run_instance( state.update(window, &window_event, &mut debug); if let Some(event) = conversion::window_event( + *window_ids.get(&window_id).unwrap(), &window_event, state.scale_factor(), state.modifiers(), @@ -787,7 +786,6 @@ pub fn update( debug: &mut Debug, messages: &mut Vec, windows: &HashMap, - window_ids: &HashMap, graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where ::Theme: StyleSheet, @@ -810,7 +808,6 @@ pub fn update( proxy, debug, windows, - window_ids, graphics_info, ); } @@ -831,7 +828,6 @@ pub fn run_command( proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, windows: &HashMap, - window_ids: &HashMap, _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where A: Application, @@ -842,10 +838,6 @@ pub fn run_command( use iced_native::system; use iced_native::window; - // TODO(derezzedex) - let window = windows.values().next().expect("No window found"); - let id = *window_ids.get(&window.id()).unwrap(); - for action in command.actions() { match action { command::Action::Future(future) => { @@ -863,38 +855,41 @@ pub fn run_command( clipboard.write(contents); } }, - command::Action::Window(action) => match action { - window::Action::Resize { width, height } => { - window.set_inner_size(winit::dpi::LogicalSize { - width, - height, - }); - } - window::Action::Move { x, y } => { - window.set_outer_position(winit::dpi::LogicalPosition { - x, - y, - }); - } - window::Action::SetMode(mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.primary_monitor(), - mode, - )); - } - window::Action::FetchMode(tag) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - window::Mode::Hidden - }; - - proxy - .send_event(Event::Application(tag(mode))) - .expect("Send message to event loop"); + command::Action::Window(id, action) => { + let window = windows.get(&id).expect("No window found"); + + match action { + window::Action::Resize { width, height } => { + window.set_inner_size(winit::dpi::LogicalSize { + width, + height, + }); + } + window::Action::Move { x, y } => { + window.set_outer_position( + winit::dpi::LogicalPosition { x, y }, + ); + } + window::Action::SetMode(mode) => { + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.primary_monitor(), + mode, + )); + } + window::Action::FetchMode(tag) => { + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + window::Mode::Hidden + }; + + proxy + .send_event(Event::Application(tag(mode))) + .expect("Send message to event loop"); + } } - }, + } command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "system")] @@ -925,7 +920,7 @@ pub fn run_command( renderer, state.logical_size(), debug, - id, + window::Id::MAIN, // TODO(derezzedex): run the operation on every widget tree ); while let Some(mut operation) = current_operation.take() { diff --git a/winit/src/window.rs b/winit/src/window.rs index f2c7037a..d9bc0d83 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -15,11 +15,15 @@ pub fn drag() -> Command { } /// Resizes the window to the given logical dimensions. -pub fn resize(width: u32, height: u32) -> Command { - Command::single(command::Action::Window(window::Action::Resize { - width, - height, - })) +pub fn resize( + id: window::Id, + width: u32, + height: u32, +) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Resize { width, height }, + )) } /// Sets the window to maximized or back. @@ -33,13 +37,13 @@ pub fn minimize(value: bool) -> Command { } /// Moves a window to the given logical coordinates. -pub fn move_to(x: i32, y: i32) -> Command { - Command::single(command::Action::Window(window::Action::Move { x, y })) +pub fn move_to(id: window::Id, x: i32, y: i32) -> Command { + Command::single(command::Action::Window(id, window::Action::Move { x, y })) } /// Sets the [`Mode`] of the window. -pub fn set_mode(mode: Mode) -> Command { - Command::single(command::Action::Window(window::Action::SetMode(mode))) +pub fn set_mode(id: window::Id, mode: Mode) -> Command { + Command::single(command::Action::Window(id, window::Action::SetMode(mode))) } /// Sets the window to maximized or back. @@ -49,9 +53,11 @@ pub fn toggle_maximize() -> Command { /// Fetches the current [`Mode`] of the window. pub fn fetch_mode( + id: window::Id, f: impl FnOnce(Mode) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(window::Action::FetchMode( - Box::new(f), - ))) + Command::single(command::Action::Window( + id, + window::Action::FetchMode(Box::new(f)), + )) } -- cgit From 064407635a0f9d79a067bad62f6f1042acaed18d Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 21 Sep 2022 19:17:25 -0300 Subject: implement `multi_window` for `iced_glutin` --- examples/multi_window/src/main.rs | 4 +- glutin/src/multi_window.rs | 564 ++++++++++++++++++++++++++++++++++- graphics/src/window/gl_compositor.rs | 2 +- 3 files changed, 561 insertions(+), 9 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 88ddf46f..0dda1804 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -70,9 +70,9 @@ impl Application for Example { ( Example { - windows: HashMap::from([(window::Id::new(0usize), window)]), + windows: HashMap::from([(window::Id::MAIN, window)]), panes_created: 1, - _focused: window::Id::new(0usize), + _focused: window::Id::MAIN, }, Command::none(), ) diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 46d00d81..c3b9e74f 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -1,15 +1,28 @@ //! Create interactive, native cross-platform applications. -use crate::{Error, Executor}; +use crate::mouse; +use crate::{Error, Executor, Runtime}; -pub use iced_winit::multi_window::{Application, StyleSheet}; +pub use iced_winit::multi_window::{ + self, Application, Event, State, StyleSheet, +}; -use iced_winit::Settings; +use iced_winit::conversion; +use iced_winit::futures; +use iced_winit::futures::channel::mpsc; +use iced_winit::renderer; +use iced_winit::user_interface; +use iced_winit::window; +use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; + +use glutin::window::Window; +use std::collections::HashMap; +use std::mem::ManuallyDrop; /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. pub fn run( - _settings: Settings, - _compositor_settings: C::Settings, + settings: Settings, + compositor_settings: C::Settings, ) -> Result<(), Error> where A: Application + 'static, @@ -17,5 +30,544 @@ where C: iced_graphics::window::GLCompositor + 'static, ::Theme: StyleSheet, { - unimplemented!("iced_glutin not implemented!") + use futures::task; + use futures::Future; + use glutin::event_loop::EventLoopBuilder; + use glutin::platform::run_return::EventLoopExtRunReturn; + use glutin::ContextBuilder; + + let mut debug = Debug::new(); + debug.startup_started(); + + let mut event_loop = EventLoopBuilder::with_user_event().build(); + let proxy = event_loop.create_proxy(); + + let runtime = { + let executor = E::new().map_err(Error::ExecutorCreationFailed)?; + let proxy = Proxy::new(event_loop.create_proxy()); + + Runtime::new(executor, proxy) + }; + + let (application, init_command) = { + let flags = settings.flags; + + runtime.enter(|| A::new(flags)) + }; + + let context = { + let builder = settings.window.into_builder( + &application.title(), + event_loop.primary_monitor(), + settings.id, + ); + + log::info!("Window builder: {:#?}", builder); + + let opengl_builder = ContextBuilder::new() + .with_vsync(true) + .with_multisampling(C::sample_count(&compositor_settings) as u16); + + let opengles_builder = opengl_builder.clone().with_gl( + glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)), + ); + + let (first_builder, second_builder) = if settings.try_opengles_first { + (opengles_builder, opengl_builder) + } else { + (opengl_builder, opengles_builder) + }; + + log::info!("Trying first builder: {:#?}", first_builder); + + let context = first_builder + .build_windowed(builder.clone(), &event_loop) + .or_else(|_| { + log::info!("Trying second builder: {:#?}", second_builder); + second_builder.build_windowed(builder, &event_loop) + }) + .map_err(|error| { + use glutin::CreationError; + use iced_graphics::Error as ContextError; + + match error { + CreationError::Window(error) => { + Error::WindowCreationFailed(error) + } + CreationError::OpenGlVersionNotSupported => { + Error::GraphicsCreationFailed( + ContextError::VersionNotSupported, + ) + } + CreationError::NoAvailablePixelFormat => { + Error::GraphicsCreationFailed( + ContextError::NoAvailablePixelFormat, + ) + } + error => Error::GraphicsCreationFailed( + ContextError::BackendError(error.to_string()), + ), + } + })?; + + #[allow(unsafe_code)] + unsafe { + context.make_current().expect("Make OpenGL context current") + } + }; + + #[allow(unsafe_code)] + let (compositor, renderer) = unsafe { + C::new(compositor_settings, |address| { + context.get_proc_address(address) + })? + }; + + let (mut sender, receiver) = mpsc::unbounded(); + + let mut instance = Box::pin(run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + receiver, + context, + init_command, + settings.exit_on_close_request, + )); + + let mut context = task::Context::from_waker(task::noop_waker_ref()); + + let _ = event_loop.run_return(move |event, event_loop, control_flow| { + use glutin::event_loop::ControlFlow; + + if let ControlFlow::ExitWithCode(_) = control_flow { + return; + } + + let event = match event { + glutin::event::Event::WindowEvent { + event: + glutin::event::WindowEvent::ScaleFactorChanged { + new_inner_size, + .. + }, + window_id, + } => Some(glutin::event::Event::WindowEvent { + event: glutin::event::WindowEvent::Resized(*new_inner_size), + window_id, + }), + glutin::event::Event::UserEvent(Event::NewWindow(id, settings)) => { + // TODO(derezzedex) + let window = settings + .into_builder( + "fix window title", + event_loop.primary_monitor(), + None, + ) + .build(event_loop) + .expect("Failed to build window"); + + Some(glutin::event::Event::UserEvent(Event::WindowCreated( + id, window, + ))) + } + _ => event.to_static(), + }; + + if let Some(event) = event { + sender.start_send(event).expect("Send event"); + + let poll = instance.as_mut().poll(&mut context); + + *control_flow = match poll { + task::Poll::Pending => ControlFlow::Wait, + task::Poll::Ready(_) => ControlFlow::Exit, + }; + } + }); + + Ok(()) +} + +async fn run_instance( + mut application: A, + mut compositor: C, + mut renderer: A::Renderer, + mut runtime: Runtime>, Event>, + mut proxy: glutin::event_loop::EventLoopProxy>, + mut debug: Debug, + mut receiver: mpsc::UnboundedReceiver< + glutin::event::Event<'_, Event>, + >, + context: glutin::ContextWrapper, + init_command: Command, + _exit_on_close_request: bool, +) where + A: Application + 'static, + E: Executor + 'static, + C: iced_graphics::window::GLCompositor + 'static, + ::Theme: StyleSheet, +{ + use glutin::event; + use iced_winit::futures::stream::StreamExt; + + let mut clipboard = Clipboard::connect(context.window()); + let mut cache = user_interface::Cache::default(); + let state = State::new(&application, context.window()); + let user_interface = multi_window::build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + window::Id::MAIN, + ); + + #[allow(unsafe_code)] + let (mut context, window) = unsafe { context.split() }; + + let mut window_ids = HashMap::from([(window.id(), window::Id::MAIN)]); + let mut windows = HashMap::from([(window::Id::MAIN, window)]); + let mut states = HashMap::from([(window::Id::MAIN, state)]); + let mut interfaces = + ManuallyDrop::new(HashMap::from([(window::Id::MAIN, user_interface)])); + + { + let state = states.get(&window::Id::MAIN).unwrap(); + + multi_window::run_command( + &application, + &mut cache, + state, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &windows, + || compositor.fetch_information(), + ); + } + runtime.track(application.subscription().map(Event::Application)); + + let mut mouse_interaction = mouse::Interaction::default(); + let mut events = Vec::new(); + let mut messages = Vec::new(); + + debug.startup_finished(); + + 'main: while let Some(event) = receiver.next().await { + match event { + event::Event::MainEventsCleared => { + for id in windows.keys().copied() { + let (filtered, remaining): (Vec<_>, Vec<_>) = + events.iter().cloned().partition( + |(window_id, _event): &( + Option, + iced_native::event::Event, + )| { + *window_id == Some(id) || *window_id == None + }, + ); + + events.retain(|el| remaining.contains(el)); + let filtered: Vec<_> = filtered + .into_iter() + .map(|(_id, event)| event) + .collect(); + + let cursor_position = + states.get(&id).unwrap().cursor_position(); + let window = windows.get(&id).unwrap(); + + if filtered.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + + let (interface_state, statuses) = { + let user_interface = interfaces.get_mut(&id).unwrap(); + user_interface.update( + &filtered, + cursor_position, + &mut renderer, + &mut clipboard, + &mut messages, + ) + }; + + debug.event_processing_finished(); + + for event in filtered.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(event); + } + + if !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated + ) + { + let state = &mut states.get_mut(&id).unwrap(); + let pure_states: HashMap<_, _> = + ManuallyDrop::into_inner(interfaces) + .drain() + .map(|(id, interface)| { + (id, interface.into_cache()) + }) + .collect(); + + // Update application + multi_window::update( + &mut application, + &mut cache, + state, + &mut renderer, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &mut messages, + &windows, + || compositor.fetch_information(), + ); + + // Update window + state.synchronize(&application, &windows, &proxy); + + let should_exit = application.should_exit(); + + interfaces = ManuallyDrop::new( + multi_window::build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + ), + ); + + if should_exit { + break 'main; + } + } + + debug.draw_started(); + let new_mouse_interaction = { + let user_interface = interfaces.get_mut(&id).unwrap(); + let state = states.get(&id).unwrap(); + + user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ) + }; + debug.draw_finished(); + + if new_mouse_interaction != mouse_interaction { + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); + + mouse_interaction = new_mouse_interaction; + } + + window.request_redraw(); + } + } + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use iced_native::event; + events.push(( + None, + iced_native::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ), + )); + } + event::Event::UserEvent(event) => match event { + Event::Application(message) => messages.push(message), + Event::WindowCreated(id, window) => { + let state = State::new(&application, &window); + let user_interface = multi_window::build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + id, + ); + + let _ = states.insert(id, state); + let _ = interfaces.insert(id, user_interface); + let _ = window_ids.insert(window.id(), id); + let _ = windows.insert(id, window); + } + Event::CloseWindow(id) => { + // TODO(derezzedex): log errors + if let Some(window) = windows.get(&id) { + if window_ids.remove(&window.id()).is_none() { + println!("Failed to remove from `window_ids`!"); + } + } + if states.remove(&id).is_none() { + println!("Failed to remove from `states`!") + } + if interfaces.remove(&id).is_none() { + println!("Failed to remove from `interfaces`!"); + } + if windows.remove(&id).is_none() { + println!("Failed to remove from `windows`!") + } + + if windows.is_empty() { + break 'main; + } + } + Event::NewWindow(_, _) => unreachable!(), + }, + event::Event::RedrawRequested(id) => { + let state = window_ids + .get(&id) + .and_then(|id| states.get_mut(id)) + .unwrap(); + + debug.render_started(); + + #[allow(unsafe_code)] + unsafe { + if !context.is_current() { + context = context + .make_current() + .expect("Make OpenGL context current"); + } + } + + if state.viewport_changed() { + let physical_size = state.physical_size(); + let logical_size = state.logical_size(); + + let mut user_interface = window_ids + .get(&id) + .and_then(|id| interfaces.remove(id)) + .unwrap(); + + debug.layout_started(); + user_interface = + user_interface.relayout(logical_size, &mut renderer); + debug.layout_finished(); + + debug.draw_started(); + let new_mouse_interaction = user_interface.draw( + &mut renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor_position(), + ); + debug.draw_finished(); + + if new_mouse_interaction != mouse_interaction { + let window = window_ids + .get(&id) + .and_then(|id| windows.get_mut(id)) + .unwrap(); + + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); + + mouse_interaction = new_mouse_interaction; + } + + context.resize(glutin::dpi::PhysicalSize::new( + physical_size.width, + physical_size.height, + )); + + compositor.resize_viewport(physical_size); + + let _ = interfaces + .insert(*window_ids.get(&id).unwrap(), user_interface); + } + + compositor.present( + &mut renderer, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); + + context.swap_buffers().expect("Swap buffers"); + + debug.render_finished(); + + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + // dbg!(window_id); + if let Some(window) = + window_ids.get(&window_id).and_then(|id| windows.get(id)) + { + if let Some(state) = window_ids + .get(&window_id) + .and_then(|id| states.get_mut(id)) + { + if multi_window::requests_exit( + &window_event, + state.modifiers(), + ) { + if let Some(id) = + window_ids.get(&window_id).cloned() + { + let message = application.close_requested(id); + messages.push(message); + } + } + + state.update(window, &window_event, &mut debug); + + if let Some(event) = conversion::window_event( + *window_ids.get(&window_id).unwrap(), + &window_event, + state.scale_factor(), + state.modifiers(), + ) { + events.push(( + window_ids.get(&window_id).cloned(), + event, + )); + } + } else { + // TODO(derezzedex): log error + } + } else { + // TODO(derezzedex): log error + // println!("{:?}: {:?}", window_id, window_event); + } + } + _ => {} + } + } + + // Manually drop the user interface + // drop(ManuallyDrop::into_inner(user_interface)); } diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index a45a7ca1..e6ae2364 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -30,7 +30,7 @@ pub trait GLCompositor: Sized { /// The settings of the [`GLCompositor`]. /// /// It's up to you to decide the configuration supported by your renderer! - type Settings: Default; + type Settings: Default + Clone; /// Creates a new [`GLCompositor`] and [`Renderer`] with the given /// [`Settings`] and an OpenGL address loader function. -- cgit From ce43514eaca0e892ad2f646a63fc29af2150d79c Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 21 Sep 2022 19:37:28 -0300 Subject: copy `multi_window::Event` from `iced_winit` --- glutin/src/multi_window.rs | 264 ++++++++++++++++++++++++++++++++++++--- glutin/src/multi_window/state.rs | 241 +++++++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+), 14 deletions(-) create mode 100644 glutin/src/multi_window/state.rs diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index c3b9e74f..4949219f 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -1,15 +1,18 @@ //! Create interactive, native cross-platform applications. +mod state; + +pub use state::State; + use crate::mouse; use crate::{Error, Executor, Runtime}; -pub use iced_winit::multi_window::{ - self, Application, Event, State, StyleSheet, -}; +pub use iced_winit::multi_window::{self, Application, StyleSheet}; use iced_winit::conversion; use iced_winit::futures; use iced_winit::futures::channel::mpsc; use iced_winit::renderer; +use iced_winit::settings; use iced_winit::user_interface; use iced_winit::window; use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; @@ -238,7 +241,7 @@ async fn run_instance( { let state = states.get(&window::Id::MAIN).unwrap(); - multi_window::run_command( + run_command( &application, &mut cache, state, @@ -324,7 +327,7 @@ async fn run_instance( .collect(); // Update application - multi_window::update( + update( &mut application, &mut cache, state, @@ -343,15 +346,13 @@ async fn run_instance( let should_exit = application.should_exit(); - interfaces = ManuallyDrop::new( - multi_window::build_user_interfaces( - &application, - &mut renderer, - &mut debug, - &states, - pure_states, - ), - ); + interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut renderer, + &mut debug, + &states, + pure_states, + )); if should_exit { break 'main; @@ -571,3 +572,238 @@ async fn run_instance( // Manually drop the user interface // drop(ManuallyDrop::into_inner(user_interface)); } + +/// TODO(derezzedex): +// This is the an wrapper around the `Application::Message` associate type +// to allows the `shell` to create internal messages, while still having +// the current user specified custom messages. +#[derive(Debug)] +pub enum Event { + /// An [`Application`] generated message + Application(Message), + + /// TODO(derezzedex) + // Create a wrapper variant of `window::Event` type instead + // (maybe we should also allow users to listen/react to those internal messages?) + NewWindow(window::Id, settings::Window), + /// TODO(derezzedex) + CloseWindow(window::Id), + /// TODO(derezzedex) + WindowCreated(window::Id, glutin::window::Window), +} + +/// Updates an [`Application`] by feeding it the provided messages, spawning any +/// resulting [`Command`], and tracking its [`Subscription`]. +pub fn update( + application: &mut A, + cache: &mut user_interface::Cache, + state: &State, + renderer: &mut A::Renderer, + runtime: &mut Runtime>, Event>, + clipboard: &mut Clipboard, + proxy: &mut glutin::event_loop::EventLoopProxy>, + debug: &mut Debug, + messages: &mut Vec, + windows: &HashMap, + graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, +) where + ::Theme: StyleSheet, +{ + for message in messages.drain(..) { + debug.log_message(&message); + + debug.update_started(); + let command = runtime.enter(|| application.update(message)); + debug.update_finished(); + + run_command( + application, + cache, + state, + renderer, + command, + runtime, + clipboard, + proxy, + debug, + windows, + graphics_info, + ); + } + + let subscription = application.subscription().map(Event::Application); + runtime.track(subscription); +} + +/// Runs the actions of a [`Command`]. +pub fn run_command( + application: &A, + cache: &mut user_interface::Cache, + state: &State, + renderer: &mut A::Renderer, + command: Command, + runtime: &mut Runtime>, Event>, + clipboard: &mut Clipboard, + proxy: &mut glutin::event_loop::EventLoopProxy>, + debug: &mut Debug, + windows: &HashMap, + _graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, +) where + A: Application, + E: Executor, + ::Theme: StyleSheet, +{ + use iced_native::command; + use iced_native::system; + use iced_native::window; + use iced_winit::clipboard; + use iced_winit::futures::FutureExt; + + for action in command.actions() { + match action { + command::Action::Future(future) => { + runtime.spawn(Box::pin(future.map(Event::Application))); + } + command::Action::Clipboard(action) => match action { + clipboard::Action::Read(tag) => { + let message = tag(clipboard.read()); + + proxy + .send_event(Event::Application(message)) + .expect("Send message to event loop"); + } + clipboard::Action::Write(contents) => { + clipboard.write(contents); + } + }, + command::Action::Window(id, action) => { + let window = windows.get(&id).expect("No window found"); + + match action { + window::Action::Resize { width, height } => { + window.set_inner_size(glutin::dpi::LogicalSize { + width, + height, + }); + } + window::Action::Move { x, y } => { + window.set_outer_position( + glutin::dpi::LogicalPosition { x, y }, + ); + } + window::Action::SetMode(mode) => { + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.primary_monitor(), + mode, + )); + } + window::Action::FetchMode(tag) => { + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + window::Mode::Hidden + }; + + proxy + .send_event(Event::Application(tag(mode))) + .expect("Send message to event loop"); + } + } + } + command::Action::System(action) => match action { + system::Action::QueryInformation(_tag) => { + #[cfg(feature = "iced_winit/system")] + { + let graphics_info = _graphics_info(); + let proxy = proxy.clone(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let message = _tag(information); + + proxy + .send_event(Event::Application(message)) + .expect("Send message to event loop") + }); + } + } + }, + command::Action::Widget(action) => { + use crate::widget::operation; + + let mut current_cache = std::mem::take(cache); + let mut current_operation = Some(action.into_operation()); + + let mut user_interface = multi_window::build_user_interface( + application, + current_cache, + renderer, + state.logical_size(), + debug, + window::Id::MAIN, // TODO(derezzedex): run the operation on every widget tree + ); + + while let Some(mut operation) = current_operation.take() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(Event::Application(message)) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + + current_cache = user_interface.into_cache(); + *cache = current_cache; + } + } + } +} + +/// TODO(derezzedex) +pub fn build_user_interfaces<'a, A>( + application: &'a A, + renderer: &mut A::Renderer, + debug: &mut Debug, + states: &HashMap>, + mut pure_states: HashMap, +) -> HashMap< + window::Id, + iced_winit::UserInterface< + 'a, + ::Message, + ::Renderer, + >, +> +where + A: Application + 'static, + ::Theme: StyleSheet, +{ + let mut interfaces = HashMap::new(); + + for (id, pure_state) in pure_states.drain() { + let state = &states.get(&id).unwrap(); + + let user_interface = multi_window::build_user_interface( + application, + pure_state, + renderer, + state.logical_size(), + debug, + id, + ); + + let _ = interfaces.insert(id, user_interface); + } + + interfaces +} diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs new file mode 100644 index 00000000..163f46bd --- /dev/null +++ b/glutin/src/multi_window/state.rs @@ -0,0 +1,241 @@ +use crate::application::{self, StyleSheet as _}; +use crate::conversion; +use crate::multi_window::{Application, Event}; +use crate::window; +use crate::{Color, Debug, Point, Size, Viewport}; + +use glutin::event::{Touch, WindowEvent}; +use glutin::event_loop::EventLoopProxy; +use glutin::window::Window; +use std::collections::HashMap; +use std::marker::PhantomData; + +/// The state of a windowed [`Application`]. +#[allow(missing_debug_implementations)] +pub struct State +where + ::Theme: application::StyleSheet, +{ + title: String, + scale_factor: f64, + viewport: Viewport, + viewport_changed: bool, + cursor_position: glutin::dpi::PhysicalPosition, + modifiers: glutin::event::ModifiersState, + theme: ::Theme, + appearance: iced_winit::application::Appearance, + application: PhantomData, +} + +impl State +where + ::Theme: application::StyleSheet, +{ + /// Creates a new [`State`] for the provided [`Application`] and window. + pub fn new(application: &A, window: &Window) -> Self { + let title = application.title(); + let scale_factor = application.scale_factor(); + let theme = application.theme(); + 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_changed: false, + // TODO: Encode cursor availability in the type-system + cursor_position: glutin::dpi::PhysicalPosition::new(-1.0, -1.0), + modifiers: glutin::event::ModifiersState::default(), + theme, + appearance, + application: PhantomData, + } + } + + /// Returns the current [`Viewport`] of the [`State`]. + pub fn viewport(&self) -> &Viewport { + &self.viewport + } + + /// TODO(derezzedex) + pub fn viewport_changed(&self) -> bool { + self.viewport_changed + } + + /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn physical_size(&self) -> Size { + self.viewport.physical_size() + } + + /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. + pub fn logical_size(&self) -> Size { + 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_position(&self) -> Point { + conversion::cursor_position( + self.cursor_position, + self.viewport.scale_factor(), + ) + } + + /// Returns the current keyboard modifiers of the [`State`]. + pub fn modifiers(&self) -> glutin::event::ModifiersState { + self.modifiers + } + + /// Returns the current theme of the [`State`]. + pub fn theme(&self) -> &::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 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_changed = true; + } + 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_changed = true; + } + WindowEvent::CursorMoved { position, .. } + | WindowEvent::Touch(Touch { + location: position, .. + }) => { + self.cursor_position = *position; + } + WindowEvent::CursorLeft { .. } => { + // TODO: Encode cursor availability in the type-system + self.cursor_position = + glutin::dpi::PhysicalPosition::new(-1.0, -1.0); + } + WindowEvent::ModifiersChanged(new_modifiers) => { + self.modifiers = *new_modifiers; + } + #[cfg(feature = "debug")] + WindowEvent::KeyboardInput { + input: + glutin::event::KeyboardInput { + virtual_keycode: + Some(glutin::event::VirtualKeyCode::F12), + state: glutin::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 [`Application::update`]. + /// + /// [`Application::update`]: crate::Program::update + pub fn synchronize( + &mut self, + application: &A, + windows: &HashMap, + proxy: &EventLoopProxy>, + ) { + let new_windows = application.windows(); + + // Check for windows to close + for window_id in windows.keys() { + if !new_windows.iter().any(|(id, _)| id == window_id) { + proxy + .send_event(Event::CloseWindow(*window_id)) + .expect("Failed to send message"); + } + } + + // Check for windows to spawn + for (id, settings) in new_windows { + if !windows.contains_key(&id) { + proxy + .send_event(Event::NewWindow(id, settings)) + .expect("Failed to send message"); + } + } + + let window = windows.values().next().expect("No window found"); + + // Update window title + let new_title = application.title(); + + if self.title != new_title { + window.set_title(&new_title); + + self.title = new_title; + } + + // Update scale factor + let new_scale_factor = application.scale_factor(); + + if self.scale_factor != new_scale_factor { + let size = window.inner_size(); + + self.viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor() * new_scale_factor, + ); + + self.scale_factor = new_scale_factor; + } + + // Update theme and appearance + self.theme = application.theme(); + self.appearance = self.theme.appearance(application.style()); + } +} -- cgit From a386788b67bf4e008916e79a8c7dd7289a3ab3cd Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 28 Sep 2022 19:10:47 -0300 Subject: use `glutin/multi_window` branch --- examples/integration_opengl/src/main.rs | 4 ++-- glutin/Cargo.toml | 4 ++-- glutin/src/application.rs | 6 +++--- glutin/src/multi_window.rs | 38 +++++++++++++++++++-------------- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs index 56470190..a9e9c732 100644 --- a/examples/integration_opengl/src/main.rs +++ b/examples/integration_opengl/src/main.rs @@ -31,7 +31,7 @@ pub fn main() { .unwrap(); unsafe { - let windowed_context = windowed_context.make_current().unwrap(); + let windowed_context = windowed_context.make_current(todo!("derezzedex")).unwrap(); let gl = glow::Context::from_loader_function(|s| { windowed_context.get_proc_address(s) as *const _ @@ -181,7 +181,7 @@ pub fn main() { ), ); - windowed_context.swap_buffers().unwrap(); + windowed_context.swap_buffers(todo!("derezzedex")).unwrap(); } _ => (), } diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 2960a0bd..75a38d22 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -21,8 +21,8 @@ version = "0.4" [dependencies.glutin] version = "0.29" -git = "https://github.com/iced-rs/glutin" -rev = "da8d291486b4c9bec12487a46c119c4b1d386abf" +git = "https://github.com/derezzedex/glutin" +rev = "e72ea919f95106cfdfdce3e7dcfdbf71a432840a" [dependencies.iced_native] version = "0.7" diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 108d7ffa..7ff6216e 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -120,7 +120,7 @@ where #[allow(unsafe_code)] unsafe { - context.make_current().expect("Make OpenGL context current") + context.make_current(todo!()).expect("Make OpenGL context current") } }; @@ -359,7 +359,7 @@ async fn run_instance( unsafe { if !context.is_current() { context = context - .make_current() + .make_current(todo!()) .expect("Make OpenGL context current"); } } @@ -415,7 +415,7 @@ async fn run_instance( &debug.overlay(), ); - context.swap_buffers().expect("Swap buffers"); + context.swap_buffers(todo!()).expect("Swap buffers"); debug.render_finished(); diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 4949219f..ce34aa31 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -58,7 +58,7 @@ where runtime.enter(|| A::new(flags)) }; - let context = { + let (context, window) = { let builder = settings.window.into_builder( &application.title(), event_loop.primary_monitor(), @@ -115,7 +115,14 @@ where #[allow(unsafe_code)] unsafe { - context.make_current().expect("Make OpenGL context current") + let (context, window) = context.split(); + + ( + context + .make_current(&window) + .expect("Make OpenGL context current"), + window, + ) } }; @@ -137,6 +144,7 @@ where debug, receiver, context, + window, init_command, settings.exit_on_close_request, )); @@ -205,7 +213,8 @@ async fn run_instance( mut receiver: mpsc::UnboundedReceiver< glutin::event::Event<'_, Event>, >, - context: glutin::ContextWrapper, + mut context: glutin::RawContext, + window: Window, init_command: Command, _exit_on_close_request: bool, ) where @@ -217,9 +226,9 @@ async fn run_instance( use glutin::event; use iced_winit::futures::stream::StreamExt; - let mut clipboard = Clipboard::connect(context.window()); + let mut clipboard = Clipboard::connect(&window); let mut cache = user_interface::Cache::default(); - let state = State::new(&application, context.window()); + let state = State::new(&application, &window); let user_interface = multi_window::build_user_interface( &application, user_interface::Cache::default(), @@ -229,9 +238,7 @@ async fn run_instance( window::Id::MAIN, ); - #[allow(unsafe_code)] - let (mut context, window) = unsafe { context.split() }; - + let mut current_context_window = window.id(); let mut window_ids = HashMap::from([(window.id(), window::Id::MAIN)]); let mut windows = HashMap::from([(window::Id::MAIN, window)]); let mut states = HashMap::from([(window::Id::MAIN, state)]); @@ -445,15 +452,19 @@ async fn run_instance( .get(&id) .and_then(|id| states.get_mut(id)) .unwrap(); + let window = + window_ids.get(&id).and_then(|id| windows.get(id)).unwrap(); debug.render_started(); #[allow(unsafe_code)] unsafe { - if !context.is_current() { + if current_context_window != id { context = context - .make_current() + .make_current(window) .expect("Make OpenGL context current"); + + current_context_window = id; } } @@ -483,11 +494,6 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != mouse_interaction { - let window = window_ids - .get(&id) - .and_then(|id| windows.get_mut(id)) - .unwrap(); - window.set_cursor_icon(conversion::mouse_interaction( new_mouse_interaction, )); @@ -513,7 +519,7 @@ async fn run_instance( &debug.overlay(), ); - context.swap_buffers().expect("Swap buffers"); + context.swap_buffers(window).expect("Swap buffers"); debug.render_finished(); -- cgit From 1bc0c480f9747826b244c30e92d8c4a29b576e4a Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 19 Oct 2022 22:56:00 -0300 Subject: move window settings to `iced_native` --- examples/integration_opengl/src/main.rs | 3 +- glutin/src/application.rs | 4 +- native/src/window.rs | 4 + native/src/window/icon.rs | 12 +++ native/src/window/position.rs | 22 ++++ native/src/window/settings.rs | 52 ++++++++++ src/window.rs | 11 +- src/window/icon.rs | 172 ------------------------------ src/window/position.rs | 32 ------ src/window/settings.rs | 70 ------------- winit/src/icon.rs | 179 ++++++++++++++++++++++++++++++++ winit/src/lib.rs | 5 +- winit/src/position.rs | 22 ---- winit/src/settings.rs | 21 ++++ 14 files changed, 301 insertions(+), 308 deletions(-) create mode 100644 native/src/window/icon.rs create mode 100644 native/src/window/position.rs create mode 100644 native/src/window/settings.rs delete mode 100644 src/window/icon.rs delete mode 100644 src/window/position.rs delete mode 100644 src/window/settings.rs create mode 100644 winit/src/icon.rs delete mode 100644 winit/src/position.rs diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs index a9e9c732..fdbd7369 100644 --- a/examples/integration_opengl/src/main.rs +++ b/examples/integration_opengl/src/main.rs @@ -31,7 +31,8 @@ pub fn main() { .unwrap(); unsafe { - let windowed_context = windowed_context.make_current(todo!("derezzedex")).unwrap(); + let windowed_context = + windowed_context.make_current(todo!("derezzedex")).unwrap(); let gl = glow::Context::from_loader_function(|s| { windowed_context.get_proc_address(s) as *const _ diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 7ff6216e..cbb23891 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -120,7 +120,9 @@ where #[allow(unsafe_code)] unsafe { - context.make_current(todo!()).expect("Make OpenGL context current") + context + .make_current(todo!()) + .expect("Make OpenGL context current") } }; diff --git a/native/src/window.rs b/native/src/window.rs index dc9e2d66..1c03fcdf 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -1,12 +1,16 @@ //! Build window-based GUI applications. mod action; mod event; +mod icon; mod id; mod mode; mod user_attention; pub use action::Action; pub use event::Event; +pub use icon::Icon; pub use id::Id; pub use mode::Mode; pub use user_attention::UserAttention; +pub use position::Position; +pub use settings::Settings; diff --git a/native/src/window/icon.rs b/native/src/window/icon.rs new file mode 100644 index 00000000..e89baf03 --- /dev/null +++ b/native/src/window/icon.rs @@ -0,0 +1,12 @@ +//! Attach an icon to the window of your application. + +/// The icon of a window. +#[derive(Debug, Clone)] +pub struct Icon { + /// TODO(derezzedex) + pub rgba: Vec, + /// TODO(derezzedex) + pub width: u32, + /// TODO(derezzedex) + pub height: u32, +} diff --git a/native/src/window/position.rs b/native/src/window/position.rs new file mode 100644 index 00000000..c260c29e --- /dev/null +++ b/native/src/window/position.rs @@ -0,0 +1,22 @@ +/// 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 + } +} diff --git a/native/src/window/settings.rs b/native/src/window/settings.rs new file mode 100644 index 00000000..67798fbe --- /dev/null +++ b/native/src/window/settings.rs @@ -0,0 +1,52 @@ +use crate::window::{Icon, Position}; + +/// The window settings of an application. +#[derive(Debug, Clone)] +pub struct Settings { + /// The initial size of the window. + pub size: (u32, u32), + + /// The initial 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. or not. + pub decorations: bool, + + /// Whether the window should be transparent. + pub transparent: bool, + + /// Whether the window will always be on top of other windows. + pub always_on_top: bool, + + /// The icon of the window. + pub icon: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + size: (1024, 768), + position: Position::default(), + min_size: None, + max_size: None, + visible: true, + resizable: true, + decorations: true, + transparent: false, + always_on_top: false, + icon: None, + } + } +} diff --git a/src/window.rs b/src/window.rs index 2018053f..73e90243 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,7 @@ //! Configure the window of your application in native platforms. -mod position; -mod settings; - -pub mod icon; - -pub use icon::Icon; -pub use position::Position; -pub use settings::Settings; +pub use iced_native::window::Icon; +pub use iced_native::window::Position; +pub use iced_native::window::Settings; #[cfg(not(target_arch = "wasm32"))] pub use crate::runtime::window::*; diff --git a/src/window/icon.rs b/src/window/icon.rs deleted file mode 100644 index bacad41a..00000000 --- a/src/window/icon.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! Attach an icon to the window of your application. -use std::fmt; -use std::io; - -#[cfg(feature = "image_rs")] -use std::path::Path; - -/// The icon of a window. -#[derive(Debug, Clone)] -pub struct Icon(iced_winit::winit::window::Icon); - -impl Icon { - /// Creates an icon from 32bpp RGBA data. - pub fn from_rgba( - rgba: Vec, - width: u32, - height: u32, - ) -> Result { - let raw = - iced_winit::winit::window::Icon::from_rgba(rgba, width, height)?; - - Ok(Icon(raw)) - } - - /// Creates an icon from an image file. - /// - /// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. - #[cfg(feature = "image_rs")] - pub fn from_file>(icon_path: P) -> Result { - let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); - - Self::from_rgba(icon.to_vec(), icon.width(), icon.height()) - } - - /// Creates an icon from the content of an image file. - /// - /// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. \ - /// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. - #[cfg(feature = "image_rs")] - pub fn from_file_data( - data: &[u8], - explicit_format: Option, - ) -> Result { - let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data)); - let icon_with_format = match explicit_format { - Some(format) => { - icon.set_format(format); - icon - } - None => icon.with_guessed_format()?, - }; - - let pixels = icon_with_format.decode()?.to_rgba8(); - - Self::from_rgba(pixels.to_vec(), pixels.width(), pixels.height()) - } -} - -/// An error produced when using `Icon::from_rgba` with invalid arguments. -#[derive(Debug)] -pub enum Error { - /// The provided RGBA data isn't divisble by 4. - /// - /// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels. - InvalidData { - /// The length of the provided RGBA data. - byte_count: usize, - }, - - /// The number of RGBA pixels does not match the provided dimensions. - DimensionsMismatch { - /// The provided width. - width: u32, - /// The provided height. - height: u32, - /// The amount of pixels of the provided RGBA data. - pixel_count: usize, - }, - - /// The underlying OS failed to create the icon. - OsError(io::Error), - - /// The `image` crate reported an error - #[cfg(feature = "image_rs")] - ImageError(image_rs::error::ImageError), -} - -impl From for Error { - fn from(os_error: std::io::Error) -> Self { - Error::OsError(os_error) - } -} - -impl From for Error { - fn from(error: iced_winit::winit::window::BadIcon) -> Self { - use iced_winit::winit::window::BadIcon; - - match error { - BadIcon::ByteCountNotDivisibleBy4 { byte_count } => { - Error::InvalidData { byte_count } - } - BadIcon::DimensionsVsPixelCount { - width, - height, - pixel_count, - .. - } => Error::DimensionsMismatch { - width, - height, - pixel_count, - }, - BadIcon::OsError(os_error) => Error::OsError(os_error), - } - } -} - -impl From for iced_winit::winit::window::Icon { - fn from(icon: Icon) -> Self { - icon.0 - } -} - -#[cfg(feature = "image_rs")] -impl From for Error { - fn from(image_error: image_rs::error::ImageError) -> Self { - Self::ImageError(image_error) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::InvalidData { byte_count } => { - write!( - f, - "The provided RGBA data (with length {:?}) isn't divisble by \ - 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \ - pixels.", - byte_count, - ) - } - Error::DimensionsMismatch { - width, - height, - pixel_count, - } => { - write!( - f, - "The number of RGBA pixels ({:?}) does not match the provided \ - dimensions ({:?}x{:?}).", - pixel_count, width, height, - ) - } - Error::OsError(e) => write!( - f, - "The underlying OS failed to create the window \ - icon: {:?}", - e - ), - #[cfg(feature = "image_rs")] - Error::ImageError(e) => { - write!(f, "Unable to create icon from a file: {:?}", e) - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(self) - } -} diff --git a/src/window/position.rs b/src/window/position.rs deleted file mode 100644 index 6b9fac41..00000000 --- a/src/window/position.rs +++ /dev/null @@ -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 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), - } - } -} diff --git a/src/window/settings.rs b/src/window/settings.rs deleted file mode 100644 index 24d0f4f9..00000000 --- a/src/window/settings.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::window::{Icon, Position}; - -/// The window settings of an application. -#[derive(Debug, Clone)] -pub struct Settings { - /// The initial size of the window. - pub size: (u32, u32), - - /// The initial 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. or not. - pub decorations: bool, - - /// Whether the window should be transparent. - pub transparent: bool, - - /// Whether the window will always be on top of other windows. - pub always_on_top: bool, - - /// The icon of the window. - pub icon: Option, -} - -impl Default for Settings { - fn default() -> Settings { - Settings { - size: (1024, 768), - position: Position::default(), - min_size: None, - max_size: None, - visible: true, - resizable: true, - decorations: true, - transparent: false, - always_on_top: false, - icon: None, - } - } -} - -impl From 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, - always_on_top: settings.always_on_top, - icon: settings.icon.map(Icon::into), - platform_specific: Default::default(), - } - } -} diff --git a/winit/src/icon.rs b/winit/src/icon.rs new file mode 100644 index 00000000..84b88b39 --- /dev/null +++ b/winit/src/icon.rs @@ -0,0 +1,179 @@ +//! Attach an icon to the window of your application. +use std::fmt; +use std::io; + +#[cfg(feature = "image_rs")] +use std::path::Path; + +/// The icon of a window. +#[derive(Debug, Clone)] +pub struct Icon(winit::window::Icon); + +impl Icon { + /// Creates an icon from 32bpp RGBA data. + pub fn from_rgba( + rgba: Vec, + width: u32, + height: u32, + ) -> Result { + let raw = winit::window::Icon::from_rgba(rgba, width, height)?; + + Ok(Icon(raw)) + } + + /// Creates an icon from an image file. + /// + /// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. + #[cfg(feature = "image_rs")] + pub fn from_file>(icon_path: P) -> Result { + let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); + + Self::from_rgba(icon.to_vec(), icon.width(), icon.height()) + } + + /// Creates an icon from the content of an image file. + /// + /// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. \ + /// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. + #[cfg(feature = "image_rs")] + pub fn from_file_data( + data: &[u8], + explicit_format: Option, + ) -> Result { + let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data)); + let icon_with_format = match explicit_format { + Some(format) => { + icon.set_format(format); + icon + } + None => icon.with_guessed_format()?, + }; + + let pixels = icon_with_format.decode()?.to_rgba8(); + + Self::from_rgba(pixels.to_vec(), pixels.width(), pixels.height()) + } +} + +/// An error produced when using `Icon::from_rgba` with invalid arguments. +#[derive(Debug)] +pub enum Error { + /// The provided RGBA data isn't divisble by 4. + /// + /// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels. + InvalidData { + /// The length of the provided RGBA data. + byte_count: usize, + }, + + /// The number of RGBA pixels does not match the provided dimensions. + DimensionsMismatch { + /// The provided width. + width: u32, + /// The provided height. + height: u32, + /// The amount of pixels of the provided RGBA data. + pixel_count: usize, + }, + + /// The underlying OS failed to create the icon. + OsError(io::Error), + + /// The `image` crate reported an error + #[cfg(feature = "image_rs")] + ImageError(image_rs::error::ImageError), +} + +impl From for Error { + fn from(os_error: std::io::Error) -> Self { + Error::OsError(os_error) + } +} + +impl From for Error { + fn from(error: winit::window::BadIcon) -> Self { + use winit::window::BadIcon; + + match error { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => { + Error::InvalidData { byte_count } + } + BadIcon::DimensionsVsPixelCount { + width, + height, + pixel_count, + .. + } => Error::DimensionsMismatch { + width, + height, + pixel_count, + }, + BadIcon::OsError(os_error) => Error::OsError(os_error), + } + } +} + +impl From for winit::window::Icon { + fn from(icon: Icon) -> Self { + icon.0 + } +} + +#[cfg(feature = "image_rs")] +impl From for Error { + fn from(image_error: image_rs::error::ImageError) -> Self { + Self::ImageError(image_error) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidData { byte_count } => { + write!( + f, + "The provided RGBA data (with length {:?}) isn't divisble by \ + 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \ + pixels.", + byte_count, + ) + } + Error::DimensionsMismatch { + width, + height, + pixel_count, + } => { + write!( + f, + "The number of RGBA pixels ({:?}) does not match the provided \ + dimensions ({:?}x{:?}).", + pixel_count, width, height, + ) + } + Error::OsError(e) => write!( + f, + "The underlying OS failed to create the window \ + icon: {:?}", + e + ), + #[cfg(feature = "image_rs")] + Error::ImageError(e) => { + write!(f, "Unable to create icon from a file: {:?}", e) + } + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self) + } +} + +impl TryFrom for Icon { + type Error = Error; + + fn try_from(icon: iced_native::window::Icon) -> Result { + Icon::from_rgba(icon.rgba, icon.width, icon.height) + } +} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 9b3c0a02..eb58482b 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -49,7 +49,7 @@ pub mod window; pub mod system; mod error; -mod position; +mod icon; mod proxy; #[cfg(feature = "application")] @@ -58,8 +58,9 @@ pub use application::Application; pub use application::Profiler; pub use clipboard::Clipboard; pub use error::Error; -pub use position::Position; +pub use icon::Icon; pub use proxy::Proxy; pub use settings::Settings; pub use iced_graphics::Viewport; +pub use iced_native::window::Position; diff --git a/winit/src/position.rs b/winit/src/position.rs deleted file mode 100644 index c260c29e..00000000 --- a/winit/src/position.rs +++ /dev/null @@ -1,22 +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 - } -} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index ea0ba361..78c8c156 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -22,6 +22,7 @@ mod platform; pub use platform::PlatformSpecific; use crate::conversion; +use crate::Icon; use crate::Position; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; @@ -201,3 +202,23 @@ impl Default for Window { } } } + +impl From for Window { + fn from(settings: iced_native::window::Settings) -> Self { + Self { + size: settings.size, + position: 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, + always_on_top: settings.always_on_top, + icon: settings.icon.and_then(|icon| { + Icon::try_from(icon).map(winit::window::Icon::from).ok() + }), + platform_specific: Default::default(), + } + } +} -- cgit From f93fa0254329ebddca21ea1a79bd8ee6d8b4bdaf Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 19 Oct 2022 23:33:20 -0300 Subject: introduce `window::spawn` and `window::close` --- glutin/src/multi_window.rs | 79 ++++++++++++++++++++++++++------------------- native/src/window/action.rs | 15 +++++++-- winit/src/application.rs | 5 +++ winit/src/multi_window.rs | 79 ++++++++++++++++++++++++++------------------- winit/src/window.rs | 16 +++++++++ 5 files changed, 123 insertions(+), 71 deletions(-) diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index ce34aa31..2a66a816 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -682,41 +682,52 @@ pub fn run_command( clipboard.write(contents); } }, - command::Action::Window(id, action) => { - let window = windows.get(&id).expect("No window found"); - - match action { - window::Action::Resize { width, height } => { - window.set_inner_size(glutin::dpi::LogicalSize { - width, - height, - }); - } - window::Action::Move { x, y } => { - window.set_outer_position( - glutin::dpi::LogicalPosition { x, y }, - ); - } - window::Action::SetMode(mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.primary_monitor(), - mode, - )); - } - window::Action::FetchMode(tag) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - window::Mode::Hidden - }; - - proxy - .send_event(Event::Application(tag(mode))) - .expect("Send message to event loop"); - } + command::Action::Window(id, action) => match action { + window::Action::Spawn { settings } => { + proxy + .send_event(Event::NewWindow(id, settings.into())) + .expect("Send message to event loop"); } - } + window::Action::Close => { + proxy + .send_event(Event::CloseWindow(id)) + .expect("Send message to event loop"); + } + window::Action::Resize { width, height } => { + let window = windows.get(&id).expect("No window found"); + window.set_inner_size(glutin::dpi::LogicalSize { + width, + height, + }); + } + window::Action::Move { x, y } => { + let window = windows.get(&id).expect("No window found"); + window.set_outer_position(glutin::dpi::LogicalPosition { + x, + y, + }); + } + window::Action::SetMode(mode) => { + let window = windows.get(&id).expect("No window found"); + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.primary_monitor(), + mode, + )); + } + window::Action::FetchMode(tag) => { + let window = windows.get(&id).expect("No window found"); + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + window::Mode::Hidden + }; + + proxy + .send_event(Event::Application(tag(mode))) + .expect("Send message to event loop"); + } + }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "iced_winit/system")] diff --git a/native/src/window/action.rs b/native/src/window/action.rs index 37fcc273..0587f25c 100644 --- a/native/src/window/action.rs +++ b/native/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::window::{Mode, UserAttention}; +use crate::window::{self, Mode, UserAttention}; use iced_futures::MaybeSend; use std::fmt; @@ -13,6 +13,11 @@ pub enum Action { /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. Drag, + /// TODO(derezzedex) + Spawn { + /// TODO(derezzedex) + settings: window::Settings, + }, /// Resize the window. Resize { /// The new logical width of the window @@ -34,9 +39,9 @@ pub enum Action { y: i32, }, /// Set the [`Mode`] of the window. - SetMode(Mode), + SetMode(window::Mode), /// Fetch the current [`Mode`] of the window. - FetchMode(Box T + 'static>), + FetchMode(Box T + 'static>), /// Sets the window to maximized or back ToggleMaximize, /// Toggles whether window has decorations @@ -81,6 +86,7 @@ impl Action { T: 'static, { match self { + Self::Spawn { settings } => Action::Spawn { settings }, Self::Close => Action::Close, Self::Drag => Action::Drag, Self::Resize { width, height } => Action::Resize { width, height }, @@ -104,6 +110,9 @@ impl fmt::Debug for Action { match self { Self::Close => write!(f, "Action::Close"), Self::Drag => write!(f, "Action::Drag"), + Self::Spawn { settings } => { + write!(f, "Action::Spawn {{ settings: {:?} }}", settings) + } Self::Resize { width, height } => write!( f, "Action::Resize {{ widget: {}, height: {} }}", diff --git a/winit/src/application.rs b/winit/src/application.rs index 4486f5d9..910f3d94 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -675,6 +675,11 @@ pub fn run_command( window::Action::Drag => { let _res = window.drag_window(); } + window::Action::Spawn { .. } | window::Action::Close => { + log::info!( + "This is only available on `multi_window::Application`" + ) + } window::Action::Resize { width, height } => { window.set_inner_size(winit::dpi::LogicalSize { width, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 9f46b88d..1d71d801 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -855,41 +855,52 @@ pub fn run_command( clipboard.write(contents); } }, - command::Action::Window(id, action) => { - let window = windows.get(&id).expect("No window found"); - - match action { - window::Action::Resize { width, height } => { - window.set_inner_size(winit::dpi::LogicalSize { - width, - height, - }); - } - window::Action::Move { x, y } => { - window.set_outer_position( - winit::dpi::LogicalPosition { x, y }, - ); - } - window::Action::SetMode(mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.primary_monitor(), - mode, - )); - } - window::Action::FetchMode(tag) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - window::Mode::Hidden - }; - - proxy - .send_event(Event::Application(tag(mode))) - .expect("Send message to event loop"); - } + command::Action::Window(id, action) => match action { + window::Action::Spawn { settings } => { + proxy + .send_event(Event::NewWindow(id, settings.into())) + .expect("Send message to event loop"); } - } + window::Action::Close => { + proxy + .send_event(Event::CloseWindow(id)) + .expect("Send message to event loop"); + } + window::Action::Resize { width, height } => { + let window = windows.get(&id).expect("No window found"); + window.set_inner_size(winit::dpi::LogicalSize { + width, + height, + }); + } + window::Action::Move { x, y } => { + let window = windows.get(&id).expect("No window found"); + window.set_outer_position(winit::dpi::LogicalPosition { + x, + y, + }); + } + window::Action::SetMode(mode) => { + let window = windows.get(&id).expect("No window found"); + window.set_visible(conversion::visible(mode)); + window.set_fullscreen(conversion::fullscreen( + window.primary_monitor(), + mode, + )); + } + window::Action::FetchMode(tag) => { + let window = windows.get(&id).expect("No window found"); + let mode = if window.is_visible().unwrap_or(true) { + conversion::mode(window.fullscreen()) + } else { + window::Mode::Hidden + }; + + proxy + .send_event(Event::Application(tag(mode))) + .expect("Send message to event loop"); + } + }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "system")] diff --git a/winit/src/window.rs b/winit/src/window.rs index d9bc0d83..fba863ef 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -14,6 +14,22 @@ pub fn drag() -> Command { Command::single(command::Action::Window(window::Action::Drag)) } +/// TODO(derezzedex) +pub fn spawn( + id: window::Id, + settings: window::Settings, +) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Spawn { settings }, + )) +} + +/// TODO(derezzedex) +pub fn close(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::Close)) +} + /// Resizes the window to the given logical dimensions. pub fn resize( id: window::Id, -- cgit From aa7164fdde0cf879139e457555c3985d4e9111f0 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 31 Oct 2022 16:57:57 -0300 Subject: update `glutin` to 0.30 --- glutin/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 75a38d22..addaa16c 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -20,9 +20,9 @@ multi_window = ["iced_winit/multi_window"] version = "0.4" [dependencies.glutin] -version = "0.29" +version = "0.30" git = "https://github.com/derezzedex/glutin" -rev = "e72ea919f95106cfdfdce3e7dcfdbf71a432840a" +rev = "2a2a97209c49929027beced68e1989b8486bdec9" [dependencies.iced_native] version = "0.7" -- cgit From 0553062be1898873fb057c0446b772ab07b551e5 Mon Sep 17 00:00:00 2001 From: Richard Date: Mon, 31 Oct 2022 20:23:24 -0300 Subject: update `iced_glutin` to use new surface api --- glutin/Cargo.toml | 3 + glutin/src/application.rs | 338 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 254 insertions(+), 87 deletions(-) diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index addaa16c..70820780 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -16,6 +16,9 @@ debug = ["iced_winit/debug"] system = ["iced_winit/system"] multi_window = ["iced_winit/multi_window"] +[dependencies.raw-window-handle] +version = "0.5.0" + [dependencies.log] version = "0.4" diff --git a/glutin/src/application.rs b/glutin/src/application.rs index cbb23891..45ff37f0 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -12,14 +12,33 @@ use iced_winit::futures; use iced_winit::futures::channel::mpsc; use iced_winit::renderer; use iced_winit::user_interface; +use iced_winit::winit; use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; -use glutin::window::Window; +use glutin::config::{ + Config, ConfigSurfaceTypes, ConfigTemplateBuilder, GlConfig, +}; +use glutin::context::{ + ContextApi, ContextAttributesBuilder, NotCurrentContext, + NotCurrentGlContextSurfaceAccessor, + PossiblyCurrentContextGlSurfaceAccessor, PossiblyCurrentGlContext, +}; +use glutin::display::{Display, DisplayApiPreference, GlDisplay}; +use glutin::surface::{ + GlSurface, Surface, SurfaceAttributesBuilder, WindowSurface, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + +use std::ffi::CString; use std::mem::ManuallyDrop; +use std::num::NonZeroU32; #[cfg(feature = "tracing")] use tracing::{info_span, instrument::Instrument}; +#[allow(unsafe_code)] +const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; + /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. pub fn run( @@ -34,9 +53,8 @@ where { use futures::task; use futures::Future; - use glutin::event_loop::EventLoopBuilder; - use glutin::platform::run_return::EventLoopExtRunReturn; - use glutin::ContextBuilder; + use winit::event_loop::EventLoopBuilder; + use winit::platform::run_return::EventLoopExtRunReturn; #[cfg(feature = "trace")] let _guard = iced_winit::Profiler::init(); @@ -63,76 +81,216 @@ where runtime.enter(|| A::new(flags)) }; - let context = { - let builder = settings.window.into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ); + let builder = settings.window.into_builder( + &application.title(), + event_loop.primary_monitor(), + settings.id, + ); - log::info!("Window builder: {:#?}", builder); + log::info!("Window builder: {:#?}", builder); - let opengl_builder = ContextBuilder::new() - .with_vsync(true) - .with_multisampling(C::sample_count(&compositor_settings) as u16); + #[allow(unsafe_code)] + let (display, window, surface, context) = unsafe { + struct Configuration(Config); + use std::fmt; + impl fmt::Debug for Configuration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let config = &self.0; + + f.debug_struct("Configuration") + .field("raw", &config) + .field("samples", &config.num_samples()) + .field("buffer_type", &config.color_buffer_type()) + .field("surface_type", &config.config_surface_types()) + .field("depth", &config.depth_size()) + .field("alpha", &config.alpha_size()) + .field("stencil", &config.stencil_size()) + .field("float_pixels", &config.float_pixels()) + .field("srgb", &config.srgb_capable()) + .field("api", &config.api()) + .finish() + } + } - let opengles_builder = opengl_builder.clone().with_gl( - glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)), - ); + impl AsRef for Configuration { + fn as_ref(&self) -> &Config { + &self.0 + } + } + + let display_handle = event_loop.raw_display_handle(); + + #[cfg(all( + any(windows, target_os = "macos"), + not(target_arch = "wasm32") + ))] + let (window, window_handle) = { + let window = builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; + + let handle = window.raw_window_handle(); - let (first_builder, second_builder) = if settings.try_opengles_first { - (opengles_builder, opengl_builder) - } else { - (opengl_builder, opengles_builder) + (window, handle) }; - log::info!("Trying first builder: {:#?}", first_builder); + #[cfg(target_arch = "wasm32")] + let preference = Err(Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "target not supported by backend" + )), + ))?; + + #[cfg(all(windows, not(target_arch = "wasm32")))] + let preference = DisplayApiPreference::WglThenEgl(Some(window_handle)); + + #[cfg(all(target_os = "macos", not(target_arch = "wasm32")))] + let preference = DisplayApiPreference::Cgl; + + #[cfg(all( + unix, + not(target_os = "macos"), + not(target_arch = "wasm32") + ))] + let preference = DisplayApiPreference::GlxThenEgl(Box::new( + winit::platform::unix::register_xlib_error_hook, + )); + + let display = + Display::new(display_handle, preference).map_err(|error| { + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create display: {error}" + )), + ) + })?; + + log::debug!("Display: {}", display.version_string()); + + let samples = C::sample_count(&compositor_settings) as u8; + let mut template = ConfigTemplateBuilder::new() + .with_surface_type(ConfigSurfaceTypes::WINDOW); + + if samples != 0 { + template = template.with_multisampling(samples); + } + + #[cfg(all(windows, not(target_arch = "wasm32")))] + let template = template.compatible_with_native_window(window_handle); + + log::debug!("Searching for display configurations"); + let configuration = display + .find_configs(template.build()) + .map_err(|_| { + Error::GraphicsCreationFailed( + iced_graphics::Error::NoAvailablePixelFormat, + ) + })? + .map(Configuration) + .inspect(|config| { + log::trace!("{config:#?}"); + }) + .min_by_key(|config| { + config.as_ref().num_samples().saturating_sub(samples) + }) + .ok_or(Error::GraphicsCreationFailed( + iced_graphics::Error::NoAvailablePixelFormat, + ))?; + + log::debug!("Selected: {configuration:#?}"); + + #[cfg(all( + unix, + not(target_os = "macos"), + not(target_arch = "wasm32") + ))] + let (window, window_handle) = { + use glutin::platform::x11::X11GlConfigExt; + let builder = + if let Some(visual) = configuration.as_ref().x11_visual() { + use winit::platform::unix::WindowBuilderExtUnix; + builder.with_x11_visual(visual.into_raw()) + } else { + builder + }; + + let window = builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; + + let handle = window.raw_window_handle(); + + (window, handle) + }; - let context = first_builder - .build_windowed(builder.clone(), &event_loop) + let attributes = + ContextAttributesBuilder::new().build(Some(window_handle)); + let fallback_attributes = ContextAttributesBuilder::new() + .with_context_api(ContextApi::Gles(None)) + .build(Some(window_handle)); + + let context = display + .create_context(configuration.as_ref(), &attributes) .or_else(|_| { - log::info!("Trying second builder: {:#?}", second_builder); - second_builder.build_windowed(builder, &event_loop) + display.create_context( + configuration.as_ref(), + &fallback_attributes, + ) }) .map_err(|error| { - use glutin::CreationError; - use iced_graphics::Error as ContextError; + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create context: {error}" + )), + ) + })?; - match error { - CreationError::Window(error) => { - Error::WindowCreationFailed(error) - } - CreationError::OpenGlVersionNotSupported => { - Error::GraphicsCreationFailed( - ContextError::VersionNotSupported, - ) - } - CreationError::NoAvailablePixelFormat => { - Error::GraphicsCreationFailed( - ContextError::NoAvailablePixelFormat, - ) - } - error => Error::GraphicsCreationFailed( - ContextError::BackendError(error.to_string()), - ), - } + let (width, height) = window.inner_size().into(); + let surface_attributes = + SurfaceAttributesBuilder::::new() + .with_srgb(Some(true)) + .build( + window_handle, + NonZeroU32::new(width).unwrap_or(ONE), + NonZeroU32::new(height).unwrap_or(ONE), + ); + + let surface = display + .create_window_surface(configuration.as_ref(), &surface_attributes) + .map_err(|error| { + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create surface: {error}" + )), + ) })?; - #[allow(unsafe_code)] - unsafe { + let context = { context - .make_current(todo!()) - .expect("Make OpenGL context current") + .make_current(&surface) + .expect("make context current") + }; + + if let Err(error) = surface.set_swap_interval( + &context, + glutin::surface::SwapInterval::Wait(ONE), + ) { + log::error!("set swap interval failed: {}", error); } + + (display, window, surface, context) }; #[allow(unsafe_code)] let (compositor, renderer) = unsafe { C::new(compositor_settings, |address| { - context.get_proc_address(address) + let address = CString::new(address).expect("address error"); + display.get_proc_address(address.as_c_str()) })? }; + let context = { context.make_not_current().expect("make context current") }; + let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin({ @@ -144,6 +302,8 @@ where proxy, debug, receiver, + window, + surface, context, init_command, settings.exit_on_close_request, @@ -159,22 +319,22 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); let _ = event_loop.run_return(move |event, _, control_flow| { - use glutin::event_loop::ControlFlow; + use winit::event_loop::ControlFlow; if let ControlFlow::ExitWithCode(_) = control_flow { return; } let event = match event { - glutin::event::Event::WindowEvent { + winit::event::Event::WindowEvent { event: - glutin::event::WindowEvent::ScaleFactorChanged { + winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. }, window_id, - } => Some(glutin::event::Event::WindowEvent { - event: glutin::event::WindowEvent::Resized(*new_inner_size), + } => Some(winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::Resized(*new_inner_size), window_id, }), _ => event.to_static(), @@ -200,10 +360,12 @@ async fn run_instance( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, - mut proxy: glutin::event_loop::EventLoopProxy, + mut proxy: winit::event_loop::EventLoopProxy, mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver>, - mut context: glutin::ContextWrapper, + mut receiver: mpsc::UnboundedReceiver>, + window: winit::window::Window, + surface: Surface, + context: NotCurrentContext, init_command: Command, exit_on_close_request: bool, ) where @@ -212,12 +374,18 @@ async fn run_instance( C: window::GLCompositor + 'static, ::Theme: StyleSheet, { - use glutin::event; use iced_winit::futures::stream::StreamExt; + use winit::event; + + let context = { + context + .make_current(&surface) + .expect("make context current") + }; - let mut clipboard = Clipboard::connect(context.window()); + let mut clipboard = Clipboard::connect(&window); let mut cache = user_interface::Cache::default(); - let mut state = application::State::new(&application, context.window()); + let mut state = application::State::new(&application, &window); let mut viewport_version = state.viewport_version(); let mut should_exit = false; @@ -232,7 +400,7 @@ async fn run_instance( &mut should_exit, &mut proxy, &mut debug, - context.window(), + &window, || compositor.fetch_information(), ); runtime.track(application.subscription()); @@ -296,12 +464,12 @@ async fn run_instance( &mut proxy, &mut debug, &mut messages, - context.window(), + &window, || compositor.fetch_information(), ); // Update window - state.synchronize(&application, context.window()); + state.synchronize(&application, &window); user_interface = ManuallyDrop::new(application::build_user_interface( @@ -329,14 +497,14 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != mouse_interaction { - context.window().set_cursor_icon( - conversion::mouse_interaction(new_mouse_interaction), - ); + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); mouse_interaction = new_mouse_interaction; } - context.window().request_redraw(); + window.request_redraw(); } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), @@ -357,13 +525,10 @@ async fn run_instance( debug.render_started(); - #[allow(unsafe_code)] - unsafe { - if !context.is_current() { - context = context - .make_current(todo!()) - .expect("Make OpenGL context current"); - } + if !context.is_current() { + context + .make_current(&surface) + .expect("Make OpenGL context current"); } let current_viewport_version = state.viewport_version(); @@ -391,19 +556,18 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != mouse_interaction { - context.window().set_cursor_icon( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); + window.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); mouse_interaction = new_mouse_interaction; } - context.resize(glutin::dpi::PhysicalSize::new( - physical_size.width, - physical_size.height, - )); + surface.resize( + &context, + NonZeroU32::new(physical_size.width).unwrap_or(ONE), + NonZeroU32::new(physical_size.height).unwrap_or(ONE), + ); compositor.resize_viewport(physical_size); @@ -417,7 +581,7 @@ async fn run_instance( &debug.overlay(), ); - context.swap_buffers(todo!()).expect("Swap buffers"); + surface.swap_buffers(&context).expect("Swap buffers"); debug.render_finished(); @@ -434,7 +598,7 @@ async fn run_instance( break; } - state.update(context.window(), &window_event, &mut debug); + state.update(&window, &window_event, &mut debug); if let Some(event) = conversion::window_event( crate::window::Id::MAIN, -- cgit From ac20f35c6245bbafffd4d047764fb04e66dcfe75 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 2 Nov 2022 10:17:49 -0300 Subject: update `glutin\multi_window` to new surface api --- glutin/src/multi_window.rs | 372 +++++++++++++++++++++++++++++---------- glutin/src/multi_window/state.rs | 20 ++- 2 files changed, 294 insertions(+), 98 deletions(-) diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 2a66a816..095e0e2c 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -15,11 +15,30 @@ use iced_winit::renderer; use iced_winit::settings; use iced_winit::user_interface; use iced_winit::window; +use iced_winit::winit; use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; -use glutin::window::Window; +use glutin::config::{ + Config, ConfigSurfaceTypes, ConfigTemplateBuilder, GlConfig, +}; +use glutin::context::{ + ContextApi, ContextAttributesBuilder, NotCurrentContext, + NotCurrentGlContextSurfaceAccessor, + PossiblyCurrentContextGlSurfaceAccessor, PossiblyCurrentGlContext, +}; +use glutin::display::{Display, DisplayApiPreference, GlDisplay}; +use glutin::surface::{ + GlSurface, Surface, SurfaceAttributesBuilder, WindowSurface, +}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + use std::collections::HashMap; +use std::ffi::CString; use std::mem::ManuallyDrop; +use std::num::NonZeroU32; + +#[allow(unsafe_code)] +const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. @@ -35,9 +54,8 @@ where { use futures::task; use futures::Future; - use glutin::event_loop::EventLoopBuilder; - use glutin::platform::run_return::EventLoopExtRunReturn; - use glutin::ContextBuilder; + use winit::event_loop::EventLoopBuilder; + use winit::platform::run_return::EventLoopExtRunReturn; let mut debug = Debug::new(); debug.startup_started(); @@ -58,81 +76,216 @@ where runtime.enter(|| A::new(flags)) }; - let (context, window) = { - let builder = settings.window.into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ); + let builder = settings.window.into_builder( + &application.title(), + event_loop.primary_monitor(), + settings.id, + ); + + log::info!("Window builder: {:#?}", builder); + + #[allow(unsafe_code)] + let (display, window, configuration, surface, context) = unsafe { + struct Configuration(Config); + use std::fmt; + impl fmt::Debug for Configuration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let config = &self.0; + + f.debug_struct("Configuration") + .field("raw", &config) + .field("samples", &config.num_samples()) + .field("buffer_type", &config.color_buffer_type()) + .field("surface_type", &config.config_surface_types()) + .field("depth", &config.depth_size()) + .field("alpha", &config.alpha_size()) + .field("stencil", &config.stencil_size()) + .field("float_pixels", &config.float_pixels()) + .field("srgb", &config.srgb_capable()) + .field("api", &config.api()) + .finish() + } + } - log::info!("Window builder: {:#?}", builder); + impl AsRef for Configuration { + fn as_ref(&self) -> &Config { + &self.0 + } + } - let opengl_builder = ContextBuilder::new() - .with_vsync(true) - .with_multisampling(C::sample_count(&compositor_settings) as u16); + let display_handle = event_loop.raw_display_handle(); - let opengles_builder = opengl_builder.clone().with_gl( - glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)), - ); + #[cfg(all( + any(windows, target_os = "macos"), + not(target_arch = "wasm32") + ))] + let (window, window_handle) = { + let window = builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; + + let handle = window.raw_window_handle(); + + (window, handle) + }; + + #[cfg(target_arch = "wasm32")] + let preference = Err(Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "target not supported by backend" + )), + ))?; + + #[cfg(all(windows, not(target_arch = "wasm32")))] + let preference = DisplayApiPreference::WglThenEgl(Some(window_handle)); + + #[cfg(all(target_os = "macos", not(target_arch = "wasm32")))] + let preference = DisplayApiPreference::Cgl; + + #[cfg(all( + unix, + not(target_os = "macos"), + not(target_arch = "wasm32") + ))] + let preference = DisplayApiPreference::GlxThenEgl(Box::new( + winit::platform::unix::register_xlib_error_hook, + )); + + let display = + Display::new(display_handle, preference).map_err(|error| { + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create display: {error}" + )), + ) + })?; + + log::debug!("Display: {}", display.version_string()); + + let samples = C::sample_count(&compositor_settings) as u8; + let mut template = ConfigTemplateBuilder::new() + .with_surface_type(ConfigSurfaceTypes::WINDOW); + + if samples != 0 { + template = template.with_multisampling(samples); + } + + #[cfg(all(windows, not(target_arch = "wasm32")))] + let template = template.compatible_with_native_window(window_handle); + + log::debug!("Searching for display configurations"); + let configuration = display + .find_configs(template.build()) + .map_err(|_| { + Error::GraphicsCreationFailed( + iced_graphics::Error::NoAvailablePixelFormat, + ) + })? + .map(Configuration) + .inspect(|config| { + log::trace!("{config:#?}"); + }) + .min_by_key(|config| { + config.as_ref().num_samples().saturating_sub(samples) + }) + .ok_or(Error::GraphicsCreationFailed( + iced_graphics::Error::NoAvailablePixelFormat, + ))?; + + log::debug!("Selected: {configuration:#?}"); + + #[cfg(all( + unix, + not(target_os = "macos"), + not(target_arch = "wasm32") + ))] + let (window, window_handle) = { + use glutin::platform::x11::X11GlConfigExt; + let builder = + if let Some(visual) = configuration.as_ref().x11_visual() { + use winit::platform::unix::WindowBuilderExtUnix; + builder.with_x11_visual(visual.into_raw()) + } else { + builder + }; + + let window = builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?; + + let handle = window.raw_window_handle(); - let (first_builder, second_builder) = if settings.try_opengles_first { - (opengles_builder, opengl_builder) - } else { - (opengl_builder, opengles_builder) + (window, handle) }; - log::info!("Trying first builder: {:#?}", first_builder); + let attributes = + ContextAttributesBuilder::new().build(Some(window_handle)); + let fallback_attributes = ContextAttributesBuilder::new() + .with_context_api(ContextApi::Gles(None)) + .build(Some(window_handle)); - let context = first_builder - .build_windowed(builder.clone(), &event_loop) + let context = display + .create_context(configuration.as_ref(), &attributes) .or_else(|_| { - log::info!("Trying second builder: {:#?}", second_builder); - second_builder.build_windowed(builder, &event_loop) + display.create_context( + configuration.as_ref(), + &fallback_attributes, + ) }) .map_err(|error| { - use glutin::CreationError; - use iced_graphics::Error as ContextError; + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create context: {error}" + )), + ) + })?; - match error { - CreationError::Window(error) => { - Error::WindowCreationFailed(error) - } - CreationError::OpenGlVersionNotSupported => { - Error::GraphicsCreationFailed( - ContextError::VersionNotSupported, - ) - } - CreationError::NoAvailablePixelFormat => { - Error::GraphicsCreationFailed( - ContextError::NoAvailablePixelFormat, - ) - } - error => Error::GraphicsCreationFailed( - ContextError::BackendError(error.to_string()), - ), - } + let (width, height) = window.inner_size().into(); + let surface_attributes = + SurfaceAttributesBuilder::::new() + .with_srgb(Some(true)) + .build( + window_handle, + NonZeroU32::new(width).unwrap_or(ONE), + NonZeroU32::new(height).unwrap_or(ONE), + ); + + let surface = display + .create_window_surface(configuration.as_ref(), &surface_attributes) + .map_err(|error| { + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create surface: {error}" + )), + ) })?; - #[allow(unsafe_code)] - unsafe { - let (context, window) = context.split(); + let context = { + context + .make_current(&surface) + .expect("make context current") + }; - ( - context - .make_current(&window) - .expect("Make OpenGL context current"), - window, - ) + if let Err(error) = surface.set_swap_interval( + &context, + glutin::surface::SwapInterval::Wait(ONE), + ) { + log::error!("set swap interval failed: {}", error); } + + (display, window, configuration.0, surface, context) }; #[allow(unsafe_code)] let (compositor, renderer) = unsafe { C::new(compositor_settings, |address| { - context.get_proc_address(address) + let address = CString::new(address).expect("address error"); + display.get_proc_address(address.as_c_str()) })? }; + let context = { context.make_not_current().expect("make context current") }; + let (mut sender, receiver) = mpsc::unbounded(); let mut instance = Box::pin(run_instance::( @@ -143,8 +296,11 @@ where proxy, debug, receiver, - context, + display, window, + configuration, + surface, + context, init_command, settings.exit_on_close_request, )); @@ -152,25 +308,25 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); let _ = event_loop.run_return(move |event, event_loop, control_flow| { - use glutin::event_loop::ControlFlow; + use winit::event_loop::ControlFlow; if let ControlFlow::ExitWithCode(_) = control_flow { return; } let event = match event { - glutin::event::Event::WindowEvent { + winit::event::Event::WindowEvent { event: - glutin::event::WindowEvent::ScaleFactorChanged { + winit::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. }, window_id, - } => Some(glutin::event::Event::WindowEvent { - event: glutin::event::WindowEvent::Resized(*new_inner_size), + } => Some(winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::Resized(*new_inner_size), window_id, }), - glutin::event::Event::UserEvent(Event::NewWindow(id, settings)) => { + winit::event::Event::UserEvent(Event::NewWindow(id, settings)) => { // TODO(derezzedex) let window = settings .into_builder( @@ -181,7 +337,7 @@ where .build(event_loop) .expect("Failed to build window"); - Some(glutin::event::Event::UserEvent(Event::WindowCreated( + Some(winit::event::Event::UserEvent(Event::WindowCreated( id, window, ))) } @@ -208,13 +364,16 @@ async fn run_instance( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime>, Event>, - mut proxy: glutin::event_loop::EventLoopProxy>, + mut proxy: winit::event_loop::EventLoopProxy>, mut debug: Debug, mut receiver: mpsc::UnboundedReceiver< - glutin::event::Event<'_, Event>, + winit::event::Event<'_, Event>, >, - mut context: glutin::RawContext, - window: Window, + display: Display, + window: winit::window::Window, + configuration: Config, + surface: Surface, + context: NotCurrentContext, init_command: Command, _exit_on_close_request: bool, ) where @@ -223,8 +382,14 @@ async fn run_instance( C: iced_graphics::window::GLCompositor + 'static, ::Theme: StyleSheet, { - use glutin::event; use iced_winit::futures::stream::StreamExt; + use winit::event; + + let context = { + context + .make_current(&surface) + .expect("make context current") + }; let mut clipboard = Clipboard::connect(&window); let mut cache = user_interface::Cache::default(); @@ -241,6 +406,7 @@ async fn run_instance( let mut current_context_window = window.id(); let mut window_ids = HashMap::from([(window.id(), window::Id::MAIN)]); let mut windows = HashMap::from([(window::Id::MAIN, window)]); + let mut surfaces = HashMap::from([(window::Id::MAIN, surface)]); let mut states = HashMap::from([(window::Id::MAIN, state)]); let mut interfaces = ManuallyDrop::new(HashMap::from([(window::Id::MAIN, user_interface)])); @@ -419,10 +585,32 @@ async fn run_instance( id, ); + let window_handle = window.raw_window_handle(); + let (width, height) = window.inner_size().into(); + let surface_attributes = + SurfaceAttributesBuilder::::new() + .with_srgb(Some(true)) + .build( + window_handle, + NonZeroU32::new(width).unwrap_or(ONE), + NonZeroU32::new(height).unwrap_or(ONE), + ); + + #[allow(unsafe_code)] + let surface = unsafe { + display + .create_window_surface( + &configuration, + &surface_attributes, + ) + .expect("failed to create surface") + }; + let _ = states.insert(id, state); let _ = interfaces.insert(id, user_interface); let _ = window_ids.insert(window.id(), id); let _ = windows.insert(id, window); + let _ = surfaces.insert(id, surface); } Event::CloseWindow(id) => { // TODO(derezzedex): log errors @@ -437,6 +625,9 @@ async fn run_instance( if interfaces.remove(&id).is_none() { println!("Failed to remove from `interfaces`!"); } + if surfaces.remove(&id).is_none() { + println!("Failed to remove from `surfaces`!") + } if windows.remove(&id).is_none() { println!("Failed to remove from `windows`!") } @@ -455,17 +646,19 @@ async fn run_instance( let window = window_ids.get(&id).and_then(|id| windows.get(id)).unwrap(); + let surface = window_ids + .get(&id) + .and_then(|id| surfaces.get(id)) + .unwrap(); + debug.render_started(); - #[allow(unsafe_code)] - unsafe { - if current_context_window != id { - context = context - .make_current(window) - .expect("Make OpenGL context current"); + if current_context_window != id { + context + .make_current(&surface) + .expect("Make OpenGL context current"); - current_context_window = id; - } + current_context_window = id; } if state.viewport_changed() { @@ -501,10 +694,11 @@ async fn run_instance( mouse_interaction = new_mouse_interaction; } - context.resize(glutin::dpi::PhysicalSize::new( - physical_size.width, - physical_size.height, - )); + surface.resize( + &context, + NonZeroU32::new(physical_size.width).unwrap_or(ONE), + NonZeroU32::new(physical_size.height).unwrap_or(ONE), + ); compositor.resize_viewport(physical_size); @@ -519,7 +713,7 @@ async fn run_instance( &debug.overlay(), ); - context.swap_buffers(window).expect("Swap buffers"); + surface.swap_buffers(&context).expect("Swap buffers"); debug.render_finished(); @@ -595,7 +789,7 @@ pub enum Event { /// TODO(derezzedex) CloseWindow(window::Id), /// TODO(derezzedex) - WindowCreated(window::Id, glutin::window::Window), + WindowCreated(window::Id, winit::window::Window), } /// Updates an [`Application`] by feeding it the provided messages, spawning any @@ -607,10 +801,10 @@ pub fn update( renderer: &mut A::Renderer, runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, - proxy: &mut glutin::event_loop::EventLoopProxy>, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, messages: &mut Vec, - windows: &HashMap, + windows: &HashMap, graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, ) where ::Theme: StyleSheet, @@ -650,9 +844,9 @@ pub fn run_command( command: Command, runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, - proxy: &mut glutin::event_loop::EventLoopProxy>, + proxy: &mut winit::event_loop::EventLoopProxy>, debug: &mut Debug, - windows: &HashMap, + windows: &HashMap, _graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, ) where A: Application, @@ -695,14 +889,14 @@ pub fn run_command( } window::Action::Resize { width, height } => { let window = windows.get(&id).expect("No window found"); - window.set_inner_size(glutin::dpi::LogicalSize { + window.set_inner_size(winit::dpi::LogicalSize { width, height, }); } window::Action::Move { x, y } => { let window = windows.get(&id).expect("No window found"); - window.set_outer_position(glutin::dpi::LogicalPosition { + window.set_outer_position(winit::dpi::LogicalPosition { x, y, }); diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs index 163f46bd..321fc4d1 100644 --- a/glutin/src/multi_window/state.rs +++ b/glutin/src/multi_window/state.rs @@ -4,9 +4,11 @@ use crate::multi_window::{Application, Event}; use crate::window; use crate::{Color, Debug, Point, Size, Viewport}; -use glutin::event::{Touch, WindowEvent}; -use glutin::event_loop::EventLoopProxy; -use glutin::window::Window; +use iced_winit::winit; +use winit::event::{Touch, WindowEvent}; +use winit::event_loop::EventLoopProxy; +use winit::window::Window; + use std::collections::HashMap; use std::marker::PhantomData; @@ -20,8 +22,8 @@ where scale_factor: f64, viewport: Viewport, viewport_changed: bool, - cursor_position: glutin::dpi::PhysicalPosition, - modifiers: glutin::event::ModifiersState, + cursor_position: winit::dpi::PhysicalPosition, + modifiers: winit::event::ModifiersState, theme: ::Theme, appearance: iced_winit::application::Appearance, application: PhantomData, @@ -53,8 +55,8 @@ where viewport, viewport_changed: false, // TODO: Encode cursor availability in the type-system - cursor_position: glutin::dpi::PhysicalPosition::new(-1.0, -1.0), - modifiers: glutin::event::ModifiersState::default(), + cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), + modifiers: winit::event::ModifiersState::default(), theme, appearance, application: PhantomData, @@ -95,7 +97,7 @@ where } /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> glutin::event::ModifiersState { + pub fn modifiers(&self) -> winit::event::ModifiersState { self.modifiers } @@ -156,7 +158,7 @@ where WindowEvent::CursorLeft { .. } => { // TODO: Encode cursor availability in the type-system self.cursor_position = - glutin::dpi::PhysicalPosition::new(-1.0, -1.0); + winit::dpi::PhysicalPosition::new(-1.0, -1.0); } WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = *new_modifiers; -- cgit From 5e4e410b18eb744cf70ae1f18b9ef08611f59150 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 3 Nov 2022 14:53:05 -0300 Subject: remove `windows` method (use commands instead) --- examples/multi_window/src/main.rs | 13 ++++--------- glutin/src/multi_window.rs | 2 +- glutin/src/multi_window/state.rs | 24 +----------------------- src/multi_window/application.rs | 13 ------------- winit/src/multi_window.rs | 5 +---- winit/src/multi_window/state.rs | 24 +----------------------- 6 files changed, 8 insertions(+), 73 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 0dda1804..2771d728 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -131,6 +131,7 @@ impl Application for Example { } WindowMessage::CloseWindow => { let _ = self.windows.remove(&id); + return window::close(id); } WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { let window = self.windows.get_mut(&id).unwrap(); @@ -173,8 +174,9 @@ impl Application for Example { title: format!("New window ({})", self.windows.len()), }; - self.windows - .insert(window::Id::new(self.windows.len()), window); + let window_id = window::Id::new(self.windows.len()); + self.windows.insert(window_id, window); + return window::spawn(window_id, Default::default()); } } WindowMessage::Dragged(pane_grid::DragEvent::Dropped { @@ -243,13 +245,6 @@ impl Application for Example { }) } - fn windows(&self) -> Vec<(window::Id, iced::window::Settings)> { - self.windows - .iter() - .map(|(&id, _window)| (id, iced::window::Settings::default())) - .collect() - } - fn close_requested(&self, window: window::Id) -> Self::Message { Message::Window(window, WindowMessage::CloseWindow) } diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 095e0e2c..2ac7f636 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -515,7 +515,7 @@ async fn run_instance( ); // Update window - state.synchronize(&application, &windows, &proxy); + state.synchronize(&application, &windows); let should_exit = application.should_exit(); diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs index 321fc4d1..28f4a895 100644 --- a/glutin/src/multi_window/state.rs +++ b/glutin/src/multi_window/state.rs @@ -1,12 +1,11 @@ use crate::application::{self, StyleSheet as _}; use crate::conversion; -use crate::multi_window::{Application, Event}; +use crate::multi_window::Application; use crate::window; use crate::{Color, Debug, Point, Size, Viewport}; use iced_winit::winit; use winit::event::{Touch, WindowEvent}; -use winit::event_loop::EventLoopProxy; use winit::window::Window; use std::collections::HashMap; @@ -189,28 +188,7 @@ where &mut self, application: &A, windows: &HashMap, - proxy: &EventLoopProxy>, ) { - let new_windows = application.windows(); - - // Check for windows to close - for window_id in windows.keys() { - if !new_windows.iter().any(|(id, _)| id == window_id) { - proxy - .send_event(Event::CloseWindow(*window_id)) - .expect("Failed to send message"); - } - } - - // Check for windows to spawn - for (id, settings) in new_windows { - if !windows.contains_key(&id) { - proxy - .send_event(Event::NewWindow(id, settings)) - .expect("Failed to send message"); - } - } - let window = windows.values().next().expect("No window found"); // Update window title diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index df45ca1e..7d559397 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -46,9 +46,6 @@ pub trait Application: Sized { /// title of your application when necessary. fn title(&self) -> String; - /// TODO(derezzedex) - fn windows(&self) -> Vec<(window::Id, window::Settings)>; - /// Handles a __message__ and updates the state of the [`Application`]. /// /// This is where you define your __update logic__. All the __messages__, @@ -170,16 +167,6 @@ where self.0.title() } - fn windows(&self) -> Vec<(window::Id, iced_winit::settings::Window)> { - self.0 - .windows() - .into_iter() - .map(|(id, settings)| { - (id, iced_winit::settings::Window::from(settings)) - }) - .collect() - } - fn update(&mut self, message: Self::Message) -> Command { self.0.update(message) } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 1d71d801..c0c233c5 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -68,9 +68,6 @@ where /// The type of __messages__ your [`Program`] will produce. type Message: std::fmt::Debug + Send; - /// TODO(derezzedex) - fn windows(&self) -> Vec<(window::Id, settings::Window)>; - /// Handles a __message__ and updates the state of the [`Program`]. /// /// This is where you define your __update logic__. All the __messages__, @@ -459,7 +456,7 @@ async fn run_instance( ); // Update window - state.synchronize(&application, &windows, &proxy); + state.synchronize(&application, &windows); let should_exit = application.should_exit(); diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index ae353e3b..a7d51df4 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -1,13 +1,12 @@ use crate::application::{self, StyleSheet as _}; use crate::conversion; -use crate::multi_window::{Application, Event}; +use crate::multi_window::Application; use crate::window; use crate::{Color, Debug, Point, Size, Viewport}; use std::collections::HashMap; use std::marker::PhantomData; use winit::event::{Touch, WindowEvent}; -use winit::event_loop::EventLoopProxy; use winit::window::Window; /// The state of a windowed [`Application`]. @@ -186,28 +185,7 @@ where &mut self, application: &A, windows: &HashMap, - proxy: &EventLoopProxy>, ) { - let new_windows = application.windows(); - - // Check for windows to close - for window_id in windows.keys() { - if !new_windows.iter().any(|(id, _)| id == window_id) { - proxy - .send_event(Event::CloseWindow(*window_id)) - .expect("Failed to send message"); - } - } - - // Check for windows to spawn - for (id, settings) in new_windows { - if !windows.contains_key(&id) { - proxy - .send_event(Event::NewWindow(id, settings)) - .expect("Failed to send message"); - } - } - let window = windows.values().next().expect("No window found"); // Update window title -- cgit From 942f1c91afb8257e289af8d0c229f74819f68361 Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Mon, 2 Jan 2023 10:58:07 -0800 Subject: merged in iced master --- examples/multi_window/src/main.rs | 2 +- glutin/src/multi_window.rs | 20 ++++++++++++++++++++ glutin/src/multi_window/state.rs | 4 ++-- winit/src/multi_window.rs | 20 ++++++++++++++++++++ winit/src/multi_window/state.rs | 4 ++-- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 2771d728..9b93eea6 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -268,7 +268,7 @@ impl Application for Example { .spacing(5) .align_items(Alignment::Center); - let pane_grid = PaneGrid::new(&window.panes, |id, pane| { + let pane_grid = PaneGrid::new(&window.panes, |id, pane, _| { let is_focused = focus == Some(id); let pin_button = button( diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 2ac7f636..746da159 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -887,6 +887,10 @@ pub fn run_command( .send_event(Event::CloseWindow(id)) .expect("Send message to event loop"); } + window::Action::Drag => { + let window = windows.get(&id).expect("No window found"); + let _res = window.drag_window(); + } window::Action::Resize { width, height } => { let window = windows.get(&id).expect("No window found"); window.set_inner_size(winit::dpi::LogicalSize { @@ -921,6 +925,22 @@ pub fn run_command( .send_event(Event::Application(tag(mode))) .expect("Send message to event loop"); } + window::Action::Maximize(value) => { + let window = windows.get(&id).expect("No window found!"); + window.set_maximized(value); + } + window::Action::Minimize(value) => { + let window = windows.get(&id).expect("No window found!"); + window.set_minimized(value); + } + window::Action::ToggleMaximize => { + let window = windows.get(&id).expect("No window found!"); + window.set_maximized(!window.is_maximized()); + } + window::Action::ToggleDecorations => { + let window = windows.get(&id).expect("No window found!"); + window.set_decorations(!window.is_decorated()); + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs index 28f4a895..e7e82876 100644 --- a/glutin/src/multi_window/state.rs +++ b/glutin/src/multi_window/state.rs @@ -37,7 +37,7 @@ where let title = application.title(); let scale_factor = application.scale_factor(); let theme = application.theme(); - let appearance = theme.appearance(application.style()); + let appearance = theme.appearance(&application.style()); let viewport = { let physical_size = window.inner_size(); @@ -216,6 +216,6 @@ where // Update theme and appearance self.theme = application.theme(); - self.appearance = self.theme.appearance(application.style()); + self.appearance = self.theme.appearance(&application.style()); } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index c0c233c5..0a2f71ad 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -863,6 +863,10 @@ pub fn run_command( .send_event(Event::CloseWindow(id)) .expect("Send message to event loop"); } + window::Action::Drag => { + let window = windows.get(&id).expect("No window found"); + let _res = window.drag_window(); + } window::Action::Resize { width, height } => { let window = windows.get(&id).expect("No window found"); window.set_inner_size(winit::dpi::LogicalSize { @@ -897,6 +901,22 @@ pub fn run_command( .send_event(Event::Application(tag(mode))) .expect("Send message to event loop"); } + window::Action::Maximize(value) => { + let window = windows.get(&id).expect("No window found!"); + window.set_maximized(value); + } + window::Action::Minimize(value) => { + let window = windows.get(&id).expect("No window found!"); + window.set_minimized(value); + } + window::Action::ToggleMaximize => { + let window = windows.get(&id).expect("No window found!"); + window.set_maximized(!window.is_maximized()); + } + window::Action::ToggleDecorations => { + let window = windows.get(&id).expect("No window found!"); + window.set_decorations(!window.is_decorated()); + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index a7d51df4..eebdcdf1 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -35,7 +35,7 @@ where let title = application.title(); let scale_factor = application.scale_factor(); let theme = application.theme(); - let appearance = theme.appearance(application.style()); + let appearance = theme.appearance(&application.style()); let viewport = { let physical_size = window.inner_size(); @@ -213,6 +213,6 @@ where // Update theme and appearance self.theme = application.theme(); - self.appearance = self.theme.appearance(application.style()); + self.appearance = self.theme.appearance(&application.style()); } } -- cgit From f43419d4752fe18065c0e1b7c2a26e65b9d6e253 Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Mon, 2 Jan 2023 18:14:31 -0800 Subject: Fixed issue with window ID on winit --- examples/multi_window/Cargo.toml | 1 + examples/multi_window/src/main.rs | 2 + winit/src/multi_window.rs | 110 ++++++++++++++++++++------------------ 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 9c3d0f21..6de895d7 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -8,5 +8,6 @@ publish = false [dependencies] iced = { path = "../..", features = ["debug", "multi_window"] } +env_logger = "0.10.0" iced_native = { path = "../../native" } iced_lazy = { path = "../../lazy" } \ No newline at end of file diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 9b93eea6..9fe6b481 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -15,6 +15,8 @@ use iced_native::{event, subscription, Event}; use std::collections::HashMap; pub fn main() -> iced::Result { + env_logger::init(); + Example::run(Settings::default()) } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 0a2f71ad..7d8bbc39 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -33,7 +33,6 @@ use std::mem::ManuallyDrop; pub enum Event { /// An [`Application`] generated message Application(Message), - /// TODO(derezzedex) // Create a wrapper variant of `window::Event` type instead // (maybe we should also allow users to listen/react to those internal messages?) @@ -197,7 +196,8 @@ where .map_err(Error::WindowCreationFailed)?; let windows: HashMap = - HashMap::from([(window::Id::new(0usize), window)]); + HashMap::from([(window::Id::MAIN, window)]); + let window = windows.values().next().expect("No window found"); #[cfg(target_arch = "wasm32")] @@ -515,64 +515,72 @@ async fn run_instance( ), )); } - event::Event::UserEvent(event) => match event { - Event::Application(message) => { - messages.push(message); - } - Event::WindowCreated(id, window) => { - let mut surface = compositor.create_surface(&window); + event::Event::UserEvent(event) => { + match event { + Event::Application(message) => { + messages.push(message); + } + Event::WindowCreated(id, window) => { + let mut surface = compositor.create_surface(&window); - let state = State::new(&application, &window); + let state = State::new(&application, &window); - let physical_size = state.physical_size(); + let physical_size = state.physical_size(); - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); - let user_interface = build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - id, - ); + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + id, + ); - let _ = states.insert(id, state); - let _ = surfaces.insert(id, surface); - let _ = interfaces.insert(id, user_interface); - let _ = window_ids.insert(window.id(), id); - let _ = windows.insert(id, window); - } - Event::CloseWindow(id) => { - // TODO(derezzedex): log errors - if let Some(window) = windows.get(&id) { - if window_ids.remove(&window.id()).is_none() { - println!("Failed to remove from `window_ids`!"); - } - } - if states.remove(&id).is_none() { - println!("Failed to remove from `states`!") - } - if interfaces.remove(&id).is_none() { - println!("Failed to remove from `interfaces`!"); - } - if windows.remove(&id).is_none() { - println!("Failed to remove from `windows`!") - } - if surfaces.remove(&id).is_none() { - println!("Failed to remove from `surfaces`!") + let _ = states.insert(id, state); + let _ = surfaces.insert(id, surface); + let _ = interfaces.insert(id, user_interface); + let _ = window_ids.insert(window.id(), id); + let _ = windows.insert(id, window); } + Event::CloseWindow(id) => { + println!("Closing window {:?}. Total: {}", id, windows.len()); - if windows.is_empty() { - break 'main; + if let Some(window) = windows.get(&id) { + if window_ids.remove(&window.id()).is_none() { + log::error!("Failed to remove window with id {:?} from window_ids.", window.id()); + } + } else { + log::error!("Could not find window with id {:?} in windows.", id); + } + if states.remove(&id).is_none() { + log::error!("Failed to remove window {:?} from states.", id); + } + if interfaces.remove(&id).is_none() { + log::error!("Failed to remove window {:?} from interfaces.", id); + } + if windows.remove(&id).is_none() { + log::error!("Failed to remove window {:?} from windows.", id); + } + if surfaces.remove(&id).is_none() { + log::error!("Failed to remove window {:?} from surfaces.", id); + } + + if windows.is_empty() { + log::info!("All windows are closed. Terminating program."); + break 'main; + } else { + log::info!("Remaining windows: {:?}", windows.len()); + } } + Event::NewWindow(_, _) => unreachable!(), } - Event::NewWindow(_, _) => unreachable!(), - }, + } event::Event::RedrawRequested(id) => { let state = window_ids .get(&id) -- cgit From 1944e98f82b7efd5b268e04ba5ced065e55a218e Mon Sep 17 00:00:00 2001 From: Bingus Date: Mon, 2 Jan 2023 21:06:59 -0800 Subject: Fix multi-window example for Glutin on MacOS --- glutin/Cargo.toml | 2 - glutin/src/multi_window.rs | 162 +++++++++++++++++++++++++-------------------- winit/src/multi_window.rs | 1 + 3 files changed, 90 insertions(+), 75 deletions(-) diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 70820780..3f902d20 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -24,8 +24,6 @@ version = "0.4" [dependencies.glutin] version = "0.30" -git = "https://github.com/derezzedex/glutin" -rev = "2a2a97209c49929027beced68e1989b8486bdec9" [dependencies.iced_native] version = "0.7" diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 746da159..35eeeb36 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -23,12 +23,11 @@ use glutin::config::{ }; use glutin::context::{ ContextApi, ContextAttributesBuilder, NotCurrentContext, - NotCurrentGlContextSurfaceAccessor, - PossiblyCurrentContextGlSurfaceAccessor, PossiblyCurrentGlContext, + NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext, }; use glutin::display::{Display, DisplayApiPreference, GlDisplay}; use glutin::surface::{ - GlSurface, Surface, SurfaceAttributesBuilder, WindowSurface, + GlSurface, Surface, SurfaceAttributesBuilder, SwapInterval, WindowSurface, }; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; @@ -240,42 +239,19 @@ where ) })?; - let (width, height) = window.inner_size().into(); - let surface_attributes = - SurfaceAttributesBuilder::::new() - .with_srgb(Some(true)) - .build( - window_handle, - NonZeroU32::new(width).unwrap_or(ONE), - NonZeroU32::new(height).unwrap_or(ONE), - ); - - let surface = display - .create_window_surface(configuration.as_ref(), &surface_attributes) - .map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create surface: {error}" - )), - ) - })?; - - let context = { - context - .make_current(&surface) - .expect("make context current") - }; - - if let Err(error) = surface.set_swap_interval( - &context, - glutin::surface::SwapInterval::Wait(ONE), - ) { - log::error!("set swap interval failed: {}", error); - } + let surface = gl_surface(&display, configuration.as_ref(), &window); (display, window, configuration.0, surface, context) }; + let windows: HashMap = + HashMap::from([(window::Id::MAIN, window)]); + + // need to make context current before trying to load GL functions + let context = context + .make_current(&surface) + .expect("Make context current."); + #[allow(unsafe_code)] let (compositor, renderer) = unsafe { C::new(compositor_settings, |address| { @@ -284,7 +260,7 @@ where })? }; - let context = { context.make_not_current().expect("make context current") }; + let context = context.make_not_current().expect("Make not current."); let (mut sender, receiver) = mpsc::unbounded(); @@ -297,9 +273,8 @@ where debug, receiver, display, - window, + windows, configuration, - surface, context, init_command, settings.exit_on_close_request, @@ -370,10 +345,9 @@ async fn run_instance( winit::event::Event<'_, Event>, >, display: Display, - window: winit::window::Window, + mut windows: HashMap, configuration: Config, - surface: Surface, - context: NotCurrentContext, + mut context: NotCurrentContext, init_command: Command, _exit_on_close_request: bool, ) where @@ -385,34 +359,48 @@ async fn run_instance( use iced_winit::futures::stream::StreamExt; use winit::event; - let context = { - context - .make_current(&surface) - .expect("make context current") - }; - - let mut clipboard = Clipboard::connect(&window); + let mut clipboard = + Clipboard::connect(windows.values().next().expect("No window found")); let mut cache = user_interface::Cache::default(); - let state = State::new(&application, &window); - let user_interface = multi_window::build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - window::Id::MAIN, - ); + let mut current_context_window = None; + let mut window_ids: HashMap<_, _> = windows + .iter() + .map(|(&id, window)| (window.id(), id)) + .collect(); + let mut states = HashMap::new(); + let mut surfaces = HashMap::new(); + let mut interfaces = ManuallyDrop::new(HashMap::new()); + + for (&id, window) in windows.keys().zip(windows.values()) { + let surface = gl_surface(&display, &configuration, &window); + let current_context = context.make_current(&surface).expect("Make current."); + let state = State::new(&application, &window); + let physical_size = state.physical_size(); + + surface.resize( + ¤t_context, + NonZeroU32::new(physical_size.width).unwrap_or(ONE), + NonZeroU32::new(physical_size.height).unwrap_or(ONE), + ); - let mut current_context_window = window.id(); - let mut window_ids = HashMap::from([(window.id(), window::Id::MAIN)]); - let mut windows = HashMap::from([(window::Id::MAIN, window)]); - let mut surfaces = HashMap::from([(window::Id::MAIN, surface)]); - let mut states = HashMap::from([(window::Id::MAIN, state)]); - let mut interfaces = - ManuallyDrop::new(HashMap::from([(window::Id::MAIN, user_interface)])); + let user_interface = multi_window::build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + id, + ); + + context = current_context.make_not_current().expect("Make not current."); + + let _ = states.insert(id, state); + let _ = surfaces.insert(id, surface); + let _ = interfaces.insert(id, user_interface); + } { - let state = states.get(&window::Id::MAIN).unwrap(); + let state = states.values().next().expect("No state found."); run_command( &application, @@ -653,12 +641,11 @@ async fn run_instance( debug.render_started(); - if current_context_window != id { - context - .make_current(&surface) - .expect("Make OpenGL context current"); + let current_context = + context.make_current(&surface).expect("Make current."); - current_context_window = id; + if current_context_window != Some(id) { + current_context_window = Some(id); } if state.viewport_changed() { @@ -695,11 +682,17 @@ async fn run_instance( } surface.resize( - &context, + ¤t_context, NonZeroU32::new(physical_size.width).unwrap_or(ONE), NonZeroU32::new(physical_size.height).unwrap_or(ONE), ); + if let Err(error) = + surface.set_swap_interval(¤t_context, SwapInterval::Wait(ONE)) + { + log::error!("Could not set swap interval for surface attached to window id: {:?}", id); + } + compositor.resize_viewport(physical_size); let _ = interfaces @@ -713,10 +706,10 @@ async fn run_instance( &debug.overlay(), ); - surface.swap_buffers(&context).expect("Swap buffers"); + surface.swap_buffers(¤t_context).expect("Swap buffers"); + context = current_context.make_not_current().expect("Make not current."); debug.render_finished(); - // TODO: Handle animations! // Maybe we can use `ControlFlow::WaitUntil` for this. } @@ -1038,3 +1031,26 @@ where interfaces } + +#[allow(unsafe_code)] +fn gl_surface( + display: &Display, + gl_config: &Config, + window: &winit::window::Window, +) -> Surface { + let (width, height) = window.inner_size().into(); + + let surface_attributes = SurfaceAttributesBuilder::::new() + .with_srgb(Some(true)) + .build( + window.raw_window_handle(), + NonZeroU32::new(width).unwrap_or(ONE), + NonZeroU32::new(height).unwrap_or(ONE), + ); + + unsafe { + display + .create_window_surface(gl_config, &surface_attributes) + .expect("failed to create surface") + } +} diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 7d8bbc39..43455148 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -320,6 +320,7 @@ async fn run_instance( for (&id, window) in windows.keys().zip(windows.values()) { let mut surface = compositor.create_surface(window); + println!("Creating surface for window: {:?}", window); let state = State::new(&application, window); -- cgit From ec41918ec40bddaba81235372f1566da59fd09f2 Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Thu, 5 Jan 2023 15:26:28 -0800 Subject: Implemented window title update functionality for multiwindow. --- examples/multi_window/Cargo.toml | 2 +- examples/multi_window/src/main.rs | 10 +- glutin/src/application.rs | 33 ++++--- glutin/src/multi_window.rs | 156 +++++++++++++------------------ glutin/src/multi_window/state.rs | 14 ++- native/src/window.rs | 4 +- native/src/window/action.rs | 8 +- native/src/window/icon.rs | 6 +- native/src/window/id.rs | 13 ++- src/multi_window/application.rs | 8 +- winit/src/application.rs | 2 +- winit/src/multi_window.rs | 190 ++++++++++++++++++++++---------------- winit/src/multi_window/state.rs | 14 ++- winit/src/window.rs | 37 ++++---- 14 files changed, 262 insertions(+), 235 deletions(-) diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 6de895d7..62198595 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -10,4 +10,4 @@ publish = false iced = { path = "../..", features = ["debug", "multi_window"] } env_logger = "0.10.0" iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } \ No newline at end of file +iced_lazy = { path = "../../lazy" } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 9fe6b481..b9f0514c 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -26,6 +26,7 @@ struct Example { _focused: window::Id, } +#[derive(Debug)] struct Window { title: String, panes: pane_grid::State, @@ -80,8 +81,11 @@ impl Application for Example { ) } - fn title(&self) -> String { - String::from("Multi windowed pane grid - Iced") + fn title(&self, window: window::Id) -> String { + self.windows + .get(&window) + .map(|w| w.title.clone()) + .unwrap_or(String::from("New Window")) } fn update(&mut self, message: Message) -> Command { @@ -262,7 +266,6 @@ impl Application for Example { &window.title, WindowMessage::TitleChanged, ), - button(text("Apply")).style(theme::Button::Primary), button(text("Close")) .on_press(WindowMessage::CloseWindow) .style(theme::Button::Destructive), @@ -389,6 +392,7 @@ impl std::fmt::Display for SelectableWindow { } } +#[derive(Debug)] struct Pane { id: usize, pub axis: pane_grid::Axis, diff --git a/glutin/src/application.rs b/glutin/src/application.rs index 45ff37f0..f43a47b9 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -245,18 +245,7 @@ where ) })?; - let (width, height) = window.inner_size().into(); - let surface_attributes = - SurfaceAttributesBuilder::::new() - .with_srgb(Some(true)) - .build( - window_handle, - NonZeroU32::new(width).unwrap_or(ONE), - NonZeroU32::new(height).unwrap_or(ONE), - ); - - let surface = display - .create_window_surface(configuration.as_ref(), &surface_attributes) + let surface = gl_surface(&display, configuration.as_ref(), &window) .map_err(|error| { Error::GraphicsCreationFailed( iced_graphics::Error::BackendError(format!( @@ -616,3 +605,23 @@ async fn run_instance( // Manually drop the user interface drop(ManuallyDrop::into_inner(user_interface)); } + +#[allow(unsafe_code)] +/// Creates a new [`glutin::Surface`]. +pub fn gl_surface( + display: &Display, + gl_config: &Config, + window: &winit::window::Window, +) -> Result, glutin::error::Error> { + let (width, height) = window.inner_size().into(); + + let surface_attributes = SurfaceAttributesBuilder::::new() + .with_srgb(Some(true)) + .build( + window.raw_window_handle(), + NonZeroU32::new(width).unwrap_or(ONE), + NonZeroU32::new(height).unwrap_or(ONE), + ); + + unsafe { display.create_window_surface(gl_config, &surface_attributes) } +} diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 35eeeb36..e79ec77d 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -12,7 +12,6 @@ use iced_winit::conversion; use iced_winit::futures; use iced_winit::futures::channel::mpsc; use iced_winit::renderer; -use iced_winit::settings; use iced_winit::user_interface; use iced_winit::window; use iced_winit::winit; @@ -26,11 +25,12 @@ use glutin::context::{ NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext, }; use glutin::display::{Display, DisplayApiPreference, GlDisplay}; -use glutin::surface::{ - GlSurface, Surface, SurfaceAttributesBuilder, SwapInterval, WindowSurface, -}; +use glutin::surface::{GlSurface, SwapInterval}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use crate::application::gl_surface; +use iced_native::window::Action; +use iced_winit::multi_window::Event; use std::collections::HashMap; use std::ffi::CString; use std::mem::ManuallyDrop; @@ -76,7 +76,7 @@ where }; let builder = settings.window.into_builder( - &application.title(), + &application.title(window::Id::MAIN), event_loop.primary_monitor(), settings.id, ); @@ -239,7 +239,14 @@ where ) })?; - let surface = gl_surface(&display, configuration.as_ref(), &window); + let surface = gl_surface(&display, configuration.as_ref(), &window) + .map_err(|error| { + Error::GraphicsCreationFailed( + iced_graphics::Error::BackendError(format!( + "failed to create surface: {error}" + )), + ) + })?; (display, window, configuration.0, surface, context) }; @@ -301,14 +308,13 @@ where event: winit::event::WindowEvent::Resized(*new_inner_size), window_id, }), - winit::event::Event::UserEvent(Event::NewWindow(id, settings)) => { - // TODO(derezzedex) + winit::event::Event::UserEvent(Event::NewWindow { + id, + settings, + title, + }) => { let window = settings - .into_builder( - "fix window title", - event_loop.primary_monitor(), - None, - ) + .into_builder(&title, event_loop.primary_monitor(), None) .build(event_loop) .expect("Failed to build window"); @@ -372,9 +378,11 @@ async fn run_instance( let mut interfaces = ManuallyDrop::new(HashMap::new()); for (&id, window) in windows.keys().zip(windows.values()) { - let surface = gl_surface(&display, &configuration, &window); - let current_context = context.make_current(&surface).expect("Make current."); - let state = State::new(&application, &window); + let surface = gl_surface(&display, &configuration, &window) + .expect("Create surface."); + let current_context = + context.make_current(&surface).expect("Make current."); + let state = State::new(&application, id, &window); let physical_size = state.physical_size(); surface.resize( @@ -392,7 +400,9 @@ async fn run_instance( id, ); - context = current_context.make_not_current().expect("Make not current."); + context = current_context + .make_not_current() + .expect("Make not current."); let _ = states.insert(id, state); let _ = surfaces.insert(id, surface); @@ -431,7 +441,7 @@ async fn run_instance( let (filtered, remaining): (Vec<_>, Vec<_>) = events.iter().cloned().partition( |(window_id, _event): &( - Option, + Option, iced_native::event::Event, )| { *window_id == Some(id) || *window_id == None @@ -503,7 +513,11 @@ async fn run_instance( ); // Update window - state.synchronize(&application, &windows); + state.synchronize( + &application, + id, + windows.get(&id).expect("No window found with ID."), + ); let should_exit = application.should_exit(); @@ -563,7 +577,7 @@ async fn run_instance( event::Event::UserEvent(event) => match event { Event::Application(message) => messages.push(message), Event::WindowCreated(id, window) => { - let state = State::new(&application, &window); + let state = State::new(&application, id, &window); let user_interface = multi_window::build_user_interface( &application, user_interface::Cache::default(), @@ -573,26 +587,8 @@ async fn run_instance( id, ); - let window_handle = window.raw_window_handle(); - let (width, height) = window.inner_size().into(); - let surface_attributes = - SurfaceAttributesBuilder::::new() - .with_srgb(Some(true)) - .build( - window_handle, - NonZeroU32::new(width).unwrap_or(ONE), - NonZeroU32::new(height).unwrap_or(ONE), - ); - - #[allow(unsafe_code)] - let surface = unsafe { - display - .create_window_surface( - &configuration, - &surface_attributes, - ) - .expect("failed to create surface") - }; + let surface = gl_surface(&display, &configuration, &window) + .expect("Create surface."); let _ = states.insert(id, state); let _ = interfaces.insert(id, user_interface); @@ -624,7 +620,7 @@ async fn run_instance( break 'main; } } - Event::NewWindow(_, _) => unreachable!(), + Event::NewWindow { .. } => unreachable!(), }, event::Event::RedrawRequested(id) => { let state = window_ids @@ -687,9 +683,10 @@ async fn run_instance( NonZeroU32::new(physical_size.height).unwrap_or(ONE), ); - if let Err(error) = - surface.set_swap_interval(¤t_context, SwapInterval::Wait(ONE)) - { + if let Err(_) = surface.set_swap_interval( + ¤t_context, + SwapInterval::Wait(ONE), + ) { log::error!("Could not set swap interval for surface attached to window id: {:?}", id); } @@ -706,9 +703,13 @@ async fn run_instance( &debug.overlay(), ); - surface.swap_buffers(¤t_context).expect("Swap buffers"); + surface + .swap_buffers(¤t_context) + .expect("Swap buffers"); - context = current_context.make_not_current().expect("Make not current."); + context = current_context + .make_not_current() + .expect("Make not current."); debug.render_finished(); // TODO: Handle animations! // Maybe we can use `ControlFlow::WaitUntil` for this. @@ -751,11 +752,10 @@ async fn run_instance( )); } } else { - // TODO(derezzedex): log error + log::error!("Window state not found for id: {:?}", window_id); } } else { - // TODO(derezzedex): log error - // println!("{:?}: {:?}", window_id, window_event); + log::error!("Window not found for id: {:?}", window_id); } } _ => {} @@ -766,25 +766,6 @@ async fn run_instance( // drop(ManuallyDrop::into_inner(user_interface)); } -/// TODO(derezzedex): -// This is the an wrapper around the `Application::Message` associate type -// to allows the `shell` to create internal messages, while still having -// the current user specified custom messages. -#[derive(Debug)] -pub enum Event { - /// An [`Application`] generated message - Application(Message), - - /// TODO(derezzedex) - // Create a wrapper variant of `window::Event` type instead - // (maybe we should also allow users to listen/react to those internal messages?) - NewWindow(window::Id, settings::Window), - /// TODO(derezzedex) - CloseWindow(window::Id), - /// TODO(derezzedex) - WindowCreated(window::Id, winit::window::Window), -} - /// Updates an [`Application`] by feeding it the provided messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update( @@ -872,7 +853,11 @@ pub fn run_command( command::Action::Window(id, action) => match action { window::Action::Spawn { settings } => { proxy - .send_event(Event::NewWindow(id, settings.into())) + .send_event(Event::NewWindow { + id, + settings: settings.into(), + title: application.title(id), + }) .expect("Send message to event loop"); } window::Action::Close => { @@ -934,6 +919,16 @@ pub fn run_command( let window = windows.get(&id).expect("No window found!"); window.set_decorations(!window.is_decorated()); } + Action::RequestUserAttention(attention_type) => { + let window = windows.get(&id).expect("No window found!"); + window.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + Action::GainFocus => { + let window = windows.get(&id).expect("No window found!"); + window.focus_window(); + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { @@ -1031,26 +1026,3 @@ where interfaces } - -#[allow(unsafe_code)] -fn gl_surface( - display: &Display, - gl_config: &Config, - window: &winit::window::Window, -) -> Surface { - let (width, height) = window.inner_size().into(); - - let surface_attributes = SurfaceAttributesBuilder::::new() - .with_srgb(Some(true)) - .build( - window.raw_window_handle(), - NonZeroU32::new(width).unwrap_or(ONE), - NonZeroU32::new(height).unwrap_or(ONE), - ); - - unsafe { - display - .create_window_surface(gl_config, &surface_attributes) - .expect("failed to create surface") - } -} diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs index e7e82876..04ec5083 100644 --- a/glutin/src/multi_window/state.rs +++ b/glutin/src/multi_window/state.rs @@ -8,7 +8,6 @@ use iced_winit::winit; use winit::event::{Touch, WindowEvent}; use winit::window::Window; -use std::collections::HashMap; use std::marker::PhantomData; /// The state of a windowed [`Application`]. @@ -33,8 +32,8 @@ where ::Theme: application::StyleSheet, { /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window: &Window) -> Self { - let title = application.title(); + pub fn new(application: &A, window_id: window::Id, window: &Window) -> Self { + let title = application.title(window_id); let scale_factor = application.scale_factor(); let theme = application.theme(); let appearance = theme.appearance(&application.style()); @@ -67,7 +66,7 @@ where &self.viewport } - /// TODO(derezzedex) + /// Returns whether or not the current [`Viewport`] has changed. pub fn viewport_changed(&self) -> bool { self.viewport_changed } @@ -187,12 +186,11 @@ where pub fn synchronize( &mut self, application: &A, - windows: &HashMap, + window_id: window::Id, + window: &Window, ) { - let window = windows.values().next().expect("No window found"); - // Update window title - let new_title = application.title(); + let new_title = application.title(window_id); if self.title != new_title { window.set_title(&new_title); diff --git a/native/src/window.rs b/native/src/window.rs index 1c03fcdf..96a5fe61 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -4,6 +4,8 @@ mod event; mod icon; mod id; mod mode; +mod position; +mod settings; mod user_attention; pub use action::Action; @@ -11,6 +13,6 @@ pub use event::Event; pub use icon::Icon; pub use id::Id; pub use mode::Mode; -pub use user_attention::UserAttention; pub use position::Position; pub use settings::Settings; +pub use user_attention::UserAttention; diff --git a/native/src/window/action.rs b/native/src/window/action.rs index 0587f25c..929663ec 100644 --- a/native/src/window/action.rs +++ b/native/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::window::{self, Mode, UserAttention}; +use crate::window; use iced_futures::MaybeSend; use std::fmt; @@ -13,9 +13,9 @@ pub enum Action { /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. Drag, - /// TODO(derezzedex) + /// Spawns a new window with the provided [`window::Settings`]. Spawn { - /// TODO(derezzedex) + /// The settings of the [`Window`]. settings: window::Settings, }, /// Resize the window. @@ -62,7 +62,7 @@ pub enum Action { /// - **macOS:** `None` has no effect. /// - **X11:** Requests for user attention must be manually cleared. /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. - RequestUserAttention(Option), + RequestUserAttention(Option), /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// diff --git a/native/src/window/icon.rs b/native/src/window/icon.rs index e89baf03..08a6acfd 100644 --- a/native/src/window/icon.rs +++ b/native/src/window/icon.rs @@ -3,10 +3,10 @@ /// The icon of a window. #[derive(Debug, Clone)] pub struct Icon { - /// TODO(derezzedex) + /// The __rgba__ color data of the window [`Icon`]. pub rgba: Vec, - /// TODO(derezzedex) + /// The width of the window [`Icon`]. pub width: u32, - /// TODO(derezzedex) + /// The height of the window [`Icon`]. pub height: u32, } diff --git a/native/src/window/id.rs b/native/src/window/id.rs index 5060e162..fa9761f5 100644 --- a/native/src/window/id.rs +++ b/native/src/window/id.rs @@ -1,15 +1,18 @@ use std::collections::hash_map::DefaultHasher; +use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -/// TODO(derezzedex) +/// The ID of the window. +/// +/// This is not necessarily the same as the window ID fetched from `winit::window::Window`. pub struct Id(u64); impl Id { /// TODO(derezzedex): maybe change `u64` to an enum `Type::{Single, Multi(u64)}` pub const MAIN: Self = Id(0); - /// TODO(derezzedex) + /// Creates a new unique window ID. pub fn new(id: impl Hash) -> Id { let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); @@ -17,3 +20,9 @@ impl Id { Id(hasher.finish()) } } + +impl Display for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Id({})", self.0) + } +} diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 7d559397..dc1ac5b0 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -44,7 +44,7 @@ pub trait Application: Sized { /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - fn title(&self) -> String; + fn title(&self, window: window::Id) -> String; /// Handles a __message__ and updates the state of the [`Application`]. /// @@ -110,7 +110,7 @@ pub trait Application: Sized { false } - /// TODO(derezzedex) + /// Requests that the [`window`] be closed. fn close_requested(&self, window: window::Id) -> Self::Message; /// Runs the [`Application`]. @@ -163,8 +163,8 @@ where (Instance(app), command) } - fn title(&self) -> String { - self.0.title() + fn title(&self, window: window::Id) -> String { + self.0.title(window) } fn update(&mut self, message: Self::Message) -> Command { diff --git a/winit/src/application.rs b/winit/src/application.rs index 910f3d94..eef6833c 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -675,7 +675,7 @@ pub fn run_command( window::Action::Drag => { let _res = window.drag_window(); } - window::Action::Spawn { .. } | window::Action::Close => { + window::Action::Spawn { .. } => { log::info!( "This is only available on `multi_window::Application`" ) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 43455148..6a2bdca9 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -22,6 +22,7 @@ use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; +use iced_native::window::Action; use std::collections::HashMap; use std::mem::ManuallyDrop; @@ -36,7 +37,14 @@ pub enum Event { /// TODO(derezzedex) // Create a wrapper variant of `window::Event` type instead // (maybe we should also allow users to listen/react to those internal messages?) - NewWindow(window::Id, settings::Window), + NewWindow { + /// The [window::Id] of the newly spawned [`Window`]. + id: window::Id, + /// The [settings::Window] of the newly spawned [`Window`]. + settings: settings::Window, + /// The title of the newly spawned [`Window`]. + title: String, + }, /// TODO(derezzedex) CloseWindow(window::Id), /// TODO(derezzedex) @@ -95,11 +103,11 @@ where /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Command); - /// Returns the current title of the [`Application`]. + /// Returns the current title of the current [`Application`] window. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - fn title(&self) -> String; + fn title(&self, window_id: window::Id) -> String; /// Returns the current [`Theme`] of the [`Application`]. fn theme(&self) -> ::Theme; @@ -144,7 +152,7 @@ where false } - /// TODO(derezzedex) + /// Requests that the [`window`] be closed. fn close_requested(&self, window: window::Id) -> Self::Message; } @@ -184,7 +192,7 @@ where }; let builder = settings.window.into_builder( - &application.title(), + &application.title(window::Id::MAIN), event_loop.primary_monitor(), settings.id, ); @@ -253,14 +261,13 @@ where event: winit::event::WindowEvent::Resized(*new_inner_size), window_id, }), - winit::event::Event::UserEvent(Event::NewWindow(id, settings)) => { - // TODO(derezzedex) + winit::event::Event::UserEvent(Event::NewWindow { + id, + settings, + title, + }) => { let window = settings - .into_builder( - "fix window title", - event_loop.primary_monitor(), - None, - ) + .into_builder(&title, event_loop.primary_monitor(), None) .build(event_loop) .expect("Failed to build window"); @@ -320,10 +327,7 @@ async fn run_instance( for (&id, window) in windows.keys().zip(windows.values()) { let mut surface = compositor.create_surface(window); - println!("Creating surface for window: {:?}", window); - - let state = State::new(&application, window); - + let state = State::new(&application, id, window); let physical_size = state.physical_size(); compositor.configure_surface( @@ -457,7 +461,11 @@ async fn run_instance( ); // Update window - state.synchronize(&application, &windows); + state.synchronize( + &application, + id, + windows.get(&id).expect("No window found with ID."), + ); let should_exit = application.should_exit(); @@ -516,72 +524,85 @@ async fn run_instance( ), )); } - event::Event::UserEvent(event) => { - match event { - Event::Application(message) => { - messages.push(message); - } - Event::WindowCreated(id, window) => { - let mut surface = compositor.create_surface(&window); - - let state = State::new(&application, &window); + event::Event::UserEvent(event) => match event { + Event::Application(message) => { + messages.push(message); + } + Event::WindowCreated(id, window) => { + let mut surface = compositor.create_surface(&window); - let physical_size = state.physical_size(); + let state = State::new(&application, id, &window); - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); + let physical_size = state.physical_size(); - let user_interface = build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - id, - ); + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); - let _ = states.insert(id, state); - let _ = surfaces.insert(id, surface); - let _ = interfaces.insert(id, user_interface); - let _ = window_ids.insert(window.id(), id); - let _ = windows.insert(id, window); - } - Event::CloseWindow(id) => { - println!("Closing window {:?}. Total: {}", id, windows.len()); + let user_interface = build_user_interface( + &application, + user_interface::Cache::default(), + &mut renderer, + state.logical_size(), + &mut debug, + id, + ); - if let Some(window) = windows.get(&id) { - if window_ids.remove(&window.id()).is_none() { - log::error!("Failed to remove window with id {:?} from window_ids.", window.id()); - } - } else { - log::error!("Could not find window with id {:?} in windows.", id); - } - if states.remove(&id).is_none() { - log::error!("Failed to remove window {:?} from states.", id); - } - if interfaces.remove(&id).is_none() { - log::error!("Failed to remove window {:?} from interfaces.", id); - } - if windows.remove(&id).is_none() { - log::error!("Failed to remove window {:?} from windows.", id); - } - if surfaces.remove(&id).is_none() { - log::error!("Failed to remove window {:?} from surfaces.", id); + let _ = states.insert(id, state); + let _ = surfaces.insert(id, surface); + let _ = interfaces.insert(id, user_interface); + let _ = window_ids.insert(window.id(), id); + let _ = windows.insert(id, window); + } + Event::CloseWindow(id) => { + if let Some(window) = windows.get(&id) { + if window_ids.remove(&window.id()).is_none() { + log::error!("Failed to remove window with id {:?} from window_ids.", window.id()); } + } else { + log::error!( + "Could not find window with id {:?} in windows.", + id + ); + } + if states.remove(&id).is_none() { + log::error!( + "Failed to remove window {:?} from states.", + id + ); + } + if interfaces.remove(&id).is_none() { + log::error!( + "Failed to remove window {:?} from interfaces.", + id + ); + } + if windows.remove(&id).is_none() { + log::error!( + "Failed to remove window {:?} from windows.", + id + ); + } + if surfaces.remove(&id).is_none() { + log::error!( + "Failed to remove window {:?} from surfaces.", + id + ); + } - if windows.is_empty() { - log::info!("All windows are closed. Terminating program."); - break 'main; - } else { - log::info!("Remaining windows: {:?}", windows.len()); - } + if windows.is_empty() { + log::info!( + "All windows are closed. Terminating program." + ); + break 'main; + } else { + log::info!("Remaining windows: {:?}", windows.len()); } - Event::NewWindow(_, _) => unreachable!(), } - } + Event::NewWindow { .. } => unreachable!(), + }, event::Event::RedrawRequested(id) => { let state = window_ids .get(&id) @@ -716,11 +737,10 @@ async fn run_instance( )); } } else { - // TODO(derezzedex): log error + log::error!("No window state found for id: {:?}", window_id); } } else { - // TODO(derezzedex): log error - // println!("{:?}: {:?}", window_id, window_event); + log::error!("No window found with id: {:?}", window_id); } } _ => {} @@ -864,7 +884,11 @@ pub fn run_command( command::Action::Window(id, action) => match action { window::Action::Spawn { settings } => { proxy - .send_event(Event::NewWindow(id, settings.into())) + .send_event(Event::NewWindow { + id, + settings: settings.into(), + title: application.title(id), + }) .expect("Send message to event loop"); } window::Action::Close => { @@ -926,6 +950,16 @@ pub fn run_command( let window = windows.get(&id).expect("No window found!"); window.set_decorations(!window.is_decorated()); } + window::Action::RequestUserAttention(attention_type) => { + let window = windows.get(&id).expect("No window found!"); + window.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + Action::GainFocus => { + let window = windows.get(&id).expect("No window found!"); + window.focus_window(); + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index eebdcdf1..7a598b98 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -4,7 +4,6 @@ use crate::multi_window::Application; use crate::window; use crate::{Color, Debug, Point, Size, Viewport}; -use std::collections::HashMap; use std::marker::PhantomData; use winit::event::{Touch, WindowEvent}; use winit::window::Window; @@ -31,8 +30,8 @@ where ::Theme: application::StyleSheet, { /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window: &Window) -> Self { - let title = application.title(); + pub fn new(application: &A, window_id: window::Id, window: &Window) -> Self { + let title = application.title(window_id); let scale_factor = application.scale_factor(); let theme = application.theme(); let appearance = theme.appearance(&application.style()); @@ -65,7 +64,7 @@ where &self.viewport } - /// TODO(derezzedex) + /// Returns whether or not the viewport changed. pub fn viewport_changed(&self) -> bool { self.viewport_changed } @@ -184,12 +183,11 @@ where pub fn synchronize( &mut self, application: &A, - windows: &HashMap, + window_id: window::Id, + window: &Window, ) { - let window = windows.values().next().expect("No window found"); - // Update window title - let new_title = application.title(); + let new_title = application.title(window_id); if self.title != new_title { window.set_title(&new_title); diff --git a/winit/src/window.rs b/winit/src/window.rs index fba863ef..5a8ff6df 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -2,19 +2,19 @@ use crate::command::{self, Command}; use iced_native::window; -pub use window::{Id, Event, Mode, UserAttention}; +pub use window::{Event, Id, Mode, UserAttention}; -/// Closes the current window and exits the application. -pub fn close() -> Command { - Command::single(command::Action::Window(window::Action::Close)) +/// Closes the window. +pub fn close(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::Close)) } /// Begins dragging the window while the left mouse button is held. -pub fn drag() -> Command { - Command::single(command::Action::Window(window::Action::Drag)) +pub fn drag(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::Drag)) } -/// TODO(derezzedex) +/// Spawns a new window. pub fn spawn( id: window::Id, settings: window::Settings, @@ -25,11 +25,6 @@ pub fn spawn( )) } -/// TODO(derezzedex) -pub fn close(id: window::Id) -> Command { - Command::single(command::Action::Window(id, window::Action::Close)) -} - /// Resizes the window to the given logical dimensions. pub fn resize( id: window::Id, @@ -43,13 +38,19 @@ pub fn resize( } /// Sets the window to maximized or back. -pub fn maximize(value: bool) -> Command { - Command::single(command::Action::Window(window::Action::Maximize(value))) +pub fn maximize(id: window::Id, value: bool) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Maximize(value), + )) } /// Set the window to minimized or back. -pub fn minimize(value: bool) -> Command { - Command::single(command::Action::Window(window::Action::Minimize(value))) +pub fn minimize(id: window::Id, value: bool) -> Command { + Command::single(command::Action::Window( + id, + window::Action::Minimize(value), + )) } /// Moves a window to the given logical coordinates. @@ -63,8 +64,8 @@ pub fn set_mode(id: window::Id, mode: Mode) -> Command { } /// Sets the window to maximized or back. -pub fn toggle_maximize() -> Command { - Command::single(command::Action::Window(window::Action::ToggleMaximize)) +pub fn toggle_maximize(id: window::Id) -> Command { + Command::single(command::Action::Window(id, window::Action::ToggleMaximize)) } /// Fetches the current [`Mode`] of the window. -- cgit From 3e5d34f25fa07fa99f57b686bbde87d73b8ed548 Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Mon, 9 Jan 2023 10:19:12 -0800 Subject: Formatting --- examples/multi_window/src/main.rs | 10 ++--- glutin/src/multi_window/state.rs | 8 +++- src/multi_window/application.rs | 79 +++++++++++++++++++++++++++++++-------- winit/src/multi_window/state.rs | 9 +++-- 4 files changed, 81 insertions(+), 25 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index b9f0514c..18536bdf 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -57,9 +57,9 @@ enum WindowMessage { } impl Application for Example { + type Executor = executor::Default; type Message = Message; type Theme = Theme; - type Executor = executor::Default; type Flags = (); fn new(_flags: ()) -> (Self, Command) { @@ -251,10 +251,6 @@ impl Application for Example { }) } - fn close_requested(&self, window: window::Id) -> Self::Message { - Message::Window(window, WindowMessage::CloseWindow) - } - fn view(&self, window_id: window::Id) -> Element { if let Some(window) = self.windows.get(&window_id) { let focus = window.focus; @@ -342,6 +338,10 @@ impl Application for Example { .center_y() .into() } + + fn close_requested(&self, window: window::Id) -> Self::Message { + Message::Window(window, WindowMessage::CloseWindow) + } } const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs index 04ec5083..8ed134b2 100644 --- a/glutin/src/multi_window/state.rs +++ b/glutin/src/multi_window/state.rs @@ -31,8 +31,12 @@ impl State where ::Theme: application::StyleSheet, { - /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window_id: window::Id, window: &Window) -> Self { + /// 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(); let theme = application.theme(); diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index dc1ac5b0..3f20382c 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -3,13 +3,62 @@ use crate::{Command, Element, Executor, Settings, Subscription}; pub use iced_native::application::{Appearance, StyleSheet}; -/// A pure version of [`Application`]. +/// An interactive cross-platform multi-window application. /// -/// Unlike the impure version, the `view` method of this trait takes an -/// immutable reference to `self` and returns a pure [`Element`]. +/// This trait is the main entrypoint of Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`](#method.run). /// -/// [`Application`]: crate::Application -/// [`Element`]: pure::Element +/// An [`Application`] can execute asynchronous actions by returning a +/// [`Command`] in some of its methods. For example, to spawn a new window, you +/// can use the `iced_winit::window::spawn()` [`Command`]. +/// +/// When using an [`Application`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +/// +/// ## 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; +/// use iced::multi_window::Application; +/// use iced::window; +/// use iced::{Command, Element, Settings, Theme}; +/// +/// pub fn main() -> iced::Result { +/// Hello::run(Settings::default()) +/// } +/// +/// struct Hello; +/// +/// impl Application for Hello { +/// type Executor = executor::Default; +/// type Message = (); +/// type Theme = Theme; +/// type Flags = (); +/// +/// fn new(_flags: ()) -> (Hello, Command) { +/// (Hello, Command::none()) +/// } +/// +/// fn title(&self, window: window::Id) -> String { +/// String::from("A cool application") +/// } +/// +/// fn update(&mut self, _message: Self::Message) -> Command { +/// Command::none() +/// } +/// +/// fn view(&self, window: window::Id) -> Element { +/// "Hello, world!".into() +/// } +/// +/// fn close_requested(&self, window: window::Id) -> Self::Message { +/// () +/// } +/// } +/// ``` pub trait Application: Sized { /// The [`Executor`] that will run commands and subscriptions. /// @@ -157,16 +206,6 @@ where type Renderer = crate::Renderer; type Message = A::Message; - fn new(flags: Self::Flags) -> (Self, Command) { - let (app, command) = A::new(flags); - - (Instance(app), command) - } - - fn title(&self, window: window::Id) -> String { - self.0.title(window) - } - fn update(&mut self, message: Self::Message) -> Command { self.0.update(message) } @@ -178,6 +217,16 @@ where self.0.view(window) } + fn new(flags: Self::Flags) -> (Self, Command) { + let (app, command) = A::new(flags); + + (Instance(app), command) + } + + fn title(&self, window: window::Id) -> String { + self.0.title(window) + } + fn theme(&self) -> A::Theme { self.0.theme() } diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 7a598b98..2c2a4693 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -29,8 +29,12 @@ impl State where ::Theme: application::StyleSheet, { - /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window_id: window::Id, window: &Window) -> Self { + /// 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(); let theme = application.theme(); @@ -191,7 +195,6 @@ where if self.title != new_title { window.set_title(&new_title); - self.title = new_title; } -- cgit From f78ccd9af9ced4c18ed4b56cbf838c6c5a5119ad Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Mon, 9 Jan 2023 11:48:34 -0800 Subject: Removed glutin's individual multi_window state since 0.30+ doesn't have its own event crate anymore --- glutin/src/multi_window.rs | 14 +-- glutin/src/multi_window/state.rs | 223 --------------------------------------- 2 files changed, 5 insertions(+), 232 deletions(-) delete mode 100644 glutin/src/multi_window/state.rs diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index e79ec77d..2b456543 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -1,8 +1,4 @@ //! Create interactive, native cross-platform applications. -mod state; - -pub use state::State; - use crate::mouse; use crate::{Error, Executor, Runtime}; @@ -382,7 +378,7 @@ async fn run_instance( .expect("Create surface."); let current_context = context.make_current(&surface).expect("Make current."); - let state = State::new(&application, id, &window); + let state = multi_window::State::new(&application, id, &window); let physical_size = state.physical_size(); surface.resize( @@ -577,7 +573,7 @@ async fn run_instance( event::Event::UserEvent(event) => match event { Event::Application(message) => messages.push(message), Event::WindowCreated(id, window) => { - let state = State::new(&application, id, &window); + let state = multi_window::State::new(&application, id, &window); let user_interface = multi_window::build_user_interface( &application, user_interface::Cache::default(), @@ -771,7 +767,7 @@ async fn run_instance( pub fn update( application: &mut A, cache: &mut user_interface::Cache, - state: &State, + state: &multi_window::State, renderer: &mut A::Renderer, runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, @@ -813,7 +809,7 @@ pub fn update( pub fn run_command( application: &A, cache: &mut user_interface::Cache, - state: &State, + state: &multi_window::State, renderer: &mut A::Renderer, command: Command, runtime: &mut Runtime>, Event>, @@ -993,7 +989,7 @@ pub fn build_user_interfaces<'a, A>( application: &'a A, renderer: &mut A::Renderer, debug: &mut Debug, - states: &HashMap>, + states: &HashMap>, mut pure_states: HashMap, ) -> HashMap< window::Id, diff --git a/glutin/src/multi_window/state.rs b/glutin/src/multi_window/state.rs deleted file mode 100644 index 8ed134b2..00000000 --- a/glutin/src/multi_window/state.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::application::{self, StyleSheet as _}; -use crate::conversion; -use crate::multi_window::Application; -use crate::window; -use crate::{Color, Debug, Point, Size, Viewport}; - -use iced_winit::winit; -use winit::event::{Touch, WindowEvent}; -use winit::window::Window; - -use std::marker::PhantomData; - -/// The state of a windowed [`Application`]. -#[allow(missing_debug_implementations)] -pub struct State -where - ::Theme: application::StyleSheet, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_changed: bool, - cursor_position: winit::dpi::PhysicalPosition, - modifiers: winit::event::ModifiersState, - theme: ::Theme, - appearance: iced_winit::application::Appearance, - application: PhantomData, -} - -impl State -where - ::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(); - let theme = application.theme(); - 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_changed: false, - // TODO: Encode cursor availability in the type-system - cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), - modifiers: winit::event::ModifiersState::default(), - theme, - appearance, - application: PhantomData, - } - } - - /// Returns the current [`Viewport`] of the [`State`]. - pub fn viewport(&self) -> &Viewport { - &self.viewport - } - - /// Returns whether or not the current [`Viewport`] has changed. - pub fn viewport_changed(&self) -> bool { - self.viewport_changed - } - - /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn physical_size(&self) -> Size { - self.viewport.physical_size() - } - - /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn logical_size(&self) -> Size { - 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_position(&self) -> Point { - conversion::cursor_position( - self.cursor_position, - self.viewport.scale_factor(), - ) - } - - /// 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) -> &::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 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_changed = true; - } - 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_changed = true; - } - WindowEvent::CursorMoved { position, .. } - | WindowEvent::Touch(Touch { - location: position, .. - }) => { - self.cursor_position = *position; - } - WindowEvent::CursorLeft { .. } => { - // TODO: Encode cursor availability in the type-system - self.cursor_position = - winit::dpi::PhysicalPosition::new(-1.0, -1.0); - } - WindowEvent::ModifiersChanged(new_modifiers) => { - self.modifiers = *new_modifiers; - } - #[cfg(feature = "debug")] - WindowEvent::KeyboardInput { - input: - glutin::event::KeyboardInput { - virtual_keycode: - Some(glutin::event::VirtualKeyCode::F12), - state: glutin::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 [`Application::update`]. - /// - /// [`Application::update`]: crate::Program::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 - let new_scale_factor = application.scale_factor(); - - if self.scale_factor != new_scale_factor { - let size = window.inner_size(); - - self.viewport = Viewport::with_physical_size( - Size::new(size.width, size.height), - window.scale_factor() * new_scale_factor, - ); - - self.scale_factor = new_scale_factor; - } - - // Update theme and appearance - self.theme = application.theme(); - self.appearance = self.theme.appearance(&application.style()); - } -} -- cgit From 790fa3e7a01a790aa3f07083fe9abf6b68fa7ba1 Mon Sep 17 00:00:00 2001 From: Bingus Date: Fri, 13 Jan 2023 11:56:28 -0800 Subject: Added tracing to multi_window applications --- glutin/Cargo.toml | 2 +- glutin/src/application.rs | 8 +-- glutin/src/multi_window.rs | 50 +++++++++++++------ winit/src/application.rs | 4 +- winit/src/application/profiler.rs | 101 ------------------------------------- winit/src/lib.rs | 4 +- winit/src/multi_window.rs | 61 ++++++++++++++++++----- winit/src/profiler.rs | 102 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 195 insertions(+), 137 deletions(-) delete mode 100644 winit/src/application/profiler.rs create mode 100644 winit/src/profiler.rs diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 3f902d20..5197b076 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -trace = ["iced_winit/trace"] +trace = ["iced_winit/trace", "tracing"] debug = ["iced_winit/debug"] system = ["iced_winit/system"] multi_window = ["iced_winit/multi_window"] diff --git a/glutin/src/application.rs b/glutin/src/application.rs index f43a47b9..a6479597 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -33,7 +33,7 @@ use std::ffi::CString; use std::mem::ManuallyDrop; use std::num::NonZeroU32; -#[cfg(feature = "tracing")] +#[cfg(feature = "trace")] use tracing::{info_span, instrument::Instrument}; #[allow(unsafe_code)] @@ -62,7 +62,7 @@ where let mut debug = Debug::new(); debug.startup_started(); - #[cfg(feature = "tracing")] + #[cfg(feature = "trace")] let _ = info_span!("Application::Glutin", "RUN").entered(); let mut event_loop = EventLoopBuilder::with_user_event().build(); @@ -298,7 +298,7 @@ where settings.exit_on_close_request, ); - #[cfg(feature = "tracing")] + #[cfg(feature = "trace")] let run_instance = run_instance.instrument(info_span!("Application", "LOOP")); @@ -509,7 +509,7 @@ async fn run_instance( messages.push(message); } event::Event::RedrawRequested(_) => { - #[cfg(feature = "tracing")] + #[cfg(feature = "trace")] let _ = info_span!("Application", "FRAME").entered(); debug.render_started(); diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 2b456543..a2e0581a 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -32,6 +32,9 @@ use std::ffi::CString; use std::mem::ManuallyDrop; use std::num::NonZeroU32; +#[cfg(feature = "tracing")] +use tracing::{info_span, instrument::Instrument}; + #[allow(unsafe_code)] const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; @@ -52,9 +55,15 @@ where use winit::event_loop::EventLoopBuilder; use winit::platform::run_return::EventLoopExtRunReturn; + #[cfg(feature = "trace")] + let _guard = iced_winit::Profiler::init(); + let mut debug = Debug::new(); debug.startup_started(); + #[cfg(feature = "tracing")] + let _ = info_span!("Application::Glutin", "RUN").entered(); + let mut event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -267,21 +276,29 @@ where let (mut sender, receiver) = mpsc::unbounded(); - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - receiver, - display, - windows, - configuration, - context, - init_command, - settings.exit_on_close_request, - )); + let mut instance = Box::pin({ + let run_instance = run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + receiver, + display, + windows, + configuration, + context, + init_command, + settings.exit_on_close_request, + ); + + #[cfg(feature = "tracing")] + let run_instance = + run_instance.instrument(info_span!("Application", "LOOP")); + + run_instance + }); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -619,6 +636,9 @@ async fn run_instance( Event::NewWindow { .. } => unreachable!(), }, event::Event::RedrawRequested(id) => { + #[cfg(feature = "tracing")] + let _ = info_span!("Application", "FRAME").entered(); + let state = window_ids .get(&id) .and_then(|id| states.get_mut(id)) diff --git a/winit/src/application.rs b/winit/src/application.rs index eef6833c..76553988 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -1,6 +1,4 @@ //! Create interactive, native cross-platform applications. -#[cfg(feature = "trace")] -mod profiler; mod state; pub use state::State; @@ -27,7 +25,7 @@ pub use iced_native::application::{Appearance, StyleSheet}; use std::mem::ManuallyDrop; #[cfg(feature = "trace")] -pub use profiler::Profiler; +pub use crate::Profiler; #[cfg(feature = "trace")] use tracing::{info_span, instrument::Instrument}; diff --git a/winit/src/application/profiler.rs b/winit/src/application/profiler.rs deleted file mode 100644 index 23eaa390..00000000 --- a/winit/src/application/profiler.rs +++ /dev/null @@ -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!("{}_trace_{}.json", curr_exe_name, time)); - - 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::>() - { - 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, - } - } -} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index eb58482b..99a46850 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -51,11 +51,13 @@ pub mod system; mod error; mod icon; mod proxy; +#[cfg(feature = "trace")] +mod profiler; #[cfg(feature = "application")] pub use application::Application; #[cfg(feature = "trace")] -pub use application::Profiler; +pub use profiler::Profiler; pub use clipboard::Clipboard; pub use error::Error; pub use icon::Icon; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 6a2bdca9..d7378a1d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -26,6 +26,11 @@ use iced_native::window::Action; use std::collections::HashMap; use std::mem::ManuallyDrop; +#[cfg(feature = "trace")] +pub use crate::Profiler; +#[cfg(feature = "trace")] +use tracing::{info_span, instrument::Instrument}; + /// TODO(derezzedex) // This is the an wrapper around the `Application::Message` associate type // to allows the `shell` to create internal messages, while still having @@ -172,9 +177,15 @@ where use futures::Future; use winit::event_loop::EventLoopBuilder; + #[cfg(feature = "trace")] + let _guard = Profiler::init(); + let mut debug = Debug::new(); debug.startup_started(); + #[cfg(feature = "trace")] + let _ = info_span!("Application", "RUN").entered(); + let event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -227,18 +238,26 @@ where let (mut sender, receiver) = mpsc::unbounded(); - let mut instance = Box::pin(run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - receiver, - init_command, - windows, - settings.exit_on_close_request, - )); + let mut instance = Box::pin({ + let run_instance = run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + receiver, + init_command, + windows, + 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()); @@ -604,6 +623,9 @@ async fn run_instance( Event::NewWindow { .. } => unreachable!(), }, event::Event::RedrawRequested(id) => { + #[cfg(feature = "trace")] + let _ = info_span!("Application", "FRAME").entered(); + let state = window_ids .get(&id) .and_then(|id| states.get_mut(id)) @@ -788,12 +810,22 @@ pub fn build_user_interface<'a, A: Application>( where ::Theme: StyleSheet, { + #[cfg(feature = "trace")] + let view_span = info_span!("Application", "VIEW").entered(); + debug.view_started(); let view = application.view(id); + + #[cfg(feature = "trace")] + let _ = view_span.exit(); debug.view_finished(); + #[cfg(feature = "trace")] + let layout_span = info_span!("Application", "LAYOUT").entered(); debug.layout_started(); let user_interface = UserInterface::build(view, size, cache, renderer); + #[cfg(feature = "trace")] + let _ = layout_span.exit(); debug.layout_finished(); user_interface @@ -817,10 +849,15 @@ pub fn update( ::Theme: StyleSheet, { for message in messages.drain(..) { + #[cfg(feature = "trace")] + let update_span = info_span!("Application", "UPDATE").entered(); + debug.log_message(&message); debug.update_started(); let command = runtime.enter(|| application.update(message)); + #[cfg(feature = "trace")] + let _ = update_span.exit(); debug.update_finished(); run_command( diff --git a/winit/src/profiler.rs b/winit/src/profiler.rs new file mode 100644 index 00000000..1f638de8 --- /dev/null +++ b/winit/src/profiler.rs @@ -0,0 +1,102 @@ +//! 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 { + log::info!("Capturing trace.."); + // 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!("{}_trace_{}.json", curr_exe_name, time)); + + 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::>() + { + 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, + } + } +} -- cgit From 7e9a12a4aa64deda193dfc0f18c34f93e3adc852 Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 18 Jan 2023 15:17:20 -0800 Subject: New iced changes --- native/src/subscription.rs | 2 +- native/src/widget/text_input.rs | 2 +- native/src/window.rs | 6 +++--- winit/src/application.rs | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 8c92efad..f517fc70 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -70,7 +70,7 @@ where events.filter_map(move |(event, status)| { future::ready(match event { - Event::Window(window::Event::RedrawRequested(_)) => None, + Event::Window(_, window::Event::RedrawRequested(_)) => None, _ => f(event, status), }) }) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 8755b85d..a62d9f35 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -782,7 +782,7 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(window::Event::RedrawRequested(now)) => { + Event::Window(_, window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = &mut state.is_focused { diff --git a/native/src/window.rs b/native/src/window.rs index d3c8c96f..660cd54f 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -5,8 +5,8 @@ mod icon; mod id; mod mode; mod position; -mod settings; mod redraw_request; +mod settings; mod user_attention; pub use action::Action; @@ -15,8 +15,8 @@ pub use icon::Icon; pub use id::Id; pub use mode::Mode; pub use position::Position; -pub use settings::Settings; pub use redraw_request::RedrawRequest; +pub use settings::Settings; pub use user_attention::UserAttention; use crate::subscription::{self, Subscription}; @@ -32,7 +32,7 @@ use crate::time::Instant; /// animations without missing any frames. pub fn frames() -> Subscription { subscription::raw_events(|event, _status| match event { - crate::Event::Window(Event::RedrawRequested(at)) => Some(at), + crate::Event::Window(_, Event::RedrawRequested(at)) => Some(at), _ => None, }) } diff --git a/winit/src/application.rs b/winit/src/application.rs index c66e08b2..d586fd21 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -402,6 +402,7 @@ async fn run_instance( // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = Event::Window( + crate::window::Id::MAIN, crate::window::Event::RedrawRequested(Instant::now()), ); -- cgit From 0a643287deece9234b64cc843a9f6ae3e6e4806e Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 18 Jan 2023 17:04:11 -0800 Subject: Added window::Id to multi_window application's scale_factor --- examples/multi_window/src/main.rs | 8 ++++++++ src/multi_window/application.rs | 6 +++--- winit/src/multi_window.rs | 2 +- winit/src/multi_window/state.rs | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 18536bdf..0d0a809b 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -12,6 +12,7 @@ use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; use iced_lazy::responsive; use iced_native::{event, subscription, Event}; +use iced_native::window::Id; use std::collections::HashMap; pub fn main() -> iced::Result { @@ -29,6 +30,7 @@ struct Example { #[derive(Debug)] struct Window { title: String, + scale: f64, panes: pane_grid::State, focus: Option, } @@ -69,6 +71,7 @@ impl Application for Example { panes, focus: None, title: String::from("Default window"), + scale: 1.0, }; ( @@ -178,6 +181,7 @@ impl Application for Example { panes, focus: None, title: format!("New window ({})", self.windows.len()), + scale: 1.0 + (self.windows.len() as f64 / 10.0), }; let window_id = window::Id::new(self.windows.len()); @@ -342,6 +346,10 @@ impl Application for Example { fn close_requested(&self, window: window::Id) -> Self::Message { Message::Window(window, WindowMessage::CloseWindow) } + + fn scale_factor(&self, window: Id) -> f64 { + self.windows.get(&window).map(|w| w.scale).unwrap_or(1.0) + } } const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 3f20382c..3af1d8d5 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -148,7 +148,7 @@ pub trait Application: Sized { /// while a scale factor of `0.5` will shrink them to half their size. /// /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { + fn scale_factor(&self, window: window::Id) -> f64 { 1.0 } @@ -239,8 +239,8 @@ where self.0.subscription() } - fn scale_factor(&self) -> f64 { - self.0.scale_factor() + fn scale_factor(&self, window: window::Id) -> f64 { + self.0.scale_factor(window) } fn should_exit(&self) -> bool { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index d7378a1d..ad65e6a5 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -146,7 +146,7 @@ where /// while a scale factor of `0.5` will shrink them to half their size. /// /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { + fn scale_factor(&self, window: window::Id) -> f64 { 1.0 } diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2c2a4693..35c69924 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -36,7 +36,7 @@ where window: &Window, ) -> Self { let title = application.title(window_id); - let scale_factor = application.scale_factor(); + let scale_factor = application.scale_factor(window_id); let theme = application.theme(); let appearance = theme.appearance(&application.style()); @@ -199,7 +199,7 @@ where } // Update scale factor - let new_scale_factor = application.scale_factor(); + let new_scale_factor = application.scale_factor(window_id); if self.scale_factor != new_scale_factor { let size = window.inner_size(); -- cgit From 367fea5dc8e94584334e880970126b40a046bfa6 Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 15 Feb 2023 11:28:36 -0800 Subject: Redraw request events for multiwindow. --- examples/solar_system/src/main.rs | 2 +- native/src/window.rs | 13 +++++++++++-- winit/src/multi_window.rs | 11 ++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9a4ee754..eb461bb0 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -89,7 +89,7 @@ impl Application for SolarSystem { } fn subscription(&self) -> Subscription { - window::frames().map(Message::Tick) + window::frames().map(|frame| Message::Tick(frame.at)) } } diff --git a/native/src/window.rs b/native/src/window.rs index 660cd54f..aa11756f 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -30,9 +30,18 @@ use crate::time::Instant; /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. -pub fn frames() -> Subscription { +pub fn frames() -> Subscription { subscription::raw_events(|event, _status| match event { - crate::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + crate::Event::Window(id, Event::RedrawRequested(at)) => { + Some(Frame { id, at }) + } _ => None, }) } + +/// The returned `Frame` for a framerate subscription. +#[derive(Debug)] +pub struct Frame { + pub id: Id, + pub at: Instant, +} diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index ad65e6a5..430e6706 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -25,6 +25,7 @@ pub use iced_native::application::{Appearance, StyleSheet}; use iced_native::window::Action; use std::collections::HashMap; use std::mem::ManuallyDrop; +use std::time::Instant; #[cfg(feature = "trace")] pub use crate::Profiler; @@ -402,7 +403,7 @@ async fn run_instance( let (filtered, remaining): (Vec<_>, Vec<_>) = events.iter().cloned().partition( |(window_id, _event): &( - Option, + Option, iced_native::event::Event, )| { *window_id == Some(id) || *window_id == None @@ -410,10 +411,14 @@ async fn run_instance( ); events.retain(|el| remaining.contains(el)); - let filtered: Vec<_> = filtered + let mut filtered: Vec<_> = filtered .into_iter() .map(|(_id, event)| event) .collect(); + filtered.push(iced_native::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + )); let cursor_position = states.get(&id).unwrap().cursor_position(); @@ -450,7 +455,7 @@ async fn run_instance( user_interface::State::Outdated, ) { - let state = &mut states.get_mut(&id).unwrap(); + let state = states.get_mut(&id).unwrap(); let pure_states: HashMap<_, _> = ManuallyDrop::into_inner(interfaces) .drain() -- cgit From 64e0e817c27d720dc954ee94de58ded35b3f9f9a Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 15 Feb 2023 14:31:16 -0800 Subject: Widget operations for multi-window. --- examples/multi_window/src/main.rs | 50 ++++++++++++++-- glutin/src/multi_window.rs | 116 +++++++++++++++++++------------------ native/src/window.rs | 3 + winit/src/multi_window.rs | 117 ++++++++++++++++++++------------------ 4 files changed, 171 insertions(+), 115 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 0d0a809b..23f08217 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -12,6 +12,7 @@ use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; use iced_lazy::responsive; use iced_native::{event, subscription, Event}; +use iced_native::widget::scrollable::{Properties, RelativeOffset}; use iced_native::window::Id; use std::collections::HashMap; @@ -56,6 +57,7 @@ enum WindowMessage { CloseFocused, SelectedWindow(pane_grid::Pane, SelectableWindow), CloseWindow, + SnapToggle, } impl Application for Example { @@ -94,6 +96,25 @@ impl Application for Example { fn update(&mut self, message: Message) -> Command { let Message::Window(id, message) = message; match message { + WindowMessage::SnapToggle => { + let window = self.windows.get_mut(&id).unwrap(); + + if let Some(focused) = &window.focus { + let pane = window.panes.get_mut(focused).unwrap(); + + let cmd = scrollable::snap_to( + pane.scrollable_id.clone(), + if pane.snapped { + RelativeOffset::START + } else { + RelativeOffset::END + }, + ); + + pane.snapped = !pane.snapped; + return cmd; + } + } WindowMessage::Split(axis, pane) => { let window = self.windows.get_mut(&id).unwrap(); let result = window.panes.split( @@ -311,7 +332,13 @@ impl Application for Example { }); pane_grid::Content::new(responsive(move |size| { - view_content(id, total_panes, pane.is_pinned, size) + view_content( + id, + pane.scrollable_id.clone(), + total_panes, + pane.is_pinned, + size, + ) })) .title_bar(title_bar) .style(if is_focused { @@ -403,24 +430,29 @@ impl std::fmt::Display for SelectableWindow { #[derive(Debug)] struct Pane { id: usize, + pub scrollable_id: scrollable::Id, pub axis: pane_grid::Axis, pub is_pinned: bool, pub is_moving: bool, + pub snapped: bool, } impl Pane { fn new(id: usize, axis: pane_grid::Axis) -> Self { Self { id, + scrollable_id: scrollable::Id::new(format!("{:?}", id)), axis, is_pinned: false, is_moving: false, + snapped: false, } } } fn view_content<'a>( pane: pane_grid::Pane, + scrollable_id: scrollable::Id, total_panes: usize, is_pinned: bool, size: Size, @@ -445,7 +477,8 @@ fn view_content<'a>( button( "Split vertically", WindowMessage::Split(pane_grid::Axis::Vertical, pane), - ) + ), + button("Snap", WindowMessage::SnapToggle,) ] .spacing(5) .max_width(150); @@ -462,15 +495,22 @@ fn view_content<'a>( controls, ] .width(Length::Fill) + .height(Length::Units(800)) .spacing(10) .align_items(Alignment::Center); - container(scrollable(content)) + Element::from( + container( + scrollable(content) + .vertical_scroll(Properties::new()) + .id(scrollable_id), + ) .width(Length::Fill) .height(Length::Fill) .padding(5) - .center_y() - .into() + .center_y(), + ) + .explain(Color::default()) } fn view_controls<'a>( diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index a2e0581a..33fe60ff 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -34,6 +34,7 @@ use std::num::NonZeroU32; #[cfg(feature = "tracing")] use tracing::{info_span, instrument::Instrument}; +use iced_native::widget::operation; #[allow(unsafe_code)] const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; @@ -294,7 +295,7 @@ where ); #[cfg(feature = "tracing")] - let run_instance = + let run_instance = run_instance.instrument(info_span!("Application", "LOOP")); run_instance @@ -380,7 +381,7 @@ async fn run_instance( let mut clipboard = Clipboard::connect(windows.values().next().expect("No window found")); - let mut cache = user_interface::Cache::default(); + let mut caches = HashMap::new(); let mut current_context_window = None; let mut window_ids: HashMap<_, _> = windows .iter() @@ -422,23 +423,20 @@ async fn run_instance( let _ = interfaces.insert(id, user_interface); } - { - let state = states.values().next().expect("No state found."); + run_command( + &application, + &mut caches, + &states, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &windows, + || compositor.fetch_information(), + ); - run_command( - &application, - &mut cache, - state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &windows, - || compositor.fetch_information(), - ); - } runtime.track(application.subscription().map(Event::Application)); let mut mouse_interaction = mouse::Interaction::default(); @@ -501,8 +499,7 @@ async fn run_instance( user_interface::State::Outdated ) { - let state = &mut states.get_mut(&id).unwrap(); - let pure_states: HashMap<_, _> = + let user_interfaces: HashMap<_, _> = ManuallyDrop::into_inner(interfaces) .drain() .map(|(id, interface)| { @@ -513,8 +510,8 @@ async fn run_instance( // Update application update( &mut application, - &mut cache, - state, + &mut caches, + &states, &mut renderer, &mut runtime, &mut clipboard, @@ -526,7 +523,7 @@ async fn run_instance( ); // Update window - state.synchronize( + states.get_mut(&id).unwrap().synchronize( &application, id, windows.get(&id).expect("No window found with ID."), @@ -539,7 +536,7 @@ async fn run_instance( &mut renderer, &mut debug, &states, - pure_states, + user_interfaces, )); if should_exit { @@ -590,7 +587,8 @@ async fn run_instance( event::Event::UserEvent(event) => match event { Event::Application(message) => messages.push(message), Event::WindowCreated(id, window) => { - let state = multi_window::State::new(&application, id, &window); + let state = + multi_window::State::new(&application, id, &window); let user_interface = multi_window::build_user_interface( &application, user_interface::Cache::default(), @@ -768,7 +766,10 @@ async fn run_instance( )); } } else { - log::error!("Window state not found for id: {:?}", window_id); + log::error!( + "Window state not found for id: {:?}", + window_id + ); } } else { log::error!("Window not found for id: {:?}", window_id); @@ -786,8 +787,8 @@ async fn run_instance( /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update( application: &mut A, - cache: &mut user_interface::Cache, - state: &multi_window::State, + caches: &mut HashMap, + states: &HashMap>, renderer: &mut A::Renderer, runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, @@ -797,6 +798,7 @@ pub fn update( windows: &HashMap, graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, ) where + A: Application + 'static, ::Theme: StyleSheet, { for message in messages.drain(..) { @@ -808,8 +810,8 @@ pub fn update( run_command( application, - cache, - state, + caches, + &states, renderer, command, runtime, @@ -828,8 +830,8 @@ pub fn update( /// Runs the actions of a [`Command`]. pub fn run_command( application: &A, - cache: &mut user_interface::Cache, - state: &multi_window::State, + caches: &mut HashMap, + states: &HashMap>, renderer: &mut A::Renderer, command: Command, runtime: &mut Runtime>, Event>, @@ -839,7 +841,7 @@ pub fn run_command( windows: &HashMap, _graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, ) where - A: Application, + A: Application + 'static, E: Executor, ::Theme: StyleSheet, { @@ -967,38 +969,42 @@ pub fn run_command( } }, command::Action::Widget(action) => { - use crate::widget::operation; - - let mut current_cache = std::mem::take(cache); + let mut current_caches = std::mem::take(caches); let mut current_operation = Some(action.into_operation()); - let mut user_interface = multi_window::build_user_interface( + let mut user_interfaces = multi_window::build_user_interfaces( application, - current_cache, renderer, - state.logical_size(), debug, - window::Id::MAIN, // TODO(derezzedex): run the operation on every widget tree + states, + current_caches, ); while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy - .send_event(Event::Application(message)) - .expect("Send message to event loop"); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); + for user_interface in user_interfaces.values_mut() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(Event::Application(message)) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } } } } - current_cache = user_interface.into_cache(); - *cache = current_cache; + let user_interfaces: HashMap<_, _> = user_interfaces + .drain() + .map(|(id, interface)| (id, interface.into_cache())) + .collect(); + + current_caches = user_interfaces; + *caches = current_caches; } } } @@ -1010,7 +1016,7 @@ pub fn build_user_interfaces<'a, A>( renderer: &mut A::Renderer, debug: &mut Debug, states: &HashMap>, - mut pure_states: HashMap, + mut user_interfaces: HashMap, ) -> HashMap< window::Id, iced_winit::UserInterface< @@ -1025,7 +1031,7 @@ where { let mut interfaces = HashMap::new(); - for (id, pure_state) in pure_states.drain() { + for (id, pure_state) in user_interfaces.drain() { let state = &states.get(&id).unwrap(); let user_interface = multi_window::build_user_interface( diff --git a/native/src/window.rs b/native/src/window.rs index aa11756f..e768ed6d 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -21,6 +21,7 @@ pub use user_attention::UserAttention; use crate::subscription::{self, Subscription}; use crate::time::Instant; +use crate::window; /// Subscribes to the frames of the window of the running application. /// @@ -42,6 +43,8 @@ pub fn frames() -> Subscription { /// The returned `Frame` for a framerate subscription. #[derive(Debug)] pub struct Frame { + /// The `window::Id` that the `Frame` was produced in. pub id: Id, + /// The `Instant` at which the frame was produced. pub at: Instant, } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 430e6706..bd7e9d44 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -4,12 +4,12 @@ mod state; pub use state::State; use crate::clipboard::{self, Clipboard}; -use crate::conversion; use crate::mouse; use crate::renderer; use crate::settings; use crate::widget::operation; use crate::window; +use crate::{conversion, multi_window}; use crate::{ Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, Settings, Size, Subscription, @@ -254,7 +254,7 @@ where ); #[cfg(feature = "trace")] - let run_instance = + let run_instance = run_instance.instrument(info_span!("Application", "LOOP")); run_instance @@ -335,7 +335,7 @@ async fn run_instance( let mut clipboard = Clipboard::connect(windows.values().next().expect("No window found")); - let mut cache = user_interface::Cache::default(); + let mut caches = HashMap::new(); let mut window_ids: HashMap<_, _> = windows .iter() .map(|(&id, window)| (window.id(), id)) @@ -368,26 +368,23 @@ async fn run_instance( let _ = states.insert(id, state); let _ = surfaces.insert(id, surface); let _ = interfaces.insert(id, user_interface); + let _ = caches.insert(id, user_interface::Cache::default()); } - { - // TODO(derezzedex) - let state = states.values().next().expect("No state found"); + run_command( + &application, + &mut caches, + &states, + &mut renderer, + init_command, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &windows, + || compositor.fetch_information(), + ); - run_command( - &application, - &mut cache, - state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &windows, - || compositor.fetch_information(), - ); - } runtime.track(application.subscription().map(Event::Application)); let mut mouse_interaction = mouse::Interaction::default(); @@ -455,8 +452,7 @@ async fn run_instance( user_interface::State::Outdated, ) { - let state = states.get_mut(&id).unwrap(); - let pure_states: HashMap<_, _> = + let user_interfaces: HashMap<_, _> = ManuallyDrop::into_inner(interfaces) .drain() .map( @@ -472,8 +468,8 @@ async fn run_instance( // Update application update( &mut application, - &mut cache, - state, + &mut caches, + &states, &mut renderer, &mut runtime, &mut clipboard, @@ -485,7 +481,7 @@ async fn run_instance( ); // Update window - state.synchronize( + states.get_mut(&id).unwrap().synchronize( &application, id, windows.get(&id).expect("No window found with ID."), @@ -498,7 +494,7 @@ async fn run_instance( &mut renderer, &mut debug, &states, - pure_states, + user_interfaces, )); if should_exit { @@ -579,6 +575,7 @@ async fn run_instance( let _ = interfaces.insert(id, user_interface); let _ = window_ids.insert(window.id(), id); let _ = windows.insert(id, window); + let _ = caches.insert(id, user_interface::Cache::default()); } Event::CloseWindow(id) => { if let Some(window) = windows.get(&id) { @@ -764,7 +761,10 @@ async fn run_instance( )); } } else { - log::error!("No window state found for id: {:?}", window_id); + log::error!( + "No window state found for id: {:?}", + window_id + ); } } else { log::error!("No window found with id: {:?}", window_id); @@ -840,8 +840,8 @@ where /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update( application: &mut A, - cache: &mut user_interface::Cache, - state: &State, + caches: &mut HashMap, + states: &HashMap>, renderer: &mut A::Renderer, runtime: &mut Runtime>, Event>, clipboard: &mut Clipboard, @@ -851,6 +851,7 @@ pub fn update( windows: &HashMap, graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where + A: Application + 'static, ::Theme: StyleSheet, { for message in messages.drain(..) { @@ -867,8 +868,8 @@ pub fn update( run_command( application, - cache, - state, + caches, + states, renderer, command, runtime, @@ -887,8 +888,8 @@ pub fn update( /// Runs the actions of a [`Command`]. pub fn run_command( application: &A, - cache: &mut user_interface::Cache, - state: &State, + caches: &mut HashMap, + states: &HashMap>, renderer: &mut A::Renderer, command: Command, runtime: &mut Runtime>, Event>, @@ -898,7 +899,7 @@ pub fn run_command( windows: &HashMap, _graphics_info: impl FnOnce() -> compositor::Information + Copy, ) where - A: Application, + A: Application + 'static, E: Executor, ::Theme: StyleSheet, { @@ -1024,36 +1025,42 @@ pub fn run_command( } }, command::Action::Widget(action) => { - let mut current_cache = std::mem::take(cache); + let mut current_caches = std::mem::take(caches); let mut current_operation = Some(action.into_operation()); - let mut user_interface = build_user_interface( + let mut user_interfaces = build_user_interfaces( application, - current_cache, renderer, - state.logical_size(), debug, - window::Id::MAIN, // TODO(derezzedex): run the operation on every widget tree + states, + current_caches, ); while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy - .send_event(Event::Application(message)) - .expect("Send message to event loop"); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); + for user_interface in user_interfaces.values_mut() { + user_interface.operate(renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(Event::Application(message)) + .expect("Send message to event loop"); + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } } } } - current_cache = user_interface.into_cache(); - *cache = current_cache; + let user_interfaces: HashMap<_, _> = user_interfaces + .drain() + .map(|(id, interface)| (id, interface.into_cache())) + .collect(); + + current_caches = user_interfaces; + *caches = current_caches; } } } @@ -1065,7 +1072,7 @@ pub fn build_user_interfaces<'a, A>( renderer: &mut A::Renderer, debug: &mut Debug, states: &HashMap>, - mut pure_states: HashMap, + mut cached_user_interfaces: HashMap, ) -> HashMap< window::Id, UserInterface< @@ -1080,12 +1087,12 @@ where { let mut interfaces = HashMap::new(); - for (id, pure_state) in pure_states.drain() { + for (id, cache) in cached_user_interfaces.drain() { let state = &states.get(&id).unwrap(); let user_interface = build_user_interface( application, - pure_state, + cache, renderer, state.logical_size(), debug, -- cgit From 3c095aa3f09f28c6fd9d2a7ba220ced407693e0b Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 15 Feb 2023 14:56:15 -0800 Subject: Merged in iced master --- glutin/src/multi_window.rs | 2 +- native/src/window/action.rs | 4 ++-- winit/src/multi_window.rs | 2 +- winit/src/window.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index 33fe60ff..da450dee 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -901,7 +901,7 @@ pub fn run_command( y, }); } - window::Action::SetMode(mode) => { + window::Action::ChangeMode(mode) => { let window = windows.get(&id).expect("No window found"); window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( diff --git a/native/src/window/action.rs b/native/src/window/action.rs index 63858bc8..e345cdfc 100644 --- a/native/src/window/action.rs +++ b/native/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::window::{Mode, UserAttention}; +use crate::window::{Mode, UserAttention, Settings}; use iced_futures::MaybeSend; use std::fmt; @@ -16,7 +16,7 @@ pub enum Action { /// Spawns a new window with the provided [`window::Settings`]. Spawn { /// The settings of the [`Window`]. - settings: window::Settings, + settings: Settings, }, /// Resize the window. Resize { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index bd7e9d44..f846e124 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -957,7 +957,7 @@ pub fn run_command( y, }); } - window::Action::SetMode(mode) => { + window::Action::ChangeMode(mode) => { let window = windows.get(&id).expect("No window found"); window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( diff --git a/winit/src/window.rs b/winit/src/window.rs index fa31dca1..97d80c38 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -60,7 +60,7 @@ pub fn move_to(id: window::Id, x: i32, y: i32) -> Command { /// Changes the [`Mode`] of the window. pub fn change_mode(id: window::Id, mode: Mode) -> Command { - Command::single(command::Action::Window(id, window::Action::SetMode(mode))) + Command::single(command::Action::Window(id, window::Action::ChangeMode(mode))) } /// Fetches the current [`Mode`] of the window. -- cgit From 8da098330b58542cc929f4f24d02e26bd654bae4 Mon Sep 17 00:00:00 2001 From: Bingus Date: Fri, 17 Feb 2023 11:42:49 -0800 Subject: Fixed widget animations implementation --- examples/multi_window/src/main.rs | 21 ++++---- native/src/widget/tree.rs | 2 +- native/src/window.rs | 1 - winit/src/multi_window.rs | 110 +++++++++++++++++++++++++++++--------- 4 files changed, 95 insertions(+), 39 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 23f08217..17d662b4 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -499,18 +499,17 @@ fn view_content<'a>( .spacing(10) .align_items(Alignment::Center); - Element::from( - container( - scrollable(content) - .vertical_scroll(Properties::new()) - .id(scrollable_id), - ) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y(), + container( + scrollable(content) + .height(Length::Fill) + .vertical_scroll(Properties::new()) + .id(scrollable_id), ) - .explain(Color::default()) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() } fn view_controls<'a>( diff --git a/native/src/widget/tree.rs b/native/src/widget/tree.rs index 0af40c33..da269632 100644 --- a/native/src/widget/tree.rs +++ b/native/src/widget/tree.rs @@ -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>( &mut self, new_children: &[impl Borrow + 'a>], diff --git a/native/src/window.rs b/native/src/window.rs index e768ed6d..a8f8b10f 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -21,7 +21,6 @@ pub use user_attention::UserAttention; use crate::subscription::{self, Subscription}; use crate::time::Instant; -use crate::window; /// Subscribes to the frames of the window of the running application. /// diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index f846e124..17eaa6fe 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -9,7 +9,7 @@ use crate::renderer; use crate::settings; use crate::widget::operation; use crate::window; -use crate::{conversion, multi_window}; +use crate::conversion; use crate::{ Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, Settings, Size, Subscription, @@ -237,7 +237,8 @@ where let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; - let (mut sender, receiver) = mpsc::unbounded(); + let (mut event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); let mut instance = Box::pin({ let run_instance = run_instance::( @@ -247,7 +248,8 @@ where runtime, proxy, debug, - receiver, + event_receiver, + control_sender, init_command, windows, settings.exit_on_close_request, @@ -299,13 +301,19 @@ where }; if let Some(event) = event { - sender.start_send(event).expect("Send event"); + event_sender.start_send(event).expect("Send event"); let poll = instance.as_mut().poll(&mut context); - *control_flow = match poll { - task::Poll::Pending => ControlFlow::Wait, - task::Poll::Ready(_) => ControlFlow::Exit, + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + *control_flow = flow; + } + } + task::Poll::Ready(_) => { + *control_flow = ControlFlow::Exit; + } }; } }) @@ -318,9 +326,10 @@ async fn run_instance( mut runtime: Runtime>, Event>, mut proxy: winit::event_loop::EventLoopProxy>, mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver< + mut event_receiver: mpsc::UnboundedReceiver< winit::event::Event<'_, Event>, >, + mut control_sender: mpsc::UnboundedSender, init_command: Command, mut windows: HashMap, _exit_on_close_request: bool, @@ -332,6 +341,7 @@ async fn run_instance( { use iced_futures::futures::stream::StreamExt; use winit::event; + use winit::event_loop::ControlFlow; let mut clipboard = Clipboard::connect(windows.values().next().expect("No window found")); @@ -390,11 +400,20 @@ async fn run_instance( let mut mouse_interaction = mouse::Interaction::default(); let mut events = Vec::new(); let mut messages = Vec::new(); + let mut redraw_pending = false; debug.startup_finished(); - 'main: while let Some(event) = receiver.next().await { + 'main: while let Some(event) = event_receiver.next().await { match event { + event::Event::NewEvents(start_cause) => { + redraw_pending = matches!( + start_cause, + event::StartCause::Init + | event::StartCause::Poll + | event::StartCause::ResumeTimeReached { .. } + ); + } event::Event::MainEventsCleared => { for id in states.keys().copied().collect::>() { let (filtered, remaining): (Vec<_>, Vec<_>) = @@ -408,29 +427,27 @@ async fn run_instance( ); events.retain(|el| remaining.contains(el)); - let mut filtered: Vec<_> = filtered + let window_events: Vec<_> = filtered .into_iter() .map(|(_id, event)| event) .collect(); - filtered.push(iced_native::Event::Window( - id, - window::Event::RedrawRequested(Instant::now()), - )); - let cursor_position = - states.get(&id).unwrap().cursor_position(); - let window = windows.get(&id).unwrap(); - - if filtered.is_empty() && messages.is_empty() { + if !redraw_pending + && window_events.is_empty() + && messages.is_empty() + { continue; } debug.event_processing_started(); + let cursor_position = + states.get(&id).unwrap().cursor_position(); + let (interface_state, statuses) = { let user_interface = interfaces.get_mut(&id).unwrap(); user_interface.update( - &filtered, + &window_events, cursor_position, &mut renderer, &mut clipboard, @@ -440,7 +457,8 @@ async fn run_instance( debug.event_processing_finished(); - for event in filtered.into_iter().zip(statuses.into_iter()) + for event in + window_events.into_iter().zip(statuses.into_iter()) { runtime.broadcast(event); } @@ -487,8 +505,6 @@ async fn run_instance( windows.get(&id).expect("No window found with ID."), ); - let should_exit = application.should_exit(); - interfaces = ManuallyDrop::new(build_user_interfaces( &application, &mut renderer, @@ -497,17 +513,35 @@ async fn run_instance( user_interfaces, )); - if should_exit { + if application.should_exit() { break 'main; } } + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = iced_native::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + ); + + let (interface_state, _) = + interfaces.get_mut(&id).unwrap().update( + &[redraw_event.clone()], + cursor_position, + &mut renderer, + &mut clipboard, + &mut messages, + ); + debug.draw_started(); let new_mouse_interaction = { - let user_interface = interfaces.get_mut(&id).unwrap(); let state = states.get(&id).unwrap(); - user_interface.draw( + interfaces.get_mut(&id).unwrap().draw( &mut renderer, state.theme(), &renderer::Style { @@ -518,6 +552,8 @@ async fn run_instance( }; debug.draw_finished(); + let window = windows.get(&id).unwrap(); + if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( new_mouse_interaction, @@ -528,6 +564,28 @@ async fn run_instance( for window in windows.values() { window.request_redraw(); + + runtime.broadcast(( + redraw_event.clone(), + crate::event::Status::Ignored, + )); + + let _ = + control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }); + + redraw_pending = false; } } } -- cgit From 2d427455ce8f9627da7c09eb80f38a615f0ddcb7 Mon Sep 17 00:00:00 2001 From: Bingus Date: Fri, 17 Feb 2023 11:50:52 -0800 Subject: Iced master merge (again) --- examples/multi_window/src/main.rs | 4 ++-- glutin/src/multi_window.rs | 18 ++++++++++++++---- src/multi_window/application.rs | 1 + winit/src/multi_window.rs | 21 ++++++++++++++++----- winit/src/window.rs | 2 +- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 17d662b4..c2687ee6 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -441,7 +441,7 @@ impl Pane { fn new(id: usize, axis: pane_grid::Axis) -> Self { Self { id, - scrollable_id: scrollable::Id::new(format!("{:?}", id)), + scrollable_id: scrollable::Id::unique(), axis, is_pinned: false, is_moving: false, @@ -495,7 +495,7 @@ fn view_content<'a>( controls, ] .width(Length::Fill) - .height(Length::Units(800)) + .height(800) .spacing(10) .align_items(Alignment::Center); diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs index da450dee..620d01d8 100644 --- a/glutin/src/multi_window.rs +++ b/glutin/src/multi_window.rs @@ -25,16 +25,15 @@ use glutin::surface::{GlSurface, SwapInterval}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use crate::application::gl_surface; -use iced_native::window::Action; use iced_winit::multi_window::Event; use std::collections::HashMap; use std::ffi::CString; use std::mem::ManuallyDrop; use std::num::NonZeroU32; +use iced_native::widget::operation; #[cfg(feature = "tracing")] use tracing::{info_span, instrument::Instrument}; -use iced_native::widget::operation; #[allow(unsafe_code)] const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; @@ -937,16 +936,27 @@ pub fn run_command( let window = windows.get(&id).expect("No window found!"); window.set_decorations(!window.is_decorated()); } - Action::RequestUserAttention(attention_type) => { + window::Action::RequestUserAttention(attention_type) => { let window = windows.get(&id).expect("No window found!"); window.request_user_attention( attention_type.map(conversion::user_attention), ); } - Action::GainFocus => { + window::Action::GainFocus => { let window = windows.get(&id).expect("No window found!"); window.focus_window(); } + window::Action::ChangeAlwaysOnTop(on_top) => { + let window = windows.get(&id).expect("No window found!"); + window.set_always_on_top(on_top); + } + window::Action::FetchId(tag) => { + let window = windows.get(&id).expect("No window found!"); + + proxy + .send_event(Event::Application(tag(window.id().into()))) + .expect("Send message to event loop.") + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 3af1d8d5..d0b515ab 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -148,6 +148,7 @@ pub trait Application: Sized { /// 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 } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 17eaa6fe..96dd1c8b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -4,12 +4,12 @@ mod state; pub use state::State; use crate::clipboard::{self, Clipboard}; +use crate::conversion; use crate::mouse; use crate::renderer; use crate::settings; use crate::widget::operation; use crate::window; -use crate::conversion; use crate::{ Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, Settings, Size, Subscription, @@ -22,7 +22,6 @@ use iced_native::user_interface::{self, UserInterface}; pub use iced_native::application::{Appearance, StyleSheet}; -use iced_native::window::Action; use std::collections::HashMap; use std::mem::ManuallyDrop; use std::time::Instant; @@ -147,6 +146,7 @@ where /// 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 } @@ -470,7 +470,7 @@ async fn run_instance( user_interface::State::Outdated, ) { - let user_interfaces: HashMap<_, _> = + let cached_interfaces: HashMap<_, _> = ManuallyDrop::into_inner(interfaces) .drain() .map( @@ -510,7 +510,7 @@ async fn run_instance( &mut renderer, &mut debug, &states, - user_interfaces, + cached_interfaces, )); if application.should_exit() { @@ -1057,10 +1057,21 @@ pub fn run_command( attention_type.map(conversion::user_attention), ); } - Action::GainFocus => { + window::Action::GainFocus => { let window = windows.get(&id).expect("No window found!"); window.focus_window(); } + window::Action::ChangeAlwaysOnTop(on_top) => { + let window = windows.get(&id).expect("No window found!"); + window.set_always_on_top(on_top); + } + window::Action::FetchId(tag) => { + let window = windows.get(&id).expect("No window found!"); + + proxy + .send_event(Event::Application(tag(window.id().into()))) + .expect("Send message to event loop.") + } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { diff --git a/winit/src/window.rs b/winit/src/window.rs index 88cd3f14..8fd415ef 100644 --- a/winit/src/window.rs +++ b/winit/src/window.rs @@ -122,7 +122,7 @@ pub fn fetch_id( id: window::Id, f: impl FnOnce(u64) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(id: window::Id, window::Action::FetchId(Box::new( + Command::single(command::Action::Window(id, window::Action::FetchId(Box::new( f, )))) } -- cgit From 3aaf5d8873b16302badb14dc5508329c943862fb Mon Sep 17 00:00:00 2001 From: Bingus Date: Fri, 17 Feb 2023 13:26:31 -0800 Subject: Fixed widget operations --- winit/src/multi_window.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 96dd1c8b..fc91f41b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -470,7 +470,7 @@ async fn run_instance( user_interface::State::Outdated, ) { - let cached_interfaces: HashMap<_, _> = + let mut cached_interfaces: HashMap<_, _> = ManuallyDrop::into_inner(interfaces) .drain() .map( @@ -486,7 +486,7 @@ async fn run_instance( // Update application update( &mut application, - &mut caches, + &mut cached_interfaces, &states, &mut renderer, &mut runtime, -- cgit From bd58d5fe25182908e99fdb0ced07b86666e45081 Mon Sep 17 00:00:00 2001 From: Bingus Date: Mon, 20 Feb 2023 12:34:04 -0800 Subject: Cargo fix --- Cargo.toml | 2 +- examples/multi_window/Cargo.toml | 2 +- examples/multi_window/src/main.rs | 304 ++++++++++++++++++++------------------ winit/src/multi_window.rs | 56 +++---- 4 files changed, 191 insertions(+), 173 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e4da8b2c..1a615da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ chrome-trace = [ "iced_glow?/tracing", ] # Enables experimental multi-window support -multi_window = ["iced_winit/multi_window", "iced_glutin/multi_window"] +multi_window = ["iced_winit/multi_window", "iced_glutin?/multi_window"] [badges] maintenance = { status = "actively-developed" } diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 62198595..0bb83f37 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -7,7 +7,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -iced = { path = "../..", features = ["debug", "multi_window"] } +iced = { path = "../..", features = ["debug", "multi_window", "tokio"] } env_logger = "0.10.0" iced_native = { path = "../../native" } iced_lazy = { path = "../../lazy" } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index c2687ee6..60f32a7d 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,5 +1,5 @@ use iced::alignment::{self, Alignment}; -use iced::executor; +use iced::{executor, time}; use iced::keyboard; use iced::multi_window::Application; use iced::theme::{self, Theme}; @@ -15,6 +15,7 @@ use iced_native::{event, subscription, Event}; use iced_native::widget::scrollable::{Properties, RelativeOffset}; use iced_native::window::Id; use std::collections::HashMap; +use std::time::{Duration, Instant}; pub fn main() -> iced::Result { env_logger::init(); @@ -25,6 +26,7 @@ pub fn main() -> iced::Result { struct Example { windows: HashMap, panes_created: usize, + count: usize, _focused: window::Id, } @@ -39,6 +41,7 @@ struct Window { #[derive(Debug, Clone)] enum Message { Window(window::Id, WindowMessage), + CountIncremented(Instant), } #[derive(Debug, Clone)] @@ -80,6 +83,7 @@ impl Application for Example { Example { windows: HashMap::from([(window::Id::MAIN, window)]), panes_created: 1, + count: 0, _focused: window::Id::MAIN, }, Command::none(), @@ -94,44 +98,29 @@ impl Application for Example { } fn update(&mut self, message: Message) -> Command { - let Message::Window(id, message) = message; match message { - WindowMessage::SnapToggle => { - let window = self.windows.get_mut(&id).unwrap(); - - if let Some(focused) = &window.focus { - let pane = window.panes.get_mut(focused).unwrap(); - - let cmd = scrollable::snap_to( - pane.scrollable_id.clone(), - if pane.snapped { - RelativeOffset::START - } else { - RelativeOffset::END - }, - ); - - pane.snapped = !pane.snapped; - return cmd; - } - } - WindowMessage::Split(axis, pane) => { - let window = self.windows.get_mut(&id).unwrap(); - let result = window.panes.split( - axis, - &pane, - Pane::new(self.panes_created, axis), - ); - - if let Some((pane, _)) = result { - window.focus = Some(pane); + Message::Window(id, message) => match message { + WindowMessage::SnapToggle => { + let window = self.windows.get_mut(&id).unwrap(); + + if let Some(focused) = &window.focus { + let pane = window.panes.get_mut(focused).unwrap(); + + let cmd = scrollable::snap_to( + pane.scrollable_id.clone(), + if pane.snapped { + RelativeOffset::START + } else { + RelativeOffset::END + }, + ); + + pane.snapped = !pane.snapped; + return cmd; + } } - - self.panes_created += 1; - } - WindowMessage::SplitFocused(axis) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { + WindowMessage::Split(axis, pane) => { + let window = self.windows.get_mut(&id).unwrap(); let result = window.panes.split( axis, &pane, @@ -144,112 +133,131 @@ impl Application for Example { self.panes_created += 1; } - } - WindowMessage::FocusAdjacent(direction) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - if let Some(adjacent) = - window.panes.adjacent(&pane, direction) - { - window.focus = Some(adjacent); + WindowMessage::SplitFocused(axis) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + let result = window.panes.split( + axis, + &pane, + Pane::new(self.panes_created, axis), + ); + + if let Some((pane, _)) = result { + window.focus = Some(pane); + } + + self.panes_created += 1; } } - } - WindowMessage::Clicked(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - window.focus = Some(pane); - } - WindowMessage::CloseWindow => { - let _ = self.windows.remove(&id); - return window::close(id); - } - WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { - let window = self.windows.get_mut(&id).unwrap(); - window.panes.resize(&split, ratio); - } - WindowMessage::SelectedWindow(pane, selected) => { - let window = self.windows.get_mut(&id).unwrap(); - let (mut pane, _) = window.panes.close(&pane).unwrap(); - pane.is_moving = false; - - if let Some(window) = self.windows.get_mut(&selected.0) { - let (&first_pane, _) = window.panes.iter().next().unwrap(); - let result = - window.panes.split(pane.axis, &first_pane, pane); - - if let Some((pane, _)) = result { - window.focus = Some(pane); + WindowMessage::FocusAdjacent(direction) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + if let Some(adjacent) = + window.panes.adjacent(&pane, direction) + { + window.focus = Some(adjacent); + } } } - } - WindowMessage::ToggleMoving(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.panes.get_mut(&pane) { - pane.is_moving = !pane.is_moving; + WindowMessage::Clicked(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + window.focus = Some(pane); } - } - WindowMessage::TitleChanged(title) => { - let window = self.windows.get_mut(&id).unwrap(); - window.title = title; - } - WindowMessage::PopOut(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some((popped, sibling)) = window.panes.close(&pane) { - window.focus = Some(sibling); - - let (panes, _) = pane_grid::State::new(popped); - let window = Window { - panes, - focus: None, - title: format!("New window ({})", self.windows.len()), - scale: 1.0 + (self.windows.len() as f64 / 10.0), - }; - - let window_id = window::Id::new(self.windows.len()); - self.windows.insert(window_id, window); - return window::spawn(window_id, Default::default()); + WindowMessage::CloseWindow => { + let _ = self.windows.remove(&id); + return window::close(id); } - } - WindowMessage::Dragged(pane_grid::DragEvent::Dropped { - pane, - target, - }) => { - let window = self.windows.get_mut(&id).unwrap(); - window.panes.swap(&pane, &target); - } - // WindowMessage::Dragged(pane_grid::DragEvent::Picked { pane }) => { - // println!("Picked {pane:?}"); - // } - WindowMessage::Dragged(_) => {} - WindowMessage::TogglePin(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(Pane { is_pinned, .. }) = - window.panes.get_mut(&pane) - { - *is_pinned = !*is_pinned; + WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { + let window = self.windows.get_mut(&id).unwrap(); + window.panes.resize(&split, ratio); } - } - WindowMessage::Close(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some((_, sibling)) = window.panes.close(&pane) { - window.focus = Some(sibling); + WindowMessage::SelectedWindow(pane, selected) => { + let window = self.windows.get_mut(&id).unwrap(); + let (mut pane, _) = window.panes.close(&pane).unwrap(); + pane.is_moving = false; + + if let Some(window) = self.windows.get_mut(&selected.0) { + let (&first_pane, _) = window.panes.iter().next().unwrap(); + let result = + window.panes.split(pane.axis, &first_pane, pane); + + if let Some((pane, _)) = result { + window.focus = Some(pane); + } + } } - } - WindowMessage::CloseFocused => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { + WindowMessage::ToggleMoving(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.panes.get_mut(&pane) { + pane.is_moving = !pane.is_moving; + } + } + WindowMessage::TitleChanged(title) => { + let window = self.windows.get_mut(&id).unwrap(); + window.title = title; + } + WindowMessage::PopOut(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some((popped, sibling)) = window.panes.close(&pane) { + window.focus = Some(sibling); + + let (panes, _) = pane_grid::State::new(popped); + let window = Window { + panes, + focus: None, + title: format!("New window ({})", self.windows.len()), + scale: 1.0 + (self.windows.len() as f64 / 10.0), + }; + + let window_id = window::Id::new(self.windows.len()); + self.windows.insert(window_id, window); + return window::spawn(window_id, Default::default()); + } + } + WindowMessage::Dragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + let window = self.windows.get_mut(&id).unwrap(); + window.panes.swap(&pane, &target); + } + // WindowMessage::Dragged(pane_grid::DragEvent::Picked { pane }) => { + // println!("Picked {pane:?}"); + // } + WindowMessage::Dragged(_) => {} + WindowMessage::TogglePin(pane) => { + let window = self.windows.get_mut(&id).unwrap(); if let Some(Pane { is_pinned, .. }) = - window.panes.get(&pane) + window.panes.get_mut(&pane) { - if !is_pinned { - if let Some((_, sibling)) = - window.panes.close(&pane) - { - window.focus = Some(sibling); + *is_pinned = !*is_pinned; + } + } + WindowMessage::Close(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some((_, sibling)) = window.panes.close(&pane) { + window.focus = Some(sibling); + } + } + WindowMessage::CloseFocused => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + if let Some(Pane { is_pinned, .. }) = + window.panes.get(&pane) + { + if !is_pinned { + if let Some((_, sibling)) = + window.panes.close(&pane) + { + window.focus = Some(sibling); + } } } } } + }, + Message::CountIncremented(_) => { + self.count += 1; } } @@ -257,23 +265,26 @@ impl Application for Example { } fn subscription(&self) -> Subscription { - subscription::events_with(|event, status| { - if let event::Status::Captured = status { - return None; - } + Subscription::batch(vec![ + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } - match event { - Event::Keyboard(keyboard::Event::KeyPressed { - modifiers, - key_code, - }) if modifiers.command() => { - handle_hotkey(key_code).map(|message| { - Message::Window(window::Id::new(0usize), message) - }) - } // TODO(derezzedex) - _ => None, - } - }) + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.command() => { + handle_hotkey(key_code).map(|message| { + Message::Window(window::Id::new(0usize), message) + }) + } // TODO(derezzedex) + _ => None, + } + }), + time::every(Duration::from_secs(1)).map(Message::CountIncremented), + ]) } fn view(&self, window_id: window::Id) -> Element { @@ -335,6 +346,7 @@ impl Application for Example { view_content( id, pane.scrollable_id.clone(), + self.count, total_panes, pane.is_pinned, size, @@ -453,6 +465,7 @@ impl Pane { fn view_content<'a>( pane: pane_grid::Pane, scrollable_id: scrollable::Id, + count: usize, total_panes: usize, is_pinned: bool, size: Size, @@ -493,6 +506,7 @@ fn view_content<'a>( let content = column![ text(format!("{}x{}", size.width, size.height)).size(24), controls, + text(format!("{count}")).size(48), ] .width(Length::Fill) .height(800) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index fc91f41b..788f39f7 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -416,6 +416,7 @@ async fn run_instance( } event::Event::MainEventsCleared => { for id in states.keys().copied().collect::>() { + // Partition events into only events for this window let (filtered, remaining): (Vec<_>, Vec<_>) = events.iter().cloned().partition( |(window_id, _event): &( @@ -426,7 +427,9 @@ async fn run_instance( }, ); + // Only retain events which have not been processed for next iteration events.retain(|el| remaining.contains(el)); + let window_events: Vec<_> = filtered .into_iter() .map(|(_id, event)| event) @@ -439,8 +442,8 @@ async fn run_instance( continue; } + // Process winit events for window debug.event_processing_started(); - let cursor_position = states.get(&id).unwrap().cursor_position(); @@ -455,15 +458,16 @@ async fn run_instance( ) }; - debug.event_processing_finished(); - for event in window_events.into_iter().zip(statuses.into_iter()) { runtime.broadcast(event); } + debug.event_processing_finished(); - // TODO(derezzedex): Should we redraw every window? We can't know what changed. + // Update application with app message(s) + // Note: without tying an app message to a window ID, we must redraw all windows + // as we cannot know what changed without some kind of damage tracking. if !messages.is_empty() || matches!( interface_state, @@ -498,7 +502,7 @@ async fn run_instance( || compositor.fetch_information(), ); - // Update window + // synchronize window state with application state. states.get_mut(&id).unwrap().synchronize( &application, id, @@ -564,29 +568,29 @@ async fn run_instance( for window in windows.values() { window.request_redraw(); + } - runtime.broadcast(( - redraw_event.clone(), - crate::event::Status::Ignored, - )); + runtime.broadcast(( + redraw_event.clone(), + crate::event::Status::Ignored, + )); - let _ = - control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - ControlFlow::Poll - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - redraw_pending = false; - } + let _ = + control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }); + + redraw_pending = false; } } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( -- cgit From 50b9c778b1e49fc83f827a340dce36a09bb291d6 Mon Sep 17 00:00:00 2001 From: Bingus Date: Tue, 21 Feb 2023 16:46:32 -0800 Subject: Fixed state syncing issue with MW. --- winit/src/multi_window.rs | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 788f39f7..1a9d4a1c 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -502,12 +502,16 @@ async fn run_instance( || compositor.fetch_information(), ); - // synchronize window state with application state. - states.get_mut(&id).unwrap().synchronize( - &application, - id, - windows.get(&id).expect("No window found with ID."), - ); + // synchronize window states with application states. + for (id, state) in states.iter_mut() { + state.synchronize( + &application, + *id, + windows + .get(id) + .expect("No window found with ID."), + ); + } interfaces = ManuallyDrop::new(build_user_interfaces( &application, @@ -575,20 +579,19 @@ async fn run_instance( crate::event::Status::Ignored, )); - let _ = - control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - ControlFlow::Poll - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); + let _ = control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }); redraw_pending = false; } -- cgit From e36daa6f937abd7cb2071fd8852a3c12263944ea Mon Sep 17 00:00:00 2001 From: bungoboingo Date: Tue, 28 Feb 2023 13:44:36 -0800 Subject: Removed glutin MW support and reverted glutin changes back to Iced master since it's being axed as we speak. --- Cargo.toml | 4 +- examples/integration_opengl/src/main.rs | 8 +- glutin/Cargo.toml | 10 +- glutin/src/application.rs | 364 +++-------- glutin/src/lib.rs | 3 - glutin/src/multi_window.rs | 1060 ------------------------------- graphics/src/window/gl_compositor.rs | 2 +- native/src/window/id.rs | 4 +- src/lib.rs | 2 +- winit/src/application.rs | 2 +- winit/src/multi_window.rs | 25 +- winit/src/multi_window/state.rs | 7 +- winit/src/profiler.rs | 1 - winit/src/settings.rs | 8 +- winit/src/settings/windows.rs | 4 +- 15 files changed, 129 insertions(+), 1375 deletions(-) delete mode 100644 glutin/src/multi_window.rs diff --git a/Cargo.toml b/Cargo.toml index 1a615da2..5276d921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,8 @@ chrome-trace = [ "iced_wgpu?/tracing", "iced_glow?/tracing", ] -# Enables experimental multi-window support -multi_window = ["iced_winit/multi_window", "iced_glutin?/multi_window"] +# Enables experimental multi-window support for iced_winit +multi_window = ["iced_winit/multi_window"] [badges] maintenance = { status = "actively-developed" } diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs index fdbd7369..4dd3a4a9 100644 --- a/examples/integration_opengl/src/main.rs +++ b/examples/integration_opengl/src/main.rs @@ -13,7 +13,6 @@ use iced_glow::{Backend, Renderer, Settings, Viewport}; use iced_glutin::conversion; use iced_glutin::glutin; use iced_glutin::renderer; -use iced_glutin::window; use iced_glutin::{program, Clipboard, Color, Debug, Size}; pub fn main() { @@ -31,8 +30,7 @@ pub fn main() { .unwrap(); unsafe { - let windowed_context = - windowed_context.make_current(todo!("derezzedex")).unwrap(); + let windowed_context = windowed_context.make_current().unwrap(); let gl = glow::Context::from_loader_function(|s| { windowed_context.get_proc_address(s) as *const _ @@ -109,7 +107,7 @@ pub fn main() { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( - window::Id::MAIN, + iced_winit::window::Id::MAIN, &event, windowed_context.window().scale_factor(), modifiers, @@ -182,7 +180,7 @@ pub fn main() { ), ); - windowed_context.swap_buffers(todo!("derezzedex")).unwrap(); + windowed_context.swap_buffers().unwrap(); } _ => (), } diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 01dd3748..10d3778b 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -11,19 +11,17 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -trace = ["iced_winit/trace", "tracing"] +trace = ["iced_winit/trace"] debug = ["iced_winit/debug"] system = ["iced_winit/system"] -multi_window = ["iced_winit/multi_window"] - -[dependencies.raw-window-handle] -version = "0.5.0" [dependencies] log = "0.4" [dependencies.glutin] -version = "0.30" +version = "0.29" +git = "https://github.com/iced-rs/glutin" +rev = "da8d291486b4c9bec12487a46c119c4b1d386abf" [dependencies.iced_native] version = "0.9" diff --git a/glutin/src/application.rs b/glutin/src/application.rs index c0a8cda4..24f38e7b 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -13,33 +13,14 @@ use iced_winit::futures::channel::mpsc; use iced_winit::renderer; use iced_winit::time::Instant; use iced_winit::user_interface; -use iced_winit::winit; use iced_winit::{Clipboard, Command, Debug, Event, Proxy, Settings}; -use glutin::config::{ - Config, ConfigSurfaceTypes, ConfigTemplateBuilder, GlConfig, -}; -use glutin::context::{ - ContextApi, ContextAttributesBuilder, NotCurrentContext, - NotCurrentGlContextSurfaceAccessor, - PossiblyCurrentContextGlSurfaceAccessor, PossiblyCurrentGlContext, -}; -use glutin::display::{Display, DisplayApiPreference, GlDisplay}; -use glutin::surface::{ - GlSurface, Surface, SurfaceAttributesBuilder, WindowSurface, -}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; - -use std::ffi::CString; +use glutin::window::Window; use std::mem::ManuallyDrop; -use std::num::NonZeroU32; -#[cfg(feature = "trace")] +#[cfg(feature = "tracing")] use tracing::{info_span, instrument::Instrument}; -#[allow(unsafe_code)] -const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; - /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. pub fn run( @@ -54,8 +35,9 @@ where { use futures::task; use futures::Future; - use winit::event_loop::EventLoopBuilder; - use winit::platform::run_return::EventLoopExtRunReturn; + use glutin::event_loop::EventLoopBuilder; + use glutin::platform::run_return::EventLoopExtRunReturn; + use glutin::ContextBuilder; #[cfg(feature = "trace")] let _guard = iced_winit::Profiler::init(); @@ -63,7 +45,7 @@ where let mut debug = Debug::new(); debug.startup_started(); - #[cfg(feature = "trace")] + #[cfg(feature = "tracing")] let _ = info_span!("Application::Glutin", "RUN").entered(); let mut event_loop = EventLoopBuilder::with_user_event().build(); @@ -82,205 +64,74 @@ where runtime.enter(|| A::new(flags)) }; - let builder = settings.window.into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ); - - log::debug!("Window builder: {:#?}", builder); - - #[allow(unsafe_code)] - let (display, window, surface, context) = unsafe { - struct Configuration(Config); - use std::fmt; - impl fmt::Debug for Configuration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let config = &self.0; - - f.debug_struct("Configuration") - .field("raw", &config) - .field("samples", &config.num_samples()) - .field("buffer_type", &config.color_buffer_type()) - .field("surface_type", &config.config_surface_types()) - .field("depth", &config.depth_size()) - .field("alpha", &config.alpha_size()) - .field("stencil", &config.stencil_size()) - .field("float_pixels", &config.float_pixels()) - .field("srgb", &config.srgb_capable()) - .field("api", &config.api()) - .finish() - } - } - - impl AsRef for Configuration { - fn as_ref(&self) -> &Config { - &self.0 - } - } - - let display_handle = event_loop.raw_display_handle(); - - #[cfg(all( - any(windows, target_os = "macos"), - not(target_arch = "wasm32") - ))] - let (window, window_handle) = { - let window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; - - let handle = window.raw_window_handle(); - - (window, handle) - }; - - #[cfg(target_arch = "wasm32")] - let preference = Err(Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "target not supported by backend" - )), - ))?; - - #[cfg(all(windows, not(target_arch = "wasm32")))] - let preference = DisplayApiPreference::WglThenEgl(Some(window_handle)); - - #[cfg(all(target_os = "macos", not(target_arch = "wasm32")))] - let preference = DisplayApiPreference::Cgl; - - #[cfg(all( - unix, - not(target_os = "macos"), - not(target_arch = "wasm32") - ))] - let preference = DisplayApiPreference::GlxThenEgl(Box::new( - winit::platform::unix::register_xlib_error_hook, - )); - - let display = - Display::new(display_handle, preference).map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create display: {error}" - )), - ) - })?; + let context = { + let builder = settings.window.into_builder( + &application.title(), + event_loop.primary_monitor(), + settings.id, + ); - log::debug!("Display: {}", display.version_string()); + log::debug!("Window builder: {:#?}", builder); - let samples = C::sample_count(&compositor_settings) as u8; - let mut template = ConfigTemplateBuilder::new() - .with_surface_type(ConfigSurfaceTypes::WINDOW); + let opengl_builder = ContextBuilder::new() + .with_vsync(true) + .with_multisampling(C::sample_count(&compositor_settings) as u16); - if samples != 0 { - template = template.with_multisampling(samples); - } + let opengles_builder = opengl_builder.clone().with_gl( + glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)), + ); - #[cfg(all(windows, not(target_arch = "wasm32")))] - let template = template.compatible_with_native_window(window_handle); - - log::debug!("Searching for display configurations"); - let configuration = display - .find_configs(template.build()) - .map_err(|_| { - Error::GraphicsCreationFailed( - iced_graphics::Error::NoAvailablePixelFormat, - ) - })? - .map(Configuration) - .inspect(|config| { - log::trace!("{config:#?}"); - }) - .min_by_key(|config| { - config.as_ref().num_samples().saturating_sub(samples) - }) - .ok_or(Error::GraphicsCreationFailed( - iced_graphics::Error::NoAvailablePixelFormat, - ))?; - - log::debug!("Selected: {configuration:#?}"); - - #[cfg(all( - unix, - not(target_os = "macos"), - not(target_arch = "wasm32") - ))] - let (window, window_handle) = { - use glutin::platform::x11::X11GlConfigExt; - let builder = - if let Some(visual) = configuration.as_ref().x11_visual() { - use winit::platform::unix::WindowBuilderExtUnix; - builder.with_x11_visual(visual.into_raw()) - } else { - builder - }; - - let window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; - - let handle = window.raw_window_handle(); - - (window, handle) + let (first_builder, second_builder) = if settings.try_opengles_first { + (opengles_builder, opengl_builder) + } else { + (opengl_builder, opengles_builder) }; - let attributes = - ContextAttributesBuilder::new().build(Some(window_handle)); - let fallback_attributes = ContextAttributesBuilder::new() - .with_context_api(ContextApi::Gles(None)) - .build(Some(window_handle)); + log::info!("Trying first builder: {:#?}", first_builder); - let context = display - .create_context(configuration.as_ref(), &attributes) + let context = first_builder + .build_windowed(builder.clone(), &event_loop) .or_else(|_| { - display.create_context( - configuration.as_ref(), - &fallback_attributes, - ) + log::info!("Trying second builder: {:#?}", second_builder); + second_builder.build_windowed(builder, &event_loop) }) .map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create context: {error}" - )), - ) - })?; + use glutin::CreationError; + use iced_graphics::Error as ContextError; - let surface = gl_surface(&display, configuration.as_ref(), &window) - .map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create surface: {error}" - )), - ) + match error { + CreationError::Window(error) => { + Error::WindowCreationFailed(error) + } + CreationError::OpenGlVersionNotSupported => { + Error::GraphicsCreationFailed( + ContextError::VersionNotSupported, + ) + } + CreationError::NoAvailablePixelFormat => { + Error::GraphicsCreationFailed( + ContextError::NoAvailablePixelFormat, + ) + } + error => Error::GraphicsCreationFailed( + ContextError::BackendError(error.to_string()), + ), + } })?; - let context = { - context - .make_current(&surface) - .expect("make context current") - }; - - if let Err(error) = surface.set_swap_interval( - &context, - glutin::surface::SwapInterval::Wait(ONE), - ) { - log::error!("set swap interval failed: {}", error); + #[allow(unsafe_code)] + unsafe { + context.make_current().expect("Make OpenGL context current") } - - (display, window, surface, context) }; #[allow(unsafe_code)] let (compositor, renderer) = unsafe { C::new(compositor_settings, |address| { - let address = CString::new(address).expect("address error"); - display.get_proc_address(address.as_c_str()) + context.get_proc_address(address) })? }; - let context = { context.make_not_current().expect("make context current") }; - let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); @@ -294,14 +145,12 @@ where debug, event_receiver, control_sender, - window, - surface, context, init_command, settings.exit_on_close_request, ); - #[cfg(feature = "trace")] + #[cfg(feature = "tracing")] let run_instance = run_instance.instrument(info_span!("Application", "LOOP")); @@ -311,22 +160,22 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); let _ = event_loop.run_return(move |event, _, control_flow| { - use winit::event_loop::ControlFlow; + use glutin::event_loop::ControlFlow; if let ControlFlow::ExitWithCode(_) = control_flow { return; } let event = match event { - winit::event::Event::WindowEvent { + glutin::event::Event::WindowEvent { event: - winit::event::WindowEvent::ScaleFactorChanged { + glutin::event::WindowEvent::ScaleFactorChanged { new_inner_size, .. }, window_id, - } => Some(winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(*new_inner_size), + } => Some(glutin::event::Event::WindowEvent { + event: glutin::event::WindowEvent::Resized(*new_inner_size), window_id, }), _ => event.to_static(), @@ -358,13 +207,13 @@ async fn run_instance( mut compositor: C, mut renderer: A::Renderer, mut runtime: Runtime, A::Message>, - mut proxy: winit::event_loop::EventLoopProxy, + mut proxy: glutin::event_loop::EventLoopProxy, mut debug: Debug, - mut event_receiver: mpsc::UnboundedReceiver, >, - mut control_sender: mpsc::UnboundedSender, - window: winit::window::Window, - surface: Surface, - context: NotCurrentContext, + mut event_receiver: mpsc::UnboundedReceiver< + glutin::event::Event<'_, A::Message>, + >, + mut control_sender: mpsc::UnboundedSender, + mut context: glutin::ContextWrapper, init_command: Command, exit_on_close_request: bool, ) where @@ -373,19 +222,13 @@ async fn run_instance( C: window::GLCompositor + 'static, ::Theme: StyleSheet, { + use glutin::event; + use glutin::event_loop::ControlFlow; use iced_winit::futures::stream::StreamExt; - use winit::event_loop::ControlFlow; - use winit::event; - let context = { - context - .make_current(&surface) - .expect("make context current") - }; - - let mut clipboard = Clipboard::connect(&window); + let mut clipboard = Clipboard::connect(context.window()); let mut cache = user_interface::Cache::default(); - let mut state = application::State::new(&application, &window); + let mut state = application::State::new(&application, context.window()); let mut viewport_version = state.viewport_version(); let mut should_exit = false; @@ -400,7 +243,7 @@ async fn run_instance( &mut should_exit, &mut proxy, &mut debug, - &window, + context.window(), || compositor.fetch_information(), ); runtime.track(application.subscription()); @@ -473,12 +316,12 @@ async fn run_instance( &mut proxy, &mut debug, &mut messages, - &window, + context.window(), || compositor.fetch_information(), ); // Update window - state.synchronize(&application, &window); + state.synchronize(&application, context.window()); user_interface = ManuallyDrop::new(application::build_user_interface( @@ -524,15 +367,16 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); + context.window().set_cursor_icon( + conversion::mouse_interaction(new_mouse_interaction), + ); mouse_interaction = new_mouse_interaction; } - window.request_redraw(); - runtime.broadcast((redraw_event, crate::event::Status::Ignored)); + context.window().request_redraw(); + runtime + .broadcast((redraw_event, crate::event::Status::Ignored)); let _ = control_sender.start_send(match interface_state { user_interface::State::Updated { @@ -564,15 +408,18 @@ async fn run_instance( messages.push(message); } event::Event::RedrawRequested(_) => { - #[cfg(feature = "trace")] + #[cfg(feature = "tracing")] let _ = info_span!("Application", "FRAME").entered(); debug.render_started(); - if !context.is_current() { - context - .make_current(&surface) - .expect("Make OpenGL context current"); + #[allow(unsafe_code)] + unsafe { + if !context.is_current() { + context = context + .make_current() + .expect("Make OpenGL context current"); + } } let current_viewport_version = state.viewport_version(); @@ -600,18 +447,19 @@ async fn run_instance( debug.draw_finished(); if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); + context.window().set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); mouse_interaction = new_mouse_interaction; } - surface.resize( - &context, - NonZeroU32::new(physical_size.width).unwrap_or(ONE), - NonZeroU32::new(physical_size.height).unwrap_or(ONE), - ); + context.resize(glutin::dpi::PhysicalSize::new( + physical_size.width, + physical_size.height, + )); compositor.resize_viewport(physical_size); @@ -625,7 +473,7 @@ async fn run_instance( &debug.overlay(), ); - surface.swap_buffers(&context).expect("Swap buffers"); + context.swap_buffers().expect("Swap buffers"); debug.render_finished(); @@ -642,7 +490,7 @@ async fn run_instance( break; } - state.update(&window, &window_event, &mut debug); + state.update(context.window(), &window_event, &mut debug); if let Some(event) = conversion::window_event( crate::window::Id::MAIN, @@ -660,23 +508,3 @@ async fn run_instance( // Manually drop the user interface drop(ManuallyDrop::into_inner(user_interface)); } - -#[allow(unsafe_code)] -/// Creates a new [`glutin::Surface`]. -pub fn gl_surface( - display: &Display, - gl_config: &Config, - window: &winit::window::Window, -) -> Result, glutin::error::Error> { - let (width, height) = window.inner_size().into(); - - let surface_attributes = SurfaceAttributesBuilder::::new() - .with_srgb(Some(true)) - .build( - window.raw_window_handle(), - NonZeroU32::new(width).unwrap_or(ONE), - NonZeroU32::new(height).unwrap_or(ONE), - ); - - unsafe { display.create_window_surface(gl_config, &surface_attributes) } -} diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs index 45d6cb5b..33afd664 100644 --- a/glutin/src/lib.rs +++ b/glutin/src/lib.rs @@ -29,8 +29,5 @@ pub use iced_winit::*; pub mod application; -#[cfg(feature = "multi_window")] -pub mod multi_window; - #[doc(no_inline)] pub use application::Application; diff --git a/glutin/src/multi_window.rs b/glutin/src/multi_window.rs deleted file mode 100644 index 620d01d8..00000000 --- a/glutin/src/multi_window.rs +++ /dev/null @@ -1,1060 +0,0 @@ -//! Create interactive, native cross-platform applications. -use crate::mouse; -use crate::{Error, Executor, Runtime}; - -pub use iced_winit::multi_window::{self, Application, StyleSheet}; - -use iced_winit::conversion; -use iced_winit::futures; -use iced_winit::futures::channel::mpsc; -use iced_winit::renderer; -use iced_winit::user_interface; -use iced_winit::window; -use iced_winit::winit; -use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; - -use glutin::config::{ - Config, ConfigSurfaceTypes, ConfigTemplateBuilder, GlConfig, -}; -use glutin::context::{ - ContextApi, ContextAttributesBuilder, NotCurrentContext, - NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext, -}; -use glutin::display::{Display, DisplayApiPreference, GlDisplay}; -use glutin::surface::{GlSurface, SwapInterval}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; - -use crate::application::gl_surface; -use iced_winit::multi_window::Event; -use std::collections::HashMap; -use std::ffi::CString; -use std::mem::ManuallyDrop; -use std::num::NonZeroU32; - -use iced_native::widget::operation; -#[cfg(feature = "tracing")] -use tracing::{info_span, instrument::Instrument}; - -#[allow(unsafe_code)] -const ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; - -/// Runs an [`Application`] with an executor, compositor, and the provided -/// settings. -pub fn run( - settings: Settings, - compositor_settings: C::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: iced_graphics::window::GLCompositor + 'static, - ::Theme: StyleSheet, -{ - use futures::task; - use futures::Future; - use winit::event_loop::EventLoopBuilder; - use winit::platform::run_return::EventLoopExtRunReturn; - - #[cfg(feature = "trace")] - let _guard = iced_winit::Profiler::init(); - - let mut debug = Debug::new(); - debug.startup_started(); - - #[cfg(feature = "tracing")] - let _ = info_span!("Application::Glutin", "RUN").entered(); - - let mut event_loop = EventLoopBuilder::with_user_event().build(); - let proxy = event_loop.create_proxy(); - - let runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - let proxy = Proxy::new(event_loop.create_proxy()); - - Runtime::new(executor, proxy) - }; - - let (application, init_command) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - let builder = settings.window.into_builder( - &application.title(window::Id::MAIN), - event_loop.primary_monitor(), - settings.id, - ); - - log::info!("Window builder: {:#?}", builder); - - #[allow(unsafe_code)] - let (display, window, configuration, surface, context) = unsafe { - struct Configuration(Config); - use std::fmt; - impl fmt::Debug for Configuration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let config = &self.0; - - f.debug_struct("Configuration") - .field("raw", &config) - .field("samples", &config.num_samples()) - .field("buffer_type", &config.color_buffer_type()) - .field("surface_type", &config.config_surface_types()) - .field("depth", &config.depth_size()) - .field("alpha", &config.alpha_size()) - .field("stencil", &config.stencil_size()) - .field("float_pixels", &config.float_pixels()) - .field("srgb", &config.srgb_capable()) - .field("api", &config.api()) - .finish() - } - } - - impl AsRef for Configuration { - fn as_ref(&self) -> &Config { - &self.0 - } - } - - let display_handle = event_loop.raw_display_handle(); - - #[cfg(all( - any(windows, target_os = "macos"), - not(target_arch = "wasm32") - ))] - let (window, window_handle) = { - let window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; - - let handle = window.raw_window_handle(); - - (window, handle) - }; - - #[cfg(target_arch = "wasm32")] - let preference = Err(Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "target not supported by backend" - )), - ))?; - - #[cfg(all(windows, not(target_arch = "wasm32")))] - let preference = DisplayApiPreference::WglThenEgl(Some(window_handle)); - - #[cfg(all(target_os = "macos", not(target_arch = "wasm32")))] - let preference = DisplayApiPreference::Cgl; - - #[cfg(all( - unix, - not(target_os = "macos"), - not(target_arch = "wasm32") - ))] - let preference = DisplayApiPreference::GlxThenEgl(Box::new( - winit::platform::unix::register_xlib_error_hook, - )); - - let display = - Display::new(display_handle, preference).map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create display: {error}" - )), - ) - })?; - - log::debug!("Display: {}", display.version_string()); - - let samples = C::sample_count(&compositor_settings) as u8; - let mut template = ConfigTemplateBuilder::new() - .with_surface_type(ConfigSurfaceTypes::WINDOW); - - if samples != 0 { - template = template.with_multisampling(samples); - } - - #[cfg(all(windows, not(target_arch = "wasm32")))] - let template = template.compatible_with_native_window(window_handle); - - log::debug!("Searching for display configurations"); - let configuration = display - .find_configs(template.build()) - .map_err(|_| { - Error::GraphicsCreationFailed( - iced_graphics::Error::NoAvailablePixelFormat, - ) - })? - .map(Configuration) - .inspect(|config| { - log::trace!("{config:#?}"); - }) - .min_by_key(|config| { - config.as_ref().num_samples().saturating_sub(samples) - }) - .ok_or(Error::GraphicsCreationFailed( - iced_graphics::Error::NoAvailablePixelFormat, - ))?; - - log::debug!("Selected: {configuration:#?}"); - - #[cfg(all( - unix, - not(target_os = "macos"), - not(target_arch = "wasm32") - ))] - let (window, window_handle) = { - use glutin::platform::x11::X11GlConfigExt; - let builder = - if let Some(visual) = configuration.as_ref().x11_visual() { - use winit::platform::unix::WindowBuilderExtUnix; - builder.with_x11_visual(visual.into_raw()) - } else { - builder - }; - - let window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; - - let handle = window.raw_window_handle(); - - (window, handle) - }; - - let attributes = - ContextAttributesBuilder::new().build(Some(window_handle)); - let fallback_attributes = ContextAttributesBuilder::new() - .with_context_api(ContextApi::Gles(None)) - .build(Some(window_handle)); - - let context = display - .create_context(configuration.as_ref(), &attributes) - .or_else(|_| { - display.create_context( - configuration.as_ref(), - &fallback_attributes, - ) - }) - .map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create context: {error}" - )), - ) - })?; - - let surface = gl_surface(&display, configuration.as_ref(), &window) - .map_err(|error| { - Error::GraphicsCreationFailed( - iced_graphics::Error::BackendError(format!( - "failed to create surface: {error}" - )), - ) - })?; - - (display, window, configuration.0, surface, context) - }; - - let windows: HashMap = - HashMap::from([(window::Id::MAIN, window)]); - - // need to make context current before trying to load GL functions - let context = context - .make_current(&surface) - .expect("Make context current."); - - #[allow(unsafe_code)] - let (compositor, renderer) = unsafe { - C::new(compositor_settings, |address| { - let address = CString::new(address).expect("address error"); - display.get_proc_address(address.as_c_str()) - })? - }; - - let context = context.make_not_current().expect("Make not current."); - - let (mut sender, receiver) = mpsc::unbounded(); - - let mut instance = Box::pin({ - let run_instance = run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - receiver, - display, - windows, - configuration, - context, - init_command, - settings.exit_on_close_request, - ); - - #[cfg(feature = "tracing")] - let run_instance = - run_instance.instrument(info_span!("Application", "LOOP")); - - run_instance - }); - - let mut context = task::Context::from_waker(task::noop_waker_ref()); - - let _ = event_loop.run_return(move |event, event_loop, control_flow| { - use winit::event_loop::ControlFlow; - - if let ControlFlow::ExitWithCode(_) = control_flow { - return; - } - - let event = match event { - winit::event::Event::WindowEvent { - event: - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - }, - window_id, - } => Some(winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(*new_inner_size), - window_id, - }), - winit::event::Event::UserEvent(Event::NewWindow { - id, - settings, - title, - }) => { - let window = settings - .into_builder(&title, event_loop.primary_monitor(), None) - .build(event_loop) - .expect("Failed to build window"); - - Some(winit::event::Event::UserEvent(Event::WindowCreated( - id, window, - ))) - } - _ => event.to_static(), - }; - - if let Some(event) = event { - sender.start_send(event).expect("Send event"); - - let poll = instance.as_mut().poll(&mut context); - - *control_flow = match poll { - task::Poll::Pending => ControlFlow::Wait, - task::Poll::Ready(_) => ControlFlow::Exit, - }; - } - }); - - Ok(()) -} - -async fn run_instance( - mut application: A, - mut compositor: C, - mut renderer: A::Renderer, - mut runtime: Runtime>, Event>, - mut proxy: winit::event_loop::EventLoopProxy>, - mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver< - winit::event::Event<'_, Event>, - >, - display: Display, - mut windows: HashMap, - configuration: Config, - mut context: NotCurrentContext, - init_command: Command, - _exit_on_close_request: bool, -) where - A: Application + 'static, - E: Executor + 'static, - C: iced_graphics::window::GLCompositor + 'static, - ::Theme: StyleSheet, -{ - use iced_winit::futures::stream::StreamExt; - use winit::event; - - let mut clipboard = - Clipboard::connect(windows.values().next().expect("No window found")); - let mut caches = HashMap::new(); - let mut current_context_window = None; - let mut window_ids: HashMap<_, _> = windows - .iter() - .map(|(&id, window)| (window.id(), id)) - .collect(); - let mut states = HashMap::new(); - let mut surfaces = HashMap::new(); - let mut interfaces = ManuallyDrop::new(HashMap::new()); - - for (&id, window) in windows.keys().zip(windows.values()) { - let surface = gl_surface(&display, &configuration, &window) - .expect("Create surface."); - let current_context = - context.make_current(&surface).expect("Make current."); - let state = multi_window::State::new(&application, id, &window); - let physical_size = state.physical_size(); - - surface.resize( - ¤t_context, - NonZeroU32::new(physical_size.width).unwrap_or(ONE), - NonZeroU32::new(physical_size.height).unwrap_or(ONE), - ); - - let user_interface = multi_window::build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - id, - ); - - context = current_context - .make_not_current() - .expect("Make not current."); - - let _ = states.insert(id, state); - let _ = surfaces.insert(id, surface); - let _ = interfaces.insert(id, user_interface); - } - - run_command( - &application, - &mut caches, - &states, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &windows, - || compositor.fetch_information(), - ); - - runtime.track(application.subscription().map(Event::Application)); - - let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); - let mut messages = Vec::new(); - - debug.startup_finished(); - - 'main: while let Some(event) = receiver.next().await { - match event { - event::Event::MainEventsCleared => { - for id in windows.keys().copied() { - let (filtered, remaining): (Vec<_>, Vec<_>) = - events.iter().cloned().partition( - |(window_id, _event): &( - Option, - iced_native::event::Event, - )| { - *window_id == Some(id) || *window_id == None - }, - ); - - events.retain(|el| remaining.contains(el)); - let filtered: Vec<_> = filtered - .into_iter() - .map(|(_id, event)| event) - .collect(); - - let cursor_position = - states.get(&id).unwrap().cursor_position(); - let window = windows.get(&id).unwrap(); - - if filtered.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - - let (interface_state, statuses) = { - let user_interface = interfaces.get_mut(&id).unwrap(); - user_interface.update( - &filtered, - cursor_position, - &mut renderer, - &mut clipboard, - &mut messages, - ) - }; - - debug.event_processing_finished(); - - for event in filtered.into_iter().zip(statuses.into_iter()) - { - runtime.broadcast(event); - } - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let user_interfaces: HashMap<_, _> = - ManuallyDrop::into_inner(interfaces) - .drain() - .map(|(id, interface)| { - (id, interface.into_cache()) - }) - .collect(); - - // Update application - update( - &mut application, - &mut caches, - &states, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &mut messages, - &windows, - || compositor.fetch_information(), - ); - - // Update window - states.get_mut(&id).unwrap().synchronize( - &application, - id, - windows.get(&id).expect("No window found with ID."), - ); - - let should_exit = application.should_exit(); - - interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut renderer, - &mut debug, - &states, - user_interfaces, - )); - - if should_exit { - break 'main; - } - } - - debug.draw_started(); - let new_mouse_interaction = { - let user_interface = interfaces.get_mut(&id).unwrap(); - let state = states.get(&id).unwrap(); - - user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ) - }; - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - - window.request_redraw(); - } - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use iced_native::event; - events.push(( - None, - iced_native::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - ), - ), - )); - } - event::Event::UserEvent(event) => match event { - Event::Application(message) => messages.push(message), - Event::WindowCreated(id, window) => { - let state = - multi_window::State::new(&application, id, &window); - let user_interface = multi_window::build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - id, - ); - - let surface = gl_surface(&display, &configuration, &window) - .expect("Create surface."); - - let _ = states.insert(id, state); - let _ = interfaces.insert(id, user_interface); - let _ = window_ids.insert(window.id(), id); - let _ = windows.insert(id, window); - let _ = surfaces.insert(id, surface); - } - Event::CloseWindow(id) => { - // TODO(derezzedex): log errors - if let Some(window) = windows.get(&id) { - if window_ids.remove(&window.id()).is_none() { - println!("Failed to remove from `window_ids`!"); - } - } - if states.remove(&id).is_none() { - println!("Failed to remove from `states`!") - } - if interfaces.remove(&id).is_none() { - println!("Failed to remove from `interfaces`!"); - } - if surfaces.remove(&id).is_none() { - println!("Failed to remove from `surfaces`!") - } - if windows.remove(&id).is_none() { - println!("Failed to remove from `windows`!") - } - - if windows.is_empty() { - break 'main; - } - } - Event::NewWindow { .. } => unreachable!(), - }, - event::Event::RedrawRequested(id) => { - #[cfg(feature = "tracing")] - let _ = info_span!("Application", "FRAME").entered(); - - let state = window_ids - .get(&id) - .and_then(|id| states.get_mut(id)) - .unwrap(); - let window = - window_ids.get(&id).and_then(|id| windows.get(id)).unwrap(); - - let surface = window_ids - .get(&id) - .and_then(|id| surfaces.get(id)) - .unwrap(); - - debug.render_started(); - - let current_context = - context.make_current(&surface).expect("Make current."); - - if current_context_window != Some(id) { - current_context_window = Some(id); - } - - if state.viewport_changed() { - let physical_size = state.physical_size(); - let logical_size = state.logical_size(); - - let mut user_interface = window_ids - .get(&id) - .and_then(|id| interfaces.remove(id)) - .unwrap(); - - debug.layout_started(); - user_interface = - user_interface.relayout(logical_size, &mut renderer); - debug.layout_finished(); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - - surface.resize( - ¤t_context, - NonZeroU32::new(physical_size.width).unwrap_or(ONE), - NonZeroU32::new(physical_size.height).unwrap_or(ONE), - ); - - if let Err(_) = surface.set_swap_interval( - ¤t_context, - SwapInterval::Wait(ONE), - ) { - log::error!("Could not set swap interval for surface attached to window id: {:?}", id); - } - - compositor.resize_viewport(physical_size); - - let _ = interfaces - .insert(*window_ids.get(&id).unwrap(), user_interface); - } - - compositor.present( - &mut renderer, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - surface - .swap_buffers(¤t_context) - .expect("Swap buffers"); - - context = current_context - .make_not_current() - .expect("Make not current."); - debug.render_finished(); - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - event::Event::WindowEvent { - event: window_event, - window_id, - } => { - // dbg!(window_id); - if let Some(window) = - window_ids.get(&window_id).and_then(|id| windows.get(id)) - { - if let Some(state) = window_ids - .get(&window_id) - .and_then(|id| states.get_mut(id)) - { - if multi_window::requests_exit( - &window_event, - state.modifiers(), - ) { - if let Some(id) = - window_ids.get(&window_id).cloned() - { - let message = application.close_requested(id); - messages.push(message); - } - } - - state.update(window, &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - *window_ids.get(&window_id).unwrap(), - &window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(( - window_ids.get(&window_id).cloned(), - event, - )); - } - } else { - log::error!( - "Window state not found for id: {:?}", - window_id - ); - } - } else { - log::error!("Window not found for id: {:?}", window_id); - } - } - _ => {} - } - } - - // Manually drop the user interface - // drop(ManuallyDrop::into_inner(user_interface)); -} - -/// Updates an [`Application`] by feeding it the provided messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update( - application: &mut A, - caches: &mut HashMap, - states: &HashMap>, - renderer: &mut A::Renderer, - runtime: &mut Runtime>, Event>, - clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy>, - debug: &mut Debug, - messages: &mut Vec, - windows: &HashMap, - graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, -) where - A: Application + 'static, - ::Theme: StyleSheet, -{ - for message in messages.drain(..) { - debug.log_message(&message); - - debug.update_started(); - let command = runtime.enter(|| application.update(message)); - debug.update_finished(); - - run_command( - application, - caches, - &states, - renderer, - command, - runtime, - clipboard, - proxy, - debug, - windows, - graphics_info, - ); - } - - let subscription = application.subscription().map(Event::Application); - runtime.track(subscription); -} - -/// Runs the actions of a [`Command`]. -pub fn run_command( - application: &A, - caches: &mut HashMap, - states: &HashMap>, - renderer: &mut A::Renderer, - command: Command, - runtime: &mut Runtime>, Event>, - clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy>, - debug: &mut Debug, - windows: &HashMap, - _graphics_info: impl FnOnce() -> iced_graphics::compositor::Information + Copy, -) where - A: Application + 'static, - E: Executor, - ::Theme: StyleSheet, -{ - use iced_native::command; - use iced_native::system; - use iced_native::window; - use iced_winit::clipboard; - use iced_winit::futures::FutureExt; - - for action in command.actions() { - match action { - command::Action::Future(future) => { - runtime.spawn(Box::pin(future.map(Event::Application))); - } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag) => { - let message = tag(clipboard.read()); - - proxy - .send_event(Event::Application(message)) - .expect("Send message to event loop"); - } - clipboard::Action::Write(contents) => { - clipboard.write(contents); - } - }, - command::Action::Window(id, action) => match action { - window::Action::Spawn { settings } => { - proxy - .send_event(Event::NewWindow { - id, - settings: settings.into(), - title: application.title(id), - }) - .expect("Send message to event loop"); - } - window::Action::Close => { - proxy - .send_event(Event::CloseWindow(id)) - .expect("Send message to event loop"); - } - window::Action::Drag => { - let window = windows.get(&id).expect("No window found"); - let _res = window.drag_window(); - } - window::Action::Resize { width, height } => { - let window = windows.get(&id).expect("No window found"); - window.set_inner_size(winit::dpi::LogicalSize { - width, - height, - }); - } - window::Action::Move { x, y } => { - let window = windows.get(&id).expect("No window found"); - window.set_outer_position(winit::dpi::LogicalPosition { - x, - y, - }); - } - window::Action::ChangeMode(mode) => { - let window = windows.get(&id).expect("No window found"); - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.primary_monitor(), - mode, - )); - } - window::Action::FetchMode(tag) => { - let window = windows.get(&id).expect("No window found"); - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - window::Mode::Hidden - }; - - proxy - .send_event(Event::Application(tag(mode))) - .expect("Send message to event loop"); - } - window::Action::Maximize(value) => { - let window = windows.get(&id).expect("No window found!"); - window.set_maximized(value); - } - window::Action::Minimize(value) => { - let window = windows.get(&id).expect("No window found!"); - window.set_minimized(value); - } - window::Action::ToggleMaximize => { - let window = windows.get(&id).expect("No window found!"); - window.set_maximized(!window.is_maximized()); - } - window::Action::ToggleDecorations => { - let window = windows.get(&id).expect("No window found!"); - window.set_decorations(!window.is_decorated()); - } - window::Action::RequestUserAttention(attention_type) => { - let window = windows.get(&id).expect("No window found!"); - window.request_user_attention( - attention_type.map(conversion::user_attention), - ); - } - window::Action::GainFocus => { - let window = windows.get(&id).expect("No window found!"); - window.focus_window(); - } - window::Action::ChangeAlwaysOnTop(on_top) => { - let window = windows.get(&id).expect("No window found!"); - window.set_always_on_top(on_top); - } - window::Action::FetchId(tag) => { - let window = windows.get(&id).expect("No window found!"); - - proxy - .send_event(Event::Application(tag(window.id().into()))) - .expect("Send message to event loop.") - } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "iced_winit/system")] - { - let graphics_info = _graphics_info(); - let proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let message = _tag(information); - - proxy - .send_event(Event::Application(message)) - .expect("Send message to event loop") - }); - } - } - }, - command::Action::Widget(action) => { - let mut current_caches = std::mem::take(caches); - let mut current_operation = Some(action.into_operation()); - - let mut user_interfaces = multi_window::build_user_interfaces( - application, - renderer, - debug, - states, - current_caches, - ); - - while let Some(mut operation) = current_operation.take() { - for user_interface in user_interfaces.values_mut() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy - .send_event(Event::Application(message)) - .expect("Send message to event loop"); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - } - - let user_interfaces: HashMap<_, _> = user_interfaces - .drain() - .map(|(id, interface)| (id, interface.into_cache())) - .collect(); - - current_caches = user_interfaces; - *caches = current_caches; - } - } - } -} - -/// TODO(derezzedex) -pub fn build_user_interfaces<'a, A>( - application: &'a A, - renderer: &mut A::Renderer, - debug: &mut Debug, - states: &HashMap>, - mut user_interfaces: HashMap, -) -> HashMap< - window::Id, - iced_winit::UserInterface< - 'a, - ::Message, - ::Renderer, - >, -> -where - A: Application + 'static, - ::Theme: StyleSheet, -{ - let mut interfaces = HashMap::new(); - - for (id, pure_state) in user_interfaces.drain() { - let state = &states.get(&id).unwrap(); - - let user_interface = multi_window::build_user_interface( - application, - pure_state, - renderer, - state.logical_size(), - debug, - id, - ); - - let _ = interfaces.insert(id, user_interface); - } - - interfaces -} diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index e6ae2364..a45a7ca1 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -30,7 +30,7 @@ pub trait GLCompositor: Sized { /// The settings of the [`GLCompositor`]. /// /// It's up to you to decide the configuration supported by your renderer! - type Settings: Default + Clone; + type Settings: Default; /// Creates a new [`GLCompositor`] and [`Renderer`] with the given /// [`Settings`] and an OpenGL address loader function. diff --git a/native/src/window/id.rs b/native/src/window/id.rs index fa9761f5..0c3e5272 100644 --- a/native/src/window/id.rs +++ b/native/src/window/id.rs @@ -4,12 +4,10 @@ use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] /// The ID of the window. -/// -/// This is not necessarily the same as the window ID fetched from `winit::window::Window`. pub struct Id(u64); impl Id { - /// TODO(derezzedex): maybe change `u64` to an enum `Type::{Single, Multi(u64)}` + /// The reserved window ID for the primary window in an Iced application. pub const MAIN: Self = Id(0); /// Creates a new unique window ID. diff --git a/src/lib.rs b/src/lib.rs index 993e94b1..65fe3b93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,7 +182,7 @@ pub mod touch; pub mod widget; pub mod window; -#[cfg(feature = "multi_window")] +#[cfg(all(not(feature = "glow"), feature = "multi_window"))] pub mod multi_window; #[cfg(all(not(feature = "glow"), feature = "wgpu"))] diff --git a/winit/src/application.rs b/winit/src/application.rs index 58556da4..1310ba1c 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -734,7 +734,7 @@ pub fn run_command( clipboard.write(contents); } }, - command::Action::Window(_id, action) => match action { + command::Action::Window(_, action) => match action { window::Action::Close => { *should_exit = true; } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 1a9d4a1c..eac8b260 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1,4 +1,4 @@ -//! Create interactive, native cross-platform applications. +//! Create interactive, native cross-platform applications for WGPU. mod state; pub use state::State; @@ -31,17 +31,14 @@ pub use crate::Profiler; #[cfg(feature = "trace")] use tracing::{info_span, instrument::Instrument}; -/// TODO(derezzedex) -// This is the an wrapper around the `Application::Message` associate type -// to allows the `shell` to create internal messages, while still having -// the current user specified custom messages. +/// This is a wrapper around the `Application::Message` associate type +/// to allows the `shell` to create internal messages, while still having +/// the current user specified custom messages. #[derive(Debug)] pub enum Event { /// An [`Application`] generated message Application(Message), - /// TODO(derezzedex) - // Create a wrapper variant of `window::Event` type instead - // (maybe we should also allow users to listen/react to those internal messages?) + /// A message which spawns a new window. NewWindow { /// The [window::Id] of the newly spawned [`Window`]. id: window::Id, @@ -50,9 +47,9 @@ pub enum Event { /// The title of the newly spawned [`Window`]. title: String, }, - /// TODO(derezzedex) + /// Close a window. CloseWindow(window::Id), - /// TODO(derezzedex) + /// A message for when the window has finished being created. WindowCreated(window::Id, winit::window::Window), } @@ -90,7 +87,7 @@ where /// background by shells. fn update(&mut self, message: Self::Message) -> Command; - /// Returns the widgets to display in the [`Program`]. + /// Returns the widgets to display for the `window` in the [`Program`]. /// /// These widgets can produce __messages__ based on user interaction. fn view( @@ -108,7 +105,7 @@ where /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Command); - /// Returns the current title of the current [`Application`] window. + /// Returns the current title of each [`Application`] window. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. @@ -137,7 +134,7 @@ where Subscription::none() } - /// Returns the scale factor of the [`Application`]. + /// 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). @@ -1142,7 +1139,7 @@ pub fn run_command( } } -/// TODO(derezzedex) +/// Build the user interfaces for every window. pub fn build_user_interfaces<'a, A>( application: &'a A, renderer: &mut A::Renderer, diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 35c69924..a7e65de7 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -8,7 +8,7 @@ use std::marker::PhantomData; use winit::event::{Touch, WindowEvent}; use winit::window::Window; -/// The state of a windowed [`Application`]. +/// The state of a multi-windowed [`Application`]. #[allow(missing_debug_implementations)] pub struct State where @@ -29,7 +29,7 @@ impl State where ::Theme: application::StyleSheet, { - /// Creates a new [`State`] for the provided [`Application`]'s window. + /// Creates a new [`State`] for the provided [`Application`]'s `window`. pub fn new( application: &A, window_id: window::Id, @@ -116,8 +116,7 @@ where self.appearance.text_color } - /// Processes the provided window event and updates the [`State`] - /// accordingly. + /// Processes the provided window event and updates the [`State`] accordingly. pub fn update( &mut self, window: &Window, diff --git a/winit/src/profiler.rs b/winit/src/profiler.rs index ff9bbdc0..7031507a 100644 --- a/winit/src/profiler.rs +++ b/winit/src/profiler.rs @@ -21,7 +21,6 @@ pub struct Profiler { impl Profiler { /// Initializes the [`Profiler`]. pub fn init() -> Self { - log::info!("Capturing trace.."); // Registry stores the spans & generates unique span IDs let subscriber = Registry::default(); diff --git a/winit/src/settings.rs b/winit/src/settings.rs index b26de542..88d7c1de 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -179,9 +179,9 @@ impl Window { { use winit::platform::windows::WindowBuilderExtWindows; - // if let Some(parent) = self.platform_specific.parent { - // window_builder = window_builder.with_parent_window(parent); - // } + if let Some(parent) = self.platform_specific.parent { + window_builder = window_builder.with_parent_window(parent); + } window_builder = window_builder .with_drag_and_drop(self.platform_specific.drag_and_drop); @@ -227,7 +227,7 @@ impl From for Window { fn from(settings: iced_native::window::Settings) -> Self { Self { size: settings.size, - position: Position::from(settings.position), + position: settings.position, min_size: settings.min_size, max_size: settings.max_size, visible: settings.visible, diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs index 0891ec2c..ff03a9c5 100644 --- a/winit/src/settings/windows.rs +++ b/winit/src/settings/windows.rs @@ -4,7 +4,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PlatformSpecific { /// Parent window - // pub parent: Option, + pub parent: Option, /// Drag and drop support pub drag_and_drop: bool, @@ -13,7 +13,7 @@ pub struct PlatformSpecific { impl Default for PlatformSpecific { fn default() -> Self { Self { - // parent: None, + parent: None, drag_and_drop: true, } } -- cgit From 8ba18430800142965549077373e2a45d0a3429a1 Mon Sep 17 00:00:00 2001 From: Bingus Date: Mon, 13 Mar 2023 14:16:45 -0700 Subject: Code cleanup, clearer comments + removed some unnecessary dupe; Removed `Frames` struct return for `window::frames()` since we are just redrawing every window anyways; Interface dropping; --- Cargo.toml | 4 +- examples/multi_window/Cargo.toml | 2 +- examples/solar_system/src/main.rs | 2 +- native/src/window.rs | 15 +---- src/lib.rs | 2 +- src/multi_window/application.rs | 5 +- winit/Cargo.toml | 2 +- winit/src/application.rs | 2 +- winit/src/lib.rs | 2 +- winit/src/multi_window.rs | 126 ++++++++++++++------------------------ winit/src/multi_window/state.rs | 2 +- 11 files changed, 60 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5276d921..7f89b05e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,8 +46,8 @@ chrome-trace = [ "iced_wgpu?/tracing", "iced_glow?/tracing", ] -# Enables experimental multi-window support for iced_winit -multi_window = ["iced_winit/multi_window"] +# Enables experimental multi-window support for iced_winit + wgpu. +multi-window = ["iced_winit/multi-window"] [badges] maintenance = { status = "actively-developed" } diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 0bb83f37..a59a0e68 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -7,7 +7,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -iced = { path = "../..", features = ["debug", "multi_window", "tokio"] } +iced = { path = "../..", features = ["debug", "multi-window", "tokio"] } env_logger = "0.10.0" iced_native = { path = "../../native" } iced_lazy = { path = "../../lazy" } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index eb461bb0..9a4ee754 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -89,7 +89,7 @@ impl Application for SolarSystem { } fn subscription(&self) -> Subscription { - window::frames().map(|frame| Message::Tick(frame.at)) + window::frames().map(Message::Tick) } } diff --git a/native/src/window.rs b/native/src/window.rs index a8f8b10f..660cd54f 100644 --- a/native/src/window.rs +++ b/native/src/window.rs @@ -30,20 +30,9 @@ use crate::time::Instant; /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. -pub fn frames() -> Subscription { +pub fn frames() -> Subscription { subscription::raw_events(|event, _status| match event { - crate::Event::Window(id, Event::RedrawRequested(at)) => { - Some(Frame { id, at }) - } + crate::Event::Window(_, Event::RedrawRequested(at)) => Some(at), _ => None, }) } - -/// The returned `Frame` for a framerate subscription. -#[derive(Debug)] -pub struct Frame { - /// The `window::Id` that the `Frame` was produced in. - pub id: Id, - /// The `Instant` at which the frame was produced. - pub at: Instant, -} diff --git a/src/lib.rs b/src/lib.rs index 65fe3b93..e7481c77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,7 +182,7 @@ pub mod touch; pub mod widget; pub mod window; -#[cfg(all(not(feature = "glow"), feature = "multi_window"))] +#[cfg(all(not(feature = "glow"), feature = "multi-window"))] pub mod multi_window; #[cfg(all(not(feature = "glow"), feature = "wgpu"))] diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index d0b515ab..1fb4bcd4 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -139,7 +139,7 @@ pub trait Application: Sized { window: window::Id, ) -> Element<'_, Self::Message, crate::Renderer>; - /// Returns the scale factor of the [`Application`]. + /// 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). @@ -160,7 +160,8 @@ pub trait Application: Sized { false } - /// Requests that the [`window`] be closed. + /// Returns the `Self::Message` that should be processed when a `window` is requested to + /// be closed. fn close_requested(&self, window: window::Id) -> Self::Message; /// Runs the [`Application`]. diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 7de69528..9efd1890 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -16,7 +16,7 @@ chrome-trace = ["trace", "tracing-chrome"] debug = ["iced_native/debug"] system = ["sysinfo"] application = [] -multi_window = [] +multi-window = [] [dependencies] window_clipboard = "0.2" diff --git a/winit/src/application.rs b/winit/src/application.rs index 906da3c6..fe97486f 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -743,7 +743,7 @@ pub fn run_command( } window::Action::Spawn { .. } => { log::info!( - "This is only available on `multi_window::Application`" + "Spawning a window is only available with `multi_window::Application`s." ) } window::Action::Resize { width, height } => { diff --git a/winit/src/lib.rs b/winit/src/lib.rs index fe5768df..54b9c31f 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -35,7 +35,7 @@ pub use iced_native::*; pub use winit; -#[cfg(feature = "multi_window")] +#[cfg(feature = "multi-window")] pub mod multi_window; #[cfg(feature = "application")] diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index eac8b260..d5da406c 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -33,7 +33,7 @@ use tracing::{info_span, instrument::Instrument}; /// This is a wrapper around the `Application::Message` associate type /// to allows the `shell` to create internal messages, while still having -/// the current user specified custom messages. +/// the current user-specified custom messages. #[derive(Debug)] pub enum Event { /// An [`Application`] generated message @@ -53,9 +53,9 @@ pub enum Event { WindowCreated(window::Id, winit::window::Window), } -/// An interactive, native cross-platform application. +/// An interactive, native, cross-platform, multi-windowed application. /// -/// This trait is the main entrypoint of Iced. Once implemented, you can run +/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run /// your GUI application by simply calling [`run`]. It will run in /// its own window. /// @@ -105,7 +105,7 @@ where /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Command); - /// Returns the current title of each [`Application`] window. + /// Returns the current title of each window of the [`Application`]. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. @@ -155,7 +155,8 @@ where false } - /// Requests that the [`window`] be closed. + /// Returns the `Self::Message` that should be processed when a `window` is requested to + /// be closed. fn close_requested(&self, window: window::Id) -> Self::Message; } @@ -462,9 +463,9 @@ async fn run_instance( } debug.event_processing_finished(); - // Update application with app message(s) - // Note: without tying an app message to a window ID, we must redraw all windows - // as we cannot know what changed without some kind of damage tracking. + // Update application with app messages + // Unless we implement some kind of diffing, we must redraw all windows as we + // cannot know what changed. if !messages.is_empty() || matches!( interface_state, @@ -612,9 +613,7 @@ async fn run_instance( } Event::WindowCreated(id, window) => { let mut surface = compositor.create_surface(&window); - let state = State::new(&application, id, &window); - let physical_size = state.physical_size(); compositor.configure_surface( @@ -776,14 +775,12 @@ async fn run_instance( } _ => { debug.render_finished(); + log::error!("Error {error:?} when presenting surface."); - // Try rendering again next frame. - // TODO(derezzedex) - windows - .values() - .next() - .expect("No window found") - .request_redraw(); + // Try rendering windows again next frame. + for window in windows.values() { + window.request_redraw(); + } } }, } @@ -792,80 +789,45 @@ async fn run_instance( event: window_event, window_id, } => { - // dbg!(window_id); - if let Some(window) = - window_ids.get(&window_id).and_then(|id| windows.get(id)) - { - if let Some(state) = window_ids + if let (Some(window), Some(state)) = ( + window_ids.get(&window_id).and_then(|id| windows.get(id)), + window_ids .get(&window_id) - .and_then(|id| states.get_mut(id)) - { - if requests_exit(&window_event, state.modifiers()) { - if let Some(id) = - window_ids.get(&window_id).cloned() - { - let message = application.close_requested(id); - messages.push(message); - } + .and_then(|id| states.get_mut(id)), + ) { + if crate::application::requests_exit(&window_event, state.modifiers()) { + if let Some(id) = window_ids.get(&window_id).cloned() { + let message = application.close_requested(id); + messages.push(message); } + } - state.update(window, &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - *window_ids.get(&window_id).unwrap(), - &window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(( - window_ids.get(&window_id).cloned(), - event, - )); - } - } else { - log::error!( - "No window state found for id: {:?}", - window_id - ); + state.update(window, &window_event, &mut debug); + + if let Some(event) = conversion::window_event( + *window_ids.get(&window_id).unwrap(), + &window_event, + state.scale_factor(), + state.modifiers(), + ) { + events + .push((window_ids.get(&window_id).cloned(), event)); } } else { - log::error!("No window found with id: {:?}", window_id); + log::error!( + "Could not find window or state for id: {window_id:?}" + ); } } _ => {} } } - // Manually drop the user interface - // drop(ManuallyDrop::into_inner(user_interface)); -} - -/// Returns true if the provided event should cause an [`Application`] to -/// exit. -pub fn requests_exit( - event: &winit::event::WindowEvent<'_>, - _modifiers: winit::event::ModifiersState, -) -> bool { - use winit::event::WindowEvent; - - match event { - WindowEvent::CloseRequested => true, - #[cfg(target_os = "macos")] - WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::Q), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if _modifiers.logo() => true, - _ => false, - } + // Manually drop the user interfaces + drop(ManuallyDrop::into_inner(interfaces)); } -/// Builds a [`UserInterface`] for the provided [`Application`], logging -/// [`struct@Debug`] information accordingly. +/// Builds a window's [`UserInterface`] for the [`Application`]. pub fn build_user_interface<'a, A: Application>( application: &'a A, cache: user_interface::Cache, @@ -890,7 +852,9 @@ where #[cfg(feature = "trace")] let layout_span = info_span!("Application", "LAYOUT").entered(); debug.layout_started(); + let user_interface = UserInterface::build(view, size, cache, renderer); + #[cfg(feature = "trace")] let _ = layout_span.exit(); debug.layout_finished(); @@ -898,7 +862,7 @@ where user_interface } -/// Updates an [`Application`] by feeding it the provided messages, spawning any +/// Updates an [`Application`] by feeding it messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. pub fn update( application: &mut A, @@ -923,7 +887,9 @@ pub fn update( debug.log_message(&message); debug.update_started(); + let command = runtime.enter(|| application.update(message)); + #[cfg(feature = "trace")] let _ = update_span.exit(); debug.update_finished(); @@ -1023,7 +989,7 @@ pub fn run_command( let window = windows.get(&id).expect("No window found"); window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( - window.primary_monitor(), + window.current_monitor(), mode, )); } diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index a7e65de7..d0e442d0 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -179,7 +179,7 @@ where /// Synchronizes the [`State`] with its [`Application`] and its respective /// window. /// - /// Normally an [`Application`] should be synchronized with its [`State`] + /// Normally, an [`Application`] should be synchronized with its [`State`] /// and window after calling [`Application::update`]. /// /// [`Application::update`]: crate::Program::update -- cgit From ce4b2c93d9802dfb8cd3fc9033d76651d4bbc75b Mon Sep 17 00:00:00 2001 From: Bingus Date: Mon, 13 Mar 2023 18:19:16 -0700 Subject: Added simpler MW example --- examples/multi_window/Cargo.toml | 8 +- examples/multi_window/src/main.rs | 654 +++++--------------------------- examples/multi_window_panes/Cargo.toml | 12 + examples/multi_window_panes/src/main.rs | 624 ++++++++++++++++++++++++++++++ native/src/window/id.rs | 2 + winit/src/multi_window.rs | 2 +- 6 files changed, 742 insertions(+), 560 deletions(-) create mode 100644 examples/multi_window_panes/Cargo.toml create mode 100644 examples/multi_window_panes/src/main.rs diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index a59a0e68..0cb5d546 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -1,13 +1,9 @@ [package] name = "multi_window" version = "0.1.0" -authors = ["Richard Custodio "] +authors = ["Bingus "] edition = "2021" publish = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -iced = { path = "../..", features = ["debug", "multi-window", "tokio"] } -env_logger = "0.10.0" -iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "multi-window"] } \ No newline at end of file diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 60f32a7d..5699ece0 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,90 +1,50 @@ -use iced::alignment::{self, Alignment}; -use iced::{executor, time}; -use iced::keyboard; -use iced::multi_window::Application; -use iced::theme::{self, Theme}; -use iced::widget::pane_grid::{self, PaneGrid}; -use iced::widget::{ - button, column, container, pick_list, row, scrollable, text, text_input, +use iced::multi_window::{self, Application}; +use iced::widget::{button, column, container, scrollable, text, text_input}; +use iced::{ + executor, window, Alignment, Command, Element, Length, Settings, Theme, }; -use iced::window; -use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; -use iced_lazy::responsive; -use iced_native::{event, subscription, Event}; - -use iced_native::widget::scrollable::{Properties, RelativeOffset}; -use iced_native::window::Id; use std::collections::HashMap; -use std::time::{Duration, Instant}; - -pub fn main() -> iced::Result { - env_logger::init(); +fn main() -> iced::Result { Example::run(Settings::default()) } +#[derive(Default)] struct Example { + windows_count: usize, windows: HashMap, - panes_created: usize, - count: usize, - _focused: window::Id, } -#[derive(Debug)] struct Window { + id: window::Id, title: String, - scale: f64, - panes: pane_grid::State, - focus: Option, + scale_input: String, + current_scale: f64, } #[derive(Debug, Clone)] enum Message { - Window(window::Id, WindowMessage), - CountIncremented(Instant), -} - -#[derive(Debug, Clone)] -enum WindowMessage { - Split(pane_grid::Axis, pane_grid::Pane), - SplitFocused(pane_grid::Axis), - FocusAdjacent(pane_grid::Direction), - Clicked(pane_grid::Pane), - Dragged(pane_grid::DragEvent), - PopOut(pane_grid::Pane), - Resized(pane_grid::ResizeEvent), - TitleChanged(String), - ToggleMoving(pane_grid::Pane), - TogglePin(pane_grid::Pane), - Close(pane_grid::Pane), - CloseFocused, - SelectedWindow(pane_grid::Pane, SelectableWindow), - CloseWindow, - SnapToggle, + ScaleInputChanged(window::Id, String), + ScaleChanged(window::Id, String), + TitleChanged(window::Id, String), + CloseWindow(window::Id), + NewWindow, } -impl Application for Example { +impl multi_window::Application for Example { type Executor = executor::Default; type Message = Message; type Theme = Theme; type Flags = (); fn new(_flags: ()) -> (Self, Command) { - let (panes, _) = - pane_grid::State::new(Pane::new(0, pane_grid::Axis::Horizontal)); - let window = Window { - panes, - focus: None, - title: String::from("Default window"), - scale: 1.0, - }; - ( Example { - windows: HashMap::from([(window::Id::MAIN, window)]), - panes_created: 1, - count: 0, - _focused: window::Id::MAIN, + windows_count: 0, + windows: HashMap::from([( + window::Id::MAIN, + Window::new(window::Id::MAIN), + )]), }, Command::none(), ) @@ -93,530 +53,118 @@ impl Application for Example { fn title(&self, window: window::Id) -> String { self.windows .get(&window) - .map(|w| w.title.clone()) - .unwrap_or(String::from("New Window")) + .map(|window| window.title.clone()) + .unwrap_or("Example".to_string()) } fn update(&mut self, message: Message) -> Command { match message { - Message::Window(id, message) => match message { - WindowMessage::SnapToggle => { - let window = self.windows.get_mut(&id).unwrap(); - - if let Some(focused) = &window.focus { - let pane = window.panes.get_mut(focused).unwrap(); - - let cmd = scrollable::snap_to( - pane.scrollable_id.clone(), - if pane.snapped { - RelativeOffset::START - } else { - RelativeOffset::END - }, - ); - - pane.snapped = !pane.snapped; - return cmd; - } - } - WindowMessage::Split(axis, pane) => { - let window = self.windows.get_mut(&id).unwrap(); - let result = window.panes.split( - axis, - &pane, - Pane::new(self.panes_created, axis), - ); - - if let Some((pane, _)) = result { - window.focus = Some(pane); - } - - self.panes_created += 1; - } - WindowMessage::SplitFocused(axis) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - let result = window.panes.split( - axis, - &pane, - Pane::new(self.panes_created, axis), - ); - - if let Some((pane, _)) = result { - window.focus = Some(pane); - } - - self.panes_created += 1; - } - } - WindowMessage::FocusAdjacent(direction) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - if let Some(adjacent) = - window.panes.adjacent(&pane, direction) - { - window.focus = Some(adjacent); - } - } - } - WindowMessage::Clicked(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - window.focus = Some(pane); - } - WindowMessage::CloseWindow => { - let _ = self.windows.remove(&id); - return window::close(id); - } - WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { - let window = self.windows.get_mut(&id).unwrap(); - window.panes.resize(&split, ratio); - } - WindowMessage::SelectedWindow(pane, selected) => { - let window = self.windows.get_mut(&id).unwrap(); - let (mut pane, _) = window.panes.close(&pane).unwrap(); - pane.is_moving = false; - - if let Some(window) = self.windows.get_mut(&selected.0) { - let (&first_pane, _) = window.panes.iter().next().unwrap(); - let result = - window.panes.split(pane.axis, &first_pane, pane); - - if let Some((pane, _)) = result { - window.focus = Some(pane); - } - } - } - WindowMessage::ToggleMoving(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.panes.get_mut(&pane) { - pane.is_moving = !pane.is_moving; - } - } - WindowMessage::TitleChanged(title) => { - let window = self.windows.get_mut(&id).unwrap(); - window.title = title; - } - WindowMessage::PopOut(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some((popped, sibling)) = window.panes.close(&pane) { - window.focus = Some(sibling); + Message::ScaleInputChanged(id, scale) => { + let window = + self.windows.get_mut(&id).expect("Window not found!"); + window.scale_input = scale; + } + Message::ScaleChanged(id, scale) => { + let window = + self.windows.get_mut(&id).expect("Window not found!"); + + window.current_scale = scale + .parse::() + .unwrap_or(window.current_scale) + .clamp(0.5, 5.0); + } + Message::TitleChanged(id, title) => { + let window = + self.windows.get_mut(&id).expect("Window not found."); - let (panes, _) = pane_grid::State::new(popped); - let window = Window { - panes, - focus: None, - title: format!("New window ({})", self.windows.len()), - scale: 1.0 + (self.windows.len() as f64 / 10.0), - }; + window.title = title; + } + Message::CloseWindow(id) => { + return window::close(id); + } + Message::NewWindow => { + self.windows_count += 1; + let id = window::Id::new(self.windows_count); + self.windows.insert(id, Window::new(id)); - let window_id = window::Id::new(self.windows.len()); - self.windows.insert(window_id, window); - return window::spawn(window_id, Default::default()); - } - } - WindowMessage::Dragged(pane_grid::DragEvent::Dropped { - pane, - target, - }) => { - let window = self.windows.get_mut(&id).unwrap(); - window.panes.swap(&pane, &target); - } - // WindowMessage::Dragged(pane_grid::DragEvent::Picked { pane }) => { - // println!("Picked {pane:?}"); - // } - WindowMessage::Dragged(_) => {} - WindowMessage::TogglePin(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(Pane { is_pinned, .. }) = - window.panes.get_mut(&pane) - { - *is_pinned = !*is_pinned; - } - } - WindowMessage::Close(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some((_, sibling)) = window.panes.close(&pane) { - window.focus = Some(sibling); - } - } - WindowMessage::CloseFocused => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - if let Some(Pane { is_pinned, .. }) = - window.panes.get(&pane) - { - if !is_pinned { - if let Some((_, sibling)) = - window.panes.close(&pane) - { - window.focus = Some(sibling); - } - } - } - } - } - }, - Message::CountIncremented(_) => { - self.count += 1; + return window::spawn(id, window::Settings::default()); } } Command::none() } - fn subscription(&self) -> Subscription { - Subscription::batch(vec![ - subscription::events_with(|event, status| { - if let event::Status::Captured = status { - return None; - } - - match event { - Event::Keyboard(keyboard::Event::KeyPressed { - modifiers, - key_code, - }) if modifiers.command() => { - handle_hotkey(key_code).map(|message| { - Message::Window(window::Id::new(0usize), message) - }) - } // TODO(derezzedex) - _ => None, - } - }), - time::every(Duration::from_secs(1)).map(Message::CountIncremented), - ]) - } - - fn view(&self, window_id: window::Id) -> Element { - if let Some(window) = self.windows.get(&window_id) { - let focus = window.focus; - let total_panes = window.panes.len(); - - let window_controls = row![ - text_input( - "Window title", - &window.title, - WindowMessage::TitleChanged, - ), - button(text("Close")) - .on_press(WindowMessage::CloseWindow) - .style(theme::Button::Destructive), - ] - .spacing(5) - .align_items(Alignment::Center); - - let pane_grid = PaneGrid::new(&window.panes, |id, pane, _| { - let is_focused = focus == Some(id); - - let pin_button = button( - text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), - ) - .on_press(WindowMessage::TogglePin(id)) - .padding(3); - - let title = row![ - pin_button, - "Pane", - text(pane.id.to_string()).style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }), - ] - .spacing(5); - - let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls( - id, - total_panes, - pane.is_pinned, - pane.is_moving, - &window.title, - window_id, - &self.windows, - )) - .padding(10) - .style(if is_focused { - style::title_bar_focused - } else { - style::title_bar_active - }); + fn view(&self, window: window::Id) -> Element { + let window = self + .windows + .get(&window) + .map(|window| window.view()) + .unwrap(); - pane_grid::Content::new(responsive(move |size| { - view_content( - id, - pane.scrollable_id.clone(), - self.count, - total_panes, - pane.is_pinned, - size, - ) - })) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) - }) + container(window) .width(Length::Fill) .height(Length::Fill) - .spacing(10) - .on_click(WindowMessage::Clicked) - .on_drag(WindowMessage::Dragged) - .on_resize(10, WindowMessage::Resized); - - let content: Element<_> = column![window_controls, pane_grid] - .width(Length::Fill) - .height(Length::Fill) - .padding(10) - .into(); - - return content - .map(move |message| Message::Window(window_id, message)); - } - - container(text("This shouldn't be possible!").size(20)) .center_x() .center_y() .into() } - fn close_requested(&self, window: window::Id) -> Self::Message { - Message::Window(window, WindowMessage::CloseWindow) - } - - fn scale_factor(&self, window: Id) -> f64 { - self.windows.get(&window).map(|w| w.scale).unwrap_or(1.0) - } -} - -const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( - 0xFF as f32 / 255.0, - 0xC7 as f32 / 255.0, - 0xC7 as f32 / 255.0, -); -const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( - 0xFF as f32 / 255.0, - 0x47 as f32 / 255.0, - 0x47 as f32 / 255.0, -); - -fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { - use keyboard::KeyCode; - use pane_grid::{Axis, Direction}; - - let direction = match key_code { - KeyCode::Up => Some(Direction::Up), - KeyCode::Down => Some(Direction::Down), - KeyCode::Left => Some(Direction::Left), - KeyCode::Right => Some(Direction::Right), - _ => None, - }; - - match key_code { - KeyCode::V => Some(WindowMessage::SplitFocused(Axis::Vertical)), - KeyCode::H => Some(WindowMessage::SplitFocused(Axis::Horizontal)), - KeyCode::W => Some(WindowMessage::CloseFocused), - _ => direction.map(WindowMessage::FocusAdjacent), - } -} - -#[derive(Debug, Clone)] -struct SelectableWindow(window::Id, String); - -impl PartialEq for SelectableWindow { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 + fn scale_factor(&self, window: window::Id) -> f64 { + self.windows + .get(&window) + .map(|window| window.current_scale) + .unwrap_or(1.0) } -} - -impl Eq for SelectableWindow {} -impl std::fmt::Display for SelectableWindow { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.1.fmt(f) + fn close_requested(&self, window: window::Id) -> Self::Message { + Message::CloseWindow(window) } } -#[derive(Debug)] -struct Pane { - id: usize, - pub scrollable_id: scrollable::Id, - pub axis: pane_grid::Axis, - pub is_pinned: bool, - pub is_moving: bool, - pub snapped: bool, -} - -impl Pane { - fn new(id: usize, axis: pane_grid::Axis) -> Self { +impl Window { + fn new(id: window::Id) -> Self { Self { id, - scrollable_id: scrollable::Id::unique(), - axis, - is_pinned: false, - is_moving: false, - snapped: false, + title: "Window".to_string(), + scale_input: "1.0".to_string(), + current_scale: 1.0, } } -} - -fn view_content<'a>( - pane: pane_grid::Pane, - scrollable_id: scrollable::Id, - count: usize, - total_panes: usize, - is_pinned: bool, - size: Size, -) -> Element<'a, WindowMessage> { - let button = |label, message| { - button( - text(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - }; - let mut controls = column![ - button( - "Split horizontally", - WindowMessage::Split(pane_grid::Axis::Horizontal, pane), - ), - button( - "Split vertically", - WindowMessage::Split(pane_grid::Axis::Vertical, pane), - ), - button("Snap", WindowMessage::SnapToggle,) - ] - .spacing(5) - .max_width(150); - - if total_panes > 1 && !is_pinned { - controls = controls.push( - button("Close", WindowMessage::Close(pane)) - .style(theme::Button::Destructive), - ); + fn view(&self) -> Element { + window_view(self.id, &self.scale_input, &self.title) } - - let content = column![ - text(format!("{}x{}", size.width, size.height)).size(24), - controls, - text(format!("{count}")).size(48), - ] - .width(Length::Fill) - .height(800) - .spacing(10) - .align_items(Alignment::Center); - - container( - scrollable(content) - .height(Length::Fill) - .vertical_scroll(Properties::new()) - .id(scrollable_id), - ) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y() - .into() } -fn view_controls<'a>( - pane: pane_grid::Pane, - total_panes: usize, - is_pinned: bool, - is_moving: bool, - window_title: &'a str, - window_id: window::Id, - windows: &HashMap, -) -> Element<'a, WindowMessage> { - let window_selector = { - let options: Vec<_> = windows - .iter() - .map(|(id, window)| SelectableWindow(*id, window.title.clone())) - .collect(); - pick_list( - options, - Some(SelectableWindow(window_id, window_title.to_string())), - move |window| WindowMessage::SelectedWindow(pane, window), - ) - }; - - let mut move_to = button(text("Move to").size(14)).padding(3); - - let mut pop_out = button(text("Pop Out").size(14)).padding(3); - - let mut close = button(text("Close").size(14)) - .style(theme::Button::Destructive) - .padding(3); - - if total_panes > 1 && !is_pinned { - close = close.on_press(WindowMessage::Close(pane)); - pop_out = pop_out.on_press(WindowMessage::PopOut(pane)); - } - - if windows.len() > 1 && total_panes > 1 && !is_pinned { - move_to = move_to.on_press(WindowMessage::ToggleMoving(pane)); - } - - let mut content = row![].spacing(10); - if is_moving { - content = content.push(pop_out).push(window_selector).push(close); - } else { - content = content.push(pop_out).push(move_to).push(close); - } - - content.into() -} - -mod style { - use iced::widget::container; - use iced::Theme; - - pub fn title_bar_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), - ..Default::default() - } - } - - pub fn title_bar_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.primary.strong.text), - background: Some(palette.primary.strong.color.into()), - ..Default::default() - } - } - - pub fn pane_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.background.strong.color, - ..Default::default() - } - } - - pub fn pane_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); +fn window_view<'a>( + id: window::Id, + scale_input: &'a str, + title: &'a str, +) -> Element<'a, Message> { + let scale_input = column![ + text("Window scale factor:"), + text_input("Window Scale", scale_input, move |msg| { + Message::ScaleInputChanged(id, msg) + }) + .on_submit(Message::ScaleChanged(id, scale_input.to_string())) + ]; + + let title_input = column![ + text("Window title:"), + text_input("Window Title", title, move |msg| { + Message::TitleChanged(id, msg) + }) + ]; + + 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::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.primary.strong.color, - ..Default::default() - } - } + container(content).width(200).center_x().into() } diff --git a/examples/multi_window_panes/Cargo.toml b/examples/multi_window_panes/Cargo.toml new file mode 100644 index 00000000..1b3f3ec6 --- /dev/null +++ b/examples/multi_window_panes/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "multi_window_panes" +version = "0.1.0" +authors = ["Richard Custodio "] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["debug", "multi-window", "tokio"] } +env_logger = "0.10.0" +iced_native = { path = "../../native" } +iced_lazy = { path = "../../lazy" } diff --git a/examples/multi_window_panes/src/main.rs b/examples/multi_window_panes/src/main.rs new file mode 100644 index 00000000..b8b63769 --- /dev/null +++ b/examples/multi_window_panes/src/main.rs @@ -0,0 +1,624 @@ +use iced::alignment::{self, Alignment}; +use iced::{executor, time}; +use iced::keyboard; +use iced::multi_window::Application; +use iced::theme::{self, Theme}; +use iced::widget::pane_grid::{self, PaneGrid}; +use iced::widget::{ + button, column, container, pick_list, row, scrollable, text, text_input, +}; +use iced::window; +use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; +use iced_lazy::responsive; +use iced_native::{event, subscription, Event}; + +use iced_native::widget::scrollable::{Properties, RelativeOffset}; +use iced_native::window::Id; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +pub fn main() -> iced::Result { + env_logger::init(); + + Example::run(Settings::default()) +} + +struct Example { + windows: HashMap, + panes_created: usize, + count: usize, + _focused: window::Id, +} + +#[derive(Debug)] +struct Window { + title: String, + scale: f64, + panes: pane_grid::State, + focus: Option, +} + +#[derive(Debug, Clone)] +enum Message { + Window(window::Id, WindowMessage), + CountIncremented(Instant), +} + +#[derive(Debug, Clone)] +enum WindowMessage { + Split(pane_grid::Axis, pane_grid::Pane), + SplitFocused(pane_grid::Axis), + FocusAdjacent(pane_grid::Direction), + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + PopOut(pane_grid::Pane), + Resized(pane_grid::ResizeEvent), + TitleChanged(String), + ToggleMoving(pane_grid::Pane), + TogglePin(pane_grid::Pane), + Close(pane_grid::Pane), + CloseFocused, + SelectedWindow(pane_grid::Pane, SelectableWindow), + CloseWindow, + SnapToggle, +} + +impl Application for Example { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: ()) -> (Self, Command) { + let (panes, _) = + pane_grid::State::new(Pane::new(0, pane_grid::Axis::Horizontal)); + let window = Window { + panes, + focus: None, + title: String::from("Default window"), + scale: 1.0, + }; + + ( + Example { + windows: HashMap::from([(window::Id::MAIN, window)]), + panes_created: 1, + count: 0, + _focused: window::Id::MAIN, + }, + Command::none(), + ) + } + + fn title(&self, window: window::Id) -> String { + self.windows + .get(&window) + .map(|w| w.title.clone()) + .unwrap_or(String::from("New Window")) + } + + fn update(&mut self, message: Message) -> Command { + match message { + Message::Window(id, message) => match message { + WindowMessage::SnapToggle => { + let window = self.windows.get_mut(&id).unwrap(); + + if let Some(focused) = &window.focus { + let pane = window.panes.get_mut(focused).unwrap(); + + let cmd = scrollable::snap_to( + pane.scrollable_id.clone(), + if pane.snapped { + RelativeOffset::START + } else { + RelativeOffset::END + }, + ); + + pane.snapped = !pane.snapped; + return cmd; + } + } + WindowMessage::Split(axis, pane) => { + let window = self.windows.get_mut(&id).unwrap(); + let result = window.panes.split( + axis, + &pane, + Pane::new(self.panes_created, axis), + ); + + if let Some((pane, _)) = result { + window.focus = Some(pane); + } + + self.panes_created += 1; + } + WindowMessage::SplitFocused(axis) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + let result = window.panes.split( + axis, + &pane, + Pane::new(self.panes_created, axis), + ); + + if let Some((pane, _)) = result { + window.focus = Some(pane); + } + + self.panes_created += 1; + } + } + WindowMessage::FocusAdjacent(direction) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + if let Some(adjacent) = + window.panes.adjacent(&pane, direction) + { + window.focus = Some(adjacent); + } + } + } + WindowMessage::Clicked(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + window.focus = Some(pane); + } + WindowMessage::CloseWindow => { + let _ = self.windows.remove(&id); + return window::close(id); + } + WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { + let window = self.windows.get_mut(&id).unwrap(); + window.panes.resize(&split, ratio); + } + WindowMessage::SelectedWindow(pane, selected) => { + let window = self.windows.get_mut(&id).unwrap(); + let (mut pane, _) = window.panes.close(&pane).unwrap(); + pane.is_moving = false; + + if let Some(window) = self.windows.get_mut(&selected.0) { + let (&first_pane, _) = window.panes.iter().next().unwrap(); + let result = + window.panes.split(pane.axis, &first_pane, pane); + + if let Some((pane, _)) = result { + window.focus = Some(pane); + } + } + } + WindowMessage::ToggleMoving(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.panes.get_mut(&pane) { + pane.is_moving = !pane.is_moving; + } + } + WindowMessage::TitleChanged(title) => { + let window = self.windows.get_mut(&id).unwrap(); + window.title = title; + } + WindowMessage::PopOut(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some((popped, sibling)) = window.panes.close(&pane) { + window.focus = Some(sibling); + + let (panes, _) = pane_grid::State::new(popped); + let window = Window { + panes, + focus: None, + title: format!("New window ({})", self.windows.len()), + scale: 1.0 + (self.windows.len() as f64 / 10.0), + }; + + let window_id = window::Id::new(self.windows.len()); + self.windows.insert(window_id, window); + return window::spawn(window_id, Default::default()); + } + } + WindowMessage::Dragged(pane_grid::DragEvent::Dropped { + pane, + target, + }) => { + let window = self.windows.get_mut(&id).unwrap(); + window.panes.swap(&pane, &target); + } + // WindowMessage::Dragged(pane_grid::DragEvent::Picked { pane }) => { + // println!("Picked {pane:?}"); + // } + WindowMessage::Dragged(_) => {} + WindowMessage::TogglePin(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(Pane { is_pinned, .. }) = + window.panes.get_mut(&pane) + { + *is_pinned = !*is_pinned; + } + } + WindowMessage::Close(pane) => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some((_, sibling)) = window.panes.close(&pane) { + window.focus = Some(sibling); + } + } + WindowMessage::CloseFocused => { + let window = self.windows.get_mut(&id).unwrap(); + if let Some(pane) = window.focus { + if let Some(Pane { is_pinned, .. }) = + window.panes.get(&pane) + { + if !is_pinned { + if let Some((_, sibling)) = + window.panes.close(&pane) + { + window.focus = Some(sibling); + } + } + } + } + } + }, + Message::CountIncremented(_) => { + self.count += 1; + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription { + Subscription::batch(vec![ + subscription::events_with(|event, status| { + if let event::Status::Captured = status { + return None; + } + + match event { + Event::Keyboard(keyboard::Event::KeyPressed { + modifiers, + key_code, + }) if modifiers.command() => { + handle_hotkey(key_code).map(|message| { + Message::Window(window::Id::new(0usize), message) + }) + } // TODO(derezzedex) + _ => None, + } + }), + time::every(Duration::from_secs(1)).map(Message::CountIncremented), + ]) + } + + fn view(&self, window: window::Id) -> Element { + let window_id = window; + + if let Some(window) = self.windows.get(&window) { + let focus = window.focus; + let total_panes = window.panes.len(); + + let window_controls = row![ + text_input( + "Window title", + &window.title, + WindowMessage::TitleChanged, + ), + button(text("Close")) + .on_press(WindowMessage::CloseWindow) + .style(theme::Button::Destructive), + ] + .spacing(5) + .align_items(Alignment::Center); + + let pane_grid = PaneGrid::new(&window.panes, |id, pane, _| { + let is_focused = focus == Some(id); + + let pin_button = button( + text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), + ) + .on_press(WindowMessage::TogglePin(id)) + .padding(3); + + let title = row![ + pin_button, + "Pane", + text(pane.id.to_string()).style(if is_focused { + PANE_ID_COLOR_FOCUSED + } else { + PANE_ID_COLOR_UNFOCUSED + }), + ] + .spacing(5); + + let title_bar = pane_grid::TitleBar::new(title) + .controls(view_controls( + id, + total_panes, + pane.is_pinned, + pane.is_moving, + &window.title, + window_id, + &self.windows, + )) + .padding(10) + .style(if is_focused { + style::title_bar_focused + } else { + style::title_bar_active + }); + + pane_grid::Content::new(responsive(move |size| { + view_content( + id, + pane.scrollable_id.clone(), + self.count, + total_panes, + pane.is_pinned, + size, + ) + })) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + }) + .width(Length::Fill) + .height(Length::Fill) + .spacing(10) + .on_click(WindowMessage::Clicked) + .on_drag(WindowMessage::Dragged) + .on_resize(10, WindowMessage::Resized); + + let content: Element<_> = column![window_controls, pane_grid] + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .into(); + + return content + .map(move |message| Message::Window(window_id, message)); + } + + container(text("This shouldn't be possible!").size(20)) + .center_x() + .center_y() + .into() + } + + fn close_requested(&self, window: window::Id) -> Self::Message { + Message::Window(window, WindowMessage::CloseWindow) + } + + fn scale_factor(&self, window: Id) -> f64 { + self.windows.get(&window).map(|w| w.scale).unwrap_or(1.0) + } +} + +const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0xC7 as f32 / 255.0, + 0xC7 as f32 / 255.0, +); +const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( + 0xFF as f32 / 255.0, + 0x47 as f32 / 255.0, + 0x47 as f32 / 255.0, +); + +fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { + use keyboard::KeyCode; + use pane_grid::{Axis, Direction}; + + let direction = match key_code { + KeyCode::Up => Some(Direction::Up), + KeyCode::Down => Some(Direction::Down), + KeyCode::Left => Some(Direction::Left), + KeyCode::Right => Some(Direction::Right), + _ => None, + }; + + match key_code { + KeyCode::V => Some(WindowMessage::SplitFocused(Axis::Vertical)), + KeyCode::H => Some(WindowMessage::SplitFocused(Axis::Horizontal)), + KeyCode::W => Some(WindowMessage::CloseFocused), + _ => direction.map(WindowMessage::FocusAdjacent), + } +} + +#[derive(Debug, Clone)] +struct SelectableWindow(window::Id, String); + +impl PartialEq for SelectableWindow { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for SelectableWindow {} + +impl std::fmt::Display for SelectableWindow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.1.fmt(f) + } +} + +#[derive(Debug)] +struct Pane { + id: usize, + pub scrollable_id: scrollable::Id, + pub axis: pane_grid::Axis, + pub is_pinned: bool, + pub is_moving: bool, + pub snapped: bool, +} + +impl Pane { + fn new(id: usize, axis: pane_grid::Axis) -> Self { + Self { + id, + scrollable_id: scrollable::Id::unique(), + axis, + is_pinned: false, + is_moving: false, + snapped: false, + } + } +} + +fn view_content<'a>( + pane: pane_grid::Pane, + scrollable_id: scrollable::Id, + count: usize, + total_panes: usize, + is_pinned: bool, + size: Size, +) -> Element<'a, WindowMessage> { + let button = |label, message| { + button( + text(label) + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center) + .size(16), + ) + .width(Length::Fill) + .padding(8) + .on_press(message) + }; + + let mut controls = column![ + button( + "Split horizontally", + WindowMessage::Split(pane_grid::Axis::Horizontal, pane), + ), + button( + "Split vertically", + WindowMessage::Split(pane_grid::Axis::Vertical, pane), + ), + button("Snap", WindowMessage::SnapToggle,) + ] + .spacing(5) + .max_width(150); + + if total_panes > 1 && !is_pinned { + controls = controls.push( + button("Close", WindowMessage::Close(pane)) + .style(theme::Button::Destructive), + ); + } + + let content = column![ + text(format!("{}x{}", size.width, size.height)).size(24), + controls, + text(format!("{count}")).size(48), + ] + .width(Length::Fill) + .height(800) + .spacing(10) + .align_items(Alignment::Center); + + container( + scrollable(content) + .height(Length::Fill) + .vertical_scroll(Properties::new()) + .id(scrollable_id), + ) + .width(Length::Fill) + .height(Length::Fill) + .padding(5) + .center_y() + .into() +} + +fn view_controls<'a>( + pane: pane_grid::Pane, + total_panes: usize, + is_pinned: bool, + is_moving: bool, + window_title: &'a str, + window_id: window::Id, + windows: &HashMap, +) -> Element<'a, WindowMessage> { + let window_selector = { + let options: Vec<_> = windows + .iter() + .map(|(id, window)| SelectableWindow(*id, window.title.clone())) + .collect(); + pick_list( + options, + Some(SelectableWindow(window_id, window_title.to_string())), + move |window| WindowMessage::SelectedWindow(pane, window), + ) + }; + + let mut move_to = button(text("Move to").size(14)).padding(3); + + let mut pop_out = button(text("Pop Out").size(14)).padding(3); + + let mut close = button(text("Close").size(14)) + .style(theme::Button::Destructive) + .padding(3); + + if total_panes > 1 && !is_pinned { + close = close.on_press(WindowMessage::Close(pane)); + pop_out = pop_out.on_press(WindowMessage::PopOut(pane)); + } + + if windows.len() > 1 && total_panes > 1 && !is_pinned { + move_to = move_to.on_press(WindowMessage::ToggleMoving(pane)); + } + + let mut content = row![].spacing(10); + if is_moving { + content = content.push(pop_out).push(window_selector).push(close); + } else { + content = content.push(pop_out).push(move_to).push(close); + } + + content.into() +} + +mod style { + use iced::widget::container; + use iced::Theme; + + pub fn title_bar_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() + } + } + + pub fn title_bar_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + text_color: Some(palette.primary.strong.text), + background: Some(palette.primary.strong.color.into()), + ..Default::default() + } + } + + pub fn pane_active(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.background.strong.color, + ..Default::default() + } + } + + pub fn pane_focused(theme: &Theme) -> container::Appearance { + let palette = theme.extended_palette(); + + container::Appearance { + background: Some(palette.background.weak.color.into()), + border_width: 2.0, + border_color: palette.primary.strong.color, + ..Default::default() + } + } +} diff --git a/native/src/window/id.rs b/native/src/window/id.rs index 0c3e5272..0a11b1aa 100644 --- a/native/src/window/id.rs +++ b/native/src/window/id.rs @@ -4,6 +4,8 @@ use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] /// The ID of the window. +/// +/// Internally Iced uses `window::Id::MAIN` as the first window spawned. pub struct Id(u64); impl Id { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index d5da406c..6e28f1fa 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -109,7 +109,7 @@ where /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. - fn title(&self, window_id: window::Id) -> String; + fn title(&self, window: window::Id) -> String; /// Returns the current [`Theme`] of the [`Application`]. fn theme(&self) -> ::Theme; -- cgit From 41836dd80d0534608e7aedfbf2319c540a23de1a Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 15 Mar 2023 18:20:38 -0700 Subject: Added per-window theme support. --- examples/multi_window_panes/src/main.rs | 41 ++++++++++++++++++++++----------- src/multi_window/application.rs | 7 +++--- winit/src/multi_window.rs | 2 +- winit/src/multi_window/state.rs | 4 ++-- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/examples/multi_window_panes/src/main.rs b/examples/multi_window_panes/src/main.rs index b8b63769..b1d0a3bc 100644 --- a/examples/multi_window_panes/src/main.rs +++ b/examples/multi_window_panes/src/main.rs @@ -1,5 +1,4 @@ use iced::alignment::{self, Alignment}; -use iced::{executor, time}; use iced::keyboard; use iced::multi_window::Application; use iced::theme::{self, Theme}; @@ -8,6 +7,7 @@ use iced::widget::{ button, column, container, pick_list, row, scrollable, text, text_input, }; use iced::window; +use iced::{executor, time}; use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; use iced_lazy::responsive; use iced_native::{event, subscription, Event}; @@ -34,6 +34,7 @@ struct Example { struct Window { title: String, scale: f64, + theme: Theme, panes: pane_grid::State, focus: Option, } @@ -77,6 +78,7 @@ impl Application for Example { focus: None, title: String::from("Default window"), scale: 1.0, + theme: Theme::default(), }; ( @@ -167,7 +169,10 @@ impl Application for Example { let _ = self.windows.remove(&id); return window::close(id); } - WindowMessage::Resized(pane_grid::ResizeEvent { split, ratio }) => { + WindowMessage::Resized(pane_grid::ResizeEvent { + split, + ratio, + }) => { let window = self.windows.get_mut(&id).unwrap(); window.panes.resize(&split, ratio); } @@ -177,7 +182,8 @@ impl Application for Example { pane.is_moving = false; if let Some(window) = self.windows.get_mut(&selected.0) { - let (&first_pane, _) = window.panes.iter().next().unwrap(); + let (&first_pane, _) = + window.panes.iter().next().unwrap(); let result = window.panes.split(pane.axis, &first_pane, pane); @@ -205,8 +211,16 @@ impl Application for Example { let window = Window { panes, focus: None, - title: format!("New window ({})", self.windows.len()), + title: format!( + "New window ({})", + self.windows.len() + ), scale: 1.0 + (self.windows.len() as f64 / 10.0), + theme: if self.windows.len() % 2 == 0 { + Theme::Light + } else { + Theme::Dark + }, }; let window_id = window::Id::new(self.windows.len()); @@ -215,15 +229,12 @@ impl Application for Example { } } WindowMessage::Dragged(pane_grid::DragEvent::Dropped { - pane, - target, - }) => { + pane, + target, + }) => { let window = self.windows.get_mut(&id).unwrap(); window.panes.swap(&pane, &target); } - // WindowMessage::Dragged(pane_grid::DragEvent::Picked { pane }) => { - // println!("Picked {pane:?}"); - // } WindowMessage::Dragged(_) => {} WindowMessage::TogglePin(pane) => { let window = self.windows.get_mut(&id).unwrap(); @@ -273,9 +284,9 @@ impl Application for Example { match event { Event::Keyboard(keyboard::Event::KeyPressed { - modifiers, - key_code, - }) if modifiers.command() => { + modifiers, + key_code, + }) if modifiers.command() => { handle_hotkey(key_code).map(|message| { Message::Window(window::Id::new(0usize), message) }) @@ -391,6 +402,10 @@ impl Application for Example { fn scale_factor(&self, window: Id) -> f64 { self.windows.get(&window).map(|w| w.scale).unwrap_or(1.0) } + + fn theme(&self, window: Id) -> Theme { + self.windows.get(&window).expect("Window not found!").theme.clone() + } } const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 1fb4bcd4..9974128c 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -107,7 +107,8 @@ pub trait Application: Sized { /// Returns the current [`Theme`] of the [`Application`]. /// /// [`Theme`]: Self::Theme - fn theme(&self) -> Self::Theme { + #[allow(unused_variables)] + fn theme(&self, window: window::Id) -> Self::Theme { Self::Theme::default() } @@ -229,8 +230,8 @@ where self.0.title(window) } - fn theme(&self) -> A::Theme { - self.0.theme() + fn theme(&self, window: window::Id) -> A::Theme { + self.0.theme(window) } fn style(&self) -> ::Style { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 6e28f1fa..9b395c1d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -112,7 +112,7 @@ where fn title(&self, window: window::Id) -> String; /// Returns the current [`Theme`] of the [`Application`]. - fn theme(&self) -> ::Theme; + fn theme(&self, window: window::Id) -> ::Theme; /// Returns the [`Style`] variation of the [`Theme`]. fn style( diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index d0e442d0..54a114ad 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -37,7 +37,7 @@ where ) -> Self { let title = application.title(window_id); let scale_factor = application.scale_factor(window_id); - let theme = application.theme(); + let theme = application.theme(window_id); let appearance = theme.appearance(&application.style()); let viewport = { @@ -212,7 +212,7 @@ where } // Update theme and appearance - self.theme = application.theme(); + self.theme = application.theme(window_id); self.appearance = self.theme.appearance(&application.style()); } } -- cgit From d53ccc857da4d4cda769904342aeb5a82a64f146 Mon Sep 17 00:00:00 2001 From: Bingus Date: Wed, 12 Jul 2023 19:21:05 -0700 Subject: refactored window storage; new helper window events (Destroyed, Created); clippy + fmt; --- .github/ISSUE_TEMPLATE/BUG-REPORT.yml | 1 - Cargo.toml | 2 +- ECOSYSTEM.md | 8 +- core/Cargo.toml | 3 + core/src/window.rs | 6 + core/src/window/event.rs | 17 + core/src/window/id.rs | 15 +- core/src/window/position.rs | 22 + core/src/window/settings.rs | 40 +- core/src/window/settings/macos.rs | 12 + core/src/window/settings/other.rs | 3 + core/src/window/settings/wasm.rs | 11 + core/src/window/settings/windows.rs | 21 + examples/events/src/main.rs | 7 +- examples/exit/src/main.rs | 2 +- examples/integration/src/main.rs | 2 +- examples/integration_opengl/src/main.rs | 0 examples/loading_spinners/src/circular.rs | 2 +- examples/loading_spinners/src/linear.rs | 2 +- examples/multi_window/Cargo.toml | 2 +- examples/multi_window/src/main.rs | 162 +++-- examples/multi_window_panes/Cargo.toml | 12 - examples/multi_window_panes/src/main.rs | 639 ----------------- examples/screenshot/src/main.rs | 7 +- examples/toast/src/main.rs | 4 +- examples/todos/src/main.rs | 2 +- futures/src/subscription.rs | 2 +- glutin/src/application.rs | 0 graphics/Cargo.toml | 1 - graphics/src/compositor.rs | 3 + native/src/subscription.rs | 0 native/src/window.rs | 0 renderer/src/compositor.rs | 16 + runtime/Cargo.toml | 1 + runtime/src/lib.rs | 3 + runtime/src/multi_window.rs | 6 + runtime/src/multi_window/program.rs | 32 + runtime/src/multi_window/state.rs | 280 ++++++++ runtime/src/window.rs | 102 +-- runtime/src/window/action.rs | 4 +- src/multi_window/application.rs | 96 ++- src/settings.rs | 2 +- src/window.rs | 4 - src/window/icon.rs | 63 ++ tiny_skia/src/window/compositor.rs | 6 + wgpu/src/window/compositor.rs | 4 + winit/Cargo.toml | 22 +- winit/src/application.rs | 86 +-- winit/src/conversion.rs | 9 +- winit/src/icon.rs | 63 -- winit/src/lib.rs | 8 +- winit/src/multi_window.rs | 1078 ++++++++++++++--------------- winit/src/multi_window/state.rs | 96 ++- winit/src/multi_window/windows.rs | 170 +++++ winit/src/profiler.rs | 101 --- winit/src/settings.rs | 246 ++----- winit/src/settings/macos.rs | 12 - winit/src/settings/other.rs | 3 - winit/src/settings/wasm.rs | 11 - winit/src/settings/windows.rs | 21 - winit/src/window.rs | 0 61 files changed, 1622 insertions(+), 1933 deletions(-) create mode 100644 core/src/window/settings/macos.rs create mode 100644 core/src/window/settings/other.rs create mode 100644 core/src/window/settings/wasm.rs create mode 100644 core/src/window/settings/windows.rs delete mode 100644 examples/integration_opengl/src/main.rs delete mode 100644 examples/multi_window_panes/Cargo.toml delete mode 100644 examples/multi_window_panes/src/main.rs delete mode 100644 glutin/src/application.rs delete mode 100644 native/src/subscription.rs delete mode 100644 native/src/window.rs create mode 100644 runtime/src/multi_window.rs create mode 100644 runtime/src/multi_window/program.rs create mode 100644 runtime/src/multi_window/state.rs create mode 100644 src/window/icon.rs delete mode 100644 winit/src/icon.rs create mode 100644 winit/src/multi_window/windows.rs delete mode 100644 winit/src/profiler.rs delete mode 100644 winit/src/settings/macos.rs delete mode 100644 winit/src/settings/other.rs delete mode 100644 winit/src/settings/wasm.rs delete mode 100644 winit/src/settings/windows.rs delete mode 100644 winit/src/window.rs diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index d4c94fcd..09b31697 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -25,7 +25,6 @@ body: 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 `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! diff --git a/Cargo.toml b/Cargo.toml index 4a82c923..a7f02055 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ system = ["iced_winit/system"] web-colors = ["iced_renderer/web-colors"] # Enables the advanced module advanced = [] -# Enables experimental multi-window support for iced_winit + wgpu. +# Enables experimental multi-window support. multi-window = ["iced_winit/multi-window"] [badges] diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 86581e4a..da3066d8 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -45,7 +45,7 @@ The widgets of a _graphical_ user interface produce some primitives that eventua Currently, there are two different official renderers: - [`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. @@ -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]. -As of now, there are two official shells: - -- [`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]. +As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`]. ## The web target 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 [`glutin`]: https://github.com/rust-windowing/glutin [`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/ diff --git a/core/Cargo.toml b/core/Cargo.toml index 55f2e85f..edf9e7c8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,5 +20,8 @@ optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant = "0.1" +[target.'cfg(windows)'.dependencies.raw-window-handle] +version = "0.5.2" + [dev-dependencies] approx = "0.5" diff --git a/core/src/window.rs b/core/src/window.rs index a6dbdfb4..10db31b6 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -2,14 +2,20 @@ pub mod icon; mod event; +mod id; mod level; mod mode; +mod position; mod redraw_request; +mod settings; mod user_attention; pub use event::Event; pub use icon::Icon; +pub use id::Id; pub use level::Level; pub use mode::Mode; +pub use position::Position; pub use redraw_request::RedrawRequest; +pub use settings::Settings; pub use user_attention::UserAttention; diff --git a/core/src/window/event.rs b/core/src/window/event.rs index e2fb5e66..3efce05e 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -1,4 +1,5 @@ use crate::time::Instant; +use crate::Size; use std::path::PathBuf; @@ -32,6 +33,22 @@ pub enum Event { /// occurs. CloseRequested, + /// A window was destroyed by the runtime. + Destroyed, + + /// A window was created. + /// + /// **Note:** this event is not supported on Wayland. + Created { + /// The position of the created 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. + position: (i32, i32), + /// 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 focused. Focused, diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 0a11b1aa..65002d43 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -1,18 +1,17 @@ use std::collections::hash_map::DefaultHasher; -use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -/// The ID of the window. +/// The id of the window. /// -/// Internally Iced uses `window::Id::MAIN` as the first window spawned. +/// Internally Iced reserves `window::Id::MAIN` for the first window spawned. pub struct Id(u64); impl Id { - /// The reserved window ID for the primary window in an Iced application. + /// The reserved window [`Id`] for the first window in an Iced application. pub const MAIN: Self = Id(0); - /// Creates a new unique window ID. + /// Creates a new unique window [`Id`]. pub fn new(id: impl Hash) -> Id { let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); @@ -20,9 +19,3 @@ impl Id { Id(hasher.finish()) } } - -impl Display for Id { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "Id({})", self.0) - } -} diff --git a/core/src/window/position.rs b/core/src/window/position.rs index e69de29b..c260c29e 100644 --- a/core/src/window/position.rs +++ b/core/src/window/position.rs @@ -0,0 +1,22 @@ +/// 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 + } +} diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index 458b9232..20811e83 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -1,6 +1,26 @@ use crate::window::{Icon, Level, Position}; -pub use iced_winit::settings::PlatformSpecific; +#[cfg(target_os = "windows")] +#[path = "settings/windows.rs"] +mod platform; + +#[cfg(target_os = "macos")] +#[path = "settings/macos.rs"] +mod platform; + +#[cfg(target_arch = "wasm32")] +#[path = "settings/wasm.rs"] +mod platform; + +#[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_arch = "wasm32" +)))] +#[path = "settings/other.rs"] +mod platform; + +pub use platform::PlatformSpecific; /// The window settings of an application. #[derive(Debug, Clone)] @@ -56,21 +76,3 @@ impl Default for Settings { } } } - -impl From 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, - } - } -} diff --git a/core/src/window/settings/macos.rs b/core/src/window/settings/macos.rs new file mode 100644 index 00000000..f86e63ad --- /dev/null +++ b/core/src/window/settings/macos.rs @@ -0,0 +1,12 @@ +//! Platform specific settings for macOS. + +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformSpecific { + /// Hides the window title. + pub title_hidden: bool, + /// Makes the titlebar transparent and allows the content to appear behind it. + pub titlebar_transparent: bool, + /// Makes the window content appear behind the titlebar. + pub fullsize_content_view: bool, +} diff --git a/core/src/window/settings/other.rs b/core/src/window/settings/other.rs new file mode 100644 index 00000000..b1103f62 --- /dev/null +++ b/core/src/window/settings/other.rs @@ -0,0 +1,3 @@ +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct PlatformSpecific; diff --git a/core/src/window/settings/wasm.rs b/core/src/window/settings/wasm.rs new file mode 100644 index 00000000..8e0f1bbc --- /dev/null +++ b/core/src/window/settings/wasm.rs @@ -0,0 +1,11 @@ +//! Platform specific settings for WebAssembly. + +/// The platform specific window settings of an application. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct PlatformSpecific { + /// The identifier of a DOM element that will be replaced with the + /// application. + /// + /// If set to `None`, the application will be appended to the HTML body. + pub target: Option, +} diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs new file mode 100644 index 00000000..45d753bd --- /dev/null +++ b/core/src/window/settings/windows.rs @@ -0,0 +1,21 @@ +//! Platform specific settings for Windows. +use raw_window_handle::RawWindowHandle; + +/// The platform specific window settings of an application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PlatformSpecific { + /// Parent window + pub parent: Option, + + /// Drag and drop support + pub drag_and_drop: bool, +} + +impl Default for PlatformSpecific { + fn default() -> Self { + Self { + parent: None, + drag_and_drop: true, + } + } +} diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 70659f52..c3ac6fd1 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -26,7 +26,7 @@ struct Events { enum Message { EventOccurred(Event), Toggled(bool), - Exit(window::Id), + Exit, } impl Application for Events { @@ -55,7 +55,8 @@ impl Application for Events { Command::none() } Message::EventOccurred(event) => { - if let Event::Window(id, window::Event::CloseRequested) = event { + if let Event::Window(id, window::Event::CloseRequested) = event + { window::close(id) } else { Command::none() @@ -66,7 +67,7 @@ impl Application for Events { Command::none() } - Message::Exit(id) => window::close(id), + Message::Exit => window::close(window::Id::MAIN), } } diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 6152f627..ec618dc1 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -34,7 +34,7 @@ impl Application for Exit { fn update(&mut self, message: Message) -> Command { match message { - Message::Confirm => window::close(), + Message::Confirm => window::close(window::Id::MAIN), Message::Exit => { self.show_confirm = true; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index a560959a..90beb097 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -6,8 +6,8 @@ use scene::Scene; use iced_wgpu::graphics::Viewport; use iced_wgpu::{wgpu, Backend, Renderer, Settings}; -use iced_winit::core::mouse; use iced_winit::core::renderer; +use iced_winit::core::{mouse, window}; use iced_winit::core::{Color, Size}; use iced_winit::runtime::program; use iced_winit::runtime::Debug; diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index 3a35e029..ff599231 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -277,7 +277,7 @@ where let state = tree.state.downcast_mut::(); - if let Event::Window(window::Event::RedrawRequested(now)) = event { + if let Event::Window(_, window::Event::RedrawRequested(now)) = event { state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index 3d95729b..8e07c12b 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -198,7 +198,7 @@ where let state = tree.state.downcast_mut::(); - 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); shell.request_redraw(RedrawRequest::At( diff --git a/examples/multi_window/Cargo.toml b/examples/multi_window/Cargo.toml index 0cb5d546..2e222dfb 100644 --- a/examples/multi_window/Cargo.toml +++ b/examples/multi_window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug", "multi-window"] } \ No newline at end of file +iced = { path = "../..", features = ["debug", "multi-window"] } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 5699ece0..58604702 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -1,25 +1,32 @@ use iced::multi_window::{self, Application}; use iced::widget::{button, column, container, scrollable, text, text_input}; +use iced::window::{Id, Position}; use iced::{ - executor, window, Alignment, Command, Element, Length, Settings, Theme, + executor, subscription, window, Alignment, Command, Element, Length, + Settings, Subscription, Theme, }; use std::collections::HashMap; fn main() -> iced::Result { - Example::run(Settings::default()) + Example::run(Settings { + exit_on_close_request: false, + ..Default::default() + }) } #[derive(Default)] struct Example { - windows_count: usize, windows: HashMap, + next_window_pos: window::Position, } +#[derive(Debug)] struct Window { - id: window::Id, title: String, scale_input: String, current_scale: f64, + theme: Theme, + input_id: iced::widget::text_input::Id, } #[derive(Debug, Clone)] @@ -28,6 +35,8 @@ enum Message { ScaleChanged(window::Id, String), TitleChanged(window::Id, String), CloseWindow(window::Id), + WindowDestroyed(window::Id), + WindowCreated(window::Id, (i32, i32)), NewWindow, } @@ -40,11 +49,8 @@ impl multi_window::Application for Example { fn new(_flags: ()) -> (Self, Command) { ( Example { - windows_count: 0, - windows: HashMap::from([( - window::Id::MAIN, - Window::new(window::Id::MAIN), - )]), + windows: HashMap::from([(window::Id::MAIN, Window::new(1))]), + next_window_pos: Position::Default, }, Command::none(), ) @@ -82,12 +88,32 @@ impl multi_window::Application for Example { Message::CloseWindow(id) => { return window::close(id); } + Message::WindowDestroyed(id) => { + self.windows.remove(&id); + } + Message::WindowCreated(id, position) => { + self.next_window_pos = window::Position::Specific( + position.0 + 20, + position.1 + 20, + ); + + if let Some(window) = self.windows.get(&id) { + return text_input::focus(window.input_id.clone()); + } + } Message::NewWindow => { - self.windows_count += 1; - let id = window::Id::new(self.windows_count); - self.windows.insert(id, Window::new(id)); - - return window::spawn(id, window::Settings::default()); + let count = self.windows.len() + 1; + let id = window::Id::new(count); + + self.windows.insert(id, Window::new(count)); + + return window::spawn( + id, + window::Settings { + position: self.next_window_pos, + ..Default::default() + }, + ); } } @@ -95,13 +121,9 @@ impl multi_window::Application for Example { } fn view(&self, window: window::Id) -> Element { - let window = self - .windows - .get(&window) - .map(|window| window.view()) - .unwrap(); + let content = self.windows.get(&window).unwrap().view(window); - container(window) + container(content) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -109,6 +131,10 @@ impl multi_window::Application for Example { .into() } + fn theme(&self, window: Id) -> Self::Theme { + self.windows.get(&window).unwrap().theme.clone() + } + fn scale_factor(&self, window: window::Id) -> f64 { self.windows .get(&window) @@ -116,55 +142,71 @@ impl multi_window::Application for Example { .unwrap_or(1.0) } - fn close_requested(&self, window: window::Id) -> Self::Message { - Message::CloseWindow(window) + fn subscription(&self) -> Subscription { + subscription::events_with(|event, _| { + if let iced::Event::Window(id, window_event) = event { + match window_event { + window::Event::CloseRequested => { + Some(Message::CloseWindow(id)) + } + window::Event::Destroyed => { + Some(Message::WindowDestroyed(id)) + } + window::Event::Created { position, .. } => { + Some(Message::WindowCreated(id, position)) + } + _ => None, + } + } else { + None + } + }) } } impl Window { - fn new(id: window::Id) -> Self { + fn new(count: usize) -> Self { Self { - id, - title: "Window".to_string(), + 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) -> Element { - window_view(self.id, &self.scale_input, &self.title) + fn view(&self, id: window::Id) -> Element { + 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() } } - -fn window_view<'a>( - id: window::Id, - scale_input: &'a str, - title: &'a str, -) -> Element<'a, Message> { - let scale_input = column![ - text("Window scale factor:"), - text_input("Window Scale", scale_input, move |msg| { - Message::ScaleInputChanged(id, msg) - }) - .on_submit(Message::ScaleChanged(id, scale_input.to_string())) - ]; - - let title_input = column![ - text("Window title:"), - text_input("Window Title", title, move |msg| { - Message::TitleChanged(id, msg) - }) - ]; - - 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() -} diff --git a/examples/multi_window_panes/Cargo.toml b/examples/multi_window_panes/Cargo.toml deleted file mode 100644 index 1b3f3ec6..00000000 --- a/examples/multi_window_panes/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "multi_window_panes" -version = "0.1.0" -authors = ["Richard Custodio "] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../..", features = ["debug", "multi-window", "tokio"] } -env_logger = "0.10.0" -iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } diff --git a/examples/multi_window_panes/src/main.rs b/examples/multi_window_panes/src/main.rs deleted file mode 100644 index b1d0a3bc..00000000 --- a/examples/multi_window_panes/src/main.rs +++ /dev/null @@ -1,639 +0,0 @@ -use iced::alignment::{self, Alignment}; -use iced::keyboard; -use iced::multi_window::Application; -use iced::theme::{self, Theme}; -use iced::widget::pane_grid::{self, PaneGrid}; -use iced::widget::{ - button, column, container, pick_list, row, scrollable, text, text_input, -}; -use iced::window; -use iced::{executor, time}; -use iced::{Color, Command, Element, Length, Settings, Size, Subscription}; -use iced_lazy::responsive; -use iced_native::{event, subscription, Event}; - -use iced_native::widget::scrollable::{Properties, RelativeOffset}; -use iced_native::window::Id; -use std::collections::HashMap; -use std::time::{Duration, Instant}; - -pub fn main() -> iced::Result { - env_logger::init(); - - Example::run(Settings::default()) -} - -struct Example { - windows: HashMap, - panes_created: usize, - count: usize, - _focused: window::Id, -} - -#[derive(Debug)] -struct Window { - title: String, - scale: f64, - theme: Theme, - panes: pane_grid::State, - focus: Option, -} - -#[derive(Debug, Clone)] -enum Message { - Window(window::Id, WindowMessage), - CountIncremented(Instant), -} - -#[derive(Debug, Clone)] -enum WindowMessage { - Split(pane_grid::Axis, pane_grid::Pane), - SplitFocused(pane_grid::Axis), - FocusAdjacent(pane_grid::Direction), - Clicked(pane_grid::Pane), - Dragged(pane_grid::DragEvent), - PopOut(pane_grid::Pane), - Resized(pane_grid::ResizeEvent), - TitleChanged(String), - ToggleMoving(pane_grid::Pane), - TogglePin(pane_grid::Pane), - Close(pane_grid::Pane), - CloseFocused, - SelectedWindow(pane_grid::Pane, SelectableWindow), - CloseWindow, - SnapToggle, -} - -impl Application for Example { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command) { - let (panes, _) = - pane_grid::State::new(Pane::new(0, pane_grid::Axis::Horizontal)); - let window = Window { - panes, - focus: None, - title: String::from("Default window"), - scale: 1.0, - theme: Theme::default(), - }; - - ( - Example { - windows: HashMap::from([(window::Id::MAIN, window)]), - panes_created: 1, - count: 0, - _focused: window::Id::MAIN, - }, - Command::none(), - ) - } - - fn title(&self, window: window::Id) -> String { - self.windows - .get(&window) - .map(|w| w.title.clone()) - .unwrap_or(String::from("New Window")) - } - - fn update(&mut self, message: Message) -> Command { - match message { - Message::Window(id, message) => match message { - WindowMessage::SnapToggle => { - let window = self.windows.get_mut(&id).unwrap(); - - if let Some(focused) = &window.focus { - let pane = window.panes.get_mut(focused).unwrap(); - - let cmd = scrollable::snap_to( - pane.scrollable_id.clone(), - if pane.snapped { - RelativeOffset::START - } else { - RelativeOffset::END - }, - ); - - pane.snapped = !pane.snapped; - return cmd; - } - } - WindowMessage::Split(axis, pane) => { - let window = self.windows.get_mut(&id).unwrap(); - let result = window.panes.split( - axis, - &pane, - Pane::new(self.panes_created, axis), - ); - - if let Some((pane, _)) = result { - window.focus = Some(pane); - } - - self.panes_created += 1; - } - WindowMessage::SplitFocused(axis) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - let result = window.panes.split( - axis, - &pane, - Pane::new(self.panes_created, axis), - ); - - if let Some((pane, _)) = result { - window.focus = Some(pane); - } - - self.panes_created += 1; - } - } - WindowMessage::FocusAdjacent(direction) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - if let Some(adjacent) = - window.panes.adjacent(&pane, direction) - { - window.focus = Some(adjacent); - } - } - } - WindowMessage::Clicked(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - window.focus = Some(pane); - } - WindowMessage::CloseWindow => { - let _ = self.windows.remove(&id); - return window::close(id); - } - WindowMessage::Resized(pane_grid::ResizeEvent { - split, - ratio, - }) => { - let window = self.windows.get_mut(&id).unwrap(); - window.panes.resize(&split, ratio); - } - WindowMessage::SelectedWindow(pane, selected) => { - let window = self.windows.get_mut(&id).unwrap(); - let (mut pane, _) = window.panes.close(&pane).unwrap(); - pane.is_moving = false; - - if let Some(window) = self.windows.get_mut(&selected.0) { - let (&first_pane, _) = - window.panes.iter().next().unwrap(); - let result = - window.panes.split(pane.axis, &first_pane, pane); - - if let Some((pane, _)) = result { - window.focus = Some(pane); - } - } - } - WindowMessage::ToggleMoving(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.panes.get_mut(&pane) { - pane.is_moving = !pane.is_moving; - } - } - WindowMessage::TitleChanged(title) => { - let window = self.windows.get_mut(&id).unwrap(); - window.title = title; - } - WindowMessage::PopOut(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some((popped, sibling)) = window.panes.close(&pane) { - window.focus = Some(sibling); - - let (panes, _) = pane_grid::State::new(popped); - let window = Window { - panes, - focus: None, - title: format!( - "New window ({})", - self.windows.len() - ), - scale: 1.0 + (self.windows.len() as f64 / 10.0), - theme: if self.windows.len() % 2 == 0 { - Theme::Light - } else { - Theme::Dark - }, - }; - - let window_id = window::Id::new(self.windows.len()); - self.windows.insert(window_id, window); - return window::spawn(window_id, Default::default()); - } - } - WindowMessage::Dragged(pane_grid::DragEvent::Dropped { - pane, - target, - }) => { - let window = self.windows.get_mut(&id).unwrap(); - window.panes.swap(&pane, &target); - } - WindowMessage::Dragged(_) => {} - WindowMessage::TogglePin(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(Pane { is_pinned, .. }) = - window.panes.get_mut(&pane) - { - *is_pinned = !*is_pinned; - } - } - WindowMessage::Close(pane) => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some((_, sibling)) = window.panes.close(&pane) { - window.focus = Some(sibling); - } - } - WindowMessage::CloseFocused => { - let window = self.windows.get_mut(&id).unwrap(); - if let Some(pane) = window.focus { - if let Some(Pane { is_pinned, .. }) = - window.panes.get(&pane) - { - if !is_pinned { - if let Some((_, sibling)) = - window.panes.close(&pane) - { - window.focus = Some(sibling); - } - } - } - } - } - }, - Message::CountIncremented(_) => { - self.count += 1; - } - } - - Command::none() - } - - fn subscription(&self) -> Subscription { - Subscription::batch(vec![ - subscription::events_with(|event, status| { - if let event::Status::Captured = status { - return None; - } - - match event { - Event::Keyboard(keyboard::Event::KeyPressed { - modifiers, - key_code, - }) if modifiers.command() => { - handle_hotkey(key_code).map(|message| { - Message::Window(window::Id::new(0usize), message) - }) - } // TODO(derezzedex) - _ => None, - } - }), - time::every(Duration::from_secs(1)).map(Message::CountIncremented), - ]) - } - - fn view(&self, window: window::Id) -> Element { - let window_id = window; - - if let Some(window) = self.windows.get(&window) { - let focus = window.focus; - let total_panes = window.panes.len(); - - let window_controls = row![ - text_input( - "Window title", - &window.title, - WindowMessage::TitleChanged, - ), - button(text("Close")) - .on_press(WindowMessage::CloseWindow) - .style(theme::Button::Destructive), - ] - .spacing(5) - .align_items(Alignment::Center); - - let pane_grid = PaneGrid::new(&window.panes, |id, pane, _| { - let is_focused = focus == Some(id); - - let pin_button = button( - text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14), - ) - .on_press(WindowMessage::TogglePin(id)) - .padding(3); - - let title = row![ - pin_button, - "Pane", - text(pane.id.to_string()).style(if is_focused { - PANE_ID_COLOR_FOCUSED - } else { - PANE_ID_COLOR_UNFOCUSED - }), - ] - .spacing(5); - - let title_bar = pane_grid::TitleBar::new(title) - .controls(view_controls( - id, - total_panes, - pane.is_pinned, - pane.is_moving, - &window.title, - window_id, - &self.windows, - )) - .padding(10) - .style(if is_focused { - style::title_bar_focused - } else { - style::title_bar_active - }); - - pane_grid::Content::new(responsive(move |size| { - view_content( - id, - pane.scrollable_id.clone(), - self.count, - total_panes, - pane.is_pinned, - size, - ) - })) - .title_bar(title_bar) - .style(if is_focused { - style::pane_focused - } else { - style::pane_active - }) - }) - .width(Length::Fill) - .height(Length::Fill) - .spacing(10) - .on_click(WindowMessage::Clicked) - .on_drag(WindowMessage::Dragged) - .on_resize(10, WindowMessage::Resized); - - let content: Element<_> = column![window_controls, pane_grid] - .width(Length::Fill) - .height(Length::Fill) - .padding(10) - .into(); - - return content - .map(move |message| Message::Window(window_id, message)); - } - - container(text("This shouldn't be possible!").size(20)) - .center_x() - .center_y() - .into() - } - - fn close_requested(&self, window: window::Id) -> Self::Message { - Message::Window(window, WindowMessage::CloseWindow) - } - - fn scale_factor(&self, window: Id) -> f64 { - self.windows.get(&window).map(|w| w.scale).unwrap_or(1.0) - } - - fn theme(&self, window: Id) -> Theme { - self.windows.get(&window).expect("Window not found!").theme.clone() - } -} - -const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb( - 0xFF as f32 / 255.0, - 0xC7 as f32 / 255.0, - 0xC7 as f32 / 255.0, -); -const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( - 0xFF as f32 / 255.0, - 0x47 as f32 / 255.0, - 0x47 as f32 / 255.0, -); - -fn handle_hotkey(key_code: keyboard::KeyCode) -> Option { - use keyboard::KeyCode; - use pane_grid::{Axis, Direction}; - - let direction = match key_code { - KeyCode::Up => Some(Direction::Up), - KeyCode::Down => Some(Direction::Down), - KeyCode::Left => Some(Direction::Left), - KeyCode::Right => Some(Direction::Right), - _ => None, - }; - - match key_code { - KeyCode::V => Some(WindowMessage::SplitFocused(Axis::Vertical)), - KeyCode::H => Some(WindowMessage::SplitFocused(Axis::Horizontal)), - KeyCode::W => Some(WindowMessage::CloseFocused), - _ => direction.map(WindowMessage::FocusAdjacent), - } -} - -#[derive(Debug, Clone)] -struct SelectableWindow(window::Id, String); - -impl PartialEq for SelectableWindow { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl Eq for SelectableWindow {} - -impl std::fmt::Display for SelectableWindow { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.1.fmt(f) - } -} - -#[derive(Debug)] -struct Pane { - id: usize, - pub scrollable_id: scrollable::Id, - pub axis: pane_grid::Axis, - pub is_pinned: bool, - pub is_moving: bool, - pub snapped: bool, -} - -impl Pane { - fn new(id: usize, axis: pane_grid::Axis) -> Self { - Self { - id, - scrollable_id: scrollable::Id::unique(), - axis, - is_pinned: false, - is_moving: false, - snapped: false, - } - } -} - -fn view_content<'a>( - pane: pane_grid::Pane, - scrollable_id: scrollable::Id, - count: usize, - total_panes: usize, - is_pinned: bool, - size: Size, -) -> Element<'a, WindowMessage> { - let button = |label, message| { - button( - text(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - }; - - let mut controls = column![ - button( - "Split horizontally", - WindowMessage::Split(pane_grid::Axis::Horizontal, pane), - ), - button( - "Split vertically", - WindowMessage::Split(pane_grid::Axis::Vertical, pane), - ), - button("Snap", WindowMessage::SnapToggle,) - ] - .spacing(5) - .max_width(150); - - if total_panes > 1 && !is_pinned { - controls = controls.push( - button("Close", WindowMessage::Close(pane)) - .style(theme::Button::Destructive), - ); - } - - let content = column![ - text(format!("{}x{}", size.width, size.height)).size(24), - controls, - text(format!("{count}")).size(48), - ] - .width(Length::Fill) - .height(800) - .spacing(10) - .align_items(Alignment::Center); - - container( - scrollable(content) - .height(Length::Fill) - .vertical_scroll(Properties::new()) - .id(scrollable_id), - ) - .width(Length::Fill) - .height(Length::Fill) - .padding(5) - .center_y() - .into() -} - -fn view_controls<'a>( - pane: pane_grid::Pane, - total_panes: usize, - is_pinned: bool, - is_moving: bool, - window_title: &'a str, - window_id: window::Id, - windows: &HashMap, -) -> Element<'a, WindowMessage> { - let window_selector = { - let options: Vec<_> = windows - .iter() - .map(|(id, window)| SelectableWindow(*id, window.title.clone())) - .collect(); - pick_list( - options, - Some(SelectableWindow(window_id, window_title.to_string())), - move |window| WindowMessage::SelectedWindow(pane, window), - ) - }; - - let mut move_to = button(text("Move to").size(14)).padding(3); - - let mut pop_out = button(text("Pop Out").size(14)).padding(3); - - let mut close = button(text("Close").size(14)) - .style(theme::Button::Destructive) - .padding(3); - - if total_panes > 1 && !is_pinned { - close = close.on_press(WindowMessage::Close(pane)); - pop_out = pop_out.on_press(WindowMessage::PopOut(pane)); - } - - if windows.len() > 1 && total_panes > 1 && !is_pinned { - move_to = move_to.on_press(WindowMessage::ToggleMoving(pane)); - } - - let mut content = row![].spacing(10); - if is_moving { - content = content.push(pop_out).push(window_selector).push(close); - } else { - content = content.push(pop_out).push(move_to).push(close); - } - - content.into() -} - -mod style { - use iced::widget::container; - use iced::Theme; - - pub fn title_bar_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), - ..Default::default() - } - } - - pub fn title_bar_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.primary.strong.text), - background: Some(palette.primary.strong.color.into()), - ..Default::default() - } - } - - pub fn pane_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.background.strong.color, - ..Default::default() - } - } - - pub fn pane_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.primary.strong.color, - ..Default::default() - } - } -} diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 83824535..7658384b 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,8 +1,8 @@ -use iced::alignment; use iced::keyboard::KeyCode; use iced::theme::{Button, Container}; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window::screenshot::{self, Screenshot}; +use iced::{alignment, window}; use iced::{ event, executor, keyboard, subscription, Alignment, Application, Command, ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription, @@ -71,7 +71,10 @@ impl Application for Example { fn update(&mut self, message: Self::Message) -> Command { match message { Message::Screenshot => { - return iced::window::screenshot(Message::ScreenshotData); + return iced::window::screenshot( + window::Id::MAIN, + Message::ScreenshotData, + ); } Message::ScreenshotData(screenshot) => { self.screenshot = Some(screenshot); diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 4282ddcf..e28c4236 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -528,7 +528,9 @@ mod toast { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> 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 = None; self.instants.iter_mut().enumerate().for_each( diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6ad7b4fb..04c8f618 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -164,7 +164,7 @@ impl Application for Todos { } } Message::ToggleFullscreen(mode) => { - window::change_mode(mode) + window::change_mode(window::Id::MAIN, mode) } _ => Command::none(), }; diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 0642a924..c087fdab 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -251,7 +251,7 @@ where events.filter_map(move |(event, status)| { future::ready(match event { - Event::Window(window::Event::RedrawRequested(_)) => None, + Event::Window(_, window::Event::RedrawRequested(_)) => None, _ => f(event, status), }) }) diff --git a/glutin/src/application.rs b/glutin/src/application.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 7a9e6aee..15d26346 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -12,7 +12,6 @@ categories = ["gui"] [features] geometry = ["lyon_path"] -opengl = [] image = ["dep:image", "kamadak-exif"] web-colors = [] diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index f7b86045..32111008 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -24,6 +24,9 @@ pub trait Compositor: Sized { compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error>; + /// Creates a [`Renderer`] for the [`Compositor`]. + fn renderer(&self) -> Self::Renderer; + /// Crates a new [`Surface`] for the given window. /// /// [`Surface`]: Self::Surface diff --git a/native/src/subscription.rs b/native/src/subscription.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/native/src/window.rs b/native/src/window.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 8b17a4b0..b5da31bf 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -46,6 +46,22 @@ impl crate::graphics::Compositor for Compositor { Err(error) } + fn renderer(&self) -> Self::Renderer { + match self { + Compositor::TinySkia(compositor) => { + Renderer::TinySkia(compositor.renderer()) + } + #[cfg(feature = "wgpu")] + Compositor::Wgpu(compositor) => { + Renderer::Wgpu(compositor.renderer()) + } + #[cfg(not(feature = "wgpu"))] + Self::Wgpu => { + panic!("`wgpu` feature was not enabled in `iced_renderer`") + } + } + } + fn create_surface( &mut self, window: &W, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a65f07f2..3d2976a7 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/iced-rs/iced" [features] debug = [] +multi-window = [] [dependencies] thiserror = "1" diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 4bbf9687..4c39f80f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -53,6 +53,9 @@ pub mod system; pub mod user_interface; pub mod window; +#[cfg(feature = "multi-window")] +pub mod multi_window; + // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. #[cfg(feature = "debug")] diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs new file mode 100644 index 00000000..cf778a20 --- /dev/null +++ b/runtime/src/multi_window.rs @@ -0,0 +1,6 @@ +//! A multi-window application. +pub mod program; +pub mod state; + +pub use program::Program; +pub use state::State; diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs new file mode 100644 index 00000000..c3989d0d --- /dev/null +++ b/runtime/src/multi_window/program.rs @@ -0,0 +1,32 @@ +//! Build interactive programs using The Elm Architecture. +use crate::{window, Command}; + +use crate::core::text; +use crate::core::{Element, Renderer}; + +/// 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; + + /// 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>; +} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs new file mode 100644 index 00000000..78c35e6c --- /dev/null +++ b/runtime/src/multi_window/state.rs @@ -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

+where + P: Program + 'static, +{ + program: P, + caches: Option>, + queued_events: Vec, + queued_messages: Vec, + mouse_interaction: mouse::Interaction, +} + +impl

State

+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: &::Theme, + style: &renderer::Style, + clipboard: &mut dyn Clipboard, + debug: &mut Debug, + ) -> (Vec, Option>) { + 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>>, + 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, + renderer: &mut P::Renderer, + size: Size, + debug: &mut Debug, +) -> Vec> { + 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 +} diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 5219fbfd..4737dcdd 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -3,101 +3,117 @@ mod action; pub mod screenshot; +pub use crate::core::window::Id; pub use action::Action; pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{Event, Icon, Level, Mode, UserAttention}; +use crate::core::window::{self, Event, Icon, Level, Mode, UserAttention}; use crate::core::Size; use crate::futures::subscription::{self, Subscription}; /// Subscribes to the frames of the window of the running application. /// /// 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. /// /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. pub fn frames() -> Subscription { subscription::raw_events(|event, _status| match event { - iced_core::Event::Window(Event::RedrawRequested(at)) => Some(at), + iced_core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), _ => None, }) } -/// Closes the current window and exits the application. -pub fn close() -> Command { - Command::single(command::Action::Window(Action::Close)) +/// Spawns a new window with the given `id` and `settings`. +pub fn spawn( + id: window::Id, + settings: window::Settings, +) -> Command { + Command::single(command::Action::Window(id, Action::Spawn { settings })) +} + +/// Closes the window with `id`. +pub fn close(id: window::Id) -> Command { + Command::single(command::Action::Window(id, Action::Close)) } /// Begins dragging the window while the left mouse button is held. -pub fn drag() -> Command { - Command::single(command::Action::Window(Action::Drag)) +pub fn drag(id: window::Id) -> Command { + Command::single(command::Action::Window(id, Action::Drag)) } /// Resizes the window to the given logical dimensions. -pub fn resize(new_size: Size) -> Command { - Command::single(command::Action::Window(Action::Resize(new_size))) +pub fn resize( + id: window::Id, + new_size: Size, +) -> Command { + Command::single(command::Action::Window(id, Action::Resize(new_size))) } -/// Fetches the current window size in logical dimensions. +/// Fetches the window's size in logical dimensions. pub fn fetch_size( + id: window::Id, f: impl FnOnce(Size) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(Action::FetchSize(Box::new(f)))) + Command::single(command::Action::Window(id, Action::FetchSize(Box::new(f)))) } /// Maximizes the window. -pub fn maximize(maximized: bool) -> Command { - Command::single(command::Action::Window(Action::Maximize(maximized))) +pub fn maximize(id: window::Id, maximized: bool) -> Command { + Command::single(command::Action::Window(id, Action::Maximize(maximized))) } -/// Minimes the window. -pub fn minimize(minimized: bool) -> Command { - Command::single(command::Action::Window(Action::Minimize(minimized))) +/// Minimizes the window. +pub fn minimize(id: window::Id, minimized: bool) -> Command { + Command::single(command::Action::Window(id, Action::Minimize(minimized))) } -/// Moves a window to the given logical coordinates. -pub fn move_to(x: i32, y: i32) -> Command { - Command::single(command::Action::Window(Action::Move { x, y })) +/// Moves the window to the given logical coordinates. +pub fn move_to(id: window::Id, x: i32, y: i32) -> Command { + Command::single(command::Action::Window(id, Action::Move { x, y })) } /// Changes the [`Mode`] of the window. -pub fn change_mode(mode: Mode) -> Command { - Command::single(command::Action::Window(Action::ChangeMode(mode))) +pub fn change_mode(id: window::Id, mode: Mode) -> Command { + Command::single(command::Action::Window(id, Action::ChangeMode(mode))) } /// Fetches the current [`Mode`] of the window. pub fn fetch_mode( + id: window::Id, f: impl FnOnce(Mode) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(Action::FetchMode(Box::new(f)))) + Command::single(command::Action::Window(id, Action::FetchMode(Box::new(f)))) } /// Toggles the window to maximized or back. -pub fn toggle_maximize() -> Command { - Command::single(command::Action::Window(Action::ToggleMaximize)) +pub fn toggle_maximize(id: window::Id) -> Command { + Command::single(command::Action::Window(id, Action::ToggleMaximize)) } /// Toggles the window decorations. -pub fn toggle_decorations() -> Command { - Command::single(command::Action::Window(Action::ToggleDecorations)) +pub fn toggle_decorations(id: window::Id) -> Command { + Command::single(command::Action::Window(id, Action::ToggleDecorations)) } -/// 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, /// see [`UserAttention`] for details. /// /// 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. pub fn request_user_attention( + id: window::Id, user_attention: Option, ) -> Command { - Command::single(command::Action::Window(Action::RequestUserAttention( - user_attention, - ))) + Command::single(command::Action::Window( + id, + Action::RequestUserAttention(user_attention), + )) } /// Brings the window to the front and sets input focus. Has no effect if the window is @@ -106,30 +122,36 @@ pub fn request_user_attention( /// 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 /// user experience. -pub fn gain_focus() -> Command { - Command::single(command::Action::Window(Action::GainFocus)) +pub fn gain_focus(id: window::Id) -> Command { + Command::single(command::Action::Window(id, Action::GainFocus)) } /// Changes the window [`Level`]. -pub fn change_level(level: Level) -> Command { - Command::single(command::Action::Window(Action::ChangeLevel(level))) +pub fn change_level(id: window::Id, level: Level) -> Command { + Command::single(command::Action::Window(id, Action::ChangeLevel(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 [`window::Id`]. pub fn fetch_id( + id: window::Id, f: impl FnOnce(u64) -> Message + 'static, ) -> Command { - Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) + Command::single(command::Action::Window(id, Action::FetchId(Box::new(f)))) } /// Changes the [`Icon`] of the window. -pub fn change_icon(icon: Icon) -> Command { - Command::single(command::Action::Window(Action::ChangeIcon(icon))) +pub fn change_icon(id: window::Id, icon: Icon) -> Command { + Command::single(command::Action::Window(id, Action::ChangeIcon(icon))) } /// Captures a [`Screenshot`] from the window. pub fn screenshot( + id: window::Id, f: impl FnOnce(Screenshot) -> Message + Send + 'static, ) -> Command { - Command::single(command::Action::Window(Action::Screenshot(Box::new(f)))) + Command::single(command::Action::Window( + id, + Action::Screenshot(Box::new(f)), + )) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index cebec4ae..d631cee1 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::core::window::{Icon, Level, Mode, UserAttention, Settings}; +use crate::core::window::{Icon, Level, Mode, Settings, UserAttention}; use crate::core::Size; use crate::futures::MaybeSend; use crate::window::Screenshot; @@ -15,7 +15,7 @@ pub enum Action { /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. Drag, - /// Spawns a new window with the provided [`window::Settings`]. + /// Spawns a new window. Spawn { /// The settings of the [`Window`]. settings: Settings, diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 9974128c..0486159e 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -1,30 +1,37 @@ use crate::window; use crate::{Command, Element, Executor, Settings, Subscription}; -pub use iced_native::application::{Appearance, StyleSheet}; +pub use crate::style::application::{Appearance, StyleSheet}; /// 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 `` 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. For example, to spawn a new window, you -/// can use the `iced_winit::window::spawn()` [`Command`]. +/// [`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; -/// use iced::multi_window::Application; -/// use iced::window; +/// 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()) @@ -32,17 +39,17 @@ pub use iced_native::application::{Appearance, StyleSheet}; /// /// struct Hello; /// -/// impl Application for Hello { +/// impl multi_window::Application for Hello { /// type Executor = executor::Default; +/// type Flags = (); /// type Message = (); /// type Theme = Theme; -/// type Flags = (); /// /// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { /// (Hello, Command::none()) /// } /// -/// fn title(&self, window: window::Id) -> String { +/// fn title(&self, _window: window::Id) -> String { /// String::from("A cool application") /// } /// @@ -50,13 +57,9 @@ pub use iced_native::application::{Appearance, StyleSheet}; /// Command::none() /// } /// -/// fn view(&self, window: window::Id) -> Element<Self::Message> { +/// fn view(&self, _window: window::Id) -> Element<Self::Message> { /// "Hello, world!".into() /// } -/// -/// fn close_requested(&self, window: window::Id) -> Self::Message { -/// () -/// } /// } /// ``` pub trait Application: Sized { @@ -89,10 +92,10 @@ pub trait Application: Sized { /// [`run`]: Self::run fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); - /// Returns the current title of the [`Application`]. + /// Returns the current title of the `window` of the [`Application`]. /// /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. + /// title of your window when necessary. fn title(&self, window: window::Id) -> String; /// Handles a __message__ and updates the state of the [`Application`]. @@ -104,7 +107,15 @@ pub trait Application: Sized { /// Any [`Command`] returned will be executed immediately in the background. fn update(&mut self, message: Self::Message) -> Command<Self::Message>; - /// Returns the current [`Theme`] of the [`Application`]. + /// 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)] @@ -112,9 +123,8 @@ pub trait Application: Sized { Self::Theme::default() } - /// Returns the current [`Style`] of the [`Theme`]. + /// Returns the current `Style` of the [`Theme`]. /// - /// [`Style`]: <Self::Theme as StyleSheet>::Style /// [`Theme`]: Self::Theme fn style(&self) -> <Self::Theme as StyleSheet>::Style { <Self::Theme as StyleSheet>::Style::default() @@ -132,14 +142,6 @@ pub trait Application: Sized { Subscription::none() } - /// Returns the widgets to display in 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 scale factor of the `window` of the [`Application`]. /// /// It can be used to dynamically control the size of the UI at runtime @@ -154,18 +156,7 @@ pub trait Application: Sized { 1.0 } - /// Returns whether the [`Application`] should be terminated. - /// - /// By default, it returns `false`. - fn should_exit(&self) -> bool { - false - } - - /// Returns the `Self::Message` that should be processed when a `window` is requested to - /// be closed. - fn close_requested(&self, window: window::Id) -> Self::Message; - - /// Runs the [`Application`]. + /// Runs the multi-window [`Application`]. /// /// On native platforms, this method will take control of the current thread /// until the [`Application`] exits. @@ -182,30 +173,28 @@ pub trait Application: Sized { let renderer_settings = crate::renderer::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, - text_multithreading: settings.text_multithreading, antialiasing: if settings.antialiasing { - Some(crate::renderer::settings::Antialiasing::MSAAx4) + Some(crate::graphics::Antialiasing::MSAAx4) } else { None }, - ..crate::renderer::Settings::from_env() + ..crate::renderer::Settings::default() }; - Ok(crate::runtime::multi_window::run::< + Ok(crate::shell::multi_window::run::< Instance<Self>, Self::Executor, - crate::renderer::window::Compositor<Self::Theme>, + crate::renderer::Compositor<Self::Theme>, >(settings.into(), renderer_settings)?) } } struct Instance<A: Application>(A); -impl<A> crate::runtime::multi_window::Application for Instance<A> +impl<A> crate::runtime::multi_window::Program for Instance<A> where A: Application, { - type Flags = A::Flags; type Renderer = crate::Renderer<A::Theme>; type Message = A::Message; @@ -219,6 +208,13 @@ where ) -> 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); @@ -245,12 +241,4 @@ where fn scale_factor(&self, window: window::Id) -> f64 { self.0.scale_factor(window) } - - fn should_exit(&self) -> bool { - self.0.should_exit() - } - - fn close_requested(&self, window: window::Id) -> Self::Message { - self.0.close_requested(window) - } } diff --git a/src/settings.rs b/src/settings.rs index 0dd46584..4ce2d135 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -91,7 +91,7 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { iced_winit::Settings { id: settings.id, - window: settings.window.into(), + window: settings.window, flags: settings.flags, exit_on_close_request: settings.exit_on_close_request, } diff --git a/src/window.rs b/src/window.rs index e4601575..9f96da52 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,12 +1,8 @@ //! Configure the window of your application in native platforms. -mod position; -mod settings; pub mod icon; pub use icon::Icon; -pub use position::Position; -pub use settings::{PlatformSpecific, Settings}; pub use crate::core::window::*; pub use crate::runtime::window::*; diff --git a/src/window/icon.rs b/src/window/icon.rs new file mode 100644 index 00000000..0fe010ca --- /dev/null +++ b/src/window/icon.rs @@ -0,0 +1,63 @@ +//! Attach an icon to the window of your application. +pub use crate::core::window::icon::*; + +use crate::core::window::icon; + +use std::io; + +#[cfg(feature = "image")] +use std::path::Path; + +/// Creates an icon from an image file. +/// +/// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. +#[cfg(feature = "image")] +pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> { + let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); + + Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) +} + +/// Creates an icon from the content of an image file. +/// +/// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. +/// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. +#[cfg(feature = "image")] +pub fn from_file_data( + data: &[u8], + explicit_format: Option<image_rs::ImageFormat>, +) -> Result<Icon, Error> { + let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data)); + let icon_with_format = match explicit_format { + Some(format) => { + icon.set_format(format); + icon + } + None => icon.with_guessed_format()?, + }; + + let pixels = icon_with_format.decode()?.to_rgba8(); + + Ok(icon::from_rgba( + pixels.to_vec(), + pixels.width(), + pixels.height(), + )?) +} + +/// An error produced when creating an [`Icon`]. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The [`Icon`] is not valid. + #[error("The icon is invalid: {0}")] + InvalidError(#[from] icon::Error), + + /// The underlying OS failed to create the icon. + #[error("The underlying OS failted to create the window icon: {0}")] + OsError(#[from] io::Error), + + /// The `image` crate reported an error. + #[cfg(feature = "image")] + #[error("Unable to create icon from a file: {0}")] + ImageError(#[from] image_rs::error::ImageError), +} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 775cf9e5..1aaba2c9 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -8,6 +8,7 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; pub struct Compositor<Theme> { + settings: Settings, _theme: PhantomData<Theme>, } @@ -33,6 +34,10 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { Ok((compositor, Renderer::new(backend))) } + fn renderer(&self) -> Self::Renderer { + Renderer::new(Backend::new(self.settings)) + } + fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( &mut self, window: &W, @@ -116,6 +121,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) { ( Compositor { + settings, _theme: PhantomData, }, Backend::new(settings), diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index cd5b20cc..814269f3 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -219,6 +219,10 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { Ok((compositor, Renderer::new(backend))) } + fn renderer(&self) -> Self::Renderer { + Renderer::new(self.create_backend()) + } + fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( &mut self, window: &W, diff --git a/winit/Cargo.toml b/winit/Cargo.toml index a4c0a402..30cec0b8 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -12,8 +12,6 @@ categories = ["gui"] [features] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -trace = ["tracing", "tracing-core", "tracing-subscriber"] -chrome-trace = ["trace", "tracing-chrome"] debug = ["iced_runtime/debug"] system = ["sysinfo"] application = [] @@ -21,7 +19,7 @@ x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] -multi-window = [] +multi-window = ["iced_runtime/multi-window"] [dependencies] window_clipboard = "0.3" @@ -47,24 +45,6 @@ path = "../graphics" version = "0.8" path = "../style" -[dependencies.tracing] -version = "0.1.37" -optional = true -features = ["std"] - -[dependencies.tracing-core] -version = "0.1.30" -optional = true - -[dependencies.tracing-subscriber] -version = "0.3.16" -optional = true -features = ["registry"] - -[dependencies.tracing-chrome] -version = "0.7.0" -optional = true - [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" diff --git a/winit/src/application.rs b/winit/src/application.rs index ab7b2495..5c45bbda 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -18,6 +18,7 @@ use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::{Command, Debug}; +use crate::settings; use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; @@ -25,11 +26,6 @@ use futures::channel::mpsc; use std::mem::ManuallyDrop; -#[cfg(feature = "trace")] -pub use crate::Profiler; -#[cfg(feature = "trace")] -use tracing::{info_span, instrument::Instrument}; - /// An interactive, native cross-platform application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run @@ -117,15 +113,9 @@ where use futures::Future; use winit::event_loop::EventLoopBuilder; - #[cfg(feature = "trace")] - let _guard = Profiler::init(); - let mut debug = Debug::new(); debug.startup_started(); - #[cfg(feature = "trace")] - let _ = info_span!("Application", "RUN").entered(); - let event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -146,14 +136,13 @@ where let target = settings.window.platform_specific.target.clone(); let should_be_visible = settings.window.visible; - let builder = settings - .window - .into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ) - .with_visible(false); + let builder = settings::window_builder( + settings.window, + &application.title(), + event_loop.primary_monitor(), + settings.id, + ) + .with_visible(false); log::debug!("Window builder: {:#?}", builder); @@ -196,28 +185,20 @@ where let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); - let mut instance = Box::pin({ - let run_instance = run_instance::<A, E, C>( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - window, - should_be_visible, - settings.exit_on_close_request, - ); - - #[cfg(feature = "trace")] - let run_instance = - run_instance.instrument(info_span!("Application", "LOOP")); - - run_instance - }); + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + renderer, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + window, + should_be_visible, + settings.exit_on_close_request, + )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -480,9 +461,6 @@ async fn run_instance<A, E, C>( messages.push(message); } event::Event::RedrawRequested(_) => { - #[cfg(feature = "trace")] - let _ = info_span!("Application", "FRAME").entered(); - let physical_size = state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -622,24 +600,12 @@ pub fn build_user_interface<'a, A: Application>( where <A::Renderer as core::Renderer>::Theme: StyleSheet, { - #[cfg(feature = "trace")] - let view_span = info_span!("Application", "VIEW").entered(); - debug.view_started(); let view = application.view(); - - #[cfg(feature = "trace")] - let _ = view_span.exit(); debug.view_finished(); - #[cfg(feature = "trace")] - let layout_span = info_span!("Application", "LAYOUT").entered(); - debug.layout_started(); let user_interface = UserInterface::build(view, size, cache, renderer); - - #[cfg(feature = "trace")] - let _ = layout_span.exit(); debug.layout_finished(); user_interface @@ -666,16 +632,10 @@ pub fn update<A: Application, C, E: Executor>( <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { - #[cfg(feature = "trace")] - let update_span = info_span!("Application", "UPDATE").entered(); - debug.log_message(&message); debug.update_started(); let command = runtime.enter(|| application.update(message)); - - #[cfg(feature = "trace")] - let _ = update_span.exit(); debug.update_finished(); run_command( @@ -750,7 +710,7 @@ pub fn run_command<A, C, E>( } window::Action::Spawn { .. } => { log::info!( - "Spawning a window is only available with `multi_window::Application`s." + "Spawning a window is only available with multi-window applications." ) } window::Action::Resize(size) => { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index fe0fce19..0625e74b 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -7,7 +7,6 @@ use crate::core::mouse; use crate::core::touch; use crate::core::window; use crate::core::{Event, Point}; -use crate::Position; /// Converts a winit window event into an iced event. pub fn window_event( @@ -169,17 +168,17 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel { pub fn position( monitor: Option<&winit::monitor::MonitorHandle>, (width, height): (u32, u32), - position: Position, + position: window::Position, ) -> Option<winit::dpi::Position> { match position { - Position::Default => None, - Position::Specific(x, y) => { + window::Position::Default => None, + window::Position::Specific(x, y) => { Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { x: f64::from(x), y: f64::from(y), })) } - Position::Centered => { + window::Position::Centered => { if let Some(monitor) = monitor { let start = monitor.position(); diff --git a/winit/src/icon.rs b/winit/src/icon.rs deleted file mode 100644 index 0fe010ca..00000000 --- a/winit/src/icon.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Attach an icon to the window of your application. -pub use crate::core::window::icon::*; - -use crate::core::window::icon; - -use std::io; - -#[cfg(feature = "image")] -use std::path::Path; - -/// Creates an icon from an image file. -/// -/// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. -#[cfg(feature = "image")] -pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> { - let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); - - Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) -} - -/// Creates an icon from the content of an image file. -/// -/// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. -/// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. -#[cfg(feature = "image")] -pub fn from_file_data( - data: &[u8], - explicit_format: Option<image_rs::ImageFormat>, -) -> Result<Icon, Error> { - let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data)); - let icon_with_format = match explicit_format { - Some(format) => { - icon.set_format(format); - icon - } - None => icon.with_guessed_format()?, - }; - - let pixels = icon_with_format.decode()?.to_rgba8(); - - Ok(icon::from_rgba( - pixels.to_vec(), - pixels.width(), - pixels.height(), - )?) -} - -/// An error produced when creating an [`Icon`]. -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// The [`Icon`] is not valid. - #[error("The icon is invalid: {0}")] - InvalidError(#[from] icon::Error), - - /// The underlying OS failed to create the icon. - #[error("The underlying OS failted to create the window icon: {0}")] - OsError(#[from] io::Error), - - /// The `image` crate reported an error. - #[cfg(feature = "image")] - #[error("Unable to create icon from a file: {0}")] - ImageError(#[from] image_rs::error::ImageError), -} diff --git a/winit/src/lib.rs b/winit/src/lib.rs index dc163430..31002f51 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -51,20 +51,14 @@ pub mod settings; pub mod system; mod error; -mod icon; mod proxy; -#[cfg(feature = "trace")] -mod profiler; #[cfg(feature = "application")] pub use application::Application; -#[cfg(feature = "trace")] -pub use profiler::Profiler; pub use clipboard::Clipboard; pub use error::Error; -pub use icon::Icon; pub use proxy::Proxy; pub use settings::Settings; +pub use crate::core::window::*; pub use iced_graphics::Viewport; -pub use iced_native::window::Position; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 9b395c1d..e6f440bc 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1,58 +1,57 @@ //! Create interactive, native cross-platform applications for WGPU. mod state; +mod windows; pub use state::State; -use crate::clipboard::{self, Clipboard}; -use crate::conversion; -use crate::mouse; -use crate::renderer; -use crate::settings; -use crate::widget::operation; -use crate::window; -use crate::{ - Command, Debug, Element, Error, Executor, Proxy, Renderer, Runtime, - Settings, Size, Subscription, -}; - -use iced_futures::futures::channel::mpsc; -use iced_futures::futures::{self, FutureExt}; -use iced_graphics::compositor; -use iced_native::user_interface::{self, UserInterface}; - -pub use iced_native::application::{Appearance, StyleSheet}; - -use std::collections::HashMap; +use crate::core::widget::operation; +use crate::core::{self, mouse, renderer, window, Size}; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::{task, Future, FutureExt, StreamExt}; +use crate::futures::{Executor, Runtime, Subscription}; +use crate::graphics::{compositor, Compositor}; +use crate::multi_window::windows::Windows; +use crate::runtime::command::{self, Command}; +use crate::runtime::multi_window::Program; +use crate::runtime::user_interface::{self, UserInterface}; +use crate::runtime::Debug; +use crate::settings::window_builder; +use crate::style::application::StyleSheet; +use crate::{conversion, settings, Clipboard, Error, Proxy, Settings}; + +use iced_runtime::user_interface::Cache; use std::mem::ManuallyDrop; use std::time::Instant; - -#[cfg(feature = "trace")] -pub use crate::Profiler; -#[cfg(feature = "trace")] -use tracing::{info_span, instrument::Instrument}; +use winit::monitor::MonitorHandle; /// This is a wrapper around the `Application::Message` associate type /// to allows the `shell` to create internal messages, while still having /// the current user-specified custom messages. #[derive(Debug)] pub enum Event<Message> { - /// An [`Application`] generated message + /// An internal event which contains an [`Application`] generated message. Application(Message), - /// A message which spawns a new window. + /// An internal event which spawns a new window. NewWindow { /// The [window::Id] of the newly spawned [`Window`]. id: window::Id, /// The [settings::Window] of the newly spawned [`Window`]. - settings: settings::Window, + settings: window::Settings, /// The title of the newly spawned [`Window`]. title: String, + /// The monitor on which to spawn the window. If `None`, will use monitor of the last window + /// spawned. + monitor: Option<MonitorHandle>, }, - /// Close a window. + /// An internal event for closing a window. CloseWindow(window::Id), - /// A message for when the window has finished being created. + /// An internal event for when the window has finished being created. WindowCreated(window::Id, winit::window::Window), } +#[allow(unsafe_code)] +unsafe impl<Message> std::marker::Send for Event<Message> {} + /// An interactive, native, cross-platform, multi-windowed application. /// /// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run @@ -64,37 +63,13 @@ pub enum Event<Message> { /// /// When using an [`Application`] with the `debug` feature enabled, a debug view /// can be toggled by pressing `F12`. -pub trait Application: Sized +pub trait Application: Program where - <Self::Renderer as crate::Renderer>::Theme: StyleSheet, + <Self::Renderer as core::Renderer>::Theme: StyleSheet, { /// The data needed to initialize your [`Application`]. type Flags; - /// The graphics backend to use to draw the [`Program`]. - type Renderer: 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 for the `window` in the [`Program`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Renderer>; - /// Initializes the [`Application`] with the flags provided to /// [`run`] as part of the [`Settings`]. /// @@ -105,19 +80,22 @@ where /// load state from a file, perform an initial HTTP request, etc. fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); - /// Returns the current title of each window of the [`Application`]. + /// Returns the current title of the [`Application`]. /// /// This title can be dynamic! The runtime will automatically update the /// title of your application when necessary. fn title(&self, window: window::Id) -> String; - /// Returns the current [`Theme`] of the [`Application`]. - fn theme(&self, window: window::Id) -> <Self::Renderer as crate::Renderer>::Theme; + /// Returns the current `Theme` of the [`Application`]. + fn theme( + &self, + window: window::Id, + ) -> <Self::Renderer as core::Renderer>::Theme; - /// Returns the [`Style`] variation of the [`Theme`]. + /// Returns the `Style` variation of the `Theme`. fn style( &self, - ) -> <<Self::Renderer as crate::Renderer>::Theme as StyleSheet>::Style { + ) -> <<Self::Renderer as core::Renderer>::Theme as StyleSheet>::Style { Default::default() } @@ -147,17 +125,6 @@ where fn scale_factor(&self, window: window::Id) -> f64 { 1.0 } - - /// Returns whether the [`Application`] should be terminated. - /// - /// By default, it returns `false`. - fn should_exit(&self) -> bool { - false - } - - /// Returns the `Self::Message` that should be processed when a `window` is requested to - /// be closed. - fn close_requested(&self, window: window::Id) -> Self::Message; } /// Runs an [`Application`] with an executor, compositor, and the provided @@ -169,22 +136,14 @@ pub fn run<A, E, C>( where A: Application + 'static, E: Executor + 'static, - C: iced_graphics::window::Compositor<Renderer = A::Renderer> + 'static, - <A::Renderer as crate::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, { - use futures::task; - use futures::Future; use winit::event_loop::EventLoopBuilder; - #[cfg(feature = "trace")] - let _guard = Profiler::init(); - let mut debug = Debug::new(); debug.startup_started(); - #[cfg(feature = "trace")] - let _ = info_span!("Application", "RUN").entered(); - let event_loop = EventLoopBuilder::with_user_event().build(); let proxy = event_loop.create_proxy(); @@ -201,68 +160,77 @@ where runtime.enter(|| A::new(flags)) }; - let builder = settings.window.into_builder( + let should_main_be_visible = settings.window.visible; + let builder = window_builder( + settings.window, &application.title(window::Id::MAIN), event_loop.primary_monitor(), settings.id, - ); + ) + .with_visible(false); log::info!("Window builder: {:#?}", builder); - let window = builder + let main_window = builder .build(&event_loop) .map_err(Error::WindowCreationFailed)?; - let windows: HashMap<window::Id, winit::window::Window> = - HashMap::from([(window::Id::MAIN, window)]); - - let window = windows.values().next().expect("No window found"); - #[cfg(target_arch = "wasm32")] { use winit::platform::web::WindowExtWebSys; - let canvas = window.canvas(); + let canvas = main_window.canvas(); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let body = document.body().unwrap(); - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); + let target = target.and_then(|target| { + body.query_selector(&format!("#{}", target)) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1(&canvas) + .expect(&format!("Could not replace #{}", node.id())); + } + None => { + let _ = body + .append_child(&canvas) + .expect("Append canvas to HTML body"); + } + }; } - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + let (mut compositor, renderer) = + C::new(compositor_settings, Some(&main_window))?; + + let windows = + Windows::new(&application, &mut compositor, renderer, main_window); let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); - let mut instance = Box::pin({ - let run_instance = run_instance::<A, E, C>( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - init_command, - windows, - settings.exit_on_close_request, - ); - - #[cfg(feature = "trace")] - let run_instance = - run_instance.instrument(info_span!("Application", "LOOP")); - - run_instance - }); + let mut instance = Box::pin(run_instance::<A, E, C>( + application, + compositor, + runtime, + proxy, + debug, + event_receiver, + control_sender, + init_command, + windows, + should_main_be_visible, + settings.exit_on_close_request, + )); let mut context = task::Context::from_waker(task::noop_waker_ref()); - platform::run(event_loop, move |event, event_loop, control_flow| { + platform::run(event_loop, move |event, window_target, control_flow| { use winit::event_loop::ControlFlow; if let ControlFlow::ExitWithCode(_) = control_flow { @@ -285,11 +253,12 @@ where id, settings, title, + monitor, }) => { - let window = settings - .into_builder(&title, event_loop.primary_monitor(), None) - .build(event_loop) - .expect("Failed to build window"); + let window = + settings::window_builder(settings, &title, monitor, None) + .build(window_target) + .expect("Failed to build window"); Some(winit::event::Event::UserEvent(Event::WindowCreated( id, window, @@ -320,7 +289,6 @@ where async fn run_instance<A, E, C>( mut application: A, mut compositor: C, - mut renderer: A::Renderer, mut runtime: Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, mut proxy: winit::event_loop::EventLoopProxy<Event<A::Message>>, mut debug: Debug, @@ -329,74 +297,65 @@ async fn run_instance<A, E, C>( >, mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, init_command: Command<A::Message>, - mut windows: HashMap<window::Id, winit::window::Window>, - _exit_on_close_request: bool, + mut windows: Windows<A, C>, + should_main_window_be_visible: bool, + exit_on_main_closed: bool, ) where A: Application + 'static, E: Executor + 'static, - C: iced_graphics::window::Compositor<Renderer = A::Renderer> + 'static, - <A::Renderer as crate::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, { - use iced_futures::futures::stream::StreamExt; use winit::event; use winit::event_loop::ControlFlow; - let mut clipboard = - Clipboard::connect(windows.values().next().expect("No window found")); - let mut caches = HashMap::new(); - let mut window_ids: HashMap<_, _> = windows - .iter() - .map(|(&id, window)| (window.id(), id)) - .collect(); - - let mut states = HashMap::new(); - let mut surfaces = HashMap::new(); - let mut interfaces = ManuallyDrop::new(HashMap::new()); - - for (&id, window) in windows.keys().zip(windows.values()) { - let mut surface = compositor.create_surface(window); - let state = State::new(&application, id, window); - let physical_size = state.physical_size(); - - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); + let mut clipboard = Clipboard::connect(windows.main()); - let user_interface = build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - id, - ); + let mut ui_caches = vec![user_interface::Cache::default()]; + let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut windows, + vec![user_interface::Cache::default()], + )); - let _ = states.insert(id, state); - let _ = surfaces.insert(id, surface); - let _ = interfaces.insert(id, user_interface); - let _ = caches.insert(id, user_interface::Cache::default()); + if should_main_window_be_visible { + windows.main().set_visible(true); } run_command( &application, - &mut caches, - &states, - &mut renderer, + &mut compositor, init_command, &mut runtime, &mut clipboard, &mut proxy, &mut debug, - &windows, - || compositor.fetch_information(), + &mut windows, + &mut ui_caches, ); - runtime.track(application.subscription().map(Event::Application)); + runtime.track( + application + .subscription() + .map(Event::Application) + .into_recipes(), + ); let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); + + let mut events = + if let Some((position, size)) = logical_bounds_of(windows.main()) { + vec![( + Some(window::Id::MAIN), + core::Event::Window( + window::Id::MAIN, + window::Event::Created { position, size }, + ), + )] + } else { + Vec::new() + }; let mut messages = Vec::new(); let mut redraw_pending = false; @@ -413,25 +372,20 @@ async fn run_instance<A, E, C>( ); } event::Event::MainEventsCleared => { - for id in states.keys().copied().collect::<Vec<_>>() { - // Partition events into only events for this window - let (filtered, remaining): (Vec<_>, Vec<_>) = - events.iter().cloned().partition( - |(window_id, _event): &( - Option<window::Id>, - iced_native::event::Event, - )| { - *window_id == Some(id) || *window_id == None - }, - ); - - // Only retain events which have not been processed for next iteration - events.retain(|el| remaining.contains(el)); - - let window_events: Vec<_> = filtered - .into_iter() - .map(|(_id, event)| event) - .collect(); + debug.event_processing_started(); + let mut uis_stale = false; + + for (i, id) in windows.ids.iter().enumerate() { + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == Some(*id) || window_id.is_none() { + window_events.push(event.clone()); + false + } else { + true + } + }); if !redraw_pending && window_events.is_empty() @@ -440,144 +394,124 @@ async fn run_instance<A, E, C>( continue; } - // Process winit events for window - debug.event_processing_started(); - let cursor_position = - states.get(&id).unwrap().cursor_position(); - - let (interface_state, statuses) = { - let user_interface = interfaces.get_mut(&id).unwrap(); - user_interface.update( - &window_events, - cursor_position, - &mut renderer, - &mut clipboard, - &mut messages, - ) - }; + let (ui_state, statuses) = user_interfaces[i].update( + &window_events, + windows.states[i].cursor(), + &mut windows.renderers[i], + &mut clipboard, + &mut messages, + ); + + if !uis_stale { + uis_stale = + matches!(ui_state, user_interface::State::Outdated); + } - for event in + for (event, status) in window_events.into_iter().zip(statuses.into_iter()) { - runtime.broadcast(event); + runtime.broadcast(event, status); } - debug.event_processing_finished(); - - // Update application with app messages - // Unless we implement some kind of diffing, we must redraw all windows as we - // cannot know what changed. - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated, - ) - { - let mut cached_interfaces: HashMap<_, _> = - ManuallyDrop::into_inner(interfaces) - .drain() - .map( - |(id, interface): ( - window::Id, - UserInterface<'_, _, _>, - )| { - (id, interface.into_cache()) - }, - ) - .collect(); - - // Update application - update( - &mut application, - &mut cached_interfaces, - &states, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &mut messages, - &windows, - || compositor.fetch_information(), - ); - - // synchronize window states with application states. - for (id, state) in states.iter_mut() { - state.synchronize( - &application, - *id, - windows - .get(id) - .expect("No window found with ID."), - ); - } + } - interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut renderer, - &mut debug, - &states, - cached_interfaces, - )); + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let mut cached_interfaces: Vec<Cache> = + ManuallyDrop::into_inner(user_interfaces) + .drain(..) + .map(UserInterface::into_cache) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut proxy, + &mut debug, + &mut messages, + &mut windows, + &mut cached_interfaces, + ); - if application.should_exit() { - break 'main; - } + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (state, (id, window)) in windows + .states + .iter_mut() + .zip(windows.ids.iter().zip(windows.raw.iter())) + { + state.synchronize(&application, *id, window); } + // rebuild UIs with the synchronized states + user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut windows, + cached_interfaces, + )); + } + + debug.draw_started(); + + for (i, id) in windows.ids.iter().enumerate() { // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes + // request redraws on state changes // // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. - let redraw_event = iced_native::Event::Window( - id, + let redraw_event = core::Event::Window( + *id, window::Event::RedrawRequested(Instant::now()), ); - let (interface_state, _) = - interfaces.get_mut(&id).unwrap().update( - &[redraw_event.clone()], - cursor_position, - &mut renderer, - &mut clipboard, - &mut messages, - ); + let cursor = windows.states[i].cursor(); + + let (ui_state, _) = user_interfaces[i].update( + &[redraw_event.clone()], + cursor, + &mut windows.renderers[i], + &mut clipboard, + &mut messages, + ); - debug.draw_started(); let new_mouse_interaction = { - let state = states.get(&id).unwrap(); + let state = &windows.states[i]; - interfaces.get_mut(&id).unwrap().draw( - &mut renderer, + user_interfaces[i].draw( + &mut windows.renderers[i], state.theme(), &renderer::Style { text_color: state.text_color(), }, - state.cursor_position(), + cursor, ) }; - debug.draw_finished(); - - let window = windows.get(&id).unwrap(); if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); + windows.raw[i].set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); mouse_interaction = new_mouse_interaction; } - for window in windows.values() { - window.request_redraw(); - } + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + windows.raw[i].request_redraw(); - runtime.broadcast(( + runtime.broadcast( redraw_event.clone(), - crate::event::Status::Ignored, - )); + core::event::Status::Ignored, + ); - let _ = control_sender.start_send(match interface_state { + let _ = control_sender.start_send(match ui_state { user_interface::State::Updated { redraw_request: Some(redraw_request), } => match redraw_request { @@ -590,17 +524,20 @@ async fn run_instance<A, E, C>( }, _ => ControlFlow::Wait, }); - - redraw_pending = false; } + + redraw_pending = false; + + debug.draw_finished(); } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), )) => { - use iced_native::event; + use crate::core::event; + events.push(( None, - iced_native::Event::PlatformSpecific( + event::Event::PlatformSpecific( event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), ), @@ -612,91 +549,48 @@ async fn run_instance<A, E, C>( messages.push(message); } Event::WindowCreated(id, window) => { - let mut surface = compositor.create_surface(&window); - let state = State::new(&application, id, &window); - let physical_size = state.physical_size(); + let bounds = logical_bounds_of(&window); - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); + let (inner_size, i) = + windows.add(&application, &mut compositor, id, window); - let user_interface = build_user_interface( + user_interfaces.push(build_user_interface( &application, user_interface::Cache::default(), - &mut renderer, - state.logical_size(), + &mut windows.renderers[i], + inner_size, &mut debug, id, - ); - - let _ = states.insert(id, state); - let _ = surfaces.insert(id, surface); - let _ = interfaces.insert(id, user_interface); - let _ = window_ids.insert(window.id(), id); - let _ = windows.insert(id, window); - let _ = caches.insert(id, user_interface::Cache::default()); + )); + ui_caches.push(user_interface::Cache::default()); + + if let Some(bounds) = bounds { + events.push(( + Some(id), + core::Event::Window( + id, + window::Event::Created { + position: bounds.0, + size: bounds.1, + }, + ), + )); + } } Event::CloseWindow(id) => { - if let Some(window) = windows.get(&id) { - if window_ids.remove(&window.id()).is_none() { - log::error!("Failed to remove window with id {:?} from window_ids.", window.id()); - } - } else { - log::error!( - "Could not find window with id {:?} in windows.", - id - ); - } - if states.remove(&id).is_none() { - log::error!( - "Failed to remove window {:?} from states.", - id - ); - } - if interfaces.remove(&id).is_none() { - log::error!( - "Failed to remove window {:?} from interfaces.", - id - ); - } - if windows.remove(&id).is_none() { - log::error!( - "Failed to remove window {:?} from windows.", - id - ); - } - if surfaces.remove(&id).is_none() { - log::error!( - "Failed to remove window {:?} from surfaces.", - id - ); - } + let i = windows.delete(id); + let _ = user_interfaces.remove(i); + let _ = ui_caches.remove(i); if windows.is_empty() { - log::info!( - "All windows are closed. Terminating program." - ); break 'main; - } else { - log::info!("Remaining windows: {:?}", windows.len()); } } Event::NewWindow { .. } => unreachable!(), }, event::Event::RedrawRequested(id) => { - #[cfg(feature = "trace")] - let _ = info_span!("Application", "FRAME").entered(); - - let state = window_ids - .get(&id) - .and_then(|id| states.get_mut(id)) - .unwrap(); - let surface = window_ids - .get(&id) - .and_then(|id| surfaces.get_mut(id)) - .unwrap(); + let i = windows.index_from_raw(id); + let state = &windows.states[i]; let physical_size = state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -704,60 +598,55 @@ async fn run_instance<A, E, C>( } debug.render_started(); + let current_viewport_version = state.viewport_version(); + let window_viewport_version = windows.viewport_versions[i]; - if state.viewport_changed() { - let mut user_interface = window_ids - .get(&id) - .and_then(|id| interfaces.remove(id)) - .unwrap(); - + if window_viewport_version != current_viewport_version { let logical_size = state.logical_size(); debug.layout_started(); - user_interface = - user_interface.relayout(logical_size, &mut renderer); + + let renderer = &mut windows.renderers[i]; + let ui = user_interfaces.remove(i); + + user_interfaces + .insert(i, ui.relayout(logical_size, renderer)); + debug.layout_finished(); debug.draw_started(); - let new_mouse_interaction = { - let state = &state; - - user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ) - }; + let new_mouse_interaction = user_interfaces[i].draw( + renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor(), + ); - let window = window_ids - .get(&id) - .and_then(|id| windows.get(id)) - .unwrap(); if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); + windows.raw[i].set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); mouse_interaction = new_mouse_interaction; } debug.draw_finished(); - let _ = interfaces - .insert(*window_ids.get(&id).unwrap(), user_interface); - compositor.configure_surface( - surface, + &mut windows.surfaces[i], physical_size.width, physical_size.height, ); + + windows.viewport_versions[i] = current_viewport_version; } match compositor.present( - &mut renderer, - surface, + &mut windows.renderers[i], + &mut windows.surfaces[i], state.viewport(), state.background_color(), &debug.overlay(), @@ -775,10 +664,12 @@ async fn run_instance<A, E, C>( } _ => { debug.render_finished(); - log::error!("Error {error:?} when presenting surface."); + log::error!( + "Error {error:?} when presenting surface." + ); - // Try rendering windows again next frame. - for window in windows.values() { + // Try rendering all windows again next frame. + for window in &windows.raw { window.request_redraw(); } } @@ -789,42 +680,58 @@ async fn run_instance<A, E, C>( event: window_event, window_id, } => { - if let (Some(window), Some(state)) = ( - window_ids.get(&window_id).and_then(|id| windows.get(id)), - window_ids - .get(&window_id) - .and_then(|id| states.get_mut(id)), - ) { - if crate::application::requests_exit(&window_event, state.modifiers()) { - if let Some(id) = window_ids.get(&window_id).cloned() { - let message = application.close_requested(id); - messages.push(message); - } + let window_deleted = windows + .pending_destroy + .iter() + .any(|(_, w_id)| window_id == *w_id); + + if matches!(window_event, winit::event::WindowEvent::Destroyed) + { + // This is the only special case, since in order trigger the Destroyed event the + // window reference from winit must be dropped, but we still want to inform the + // user that the window was destroyed so they can clean up any specific window + // code for this window + let id = windows.get_pending_destroy(window_id); + + events.push(( + None, + core::Event::Window(id, window::Event::Destroyed), + )); + } else if !window_deleted { + let i = windows.index_from_raw(window_id); + let id = windows.ids[i]; + let raw = &windows.raw[i]; + let state = &mut windows.states[i]; + + // first check if we need to just break the entire application + // e.g. a user does a force quit on MacOS, or if a user has set "exit on main closed" + // as an option in window settings and wants to close the main window + if requests_exit( + i, + exit_on_main_closed, + &window_event, + state.modifiers(), + ) { + break 'main; } - state.update(window, &window_event, &mut debug); + state.update(raw, &window_event, &mut debug); if let Some(event) = conversion::window_event( - *window_ids.get(&window_id).unwrap(), + id, &window_event, state.scale_factor(), state.modifiers(), ) { - events - .push((window_ids.get(&window_id).cloned(), event)); + events.push((Some(id), event)); } - } else { - log::error!( - "Could not find window or state for id: {window_id:?}" - ); } } _ => {} } } - // Manually drop the user interfaces - drop(ManuallyDrop::into_inner(interfaces)); + let _ = ManuallyDrop::into_inner(user_interfaces); } /// Builds a window's [`UserInterface`] for the [`Application`]. @@ -837,103 +744,79 @@ pub fn build_user_interface<'a, A: Application>( id: window::Id, ) -> UserInterface<'a, A::Message, A::Renderer> where - <A::Renderer as crate::Renderer>::Theme: StyleSheet, + <A::Renderer as core::Renderer>::Theme: StyleSheet, { - #[cfg(feature = "trace")] - let view_span = info_span!("Application", "VIEW").entered(); - debug.view_started(); let view = application.view(id); - - #[cfg(feature = "trace")] - let _ = view_span.exit(); debug.view_finished(); - #[cfg(feature = "trace")] - let layout_span = info_span!("Application", "LAYOUT").entered(); debug.layout_started(); - let user_interface = UserInterface::build(view, size, cache, renderer); - - #[cfg(feature = "trace")] - let _ = layout_span.exit(); debug.layout_finished(); user_interface } -/// Updates an [`Application`] by feeding it messages, spawning any +/// Updates a multi-window [`Application`] by feeding it messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update<A: Application, E: Executor>( +pub fn update<A: Application, C, E: Executor>( application: &mut A, - caches: &mut HashMap<window::Id, user_interface::Cache>, - states: &HashMap<window::Id, State<A>>, - renderer: &mut A::Renderer, + compositor: &mut C, runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy<Event<A::Message>>, debug: &mut Debug, messages: &mut Vec<A::Message>, - windows: &HashMap<window::Id, winit::window::Window>, - graphics_info: impl FnOnce() -> compositor::Information + Copy, + windows: &mut Windows<A, C>, + ui_caches: &mut Vec<user_interface::Cache>, ) where - A: Application + 'static, - <A::Renderer as crate::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { - #[cfg(feature = "trace")] - let update_span = info_span!("Application", "UPDATE").entered(); - debug.log_message(&message); - debug.update_started(); let command = runtime.enter(|| application.update(message)); - - #[cfg(feature = "trace")] - let _ = update_span.exit(); debug.update_finished(); run_command( application, - caches, - states, - renderer, + compositor, command, runtime, clipboard, proxy, debug, windows, - graphics_info, + ui_caches, ); } let subscription = application.subscription().map(Event::Application); - runtime.track(subscription); + runtime.track(subscription.into_recipes()); } /// Runs the actions of a [`Command`]. -pub fn run_command<A, E>( +pub fn run_command<A, C, E>( application: &A, - caches: &mut HashMap<window::Id, user_interface::Cache>, - states: &HashMap<window::Id, State<A>>, - renderer: &mut A::Renderer, + compositor: &mut C, command: Command<A::Message>, runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, clipboard: &mut Clipboard, proxy: &mut winit::event_loop::EventLoopProxy<Event<A::Message>>, debug: &mut Debug, - windows: &HashMap<window::Id, winit::window::Window>, - _graphics_info: impl FnOnce() -> compositor::Information + Copy, + windows: &mut Windows<A, C>, + ui_caches: &mut Vec<user_interface::Cache>, ) where - A: Application + 'static, + A: Application, E: Executor, - <A::Renderer as crate::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer> + 'static, + <A::Renderer as core::Renderer>::Theme: StyleSheet, { - use iced_native::command; - use iced_native::system; - use iced_native::window; + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; for action in command.actions() { match action { @@ -954,11 +837,14 @@ pub fn run_command<A, E>( }, command::Action::Window(id, action) => match action { window::Action::Spawn { settings } => { + let monitor = windows.last_monitor(); + proxy .send_event(Event::NewWindow { id, - settings: settings.into(), + settings, title: application.title(id), + monitor, }) .expect("Send message to event loop"); } @@ -968,86 +854,117 @@ pub fn run_command<A, E>( .expect("Send message to event loop"); } window::Action::Drag => { - let window = windows.get(&id).expect("No window found"); - let _res = window.drag_window(); + let _ = windows.with_raw(id).drag_window(); } - window::Action::Resize { width, height } => { - let window = windows.get(&id).expect("No window found"); - window.set_inner_size(winit::dpi::LogicalSize { - width, - height, - }); + window::Action::Resize(size) => { + windows.with_raw(id).set_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + window::Action::FetchSize(callback) => { + let window = windows.with_raw(id); + let size = window.inner_size(); + + proxy + .send_event(Event::Application(callback(Size::new( + size.width, + size.height, + )))) + .expect("Send message to event loop") + } + window::Action::Maximize(maximized) => { + windows.with_raw(id).set_maximized(maximized); + } + window::Action::Minimize(minimized) => { + windows.with_raw(id).set_minimized(minimized); } window::Action::Move { x, y } => { - let window = windows.get(&id).expect("No window found"); - window.set_outer_position(winit::dpi::LogicalPosition { - x, - y, - }); + windows.with_raw(id).set_outer_position( + winit::dpi::LogicalPosition { x, y }, + ); } window::Action::ChangeMode(mode) => { - let window = windows.get(&id).expect("No window found"); + let window = windows.with_raw(id); window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( window.current_monitor(), mode, )); } + window::Action::ChangeIcon(icon) => { + windows.with_raw(id).set_window_icon(conversion::icon(icon)) + } window::Action::FetchMode(tag) => { - let window = windows.get(&id).expect("No window found"); + let window = windows.with_raw(id); let mode = if window.is_visible().unwrap_or(true) { conversion::mode(window.fullscreen()) } else { - window::Mode::Hidden + core::window::Mode::Hidden }; proxy .send_event(Event::Application(tag(mode))) - .expect("Send message to event loop"); - } - window::Action::Maximize(value) => { - let window = windows.get(&id).expect("No window found!"); - window.set_maximized(value); - } - window::Action::Minimize(value) => { - let window = windows.get(&id).expect("No window found!"); - window.set_minimized(value); + .expect("Event loop doesn't exist."); } window::Action::ToggleMaximize => { - let window = windows.get(&id).expect("No window found!"); + let window = windows.with_raw(id); window.set_maximized(!window.is_maximized()); } window::Action::ToggleDecorations => { - let window = windows.get(&id).expect("No window found!"); + let window = windows.with_raw(id); window.set_decorations(!window.is_decorated()); } window::Action::RequestUserAttention(attention_type) => { - let window = windows.get(&id).expect("No window found!"); - window.request_user_attention( + windows.with_raw(id).request_user_attention( attention_type.map(conversion::user_attention), ); } window::Action::GainFocus => { - let window = windows.get(&id).expect("No window found!"); - window.focus_window(); + windows.with_raw(id).focus_window(); } - window::Action::ChangeAlwaysOnTop(on_top) => { - let window = windows.get(&id).expect("No window found!"); - window.set_always_on_top(on_top); + window::Action::ChangeLevel(level) => { + windows + .with_raw(id) + .set_window_level(conversion::window_level(level)); } - window::Action::FetchId(tag) => { - let window = windows.get(&id).expect("No window found!"); + window::Action::FetchId(tag) => proxy + .send_event(Event::Application(tag(windows + .with_raw(id) + .id() + .into()))) + .expect("Event loop doesn't exist."), + window::Action::Screenshot(tag) => { + let i = windows.index_from_id(id); + let state = &windows.states[i]; + let surface = &mut windows.surfaces[i]; + let renderer = &mut windows.renderers[i]; + + let bytes = compositor.screenshot( + renderer, + surface, + state.viewport(), + state.background_color(), + &debug.overlay(), + ); proxy - .send_event(Event::Application(tag(window.id().into()))) - .expect("Send message to event loop.") + .send_event(Event::Application(tag( + window::Screenshot::new( + bytes, + state.physical_size(), + ), + ))) + .expect("Event loop doesn't exist.") } }, command::Action::System(action) => match action { system::Action::QueryInformation(_tag) => { #[cfg(feature = "system")] { - let graphics_info = _graphics_info(); + let graphics_info = compositor.fetch_information(); let proxy = proxy.clone(); let _ = std::thread::spawn(move || { @@ -1058,33 +975,36 @@ pub fn run_command<A, E>( proxy .send_event(Event::Application(message)) - .expect("Send message to event loop") + .expect("Event loop doesn't exist.") }); } } }, command::Action::Widget(action) => { - let mut current_caches = std::mem::take(caches); - let mut current_operation = Some(action.into_operation()); + let mut current_operation = Some(action); - let mut user_interfaces = build_user_interfaces( + let mut uis = build_user_interfaces( application, - renderer, debug, - states, - current_caches, + windows, + std::mem::take(ui_caches), ); - while let Some(mut operation) = current_operation.take() { - for user_interface in user_interfaces.values_mut() { - user_interface.operate(renderer, operation.as_mut()); + 'operate: while let Some(mut operation) = + current_operation.take() + { + for (i, ui) in uis.iter_mut().enumerate() { + ui.operate(&windows.renderers[i], operation.as_mut()); match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { proxy .send_event(Event::Application(message)) - .expect("Send message to event loop"); + .expect("Event loop doesn't exist."); + + // operation completed, don't need to try to operate on rest of UIs + break 'operate; } operation::Outcome::Chain(next) => { current_operation = Some(next); @@ -1093,55 +1013,105 @@ pub fn run_command<A, E>( } } - let user_interfaces: HashMap<_, _> = user_interfaces - .drain() - .map(|(id, interface)| (id, interface.into_cache())) - .collect(); + *ui_caches = + uis.drain(..).map(UserInterface::into_cache).collect(); + } + command::Action::LoadFont { bytes, tagger } => { + use crate::core::text::Renderer; + + // TODO change this once we change each renderer to having a single backend reference.. :pain: + // TODO: Error handling (?) + for renderer in &mut windows.renderers { + renderer.load_font(bytes.clone()); + } - current_caches = user_interfaces; - *caches = current_caches; + proxy + .send_event(Event::Application(tagger(Ok(())))) + .expect("Send message to event loop"); } } } } -/// Build the user interfaces for every window. -pub fn build_user_interfaces<'a, A>( +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, A: Application, C: Compositor>( application: &'a A, - renderer: &mut A::Renderer, debug: &mut Debug, - states: &HashMap<window::Id, State<A>>, - mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, -) -> HashMap< - window::Id, - UserInterface< - 'a, - <A as Application>::Message, - <A as Application>::Renderer, - >, -> + windows: &mut Windows<A, C>, + mut cached_user_interfaces: Vec<user_interface::Cache>, +) -> Vec<UserInterface<'a, A::Message, A::Renderer>> where - A: Application + 'static, - <A::Renderer as crate::Renderer>::Theme: StyleSheet, + <A::Renderer as core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, { - let mut interfaces = HashMap::new(); - - for (id, cache) in cached_user_interfaces.drain() { - let state = &states.get(&id).unwrap(); - - let user_interface = build_user_interface( - application, - cache, - renderer, - state.logical_size(), - debug, - id, - ); + cached_user_interfaces + .drain(..) + .zip( + windows + .ids + .iter() + .zip(windows.states.iter().zip(windows.renderers.iter_mut())), + ) + .fold(vec![], |mut uis, (cache, (id, (state, renderer)))| { + uis.push(build_user_interface( + application, + cache, + renderer, + state.logical_size(), + debug, + *id, + )); + + uis + }) +} - let _ = interfaces.insert(id, user_interface); +/// Returns true if the provided event should cause an [`Application`] to +/// exit. +pub fn requests_exit( + window: usize, + exit_on_main_closed: bool, + event: &winit::event::WindowEvent<'_>, + _modifiers: winit::event::ModifiersState, +) -> bool { + use winit::event::WindowEvent; + + //TODO alt f4..? + match event { + WindowEvent::CloseRequested => exit_on_main_closed && window == 0, + #[cfg(target_os = "macos")] + WindowEvent::KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if _modifiers.logo() => true, + _ => false, } +} + +fn logical_bounds_of( + window: &winit::window::Window, +) -> Option<((i32, i32), Size<u32>)> { + let scale = window.scale_factor(); + let pos = window + .inner_position() + .map(|pos| { + ((pos.x as f64 / scale) as i32, (pos.y as f64 / scale) as i32) + }) + .ok()?; + let size = { + let size = window.inner_size(); + Size::new( + (size.width as f64 / scale) as u32, + (size.height as f64 / scale) as u32, + ) + }; - interfaces + Some((pos, size)) } #[cfg(not(target_arch = "wasm32"))] diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 54a114ad..f2741c3c 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -1,33 +1,50 @@ -use crate::application::{self, StyleSheet as _}; 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::window; -use crate::{Color, Debug, Point, Size, Viewport}; +use crate::style::application; +use std::fmt::{Debug, Formatter}; -use std::marker::PhantomData; +use iced_style::application::StyleSheet; use winit::event::{Touch, WindowEvent}; use winit::window::Window; /// The state of a multi-windowed [`Application`]. -#[allow(missing_debug_implementations)] pub struct State<A: Application> where - <A::Renderer as crate::Renderer>::Theme: application::StyleSheet, + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, { title: String, scale_factor: f64, viewport: Viewport, - viewport_changed: bool, - cursor_position: winit::dpi::PhysicalPosition<f64>, + viewport_version: usize, + cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::event::ModifiersState, - theme: <A::Renderer as crate::Renderer>::Theme, + theme: <A::Renderer as core::Renderer>::Theme, appearance: application::Appearance, - application: PhantomData<A>, +} + +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 crate::Renderer>::Theme: application::StyleSheet, + <A::Renderer as core::Renderer>::Theme: application::StyleSheet, { /// Creates a new [`State`] for the provided [`Application`]'s `window`. pub fn new( @@ -53,13 +70,11 @@ where title, scale_factor, viewport, - viewport_changed: false, - // TODO: Encode cursor availability in the type-system - cursor_position: winit::dpi::PhysicalPosition::new(-1.0, -1.0), + viewport_version: 0, + cursor_position: None, modifiers: winit::event::ModifiersState::default(), theme, appearance, - application: PhantomData, } } @@ -68,9 +83,11 @@ where &self.viewport } - /// Returns whether or not the viewport changed. - pub fn viewport_changed(&self) -> bool { - self.viewport_changed + /// Returns the version of the [`Viewport`] of the [`State`]. + /// + /// The version is incremented every time the [`Viewport`] changes. + pub fn viewport_version(&self) -> usize { + self.viewport_version } /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. @@ -89,11 +106,16 @@ where } /// Returns the current cursor position of the [`State`]. - pub fn cursor_position(&self) -> Point { - conversion::cursor_position( - self.cursor_position, - self.viewport.scale_factor(), - ) + 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`]. @@ -102,7 +124,7 @@ where } /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &<A::Renderer as crate::Renderer>::Theme { + pub fn theme(&self) -> &<A::Renderer as core::Renderer>::Theme { &self.theme } @@ -121,7 +143,7 @@ where &mut self, window: &Window, event: &WindowEvent<'_>, - _debug: &mut Debug, + _debug: &mut crate::runtime::Debug, ) { match event { WindowEvent::Resized(new_size) => { @@ -132,7 +154,7 @@ where window.scale_factor() * self.scale_factor, ); - self.viewport_changed = true; + self.viewport_version = self.viewport_version.wrapping_add(1); } WindowEvent::ScaleFactorChanged { scale_factor: new_scale_factor, @@ -146,18 +168,16 @@ where new_scale_factor * self.scale_factor, ); - self.viewport_changed = true; + self.viewport_version = self.viewport_version.wrapping_add(1); } WindowEvent::CursorMoved { position, .. } | WindowEvent::Touch(Touch { location: position, .. }) => { - self.cursor_position = *position; + self.cursor_position = Some(*position); } WindowEvent::CursorLeft { .. } => { - // TODO: Encode cursor availability in the type-system - self.cursor_position = - winit::dpi::PhysicalPosition::new(-1.0, -1.0); + self.cursor_position = None; } WindowEvent::ModifiersChanged(new_modifiers) => { self.modifiers = *new_modifiers; @@ -197,16 +217,20 @@ where self.title = new_title; } - // Update scale factor + // 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 { - let size = window.inner_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(size.width, size.height), + 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; } diff --git a/winit/src/multi_window/windows.rs b/winit/src/multi_window/windows.rs new file mode 100644 index 00000000..7b63defa --- /dev/null +++ b/winit/src/multi_window/windows.rs @@ -0,0 +1,170 @@ +use crate::core::{window, Size}; +use crate::multi_window::{Application, State}; +use iced_graphics::Compositor; +use iced_style::application::StyleSheet; +use std::fmt::{Debug, Formatter}; +use winit::monitor::MonitorHandle; + +pub struct Windows<A: Application, C: Compositor> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + pub ids: Vec<window::Id>, + pub raw: Vec<winit::window::Window>, + pub states: Vec<State<A>>, + pub viewport_versions: Vec<usize>, + pub surfaces: Vec<C::Surface>, + pub renderers: Vec<A::Renderer>, + pub pending_destroy: Vec<(window::Id, winit::window::WindowId)>, +} + +impl<A: Application, C: Compositor> Debug for Windows<A, C> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Windows") + .field("ids", &self.ids) + .field( + "raw", + &self + .raw + .iter() + .map(|raw| raw.id()) + .collect::<Vec<winit::window::WindowId>>(), + ) + .field("states", &self.states) + .field("viewport_versions", &self.viewport_versions) + .finish() + } +} + +impl<A: Application, C: Compositor> Windows<A, C> +where + <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, + C: Compositor<Renderer = A::Renderer>, +{ + /// Creates a new [`Windows`] with a single `window::Id::MAIN` window. + pub fn new( + application: &A, + compositor: &mut C, + renderer: A::Renderer, + main: winit::window::Window, + ) -> Self { + let state = State::new(application, window::Id::MAIN, &main); + let viewport_version = state.viewport_version(); + let physical_size = state.physical_size(); + let surface = compositor.create_surface( + &main, + physical_size.width, + physical_size.height, + ); + + Self { + ids: vec![window::Id::MAIN], + raw: vec![main], + states: vec![state], + viewport_versions: vec![viewport_version], + surfaces: vec![surface], + renderers: vec![renderer], + pending_destroy: vec![], + } + } + + /// Adds a new window to [`Windows`]. Returns the size of the newly created window in logical + /// pixels & the index of the window within [`Windows`]. + pub fn add( + &mut self, + application: &A, + compositor: &mut C, + id: window::Id, + window: winit::window::Window, + ) -> (Size, usize) { + let state = State::new(application, id, &window); + let window_size = state.logical_size(); + 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.renderer(); + + self.ids.push(id); + self.raw.push(window); + self.states.push(state); + self.viewport_versions.push(viewport_version); + self.surfaces.push(surface); + self.renderers.push(renderer); + + (window_size, self.ids.len() - 1) + } + + pub fn is_empty(&self) -> bool { + self.ids.is_empty() + } + + pub fn main(&self) -> &winit::window::Window { + &self.raw[0] + } + + pub fn index_from_raw(&self, id: winit::window::WindowId) -> usize { + self.raw + .iter() + .position(|window| window.id() == id) + .expect("No raw window in multi_window::Windows") + } + + pub fn index_from_id(&self, id: window::Id) -> usize { + self.ids + .iter() + .position(|window_id| *window_id == id) + .expect("No window in multi_window::Windows") + } + + pub fn last_monitor(&self) -> Option<MonitorHandle> { + self.raw.last().and_then(|w| w.current_monitor()) + } + + pub fn last(&self) -> usize { + self.ids.len() - 1 + } + + pub fn with_raw(&self, id: window::Id) -> &winit::window::Window { + let i = self.index_from_id(id); + &self.raw[i] + } + + /// Deletes the window with `id` from [`Windows`]. Returns the index that the window had. + pub fn delete(&mut self, id: window::Id) -> usize { + let i = self.index_from_id(id); + + let id = self.ids.remove(i); + let window = self.raw.remove(i); + let _ = self.states.remove(i); + let _ = self.viewport_versions.remove(i); + let _ = self.surfaces.remove(i); + + self.pending_destroy.push((id, window.id())); + + i + } + + /// Gets the winit `window` that is pending to be destroyed if it exists. + pub fn get_pending_destroy( + &mut self, + window: winit::window::WindowId, + ) -> window::Id { + let i = self + .pending_destroy + .iter() + .position(|(_, window_id)| window == *window_id) + .unwrap(); + + let (id, _) = self.pending_destroy.remove(i); + id + } +} diff --git a/winit/src/profiler.rs b/winit/src/profiler.rs deleted file mode 100644 index 7031507a..00000000 --- a/winit/src/profiler.rs +++ /dev/null @@ -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, - } - } -} diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 40b3d487..2b846fbd 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,35 +1,10 @@ //! Configure your application. -#[cfg(target_os = "windows")] -#[path = "settings/windows.rs"] -mod platform; - -#[cfg(target_os = "macos")] -#[path = "settings/macos.rs"] -mod platform; - -#[cfg(target_arch = "wasm32")] -#[path = "settings/wasm.rs"] -mod platform; - -#[cfg(not(any( - target_os = "windows", - target_os = "macos", - 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 crate::core::window; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; -use std::fmt; - /// The settings of an application. #[derive(Debug, Clone, Default)] pub struct Settings<Flags> { @@ -40,7 +15,7 @@ pub struct Settings<Flags> { pub id: Option<String>, /// The [`Window`] settings. - pub window: Window, + pub window: window::Settings, /// The data needed to initialize an [`Application`]. /// @@ -50,166 +25,93 @@ pub struct Settings<Flags> { /// Whether the [`Application`] should exit when the user requests the /// window to close (e.g. the user presses the close button). /// + /// With a [`multi_window::Application`] this will instead be used to determine whether the + /// application should exit when the "main"" window is closed, i.e. the first window created on + /// app launch. + /// /// [`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() +/// Converts the window settings into a `WindowBuilder` from `winit`. +pub fn window_builder( + settings: window::Settings, + title: &str, + monitor: Option<MonitorHandle>, + _id: Option<String>, +) -> WindowBuilder { + let mut window_builder = WindowBuilder::new(); + + let (width, height) = settings.size; + + window_builder = window_builder + .with_title(title) + .with_inner_size(winit::dpi::LogicalSize { width, height }) + .with_resizable(settings.resizable) + .with_decorations(settings.decorations) + .with_transparent(settings.transparent) + .with_window_icon(settings.icon.and_then(conversion::icon)) + .with_window_level(conversion::window_level(settings.level)) + .with_visible(settings.visible); + + if let Some(position) = + conversion::position(monitor.as_ref(), settings.size, settings.position) + { + window_builder = window_builder.with_position(position); } -} - -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; + if let Some((width, height)) = settings.min_size { window_builder = window_builder - .with_title(title) - .with_inner_size(winit::dpi::LogicalSize { width, height }) - .with_resizable(self.resizable) - .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 }); - } + .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 }); - } + if let Some((width, height)) = settings.max_size { + window_builder = window_builder + .with_max_inner_size(winit::dpi::LogicalSize { width, height }); + } - #[cfg(any( - target_os = "linux", - 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(any( + target_os = "linux", + 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); - } + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows; + #[allow(unsafe_code)] + unsafe { window_builder = window_builder - .with_drag_and_drop(self.platform_specific.drag_and_drop); + .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(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 = "macos")] + { + use winit::platform::macos::WindowBuilderExtMacOS; - window_builder + 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, + ); } -} -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: Default::default(), - } - } + window_builder } diff --git a/winit/src/settings/macos.rs b/winit/src/settings/macos.rs deleted file mode 100644 index f86e63ad..00000000 --- a/winit/src/settings/macos.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Platform specific settings for macOS. - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct PlatformSpecific { - /// Hides the window title. - pub title_hidden: bool, - /// Makes the titlebar transparent and allows the content to appear behind it. - pub titlebar_transparent: bool, - /// Makes the window content appear behind the titlebar. - pub fullsize_content_view: bool, -} diff --git a/winit/src/settings/other.rs b/winit/src/settings/other.rs deleted file mode 100644 index b1103f62..00000000 --- a/winit/src/settings/other.rs +++ /dev/null @@ -1,3 +0,0 @@ -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct PlatformSpecific; diff --git a/winit/src/settings/wasm.rs b/winit/src/settings/wasm.rs deleted file mode 100644 index 8e0f1bbc..00000000 --- a/winit/src/settings/wasm.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Platform specific settings for WebAssembly. - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct PlatformSpecific { - /// The identifier of a DOM element that will be replaced with the - /// application. - /// - /// If set to `None`, the application will be appended to the HTML body. - pub target: Option<String>, -} diff --git a/winit/src/settings/windows.rs b/winit/src/settings/windows.rs deleted file mode 100644 index 45d753bd..00000000 --- a/winit/src/settings/windows.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Platform specific settings for Windows. -use raw_window_handle::RawWindowHandle; - -/// The platform specific window settings of an application. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PlatformSpecific { - /// Parent window - pub parent: Option<RawWindowHandle>, - - /// Drag and drop support - pub drag_and_drop: bool, -} - -impl Default for PlatformSpecific { - fn default() -> Self { - Self { - parent: None, - drag_and_drop: true, - } - } -} diff --git a/winit/src/window.rs b/winit/src/window.rs deleted file mode 100644 index e69de29b..00000000 -- cgit From 83c7870c569a2976923ee6243a19813094d44673 Mon Sep 17 00:00:00 2001 From: Bingus <shankern@protonmail.com> Date: Mon, 24 Jul 2023 14:32:59 -0700 Subject: Moved `exit_on_close_request` to window settings. This now controls whether each INDIVIDUAL window should close on CloseRequested events. --- core/src/window/event.rs | 3 - core/src/window/settings.rs | 11 +++ examples/multi_window/src/main.rs | 6 +- src/settings.rs | 11 --- winit/src/application.rs | 4 +- winit/src/multi_window.rs | 148 +++++++++++++++++++++++--------------- winit/src/multi_window/windows.rs | 26 +++++++ winit/src/settings.rs | 12 +--- 8 files changed, 133 insertions(+), 88 deletions(-) diff --git a/core/src/window/event.rs b/core/src/window/event.rs index 3efce05e..f7759435 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -28,9 +28,6 @@ pub enum Event { RedrawRequested(Instant), /// The user has requested for the window to close. - /// - /// Usually, you will want to terminate the execution whenever this event - /// occurs. CloseRequested, /// A window was destroyed by the runtime. diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index 20811e83..eba27914 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -57,6 +57,16 @@ pub struct Settings { /// Platform specific settings. 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 { @@ -73,6 +83,7 @@ impl Default for Settings { level: Level::default(), icon: None, platform_specific: Default::default(), + exit_on_close_request: true, } } } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 58604702..51ec3595 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -8,10 +8,7 @@ use iced::{ use std::collections::HashMap; fn main() -> iced::Result { - Example::run(Settings { - exit_on_close_request: false, - ..Default::default() - }) + Example::run(Settings::default()) } #[derive(Default)] @@ -111,6 +108,7 @@ impl multi_window::Application for Example { id, window::Settings { position: self.next_window_pos, + exit_on_close_request: count % 2 == 0, ..Default::default() }, ); diff --git a/src/settings.rs b/src/settings.rs index 4ce2d135..e2a43581 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -41,14 +41,6 @@ pub struct Settings<Flags> { /// /// [`Canvas`]: crate::widget::Canvas 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> { @@ -65,7 +57,6 @@ impl<Flags> Settings<Flags> { default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, - exit_on_close_request: default_settings.exit_on_close_request, } } } @@ -82,7 +73,6 @@ where default_font: Default::default(), default_text_size: 16.0, antialiasing: false, - exit_on_close_request: true, } } } @@ -93,7 +83,6 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { id: settings.id, window: settings.window, flags: settings.flags, - exit_on_close_request: settings.exit_on_close_request, } } } diff --git a/winit/src/application.rs b/winit/src/application.rs index 5c45bbda..cffcb884 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -136,6 +136,8 @@ where let target = settings.window.platform_specific.target.clone(); let should_be_visible = settings.window.visible; + let exit_on_close_request = settings.window.exit_on_close_request; + let builder = settings::window_builder( settings.window, &application.title(), @@ -197,7 +199,7 @@ where init_command, window, should_be_visible, - settings.exit_on_close_request, + exit_on_close_request, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index e6f440bc..b67c0a48 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -46,7 +46,14 @@ pub enum Event<Message> { /// An internal event for closing a window. CloseWindow(window::Id), /// An internal event for when the window has finished being created. - WindowCreated(window::Id, winit::window::Window), + WindowCreated { + /// The internal ID of the window. + id: window::Id, + /// The raw window. + window: winit::window::Window, + /// Whether or not the window should close when a user requests it does. + exit_on_close_request: bool, + }, } #[allow(unsafe_code)] @@ -161,6 +168,8 @@ where }; let should_main_be_visible = settings.window.visible; + let exit_on_close_request = settings.window.exit_on_close_request; + let builder = window_builder( settings.window, &application.title(window::Id::MAIN), @@ -208,8 +217,13 @@ where let (mut compositor, renderer) = C::new(compositor_settings, Some(&main_window))?; - let windows = - Windows::new(&application, &mut compositor, renderer, main_window); + let windows = Windows::new( + &application, + &mut compositor, + renderer, + main_window, + exit_on_close_request, + ); let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); @@ -225,7 +239,6 @@ where init_command, windows, should_main_be_visible, - settings.exit_on_close_request, )); let mut context = task::Context::from_waker(task::noop_waker_ref()); @@ -255,14 +268,18 @@ where title, monitor, }) => { + let exit_on_close_request = settings.exit_on_close_request; + let window = settings::window_builder(settings, &title, monitor, None) .build(window_target) .expect("Failed to build window"); - Some(winit::event::Event::UserEvent(Event::WindowCreated( - id, window, - ))) + Some(winit::event::Event::UserEvent(Event::WindowCreated { + id, + window, + exit_on_close_request, + })) } _ => event.to_static(), }; @@ -299,7 +316,6 @@ async fn run_instance<A, E, C>( init_command: Command<A::Message>, mut windows: Windows<A, C>, should_main_window_be_visible: bool, - exit_on_main_closed: bool, ) where A: Application + 'static, E: Executor + 'static, @@ -548,11 +564,20 @@ async fn run_instance<A, E, C>( Event::Application(message) => { messages.push(message); } - Event::WindowCreated(id, window) => { + Event::WindowCreated { + id, + window, + exit_on_close_request, + } => { let bounds = logical_bounds_of(&window); - let (inner_size, i) = - windows.add(&application, &mut compositor, id, window); + let (inner_size, i) = windows.add( + &application, + &mut compositor, + id, + window, + exit_on_close_request, + ); user_interfaces.push(build_user_interface( &application, @@ -680,50 +705,61 @@ async fn run_instance<A, E, C>( event: window_event, window_id, } => { - let window_deleted = windows - .pending_destroy - .iter() - .any(|(_, w_id)| window_id == *w_id); + let window_index = + windows.raw.iter().position(|w| w.id() == window_id); + + match window_index { + Some(i) => { + let id = windows.ids[i]; + let raw = &windows.raw[i]; + let exit_on_close_request = + windows.exit_on_close_requested[i]; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && exit_on_close_request + { + let i = windows.delete(id); + let _ = user_interfaces.remove(i); + let _ = ui_caches.remove(i); + + if windows.is_empty() { + break 'main; + } + } else { + let state = &mut windows.states[i]; + state.update(raw, &window_event, &mut debug); - if matches!(window_event, winit::event::WindowEvent::Destroyed) - { - // This is the only special case, since in order trigger the Destroyed event the - // window reference from winit must be dropped, but we still want to inform the - // user that the window was destroyed so they can clean up any specific window - // code for this window - let id = windows.get_pending_destroy(window_id); - - events.push(( - None, - core::Event::Window(id, window::Event::Destroyed), - )); - } else if !window_deleted { - let i = windows.index_from_raw(window_id); - let id = windows.ids[i]; - let raw = &windows.raw[i]; - let state = &mut windows.states[i]; - - // first check if we need to just break the entire application - // e.g. a user does a force quit on MacOS, or if a user has set "exit on main closed" - // as an option in window settings and wants to close the main window - if requests_exit( - i, - exit_on_main_closed, - &window_event, - state.modifiers(), - ) { - break 'main; + if let Some(event) = conversion::window_event( + id, + &window_event, + state.scale_factor(), + state.modifiers(), + ) { + events.push((Some(id), event)); + } + } } - - state.update(raw, &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - id, - &window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push((Some(id), event)); + None => { + // This is the only special case, since in order to trigger the Destroyed event the + // window reference from winit must be dropped, but we still want to inform the + // user that the window was destroyed so they can clean up any specific window + // code for this window + if matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) { + let id = windows.get_pending_destroy(window_id); + + events.push(( + None, + core::Event::Window( + id, + window::Event::Destroyed, + ), + )); + } } } } @@ -1068,17 +1104,13 @@ where /// Returns true if the provided event should cause an [`Application`] to /// exit. -pub fn requests_exit( - window: usize, - exit_on_main_closed: bool, +pub fn user_force_quit( event: &winit::event::WindowEvent<'_>, _modifiers: winit::event::ModifiersState, ) -> bool { use winit::event::WindowEvent; - //TODO alt f4..? match event { - WindowEvent::CloseRequested => exit_on_main_closed && window == 0, #[cfg(target_os = "macos")] WindowEvent::KeyboardInput { input: diff --git a/winit/src/multi_window/windows.rs b/winit/src/multi_window/windows.rs index 7b63defa..1f606b31 100644 --- a/winit/src/multi_window/windows.rs +++ b/winit/src/multi_window/windows.rs @@ -14,6 +14,7 @@ where pub raw: Vec<winit::window::Window>, pub states: Vec<State<A>>, pub viewport_versions: Vec<usize>, + pub exit_on_close_requested: Vec<bool>, pub surfaces: Vec<C::Surface>, pub renderers: Vec<A::Renderer>, pub pending_destroy: Vec<(window::Id, winit::window::WindowId)>, @@ -52,6 +53,7 @@ where compositor: &mut C, renderer: A::Renderer, main: winit::window::Window, + exit_on_close_requested: bool, ) -> Self { let state = State::new(application, window::Id::MAIN, &main); let viewport_version = state.viewport_version(); @@ -67,6 +69,7 @@ where raw: vec![main], states: vec![state], viewport_versions: vec![viewport_version], + exit_on_close_requested: vec![exit_on_close_requested], surfaces: vec![surface], renderers: vec![renderer], pending_destroy: vec![], @@ -81,6 +84,7 @@ where compositor: &mut C, id: window::Id, window: winit::window::Window, + exit_on_close_requested: bool, ) -> (Size, usize) { let state = State::new(application, id, &window); let window_size = state.logical_size(); @@ -96,6 +100,7 @@ where self.ids.push(id); self.raw.push(window); self.states.push(state); + self.exit_on_close_requested.push(exit_on_close_requested); self.viewport_versions.push(viewport_version); self.surfaces.push(surface); self.renderers.push(renderer); @@ -145,6 +150,7 @@ where let id = self.ids.remove(i); let window = self.raw.remove(i); let _ = self.states.remove(i); + let _ = self.exit_on_close_requested.remove(i); let _ = self.viewport_versions.remove(i); let _ = self.surfaces.remove(i); @@ -167,4 +173,24 @@ where let (id, _) = self.pending_destroy.remove(i); id } + + /// Returns the windows that need to be requested to closed, and also the windows that can be + /// closed immediately. + pub fn partition_close_requests(&self) -> (Vec<window::Id>, Vec<window::Id>) { + self.exit_on_close_requested.iter().enumerate().fold( + (vec![], vec![]), + |(mut close_immediately, mut needs_request_closed), + (i, close)| { + let id = self.ids[i]; + + if *close { + close_immediately.push(id); + } else { + needs_request_closed.push(id); + } + + (close_immediately, needs_request_closed) + }, + ) + } } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 2b846fbd..c0b3b047 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,6 +1,6 @@ //! Configure your application. -use crate::conversion; use crate::core::window; +use crate::conversion; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; @@ -21,16 +21,6 @@ pub struct Settings<Flags> { /// /// [`Application`]: crate::Application pub flags: Flags, - - /// Whether the [`Application`] should exit when the user requests the - /// window to close (e.g. the user presses the close button). - /// - /// With a [`multi_window::Application`] this will instead be used to determine whether the - /// application should exit when the "main"" window is closed, i.e. the first window created on - /// app launch. - /// - /// [`Application`]: crate::Application - pub exit_on_close_request: bool, } /// Converts the window settings into a `WindowBuilder` from `winit`. -- cgit From 346af3f8b0baa418fd37b878bc2930ff0bd57cc0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 11 Sep 2023 02:47:24 +0200 Subject: Make `FontSystem` global and simplify `Paragraph` API --- core/src/renderer/null.rs | 34 ++-------- core/src/text.rs | 90 +++++++------------------ core/src/widget/text.rs | 23 +++---- graphics/Cargo.toml | 12 ++-- graphics/src/backend.rs | 4 -- graphics/src/renderer.rs | 35 ---------- graphics/src/text.rs | 47 ++++++------- graphics/src/text/paragraph.rs | 147 +++++++++++++++++++---------------------- renderer/src/lib.rs | 16 ----- tiny_skia/src/backend.rs | 5 -- tiny_skia/src/text.rs | 21 +++--- wgpu/src/backend.rs | 5 -- wgpu/src/text.rs | 17 +++-- widget/src/pick_list.rs | 22 +++--- widget/src/text_input.rs | 17 ++--- 15 files changed, 166 insertions(+), 329 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 55d58a59..0ffd3649 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -58,16 +58,6 @@ impl text::Renderer for Null { fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - fn create_paragraph(&self, _text: Text<'_, Self::Font>) -> Self::Paragraph { - } - - fn resize_paragraph( - &self, - _paragraph: &mut Self::Paragraph, - _new_bounds: Size, - ) { - } - fn fill_paragraph( &mut self, _paragraph: &Self::Paragraph, @@ -88,24 +78,12 @@ impl text::Renderer for Null { impl text::Paragraph for () { type Font = Font; - fn content(&self) -> &str { - "" - } - - fn text_size(&self) -> Pixels { - Pixels(16.0) - } - - fn font(&self) -> Self::Font { - Font::default() - } + fn with_text(_text: Text<'_, Self::Font>) -> Self {} - fn line_height(&self) -> text::LineHeight { - text::LineHeight::default() - } + fn resize(&mut self, _new_bounds: Size) {} - fn shaping(&self) -> text::Shaping { - text::Shaping::default() + fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference { + text::Difference::None } fn horizontal_alignment(&self) -> alignment::Horizontal { @@ -120,10 +98,6 @@ impl text::Paragraph for () { None } - fn bounds(&self) -> Size { - Size::ZERO - } - fn min_bounds(&self) -> Size { Size::ZERO } diff --git a/core/src/text.rs b/core/src/text.rs index 0e3617b1..ff85696e 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -156,33 +156,6 @@ pub trait Renderer: crate::Renderer { /// Loads a [`Self::Font`] from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph; - - /// Lays out the given [`Paragraph`] with some new boundaries. - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ); - - /// Updates a [`Paragraph`] to match the given [`Text`], if needed. - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - match compare(paragraph, text) { - Difference::None => {} - Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - /// Draws the given [`Paragraph`] at the given position and with the given /// [`Color`]. fn fill_paragraph( @@ -201,25 +174,21 @@ pub trait Renderer: crate::Renderer { color: Color, ); } + /// A text paragraph. -pub trait Paragraph: Default { +pub trait Paragraph: Sized + Default { /// The font of this [`Paragraph`]. - type Font; - - /// Returns the content of the [`Paragraph`]. - fn content(&self) -> &str; - - /// Returns the text size of the [`Paragraph`]. - fn text_size(&self) -> Pixels; + type Font: Copy + PartialEq; - /// Returns the [`LineHeight`] of the [`Paragraph`]. - fn line_height(&self) -> LineHeight; + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_text(text: Text<'_, Self::Font>) -> Self; - /// Returns the [`Self::Font`] of the [`Paragraph`]. - fn font(&self) -> Self::Font; + /// Lays out the [`Paragraph`] with some new boundaries. + fn resize(&mut self, new_bounds: Size); - /// Returns the [`Shaping`] strategy of the [`Paragraph`]. - fn shaping(&self) -> Shaping; + /// Compares the [`Paragraph`] with some desired [`Text`] and returns the + /// [`Difference`]. + fn compare(&self, text: Text<'_, Self::Font>) -> Difference; /// Returns the horizontal alignment of the [`Paragraph`]. fn horizontal_alignment(&self) -> alignment::Horizontal; @@ -227,9 +196,6 @@ pub trait Paragraph: Default { /// Returns the vertical alignment of the [`Paragraph`]. fn vertical_alignment(&self) -> alignment::Vertical; - /// Returns the boundaries of the [`Paragraph`]. - fn bounds(&self) -> Size; - /// Returns the minimum boundaries that can fit the contents of the /// [`Paragraph`]. fn min_bounds(&self) -> Size; @@ -241,6 +207,19 @@ pub trait Paragraph: Default { /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; + /// Updates the [`Paragraph`] to match the given [`Text`], if needed. + fn update(&mut self, text: Text<'_, Self::Font>) { + match self.compare(text) { + Difference::None => {} + Difference::Bounds => { + self.resize(text.bounds); + } + Difference::Shape => { + *self = Self::with_text(text); + } + } + } + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. fn min_width(&self) -> f32 { self.min_bounds().width @@ -276,26 +255,3 @@ pub enum Difference { /// the text is necessary. Shape, } - -/// Compares a [`Paragraph`] with some desired [`Text`] and returns the -/// [`Difference`]. -pub fn compare<Font: PartialEq>( - paragraph: &impl Paragraph<Font = Font>, - text: Text<'_, Font>, -) -> Difference { - if paragraph.content() != text.content - || paragraph.text_size() != text.size - || paragraph.line_height().to_absolute(text.size) - != text.line_height.to_absolute(text.size) - || paragraph.font() != text.font - || paragraph.shaping() != text.shaping - || paragraph.horizontal_alignment() != text.horizontal_alignment - || paragraph.vertical_alignment() != text.vertical_alignment - { - Difference::Shape - } else if paragraph.bounds() != text.bounds { - Difference::Bounds - } else { - Difference::None - } -} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 53ed463e..c7c9f539 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -212,19 +212,16 @@ where let State(ref mut paragraph) = state; - renderer.update_paragraph( - paragraph, - text::Text { - content, - bounds, - size, - line_height, - font, - shaping, - horizontal_alignment, - vertical_alignment, - }, - ); + paragraph.update(text::Text { + content, + bounds, + size, + line_height, + font, + shaping, + horizontal_alignment, + vertical_alignment, + }); let size = limits.resolve(paragraph.min_bounds()); diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index ff698649..26bd1435 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -25,16 +25,15 @@ iced_core.workspace = true bitflags.workspace = true bytemuck.workspace = true +cosmic-text.workspace = true glam.workspace = true half.workspace = true log.workspace = true +once_cell.workspace = true raw-window-handle.workspace = true -thiserror.workspace = true -cosmic-text.workspace = true rustc-hash.workspace = true - -lyon_path.workspace = true -lyon_path.optional = true +thiserror.workspace = true +twox-hash.workspace = true image.workspace = true image.optional = true @@ -42,7 +41,8 @@ image.optional = true kamadak-exif.workspace = true kamadak-exif.optional = true -twox-hash.workspace = true +lyon_path.workspace = true +lyon_path.optional = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] twox-hash.workspace = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index c2ac82ba..10eb337f 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -2,7 +2,6 @@ use crate::core::image; use crate::core::svg; use crate::core::Size; -use crate::text; use std::borrow::Cow; @@ -18,9 +17,6 @@ pub trait Backend { pub trait Text { /// Loads a font from its bytes. fn load_font(&mut self, font: Cow<'static, [u8]>); - - /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`]. - fn font_system(&self) -> &text::FontSystem; } /// A graphics backend that supports image rendering. diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index d4df29a5..c5033d36 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -158,41 +158,6 @@ where self.backend.load_font(bytes); } - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph { - text::Paragraph::with_text(text, self.backend.font_system()) - } - - fn update_paragraph( - &self, - paragraph: &mut Self::Paragraph, - text: Text<'_, Self::Font>, - ) { - let font_system = self.backend.font_system(); - - if paragraph.version() != font_system.version() { - // The font system has changed, paragraph fonts may be outdated - *paragraph = self.create_paragraph(text); - } else { - match core::text::compare(paragraph, text) { - core::text::Difference::None => {} - core::text::Difference::Bounds => { - self.resize_paragraph(paragraph, text.bounds); - } - core::text::Difference::Shape => { - *paragraph = self.create_paragraph(text); - } - } - } - } - - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ) { - paragraph.resize(new_bounds, self.backend.font_system()); - } - fn fill_paragraph( &mut self, paragraph: &Self::Paragraph, diff --git a/graphics/src/text.rs b/graphics/src/text.rs index bc06aa3c..f5ccaf52 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -10,40 +10,39 @@ use crate::core::font::{self, Font}; use crate::core::text::Shaping; use crate::core::Size; +use once_cell::sync::OnceCell; use std::borrow::Cow; -use std::sync::{self, Arc, RwLock}; +use std::sync::{Arc, RwLock}; -#[allow(missing_debug_implementations)] -pub struct FontSystem { - raw: RwLock<cosmic_text::FontSystem>, - version: Version, -} +pub fn font_system() -> &'static RwLock<FontSystem> { + static FONT_SYSTEM: OnceCell<RwLock<FontSystem>> = OnceCell::new(); -impl FontSystem { - pub fn new() -> Self { - FontSystem { - raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts( + FONT_SYSTEM.get_or_init(|| { + RwLock::new(FontSystem { + raw: cosmic_text::FontSystem::new_with_fonts( [cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), ))] .into_iter(), - )), + ), version: Version::default(), - } - } + }) + }) +} - pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem { - self.raw.get_mut().expect("Lock font system") - } +#[allow(missing_debug_implementations)] +pub struct FontSystem { + raw: cosmic_text::FontSystem, + version: Version, +} - pub fn write( - &self, - ) -> (sync::RwLockWriteGuard<'_, cosmic_text::FontSystem>, Version) { - (self.raw.write().expect("Write font system"), self.version) +impl FontSystem { + pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { + &mut self.raw } pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - let _ = self.get_mut().db_mut().load_font_source( + let _ = self.raw.db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); @@ -58,12 +57,6 @@ impl FontSystem { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); -impl Default for FontSystem { - fn default() -> Self { - Self::new() - } -} - pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer .layout_runs() diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index e4350cff..d0396e8e 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -2,7 +2,7 @@ use crate::core; use crate::core::alignment; use crate::core::text::{Hit, LineHeight, Shaping, Text}; use crate::core::{Font, Pixels, Point, Size}; -use crate::text::{self, FontSystem}; +use crate::text; use std::fmt; use std::sync::{self, Arc}; @@ -27,13 +27,39 @@ impl Paragraph { Self::default() } - pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self { + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().buffer + } + + pub fn downgrade(&self) -> Weak { + let paragraph = self.internal(); + + Weak { + raw: Arc::downgrade(paragraph), + min_bounds: paragraph.min_bounds, + horizontal_alignment: paragraph.horizontal_alignment, + vertical_alignment: paragraph.vertical_alignment, + } + } + + fn internal(&self) -> &Arc<Internal> { + self.0 + .as_ref() + .expect("paragraph should always be initialized") + } +} + +impl core::text::Paragraph for Paragraph { + type Font = Font; + + fn with_text(text: Text<'_, Font>) -> Self { log::trace!("Allocating paragraph: {}", text.content); - let (mut font_system, version) = font_system.write(); + let mut font_system = + text::font_system().write().expect("Write font system"); let mut buffer = cosmic_text::Buffer::new( - &mut font_system, + font_system.raw(), cosmic_text::Metrics::new( text.size.into(), text.line_height.to_absolute(text.size).into(), @@ -41,13 +67,13 @@ impl Paragraph { ); buffer.set_size( - &mut font_system, + font_system.raw(), text.bounds.width, text.bounds.height, ); buffer.set_text( - &mut font_system, + font_system.raw(), text.content, text::to_attributes(text.font), text::to_shaping(text.shaping), @@ -64,30 +90,11 @@ impl Paragraph { shaping: text.shaping, bounds: text.bounds, min_bounds, - version, + version: font_system.version(), }))) } - pub fn buffer(&self) -> &cosmic_text::Buffer { - &self.internal().buffer - } - - pub fn version(&self) -> text::Version { - self.internal().version - } - - pub fn downgrade(&self) -> Weak { - let paragraph = self.internal(); - - Weak { - raw: Arc::downgrade(paragraph), - min_bounds: paragraph.min_bounds, - horizontal_alignment: paragraph.horizontal_alignment, - vertical_alignment: paragraph.vertical_alignment, - } - } - - pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) { + fn resize(&mut self, new_bounds: Size) { let paragraph = self .0 .take() @@ -95,10 +102,11 @@ impl Paragraph { match Arc::try_unwrap(paragraph) { Ok(mut internal) => { - let (mut font_system, _) = font_system.write(); + let mut font_system = + text::font_system().write().expect("Write font system"); internal.buffer.set_size( - &mut font_system, + font_system.raw(), new_bounds.width, new_bounds.height, ); @@ -113,55 +121,42 @@ impl Paragraph { // If there is a strong reference somewhere, we recompute the // buffer from scratch - *self = Self::with_text( - Text { - content: &internal.content, - bounds: internal.bounds, - size: Pixels(metrics.font_size), - line_height: LineHeight::Absolute(Pixels( - metrics.line_height, - )), - font: internal.font, - horizontal_alignment: internal.horizontal_alignment, - vertical_alignment: internal.vertical_alignment, - shaping: internal.shaping, - }, - font_system, - ); + *self = Self::with_text(Text { + content: &internal.content, + bounds: internal.bounds, + size: Pixels(metrics.font_size), + line_height: LineHeight::Absolute(Pixels( + metrics.line_height, + )), + font: internal.font, + horizontal_alignment: internal.horizontal_alignment, + vertical_alignment: internal.vertical_alignment, + shaping: internal.shaping, + }); } } } - fn internal(&self) -> &Arc<Internal> { - self.0 - .as_ref() - .expect("paragraph should always be initialized") - } -} - -impl core::text::Paragraph for Paragraph { - type Font = Font; - - fn content(&self) -> &str { - &self.internal().content - } - - fn text_size(&self) -> Pixels { - Pixels(self.internal().buffer.metrics().font_size) - } - - fn line_height(&self) -> LineHeight { - LineHeight::Absolute(Pixels( - self.internal().buffer.metrics().line_height, - )) - } - - fn font(&self) -> Font { - self.internal().font - } - - fn shaping(&self) -> Shaping { - self.internal().shaping + fn compare(&self, text: Text<'_, Font>) -> core::text::Difference { + let font_system = text::font_system().read().expect("Read font system"); + let paragraph = self.internal(); + let metrics = paragraph.buffer.metrics(); + + if paragraph.version != font_system.version + || paragraph.content != text.content + || metrics.font_size != text.size.0 + || metrics.line_height != text.line_height.to_absolute(text.size).0 + || paragraph.font != text.font + || paragraph.shaping != text.shaping + || paragraph.horizontal_alignment != text.horizontal_alignment + || paragraph.vertical_alignment != text.vertical_alignment + { + core::text::Difference::Shape + } else if paragraph.bounds != text.bounds { + core::text::Difference::Bounds + } else { + core::text::Difference::None + } } fn horizontal_alignment(&self) -> alignment::Horizontal { @@ -172,10 +167,6 @@ impl core::text::Paragraph for Paragraph { self.internal().vertical_alignment } - fn bounds(&self) -> Size { - self.internal().bounds - } - fn min_bounds(&self) -> Size { self.internal().min_bounds } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 8bdf231d..73e56890 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -173,22 +173,6 @@ impl<T> text::Renderer for Renderer<T> { delegate!(self, renderer, renderer.default_size()) } - fn create_paragraph(&self, text: Text<'_, Self::Font>) -> Self::Paragraph { - delegate!(self, renderer, renderer.create_paragraph(text)) - } - - fn resize_paragraph( - &self, - paragraph: &mut Self::Paragraph, - new_bounds: Size, - ) { - delegate!( - self, - renderer, - renderer.resize_paragraph(paragraph, new_bounds) - ); - } - fn load_font(&mut self, bytes: Cow<'static, [u8]>) { delegate!(self, renderer, renderer.load_font(bytes)); } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index c721d96e..72184c8a 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,6 +1,5 @@ use crate::core::{Background, Color, Gradient, Rectangle, Vector}; use crate::graphics::backend; -use crate::graphics::text; use crate::graphics::{Damage, Viewport}; use crate::primitive::{self, Primitive}; @@ -805,10 +804,6 @@ impl iced_graphics::Backend for Backend { } impl backend::Text for Backend { - fn font_system(&self) -> &text::FontSystem { - self.text_pipeline.font_system() - } - fn load_font(&mut self, font: Cow<'static, [u8]>) { self.text_pipeline.load_font(font); } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index cb3ef54c..4f6e3941 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -2,8 +2,8 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle}; use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::font_system; use crate::graphics::text::paragraph; -use crate::graphics::text::FontSystem; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; @@ -12,7 +12,6 @@ use std::collections::hash_map; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: FontSystem, glyph_cache: GlyphCache, cache: RefCell<Cache>, } @@ -20,18 +19,16 @@ pub struct Pipeline { impl Pipeline { pub fn new() -> Self { Pipeline { - font_system: FontSystem::new(), glyph_cache: GlyphCache::new(), cache: RefCell::new(Cache::new()), } } - pub fn font_system(&self) -> &FontSystem { - &self.font_system - } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.load_font(bytes); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -51,8 +48,10 @@ impl Pipeline { return; }; + let mut font_system = font_system().write().expect("Write font system"); + draw( - self.font_system.get_mut(), + font_system.raw(), &mut self.glyph_cache, paragraph.buffer(), Rectangle::new(position, paragraph.min_bounds()), @@ -82,7 +81,9 @@ impl Pipeline { ) { let line_height = f32::from(line_height.to_absolute(size)); - let font_system = self.font_system.get_mut(); + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + let key = cache::Key { bounds: bounds.size(), content, diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 65c63f19..3d1755e1 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,5 +1,4 @@ use crate::core::{Color, Size}; -use crate::graphics; use crate::graphics::backend; use crate::graphics::color; use crate::graphics::{Transformation, Viewport}; @@ -310,10 +309,6 @@ impl crate::graphics::Backend for Backend { } impl backend::Text for Backend { - fn font_system(&self) -> &graphics::text::FontSystem { - self.text_pipeline.font_system() - } - fn load_font(&mut self, font: Cow<'static, [u8]>) { self.text_pipeline.load_font(font); } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index bd4f3e06..5c9f4d7e 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{FontSystem, Paragraph}; +use crate::graphics::text::{font_system, Paragraph}; use crate::layer::Text; use std::borrow::Cow; @@ -10,7 +10,6 @@ use std::cell::RefCell; #[allow(missing_debug_implementations)] pub struct Pipeline { - font_system: FontSystem, renderers: Vec<glyphon::TextRenderer>, atlas: glyphon::TextAtlas, prepare_layer: usize, @@ -24,7 +23,6 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { - font_system: FontSystem::new(), renderers: Vec::new(), atlas: glyphon::TextAtlas::with_color_mode( device, @@ -41,12 +39,11 @@ impl Pipeline { } } - pub fn font_system(&self) -> &FontSystem { - &self.font_system - } - pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { - self.font_system.load_font(bytes); + font_system() + .write() + .expect("Write font system") + .load_font(bytes); self.cache = RefCell::new(Cache::new()); } @@ -69,7 +66,9 @@ impl Pipeline { )); } - let font_system = self.font_system.get_mut(); + let mut font_system = font_system().write().expect("Write font system"); + let font_system = font_system.raw(); + let renderer = &mut self.renderers[self.prepare_layer]; let cache = self.cache.get_mut(); diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 056a5e65..4b89d6ff 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -415,23 +415,17 @@ where for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { let label = option.to_string(); - renderer.update_paragraph( - paragraph, - Text { - content: &label, - ..option_text - }, - ); + paragraph.update(Text { + content: &label, + ..option_text + }); } if let Some(placeholder) = placeholder { - renderer.update_paragraph( - &mut state.placeholder, - Text { - content: placeholder, - ..option_text - }, - ); + state.placeholder.update(Text { + content: placeholder, + ..option_text + }); } let max_width = match width { diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 7d5ae806..f9a2d419 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -523,18 +523,15 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.placeholder, placeholder_text); + state.placeholder.update(placeholder_text); let secure_value = is_secure.then(|| value.secure()); let value = secure_value.as_ref().unwrap_or(value); - renderer.update_paragraph( - &mut state.value, - Text { - content: &value.to_string(), - ..placeholder_text - }, - ); + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); if let Some(icon) = icon { let icon_text = Text { @@ -548,7 +545,7 @@ where shaping: text::Shaping::Advanced, }; - renderer.update_paragraph(&mut state.icon, icon_text); + state.icon.update(icon_text); let icon_width = state.icon.min_width(); @@ -1461,7 +1458,7 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = renderer.create_paragraph(Text { + state.value = Renderer::Paragraph::with_text(Text { font, line_height, content: &value.to_string(), -- cgit From 6448429103c9c82b90040ac5a5a097bdded23f82 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 12 Sep 2023 14:51:00 +0200 Subject: Draft `Editor` API and `TextEditor` widget --- core/src/layout/limits.rs | 2 +- core/src/lib.rs | 2 +- core/src/renderer/null.rs | 38 ++++ core/src/text.rs | 123 ++++-------- core/src/text/editor.rs | 68 +++++++ core/src/text/paragraph.rs | 59 ++++++ examples/editor/Cargo.toml | 10 + examples/editor/src/main.rs | 49 +++++ graphics/src/damage.rs | 7 + graphics/src/primitive.rs | 10 + graphics/src/renderer.rs | 14 ++ graphics/src/text.rs | 2 + graphics/src/text/editor.rs | 327 +++++++++++++++++++++++++++++++ renderer/src/lib.rs | 19 +- style/src/lib.rs | 1 + style/src/text_editor.rs | 47 +++++ style/src/theme.rs | 113 +++++++++++ tiny_skia/src/backend.rs | 25 +++ tiny_skia/src/text.rs | 32 ++++ wgpu/src/layer.rs | 15 +- wgpu/src/layer/text.rs | 8 +- wgpu/src/text.rs | 28 ++- widget/src/helpers.rs | 15 ++ widget/src/lib.rs | 5 +- widget/src/text_editor.rs | 457 ++++++++++++++++++++++++++++++++++++++++++++ 25 files changed, 1384 insertions(+), 92 deletions(-) create mode 100644 core/src/text/editor.rs create mode 100644 core/src/text/paragraph.rs create mode 100644 examples/editor/Cargo.toml create mode 100644 examples/editor/src/main.rs create mode 100644 graphics/src/text/editor.rs create mode 100644 style/src/text_editor.rs create mode 100644 widget/src/text_editor.rs diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index 5d3c1556..39a3d98b 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -2,7 +2,7 @@ use crate::{Length, Padding, Size}; /// A set of size constraints for layouting. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Limits { min: Size, max: Size, diff --git a/core/src/lib.rs b/core/src/lib.rs index 1bfba7bd..13a9f06b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,7 +12,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( missing_debug_implementations, - missing_docs, + // missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 0ffd3649..adf75969 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -43,6 +43,7 @@ impl Renderer for Null { impl text::Renderer for Null { type Font = Font; type Paragraph = (); + type Editor = (); const ICON_FONT: Font = Font::DEFAULT; const CHECKMARK_ICON: char = '0'; @@ -66,6 +67,14 @@ impl text::Renderer for Null { ) { } + fn fill_editor( + &mut self, + _editor: &Self::Editor, + _position: Point, + _color: Color, + ) { + } + fn fill_text( &mut self, _paragraph: Text<'_, Self::Font>, @@ -106,3 +115,32 @@ impl text::Paragraph for () { None } } + +impl text::Editor for () { + type Font = Font; + + fn with_text(_text: &str) -> Self {} + + fn cursor(&self) -> text::editor::Cursor { + text::editor::Cursor::Caret(Point::ORIGIN) + } + + fn perform(&mut self, _action: text::editor::Action) {} + + fn bounds(&self) -> Size { + Size::ZERO + } + + fn min_bounds(&self) -> Size { + Size::ZERO + } + + fn update( + &mut self, + _new_bounds: Size, + _new_font: Self::Font, + _new_size: Pixels, + _new_line_height: text::LineHeight, + ) { + } +} diff --git a/core/src/text.rs b/core/src/text.rs index ff85696e..5aacbcc5 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -1,4 +1,11 @@ //! Draw and interact with text. +mod paragraph; + +pub mod editor; + +pub use editor::Editor; +pub use paragraph::Paragraph; + use crate::alignment; use crate::{Color, Pixels, Point, Size}; @@ -126,6 +133,31 @@ impl Hit { } } +/// The difference detected in some text. +/// +/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some +/// [`Text`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Difference { + /// No difference. + /// + /// The text can be reused as it is! + None, + + /// A bounds difference. + /// + /// This normally means a relayout is necessary, but the shape of the text can + /// be reused. + Bounds, + + /// A shape difference. + /// + /// The contents, alignment, sizes, fonts, or any other essential attributes + /// of the shape of the text have changed. A complete reshape and relayout of + /// the text is necessary. + Shape, +} + /// A renderer capable of measuring and drawing [`Text`]. pub trait Renderer: crate::Renderer { /// The font type used. @@ -134,6 +166,9 @@ pub trait Renderer: crate::Renderer { /// The [`Paragraph`] of this [`Renderer`]. type Paragraph: Paragraph<Font = Self::Font> + 'static; + /// The [`Editor`] of this [`Renderer`]. + type Editor: Editor<Font = Self::Font> + 'static; + /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -165,6 +200,13 @@ pub trait Renderer: crate::Renderer { color: Color, ); + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ); + /// Draws the given [`Text`] at the given position and with the given /// [`Color`]. fn fill_text( @@ -174,84 +216,3 @@ pub trait Renderer: crate::Renderer { color: Color, ); } - -/// A text paragraph. -pub trait Paragraph: Sized + Default { - /// The font of this [`Paragraph`]. - type Font: Copy + PartialEq; - - /// Creates a new [`Paragraph`] laid out with the given [`Text`]. - fn with_text(text: Text<'_, Self::Font>) -> Self; - - /// Lays out the [`Paragraph`] with some new boundaries. - fn resize(&mut self, new_bounds: Size); - - /// Compares the [`Paragraph`] with some desired [`Text`] and returns the - /// [`Difference`]. - fn compare(&self, text: Text<'_, Self::Font>) -> Difference; - - /// Returns the horizontal alignment of the [`Paragraph`]. - fn horizontal_alignment(&self) -> alignment::Horizontal; - - /// Returns the vertical alignment of the [`Paragraph`]. - fn vertical_alignment(&self) -> alignment::Vertical; - - /// Returns the minimum boundaries that can fit the contents of the - /// [`Paragraph`]. - fn min_bounds(&self) -> Size; - - /// Tests whether the provided point is within the boundaries of the - /// [`Paragraph`], returning information about the nearest character. - fn hit_test(&self, point: Point) -> Option<Hit>; - - /// Returns the distance to the given grapheme index in the [`Paragraph`]. - fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; - - /// Updates the [`Paragraph`] to match the given [`Text`], if needed. - fn update(&mut self, text: Text<'_, Self::Font>) { - match self.compare(text) { - Difference::None => {} - Difference::Bounds => { - self.resize(text.bounds); - } - Difference::Shape => { - *self = Self::with_text(text); - } - } - } - - /// Returns the minimum width that can fit the contents of the [`Paragraph`]. - fn min_width(&self) -> f32 { - self.min_bounds().width - } - - /// Returns the minimum height that can fit the contents of the [`Paragraph`]. - fn min_height(&self) -> f32 { - self.min_bounds().height - } -} - -/// The difference detected in some text. -/// -/// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some -/// [`Text`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Difference { - /// No difference. - /// - /// The text can be reused as it is! - None, - - /// A bounds difference. - /// - /// This normally means a relayout is necessary, but the shape of the text can - /// be reused. - Bounds, - - /// A shape difference. - /// - /// The contents, alignment, sizes, fonts, or any other essential attributes - /// of the shape of the text have changed. A complete reshape and relayout of - /// the text is necessary. - Shape, -} diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs new file mode 100644 index 00000000..a4fd0ec1 --- /dev/null +++ b/core/src/text/editor.rs @@ -0,0 +1,68 @@ +use crate::text::LineHeight; +use crate::{Pixels, Point, Rectangle, Size}; + +pub trait Editor: Sized + Default { + type Font: Copy + PartialEq + Default; + + /// Creates a new [`Editor`] laid out with the given text. + fn with_text(text: &str) -> Self; + + fn cursor(&self) -> Cursor; + + fn perform(&mut self, action: Action); + + /// Returns the current boundaries of the [`Editor`]. + fn bounds(&self) -> Size; + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Editor`]. + fn min_bounds(&self) -> Size; + + /// Updates the [`Editor`] with some new attributes. + fn update( + &mut self, + new_bounds: Size, + new_font: Self::Font, + new_size: Pixels, + new_line_height: LineHeight, + ); + + /// Returns the minimum width that can fit the contents of the [`Editor`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Editor`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { + MoveLeft, + MoveRight, + MoveUp, + MoveDown, + MoveLeftWord, + MoveRightWord, + MoveHome, + MoveEnd, + SelectWord, + SelectLine, + Insert(char), + Backspace, + Delete, + Click(Point), + Drag(Point), +} + +/// The cursor of an [`Editor`]. +#[derive(Debug, Clone)] +pub enum Cursor { + /// Cursor without a selection + Caret(Point), + + /// Cursor selecting a range of text + Selection(Vec<Rectangle>), +} diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs new file mode 100644 index 00000000..de1fb74d --- /dev/null +++ b/core/src/text/paragraph.rs @@ -0,0 +1,59 @@ +use crate::alignment; +use crate::text::{Difference, Hit, Text}; +use crate::{Point, Size}; + +/// A text paragraph. +pub trait Paragraph: Sized + Default { + /// The font of this [`Paragraph`]. + type Font: Copy + PartialEq; + + /// Creates a new [`Paragraph`] laid out with the given [`Text`]. + fn with_text(text: Text<'_, Self::Font>) -> Self; + + /// Lays out the [`Paragraph`] with some new boundaries. + fn resize(&mut self, new_bounds: Size); + + /// Compares the [`Paragraph`] with some desired [`Text`] and returns the + /// [`Difference`]. + fn compare(&self, text: Text<'_, Self::Font>) -> Difference; + + /// Returns the horizontal alignment of the [`Paragraph`]. + fn horizontal_alignment(&self) -> alignment::Horizontal; + + /// Returns the vertical alignment of the [`Paragraph`]. + fn vertical_alignment(&self) -> alignment::Vertical; + + /// Returns the minimum boundaries that can fit the contents of the + /// [`Paragraph`]. + fn min_bounds(&self) -> Size; + + /// Tests whether the provided point is within the boundaries of the + /// [`Paragraph`], returning information about the nearest character. + fn hit_test(&self, point: Point) -> Option<Hit>; + + /// Returns the distance to the given grapheme index in the [`Paragraph`]. + fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; + + /// Updates the [`Paragraph`] to match the given [`Text`], if needed. + fn update(&mut self, text: Text<'_, Self::Font>) { + match self.compare(text) { + Difference::None => {} + Difference::Bounds => { + self.resize(text.bounds); + } + Difference::Shape => { + *self = Self::with_text(text); + } + } + } + + /// Returns the minimum width that can fit the contents of the [`Paragraph`]. + fn min_width(&self) -> f32 { + self.min_bounds().width + } + + /// Returns the minimum height that can fit the contents of the [`Paragraph`]. + fn min_height(&self) -> f32 { + self.min_bounds().height + } +} diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml new file mode 100644 index 00000000..528cf23c --- /dev/null +++ b/examples/editor/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "editor" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug"] \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs new file mode 100644 index 00000000..50989ac5 --- /dev/null +++ b/examples/editor/src/main.rs @@ -0,0 +1,49 @@ +use iced::widget::{container, text_editor}; +use iced::{Element, Font, Sandbox, Settings}; + +pub fn main() -> iced::Result { + Editor::run(Settings::default()) +} + +struct Editor { + content: text_editor::Content, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Edit(text_editor::Action), +} + +impl Sandbox for Editor { + type Message = Message; + + fn new() -> Self { + Self { + content: text_editor::Content::with(include_str!( + "../../../README.md" + )), + } + } + + fn title(&self) -> String { + String::from("Editor - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::Edit(action) => { + self.content.edit(action); + } + } + } + + fn view(&self) -> Element<Message> { + container( + text_editor(&self.content) + .on_edit(Message::Edit) + .font(Font::with_name("Hasklug Nerd Font Mono")), + ) + .padding(20) + .into() + } +} diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 3276c2d4..595cc274 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -66,6 +66,13 @@ impl<T: Damage> Damage for Primitive<T> { bounds.expand(1.5) } + Self::Editor { + editor, position, .. + } => { + let bounds = Rectangle::new(*position, editor.bounds); + + bounds.expand(1.5) + } Self::Quad { bounds, .. } | Self::Image { bounds, .. } | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 8a97e6e7..ce0b734b 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -4,6 +4,7 @@ use crate::core::image; use crate::core::svg; use crate::core::text; use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; +use crate::text::editor; use crate::text::paragraph; use std::sync::Arc; @@ -41,6 +42,15 @@ pub enum Primitive<T> { /// The color of the paragraph. color: Color, }, + /// An editor primitive + Editor { + /// The [`editor::Weak`] reference. + editor: editor::Weak, + /// The position of the paragraph. + position: Point, + /// The color of the paragraph. + color: Color, + }, /// A quad primitive Quad { /// The bounds of the quad diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index c5033d36..9b699183 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -141,6 +141,7 @@ where { type Font = Font; type Paragraph = text::Paragraph; + type Editor = text::Editor; const ICON_FONT: Font = Font::with_name("Iced-Icons"); const CHECKMARK_ICON: char = '\u{f00c}'; @@ -171,6 +172,19 @@ where }); } + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ) { + self.primitives.push(Primitive::Editor { + editor: editor.downgrade(), + position, + color, + }); + } + fn fill_text( &mut self, text: Text<'_, Self::Font>, diff --git a/graphics/src/text.rs b/graphics/src/text.rs index f5ccaf52..280e4f01 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -1,7 +1,9 @@ pub mod cache; +pub mod editor; pub mod paragraph; pub use cache::Cache; +pub use editor::Editor; pub use paragraph::Paragraph; pub use cosmic_text; diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs new file mode 100644 index 00000000..53f63fea --- /dev/null +++ b/graphics/src/text/editor.rs @@ -0,0 +1,327 @@ +use crate::core::text::editor::{self, Action, Cursor}; +use crate::core::text::LineHeight; +use crate::core::{Font, Pixels, Point, Size}; +use crate::text; + +use cosmic_text::Edit; + +use std::fmt; +use std::sync::{self, Arc}; + +#[derive(Debug, PartialEq)] +pub struct Editor(Option<Arc<Internal>>); + +struct Internal { + editor: cosmic_text::Editor, + font: Font, + bounds: Size, + min_bounds: Size, + version: text::Version, +} + +impl Editor { + pub fn new() -> Self { + Self::default() + } + + pub fn buffer(&self) -> &cosmic_text::Buffer { + &self.internal().editor.buffer() + } + + pub fn downgrade(&self) -> Weak { + let editor = self.internal(); + + Weak { + raw: Arc::downgrade(editor), + bounds: editor.bounds, + } + } + + fn internal(&self) -> &Arc<Internal> { + self.0 + .as_ref() + .expect("editor should always be initialized") + } +} + +impl editor::Editor for Editor { + type Font = Font; + + fn with_text(text: &str) -> Self { + let mut buffer = cosmic_text::Buffer::new_empty(cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }); + + buffer.set_text( + text::font_system() + .write() + .expect("Write font system") + .raw(), + text, + cosmic_text::Attrs::new(), + cosmic_text::Shaping::Advanced, + ); + + Editor(Some(Arc::new(Internal { + editor: cosmic_text::Editor::new(buffer), + ..Default::default() + }))) + } + + fn cursor(&self) -> editor::Cursor { + let internal = self.internal(); + + match internal.editor.select_opt() { + Some(selection) => { + // TODO + Cursor::Selection(vec![]) + } + None => { + let cursor = internal.editor.cursor(); + let buffer = internal.editor.buffer(); + + let lines_before_cursor: usize = buffer + .lines + .iter() + .take(cursor.line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + let line = buffer + .lines + .get(cursor.line) + .expect("Cursor line should be present"); + + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + let mut lines = layout.iter().enumerate(); + + let (subline, offset) = lines + .find_map(|(i, line)| { + let start = line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let is_cursor_after_start = start <= cursor.index; + + let is_cursor_before_end = match cursor.affinity { + cosmic_text::Affinity::Before => { + cursor.index <= end + } + cosmic_text::Affinity::After => cursor.index < end, + }; + + if is_cursor_after_start && is_cursor_before_end { + let offset = line + .glyphs + .iter() + .take_while(|glyph| cursor.index > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + Some((i, offset)) + } else { + None + } + }) + .unwrap_or((0, 0.0)); + + let line_height = buffer.metrics().line_height; + + let scroll_offset = buffer.scroll() as f32 * line_height; + + Cursor::Caret(Point::new( + offset, + (lines_before_cursor + subline) as f32 * line_height + - scroll_offset, + )) + } + } + } + + fn perform(&mut self, action: Action) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let editor = + self.0.take().expect("Editor should always be initialized"); + + // TODO: Handle multiple strong references somehow + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let editor = &mut internal.editor; + + let mut act = |action| editor.action(font_system.raw(), action); + + match action { + Action::MoveLeft => act(cosmic_text::Action::Left), + Action::MoveRight => act(cosmic_text::Action::Right), + Action::MoveUp => act(cosmic_text::Action::Up), + Action::MoveDown => act(cosmic_text::Action::Down), + Action::Insert(c) => act(cosmic_text::Action::Insert(c)), + Action::Backspace => act(cosmic_text::Action::Backspace), + Action::Delete => act(cosmic_text::Action::Delete), + Action::Click(position) => act(cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }), + Action::Drag(position) => act(cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }), + _ => todo!(), + } + + editor.shape_as_needed(font_system.raw()); + + self.0 = Some(Arc::new(internal)); + } + + fn bounds(&self) -> Size { + self.internal().bounds + } + + fn min_bounds(&self) -> Size { + self.internal().min_bounds + } + + fn update( + &mut self, + new_bounds: Size, + new_font: Font, + new_size: Pixels, + new_line_height: LineHeight, + ) { + let editor = + self.0.take().expect("editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut changed = false; + + if new_font != internal.font { + for line in internal.editor.buffer_mut().lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + text::to_attributes(new_font), + )); + } + + changed = true; + } + + let metrics = internal.editor.buffer().metrics(); + let new_line_height = new_line_height.to_absolute(new_size); + + if new_size.0 != metrics.font_size + || new_line_height.0 != metrics.line_height + { + internal.editor.buffer_mut().set_metrics( + font_system.raw(), + cosmic_text::Metrics::new(new_size.0, new_line_height.0), + ); + + changed = true; + } + + if new_bounds != internal.bounds { + internal.editor.buffer_mut().set_size( + font_system.raw(), + new_bounds.width, + new_bounds.height, + ); + + internal.bounds = new_bounds; + changed = true; + } + + if changed { + internal.min_bounds = text::measure(&internal.editor.buffer()); + } + + self.0 = Some(Arc::new(internal)); + } +} + +impl Default for Editor { + fn default() -> Self { + Self(Some(Arc::new(Internal::default()))) + } +} + +impl PartialEq for Internal { + fn eq(&self, other: &Self) -> bool { + self.font == other.font + && self.bounds == other.bounds + && self.min_bounds == other.min_bounds + && self.editor.buffer().metrics() == other.editor.buffer().metrics() + } +} + +impl Default for Internal { + fn default() -> Self { + Self { + editor: cosmic_text::Editor::new(cosmic_text::Buffer::new_empty( + cosmic_text::Metrics { + font_size: 1.0, + line_height: 1.0, + }, + )), + font: Font::default(), + bounds: Size::ZERO, + min_bounds: Size::ZERO, + version: text::Version::default(), + } + } +} + +impl fmt::Debug for Internal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Internal") + .field("font", &self.font) + .field("bounds", &self.bounds) + .field("min_bounds", &self.min_bounds) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct Weak { + raw: sync::Weak<Internal>, + pub bounds: Size, +} + +impl Weak { + pub fn upgrade(&self) -> Option<Editor> { + self.raw.upgrade().map(Some).map(Editor) + } +} + +impl PartialEq for Weak { + fn eq(&self, other: &Self) -> bool { + match (self.raw.upgrade(), other.raw.upgrade()) { + (Some(p1), Some(p2)) => p1 == p2, + _ => false, + } + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 73e56890..6f044af6 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -32,6 +32,7 @@ use crate::core::text::{self, Text}; use crate::core::{ Background, Color, Font, Pixels, Point, Rectangle, Size, Vector, }; +use crate::graphics::text::Editor; use crate::graphics::text::Paragraph; use crate::graphics::Mesh; @@ -159,6 +160,7 @@ impl<T> core::Renderer for Renderer<T> { impl<T> text::Renderer for Renderer<T> { type Font = Font; type Paragraph = Paragraph; + type Editor = Editor; const ICON_FONT: Font = iced_tiny_skia::Renderer::<T>::ICON_FONT; const CHECKMARK_ICON: char = iced_tiny_skia::Renderer::<T>::CHECKMARK_ICON; @@ -179,14 +181,27 @@ impl<T> text::Renderer for Renderer<T> { fn fill_paragraph( &mut self, - text: &Self::Paragraph, + paragraph: &Self::Paragraph, position: Point, color: Color, ) { delegate!( self, renderer, - renderer.fill_paragraph(text, position, color) + renderer.fill_paragraph(paragraph, position, color) + ); + } + + fn fill_editor( + &mut self, + editor: &Self::Editor, + position: Point, + color: Color, + ) { + delegate!( + self, + renderer, + renderer.fill_editor(editor, position, color) ); } diff --git a/style/src/lib.rs b/style/src/lib.rs index 0c555ed8..7a97ac77 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -35,6 +35,7 @@ pub mod rule; pub mod scrollable; pub mod slider; pub mod svg; +pub mod text_editor; pub mod text_input; pub mod theme; pub mod toggler; diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs new file mode 100644 index 00000000..45c9bad8 --- /dev/null +++ b/style/src/text_editor.rs @@ -0,0 +1,47 @@ +//! Change the appearance of a text editor. +use iced_core::{Background, BorderRadius, Color}; + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The border radius of the text input. + pub border_radius: BorderRadius, + /// The border width of the text input. + pub border_width: f32, + /// The border [`Color`] of the text input. + pub border_color: Color, +} + +/// A set of rules that dictate the style of a text input. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the style of an active text input. + fn active(&self, style: &Self::Style) -> Appearance; + + /// Produces the style of a focused text input. + fn focused(&self, style: &Self::Style) -> Appearance; + + /// Produces the [`Color`] of the placeholder of a text input. + fn placeholder_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the value of a text input. + fn value_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the value of a disabled text input. + fn disabled_color(&self, style: &Self::Style) -> Color; + + /// Produces the [`Color`] of the selection of a text input. + fn selection_color(&self, style: &Self::Style) -> Color; + + /// Produces the style of an hovered text input. + fn hovered(&self, style: &Self::Style) -> Appearance { + self.focused(style) + } + + /// Produces the style of a disabled text input. + fn disabled(&self, style: &Self::Style) -> Appearance; +} diff --git a/style/src/theme.rs b/style/src/theme.rs index 893d7202..a1501c01 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -17,6 +17,7 @@ use crate::rule; use crate::scrollable; use crate::slider; use crate::svg; +use crate::text_editor; use crate::text_input; use crate::toggler; @@ -1174,3 +1175,115 @@ impl text_input::StyleSheet for Theme { self.placeholder_color(style) } } + +/// The style of a text input. +#[derive(Default)] +pub enum TextEditor { + /// The default style. + #[default] + Default, + /// A custom style. + Custom(Box<dyn text_editor::StyleSheet<Style = Theme>>), +} + +impl text_editor::StyleSheet for Theme { + type Style = TextEditor; + + fn active(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.active(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn hovered(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.hovered(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.base.text, + } + } + + fn focused(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.focused(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.base.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.primary.strong.color, + } + } + + fn placeholder_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.placeholder_color(self); + } + + let palette = self.extended_palette(); + + palette.background.strong.color + } + + fn value_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.value_color(self); + } + + let palette = self.extended_palette(); + + palette.background.base.text + } + + fn selection_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.selection_color(self); + } + + let palette = self.extended_palette(); + + palette.primary.weak.color + } + + fn disabled(&self, style: &Self::Style) -> text_editor::Appearance { + if let TextEditor::Custom(custom) = style { + return custom.disabled(self); + } + + let palette = self.extended_palette(); + + text_editor::Appearance { + background: palette.background.weak.color.into(), + border_radius: 2.0.into(), + border_width: 1.0, + border_color: palette.background.strong.color, + } + } + + fn disabled_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(custom) = style { + return custom.disabled_color(self); + } + + self.placeholder_color(style) + } +} diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 72184c8a..5f66dff2 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -383,6 +383,31 @@ impl Backend { clip_mask, ); } + Primitive::Editor { + editor, + position, + color, + } => { + let physical_bounds = + (Rectangle::new(*position, editor.bounds) + translation) + * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_editor( + editor, + *position + translation, + *color, + scale_factor, + pixels, + clip_mask, + ); + } Primitive::Text { content, bounds, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 4f6e3941..d055c749 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -2,6 +2,7 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle}; use crate::graphics::text::cache::{self, Cache}; +use crate::graphics::text::editor; use crate::graphics::text::font_system; use crate::graphics::text::paragraph; @@ -64,6 +65,37 @@ impl Pipeline { ); } + pub fn draw_editor( + &mut self, + editor: &editor::Weak, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + use crate::core::text::Editor as _; + + let Some(editor) = editor.upgrade() else { + return; + }; + + let mut font_system = font_system().write().expect("Write font system"); + + draw( + font_system.raw(), + &mut self.glyph_cache, + editor.buffer(), + Rectangle::new(position, editor.min_bounds()), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + pub fn draw_cached( &mut self, content: &str, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 7a5a0f7c..10b3332d 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -120,12 +120,25 @@ impl<'a> Layer<'a> { } => { let layer = &mut layers[current_layer]; - layer.text.push(Text::Managed { + layer.text.push(Text::Paragraph { paragraph: paragraph.clone(), position: *position + translation, color: *color, }); } + Primitive::Editor { + editor, + position, + color, + } => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text::Editor { + editor: editor.clone(), + position: *position + translation, + color: *color, + }); + } Primitive::Text { content, bounds, diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index b61615d6..d46b39da 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,16 +1,22 @@ use crate::core::alignment; use crate::core::text; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics::text::editor; use crate::graphics::text::paragraph; /// A paragraph of text. #[derive(Debug, Clone)] pub enum Text<'a> { - Managed { + Paragraph { paragraph: paragraph::Weak, position: Point, color: Color, }, + Editor { + editor: editor::Weak, + position: Point, + color: Color, + }, Cached(Cached<'a>), } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 5c9f4d7e..397c38dd 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{font_system, Paragraph}; +use crate::graphics::text::{font_system, Editor, Paragraph}; use crate::layer::Text; use std::borrow::Cow; @@ -74,15 +74,19 @@ impl Pipeline { enum Allocation { Paragraph(Paragraph), + Editor(Editor), Cache(cache::KeyHash), } let allocations: Vec<_> = sections .iter() .map(|section| match section { - Text::Managed { paragraph, .. } => { + Text::Paragraph { paragraph, .. } => { paragraph.upgrade().map(Allocation::Paragraph) } + Text::Editor { editor, .. } => { + editor.upgrade().map(Allocation::Editor) + } Text::Cached(text) => { let (key, _) = cache.allocate( font_system, @@ -117,7 +121,7 @@ impl Pipeline { vertical_alignment, color, ) = match section { - Text::Managed { + Text::Paragraph { position, color, .. } => { use crate::core::text::Paragraph as _; @@ -135,6 +139,24 @@ impl Pipeline { *color, ) } + Text::Editor { + position, color, .. + } => { + use crate::core::text::Editor as _; + + let Some(Allocation::Editor(editor)) = allocation + else { + return None; + }; + + ( + editor.buffer(), + Rectangle::new(*position, editor.min_bounds()), + alignment::Horizontal::Left, + alignment::Vertical::Top, + *color, + ) + } Text::Cached(text) => { let Some(Allocation::Cache(key)) = allocation else { return None; diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3c9c2b29..61541eac 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -16,6 +16,7 @@ use crate::runtime::Command; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; +use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; @@ -206,6 +207,20 @@ where TextInput::new(placeholder, value) } +/// Creates a new [`TextEditor`]. +/// +/// [`TextEditor`]: crate::TextEditor +pub fn text_editor<'a, Message, Renderer>( + content: &'a text_editor::Content<Renderer>, +) -> TextEditor<'a, Message, Renderer> +where + Message: Clone, + Renderer: core::text::Renderer, + Renderer::Theme: text_editor::StyleSheet, +{ + TextEditor::new(content) +} + /// Creates a new [`Slider`]. /// /// [`Slider`]: crate::Slider diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 7e204171..f8e5e865 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,8 +4,8 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![deny( - missing_debug_implementations, - missing_docs, + // missing_debug_implementations, + // missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, @@ -41,6 +41,7 @@ pub mod scrollable; pub mod slider; pub mod space; pub mod text; +pub mod text_editor; pub mod text_input; pub mod toggler; pub mod tooltip; diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs new file mode 100644 index 00000000..d09f2c3e --- /dev/null +++ b/widget/src/text_editor.rs @@ -0,0 +1,457 @@ +use crate::core::event::{self, Event}; +use crate::core::keyboard; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::editor::{Cursor, Editor as _}; +use crate::core::text::{self, LineHeight}; +use crate::core::widget::{self, Widget}; +use crate::core::{ + Clipboard, Color, Element, Length, Padding, Pixels, Point, Rectangle, + Shell, Vector, +}; + +use std::cell::RefCell; + +pub use crate::style::text_editor::{Appearance, StyleSheet}; +pub use text::editor::Action; + +pub struct TextEditor<'a, Message, Renderer = crate::Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + content: &'a Content<Renderer>, + font: Option<Renderer::Font>, + text_size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + padding: Padding, + style: <Renderer::Theme as StyleSheet>::Style, + on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, +} + +impl<'a, Message, Renderer> TextEditor<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + pub fn new(content: &'a Content<Renderer>) -> Self { + Self { + content, + font: None, + text_size: None, + line_height: LineHeight::default(), + width: Length::Fill, + height: Length::Fill, + padding: Padding::new(5.0), + style: Default::default(), + on_edit: None, + } + } + + pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { + self.on_edit = Some(Box::new(on_edit)); + self + } + + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + pub fn padding(mut self, padding: impl Into<Padding>) -> Self { + self.padding = padding.into(); + self + } +} + +pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>) +where + R: text::Renderer; + +struct Internal<R> +where + R: text::Renderer, +{ + editor: R::Editor, + is_dirty: bool, +} + +impl<R> Content<R> +where + R: text::Renderer, +{ + pub fn new() -> Self { + Self::with("") + } + + pub fn with(text: &str) -> Self { + Self(RefCell::new(Internal { + editor: R::Editor::with_text(text), + is_dirty: true, + })) + } + + pub fn edit(&mut self, action: Action) { + let internal = self.0.get_mut(); + + internal.editor.perform(action); + internal.is_dirty = true; + } +} + +impl<Renderer> Default for Content<Renderer> +where + Renderer: text::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +struct State { + is_focused: bool, + is_dragging: bool, + last_click: Option<mouse::Click>, +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextEditor<'a, Message, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::<State>() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State { + is_focused: false, + is_dragging: false, + last_click: None, + }) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> iced_renderer::core::layout::Node { + let mut internal = self.content.0.borrow_mut(); + + internal.editor.update( + limits.pad(self.padding).max(), + self.font.unwrap_or_else(|| renderer.default_font()), + self.text_size.unwrap_or_else(|| renderer.default_size()), + self.line_height, + ); + + layout::Node::new(limits.max()) + } + + fn on_event( + &mut self, + tree: &mut widget::Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let Some(on_edit) = self.on_edit.as_ref() else { + return event::Status::Ignored; + }; + + let state = tree.state.downcast_mut::<State>(); + + let Some(update) = Update::from_event( + event, + state, + layout.bounds(), + self.padding, + cursor, + ) else { + return event::Status::Ignored; + }; + + match update { + Update::Focus { click, action } => { + state.is_focused = true; + state.last_click = Some(click); + shell.publish(on_edit(action)); + } + Update::Unfocus => { + state.is_focused = false; + state.is_dragging = false; + } + Update::Click { click, action } => { + state.last_click = Some(click); + state.is_dragging = true; + shell.publish(on_edit(action)); + } + Update::StopDragging => { + state.is_dragging = false; + } + Update::Edit(action) => { + shell.publish(on_edit(action)); + } + Update::Copy => {} + Update::Paste => if let Some(_contents) = clipboard.read() {}, + } + + event::Status::Captured + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &<Renderer as renderer::Renderer>::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + let internal = self.content.0.borrow(); + let state = tree.state.downcast_ref::<State>(); + + let is_disabled = self.on_edit.is_none(); + let is_mouse_over = cursor.is_over(bounds); + + let appearance = if is_disabled { + theme.disabled(&self.style) + } else if state.is_focused { + theme.focused(&self.style) + } else if is_mouse_over { + theme.hovered(&self.style) + } else { + theme.active(&self.style) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: appearance.border_radius, + border_width: appearance.border_width, + border_color: appearance.border_color, + }, + appearance.background, + ); + + renderer.fill_editor( + &internal.editor, + bounds.position() + + Vector::new(self.padding.left, self.padding.top), + style.text_color, + ); + + if state.is_focused { + match internal.editor.cursor() { + Cursor::Caret(position) => { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x + bounds.x + self.padding.left, + y: position.y + bounds.y + self.padding.top, + width: 1.0, + height: self + .line_height + .to_absolute(self.text_size.unwrap_or_else( + || renderer.default_size(), + )) + .into(), + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.value_color(&self.style), + ); + } + Cursor::Selection(ranges) => { + for range in ranges { + renderer.fill_quad( + renderer::Quad { + bounds: range + Vector::new(bounds.x, bounds.y), + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.selection_color(&self.style), + ); + } + } + } + } + } + + fn mouse_interaction( + &self, + _state: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let is_disabled = self.on_edit.is_none(); + + if cursor.is_over(layout.bounds()) { + if is_disabled { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } + } +} + +impl<'a, Message, Renderer> From<TextEditor<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self { + Self::new(text_editor) + } +} + +enum Update { + Focus { click: mouse::Click, action: Action }, + Unfocus, + Click { click: mouse::Click, action: Action }, + StopDragging, + Edit(Action), + Copy, + Paste, +} + +impl Update { + fn from_event( + event: Event, + state: &State, + bounds: Rectangle, + padding: Padding, + cursor: mouse::Cursor, + ) -> Option<Self> { + match event { + Event::Mouse(event) => match event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + if let Some(cursor_position) = cursor.position_in(bounds) { + let cursor_position = cursor_position + - Vector::new(padding.top, padding.left); + + if state.is_focused { + let click = mouse::Click::new( + cursor_position, + state.last_click, + ); + + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(cursor_position) + } + mouse::click::Kind::Double => { + Action::SelectWord + } + mouse::click::Kind::Triple => { + Action::SelectLine + } + }; + + Some(Update::Click { click, action }) + } else { + Some(Update::Focus { + click: mouse::Click::new(cursor_position, None), + action: Action::Click(cursor_position), + }) + } + } else if state.is_focused { + Some(Update::Unfocus) + } else { + None + } + } + mouse::Event::ButtonReleased(mouse::Button::Left) => { + Some(Update::StopDragging) + } + mouse::Event::CursorMoved { .. } if state.is_dragging => { + let cursor_position = cursor.position_in(bounds)? + - Vector::new(padding.top, padding.left); + + Some(Self::Edit(Action::Drag(cursor_position))) + } + _ => None, + }, + Event::Keyboard(event) => match event { + keyboard::Event::KeyPressed { + key_code, + modifiers, + } if state.is_focused => match key_code { + keyboard::KeyCode::Left => { + if platform::is_jump_modifier_pressed(modifiers) { + Some(Self::Edit(Action::MoveLeftWord)) + } else { + Some(Self::Edit(Action::MoveLeft)) + } + } + keyboard::KeyCode::Right => { + if platform::is_jump_modifier_pressed(modifiers) { + Some(Self::Edit(Action::MoveRightWord)) + } else { + Some(Self::Edit(Action::MoveRight)) + } + } + keyboard::KeyCode::Up => Some(Self::Edit(Action::MoveUp)), + keyboard::KeyCode::Down => { + Some(Self::Edit(Action::MoveDown)) + } + keyboard::KeyCode::Backspace => { + Some(Self::Edit(Action::Backspace)) + } + keyboard::KeyCode::Delete => { + Some(Self::Edit(Action::Delete)) + } + keyboard::KeyCode::Escape => Some(Self::Unfocus), + _ => None, + }, + keyboard::Event::CharacterReceived(c) if state.is_focused => { + Some(Self::Edit(Action::Insert(c))) + } + _ => None, + }, + _ => None, + } + } +} + +mod platform { + use crate::core::keyboard; + + pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { + if cfg!(target_os = "macos") { + modifiers.alt() + } else { + modifiers.control() + } + } +} -- cgit From 1455911b636f19810e12eeb12a6eed11c5244cfe Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 12 Sep 2023 15:03:23 +0200 Subject: Add `Enter` variant to `Action` in `text::Editor` --- core/src/text/editor.rs | 1 + graphics/src/text/editor.rs | 1 + widget/src/text_editor.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index a4fd0ec1..09d4efde 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -51,6 +51,7 @@ pub enum Action { SelectWord, SelectLine, Insert(char), + Enter, Backspace, Delete, Click(Point), diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 53f63fea..b4d6819f 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -176,6 +176,7 @@ impl editor::Editor for Editor { Action::MoveUp => act(cosmic_text::Action::Up), Action::MoveDown => act(cosmic_text::Action::Down), Action::Insert(c) => act(cosmic_text::Action::Insert(c)), + Action::Enter => act(cosmic_text::Action::Enter), Action::Backspace => act(cosmic_text::Action::Backspace), Action::Delete => act(cosmic_text::Action::Delete), Action::Click(position) => act(cosmic_text::Action::Click { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index d09f2c3e..fcbd3dad 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -425,6 +425,7 @@ impl Update { keyboard::KeyCode::Down => { Some(Self::Edit(Action::MoveDown)) } + keyboard::KeyCode::Enter => Some(Self::Edit(Action::Enter)), keyboard::KeyCode::Backspace => { Some(Self::Edit(Action::Backspace)) } -- cgit From abab1448576fbfa4717b65cdf1455debf44f2df5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 12 Sep 2023 18:20:02 +0200 Subject: Return `Cursor::Caret` if selection matches cursor position in `Editor::cursor` --- graphics/src/text/editor.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index b4d6819f..7b0ddec1 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -72,15 +72,18 @@ impl editor::Editor for Editor { fn cursor(&self) -> editor::Cursor { let internal = self.internal(); + let cursor = internal.editor.cursor(); + let buffer = internal.editor.buffer(); + match internal.editor.select_opt() { - Some(selection) => { + Some(selection) + if cursor.line != selection.line + || cursor.index != selection.index => + { // TODO Cursor::Selection(vec![]) } - None => { - let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); - + _ => { let lines_before_cursor: usize = buffer .lines .iter() -- cgit From 4389ab9865d13e17ce3c66223d7c149437be692b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 12 Sep 2023 18:27:30 +0200 Subject: Fix cursor offset with `Affinity::After` at the end of lines in `Editor::cursor` --- graphics/src/text/editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 7b0ddec1..b39e9831 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -143,7 +143,10 @@ impl editor::Editor for Editor { None } }) - .unwrap_or((0, 0.0)); + .unwrap_or(( + 0, + layout.last().map(|line| line.w).unwrap_or(0.0), + )); let line_height = buffer.metrics().line_height; -- cgit From a28ed825c1f48c61a655c5583eb207999e98f400 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 12 Sep 2023 20:57:46 +0200 Subject: Fix subline positioning in `Editor::cursor` --- graphics/src/text/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index b39e9831..52a5d942 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -144,7 +144,7 @@ impl editor::Editor for Editor { } }) .unwrap_or(( - 0, + layout.len().saturating_sub(1), layout.last().map(|line| line.w).unwrap_or(0.0), )); -- cgit From 40eb648f1e1e2ceb2782eddacbbc966f44de6961 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 13 Sep 2023 15:00:33 +0200 Subject: Implement `Cursor::Selection` calculation in `Editor::cursor` --- graphics/src/text/editor.rs | 118 ++++++++++++++++++++++++++++++++++++++++++-- widget/src/text_editor.rs | 6 ++- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 52a5d942..3544bde6 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,6 +1,6 @@ use crate::core::text::editor::{self, Action, Cursor}; use crate::core::text::LineHeight; -use crate::core::{Font, Pixels, Point, Size}; +use crate::core::{Font, Pixels, Point, Rectangle, Size, Vector}; use crate::text; use cosmic_text::Edit; @@ -80,8 +80,70 @@ impl editor::Editor for Editor { if cursor.line != selection.line || cursor.index != selection.index => { - // TODO - Cursor::Selection(vec![]) + let line_height = buffer.metrics().line_height; + let scroll_offset = buffer.scroll() as f32 * line_height; + + let (start, end) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + + let visual_lines_before_start: usize = buffer + .lines + .iter() + .take(start.line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + let selected_lines = end.line - start.line + 1; + + let regions = buffer + .lines + .iter() + .skip(start.line) + .take(selected_lines) + .enumerate() + .flat_map(|(i, line)| { + highlight_line( + line, + if i == 0 { start.index } else { 0 }, + if i == selected_lines - 1 { + end.index + } else { + line.text().len() + }, + ) + }) + .enumerate() + .filter_map(|(visual_line, (x, width))| { + if width > 0.0 { + Some(Rectangle { + x, + width, + y: visual_line as f32 * line_height, + height: line_height, + }) + } else { + None + } + }) + .map(|region| { + region + + Vector::new( + 0.0, + visual_lines_before_start as f32 * line_height + + scroll_offset, + ) + }) + .collect(); + + Cursor::Selection(regions) } _ => { let lines_before_cursor: usize = buffer @@ -332,3 +394,53 @@ impl PartialEq for Weak { } } } + +fn highlight_line<'a>( + line: &'a cosmic_text::BufferLine, + from: usize, + to: usize, +) -> impl Iterator<Item = (f32, f32)> + 'a { + let layout = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached"); + + layout.iter().map(move |visual_line| { + let start = visual_line + .glyphs + .first() + .map(|glyph| glyph.start) + .unwrap_or(0); + let end = visual_line + .glyphs + .last() + .map(|glyph| glyph.end) + .unwrap_or(0); + + let range = start.max(from)..end.min(to); + + if range.is_empty() { + (0.0, 0.0) + } else if range.start == start && range.end == end { + (0.0, visual_line.w) + } else { + let first_glyph = visual_line + .glyphs + .iter() + .position(|glyph| range.start <= glyph.start) + .unwrap_or(0); + + let mut glyphs = visual_line.glyphs.iter(); + + let x = + glyphs.by_ref().take(first_glyph).map(|glyph| glyph.w).sum(); + + let width: f32 = glyphs + .take_while(|glyph| range.end > glyph.start) + .map(|glyph| glyph.w) + .sum(); + + (x, width) + } + }) +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index fcbd3dad..12e66f68 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -288,7 +288,11 @@ where for range in ranges { renderer.fill_quad( renderer::Quad { - bounds: range + Vector::new(bounds.x, bounds.y), + bounds: range + + Vector::new( + bounds.x + self.padding.left, + bounds.y + self.padding.top, + ), border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, -- cgit From d502c9f16fc78bf6b5253152751480c5b5e5999c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 13 Sep 2023 15:16:47 +0200 Subject: Unify `Focus` and `Click` updates in `widget::text_editor` --- widget/src/text_editor.rs | 48 ++++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 12e66f68..a8069069 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -189,16 +189,12 @@ where }; match update { - Update::Focus { click, action } => { - state.is_focused = true; - state.last_click = Some(click); - shell.publish(on_edit(action)); - } Update::Unfocus => { state.is_focused = false; state.is_dragging = false; } Update::Click { click, action } => { + state.is_focused = true; state.last_click = Some(click); state.is_dragging = true; shell.publish(on_edit(action)); @@ -340,9 +336,8 @@ where } enum Update { - Focus { click: mouse::Click, action: Action }, - Unfocus, Click { click: mouse::Click, action: Action }, + Unfocus, StopDragging, Edit(Action), Copy, @@ -364,31 +359,20 @@ impl Update { let cursor_position = cursor_position - Vector::new(padding.top, padding.left); - if state.is_focused { - let click = mouse::Click::new( - cursor_position, - state.last_click, - ); - - let action = match click.kind() { - mouse::click::Kind::Single => { - Action::Click(cursor_position) - } - mouse::click::Kind::Double => { - Action::SelectWord - } - mouse::click::Kind::Triple => { - Action::SelectLine - } - }; - - Some(Update::Click { click, action }) - } else { - Some(Update::Focus { - click: mouse::Click::new(cursor_position, None), - action: Action::Click(cursor_position), - }) - } + let click = mouse::Click::new( + cursor_position, + state.last_click, + ); + + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(cursor_position) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; + + Some(Update::Click { click, action }) } else if state.is_focused { Some(Update::Unfocus) } else { -- cgit From 52b36a9574f45138363a4bfc6394c6da03baa433 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 13 Sep 2023 15:17:04 +0200 Subject: Use `Theme::Dark` in `editor` example --- examples/editor/src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 50989ac5..2a70b34c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,5 +1,5 @@ use iced::widget::{container, text_editor}; -use iced::{Element, Font, Sandbox, Settings}; +use iced::{Element, Font, Sandbox, Settings, Theme}; pub fn main() -> iced::Result { Editor::run(Settings::default()) @@ -46,4 +46,8 @@ impl Sandbox for Editor { .padding(20) .into() } + + fn theme(&self) -> Theme { + Theme::Dark + } } -- cgit From f4c51a96d50953d5fb6e9eb62194f226e2cbfd3c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 13 Sep 2023 16:11:43 +0200 Subject: Introduce `Motion` concept in `core::text::editor` --- core/src/text/editor.rs | 38 ++++++++++++---- graphics/src/text/editor.rs | 106 ++++++++++++++++++++++++++++++++++---------- widget/src/text_editor.rs | 77 ++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 65 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 09d4efde..f87e18f3 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -40,14 +40,8 @@ pub trait Editor: Sized + Default { #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - MoveLeft, - MoveRight, - MoveUp, - MoveDown, - MoveLeftWord, - MoveRightWord, - MoveHome, - MoveEnd, + Move(Motion), + Select(Motion), SelectWord, SelectLine, Insert(char), @@ -58,6 +52,34 @@ pub enum Action { Drag(Point), } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Motion { + Left, + Right, + Up, + Down, + WordLeft, + WordRight, + Home, + End, + PageUp, + PageDown, + DocumentStart, + DocumentEnd, +} + +impl Motion { + pub fn widen(self) -> Self { + match self { + Self::Left => Self::WordLeft, + Self::Right => Self::WordRight, + Self::Home => Self::DocumentStart, + Self::End => Self::DocumentEnd, + _ => self, + } + } +} + /// The cursor of an [`Editor`]. #[derive(Debug, Clone)] pub enum Cursor { diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 3544bde6..747f3a80 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,4 +1,4 @@ -use crate::core::text::editor::{self, Action, Cursor}; +use crate::core::text::editor::{self, Action, Cursor, Motion}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size, Vector}; use crate::text; @@ -76,10 +76,7 @@ impl editor::Editor for Editor { let buffer = internal.editor.buffer(); match internal.editor.select_opt() { - Some(selection) - if cursor.line != selection.line - || cursor.index != selection.index => - { + Some(selection) => { let line_height = buffer.metrics().line_height; let scroll_offset = buffer.scroll() as f32 * line_height; @@ -236,26 +233,87 @@ impl editor::Editor for Editor { let editor = &mut internal.editor; - let mut act = |action| editor.action(font_system.raw(), action); - match action { - Action::MoveLeft => act(cosmic_text::Action::Left), - Action::MoveRight => act(cosmic_text::Action::Right), - Action::MoveUp => act(cosmic_text::Action::Up), - Action::MoveDown => act(cosmic_text::Action::Down), - Action::Insert(c) => act(cosmic_text::Action::Insert(c)), - Action::Enter => act(cosmic_text::Action::Enter), - Action::Backspace => act(cosmic_text::Action::Backspace), - Action::Delete => act(cosmic_text::Action::Delete), - Action::Click(position) => act(cosmic_text::Action::Click { - x: position.x as i32, - y: position.y as i32, - }), - Action::Drag(position) => act(cosmic_text::Action::Drag { - x: position.x as i32, - y: position.y as i32, - }), - _ => todo!(), + // Motion events + Action::Move(motion) => { + if let Some(_selection) = editor.select_opt() { + editor.set_select_opt(None); + } else { + editor.action( + font_system.raw(), + match motion { + Motion::Left => cosmic_text::Action::Left, + Motion::Right => cosmic_text::Action::Right, + Motion::Up => cosmic_text::Action::Up, + Motion::Down => cosmic_text::Action::Down, + Motion::WordLeft => cosmic_text::Action::LeftWord, + Motion::WordRight => cosmic_text::Action::RightWord, + Motion::Home => cosmic_text::Action::Home, + Motion::End => cosmic_text::Action::End, + Motion::PageUp => cosmic_text::Action::PageUp, + Motion::PageDown => cosmic_text::Action::PageDown, + Motion::DocumentStart => { + cosmic_text::Action::BufferStart + } + Motion::DocumentEnd => { + cosmic_text::Action::BufferEnd + } + }, + ); + } + } + + // Selection events + Action::Select(_motion) => todo!(), + Action::SelectWord => todo!(), + Action::SelectLine => todo!(), + + // Editing events + Action::Insert(c) => { + editor + .action(font_system.raw(), cosmic_text::Action::Insert(c)); + } + Action::Enter => { + editor.action(font_system.raw(), cosmic_text::Action::Enter); + } + Action::Backspace => { + editor + .action(font_system.raw(), cosmic_text::Action::Backspace); + } + Action::Delete => { + editor.action(font_system.raw(), cosmic_text::Action::Delete); + } + + // Mouse events + Action::Click(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }, + ); + } + Action::Drag(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }, + ); + + // Deselect if selection matches cursor position + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + if cursor.line == selection.line + && cursor.index == selection.index + { + editor.set_select_opt(None); + } + } + } } editor.shape_as_needed(font_system.raw()); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a8069069..38c243bd 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -14,7 +14,7 @@ use crate::core::{ use std::cell::RefCell; pub use crate::style::text_editor::{Appearance, StyleSheet}; -pub use text::editor::Action; +pub use text::editor::{Action, Motion}; pub struct TextEditor<'a, Message, Renderer = crate::Renderer> where @@ -189,16 +189,16 @@ where }; match update { - Update::Unfocus => { - state.is_focused = false; - state.is_dragging = false; - } Update::Click { click, action } => { state.is_focused = true; - state.last_click = Some(click); state.is_dragging = true; + state.last_click = Some(click); shell.publish(on_edit(action)); } + Update::Unfocus => { + state.is_focused = false; + state.is_dragging = false; + } Update::StopDragging => { state.is_dragging = false; } @@ -352,6 +352,9 @@ impl Update { padding: Padding, cursor: mouse::Cursor, ) -> Option<Self> { + let edit = |action| Some(Update::Edit(action)); + let move_ = |motion| Some(Update::Edit(Action::Move(motion))); + match event { Event::Mouse(event) => match event { mouse::Event::ButtonPressed(mouse::Button::Left) => { @@ -386,7 +389,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - Some(Self::Edit(Action::Drag(cursor_position))) + edit(Action::Drag(cursor_position)) } _ => None, }, @@ -394,37 +397,31 @@ impl Update { keyboard::Event::KeyPressed { key_code, modifiers, - } if state.is_focused => match key_code { - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) { - Some(Self::Edit(Action::MoveLeftWord)) + } if state.is_focused => { + if let Some(motion) = motion(key_code) { + let motion = if modifiers.control() { + motion.widen() } else { - Some(Self::Edit(Action::MoveLeft)) - } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) { - Some(Self::Edit(Action::MoveRightWord)) + motion + }; + + return edit(if modifiers.shift() { + Action::Select(motion) } else { - Some(Self::Edit(Action::MoveRight)) - } - } - keyboard::KeyCode::Up => Some(Self::Edit(Action::MoveUp)), - keyboard::KeyCode::Down => { - Some(Self::Edit(Action::MoveDown)) - } - keyboard::KeyCode::Enter => Some(Self::Edit(Action::Enter)), - keyboard::KeyCode::Backspace => { - Some(Self::Edit(Action::Backspace)) + Action::Move(motion) + }); } - keyboard::KeyCode::Delete => { - Some(Self::Edit(Action::Delete)) + + match key_code { + keyboard::KeyCode::Enter => edit(Action::Enter), + keyboard::KeyCode::Backspace => edit(Action::Backspace), + keyboard::KeyCode::Delete => edit(Action::Delete), + keyboard::KeyCode::Escape => Some(Self::Unfocus), + _ => None, } - keyboard::KeyCode::Escape => Some(Self::Unfocus), - _ => None, - }, + } keyboard::Event::CharacterReceived(c) if state.is_focused => { - Some(Self::Edit(Action::Insert(c))) + edit(Action::Insert(c)) } _ => None, }, @@ -433,6 +430,20 @@ impl Update { } } +fn motion(key_code: keyboard::KeyCode) -> Option<Motion> { + match key_code { + keyboard::KeyCode::Left => Some(Motion::Left), + keyboard::KeyCode::Right => Some(Motion::Right), + keyboard::KeyCode::Up => Some(Motion::Up), + keyboard::KeyCode::Down => Some(Motion::Down), + keyboard::KeyCode::Home => Some(Motion::Home), + keyboard::KeyCode::End => Some(Motion::End), + keyboard::KeyCode::PageUp => Some(Motion::PageUp), + keyboard::KeyCode::PageDown => Some(Motion::PageDown), + _ => None, + } +} + mod platform { use crate::core::keyboard; -- cgit From f14ef7a6069cf45ae11261d7d20df6a5d7870dde Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 13 Sep 2023 16:31:56 +0200 Subject: Fix `clippy` lints --- graphics/src/text/editor.rs | 27 ++++++++++++++++++--------- widget/src/helpers.rs | 6 +++--- widget/src/text_editor.rs | 30 ++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 747f3a80..d31ea390 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -25,7 +25,7 @@ impl Editor { } pub fn buffer(&self) -> &cosmic_text::Buffer { - &self.internal().editor.buffer() + self.internal().editor.buffer() } pub fn downgrade(&self) -> Weak { @@ -53,11 +53,11 @@ impl editor::Editor for Editor { line_height: 1.0, }); + let mut font_system = + text::font_system().write().expect("Write font system"); + buffer.set_text( - text::font_system() - .write() - .expect("Write font system") - .raw(), + font_system.raw(), text, cosmic_text::Attrs::new(), cosmic_text::Shaping::Advanced, @@ -65,6 +65,7 @@ impl editor::Editor for Editor { Editor(Some(Arc::new(Internal { editor: cosmic_text::Editor::new(buffer), + version: font_system.version(), ..Default::default() }))) } @@ -347,6 +348,14 @@ impl editor::Editor for Editor { let mut changed = false; + if font_system.version() != internal.version { + for line in internal.editor.buffer_mut().lines.iter_mut() { + line.reset(); + } + + changed = true; + } + if new_font != internal.font { for line in internal.editor.buffer_mut().lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( @@ -383,7 +392,7 @@ impl editor::Editor for Editor { } if changed { - internal.min_bounds = text::measure(&internal.editor.buffer()); + internal.min_bounds = text::measure(internal.editor.buffer()); } self.0 = Some(Arc::new(internal)); @@ -453,11 +462,11 @@ impl PartialEq for Weak { } } -fn highlight_line<'a>( - line: &'a cosmic_text::BufferLine, +fn highlight_line( + line: &cosmic_text::BufferLine, from: usize, to: usize, -) -> impl Iterator<Item = (f32, f32)> + 'a { +) -> impl Iterator<Item = (f32, f32)> + '_ { let layout = line .layout_opt() .as_ref() diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 61541eac..e3f31513 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -210,9 +210,9 @@ where /// Creates a new [`TextEditor`]. /// /// [`TextEditor`]: crate::TextEditor -pub fn text_editor<'a, Message, Renderer>( - content: &'a text_editor::Content<Renderer>, -) -> TextEditor<'a, Message, Renderer> +pub fn text_editor<Message, Renderer>( + content: &text_editor::Content<Renderer>, +) -> TextEditor<'_, Message, Renderer> where Message: Clone, Renderer: core::text::Renderer, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 38c243bd..48de6409 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -7,8 +7,8 @@ use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Color, Element, Length, Padding, Pixels, Point, Rectangle, - Shell, Vector, + Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, + Vector, }; use std::cell::RefCell; @@ -205,8 +205,12 @@ where Update::Edit(action) => { shell.publish(on_edit(action)); } - Update::Copy => {} - Update::Paste => if let Some(_contents) = clipboard.read() {}, + Update::Copy => todo!(), + Update::Paste => { + if let Some(_contents) = clipboard.read() { + todo!() + } + } } event::Status::Captured @@ -353,7 +357,6 @@ impl Update { cursor: mouse::Cursor, ) -> Option<Self> { let edit = |action| Some(Update::Edit(action)); - let move_ = |motion| Some(Update::Edit(Action::Move(motion))); match event { Event::Mouse(event) => match event { @@ -399,11 +402,12 @@ impl Update { modifiers, } if state.is_focused => { if let Some(motion) = motion(key_code) { - let motion = if modifiers.control() { - motion.widen() - } else { - motion - }; + let motion = + if platform::is_jump_modifier_pressed(modifiers) { + motion.widen() + } else { + motion + }; return edit(if modifiers.shift() { Action::Select(motion) @@ -417,6 +421,12 @@ impl Update { keyboard::KeyCode::Backspace => edit(Action::Backspace), keyboard::KeyCode::Delete => edit(Action::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), + keyboard::KeyCode::C => Some(Self::Copy), + keyboard::KeyCode::V + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } _ => None, } } -- cgit From c829b4b04e1274f157ea7bb3adf832c4c53ce3e8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 13 Sep 2023 17:55:33 +0200 Subject: Fix unused import in `iced_renderer` --- renderer/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 6f044af6..81f60886 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -29,9 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::text::{self, Text}; -use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Vector, -}; +use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector}; use crate::graphics::text::Editor; use crate::graphics::text::Paragraph; use crate::graphics::Mesh; @@ -219,7 +217,10 @@ impl<T> text::Renderer for Renderer<T> { impl<T> crate::core::image::Renderer for Renderer<T> { type Handle = crate::core::image::Handle; - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { + fn dimensions( + &self, + handle: &crate::core::image::Handle, + ) -> core::Size<u32> { delegate!(self, renderer, renderer.dimensions(handle)) } @@ -230,7 +231,7 @@ impl<T> crate::core::image::Renderer for Renderer<T> { #[cfg(feature = "svg")] impl<T> crate::core::svg::Renderer for Renderer<T> { - fn dimensions(&self, handle: &crate::core::svg::Handle) -> Size<u32> { + fn dimensions(&self, handle: &crate::core::svg::Handle) -> core::Size<u32> { delegate!(self, renderer, renderer.dimensions(handle)) } -- cgit From ab020383b9fd7f2cc15d145dd1a3c0870dc71d8b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 00:41:15 +0200 Subject: Fix scrolling offset for `Cursor::Selection` --- graphics/src/text/editor.rs | 74 +++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d31ea390..c0f8d9d5 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,6 +1,6 @@ use crate::core::text::editor::{self, Action, Cursor, Motion}; use crate::core::text::LineHeight; -use crate::core::{Font, Pixels, Point, Rectangle, Size, Vector}; +use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; use cosmic_text::Edit; @@ -78,29 +78,18 @@ impl editor::Editor for Editor { match internal.editor.select_opt() { Some(selection) => { - let line_height = buffer.metrics().line_height; - let scroll_offset = buffer.scroll() as f32 * line_height; - let (start, end) = if cursor < selection { (cursor, selection) } else { (selection, cursor) }; - let visual_lines_before_start: usize = buffer - .lines - .iter() - .take(start.line) - .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() - }) - .sum(); - + let line_height = buffer.metrics().line_height; let selected_lines = end.line - start.line + 1; + let visual_lines_offset = + visual_lines_offset(start.line, buffer); + let regions = buffer .lines .iter() @@ -124,37 +113,24 @@ impl editor::Editor for Editor { Some(Rectangle { x, width, - y: visual_line as f32 * line_height, + y: (visual_line as i32 + visual_lines_offset) + as f32 + * line_height, height: line_height, }) } else { None } }) - .map(|region| { - region - + Vector::new( - 0.0, - visual_lines_before_start as f32 * line_height - + scroll_offset, - ) - }) .collect(); Cursor::Selection(regions) } _ => { - let lines_before_cursor: usize = buffer - .lines - .iter() - .take(cursor.line) - .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() - }) - .sum(); + let line_height = buffer.metrics().line_height; + + let visual_lines_offset = + visual_lines_offset(cursor.line, buffer); let line = buffer .lines @@ -168,7 +144,7 @@ impl editor::Editor for Editor { let mut lines = layout.iter().enumerate(); - let (subline, offset) = lines + let (visual_line, offset) = lines .find_map(|(i, line)| { let start = line .glyphs @@ -208,14 +184,10 @@ impl editor::Editor for Editor { layout.last().map(|line| line.w).unwrap_or(0.0), )); - let line_height = buffer.metrics().line_height; - - let scroll_offset = buffer.scroll() as f32 * line_height; - Cursor::Caret(Point::new( offset, - (lines_before_cursor + subline) as f32 * line_height - - scroll_offset, + (visual_lines_offset + visual_line as i32) as f32 + * line_height, )) } } @@ -511,3 +483,19 @@ fn highlight_line( } }) } + +fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { + let visual_lines_before_start: usize = buffer + .lines + .iter() + .take(line) + .map(|line| { + line.layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() + }) + .sum(); + + visual_lines_before_start as i32 - buffer.scroll() +} -- cgit From e6c2db8a9312e3fe37f30f049d1fa497892f1a86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 00:47:04 +0200 Subject: Fix `Cursor::Caret` position on lines that wrap on whitespace --- graphics/src/text/editor.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c0f8d9d5..83d41c85 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -157,7 +157,7 @@ impl editor::Editor for Editor { .map(|glyph| glyph.end) .unwrap_or(0); - let is_cursor_after_start = start <= cursor.index; + let is_cursor_before_start = start > cursor.index; let is_cursor_before_end = match cursor.affinity { cosmic_text::Affinity::Before => { @@ -166,7 +166,17 @@ impl editor::Editor for Editor { cosmic_text::Affinity::After => cursor.index < end, }; - if is_cursor_after_start && is_cursor_before_end { + if is_cursor_before_start { + // Sometimes, the glyph we are looking for is right + // between lines. This can happen when a line wraps + // on a space. + // In that case, we can assume the cursor is at the + // end of the previous line. + // i is guaranteed to be > 0 because `start` is always + // 0 for the first line, so there is no way for the + // cursor to be before it. + Some((i - 1, layout[i - 1].w)) + } else if is_cursor_before_end { let offset = line .glyphs .iter() -- cgit From b24b94d82778733ddae1b824d0d7690afcec3056 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 14:18:49 +0200 Subject: Handle motions when a selection is present in `text::Editor` --- core/src/text/editor.rs | 23 +++++++++++++++ graphics/src/text/editor.rs | 70 ++++++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index f87e18f3..3adfc61a 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -78,6 +78,29 @@ impl Motion { _ => self, } } + + pub fn direction(&self) -> Direction { + match self { + Self::Left + | Self::Up + | Self::WordLeft + | Self::Home + | Self::PageUp + | Self::DocumentStart => Direction::Left, + Self::Right + | Self::Down + | Self::WordRight + | Self::End + | Self::PageDown + | Self::DocumentEnd => Direction::Right, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Left, + Right, } /// The cursor of an [`Editor`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 83d41c85..d88bcd1d 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,4 +1,4 @@ -use crate::core::text::editor::{self, Action, Cursor, Motion}; +use crate::core::text::editor::{self, Action, Cursor, Direction, Motion}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -219,30 +219,37 @@ impl editor::Editor for Editor { match action { // Motion events Action::Move(motion) => { - if let Some(_selection) = editor.select_opt() { + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + let (left, right) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + editor.set_select_opt(None); + + match motion { + // These motions are performed as-is even when a selection + // is present + Motion::Home + | Motion::End + | Motion::DocumentStart + | Motion::DocumentEnd => { + editor.action( + font_system.raw(), + motion_to_action(motion), + ); + } + // Other motions simply move the cursor to one end of the selection + _ => editor.set_cursor(match motion.direction() { + Direction::Left => left, + Direction::Right => right, + }), + } } else { - editor.action( - font_system.raw(), - match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => { - cosmic_text::Action::BufferStart - } - Motion::DocumentEnd => { - cosmic_text::Action::BufferEnd - } - }, - ); + editor.action(font_system.raw(), motion_to_action(motion)); } } @@ -509,3 +516,20 @@ fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { visual_lines_before_start as i32 - buffer.scroll() } + +fn motion_to_action(motion: Motion) -> cosmic_text::Action { + match motion { + Motion::Left => cosmic_text::Action::Left, + Motion::Right => cosmic_text::Action::Right, + Motion::Up => cosmic_text::Action::Up, + Motion::Down => cosmic_text::Action::Down, + Motion::WordLeft => cosmic_text::Action::LeftWord, + Motion::WordRight => cosmic_text::Action::RightWord, + Motion::Home => cosmic_text::Action::Home, + Motion::End => cosmic_text::Action::End, + Motion::PageUp => cosmic_text::Action::PageUp, + Motion::PageDown => cosmic_text::Action::PageDown, + Motion::DocumentStart => cosmic_text::Action::BufferStart, + Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + } +} -- cgit From edd591847599a3e47601646ce075cb5b71ea751b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 14:25:46 +0200 Subject: Implement motion selection in `text::Editor` --- graphics/src/text/editor.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index d88bcd1d..c6b2abd5 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -254,7 +254,26 @@ impl editor::Editor for Editor { } // Selection events - Action::Select(_motion) => todo!(), + Action::Select(motion) => { + let cursor = editor.cursor(); + + if editor.select_opt().is_none() { + editor.set_select_opt(Some(cursor)); + } + + editor.action(font_system.raw(), motion_to_action(motion)); + + // Deselect if selection matches cursor position + if let Some(selection) = editor.select_opt() { + let cursor = editor.cursor(); + + if cursor.line == selection.line + && cursor.index == selection.index + { + editor.set_select_opt(None); + } + } + } Action::SelectWord => todo!(), Action::SelectLine => todo!(), -- cgit From f7d66899f1ae087a87be5d084ec1ee9a03dd4ecc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 15:20:23 +0200 Subject: Implement `Action::SelectWord` in `text::Editor` --- graphics/Cargo.toml | 1 + graphics/src/text/editor.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 26bd1435..3165810b 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -34,6 +34,7 @@ raw-window-handle.workspace = true rustc-hash.workspace = true thiserror.workspace = true twox-hash.workspace = true +unicode-segmentation.workspace = true image.workspace = true image.optional = true diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index c6b2abd5..3fd2c4fe 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -274,7 +274,66 @@ impl editor::Editor for Editor { } } } - Action::SelectWord => todo!(), + Action::SelectWord => { + use unicode_segmentation::UnicodeSegmentation; + + let cursor = editor.cursor(); + + if let Some(line) = editor.buffer().lines.get(cursor.line) { + let (start, end) = + UnicodeSegmentation::unicode_word_indices(line.text()) + // Split words with dots + .flat_map(|(i, word)| { + word.split('.').scan(i, |current, word| { + let start = *current; + *current += word.len() + 1; + + Some((start, word)) + }) + }) + // Turn words into ranges + .map(|(i, word)| (i, i + word.len())) + // Find the word at cursor + .find(|&(start, end)| { + start <= cursor.index && cursor.index < end + }) + // Cursor is not in a word. Let's select its punctuation cluster. + .unwrap_or_else(|| { + let start = line.text()[..cursor.index] + .char_indices() + .rev() + .take_while(|(_, c)| { + c.is_ascii_punctuation() + }) + .map(|(i, _)| i) + .last() + .unwrap_or(cursor.index); + + let end = line.text()[cursor.index..] + .char_indices() + .skip_while(|(_, c)| { + c.is_ascii_punctuation() + }) + .map(|(i, _)| i + cursor.index) + .next() + .unwrap_or(cursor.index); + + (start, end) + }); + + if start != end { + editor.set_cursor(cosmic_text::Cursor { + index: start, + ..cursor + }); + + editor.set_select_opt(Some(cosmic_text::Cursor { + index: end, + ..cursor + })); + } + } + } Action::SelectLine => todo!(), // Editing events -- cgit From 8cad1d682a306071f1f03bff4e70196adc946491 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 15:23:20 +0200 Subject: Implement `Action::SelectLine` in `text::Editor` --- graphics/src/text/editor.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 3fd2c4fe..8eec94c9 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -334,7 +334,24 @@ impl editor::Editor for Editor { } } } - Action::SelectLine => todo!(), + Action::SelectLine => { + let cursor = editor.cursor(); + + if let Some(line_length) = editor + .buffer() + .lines + .get(cursor.line) + .map(|line| line.text().len()) + { + editor + .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); + + editor.set_select_opt(Some(cosmic_text::Cursor { + index: line_length, + ..cursor + })); + } + } // Editing events Action::Insert(c) => { -- cgit From c7d02e24e6f8265c205a68bd97b2643d40ae30ee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 18:57:09 +0200 Subject: Remove `Editor::min_bounds` and use `bounds` instead --- core/src/renderer/null.rs | 4 ---- core/src/text/editor.rs | 14 -------------- graphics/src/text/editor.rs | 4 ---- tiny_skia/src/text.rs | 2 +- wgpu/src/text.rs | 2 +- 5 files changed, 2 insertions(+), 24 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index adf75969..e714e492 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -131,10 +131,6 @@ impl text::Editor for () { Size::ZERO } - fn min_bounds(&self) -> Size { - Size::ZERO - } - fn update( &mut self, _new_bounds: Size, diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 3adfc61a..56cda3ef 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -14,10 +14,6 @@ pub trait Editor: Sized + Default { /// Returns the current boundaries of the [`Editor`]. fn bounds(&self) -> Size; - /// Returns the minimum boundaries that can fit the contents of the - /// [`Editor`]. - fn min_bounds(&self) -> Size; - /// Updates the [`Editor`] with some new attributes. fn update( &mut self, @@ -26,16 +22,6 @@ pub trait Editor: Sized + Default { new_size: Pixels, new_line_height: LineHeight, ); - - /// Returns the minimum width that can fit the contents of the [`Editor`]. - fn min_width(&self) -> f32 { - self.min_bounds().width - } - - /// Returns the minimum height that can fit the contents of the [`Editor`]. - fn min_height(&self) -> f32 { - self.min_bounds().height - } } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 8eec94c9..6d9e9bb6 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -410,10 +410,6 @@ impl editor::Editor for Editor { self.internal().bounds } - fn min_bounds(&self) -> Size { - self.internal().min_bounds - } - fn update( &mut self, new_bounds: Size, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index d055c749..96cfbf32 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -86,7 +86,7 @@ impl Pipeline { font_system.raw(), &mut self.glyph_cache, editor.buffer(), - Rectangle::new(position, editor.min_bounds()), + Rectangle::new(position, editor.bounds()), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 397c38dd..581df0cb 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -151,7 +151,7 @@ impl Pipeline { ( editor.buffer(), - Rectangle::new(*position, editor.min_bounds()), + Rectangle::new(*position, editor.bounds()), alignment::Horizontal::Left, alignment::Vertical::Top, *color, -- cgit From 3afac11784b9cedc7e6208e3bf1d0365e1f5e902 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 18:58:52 +0200 Subject: Remove `min_bounds` field in `graphics::text::Editor` --- graphics/src/text/editor.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 6d9e9bb6..07a2d72a 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -15,7 +15,6 @@ struct Internal { editor: cosmic_text::Editor, font: Font, bounds: Size, - min_bounds: Size, version: text::Version, } @@ -426,14 +425,10 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); - let mut changed = false; - if font_system.version() != internal.version { for line in internal.editor.buffer_mut().lines.iter_mut() { line.reset(); } - - changed = true; } if new_font != internal.font { @@ -442,8 +437,6 @@ impl editor::Editor for Editor { text::to_attributes(new_font), )); } - - changed = true; } let metrics = internal.editor.buffer().metrics(); @@ -456,8 +449,6 @@ impl editor::Editor for Editor { font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); - - changed = true; } if new_bounds != internal.bounds { @@ -468,11 +459,6 @@ impl editor::Editor for Editor { ); internal.bounds = new_bounds; - changed = true; - } - - if changed { - internal.min_bounds = text::measure(internal.editor.buffer()); } self.0 = Some(Arc::new(internal)); @@ -489,7 +475,6 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.min_bounds == other.min_bounds && self.editor.buffer().metrics() == other.editor.buffer().metrics() } } @@ -505,7 +490,6 @@ impl Default for Internal { )), font: Font::default(), bounds: Size::ZERO, - min_bounds: Size::ZERO, version: text::Version::default(), } } @@ -516,7 +500,6 @@ impl fmt::Debug for Internal { f.debug_struct("Internal") .field("font", &self.font) .field("bounds", &self.bounds) - .field("min_bounds", &self.min_bounds) .finish() } } -- cgit From 8e6e37e0cee79a2f293abedd18a6a7249575bb63 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 19:05:50 +0200 Subject: Fix broken intra-doc links --- core/src/text.rs | 2 ++ widget/src/lib.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/src/text.rs b/core/src/text.rs index 5aacbcc5..90581fea 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -137,6 +137,8 @@ impl Hit { /// /// You will obtain a [`Difference`] when you [`compare`] a [`Paragraph`] with some /// [`Text`]. +/// +/// [`compare`]: Paragraph::compare #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Difference { /// No difference. diff --git a/widget/src/lib.rs b/widget/src/lib.rs index f8e5e865..4c318d75 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -93,6 +93,8 @@ pub use space::Space; #[doc(no_inline)] pub use text::Text; #[doc(no_inline)] +pub use text_editor::TextEditor; +#[doc(no_inline)] pub use text_input::TextInput; #[doc(no_inline)] pub use toggler::Toggler; -- cgit From f7fc13d98c52a9260b1ab55394a0c3d2693318ed Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 14 Sep 2023 22:55:54 +0200 Subject: Fix `Copy` action being triggered without any modifiers --- widget/src/text_editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 48de6409..114d35ef 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -421,7 +421,9 @@ impl Update { keyboard::KeyCode::Backspace => edit(Action::Backspace), keyboard::KeyCode::Delete => edit(Action::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), - keyboard::KeyCode::C => Some(Self::Copy), + keyboard::KeyCode::C if modifiers.command() => { + Some(Self::Copy) + } keyboard::KeyCode::V if modifiers.command() && !modifiers.alt() => { -- cgit From c6d0443627c22dcf1576303e5a426aa3622f1b7d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 16 Sep 2023 15:27:25 +0200 Subject: Implement methods to query the contents of a `TextEditor` --- core/src/renderer/null.rs | 12 ++++++++++++ core/src/text/editor.rs | 6 ++++++ graphics/src/text/editor.rs | 41 ++++++++++++++++++++++++++++++++++++++ widget/src/text_editor.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e714e492..01a52c7a 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -125,6 +125,18 @@ impl text::Editor for () { text::editor::Cursor::Caret(Point::ORIGIN) } + fn selection(&self) -> Option<String> { + None + } + + fn line(&self, _index: usize) -> Option<&str> { + None + } + + fn line_count(&self) -> usize { + 0 + } + fn perform(&mut self, _action: text::editor::Action) {} fn bounds(&self) -> Size { diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 56cda3ef..5532fac5 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -9,6 +9,12 @@ pub trait Editor: Sized + Default { fn cursor(&self) -> Cursor; + fn selection(&self) -> Option<String>; + + fn line(&self, index: usize) -> Option<&str>; + + fn line_count(&self) -> usize; + fn perform(&mut self, action: Action); /// Returns the current boundaries of the [`Editor`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 07a2d72a..1e375a25 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -69,6 +69,47 @@ impl editor::Editor for Editor { }))) } + fn line(&self, index: usize) -> Option<&str> { + self.buffer() + .lines + .get(index) + .map(cosmic_text::BufferLine::text) + } + + fn line_count(&self) -> usize { + self.buffer().lines.len() + } + + fn selection(&self) -> Option<String> { + let internal = self.internal(); + + let cursor = internal.editor.cursor(); + let selection = internal.editor.select_opt()?; + + let (start, end) = if cursor < selection { + (cursor, selection) + } else { + (selection, cursor) + }; + + Some( + internal.editor.buffer().lines[start.line..=end.line] + .iter() + .enumerate() + .map(|(i, line)| { + if i == 0 { + &line.text()[start.index..] + } else if i == end.line - start.line { + &line.text()[..end.index] + } else { + line.text() + } + }) + .collect::<Vec<_>>() + .join("\n"), + ) + } + fn cursor(&self) -> editor::Cursor { let internal = self.internal(); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 114d35ef..ec7a6d1d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -100,6 +100,54 @@ where internal.editor.perform(action); internal.is_dirty = true; } + + pub fn line_count(&self) -> usize { + self.0.borrow().editor.line_count() + } + + pub fn line( + &self, + index: usize, + ) -> Option<impl std::ops::Deref<Target = str> + '_> { + std::cell::Ref::filter_map(self.0.borrow(), |internal| { + internal.editor.line(index) + }) + .ok() + } + + pub fn lines( + &self, + ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> { + struct Lines<'a, Renderer: text::Renderer> { + internal: std::cell::Ref<'a, Internal<Renderer>>, + current: usize, + } + + impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { + type Item = std::cell::Ref<'a, str>; + + fn next(&mut self) -> Option<Self::Item> { + let line = std::cell::Ref::filter_map( + std::cell::Ref::clone(&self.internal), + |internal| internal.editor.line(self.current), + ) + .ok()?; + + self.current += 1; + + Some(line) + } + } + + Lines { + internal: self.0.borrow(), + current: 0, + } + } + + pub fn selection(&self) -> Option<String> { + self.0.borrow().editor.selection() + } } impl<Renderer> Default for Content<Renderer> -- cgit From d051f21597bb333ac10183aaa3214a292e9aa365 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 16 Sep 2023 15:40:16 +0200 Subject: Implement `Copy` and `Paste` actions for `text::Editor` --- core/src/text/editor.rs | 5 ++++- examples/editor/src/main.rs | 2 +- graphics/src/text/editor.rs | 11 +++++++++++ widget/src/text_editor.rs | 11 ++++++++--- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 5532fac5..003557c1 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,6 +1,8 @@ use crate::text::LineHeight; use crate::{Pixels, Point, Rectangle, Size}; +use std::sync::Arc; + pub trait Editor: Sized + Default { type Font: Copy + PartialEq + Default; @@ -30,13 +32,14 @@ pub trait Editor: Sized + Default { ); } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Action { Move(Motion), Select(Motion), SelectWord, SelectLine, Insert(char), + Paste(Arc<String>), Enter, Backspace, Delete, diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 2a70b34c..11819c69 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -9,7 +9,7 @@ struct Editor { content: text_editor::Content, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 1e375a25..1890cb82 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -398,6 +398,17 @@ impl editor::Editor for Editor { editor .action(font_system.raw(), cosmic_text::Action::Insert(c)); } + Action::Paste(text) => { + editor.insert_string(&text, None); + + // TODO: Fix cosmic-text + // Cursor should be marked as moved after `insert_string`. + let cursor = editor.cursor(); + + editor + .buffer_mut() + .shape_until_cursor(font_system.raw(), cursor); + } Action::Enter => { editor.action(font_system.raw(), cosmic_text::Action::Enter); } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ec7a6d1d..0bb6b7d3 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -12,6 +12,7 @@ use crate::core::{ }; use std::cell::RefCell; +use std::sync::Arc; pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Motion}; @@ -253,10 +254,14 @@ where Update::Edit(action) => { shell.publish(on_edit(action)); } - Update::Copy => todo!(), + Update::Copy => { + if let Some(selection) = self.content.selection() { + clipboard.write(selection); + } + } Update::Paste => { - if let Some(_contents) = clipboard.read() { - todo!() + if let Some(contents) = clipboard.read() { + shell.publish(on_edit(Action::Paste(Arc::new(contents)))); } } } -- cgit From c9dbccba468da683af2513535c40374da804aa60 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 16 Sep 2023 16:27:02 +0200 Subject: Use fork of `cosmic-text` with some minor fixes --- Cargo.toml | 4 ++++ graphics/src/text/editor.rs | 36 +----------------------------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af74a3cf..f8dd5f14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,3 +151,7 @@ wgpu = "0.17" winapi = "0.3" window_clipboard = "0.3" winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } + +[patch.crates-io.cosmic-text] +git = "https://github.com/hecrj/cosmic-text.git" +rev = "cb83458e7d0b84ef37c5beb72dda5046d7d343a6" diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 1890cb82..a828a3bc 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -81,33 +81,7 @@ impl editor::Editor for Editor { } fn selection(&self) -> Option<String> { - let internal = self.internal(); - - let cursor = internal.editor.cursor(); - let selection = internal.editor.select_opt()?; - - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - Some( - internal.editor.buffer().lines[start.line..=end.line] - .iter() - .enumerate() - .map(|(i, line)| { - if i == 0 { - &line.text()[start.index..] - } else if i == end.line - start.line { - &line.text()[..end.index] - } else { - line.text() - } - }) - .collect::<Vec<_>>() - .join("\n"), - ) + self.internal().editor.copy_selection() } fn cursor(&self) -> editor::Cursor { @@ -400,14 +374,6 @@ impl editor::Editor for Editor { } Action::Paste(text) => { editor.insert_string(&text, None); - - // TODO: Fix cosmic-text - // Cursor should be marked as moved after `insert_string`. - let cursor = editor.cursor(); - - editor - .buffer_mut() - .shape_until_cursor(font_system.raw(), cursor); } Action::Enter => { editor.action(font_system.raw(), cosmic_text::Action::Enter); -- cgit From 45c5cfe5774ac99a6e1b1d1014418f68b21b41cf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 16 Sep 2023 19:05:31 +0200 Subject: Avoid drag on double or triple click for now in `TextEditor` --- core/src/mouse/click.rs | 4 ++++ widget/src/text_editor.rs | 52 +++++++++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 4a7d796c..e8e5fb56 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -61,6 +61,10 @@ impl Click { self.kind } + pub fn position(&self) -> Point { + self.position + } + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { let duration = if time > self.time { Some(time - self.time) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0bb6b7d3..68e3c656 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -162,8 +162,8 @@ where struct State { is_focused: bool, - is_dragging: bool, last_click: Option<mouse::Click>, + drag_click: Option<mouse::click::Kind>, } impl<'a, Message, Renderer> Widget<Message, Renderer> @@ -179,8 +179,8 @@ where fn state(&self) -> widget::tree::State { widget::tree::State::new(State { is_focused: false, - is_dragging: false, last_click: None, + drag_click: None, }) } @@ -238,18 +238,27 @@ where }; match update { - Update::Click { click, action } => { + Update::Click(click) => { + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(click.position()) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; + state.is_focused = true; - state.is_dragging = true; state.last_click = Some(click); + state.drag_click = Some(click.kind()); + shell.publish(on_edit(action)); } Update::Unfocus => { state.is_focused = false; - state.is_dragging = false; + state.drag_click = None; } - Update::StopDragging => { - state.is_dragging = false; + Update::Release => { + state.drag_click = None; } Update::Edit(action) => { shell.publish(on_edit(action)); @@ -393,9 +402,9 @@ where } enum Update { - Click { click: mouse::Click, action: Action }, + Click(mouse::Click), Unfocus, - StopDragging, + Release, Edit(Action), Copy, Paste, @@ -423,15 +432,7 @@ impl Update { state.last_click, ); - let action = match click.kind() { - mouse::click::Kind::Single => { - Action::Click(cursor_position) - } - mouse::click::Kind::Double => Action::SelectWord, - mouse::click::Kind::Triple => Action::SelectLine, - }; - - Some(Update::Click { click, action }) + Some(Update::Click(click)) } else if state.is_focused { Some(Update::Unfocus) } else { @@ -439,14 +440,17 @@ impl Update { } } mouse::Event::ButtonReleased(mouse::Button::Left) => { - Some(Update::StopDragging) + Some(Update::Release) } - mouse::Event::CursorMoved { .. } if state.is_dragging => { - let cursor_position = cursor.position_in(bounds)? - - Vector::new(padding.top, padding.left); + mouse::Event::CursorMoved { .. } => match state.drag_click { + Some(mouse::click::Kind::Single) => { + let cursor_position = cursor.position_in(bounds)? + - Vector::new(padding.top, padding.left); - edit(Action::Drag(cursor_position)) - } + edit(Action::Drag(cursor_position)) + } + _ => None, + }, _ => None, }, Event::Keyboard(event) => match event { -- cgit From 723111bb0df486bffaedcaed0722b1793d65bfe3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 16 Sep 2023 19:09:31 +0200 Subject: Remove unnecessary `into_iter` call in `graphics::text` --- graphics/src/text.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 280e4f01..b4aeb2be 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -21,12 +21,11 @@ pub fn font_system() -> &'static RwLock<FontSystem> { FONT_SYSTEM.get_or_init(|| { RwLock::new(FontSystem { - raw: cosmic_text::FontSystem::new_with_fonts( - [cosmic_text::fontdb::Source::Binary(Arc::new( + raw: cosmic_text::FontSystem::new_with_fonts([ + cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), - ))] - .into_iter(), - ), + )), + ]), version: Version::default(), }) }) -- cgit From 76dc82e8e8b5201ec10f8d00d851c1decf998583 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 15:29:14 +0200 Subject: Draft `Highlighter` API --- core/src/renderer/null.rs | 11 ++++++++ core/src/text.rs | 2 ++ core/src/text/editor.rs | 9 ++++++ core/src/text/highlighter.rs | 56 ++++++++++++++++++++++++++++++++++++ graphics/src/text.rs | 8 +++++- graphics/src/text/editor.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++ style/src/lib.rs | 2 +- style/src/text_editor.rs | 16 ++++++++++- widget/src/helpers.rs | 2 +- widget/src/text_editor.rs | 64 ++++++++++++++++++++++++++++++++---------- 10 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 core/src/text/highlighter.rs diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 01a52c7a..21597c8e 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -149,6 +149,17 @@ impl text::Editor for () { _new_font: Self::Font, _new_size: Pixels, _new_line_height: text::LineHeight, + _new_highlighter: &mut impl text::Highlighter, + ) { + } + + fn highlight<H: text::Highlighter>( + &mut self, + _font: Self::Font, + _highlighter: &mut H, + _format_highlight: impl Fn( + &H::Highlight, + ) -> text::highlighter::Format<Self::Font>, ) { } } diff --git a/core/src/text.rs b/core/src/text.rs index 90581fea..9b9c753c 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -2,8 +2,10 @@ mod paragraph; pub mod editor; +pub mod highlighter; pub use editor::Editor; +pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 003557c1..0f439c8d 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,3 +1,4 @@ +use crate::text::highlighter::{self, Highlighter}; use crate::text::LineHeight; use crate::{Pixels, Point, Rectangle, Size}; @@ -29,6 +30,14 @@ pub trait Editor: Sized + Default { new_font: Self::Font, new_size: Pixels, new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, + ); + + fn highlight<H: Highlighter>( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, ); } diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs new file mode 100644 index 00000000..1f9ac840 --- /dev/null +++ b/core/src/text/highlighter.rs @@ -0,0 +1,56 @@ +use crate::Color; + +use std::hash::Hash; +use std::ops::Range; + +pub trait Highlighter: Clone + 'static { + type Settings: Hash; + type Highlight; + + type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)> + where + Self: 'a; + + fn new(settings: &Self::Settings) -> Self; + + fn change_line(&mut self, line: usize); + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; + + fn current_line(&self) -> usize; +} + +#[derive(Debug, Clone, Copy)] +pub struct Style { + pub color: Color, +} + +#[derive(Debug, Clone, Copy)] +pub struct PlainText; + +impl Highlighter for PlainText { + type Settings = (); + type Highlight = (); + + type Iterator<'a> = std::iter::Empty<(Range<usize>, Self::Highlight)>; + + fn new(_settings: &Self::Settings) -> Self { + Self + } + + fn change_line(&mut self, _line: usize) {} + + fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> { + std::iter::empty() + } + + fn current_line(&self) -> usize { + usize::MAX + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Format<Font> { + pub color: Option<Color>, + pub font: Option<Font>, +} diff --git a/graphics/src/text.rs b/graphics/src/text.rs index b4aeb2be..5fcfc699 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -10,7 +10,7 @@ pub use cosmic_text; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::Size; +use crate::core::{Color, Size}; use once_cell::sync::OnceCell; use std::borrow::Cow; @@ -129,3 +129,9 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { Shaping::Advanced => cosmic_text::Shaping::Advanced, } } + +pub fn to_color(color: Color) -> cosmic_text::Color { + let [r, g, b, a] = color.into_rgba8(); + + cosmic_text::Color::rgba(r, g, b, a) +} diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index a828a3bc..901b4295 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,4 +1,5 @@ use crate::core::text::editor::{self, Action, Cursor, Direction, Motion}; +use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; @@ -15,6 +16,7 @@ struct Internal { editor: cosmic_text::Editor, font: Font, bounds: Size, + topmost_line_changed: Option<usize>, version: text::Version, } @@ -433,6 +435,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_highlighter: &mut impl Highlighter, ) { let editor = self.0.take().expect("editor should always be initialized"); @@ -479,6 +482,69 @@ impl editor::Editor for Editor { internal.bounds = new_bounds; } + if let Some(topmost_line_changed) = internal.topmost_line_changed.take() + { + new_highlighter.change_line(topmost_line_changed); + } + + self.0 = Some(Arc::new(internal)); + } + + fn highlight<H: Highlighter>( + &mut self, + font: Self::Font, + highlighter: &mut H, + format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, + ) { + let internal = self.internal(); + + let scroll = internal.editor.buffer().scroll(); + let visible_lines = internal.editor.buffer().visible_lines(); + let last_visible_line = (scroll + visible_lines - 1) as usize; + + let current_line = highlighter.current_line(); + + if current_line > last_visible_line { + return; + } + + let editor = + self.0.take().expect("editor should always be initialized"); + + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let attributes = text::to_attributes(font); + + for line in &mut internal.editor.buffer_mut().lines + [current_line..=last_visible_line] + { + let mut list = cosmic_text::AttrsList::new(attributes); + + for (range, highlight) in highlighter.highlight_line(line.text()) { + let format = format_highlight(&highlight); + + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } + + let _ = line.set_attrs_list(list); + } + + internal.editor.shape_as_needed(font_system.raw()); + self.0 = Some(Arc::new(internal)); } } @@ -508,6 +574,7 @@ impl Default for Internal { )), font: Font::default(), bounds: Size::ZERO, + topmost_line_changed: None, version: text::Version::default(), } } diff --git a/style/src/lib.rs b/style/src/lib.rs index 7a97ac77..c9879f24 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -15,7 +15,7 @@ clippy::needless_borrow, clippy::new_without_default, clippy::useless_conversion, - missing_docs, + // missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs index 45c9bad8..f1c31287 100644 --- a/style/src/text_editor.rs +++ b/style/src/text_editor.rs @@ -1,5 +1,6 @@ //! Change the appearance of a text editor. -use iced_core::{Background, BorderRadius, Color}; +use crate::core::text::highlighter; +use crate::core::{self, Background, BorderRadius, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] @@ -45,3 +46,16 @@ pub trait StyleSheet { /// Produces the style of a disabled text input. fn disabled(&self, style: &Self::Style) -> Appearance; } + +pub trait Highlight<Font = core::Font, Theme = crate::Theme> { + fn format(&self, theme: &Theme) -> highlighter::Format<Font>; +} + +impl<Font, Theme> Highlight<Font, Theme> for () { + fn format(&self, _theme: &Theme) -> highlighter::Format<Font> { + highlighter::Format { + color: None, + font: None, + } + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e3f31513..e0b58722 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -212,7 +212,7 @@ where /// [`TextEditor`]: crate::TextEditor pub fn text_editor<Message, Renderer>( content: &text_editor::Content<Renderer>, -) -> TextEditor<'_, Message, Renderer> +) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Renderer> where Message: Clone, Renderer: core::text::Renderer, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 68e3c656..b17e1156 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -4,6 +4,7 @@ use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; +use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; use crate::core::{ @@ -12,13 +13,15 @@ use crate::core::{ }; use std::cell::RefCell; +use std::ops::DerefMut; use std::sync::Arc; -pub use crate::style::text_editor::{Appearance, StyleSheet}; +pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet}; pub use text::editor::{Action, Motion}; -pub struct TextEditor<'a, Message, Renderer = crate::Renderer> +pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where + Highlighter: text::Highlighter, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { @@ -31,9 +34,11 @@ where padding: Padding, style: <Renderer::Theme as StyleSheet>::Style, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, + highlighter_settings: Highlighter::Settings, } -impl<'a, Message, Renderer> TextEditor<'a, Message, Renderer> +impl<'a, Message, Renderer> + TextEditor<'a, highlighter::PlainText, Message, Renderer> where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -49,9 +54,19 @@ where padding: Padding::new(5.0), style: Default::default(), on_edit: None, + highlighter_settings: (), } } +} +impl<'a, Highlighter, Message, Renderer> + TextEditor<'a, Highlighter, Message, Renderer> +where + Highlighter: text::Highlighter, + Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>, + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { self.on_edit = Some(Box::new(on_edit)); self @@ -160,20 +175,23 @@ where } } -struct State { +struct State<Highlighter> { is_focused: bool, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, + highlighter: RefCell<Highlighter>, } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for TextEditor<'a, Message, Renderer> +impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer> + for TextEditor<'a, Highlighter, Message, Renderer> where + Highlighter: text::Highlighter, + Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { fn tag(&self) -> widget::tree::Tag { - widget::tree::Tag::of::<State>() + widget::tree::Tag::of::<State<Highlighter>>() } fn state(&self) -> widget::tree::State { @@ -181,6 +199,9 @@ where is_focused: false, last_click: None, drag_click: None, + highlighter: RefCell::new(Highlighter::new( + &self.highlighter_settings, + )), }) } @@ -194,17 +215,19 @@ where fn layout( &self, - _tree: &mut widget::Tree, + tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> iced_renderer::core::layout::Node { let mut internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_mut::<State<Highlighter>>(); internal.editor.update( limits.pad(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, + state.highlighter.borrow_mut().deref_mut(), ); layout::Node::new(limits.max()) @@ -225,7 +248,7 @@ where return event::Status::Ignored; }; - let state = tree.state.downcast_mut::<State>(); + let state = tree.state.downcast_mut::<State<Highlighter>>(); let Some(update) = Update::from_event( event, @@ -290,8 +313,14 @@ where ) { let bounds = layout.bounds(); - let internal = self.content.0.borrow(); - let state = tree.state.downcast_ref::<State>(); + let mut internal = self.content.0.borrow_mut(); + let state = tree.state.downcast_ref::<State<Highlighter>>(); + + internal.editor.highlight( + self.font.unwrap_or_else(|| renderer.default_font()), + state.highlighter.borrow_mut().deref_mut(), + |highlight| highlight.format(theme), + ); let is_disabled = self.on_edit.is_none(); let is_mouse_over = cursor.is_over(bounds); @@ -389,14 +418,19 @@ where } } -impl<'a, Message, Renderer> From<TextEditor<'a, Message, Renderer>> +impl<'a, Highlighter, Message, Renderer> + From<TextEditor<'a, Highlighter, Message, Renderer>> for Element<'a, Message, Renderer> where + Highlighter: text::Highlighter, + Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>, Message: 'a, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self { + fn from( + text_editor: TextEditor<'a, Highlighter, Message, Renderer>, + ) -> Self { Self::new(text_editor) } } @@ -411,9 +445,9 @@ enum Update { } impl Update { - fn from_event( + fn from_event<H: Highlighter>( event: Event, - state: &State, + state: &State<H>, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, -- cgit From d3011992a76e83e12f74402c2ade616cdc7f1497 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 19:03:58 +0200 Subject: Implement basic syntax highlighting with `syntect` in `editor` example --- core/src/text/highlighter.rs | 2 +- examples/editor/Cargo.toml | 4 +- examples/editor/src/main.rs | 168 ++++++++++++++++++++++++++++++++++++++++++- graphics/src/text/editor.rs | 25 ++++++- widget/src/text_editor.rs | 18 +++++ 5 files changed, 211 insertions(+), 6 deletions(-) diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index 1f9ac840..a929826f 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -3,7 +3,7 @@ use crate::Color; use std::hash::Hash; use std::ops::Range; -pub trait Highlighter: Clone + 'static { +pub trait Highlighter: 'static { type Settings: Hash; type Highlight; diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 528cf23c..930ee592 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,4 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] \ No newline at end of file +iced.features = ["advanced", "debug"] + +syntect = "5.1" \ No newline at end of file diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 11819c69..a72feebc 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,6 +1,8 @@ use iced::widget::{container, text_editor}; use iced::{Element, Font, Sandbox, Settings, Theme}; +use highlighter::Highlighter; + pub fn main() -> iced::Result { Editor::run(Settings::default()) } @@ -41,7 +43,10 @@ impl Sandbox for Editor { container( text_editor(&self.content) .on_edit(Message::Edit) - .font(Font::with_name("Hasklug Nerd Font Mono")), + .font(Font::with_name("Hasklug Nerd Font Mono")) + .highlight::<Highlighter>(highlighter::Settings { + token: String::from("md"), + }), ) .padding(20) .into() @@ -51,3 +56,164 @@ impl Sandbox for Editor { Theme::Dark } } + +mod highlighter { + use iced::advanced::text::highlighter; + use iced::widget::text_editor; + use iced::{Color, Font, Theme}; + + use std::ops::Range; + use syntect::highlighting; + use syntect::parsing; + + #[derive(Debug, Clone, Hash)] + pub struct Settings { + pub token: String, + } + + pub struct Highlight(highlighting::StyleModifier); + + impl text_editor::Highlight for Highlight { + fn format(&self, _theme: &Theme) -> highlighter::Format<Font> { + highlighter::Format { + color: self.0.foreground.map(|color| { + Color::from_rgba8( + color.r, + color.g, + color.b, + color.a as f32 / 255.0, + ) + }), + font: None, + } + } + } + + pub struct Highlighter { + syntaxes: parsing::SyntaxSet, + parser: parsing::ParseState, + stack: parsing::ScopeStack, + theme: highlighting::Theme, + token: String, + current_line: usize, + } + + impl highlighter::Highlighter for Highlighter { + type Settings = Settings; + type Highlight = Highlight; + + type Iterator<'a> = + Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>; + + fn new(settings: &Self::Settings) -> Self { + let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); + + let syntax = syntaxes + .find_syntax_by_token(&settings.token) + .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + + let parser = parsing::ParseState::new(&syntax); + let stack = parsing::ScopeStack::new(); + + let theme = highlighting::ThemeSet::load_defaults() + .themes + .remove("base16-mocha.dark") + .unwrap(); + + Highlighter { + syntaxes, + parser, + stack, + theme, + token: settings.token.clone(), + current_line: 0, + } + } + + fn change_line(&mut self, _line: usize) { + // TODO: Caching + let syntax = self + .syntaxes + .find_syntax_by_token(&self.token) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()); + + self.parser = parsing::ParseState::new(&syntax); + self.stack = parsing::ScopeStack::new(); + self.current_line = 0; + } + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + self.current_line += 1; + + let ops = self + .parser + .parse_line(line, &self.syntaxes) + .unwrap_or_default(); + + Box::new( + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let highlighter = + highlighting::Highlighter::new(&self.theme); + let _ = self.stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight( + highlighter + .style_mod_for_stack(&self.stack.scopes), + ), + )) + } + }), + ) + } + + fn current_line(&self) -> usize { + self.current_line + } + } + + pub struct ScopeRangeIterator { + ops: Vec<(usize, parsing::ScopeStackOp)>, + line_length: usize, + index: usize, + last_str_index: usize, + } + + impl Iterator for ScopeRangeIterator { + type Item = (std::ops::Range<usize>, parsing::ScopeStackOp); + + fn next(&mut self) -> Option<Self::Item> { + if self.index > self.ops.len() { + return None; + } + + let next_str_i = if self.index == self.ops.len() { + self.line_length + } else { + self.ops[self.index].0 + }; + + let range = self.last_str_index..next_str_i; + self.last_str_index = next_str_i; + + let op = if self.index == 0 { + parsing::ScopeStackOp::Noop + } else { + self.ops[self.index - 1].1.clone() + }; + + self.index += 1; + Some((range, op)) + } + } +} diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 901b4295..58fcc3dc 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -447,17 +447,26 @@ impl editor::Editor for Editor { text::font_system().write().expect("Write font system"); if font_system.version() != internal.version { + log::trace!("Updating `FontSystem` of `Editor`..."); + for line in internal.editor.buffer_mut().lines.iter_mut() { line.reset(); } } if new_font != internal.font { + log::trace!("Updating font of `Editor`..."); + for line in internal.editor.buffer_mut().lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); } + + internal.font = new_font; + internal.topmost_line_changed = Some(0); + + internal.editor.shape_as_needed(font_system.raw()); } let metrics = internal.editor.buffer().metrics(); @@ -466,6 +475,8 @@ impl editor::Editor for Editor { if new_size.0 != metrics.font_size || new_line_height.0 != metrics.line_height { + log::trace!("Updating `Metrics` of `Editor`..."); + internal.editor.buffer_mut().set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), @@ -473,6 +484,8 @@ impl editor::Editor for Editor { } if new_bounds != internal.bounds { + log::trace!("Updating size of `Editor`..."); + internal.editor.buffer_mut().set_size( font_system.raw(), new_bounds.width, @@ -484,6 +497,10 @@ impl editor::Editor for Editor { if let Some(topmost_line_changed) = internal.topmost_line_changed.take() { + log::trace!( + "Notifying highlighter of line change: {topmost_line_changed}" + ); + new_highlighter.change_line(topmost_line_changed); } @@ -497,10 +514,12 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format<Self::Font>, ) { let internal = self.internal(); + let buffer = internal.editor.buffer(); - let scroll = internal.editor.buffer().scroll(); - let visible_lines = internal.editor.buffer().visible_lines(); - let last_visible_line = (scroll + visible_lines - 1) as usize; + let scroll = buffer.scroll(); + let visible_lines = buffer.visible_lines(); + let last_visible_line = + ((scroll + visible_lines) as usize).min(buffer.lines.len()) - 1; let current_line = highlighter.current_line(); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index b17e1156..03adbb59 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -81,6 +81,24 @@ where self.padding = padding.into(); self } + + pub fn highlight<H: text::Highlighter>( + self, + settings: H::Settings, + ) -> TextEditor<'a, H, Message, Renderer> { + TextEditor { + content: self.content, + font: self.font, + text_size: self.text_size, + line_height: self.line_height, + width: self.width, + height: self.height, + padding: self.padding, + style: self.style, + on_edit: self.on_edit, + highlighter_settings: settings, + } + } } pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>) -- cgit From 23d00445ff1225b3e5ca99cb27966143cda8a2ce Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 19:06:20 +0200 Subject: Use `saturating_sub` for `last_visible_line` in `text::Editor` --- graphics/src/text/editor.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 58fcc3dc..fbae287e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -518,8 +518,9 @@ impl editor::Editor for Editor { let scroll = buffer.scroll(); let visible_lines = buffer.visible_lines(); - let last_visible_line = - ((scroll + visible_lines) as usize).min(buffer.lines.len()) - 1; + let last_visible_line = ((scroll + visible_lines) as usize) + .min(buffer.lines.len()) + .saturating_sub(1); let current_line = highlighter.current_line(); -- cgit From 2897986f2ded7318894a52572bec3d62754ebfaa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 19:27:51 +0200 Subject: Notify `Highlighter` of topmost line change --- core/src/text/editor.rs | 9 +++++-- graphics/src/text/editor.rs | 58 +++++++++++++++++++++++++++++++-------------- widget/src/text_editor.rs | 25 ++++++++++--------- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 0f439c8d..2144715f 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -47,13 +47,18 @@ pub enum Action { Select(Motion), SelectWord, SelectLine, + Edit(Edit), + Click(Point), + Drag(Point), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Edit { Insert(char), Paste(Arc<String>), Enter, Backspace, Delete, - Click(Point), - Drag(Point), } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index fbae287e..47c210bd 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,10 +1,12 @@ -use crate::core::text::editor::{self, Action, Cursor, Direction, Motion}; +use crate::core::text::editor::{ + self, Action, Cursor, Direction, Edit, Motion, +}; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::LineHeight; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; -use cosmic_text::Edit; +use cosmic_text::Edit as _; use std::fmt; use std::sync::{self, Arc}; @@ -370,22 +372,42 @@ impl editor::Editor for Editor { } // Editing events - Action::Insert(c) => { - editor - .action(font_system.raw(), cosmic_text::Action::Insert(c)); - } - Action::Paste(text) => { - editor.insert_string(&text, None); - } - Action::Enter => { - editor.action(font_system.raw(), cosmic_text::Action::Enter); - } - Action::Backspace => { - editor - .action(font_system.raw(), cosmic_text::Action::Backspace); - } - Action::Delete => { - editor.action(font_system.raw(), cosmic_text::Action::Delete); + Action::Edit(edit) => { + match edit { + Edit::Insert(c) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Insert(c), + ); + } + Edit::Paste(text) => { + editor.insert_string(&text, None); + } + Edit::Enter => { + editor.action( + font_system.raw(), + cosmic_text::Action::Enter, + ); + } + Edit::Backspace => { + editor.action( + font_system.raw(), + cosmic_text::Action::Backspace, + ); + } + Edit::Delete => { + editor.action( + font_system.raw(), + cosmic_text::Action::Delete, + ); + } + } + + let cursor = editor.cursor(); + let selection = editor.select_opt().unwrap_or(cursor); + + internal.topmost_line_changed = + Some(cursor.min(selection).line); } // Mouse events diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 03adbb59..c30e185f 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -17,7 +17,7 @@ use std::ops::DerefMut; use std::sync::Arc; pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet}; -pub use text::editor::{Action, Motion}; +pub use text::editor::{Action, Edit, Motion}; pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where @@ -301,7 +301,7 @@ where Update::Release => { state.drag_click = None; } - Update::Edit(action) => { + Update::Action(action) => { shell.publish(on_edit(action)); } Update::Copy => { @@ -311,7 +311,9 @@ where } Update::Paste => { if let Some(contents) = clipboard.read() { - shell.publish(on_edit(Action::Paste(Arc::new(contents)))); + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(contents), + )))); } } } @@ -457,7 +459,7 @@ enum Update { Click(mouse::Click), Unfocus, Release, - Edit(Action), + Action(Action), Copy, Paste, } @@ -470,7 +472,8 @@ impl Update { padding: Padding, cursor: mouse::Cursor, ) -> Option<Self> { - let edit = |action| Some(Update::Edit(action)); + let action = |action| Some(Update::Action(action)); + let edit = |edit| action(Action::Edit(edit)); match event { Event::Mouse(event) => match event { @@ -499,7 +502,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - edit(Action::Drag(cursor_position)) + action(Action::Drag(cursor_position)) } _ => None, }, @@ -518,7 +521,7 @@ impl Update { motion }; - return edit(if modifiers.shift() { + return action(if modifiers.shift() { Action::Select(motion) } else { Action::Move(motion) @@ -526,9 +529,9 @@ impl Update { } match key_code { - keyboard::KeyCode::Enter => edit(Action::Enter), - keyboard::KeyCode::Backspace => edit(Action::Backspace), - keyboard::KeyCode::Delete => edit(Action::Delete), + keyboard::KeyCode::Enter => edit(Edit::Enter), + keyboard::KeyCode::Backspace => edit(Edit::Backspace), + keyboard::KeyCode::Delete => edit(Edit::Delete), keyboard::KeyCode::Escape => Some(Self::Unfocus), keyboard::KeyCode::C if modifiers.command() => { Some(Self::Copy) @@ -542,7 +545,7 @@ impl Update { } } keyboard::Event::CharacterReceived(c) if state.is_focused => { - edit(Action::Insert(c)) + edit(Edit::Insert(c)) } _ => None, }, -- cgit From 790c0dabcf0a50a2466e47daeb4f1e149b2ede5a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 21:45:13 +0200 Subject: Implement syntax highlighting cache in `editor` example --- Cargo.toml | 4 +++ examples/editor/src/main.rs | 67 ++++++++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f8dd5f14..70f84460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,3 +155,7 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" rev = "cb83458e7d0b84ef37c5beb72dda5046d7d343a6" + +[patch.crates-io.rangemap] +git = "https://github.com/hecrj/rangemap.git" +branch = "fix/partial-eq" diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a72feebc..1235d38b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -64,7 +64,7 @@ mod highlighter { use std::ops::Range; use syntect::highlighting; - use syntect::parsing; + use syntect::parsing::{self, SyntaxReference}; #[derive(Debug, Clone, Hash)] pub struct Settings { @@ -91,13 +91,14 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, - parser: parsing::ParseState, - stack: parsing::ScopeStack, + syntax: SyntaxReference, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, theme: highlighting::Theme, - token: String, current_line: usize, } + const LINES_PER_SNAPSHOT: usize = 50; + impl highlighter::Highlighter for Highlighter { type Settings = Settings; type Highlight = Highlight; @@ -121,34 +122,53 @@ mod highlighter { .unwrap(); Highlighter { + syntax: syntax.clone(), syntaxes, - parser, - stack, + caches: vec![(parser, stack)], theme, - token: settings.token.clone(), current_line: 0, } } - fn change_line(&mut self, _line: usize) { - // TODO: Caching - let syntax = self - .syntaxes - .find_syntax_by_token(&self.token) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()); + fn change_line(&mut self, line: usize) { + let snapshot = line / LINES_PER_SNAPSHOT; + + if snapshot <= self.caches.len() { + self.caches.truncate(snapshot); + self.current_line = snapshot * LINES_PER_SNAPSHOT; + } else { + self.caches.truncate(1); + self.current_line = 0; + } + + let (parser, stack) = + self.caches.last().cloned().unwrap_or_else(|| { + ( + parsing::ParseState::new(&self.syntax), + parsing::ScopeStack::new(), + ) + }); - self.parser = parsing::ParseState::new(&syntax); - self.stack = parsing::ScopeStack::new(); - self.current_line = 0; + self.caches.push((parser, stack)); } fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { + let (parser, stack) = + self.caches.last().expect("Caches must not be empty"); + + self.caches.push((parser.clone(), stack.clone())); + } + self.current_line += 1; - let ops = self - .parser - .parse_line(line, &self.syntaxes) - .unwrap_or_default(); + let (parser, stack) = + self.caches.last_mut().expect("Caches must not be empty"); + + let ops = + parser.parse_line(line, &self.syntaxes).unwrap_or_default(); + + let highlighter = highlighting::Highlighter::new(&self.theme); Box::new( ScopeRangeIterator { @@ -158,9 +178,7 @@ mod highlighter { last_str_index: 0, } .filter_map(move |(range, scope)| { - let highlighter = - highlighting::Highlighter::new(&self.theme); - let _ = self.stack.apply(&scope); + let _ = stack.apply(&scope); if range.is_empty() { None @@ -168,8 +186,7 @@ mod highlighter { Some(( range, Highlight( - highlighter - .style_mod_for_stack(&self.stack.scopes), + highlighter.style_mod_for_stack(&stack.scopes), ), )) } -- cgit From 86d396cf8bede8155bdd4a7d3f115a0108c67297 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 23:15:38 +0200 Subject: Avoid adding unnecessary spans when syntax highlighting --- Cargo.toml | 2 +- graphics/src/text/editor.rs | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70f84460..ac59085d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" -rev = "cb83458e7d0b84ef37c5beb72dda5046d7d343a6" +branch = "editor-fixes" [patch.crates-io.rangemap] git = "https://github.com/hecrj/rangemap.git" diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 47c210bd..95061c3c 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -569,17 +569,19 @@ impl editor::Editor for Editor { for (range, highlight) in highlighter.highlight_line(line.text()) { let format = format_highlight(&highlight); - list.add_span( - range, - cosmic_text::Attrs { - color_opt: format.color.map(text::to_color), - ..if let Some(font) = format.font { - text::to_attributes(font) - } else { - attributes - } - }, - ); + if format.color.is_some() || format.font.is_some() { + list.add_span( + range, + cosmic_text::Attrs { + color_opt: format.color.map(text::to_color), + ..if let Some(font) = format.font { + text::to_attributes(font) + } else { + attributes + } + }, + ); + } } let _ = line.set_attrs_list(list); -- cgit From 8f8528a4ccee049aba779fe86cda786a52afac30 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 17 Sep 2023 23:20:15 +0200 Subject: Fix unnecessary dereference in `editor` example --- examples/editor/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 1235d38b..74649676 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -113,7 +113,7 @@ mod highlighter { .find_syntax_by_token(&settings.token) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - let parser = parsing::ParseState::new(&syntax); + let parser = parsing::ParseState::new(syntax); let stack = parsing::ScopeStack::new(); let theme = highlighting::ThemeSet::load_defaults() -- cgit From d1440ceca6340d045e556eb05354c254881732f0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 13:39:47 +0200 Subject: Find correct `last_visible_line` in `Editor::highlight` --- graphics/src/text/editor.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 95061c3c..18c9b572 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -538,11 +538,27 @@ impl editor::Editor for Editor { let internal = self.internal(); let buffer = internal.editor.buffer(); - let scroll = buffer.scroll(); - let visible_lines = buffer.visible_lines(); - let last_visible_line = ((scroll + visible_lines) as usize) - .min(buffer.lines.len()) - .saturating_sub(1); + let mut window = buffer.scroll() + buffer.visible_lines(); + + let last_visible_line = buffer + .lines + .iter() + .enumerate() + .find_map(|(i, line)| { + let visible_lines = line + .layout_opt() + .as_ref() + .expect("Line layout should be cached") + .len() as i32; + + if window > visible_lines { + window -= visible_lines; + None + } else { + Some(i) + } + }) + .unwrap_or(buffer.lines.len()); let current_line = highlighter.current_line(); -- cgit From a01b123cec1b57a9100d56f567fcfbf91967b12f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 13:57:47 +0200 Subject: Shape as needed only in `update` during `layout` --- graphics/src/text/editor.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 18c9b572..59096e74 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -442,8 +442,6 @@ impl editor::Editor for Editor { } } - editor.shape_as_needed(font_system.raw()); - self.0 = Some(Arc::new(internal)); } @@ -487,8 +485,6 @@ impl editor::Editor for Editor { internal.font = new_font; internal.topmost_line_changed = Some(0); - - internal.editor.shape_as_needed(font_system.raw()); } let metrics = internal.editor.buffer().metrics(); @@ -526,6 +522,8 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } + internal.editor.shape_as_needed(font_system.raw()); + self.0 = Some(Arc::new(internal)); } -- cgit From b5466f41ca33452fb0d4e8470856c027d3b26e39 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 13:58:39 +0200 Subject: Fix inconsistent `expect` messages in `text::editor` --- graphics/src/text/editor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 59096e74..de1b998b 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -43,7 +43,7 @@ impl Editor { fn internal(&self) -> &Arc<Internal> { self.0 .as_ref() - .expect("editor should always be initialized") + .expect("Editor should always be initialized") } } @@ -458,7 +458,7 @@ impl editor::Editor for Editor { new_highlighter: &mut impl Highlighter, ) { let editor = - self.0.take().expect("editor should always be initialized"); + self.0.take().expect("Editor should always be initialized"); let mut internal = Arc::try_unwrap(editor) .expect("Editor cannot have multiple strong references"); @@ -565,7 +565,7 @@ impl editor::Editor for Editor { } let editor = - self.0.take().expect("editor should always be initialized"); + self.0.take().expect("Editor should always be initialized"); let mut internal = Arc::try_unwrap(editor) .expect("Editor cannot have multiple strong references"); -- cgit From 61ef8f3249218b301d434d04c483ba70562c1df4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 13:58:55 +0200 Subject: Update `version` properly when `FontSystem` changes in `text::editor` --- graphics/src/text/editor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index de1b998b..4673fce3 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -472,6 +472,9 @@ impl editor::Editor for Editor { for line in internal.editor.buffer_mut().lines.iter_mut() { line.reset(); } + + internal.version = font_system.version(); + internal.topmost_line_changed = Some(0); } if new_font != internal.font { -- cgit From 8446fe6de52fa68077d23d39f728f79a29b52f00 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 14:38:54 +0200 Subject: Implement theme selector in `editor` example --- core/src/text/highlighter.rs | 7 ++- examples/editor/src/main.rs | 101 +++++++++++++++++++++++++++++++++++-------- widget/src/text_editor.rs | 13 +++++- 3 files changed, 99 insertions(+), 22 deletions(-) diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index a929826f..b462d083 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -1,10 +1,9 @@ use crate::Color; -use std::hash::Hash; use std::ops::Range; pub trait Highlighter: 'static { - type Settings: Hash; + type Settings: PartialEq + Clone; type Highlight; type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)> @@ -13,6 +12,8 @@ pub trait Highlighter: 'static { fn new(settings: &Self::Settings) -> Self; + fn update(&mut self, new_settings: &Self::Settings); + fn change_line(&mut self, line: usize); fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; @@ -38,6 +39,8 @@ impl Highlighter for PlainText { Self } + fn update(&mut self, _new_settings: &Self::Settings) {} + fn change_line(&mut self, _line: usize) {} fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> { diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 74649676..fa35ba0f 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,5 +1,5 @@ -use iced::widget::{container, text_editor}; -use iced::{Element, Font, Sandbox, Settings, Theme}; +use iced::widget::{column, horizontal_space, pick_list, row, text_editor}; +use iced::{Element, Font, Length, Sandbox, Settings, Theme}; use highlighter::Highlighter; @@ -9,11 +9,13 @@ pub fn main() -> iced::Result { struct Editor { content: text_editor::Content, + theme: highlighter::Theme, } #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), + ThemeSelected(highlighter::Theme), } impl Sandbox for Editor { @@ -21,9 +23,8 @@ impl Sandbox for Editor { fn new() -> Self { Self { - content: text_editor::Content::with(include_str!( - "../../../README.md" - )), + content: text_editor::Content::with(include_str!("main.rs")), + theme: highlighter::Theme::SolarizedDark, } } @@ -36,18 +37,33 @@ impl Sandbox for Editor { Message::Edit(action) => { self.content.edit(action); } + Message::ThemeSelected(theme) => { + self.theme = theme; + } } } fn view(&self) -> Element<Message> { - container( + column![ + row![ + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .padding([5, 10]) + ] + .spacing(10), text_editor(&self.content) .on_edit(Message::Edit) .font(Font::with_name("Hasklug Nerd Font Mono")) .highlight::<Highlighter>(highlighter::Settings { - token: String::from("md"), + theme: self.theme, + extension: String::from("rs"), }), - ) + ] + .spacing(10) .padding(20) .into() } @@ -60,21 +76,52 @@ impl Sandbox for Editor { mod highlighter { use iced::advanced::text::highlighter; use iced::widget::text_editor; - use iced::{Color, Font, Theme}; + use iced::{Color, Font}; use std::ops::Range; use syntect::highlighting; use syntect::parsing::{self, SyntaxReference}; - #[derive(Debug, Clone, Hash)] + #[derive(Debug, Clone, PartialEq)] pub struct Settings { - pub token: String, + pub theme: Theme, + pub extension: String, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Theme { + SolarizedDark, + InspiredGitHub, + Base16Mocha, + } + + impl Theme { + pub const ALL: &[Self] = + &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; + + fn key(&self) -> &'static str { + match self { + Theme::InspiredGitHub => "InspiredGitHub", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::SolarizedDark => "Solarized (dark)", + } + } + } + + impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::SolarizedDark => write!(f, "Solarized Dark"), + } + } } pub struct Highlight(highlighting::StyleModifier); impl text_editor::Highlight for Highlight { - fn format(&self, _theme: &Theme) -> highlighter::Format<Font> { + fn format(&self, _theme: &iced::Theme) -> highlighter::Format<Font> { highlighter::Format { color: self.0.foreground.map(|color| { Color::from_rgba8( @@ -92,8 +139,8 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, syntax: SyntaxReference, - caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, theme: highlighting::Theme, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, } @@ -110,26 +157,42 @@ mod highlighter { let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); let syntax = syntaxes - .find_syntax_by_token(&settings.token) + .find_syntax_by_token(&settings.extension) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - let parser = parsing::ParseState::new(syntax); - let stack = parsing::ScopeStack::new(); - let theme = highlighting::ThemeSet::load_defaults() .themes - .remove("base16-mocha.dark") + .remove(settings.theme.key()) .unwrap(); + let parser = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + Highlighter { syntax: syntax.clone(), syntaxes, - caches: vec![(parser, stack)], theme, + caches: vec![(parser, stack)], current_line: 0, } } + fn update(&mut self, new_settings: &Self::Settings) { + self.syntax = self + .syntaxes + .find_syntax_by_token(&new_settings.extension) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) + .clone(); + + self.theme = highlighting::ThemeSet::load_defaults() + .themes + .remove(new_settings.theme.key()) + .unwrap(); + + // Restart the highlighter + self.change_line(0); + } + fn change_line(&mut self, line: usize) { let snapshot = line / LINES_PER_SNAPSHOT; diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index c30e185f..0cde2c98 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -193,11 +193,12 @@ where } } -struct State<Highlighter> { +struct State<Highlighter: text::Highlighter> { is_focused: bool, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, highlighter: RefCell<Highlighter>, + highlighter_settings: Highlighter::Settings, } impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer> @@ -220,6 +221,7 @@ where highlighter: RefCell::new(Highlighter::new( &self.highlighter_settings, )), + highlighter_settings: self.highlighter_settings.clone(), }) } @@ -240,6 +242,15 @@ where let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_mut::<State<Highlighter>>(); + if state.highlighter_settings != self.highlighter_settings { + state + .highlighter + .borrow_mut() + .update(&self.highlighter_settings); + + state.highlighter_settings = self.highlighter_settings.clone(); + } + internal.editor.update( limits.pad(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), -- cgit From e7326f0af6f16cf2ff04fbac93bf296a044923f4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 19:07:41 +0200 Subject: Flesh out the `editor` example a bit more --- core/src/renderer/null.rs | 4 + core/src/text/editor.rs | 8 ++ examples/editor/Cargo.toml | 8 +- examples/editor/fonts/icons.ttf | Bin 0 -> 6352 bytes examples/editor/src/main.rs | 287 ++++++++++++++++++++++++++++++++++++---- graphics/src/text/editor.rs | 8 +- src/settings.rs | 8 ++ widget/src/text_editor.rs | 4 + winit/src/application.rs | 9 +- winit/src/settings.rs | 4 + 10 files changed, 312 insertions(+), 28 deletions(-) create mode 100644 examples/editor/fonts/icons.ttf diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 21597c8e..da0f32de 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -125,6 +125,10 @@ impl text::Editor for () { text::editor::Cursor::Caret(Point::ORIGIN) } + fn cursor_position(&self) -> (usize, usize) { + (0, 0) + } + fn selection(&self) -> Option<String> { None } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 2144715f..13bafc3d 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -12,6 +12,8 @@ pub trait Editor: Sized + Default { fn cursor(&self) -> Cursor; + fn cursor_position(&self) -> (usize, usize); + fn selection(&self) -> Option<String>; fn line(&self, index: usize) -> Option<&str>; @@ -52,6 +54,12 @@ pub enum Action { Drag(Point), } +impl Action { + pub fn is_edit(&self) -> bool { + matches!(self, Self::Edit(_)) + } +} + #[derive(Debug, Clone, PartialEq)] pub enum Edit { Insert(char), diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index 930ee592..eeb34aa1 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,6 +7,10 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "debug"] +iced.features = ["advanced", "tokio", "debug"] -syntect = "5.1" \ No newline at end of file +tokio.workspace = true +tokio.features = ["fs"] + +syntect = "5.1" +rfd = "0.12" diff --git a/examples/editor/fonts/icons.ttf b/examples/editor/fonts/icons.ttf new file mode 100644 index 00000000..393c6922 Binary files /dev/null and b/examples/editor/fonts/icons.ttf differ diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index fa35ba0f..09c4b9b5 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,70 +1,218 @@ -use iced::widget::{column, horizontal_space, pick_list, row, text_editor}; -use iced::{Element, Font, Length, Sandbox, Settings, Theme}; +use iced::executor; +use iced::theme::{self, Theme}; +use iced::widget::{ + button, column, container, horizontal_space, pick_list, row, text, + text_editor, tooltip, +}; +use iced::{Application, Command, Element, Font, Length, Settings}; use highlighter::Highlighter; +use std::ffi; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + pub fn main() -> iced::Result { - Editor::run(Settings::default()) + Editor::run(Settings { + fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], + default_font: Font { + monospaced: true, + ..Font::with_name("Hasklug Nerd Font Mono") + }, + ..Settings::default() + }) } struct Editor { + file: Option<PathBuf>, content: text_editor::Content, theme: highlighter::Theme, + is_loading: bool, + is_dirty: bool, } #[derive(Debug, Clone)] enum Message { Edit(text_editor::Action), ThemeSelected(highlighter::Theme), + NewFile, + OpenFile, + FileOpened(Result<(PathBuf, Arc<String>), Error>), + SaveFile, + FileSaved(Result<PathBuf, Error>), } -impl Sandbox for Editor { +impl Application for Editor { type Message = Message; - - fn new() -> Self { - Self { - content: text_editor::Content::with(include_str!("main.rs")), - theme: highlighter::Theme::SolarizedDark, - } + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + ( + Self { + file: None, + content: text_editor::Content::new(), + theme: highlighter::Theme::SolarizedDark, + is_loading: true, + is_dirty: false, + }, + Command::perform(load_file(default_file()), Message::FileOpened), + ) } fn title(&self) -> String { String::from("Editor - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command<Message> { match message { Message::Edit(action) => { + self.is_dirty = self.is_dirty || action.is_edit(); + self.content.edit(action); + + Command::none() } Message::ThemeSelected(theme) => { self.theme = theme; + + Command::none() + } + Message::NewFile => { + if !self.is_loading { + self.file = None; + self.content = text_editor::Content::new(); + } + + Command::none() + } + Message::OpenFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + Command::perform(open_file(), Message::FileOpened) + } + } + Message::FileOpened(result) => { + self.is_loading = false; + self.is_dirty = false; + + if let Ok((path, contents)) = result { + self.file = Some(path); + self.content = text_editor::Content::with(&contents); + } + + Command::none() + } + Message::SaveFile => { + if self.is_loading { + Command::none() + } else { + self.is_loading = true; + + let mut contents = self.content.lines().enumerate().fold( + String::new(), + |mut contents, (i, line)| { + if i > 0 { + contents.push_str("\n"); + } + + contents.push_str(&line); + + contents + }, + ); + + if !contents.ends_with("\n") { + contents.push_str("\n"); + } + + Command::perform( + save_file(self.file.clone(), contents), + Message::FileSaved, + ) + } + } + Message::FileSaved(result) => { + self.is_loading = false; + + if let Ok(path) = result { + self.file = Some(path); + self.is_dirty = false; + } + + Command::none() } } } fn view(&self) -> Element<Message> { + let controls = row![ + action(new_icon(), "New file", Some(Message::NewFile)), + action( + open_icon(), + "Open file", + (!self.is_loading).then_some(Message::OpenFile) + ), + action( + save_icon(), + "Save file", + self.is_dirty.then_some(Message::SaveFile) + ), + horizontal_space(Length::Fill), + pick_list( + highlighter::Theme::ALL, + Some(self.theme), + Message::ThemeSelected + ) + .text_size(14) + .padding([5, 10]) + ] + .spacing(10); + + let status = row![ + text(if let Some(path) = &self.file { + let path = path.display().to_string(); + + if path.len() > 60 { + format!("...{}", &path[path.len() - 40..]) + } else { + path + } + } else { + String::from("New file") + }), + horizontal_space(Length::Fill), + text({ + let (line, column) = self.content.cursor_position(); + + format!("{}:{}", line + 1, column + 1) + }) + ] + .spacing(10); + column![ - row![ - horizontal_space(Length::Fill), - pick_list( - highlighter::Theme::ALL, - Some(self.theme), - Message::ThemeSelected - ) - .padding([5, 10]) - ] - .spacing(10), + controls, text_editor(&self.content) .on_edit(Message::Edit) - .font(Font::with_name("Hasklug Nerd Font Mono")) .highlight::<Highlighter>(highlighter::Settings { theme: self.theme, - extension: String::from("rs"), + extension: self + .file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .map(str::to_string) + .unwrap_or(String::from("rs")), }), + status, ] .spacing(10) - .padding(20) + .padding(10) .into() } @@ -73,6 +221,97 @@ impl Sandbox for Editor { } } +#[derive(Debug, Clone)] +pub enum Error { + DialogClosed, + IoError(io::ErrorKind), +} + +fn default_file() -> PathBuf { + PathBuf::from(format!("{}/src/main.rs", env!("CARGO_MANIFEST_DIR"))) +} + +async fn open_file() -> Result<(PathBuf, Arc<String>), Error> { + let picked_file = rfd::AsyncFileDialog::new() + .set_title("Open a text file...") + .pick_file() + .await + .ok_or(Error::DialogClosed)?; + + load_file(picked_file.path().to_owned()).await +} + +async fn load_file(path: PathBuf) -> Result<(PathBuf, Arc<String>), Error> { + let contents = tokio::fs::read_to_string(&path) + .await + .map(Arc::new) + .map_err(|error| Error::IoError(error.kind()))?; + + Ok((path, contents)) +} + +async fn save_file( + path: Option<PathBuf>, + contents: String, +) -> Result<PathBuf, Error> { + let path = if let Some(path) = path { + path + } else { + rfd::AsyncFileDialog::new() + .save_file() + .await + .as_ref() + .map(rfd::FileHandle::path) + .map(Path::to_owned) + .ok_or(Error::DialogClosed)? + }; + + let _ = tokio::fs::write(&path, contents) + .await + .map_err(|error| Error::IoError(error.kind()))?; + + Ok(path) +} + +fn action<'a, Message: Clone + 'a>( + content: impl Into<Element<'a, Message>>, + label: &'a str, + on_press: Option<Message>, +) -> Element<'a, Message> { + let action = + button(container(content).width(Length::Fill).center_x()).width(40); + + if let Some(on_press) = on_press { + tooltip( + action.on_press(on_press), + label, + tooltip::Position::FollowCursor, + ) + .style(theme::Container::Box) + .into() + } else { + action.style(theme::Button::Secondary).into() + } +} + +fn new_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e800}') +} + +fn save_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0e801}') +} + +fn open_icon<'a, Message>() -> Element<'a, Message> { + icon('\u{0f115}') +} + +fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { + const ICON_FONT: Font = Font::with_name("editor-icons"); + + text(codepoint).font(ICON_FONT).into() +} + mod highlighter { use iced::advanced::text::highlighter; use iced::widget::text_editor; diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4673fce3..dfb91f34 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -221,6 +221,12 @@ impl editor::Editor for Editor { } } + fn cursor_position(&self) -> (usize, usize) { + let cursor = self.internal().editor.cursor(); + + (cursor.line, cursor.index) + } + fn perform(&mut self, action: Action) { let mut font_system = text::font_system().write().expect("Write font system"); @@ -559,7 +565,7 @@ impl editor::Editor for Editor { Some(i) } }) - .unwrap_or(buffer.lines.len()); + .unwrap_or(buffer.lines.len().saturating_sub(1)); let current_line = highlighter.current_line(); diff --git a/src/settings.rs b/src/settings.rs index d9778d7e..6b9ce095 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,6 +2,8 @@ use crate::window; use crate::{Font, Pixels}; +use std::borrow::Cow; + /// The settings of an application. #[derive(Debug, Clone)] pub struct Settings<Flags> { @@ -21,6 +23,9 @@ pub struct Settings<Flags> { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + pub fonts: Vec<Cow<'static, [u8]>>, + /// The default [`Font`] to be used. /// /// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif). @@ -62,6 +67,7 @@ impl<Flags> Settings<Flags> { flags, id: default_settings.id, window: default_settings.window, + fonts: default_settings.fonts, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, @@ -79,6 +85,7 @@ where id: None, window: Default::default(), flags: Default::default(), + fonts: Default::default(), default_font: Default::default(), default_text_size: Pixels(16.0), antialiasing: false, @@ -93,6 +100,7 @@ impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { id: settings.id, window: settings.window.into(), flags: settings.flags, + fonts: settings.fonts, exit_on_close_request: settings.exit_on_close_request, } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 0cde2c98..970ec031 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -182,6 +182,10 @@ where pub fn selection(&self) -> Option<String> { self.0.borrow().editor.selection() } + + pub fn cursor_position(&self) -> (usize, usize) { + self.0.borrow().editor.cursor_position() + } } impl<Renderer> Default for Content<Renderer> diff --git a/winit/src/application.rs b/winit/src/application.rs index d1689452..e80e9783 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -193,7 +193,14 @@ where }; } - let (compositor, renderer) = C::new(compositor_settings, Some(&window))?; + let (compositor, mut renderer) = + C::new(compositor_settings, Some(&window))?; + + for font in settings.fonts { + use crate::core::text::Renderer; + + renderer.load_font(font); + } let (mut event_sender, event_receiver) = mpsc::unbounded(); let (control_sender, mut control_receiver) = mpsc::unbounded(); diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 8d3e1b47..b4a1dd61 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -33,6 +33,7 @@ use crate::Position; use winit::monitor::MonitorHandle; use winit::window::WindowBuilder; +use std::borrow::Cow; use std::fmt; /// The settings of an application. @@ -52,6 +53,9 @@ pub struct Settings<Flags> { /// [`Application`]: crate::Application pub flags: Flags, + /// The fonts to load on boot. + 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). /// -- cgit From 161a971d065b3254a2f11cb374d2c94c2d67646b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 19:08:57 +0200 Subject: Fix `clippy` lints --- examples/editor/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 09c4b9b5..785dfb3b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -118,7 +118,7 @@ impl Application for Editor { String::new(), |mut contents, (i, line)| { if i > 0 { - contents.push_str("\n"); + contents.push('\n'); } contents.push_str(&line); @@ -127,8 +127,8 @@ impl Application for Editor { }, ); - if !contents.ends_with("\n") { - contents.push_str("\n"); + if !contents.ends_with('\n') { + contents.push('\n'); } Command::perform( @@ -266,7 +266,7 @@ async fn save_file( .ok_or(Error::DialogClosed)? }; - let _ = tokio::fs::write(&path, contents) + tokio::fs::write(&path, contents) .await .map_err(|error| Error::IoError(error.kind()))?; -- cgit From 8eec0033dee816bfcc102fc4f511c8bfe08c14ee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 19:24:09 +0200 Subject: Remove unnecessary `monospaced` flag in `Font` --- core/src/font.rs | 4 ---- examples/editor/src/main.rs | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/core/src/font.rs b/core/src/font.rs index 7f647847..2b68decf 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -12,8 +12,6 @@ pub struct Font { pub stretch: Stretch, /// The [`Style`] of the [`Font`]. pub style: Style, - /// Whether if the [`Font`] is monospaced or not. - pub monospaced: bool, } impl Font { @@ -23,13 +21,11 @@ impl Font { weight: Weight::Normal, stretch: Stretch::Normal, style: Style::Normal, - monospaced: false, }; /// A monospaced font with normal [`Weight`]. pub const MONOSPACE: Font = Font { family: Family::Monospace, - monospaced: true, ..Self::DEFAULT }; diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 785dfb3b..5018b3cb 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -16,10 +16,7 @@ use std::sync::Arc; pub fn main() -> iced::Result { Editor::run(Settings { fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font { - monospaced: true, - ..Font::with_name("Hasklug Nerd Font Mono") - }, + default_font: Font::with_name("Hasklug Nerd Font Mono"), ..Settings::default() }) } -- cgit From d1d0b3aaee84003278b9db3e86687e776f20b346 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 20:14:38 +0200 Subject: Use `Font::MONOSPACE` in `editor` example --- Cargo.toml | 2 +- examples/editor/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac59085d..e887afc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" -branch = "editor-fixes" +branch = "respect-fontconfig-aliases" [patch.crates-io.rangemap] git = "https://github.com/hecrj/rangemap.git" diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 5018b3cb..277eb3e9 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -16,7 +16,7 @@ use std::sync::Arc; pub fn main() -> iced::Result { Editor::run(Settings { fonts: vec![include_bytes!("../fonts/icons.ttf").as_slice().into()], - default_font: Font::with_name("Hasklug Nerd Font Mono"), + default_font: Font::MONOSPACE, ..Settings::default() }) } -- cgit From 36e867de693d4e9fc64da3d9d7745a5b1398d8a5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 18 Sep 2023 20:59:39 +0200 Subject: Fix `lint` and `test` GitHub CI workflows --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fd98374..af34bb13 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,7 @@ name: Lint on: [push, pull_request] jobs: all: - runs-on: ubuntu-latest + runs-on: macOS-latest steps: - uses: hecrj/setup-rust-action@v1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac8d27f9..215b616b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: run: | export DEBIAN_FRONTED=noninteractive sudo apt-get -qq update - sudo apt-get install -y libxkbcommon-dev + sudo apt-get install -y libxkbcommon-dev libgtk-3-dev - name: Run tests run: | cargo test --verbose --workspace -- cgit From 4e757a26d0c1c58001f31cf0592131cd5ad886ad Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 01:18:06 +0200 Subject: Implement `Scroll` action in `text::editor` --- core/src/text/editor.rs | 1 + graphics/src/text/editor.rs | 6 ++++++ widget/src/text_editor.rs | 12 ++++++++++++ 3 files changed, 19 insertions(+) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 13bafc3d..e9d66ce9 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -52,6 +52,7 @@ pub enum Action { Edit(Edit), Click(Point), Drag(Point), + Scroll { lines: i32 }, } impl Action { diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index dfb91f34..a05312dc 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -446,6 +446,12 @@ impl editor::Editor for Editor { } } } + Action::Scroll { lines } => { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); + } } self.0 = Some(Arc::new(internal)); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 970ec031..ad12a076 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -521,6 +521,18 @@ impl Update { } _ => None, }, + mouse::Event::WheelScrolled { delta } => { + action(Action::Scroll { + lines: match delta { + mouse::ScrollDelta::Lines { y, .. } => { + -y as i32 * 4 + } + mouse::ScrollDelta::Pixels { y, .. } => { + -y.signum() as i32 + } + }, + }) + } _ => None, }, Event::Keyboard(event) => match event { -- cgit From 06dc12bfbf75958c6534306b3d1b57ae47bdb37a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 19:35:28 +0200 Subject: Simplify `editor` example --- examples/editor/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 277eb3e9..6def2082 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -4,7 +4,7 @@ use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Application, Command, Element, Font, Length, Settings}; +use iced::{Alignment, Application, Command, Element, Font, Length, Settings}; use highlighter::Highlighter; @@ -169,7 +169,8 @@ impl Application for Editor { .text_size(14) .padding([5, 10]) ] - .spacing(10); + .spacing(10) + .align_items(Alignment::Center); let status = row![ text(if let Some(path) = &self.file { @@ -275,8 +276,7 @@ fn action<'a, Message: Clone + 'a>( label: &'a str, on_press: Option<Message>, ) -> Element<'a, Message> { - let action = - button(container(content).width(Length::Fill).center_x()).width(40); + let action = button(container(content).width(30).center_x()); if let Some(on_press) = on_press { tooltip( @@ -316,7 +316,7 @@ mod highlighter { use std::ops::Range; use syntect::highlighting; - use syntect::parsing::{self, SyntaxReference}; + use syntect::parsing; #[derive(Debug, Clone, PartialEq)] pub struct Settings { @@ -374,7 +374,7 @@ mod highlighter { pub struct Highlighter { syntaxes: parsing::SyntaxSet, - syntax: SyntaxReference, + syntax: parsing::SyntaxReference, theme: highlighting::Theme, caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, -- cgit From c0a141ab026f5686d6bd92c8807b174396cb9105 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 19:39:23 +0200 Subject: Save file on `Cmd+S` in `editor` example --- examples/editor/src/main.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 6def2082..36d4287c 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,10 +1,14 @@ use iced::executor; +use iced::keyboard; use iced::theme::{self, Theme}; use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; -use iced::{Alignment, Application, Command, Element, Font, Length, Settings}; +use iced::{ + Alignment, Application, Command, Element, Font, Length, Settings, + Subscription, +}; use highlighter::Highlighter; @@ -147,6 +151,15 @@ impl Application for Editor { } } + fn subscription(&self) -> Subscription<Message> { + keyboard::on_key_press(|key_code, modifiers| match key_code { + keyboard::KeyCode::S if modifiers.command() => { + Some(Message::SaveFile) + } + _ => None, + }) + } + fn view(&self) -> Element<Message> { let controls = row![ action(new_icon(), "New file", Some(Message::NewFile)), -- cgit From f806d001e6fb44b5a45029ca257261e6e0d4d4b2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 20:48:50 +0200 Subject: Introduce new `iced_highlighter` subcrate --- Cargo.toml | 10 +- core/src/text/highlighter.rs | 11 +- examples/editor/Cargo.toml | 2 +- examples/editor/src/main.rs | 251 +++---------------------------------------- highlighter/Cargo.toml | 16 +++ highlighter/src/lib.rs | 225 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + style/src/text_editor.rs | 16 +-- widget/src/text_editor.rs | 28 ++++- 9 files changed, 302 insertions(+), 260 deletions(-) create mode 100644 highlighter/Cargo.toml create mode 100644 highlighter/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e887afc0..8899fa67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ system = ["iced_winit/system"] web-colors = ["iced_renderer/web-colors"] # Enables the WebGL backend, replacing WebGPU webgl = ["iced_renderer/webgl"] +# Enables the syntax `highlighter` module +highlighter = ["iced_highlighter"] # Enables the advanced module advanced = [] @@ -58,6 +60,9 @@ iced_widget.workspace = true iced_winit.features = ["application"] iced_winit.workspace = true +iced_highlighter.workspace = true +iced_highlighter.optional = true + thiserror.workspace = true image.workspace = true @@ -78,8 +83,9 @@ members = [ "core", "futures", "graphics", - "runtime", + "highlighter", "renderer", + "runtime", "style", "tiny_skia", "wgpu", @@ -103,6 +109,7 @@ iced = { version = "0.12", path = "." } iced_core = { version = "0.12", path = "core" } iced_futures = { version = "0.12", path = "futures" } iced_graphics = { version = "0.12", path = "graphics" } +iced_highlighter = { version = "0.12", path = "highlighter" } iced_renderer = { version = "0.12", path = "renderer" } iced_runtime = { version = "0.12", path = "runtime" } iced_style = { version = "0.12", path = "style" } @@ -137,6 +144,7 @@ resvg = "0.35" rustc-hash = "1.0" smol = "1.0" softbuffer = "0.2" +syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" tiny-skia = "0.10" diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index b462d083..9a9cff89 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -52,8 +52,17 @@ impl Highlighter for PlainText { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Format<Font> { pub color: Option<Color>, pub font: Option<Font>, } + +impl<Font> Default for Format<Font> { + fn default() -> Self { + Self { + color: None, + font: None, + } + } +} diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index eeb34aa1..a77b1e9f 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["advanced", "tokio", "debug"] +iced.features = ["highlighter", "tokio", "debug"] tokio.workspace = true tokio.features = ["fs"] diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 36d4287c..d513090f 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,4 +1,5 @@ use iced::executor; +use iced::highlighter::{self, Highlighter}; use iced::keyboard; use iced::theme::{self, Theme}; use iced::widget::{ @@ -10,8 +11,6 @@ use iced::{ Subscription, }; -use highlighter::Highlighter; - use std::ffi; use std::io; use std::path::{Path, PathBuf}; @@ -210,16 +209,19 @@ impl Application for Editor { controls, text_editor(&self.content) .on_edit(Message::Edit) - .highlight::<Highlighter>(highlighter::Settings { - theme: self.theme, - extension: self - .file - .as_deref() - .and_then(Path::extension) - .and_then(ffi::OsStr::to_str) - .map(str::to_string) - .unwrap_or(String::from("rs")), - }), + .highlight::<Highlighter>( + highlighter::Settings { + theme: self.theme, + extension: self + .file + .as_deref() + .and_then(Path::extension) + .and_then(ffi::OsStr::to_str) + .map(str::to_string) + .unwrap_or(String::from("rs")), + }, + |highlight, _theme| highlight.to_format() + ), status, ] .spacing(10) @@ -321,228 +323,3 @@ fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { text(codepoint).font(ICON_FONT).into() } - -mod highlighter { - use iced::advanced::text::highlighter; - use iced::widget::text_editor; - use iced::{Color, Font}; - - use std::ops::Range; - use syntect::highlighting; - use syntect::parsing; - - #[derive(Debug, Clone, PartialEq)] - pub struct Settings { - pub theme: Theme, - pub extension: String, - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Theme { - SolarizedDark, - InspiredGitHub, - Base16Mocha, - } - - impl Theme { - pub const ALL: &[Self] = - &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; - - fn key(&self) -> &'static str { - match self { - Theme::InspiredGitHub => "InspiredGitHub", - Theme::Base16Mocha => "base16-mocha.dark", - Theme::SolarizedDark => "Solarized (dark)", - } - } - } - - impl std::fmt::Display for Theme { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Theme::InspiredGitHub => write!(f, "Inspired GitHub"), - Theme::Base16Mocha => write!(f, "Mocha"), - Theme::SolarizedDark => write!(f, "Solarized Dark"), - } - } - } - - pub struct Highlight(highlighting::StyleModifier); - - impl text_editor::Highlight for Highlight { - fn format(&self, _theme: &iced::Theme) -> highlighter::Format<Font> { - highlighter::Format { - color: self.0.foreground.map(|color| { - Color::from_rgba8( - color.r, - color.g, - color.b, - color.a as f32 / 255.0, - ) - }), - font: None, - } - } - } - - pub struct Highlighter { - syntaxes: parsing::SyntaxSet, - syntax: parsing::SyntaxReference, - theme: highlighting::Theme, - caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, - current_line: usize, - } - - const LINES_PER_SNAPSHOT: usize = 50; - - impl highlighter::Highlighter for Highlighter { - type Settings = Settings; - type Highlight = Highlight; - - type Iterator<'a> = - Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>; - - fn new(settings: &Self::Settings) -> Self { - let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); - - let syntax = syntaxes - .find_syntax_by_token(&settings.extension) - .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); - - let theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(settings.theme.key()) - .unwrap(); - - let parser = parsing::ParseState::new(syntax); - let stack = parsing::ScopeStack::new(); - - Highlighter { - syntax: syntax.clone(), - syntaxes, - theme, - caches: vec![(parser, stack)], - current_line: 0, - } - } - - fn update(&mut self, new_settings: &Self::Settings) { - self.syntax = self - .syntaxes - .find_syntax_by_token(&new_settings.extension) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) - .clone(); - - self.theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(new_settings.theme.key()) - .unwrap(); - - // Restart the highlighter - self.change_line(0); - } - - fn change_line(&mut self, line: usize) { - let snapshot = line / LINES_PER_SNAPSHOT; - - if snapshot <= self.caches.len() { - self.caches.truncate(snapshot); - self.current_line = snapshot * LINES_PER_SNAPSHOT; - } else { - self.caches.truncate(1); - self.current_line = 0; - } - - let (parser, stack) = - self.caches.last().cloned().unwrap_or_else(|| { - ( - parsing::ParseState::new(&self.syntax), - parsing::ScopeStack::new(), - ) - }); - - self.caches.push((parser, stack)); - } - - fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { - if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { - let (parser, stack) = - self.caches.last().expect("Caches must not be empty"); - - self.caches.push((parser.clone(), stack.clone())); - } - - self.current_line += 1; - - let (parser, stack) = - self.caches.last_mut().expect("Caches must not be empty"); - - let ops = - parser.parse_line(line, &self.syntaxes).unwrap_or_default(); - - let highlighter = highlighting::Highlighter::new(&self.theme); - - Box::new( - ScopeRangeIterator { - ops, - line_length: line.len(), - index: 0, - last_str_index: 0, - } - .filter_map(move |(range, scope)| { - let _ = stack.apply(&scope); - - if range.is_empty() { - None - } else { - Some(( - range, - Highlight( - highlighter.style_mod_for_stack(&stack.scopes), - ), - )) - } - }), - ) - } - - fn current_line(&self) -> usize { - self.current_line - } - } - - pub struct ScopeRangeIterator { - ops: Vec<(usize, parsing::ScopeStackOp)>, - line_length: usize, - index: usize, - last_str_index: usize, - } - - impl Iterator for ScopeRangeIterator { - type Item = (std::ops::Range<usize>, parsing::ScopeStackOp); - - fn next(&mut self) -> Option<Self::Item> { - if self.index > self.ops.len() { - return None; - } - - let next_str_i = if self.index == self.ops.len() { - self.line_length - } else { - self.ops[self.index].0 - }; - - let range = self.last_str_index..next_str_i; - self.last_str_index = next_str_i; - - let op = if self.index == 0 { - parsing::ScopeStackOp::Noop - } else { - self.ops[self.index - 1].1.clone() - }; - - self.index += 1; - Some((range, op)) - } - } -} diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml new file mode 100644 index 00000000..311d2998 --- /dev/null +++ b/highlighter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "iced_highlighter" +description = "A syntax higlighter for iced" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true + +[dependencies] +iced_core.workspace = true + +syntect.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs new file mode 100644 index 00000000..79cfafcd --- /dev/null +++ b/highlighter/src/lib.rs @@ -0,0 +1,225 @@ +use iced_core as core; + +use crate::core::text::highlighter::{self, Format}; +use crate::core::{Color, Font}; + +use std::ops::Range; +use syntect::highlighting; +use syntect::parsing; + +pub struct Highlighter { + syntaxes: parsing::SyntaxSet, + syntax: parsing::SyntaxReference, + theme: highlighting::Theme, + caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, + current_line: usize, +} + +const LINES_PER_SNAPSHOT: usize = 50; + +impl highlighter::Highlighter for Highlighter { + type Settings = Settings; + type Highlight = Highlight; + + type Iterator<'a> = + Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>; + + fn new(settings: &Self::Settings) -> Self { + let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); + + let syntax = syntaxes + .find_syntax_by_token(&settings.extension) + .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + + let theme = highlighting::ThemeSet::load_defaults() + .themes + .remove(settings.theme.key()) + .unwrap(); + + let parser = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + + Highlighter { + syntax: syntax.clone(), + syntaxes, + theme, + caches: vec![(parser, stack)], + current_line: 0, + } + } + + fn update(&mut self, new_settings: &Self::Settings) { + self.syntax = self + .syntaxes + .find_syntax_by_token(&new_settings.extension) + .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) + .clone(); + + self.theme = highlighting::ThemeSet::load_defaults() + .themes + .remove(new_settings.theme.key()) + .unwrap(); + + // Restart the highlighter + self.change_line(0); + } + + fn change_line(&mut self, line: usize) { + let snapshot = line / LINES_PER_SNAPSHOT; + + if snapshot <= self.caches.len() { + self.caches.truncate(snapshot); + self.current_line = snapshot * LINES_PER_SNAPSHOT; + } else { + self.caches.truncate(1); + self.current_line = 0; + } + + let (parser, stack) = + self.caches.last().cloned().unwrap_or_else(|| { + ( + parsing::ParseState::new(&self.syntax), + parsing::ScopeStack::new(), + ) + }); + + self.caches.push((parser, stack)); + } + + fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { + if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { + let (parser, stack) = + self.caches.last().expect("Caches must not be empty"); + + self.caches.push((parser.clone(), stack.clone())); + } + + self.current_line += 1; + + let (parser, stack) = + self.caches.last_mut().expect("Caches must not be empty"); + + let ops = parser.parse_line(line, &self.syntaxes).unwrap_or_default(); + + let highlighter = highlighting::Highlighter::new(&self.theme); + + Box::new( + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let _ = stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight( + highlighter.style_mod_for_stack(&stack.scopes), + ), + )) + } + }), + ) + } + + fn current_line(&self) -> usize { + self.current_line + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Settings { + pub theme: Theme, + pub extension: String, +} + +pub struct Highlight(highlighting::StyleModifier); + +impl Highlight { + pub fn color(&self) -> Option<Color> { + self.0.foreground.map(|color| { + Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0) + }) + } + + pub fn font(&self) -> Option<Font> { + None + } + + pub fn to_format(&self) -> Format<Font> { + Format { + color: self.color(), + font: self.font(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { + SolarizedDark, + InspiredGitHub, + Base16Mocha, +} + +impl Theme { + pub const ALL: &[Self] = + &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; + + fn key(&self) -> &'static str { + match self { + Theme::InspiredGitHub => "InspiredGitHub", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::SolarizedDark => "Solarized (dark)", + } + } +} + +impl std::fmt::Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::SolarizedDark => write!(f, "Solarized Dark"), + } + } +} + +pub struct ScopeRangeIterator { + ops: Vec<(usize, parsing::ScopeStackOp)>, + line_length: usize, + index: usize, + last_str_index: usize, +} + +impl Iterator for ScopeRangeIterator { + type Item = (std::ops::Range<usize>, parsing::ScopeStackOp); + + fn next(&mut self) -> Option<Self::Item> { + if self.index > self.ops.len() { + return None; + } + + let next_str_i = if self.index == self.ops.len() { + self.line_length + } else { + self.ops[self.index].0 + }; + + let range = self.last_str_index..next_str_i; + self.last_str_index = next_str_i; + + let op = if self.index == 0 { + parsing::ScopeStackOp::Noop + } else { + self.ops[self.index - 1].1.clone() + }; + + self.index += 1; + Some((range, op)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3cbe716a..e435a041 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,6 +174,9 @@ use iced_winit::runtime; pub use iced_futures::futures; +#[cfg(feature = "highlighter")] +pub use iced_highlighter as highlighter; + mod error; mod sandbox; diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs index f1c31287..f6bae7e6 100644 --- a/style/src/text_editor.rs +++ b/style/src/text_editor.rs @@ -1,6 +1,5 @@ //! Change the appearance of a text editor. -use crate::core::text::highlighter; -use crate::core::{self, Background, BorderRadius, Color}; +use crate::core::{Background, BorderRadius, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] @@ -46,16 +45,3 @@ pub trait StyleSheet { /// Produces the style of a disabled text input. fn disabled(&self, style: &Self::Style) -> Appearance; } - -pub trait Highlight<Font = core::Font, Theme = crate::Theme> { - fn format(&self, theme: &Theme) -> highlighter::Format<Font>; -} - -impl<Font, Theme> Highlight<Font, Theme> for () { - fn format(&self, _theme: &Theme) -> highlighter::Format<Font> { - highlighter::Format { - color: None, - font: None, - } - } -} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ad12a076..c384b8a2 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -16,7 +16,7 @@ use std::cell::RefCell; use std::ops::DerefMut; use std::sync::Arc; -pub use crate::style::text_editor::{Appearance, Highlight, StyleSheet}; +pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> @@ -35,6 +35,10 @@ where style: <Renderer::Theme as StyleSheet>::Style, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, + highlighter_format: fn( + &Highlighter::Highlight, + &Renderer::Theme, + ) -> highlighter::Format<Renderer::Font>, } impl<'a, Message, Renderer> @@ -55,6 +59,9 @@ where style: Default::default(), on_edit: None, highlighter_settings: (), + highlighter_format: |_highlight, _theme| { + highlighter::Format::default() + }, } } } @@ -63,7 +70,6 @@ impl<'a, Highlighter, Message, Renderer> TextEditor<'a, Highlighter, Message, Renderer> where Highlighter: text::Highlighter, - Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { @@ -85,6 +91,10 @@ where pub fn highlight<H: text::Highlighter>( self, settings: H::Settings, + to_format: fn( + &H::Highlight, + &Renderer::Theme, + ) -> highlighter::Format<Renderer::Font>, ) -> TextEditor<'a, H, Message, Renderer> { TextEditor { content: self.content, @@ -97,6 +107,7 @@ where style: self.style, on_edit: self.on_edit, highlighter_settings: settings, + highlighter_format: to_format, } } } @@ -203,13 +214,13 @@ struct State<Highlighter: text::Highlighter> { drag_click: Option<mouse::click::Kind>, highlighter: RefCell<Highlighter>, highlighter_settings: Highlighter::Settings, + highlighter_format_address: usize, } impl<'a, Highlighter, Message, Renderer> Widget<Message, Renderer> for TextEditor<'a, Highlighter, Message, Renderer> where Highlighter: text::Highlighter, - Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>, Renderer: text::Renderer, Renderer::Theme: StyleSheet, { @@ -226,6 +237,7 @@ where &self.highlighter_settings, )), highlighter_settings: self.highlighter_settings.clone(), + highlighter_format_address: self.highlighter_format as usize, }) } @@ -246,6 +258,13 @@ where let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_mut::<State<Highlighter>>(); + if state.highlighter_format_address != self.highlighter_format as usize + { + state.highlighter.borrow_mut().change_line(0); + + state.highlighter_format_address = self.highlighter_format as usize; + } + if state.highlighter_settings != self.highlighter_settings { state .highlighter @@ -354,7 +373,7 @@ where internal.editor.highlight( self.font.unwrap_or_else(|| renderer.default_font()), state.highlighter.borrow_mut().deref_mut(), - |highlight| highlight.format(theme), + |highlight| (self.highlighter_format)(highlight, theme), ); let is_disabled = self.on_edit.is_none(); @@ -458,7 +477,6 @@ impl<'a, Highlighter, Message, Renderer> for Element<'a, Message, Renderer> where Highlighter: text::Highlighter, - Highlighter::Highlight: Highlight<Renderer::Font, Renderer::Theme>, Message: 'a, Renderer: text::Renderer, Renderer::Theme: StyleSheet, -- cgit From 77db1699028cf50fb92b9282ffd1f73507fce974 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 20:55:39 +0200 Subject: Fix typo in `higlighter` (why is it so hard to spell?) --- highlighter/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml index 311d2998..488546c0 100644 --- a/highlighter/Cargo.toml +++ b/highlighter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_highlighter" -description = "A syntax higlighter for iced" +description = "A syntax highlighter for iced" version.workspace = true authors.workspace = true edition.workspace = true -- cgit From 01667446549d10fab18f3ca0306f278b0fe22b13 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 20:56:50 +0200 Subject: Add `iced_highlighter` to `document` workflow --- .github/workflows/document.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 230c5cb0..62e28ca3 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -15,6 +15,7 @@ jobs: RUSTDOCFLAGS="--cfg docsrs" \ cargo doc --no-deps --all-features \ -p iced_core \ + -p iced_highlighter \ -p iced_style \ -p iced_futures \ -p iced_runtime \ -- cgit From d9fbecf0d80234d63e7e5711f28fc35ee75fa503 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 20:58:15 +0200 Subject: Remove `syntect` dependency from `editor` example --- examples/editor/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index a77b1e9f..a3f6ea3b 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -12,5 +12,4 @@ iced.features = ["highlighter", "tokio", "debug"] tokio.workspace = true tokio.features = ["fs"] -syntect = "5.1" rfd = "0.12" -- cgit From a9ee8f62fdd0f74976947c21199684829aa8a496 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 21:57:09 +0200 Subject: Reuse syntaxes and themes lazily in `iced_highlighter` --- highlighter/Cargo.toml | 1 + highlighter/src/lib.rs | 51 +++++++++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml index 488546c0..2d108d6f 100644 --- a/highlighter/Cargo.toml +++ b/highlighter/Cargo.toml @@ -13,4 +13,5 @@ keywords.workspace = true [dependencies] iced_core.workspace = true +once_cell.workspace = true syntect.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 79cfafcd..b80d6499 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -3,20 +3,26 @@ use iced_core as core; use crate::core::text::highlighter::{self, Format}; use crate::core::{Color, Font}; +use once_cell::sync::Lazy; use std::ops::Range; use syntect::highlighting; use syntect::parsing; +static SYNTAXES: Lazy<parsing::SyntaxSet> = + Lazy::new(|| parsing::SyntaxSet::load_defaults_nonewlines()); + +static THEMES: Lazy<highlighting::ThemeSet> = + Lazy::new(|| highlighting::ThemeSet::load_defaults()); + +const LINES_PER_SNAPSHOT: usize = 50; + pub struct Highlighter { - syntaxes: parsing::SyntaxSet, - syntax: parsing::SyntaxReference, - theme: highlighting::Theme, + syntax: &'static parsing::SyntaxReference, + highlighter: highlighting::Highlighter<'static>, caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, } -const LINES_PER_SNAPSHOT: usize = 50; - impl highlighter::Highlighter for Highlighter { type Settings = Settings; type Highlight = Highlight; @@ -25,40 +31,33 @@ impl highlighter::Highlighter for Highlighter { Box<dyn Iterator<Item = (Range<usize>, Self::Highlight)> + 'a>; fn new(settings: &Self::Settings) -> Self { - let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); - - let syntax = syntaxes + let syntax = SYNTAXES .find_syntax_by_token(&settings.extension) - .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); - let theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(settings.theme.key()) - .unwrap(); + let highlighter = highlighting::Highlighter::new( + &THEMES.themes[settings.theme.key()], + ); let parser = parsing::ParseState::new(syntax); let stack = parsing::ScopeStack::new(); Highlighter { - syntax: syntax.clone(), - syntaxes, - theme, + syntax, + highlighter, caches: vec![(parser, stack)], current_line: 0, } } fn update(&mut self, new_settings: &Self::Settings) { - self.syntax = self - .syntaxes + self.syntax = SYNTAXES .find_syntax_by_token(&new_settings.extension) - .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) - .clone(); + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); - self.theme = highlighting::ThemeSet::load_defaults() - .themes - .remove(new_settings.theme.key()) - .unwrap(); + self.highlighter = highlighting::Highlighter::new( + &THEMES.themes[new_settings.theme.key()], + ); // Restart the highlighter self.change_line(0); @@ -99,9 +98,9 @@ impl highlighter::Highlighter for Highlighter { let (parser, stack) = self.caches.last_mut().expect("Caches must not be empty"); - let ops = parser.parse_line(line, &self.syntaxes).unwrap_or_default(); + let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default(); - let highlighter = highlighting::Highlighter::new(&self.theme); + let highlighter = &self.highlighter; Box::new( ScopeRangeIterator { -- cgit From 9af0a27e675b71164f32f8d82eb4cde9cdd459f3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 22:28:28 +0200 Subject: Draw colored glyphs in `iced_tiny_skia` --- tiny_skia/src/text.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 96cfbf32..d1b33293 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -188,7 +188,7 @@ fn draw( if let Some((buffer, placement)) = glyph_cache.allocate( physical_glyph.cache_key, - color, + glyph.color_opt.map(from_color).unwrap_or(color), font_system, &mut swash, ) { @@ -213,6 +213,12 @@ fn draw( } } +fn from_color(color: cosmic_text::Color) -> Color { + let [r, g, b, a] = color.as_rgba(); + + Color::from_rgba8(r, g, b, a as f32 / 255.0) +} + #[derive(Debug, Clone, Default)] struct GlyphCache { entries: FxHashMap< -- cgit From be340a8cd822be1ea0fe4c1b1f3a62ca66d705b4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Sep 2023 23:00:20 +0200 Subject: Fix gamma correction for colored glyphs in `iced_wgpu` --- core/src/color.rs | 20 ++++++++++++++++++++ graphics/src/text.rs | 12 +++++++++--- tiny_skia/src/text.rs | 14 +++++++++++++- wgpu/src/text.rs | 13 ++----------- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index 1392f28b..cce8b340 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -89,6 +89,26 @@ impl Color { } } + /// Creates a [`Color`] from its linear RGBA components. + pub fn from_linear_rgba(r: f32, g: f32, b: f32, a: f32) -> Self { + // As described in: + // https://en.wikipedia.org/wiki/SRGB + fn gamma_component(u: f32) -> f32 { + if u < 0.0031308 { + 12.92 * u + } else { + 1.055 * u.powf(1.0 / 2.4) - 0.055 + } + } + + Self { + r: gamma_component(r), + g: gamma_component(g), + b: gamma_component(b), + a, + } + } + /// Converts the [`Color`] into its RGBA8 equivalent. #[must_use] pub fn into_rgba8(self) -> [u8; 4] { diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 5fcfc699..c10eacad 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -8,6 +8,7 @@ pub use paragraph::Paragraph; pub use cosmic_text; +use crate::color; use crate::core::font::{self, Font}; use crate::core::text::Shaping; use crate::core::{Color, Size}; @@ -131,7 +132,12 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } pub fn to_color(color: Color) -> cosmic_text::Color { - let [r, g, b, a] = color.into_rgba8(); - - cosmic_text::Color::rgba(r, g, b, a) + let [r, g, b, a] = color::pack(color).components(); + + cosmic_text::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index d1b33293..70e95d01 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,6 +1,7 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; use crate::graphics::text::editor; use crate::graphics::text::font_system; @@ -216,7 +217,18 @@ fn draw( fn from_color(color: cosmic_text::Color) -> Color { let [r, g, b, a] = color.as_rgba(); - Color::from_rgba8(r, g, b, a as f32 / 255.0) + if color::GAMMA_CORRECTION { + // `cosmic_text::Color` is linear RGB in this case, so we + // need to convert back to sRGB + Color::from_linear_rgba( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) + } else { + Color::from_rgba8(r, g, b, a as f32 / 255.0) + } } #[derive(Debug, Clone, Default)] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 581df0cb..f746be63 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ use crate::core::alignment; use crate::core::{Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; -use crate::graphics::text::{font_system, Editor, Paragraph}; +use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; use crate::layer::Text; use std::borrow::Cow; @@ -214,16 +214,7 @@ impl Pipeline { right: (clip_bounds.x + clip_bounds.width) as i32, bottom: (clip_bounds.y + clip_bounds.height) as i32, }, - default_color: { - let [r, g, b, a] = color::pack(color).components(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }, + default_color: to_color(color), }) }, ); -- cgit From 93d6f748f69fc4ccf6c18f95c5f16b369c776da0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 20 Sep 2023 01:13:36 +0200 Subject: Fix `clippy` lints in `iced_highlighter` --- highlighter/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index b80d6499..f5a4fae5 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -9,10 +9,10 @@ use syntect::highlighting; use syntect::parsing; static SYNTAXES: Lazy<parsing::SyntaxSet> = - Lazy::new(|| parsing::SyntaxSet::load_defaults_nonewlines()); + Lazy::new(parsing::SyntaxSet::load_defaults_nonewlines); static THEMES: Lazy<highlighting::ThemeSet> = - Lazy::new(|| highlighting::ThemeSet::load_defaults()); + Lazy::new(highlighting::ThemeSet::load_defaults); const LINES_PER_SNAPSHOT: usize = 50; @@ -77,7 +77,7 @@ impl highlighter::Highlighter for Highlighter { let (parser, stack) = self.caches.last().cloned().unwrap_or_else(|| { ( - parsing::ParseState::new(&self.syntax), + parsing::ParseState::new(self.syntax), parsing::ScopeStack::new(), ) }); -- cgit From ff78e97ad7df4db3b2a97b94e99854f2f9e3021a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 20 Sep 2023 01:21:42 +0200 Subject: Introduce more themes to `iced_highlighter` --- examples/editor/src/main.rs | 6 +++++- highlighter/src/lib.rs | 35 ++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index d513090f..f49ca6e8 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -230,7 +230,11 @@ impl Application for Editor { } fn theme(&self) -> Theme { - Theme::Dark + if self.theme.is_dark() { + Theme::Dark + } else { + Theme::Light + } } } diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index f5a4fae5..db28b5b1 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -161,19 +161,38 @@ impl Highlight { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Theme { SolarizedDark, - InspiredGitHub, Base16Mocha, + Base16Ocean, + Base16Eighties, + InspiredGitHub, } impl Theme { - pub const ALL: &[Self] = - &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; + pub const ALL: &[Self] = &[ + Self::SolarizedDark, + Self::Base16Mocha, + Self::Base16Ocean, + Self::Base16Eighties, + Self::InspiredGitHub, + ]; + + pub fn is_dark(self) -> bool { + match self { + Self::SolarizedDark + | Self::Base16Mocha + | Self::Base16Ocean + | Self::Base16Eighties => true, + Self::InspiredGitHub => false, + } + } fn key(&self) -> &'static str { match self { - Theme::InspiredGitHub => "InspiredGitHub", - Theme::Base16Mocha => "base16-mocha.dark", Theme::SolarizedDark => "Solarized (dark)", + Theme::Base16Mocha => "base16-mocha.dark", + Theme::Base16Ocean => "base16-ocean.dark", + Theme::Base16Eighties => "base16-eighties.dark", + Theme::InspiredGitHub => "InspiredGitHub", } } } @@ -181,9 +200,11 @@ impl Theme { impl std::fmt::Display for Theme { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Theme::InspiredGitHub => write!(f, "Inspired GitHub"), - Theme::Base16Mocha => write!(f, "Mocha"), Theme::SolarizedDark => write!(f, "Solarized Dark"), + Theme::Base16Mocha => write!(f, "Mocha"), + Theme::Base16Ocean => write!(f, "Ocean"), + Theme::Base16Eighties => write!(f, "Eighties"), + Theme::InspiredGitHub => write!(f, "Inspired GitHub"), } } } -- cgit From 29fb4eab878a7ba399cae6ab1ec18a71e369ee59 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 20 Sep 2023 01:23:50 +0200 Subject: Scroll `TextEditor` only if `cursor.is_over(bounds)` --- widget/src/text_editor.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index c384b8a2..4191e02c 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -539,7 +539,9 @@ impl Update { } _ => None, }, - mouse::Event::WheelScrolled { delta } => { + mouse::Event::WheelScrolled { delta } + if cursor.is_over(bounds) => + { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { -- cgit From 25d47c3238ce23854e2c78e2bd9ad2b1f4b326b3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 21 Sep 2023 06:05:46 +0200 Subject: Remove `rangemap` patch in `Cargo.toml` --- Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8899fa67..77d4c647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,7 +163,3 @@ winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8 [patch.crates-io.cosmic-text] git = "https://github.com/hecrj/cosmic-text.git" branch = "respect-fontconfig-aliases" - -[patch.crates-io.rangemap] -git = "https://github.com/hecrj/rangemap.git" -branch = "fix/partial-eq" -- cgit From da5dd2526a2d9ee27e9405ed19c0f7a641160c54 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 21 Sep 2023 06:07:19 +0200 Subject: Round `ScrollDelta::Lines` in `TextEditor` --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 4191e02c..ac927fbc 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -545,7 +545,7 @@ impl Update { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { - -y as i32 * 4 + -y.round() as i32 * 4 } mouse::ScrollDelta::Pixels { y, .. } => { -y.signum() as i32 -- cgit From 7373dd856b8837c2d91067b45e43b8f0e767c917 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 21 Sep 2023 06:13:08 +0200 Subject: Scroll at least one line on macOS in `TextEditor` --- widget/src/text_editor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ac927fbc..76f3cc18 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -545,7 +545,11 @@ impl Update { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { - -y.round() as i32 * 4 + if y > 0.0 { + -(y * 4.0).min(1.0) as i32 + } else { + 0 + } } mouse::ScrollDelta::Pixels { y, .. } => { -y.signum() as i32 -- cgit From 68d49459ce0e8b28e56b71970cb26e66ac1b01b4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 21 Sep 2023 06:17:47 +0200 Subject: Fix vertical scroll for `TextEditor` --- widget/src/text_editor.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 76f3cc18..e8187b9c 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -545,8 +545,9 @@ impl Update { action(Action::Scroll { lines: match delta { mouse::ScrollDelta::Lines { y, .. } => { - if y > 0.0 { - -(y * 4.0).min(1.0) as i32 + if y.abs() > 0.0 { + (y.signum() * -(y.abs() * 4.0).max(1.0)) + as i32 } else { 0 } -- cgit From 70e49df4289b925d24f92ce5c91ef2b03dbc54e3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 22 Sep 2023 05:50:31 +0200 Subject: Fix selection clipping out of bounds in `TextEditor` --- widget/src/text_editor.rs | 57 +++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index e8187b9c..c142c22d 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -406,38 +406,47 @@ where style.text_color, ); + let translation = Vector::new( + bounds.x + self.padding.left, + bounds.y + self.padding.top, + ); + if state.is_focused { match internal.editor.cursor() { Cursor::Caret(position) => { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: position.x + bounds.x + self.padding.left, - y: position.y + bounds.y + self.padding.top, - width: 1.0, - height: self - .line_height - .to_absolute(self.text_size.unwrap_or_else( - || renderer.default_size(), - )) - .into(), + let position = position + translation; + + if bounds.contains(position) { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: position.x, + y: position.y, + width: 1.0, + height: self + .line_height + .to_absolute( + self.text_size.unwrap_or_else( + || renderer.default_size(), + ), + ) + .into(), + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - theme.value_color(&self.style), - ); + theme.value_color(&self.style), + ); + } } Cursor::Selection(ranges) => { - for range in ranges { + for range in ranges.into_iter().filter_map(|range| { + bounds.intersection(&(range + translation)) + }) { renderer.fill_quad( renderer::Quad { - bounds: range - + Vector::new( - bounds.x + self.padding.left, - bounds.y + self.padding.top, - ), + bounds: range, border_radius: 0.0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, -- cgit From af21cf82492bf7ffa1241cebae182c5916fc07d1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 22 Sep 2023 05:55:27 +0200 Subject: Remove `patch.crates-io` section for `cosmic-text` in `Cargo.toml` --- Cargo.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 77d4c647..888e2df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,10 +121,10 @@ iced_winit = { version = "0.12", path = "winit" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = "0.9" +cosmic-text = { git = "https://github.com/pop-os/cosmic-text.git", rev = "30398c2f0cb79267d440870bc47967579e31a2ae" } futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "20f0f8fa80e0d0df4c63634ce9176fa489546ca9" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "0a8366be5ec6d48c3e10c996ba840936992d878f" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -159,7 +159,3 @@ wgpu = "0.17" winapi = "0.3" window_clipboard = "0.3" winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } - -[patch.crates-io.cosmic-text] -git = "https://github.com/hecrj/cosmic-text.git" -branch = "respect-fontconfig-aliases" -- cgit From 8cc19de254c37d3123d5ea1b6513f1f34d35c7c8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 22 Sep 2023 06:00:51 +0200 Subject: Add `text` helper method for `text_editor::Content` --- examples/editor/src/main.rs | 19 +------------------ widget/src/text_editor.rs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index f49ca6e8..a69e1f54 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -114,25 +114,8 @@ impl Application for Editor { } else { self.is_loading = true; - let mut contents = self.content.lines().enumerate().fold( - String::new(), - |mut contents, (i, line)| { - if i > 0 { - contents.push('\n'); - } - - contents.push_str(&line); - - contents - }, - ); - - if !contents.ends_with('\n') { - contents.push('\n'); - } - Command::perform( - save_file(self.file.clone(), contents), + save_file(self.file.clone(), self.content.text()), Message::FileSaved, ) } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index c142c22d..6d25967e 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -190,6 +190,27 @@ where } } + pub fn text(&self) -> String { + let mut text = self.lines().enumerate().fold( + String::new(), + |mut contents, (i, line)| { + if i > 0 { + contents.push('\n'); + } + + contents.push_str(&line); + + contents + }, + ); + + if !text.ends_with('\n') { + text.push('\n'); + } + + text + } + pub fn selection(&self) -> Option<String> { self.0.borrow().editor.selection() } -- cgit From e0233ebc3ce4791d094c52eeef81cce78b9bc578 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott <idscott@system76.com> Date: Thu, 3 Aug 2023 10:19:28 -0700 Subject: Fix `Command<T>::perform` to return a `Command<T>` This seems like clearly the correct thing to do here. If the type bound on `Command` isn't specified, it makes no difference, since the generic is inferred in a way that works with either definition. But this is important if `Command<T>` is aliased with a concrete type. --- runtime/src/command.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/src/command.rs b/runtime/src/command.rs index cd4c51ff..b74097bd 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -40,9 +40,9 @@ impl<T> Command<T> { /// Creates a [`Command`] that performs the action of the given future. pub fn perform<A>( - future: impl Future<Output = T> + 'static + MaybeSend, - f: impl FnOnce(T) -> A + 'static + MaybeSend, - ) -> Command<A> { + future: impl Future<Output = A> + 'static + MaybeSend, + f: impl FnOnce(A) -> T + 'static + MaybeSend, + ) -> Command<T> { use iced_futures::futures::FutureExt; Command::single(Action::Future(Box::pin(future.map(f)))) -- cgit From 54e6d2b5fa1fe29e2e3588b51f6cfff36563cefc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 18 Oct 2023 17:49:19 -0500 Subject: Fix lint in `screenshot` example --- examples/screenshot/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index ab0a2ae3..f781a401 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -298,10 +298,7 @@ fn numeric_input( ) -> Element<'_, Option<u32>> { text_input( placeholder, - &value - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(String::new), + &value.as_ref().map(ToString::to_string).unwrap_or_default(), ) .on_input(move |text| { if text.is_empty() { -- cgit From f1b1344d59fa7354615f560bd25ed01ad0c9f865 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 22 Oct 2023 15:08:08 +0200 Subject: Run `cargo update` before `cargo audit` in `audit` workflow --- .github/workflows/audit.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index e9f4b0c5..bfb617fb 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -12,6 +12,8 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - uses: actions/checkout@master + - name: Resolve dependencies + run: cargo update - name: Audit vulnerabilities run: cargo audit -- cgit From 86b877517feb15b2155c6cfef29246a3f281c8ae Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 03:21:40 +0200 Subject: Update `wgpu` to `0.18` and `cosmic-text` to `0.10` --- Cargo.toml | 6 +++--- examples/integration/src/scene.rs | 4 +++- wgpu/src/backend.rs | 8 ++++++-- wgpu/src/color.rs | 4 +++- wgpu/src/triangle.rs | 7 ++++++- wgpu/src/triangle/msaa.rs | 4 +++- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af74a3cf..bb8b4752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,10 +114,10 @@ iced_winit = { version = "0.12", path = "winit" } async-std = "1.0" bitflags = "1.0" bytemuck = { version = "1.0", features = ["derive"] } -cosmic-text = "0.9" +cosmic-text = "0.10" futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "20f0f8fa80e0d0df4c63634ce9176fa489546ca9" } +glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "2caa9fc5e5923c1d827d177c3619cab7e9885b85" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -147,7 +147,7 @@ unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3" -wgpu = "0.17" +wgpu = "0.18" winapi = "0.3" window_clipboard = "0.3" winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 01808f40..e29558bf 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -36,10 +36,12 @@ impl Scene { a: a as f64, } }), - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }) } diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 65c63f19..32b8a189 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -222,10 +222,12 @@ impl Backend { }), None => wgpu::LoadOp::Load, }, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }, )); @@ -271,11 +273,13 @@ impl Backend { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, }, )], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }, )); } diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 20827e3c..4598b0a6 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -143,10 +143,12 @@ pub fn convert( resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }); pass.set_pipeline(&pipeline); diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 644c9f84..69270a73 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -300,10 +300,15 @@ impl Pipeline { wgpu::RenderPassColorAttachment { view: attachment, resolve_target, - ops: wgpu::Operations { load, store: true }, + ops: wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }, }, )], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }); let layer = &mut self.layers[layer]; diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 320b5b12..14abd20b 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -167,10 +167,12 @@ impl Blit { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }); render_pass.set_pipeline(&self.pipeline); -- cgit From a00ebcde3d698bc6b59a7a258e91c3612a6faaaf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 03:24:59 +0200 Subject: Remove unnecessary `into_iter` call in `iced_graphics` --- graphics/src/text.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index bc06aa3c..a7d52645 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -22,12 +22,11 @@ pub struct FontSystem { impl FontSystem { pub fn new() -> Self { FontSystem { - raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts( - [cosmic_text::fontdb::Source::Binary(Arc::new( + raw: RwLock::new(cosmic_text::FontSystem::new_with_fonts([ + cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), - ))] - .into_iter(), - )), + )), + ])), version: Version::default(), } } -- cgit From 625cd745f38215b1cb8f629cdc6d2fa41c9a739a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 05:04:14 +0200 Subject: Write documentation for the new text APIs --- core/src/lib.rs | 2 +- core/src/mouse/click.rs | 1 + core/src/text.rs | 2 ++ core/src/text/editor.rs | 51 +++++++++++++++++++++++++++++++++++++++++- core/src/text/highlighter.rs | 30 ++++++++++++++++++++----- examples/editor/src/main.rs | 10 ++++----- graphics/src/lib.rs | 2 +- graphics/src/text.rs | 13 +++++++++++ graphics/src/text/cache.rs | 19 ++++++++++++++++ graphics/src/text/editor.rs | 12 ++++++++++ graphics/src/text/paragraph.rs | 14 ++++++++++++ style/src/lib.rs | 2 +- wgpu/src/layer/text.rs | 7 +++++- wgpu/src/lib.rs | 2 +- widget/src/lib.rs | 4 ++-- widget/src/text_editor.rs | 36 +++++++++++++++++++++++++---- 16 files changed, 185 insertions(+), 22 deletions(-) diff --git a/core/src/lib.rs b/core/src/lib.rs index 9eb3da34..54ea5839 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,7 +12,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( missing_debug_implementations, - // missing_docs, + missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index b427da6c..6f3844be 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -61,6 +61,7 @@ impl Click { self.kind } + /// Returns the position of the [`Click`]. pub fn position(&self) -> Point { self.position } diff --git a/core/src/text.rs b/core/src/text.rs index 9b9c753c..546d0b5c 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -204,6 +204,8 @@ pub trait Renderer: crate::Renderer { color: Color, ); + /// Draws the given [`Editor`] at the given position and with the given + /// [`Color`]. fn fill_editor( &mut self, editor: &Self::Editor, diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index e9d66ce9..ebb0eee2 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -1,25 +1,36 @@ +//! Edit text. use crate::text::highlighter::{self, Highlighter}; use crate::text::LineHeight; use crate::{Pixels, Point, Rectangle, Size}; use std::sync::Arc; +/// A component that can be used by widgets to edit multi-line text. pub trait Editor: Sized + Default { + /// The [`Font`] of the [`Editor`]. type Font: Copy + PartialEq + Default; /// Creates a new [`Editor`] laid out with the given text. fn with_text(text: &str) -> Self; + /// Returns the current [`Cursor`] of the [`Editor`]. fn cursor(&self) -> Cursor; + /// Returns the current cursor position of the [`Editor`]. + /// + /// Line and column, respectively. fn cursor_position(&self) -> (usize, usize); + /// Returns the current selected text of the [`Editor`]. fn selection(&self) -> Option<String>; + /// Returns the text of the given line in the [`Editor`], if it exists. fn line(&self, index: usize) -> Option<&str>; + /// Returns the amount of lines in the [`Editor`]. fn line_count(&self) -> usize; + /// Performs an [`Action`] on the [`Editor`]. fn perform(&mut self, action: Action); /// Returns the current boundaries of the [`Editor`]. @@ -35,6 +46,7 @@ pub trait Editor: Sized + Default { new_highlighter: &mut impl Highlighter, ); + /// Runs a text [`Highlighter`] in the [`Editor`]. fn highlight<H: Highlighter>( &mut self, font: Self::Font, @@ -43,50 +55,83 @@ pub trait Editor: Sized + Default { ); } +/// An interaction with an [`Editor`]. #[derive(Debug, Clone, PartialEq)] pub enum Action { + /// Apply a [`Motion`]. Move(Motion), + /// Select text with a given [`Motion`]. Select(Motion), + /// Select the word at the current cursor. SelectWord, + /// Select the line at the current cursor. SelectLine, + /// Perform an [`Edit`]. Edit(Edit), + /// Click the [`Editor`] at the given [`Point`]. Click(Point), + /// Drag the mouse on the [`Editor`] to the given [`Point`]. Drag(Point), - Scroll { lines: i32 }, + /// Scroll the [`Editor`] a certain amount of lines. + Scroll { + /// The amount of lines to scroll. + lines: i32, + }, } impl Action { + /// Returns whether the [`Action`] is an editing action. pub fn is_edit(&self) -> bool { matches!(self, Self::Edit(_)) } } +/// An action that edits text. #[derive(Debug, Clone, PartialEq)] pub enum Edit { + /// Insert the given character. Insert(char), + /// Paste the given text. Paste(Arc<String>), + /// Break the current line. Enter, + /// Delete the previous character. Backspace, + /// Delete the next character. Delete, } +/// A cursor movement. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Motion { + /// Move left. Left, + /// Move right. Right, + /// Move up. Up, + /// Move down. Down, + /// Move to the left boundary of a word. WordLeft, + /// Move to the right boundary of a word. WordRight, + /// Move to the start of the line. Home, + /// Move to the end of the line. End, + /// Move to the start of the previous window. PageUp, + /// Move to the start of the next window. PageDown, + /// Move to the start of the text. DocumentStart, + /// Move to the end of the text. DocumentEnd, } impl Motion { + /// Widens the [`Motion`], if possible. pub fn widen(self) -> Self { match self { Self::Left => Self::WordLeft, @@ -97,6 +142,7 @@ impl Motion { } } + /// Returns the [`Direction`] of the [`Motion`]. pub fn direction(&self) -> Direction { match self { Self::Left @@ -115,9 +161,12 @@ impl Motion { } } +/// A direction in some text. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { + /// <- Left, + /// -> Right, } diff --git a/core/src/text/highlighter.rs b/core/src/text/highlighter.rs index 9a9cff89..a0535228 100644 --- a/core/src/text/highlighter.rs +++ b/core/src/text/highlighter.rs @@ -1,31 +1,48 @@ +//! Highlight text. use crate::Color; use std::ops::Range; +/// A type capable of highlighting text. +/// +/// A [`Highlighter`] highlights lines in sequence. When a line changes, +/// it must be notified and the lines after the changed one must be fed +/// again to the [`Highlighter`]. pub trait Highlighter: 'static { + /// The settings to configure the [`Highlighter`]. type Settings: PartialEq + Clone; + + /// The output of the [`Highlighter`]. type Highlight; + /// The highlight iterator type. type Iterator<'a>: Iterator<Item = (Range<usize>, Self::Highlight)> where Self: 'a; + /// Creates a new [`Highlighter`] from its [`Self::Settings`]. fn new(settings: &Self::Settings) -> Self; + /// Updates the [`Highlighter`] with some new [`Self::Settings`]. fn update(&mut self, new_settings: &Self::Settings); + /// Notifies the [`Highlighter`] that the line at the given index has changed. fn change_line(&mut self, line: usize); + /// Highlights the given line. + /// + /// If a line changed prior to this, the first line provided here will be the + /// line that changed. fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_>; + /// Returns the current line of the [`Highlighter`]. + /// + /// If `change_line` has been called, this will normally be the least index + /// that changed. fn current_line(&self) -> usize; } -#[derive(Debug, Clone, Copy)] -pub struct Style { - pub color: Color, -} - +/// A highlighter that highlights nothing. #[derive(Debug, Clone, Copy)] pub struct PlainText; @@ -52,9 +69,12 @@ impl Highlighter for PlainText { } } +/// The format of some text. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Format<Font> { + /// The [`Color`] of the text. pub color: Option<Color>, + /// The `Font` of the text. pub font: Option<Font>, } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index a69e1f54..03d1e283 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -34,7 +34,7 @@ struct Editor { #[derive(Debug, Clone)] enum Message { - Edit(text_editor::Action), + ActionPerformed(text_editor::Action), ThemeSelected(highlighter::Theme), NewFile, OpenFile, @@ -68,10 +68,10 @@ impl Application for Editor { fn update(&mut self, message: Message) -> Command<Message> { match message { - Message::Edit(action) => { + Message::ActionPerformed(action) => { self.is_dirty = self.is_dirty || action.is_edit(); - self.content.edit(action); + self.content.perform(action); Command::none() } @@ -103,7 +103,7 @@ impl Application for Editor { if let Ok((path, contents)) = result { self.file = Some(path); - self.content = text_editor::Content::with(&contents); + self.content = text_editor::Content::with_text(&contents); } Command::none() @@ -191,7 +191,7 @@ impl Application for Editor { column![ controls, text_editor(&self.content) - .on_edit(Message::Edit) + .on_action(Message::ActionPerformed) .highlight::<Highlighter>( highlighter::Settings { theme: self.theme, diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index a0729058..7a213909 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links diff --git a/graphics/src/text.rs b/graphics/src/text.rs index c10eacad..7261900e 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -1,3 +1,4 @@ +//! Draw text. pub mod cache; pub mod editor; pub mod paragraph; @@ -17,6 +18,7 @@ use once_cell::sync::OnceCell; use std::borrow::Cow; use std::sync::{Arc, RwLock}; +/// Returns the global [`FontSystem`]. pub fn font_system() -> &'static RwLock<FontSystem> { static FONT_SYSTEM: OnceCell<RwLock<FontSystem>> = OnceCell::new(); @@ -32,6 +34,7 @@ pub fn font_system() -> &'static RwLock<FontSystem> { }) } +/// A set of system fonts. #[allow(missing_debug_implementations)] pub struct FontSystem { raw: cosmic_text::FontSystem, @@ -39,10 +42,12 @@ pub struct FontSystem { } impl FontSystem { + /// Returns the raw [`cosmic_text::FontSystem`]. pub fn raw(&mut self) -> &mut cosmic_text::FontSystem { &mut self.raw } + /// Loads a font from its bytes. pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { let _ = self.raw.db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), @@ -51,14 +56,19 @@ impl FontSystem { self.version = Version(self.version.0 + 1); } + /// Returns the current [`Version`] of the [`FontSystem`]. + /// + /// Loading a font will increase the version of a [`FontSystem`]. pub fn version(&self) -> Version { self.version } } +/// A version number. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); +/// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer .layout_runs() @@ -69,6 +79,7 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { Size::new(width, total_lines as f32 * buffer.metrics().line_height) } +/// Returns the attributes of the given [`Font`]. pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> { cosmic_text::Attrs::new() .family(to_family(font.family)) @@ -124,6 +135,7 @@ fn to_style(style: font::Style) -> cosmic_text::Style { } } +/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { match shaping { Shaping::Basic => cosmic_text::Shaping::Basic, @@ -131,6 +143,7 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color::pack(color).components(); diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 577c4687..b3293dd4 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -1,3 +1,4 @@ +//! Cache text. use crate::core::{Font, Size}; use crate::text; @@ -5,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; +/// A store of recently used sections of text. #[allow(missing_debug_implementations)] #[derive(Default)] pub struct Cache { @@ -21,14 +23,17 @@ type HashBuilder = twox_hash::RandomXxHashBuilder64; type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; impl Cache { + /// Creates a new empty [`Cache`]. pub fn new() -> Self { Self::default() } + /// Gets the text [`Entry`] with the given [`KeyHash`]. pub fn get(&self, key: &KeyHash) -> Option<&Entry> { self.entries.get(key) } + /// Allocates a text [`Entry`] if it is not already present in the [`Cache`]. pub fn allocate( &mut self, font_system: &mut cosmic_text::FontSystem, @@ -88,6 +93,9 @@ impl Cache { (hash, self.entries.get_mut(&hash).unwrap()) } + /// Trims the [`Cache`]. + /// + /// This will clear the sections of text that have not been used since the last `trim`. pub fn trim(&mut self) { self.entries .retain(|key, _| self.recently_used.contains(key)); @@ -99,13 +107,20 @@ impl Cache { } } +/// A cache key representing a section of text. #[derive(Debug, Clone, Copy)] pub struct Key<'a> { + /// The content of the text. pub content: &'a str, + /// The size of the text. pub size: f32, + /// The line height of the text. pub line_height: f32, + /// The [`Font`] of the text. pub font: Font, + /// The bounds of the text. pub bounds: Size, + /// The shaping strategy of the text. pub shaping: text::Shaping, } @@ -123,10 +138,14 @@ impl Key<'_> { } } +/// The hash of a [`Key`]. pub type KeyHash = u64; +/// A cache entry. #[allow(missing_debug_implementations)] pub struct Entry { + /// The buffer of text, ready for drawing. pub buffer: cosmic_text::Buffer, + /// The minimum bounds of the text. pub min_bounds: Size, } diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index a05312dc..d5262ae8 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,3 +1,4 @@ +//! Draw and edit text. use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; @@ -11,6 +12,7 @@ use cosmic_text::Edit as _; use std::fmt; use std::sync::{self, Arc}; +/// A multi-line text editor. #[derive(Debug, PartialEq)] pub struct Editor(Option<Arc<Internal>>); @@ -23,14 +25,21 @@ struct Internal { } impl Editor { + /// Creates a new empty [`Editor`]. pub fn new() -> Self { Self::default() } + /// Returns the buffer of the [`Editor`]. pub fn buffer(&self) -> &cosmic_text::Buffer { self.internal().editor.buffer() } + /// Creates a [`Weak`] reference to the [`Editor`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. pub fn downgrade(&self) -> Weak { let editor = self.internal(); @@ -662,13 +671,16 @@ impl fmt::Debug for Internal { } } +/// A weak reference to an [`Editor`]. #[derive(Debug, Clone)] pub struct Weak { raw: sync::Weak<Internal>, + /// The bounds of the [`Editor`]. pub bounds: Size, } impl Weak { + /// Tries to update the reference into an [`Editor`]. pub fn upgrade(&self) -> Option<Editor> { self.raw.upgrade().map(Some).map(Editor) } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index d0396e8e..ccfe4a61 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,3 +1,4 @@ +//! Draw paragraphs. use crate::core; use crate::core::alignment; use crate::core::text::{Hit, LineHeight, Shaping, Text}; @@ -7,6 +8,7 @@ use crate::text; use std::fmt; use std::sync::{self, Arc}; +/// A bunch of text. #[derive(Clone, PartialEq)] pub struct Paragraph(Option<Arc<Internal>>); @@ -23,14 +25,21 @@ struct Internal { } impl Paragraph { + /// Creates a new empty [`Paragraph`]. pub fn new() -> Self { Self::default() } + /// Returns the buffer of the [`Paragraph`]. pub fn buffer(&self) -> &cosmic_text::Buffer { &self.internal().buffer } + /// Creates a [`Weak`] reference to the [`Paragraph`]. + /// + /// This is useful to avoid cloning the [`Editor`] when + /// referential guarantees are unnecessary. For instance, + /// when creating a rendering tree. pub fn downgrade(&self) -> Weak { let paragraph = self.internal(); @@ -269,15 +278,20 @@ impl Default for Internal { } } +/// A weak reference to a [`Paragraph`]. #[derive(Debug, Clone)] pub struct Weak { raw: sync::Weak<Internal>, + /// The minimum bounds of the [`Paragraph`]. pub min_bounds: Size, + /// The horizontal alignment of the [`Paragraph`]. pub horizontal_alignment: alignment::Horizontal, + /// The vertical alignment of the [`Paragraph`]. pub vertical_alignment: alignment::Vertical, } impl Weak { + /// Tries to update the reference into a [`Paragraph`]. pub fn upgrade(&self) -> Option<Paragraph> { self.raw.upgrade().map(Some).map(Paragraph) } diff --git a/style/src/lib.rs b/style/src/lib.rs index 35460f4b..e4097434 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -10,7 +10,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![deny( unused_results, - // missing_docs, + missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index d46b39da..66417cec 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -4,19 +4,24 @@ use crate::core::{Color, Font, Pixels, Point, Rectangle}; use crate::graphics::text::editor; use crate::graphics::text::paragraph; -/// A paragraph of text. +/// A text primitive. #[derive(Debug, Clone)] pub enum Text<'a> { + /// A paragraph. + #[allow(missing_docs)] Paragraph { paragraph: paragraph::Weak, position: Point, color: Color, }, + /// An editor. + #[allow(missing_docs)] Editor { editor: editor::Weak, position: Point, color: Color, }, + /// A cached text. Cached(Cached<'a>), } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6d26723e..424dfeb3 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -23,7 +23,7 @@ #![forbid(rust_2018_idioms)] #![deny( missing_debug_implementations, - //missing_docs, + missing_docs, unsafe_code, unused_results, rustdoc::broken_intra_doc_links diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 97e4ac58..e3335a98 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,8 +4,8 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![deny( - // missing_debug_implementations, - // missing_docs, + //missing_debug_implementations, + missing_docs, unused_results, rustdoc::broken_intra_doc_links )] diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 6d25967e..da1905dc 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,3 +1,4 @@ +//! Display a multi-line text input for text editing. use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout::{self, Layout}; @@ -19,6 +20,7 @@ use std::sync::Arc; pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; +/// A multi-line text input. pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where Highlighter: text::Highlighter, @@ -47,6 +49,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { + /// Creates new [`TextEditor`] with the given [`Content`]. pub fn new(content: &'a Content<Renderer>) -> Self { Self { content, @@ -73,21 +76,34 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - pub fn on_edit(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { + /// Sets the message that should be produced when some action is performed in + /// the [`TextEditor`]. + /// + /// If this method is not called, the [`TextEditor`] will be disabled. + pub fn on_action( + mut self, + on_edit: impl Fn(Action) -> Message + 'a, + ) -> Self { self.on_edit = Some(Box::new(on_edit)); self } + /// Sets the [`Font`] of the [`TextEditor`]. + /// + /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = Some(font.into()); self } + /// Sets the [`Padding`] of the [`TextEditor`]. pub fn padding(mut self, padding: impl Into<Padding>) -> Self { self.padding = padding.into(); self } + /// Highlights the [`TextEditor`] with the given [`Highlighter`] and + /// a strategy to turn its highlights into some text format. pub fn highlight<H: text::Highlighter>( self, settings: H::Settings, @@ -112,6 +128,7 @@ where } } +/// The content of a [`TextEditor`]. pub struct Content<R = crate::Renderer>(RefCell<Internal<R>>) where R: text::Renderer; @@ -128,28 +145,33 @@ impl<R> Content<R> where R: text::Renderer, { + /// Creates an empty [`Content`]. pub fn new() -> Self { - Self::with("") + Self::with_text("") } - pub fn with(text: &str) -> Self { + /// Creates a [`Content`] with the given text. + pub fn with_text(text: &str) -> Self { Self(RefCell::new(Internal { editor: R::Editor::with_text(text), is_dirty: true, })) } - pub fn edit(&mut self, action: Action) { + /// Performs an [`Action`] on the [`Content`]. + pub fn perform(&mut self, action: Action) { let internal = self.0.get_mut(); internal.editor.perform(action); internal.is_dirty = true; } + /// Returns the amount of lines of the [`Content`]. pub fn line_count(&self) -> usize { self.0.borrow().editor.line_count() } + /// Returns the text of the line at the given index, if it exists. pub fn line( &self, index: usize, @@ -160,6 +182,7 @@ where .ok() } + /// Returns an iterator of the text of the lines in the [`Content`]. pub fn lines( &self, ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> { @@ -190,6 +213,9 @@ where } } + /// Returns the text of the [`Content`]. + /// + /// Lines are joined with `'\n'`. pub fn text(&self) -> String { let mut text = self.lines().enumerate().fold( String::new(), @@ -211,10 +237,12 @@ where text } + /// Returns the selected text of the [`Content`]. pub fn selection(&self) -> Option<String> { self.0.borrow().editor.selection() } + /// Returns the current cursor position of the [`Content`]. pub fn cursor_position(&self) -> (usize, usize) { self.0.borrow().editor.cursor_position() } -- cgit From e579d8553088c7d17784e7ff8f6e21360c2bd9ef Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 05:08:06 +0200 Subject: Implement missing debug implementations in `iced_widget` --- widget/src/lib.rs | 2 +- widget/src/text_editor.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/widget/src/lib.rs b/widget/src/lib.rs index e3335a98..2f959370 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -4,7 +4,7 @@ )] #![forbid(unsafe_code, rust_2018_idioms)] #![deny( - //missing_debug_implementations, + missing_debug_implementations, missing_docs, unused_results, rustdoc::broken_intra_doc_links diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index da1905dc..ac24920f 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -14,6 +14,7 @@ use crate::core::{ }; use std::cell::RefCell; +use std::fmt; use std::ops::DerefMut; use std::sync::Arc; @@ -21,6 +22,7 @@ pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; /// A multi-line text input. +#[allow(missing_debug_implementations)] pub struct TextEditor<'a, Highlighter, Message, Renderer = crate::Renderer> where Highlighter: text::Highlighter, @@ -257,6 +259,21 @@ where } } +impl<Renderer> fmt::Debug for Content<Renderer> +where + Renderer: text::Renderer, + Renderer::Editor: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let internal = self.0.borrow(); + + f.debug_struct("Content") + .field("editor", &internal.editor) + .field("is_dirty", &internal.is_dirty) + .finish() + } +} + struct State<Highlighter: text::Highlighter> { is_focused: bool, last_click: Option<mouse::Click>, -- cgit From 57f9024e89256ad3f99a3ab19bdc8524c1defa54 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 05:19:35 +0200 Subject: Fix intra-doc broken links --- .cargo/config.toml | 2 -- core/src/text/editor.rs | 2 +- graphics/src/text/paragraph.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3e02dda8..85a46cda 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -17,8 +17,6 @@ clippy --workspace --no-deps -- \ -D clippy::useless_conversion """ -#![allow(clippy::inherent_to_string, clippy::type_complexity)] - nitpick = """ clippy --workspace --no-deps -- \ -D warnings \ diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index ebb0eee2..f3c6e342 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -7,7 +7,7 @@ use std::sync::Arc; /// A component that can be used by widgets to edit multi-line text. pub trait Editor: Sized + Default { - /// The [`Font`] of the [`Editor`]. + /// The font of the [`Editor`]. type Font: Copy + PartialEq + Default; /// Creates a new [`Editor`] laid out with the given text. diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index ccfe4a61..4a08a8f4 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -37,7 +37,7 @@ impl Paragraph { /// Creates a [`Weak`] reference to the [`Paragraph`]. /// - /// This is useful to avoid cloning the [`Editor`] when + /// This is useful to avoid cloning the [`Paragraph`] when /// referential guarantees are unnecessary. For instance, /// when creating a rendering tree. pub fn downgrade(&self) -> Weak { -- cgit From 3ec5ad42251d4f35861f3bed621223e383742b12 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 06:00:28 +0200 Subject: Use upstream repository for `glyphon` dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bb8b4752..03df14c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = "0.10" futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "2caa9fc5e5923c1d827d177c3619cab7e9885b85" } +glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "2caa9fc5e5923c1d827d177c3619cab7e9885b85" } guillotiere = "0.6" half = "2.2" image = "0.24" -- cgit From c07315b84eb59daeb9bbe7480f30dc0937ceca13 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 27 Oct 2023 05:53:29 +0200 Subject: Disable maximize window button if `Settings::resizable` is `false` --- winit/src/settings.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 867dad0f..16c9fcdc 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -130,6 +130,12 @@ impl Window { .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)) -- cgit From c8eca4e6bfae82013e6bb08e9d8bf66560b36564 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Fri, 27 Oct 2023 16:37:58 +0200 Subject: Improve `TextEditor` scroll interaction with a touchpad --- widget/src/text_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index ac24920f..1708a2e5 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -628,7 +628,7 @@ impl Update { } } mouse::ScrollDelta::Pixels { y, .. } => { - -y.signum() as i32 + (-y / 4.0) as i32 } }, }) -- cgit From 98e088e731e6fbd5b5035033ae61bda823ced988 Mon Sep 17 00:00:00 2001 From: dtzxporter <dtzxporter@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:15:00 -0400 Subject: Migrate twox-hash -> xxhash_rust. Switch to Xxh3 for better performance. xxhash-rust is more maintained, built against `::core`, so no workaround for wasm is necessary. Switch to Xxh3 for better performance, which shows when loading/hashing image buffers. --- Cargo.toml | 2 +- core/Cargo.toml | 2 +- core/src/hasher.rs | 5 +++-- graphics/Cargo.toml | 6 +----- graphics/src/text/cache.rs | 6 +----- tiny_skia/Cargo.toml | 6 +----- 6 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a286b9b..f625f1ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ thiserror = "1.0" tiny-skia = "0.10" tokio = "1.0" tracing = "0.1" -twox-hash = { version = "1.0", default-features = false } +xxhash-rust = { version = "0.8.7", default-features = false, features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index 7acb7511..82946847 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,7 +14,7 @@ keywords.workspace = true bitflags.workspace = true log.workspace = true thiserror.workspace = true -twox-hash.workspace = true +xxhash-rust.workspace = true num-traits.workspace = true palette.workspace = true diff --git a/core/src/hasher.rs b/core/src/hasher.rs index 9d8f75b3..a13d78af 100644 --- a/core/src/hasher.rs +++ b/core/src/hasher.rs @@ -1,6 +1,7 @@ /// The hasher used to compare layouts. -#[derive(Debug, Default)] -pub struct Hasher(twox_hash::XxHash64); +#[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways. +#[derive(Default)] +pub struct Hasher(xxhash_rust::xxh3::Xxh3); impl core::hash::Hasher for Hasher { fn write(&mut self, bytes: &[u8]) { diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 3165810b..a7aea352 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -33,8 +33,8 @@ once_cell.workspace = true raw-window-handle.workspace = true rustc-hash.workspace = true thiserror.workspace = true -twox-hash.workspace = true unicode-segmentation.workspace = true +xxhash-rust.workspace = true image.workspace = true image.optional = true @@ -44,7 +44,3 @@ kamadak-exif.optional = true lyon_path.workspace = true lyon_path.optional = true - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -twox-hash.workspace = true -twox-hash.features = ["std"] diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index b3293dd4..7fb33567 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -16,11 +16,7 @@ pub struct Cache { hasher: HashBuilder, } -#[cfg(not(target_arch = "wasm32"))] -type HashBuilder = twox_hash::RandomXxHashBuilder64; - -#[cfg(target_arch = "wasm32")] -type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>; +type HashBuilder = xxhash_rust::xxh3::Xxh3Builder; impl Cache { /// Creates a new empty [`Cache`]. diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 15a6928a..df4c6143 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -26,11 +26,7 @@ raw-window-handle.workspace = true rustc-hash.workspace = true softbuffer.workspace = true tiny-skia.workspace = true -twox-hash.workspace = true +xxhash-rust.workspace = true resvg.workspace = true resvg.optional = true - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -twox-hash.workspace = true -twox-hash.features = ["std"] -- cgit From 4b69c71d5b570ce716b9c202e9a47d5ae9ce3ae0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 03:43:03 +0100 Subject: Remove patch version from `xxhash-rust` dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f625f1ad..4bcf7c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ thiserror = "1.0" tiny-skia = "0.10" tokio = "1.0" tracing = "0.1" -xxhash-rust = { version = "0.8.7", default-features = false, features = ["xxh3"] } +xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" -- cgit From 107e842071f1300df5e0bfcb26ee0a99024e51d8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 03:43:50 +0100 Subject: Remove unnecessary `default-features` attribute from `xxhash-rust` dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4bcf7c7a..ac34a4ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ thiserror = "1.0" tiny-skia = "0.10" tokio = "1.0" tracing = "0.1" -xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } +xxhash-rust = { version = "0.8", features = ["xxh3"] } unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" -- cgit From 2aaaf2cd0cb56f9efc946159a0232270f8d37eeb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 04:03:25 +0100 Subject: Call `convert_text` on `svg` node before rendering `tiny-skia` does not support text rendering, so we convert the text nodes to path nodes prior to that. --- Cargo.toml | 4 ++-- tiny_skia/src/vector.rs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1a286b9b..18dd8d3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -140,14 +140,14 @@ ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } raw-window-handle = "0.5" -resvg = "0.35" +resvg = "0.36" rustc-hash = "1.0" smol = "1.0" softbuffer = "0.2" syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" -tiny-skia = "0.10" +tiny-skia = "0.11" tokio = "1.0" tracing = "0.1" twox-hash = { version = "1.0", default-features = false } diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index a1cd269d..9c2893a2 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -1,7 +1,8 @@ use crate::core::svg::{Data, Handle}; use crate::core::{Color, Rectangle, Size}; +use crate::graphics::text; -use resvg::usvg; +use resvg::usvg::{self, TreeTextToPath}; use rustc_hash::{FxHashMap, FxHashSet}; use std::cell::RefCell; @@ -77,7 +78,7 @@ impl Cache { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let svg = match handle.data() { + let mut svg = match handle.data() { Data::Path(path) => { fs::read_to_string(path).ok().and_then(|contents| { usvg::Tree::from_str( @@ -92,6 +93,15 @@ impl Cache { } }; + if let Some(svg) = &mut svg { + if svg.has_text_nodes() { + let mut font_system = + text::font_system().write().expect("Read font system"); + + svg.convert_text(font_system.raw().db_mut()); + } + } + let _ = entry.insert(svg); } -- cgit From bb2f557d6a75850aed8e8689348f7a544b364bf6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 04:36:45 +0100 Subject: Fix `artifacts` job in `audit` workflow --- .github/workflows/audit.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index bfb617fb..5f5f7f65 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -24,5 +24,7 @@ jobs: - name: Install cargo-outdated run: cargo install cargo-outdated - uses: actions/checkout@master + - name: Delete `web-sys` dependency from `integration` example + run: sed '$d' examples/integration/Cargo.toml - name: Find outdated dependencies run: cargo outdated --workspace --exit-code 1 -- cgit From ef015a5e72802c059784e74d611f351df75403c0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 04:46:11 +0100 Subject: Run `sed` with `-i` option in `artifacts` job --- .github/workflows/audit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 5f5f7f65..80bbcacd 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -25,6 +25,6 @@ jobs: run: cargo install cargo-outdated - uses: actions/checkout@master - name: Delete `web-sys` dependency from `integration` example - run: sed '$d' examples/integration/Cargo.toml + run: sed -i '$d' examples/integration/Cargo.toml - name: Find outdated dependencies run: cargo outdated --workspace --exit-code 1 -- cgit From 5759096a4c33935fcdf5f96606143e4f21159186 Mon Sep 17 00:00:00 2001 From: Remmirad <remmirad@posteo.net> Date: Wed, 31 May 2023 15:46:21 +0200 Subject: Implement texture filtering options --- core/src/image.rs | 32 +++++++++++++++++++++++ tiny_skia/src/raster.rs | 7 ++++- wgpu/src/image.rs | 69 ++++++++++++++++++++++++++++++++----------------- widget/src/image.rs | 2 +- 4 files changed, 84 insertions(+), 26 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 85d9d475..9a6011a3 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -5,11 +5,31 @@ use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; +/// Image filter method +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FilterMethod { + /// Bilinear interpolation + #[default] + Linear, + /// Nearest Neighbor + Nearest, +} + +/// Texture filter settings +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +pub struct TextureFilter { + /// Filter for scaling the image down. + pub min: FilterMethod, + /// Filter for scaling the image up. + pub mag: FilterMethod, +} + /// A handle of some image data. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, data: Data, + filter: TextureFilter, } impl Handle { @@ -56,6 +76,7 @@ impl Handle { Handle { id: hasher.finish(), data, + filter: TextureFilter::default(), } } @@ -68,6 +89,17 @@ impl Handle { pub fn data(&self) -> &Data { &self.data } + + /// Returns a reference to the [`TextureFilter`]. + pub fn filter(&self) -> &TextureFilter { + &self.filter + } + + /// Sets the texture filtering methods. + pub fn set_filter(mut self, filter: TextureFilter) -> Self { + self.filter = filter; + self + } } impl<T> From<T> for Handle diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index d13b1167..95f74ad1 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -39,12 +39,17 @@ impl Pipeline { let transform = transform.pre_scale(width_scale, height_scale); + let quality = match handle.filter().mag { + raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, + raster::FilterMethod::Nearest => tiny_skia::FilterQuality::Nearest, + }; + pixels.draw_pixmap( (bounds.x / width_scale) as i32, (bounds.y / height_scale) as i32, image, &tiny_skia::PixmapPaint { - quality: tiny_skia::FilterQuality::Bilinear, + quality: quality, ..Default::default() }, transform, diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 553ba330..a0fe7e83 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -7,6 +7,7 @@ mod raster; mod vector; use atlas::Atlas; +use iced_graphics::core::image::{TextureFilter, FilterMethod}; use crate::core::{Rectangle, Size}; use crate::graphics::Transformation; @@ -14,6 +15,7 @@ use crate::layer; use crate::Buffer; use std::cell::RefCell; +use std::collections::HashMap; use std::mem; use bytemuck::{Pod, Zeroable}; @@ -37,7 +39,7 @@ pub struct Pipeline { pipeline: wgpu::RenderPipeline, vertices: wgpu::Buffer, indices: wgpu::Buffer, - sampler: wgpu::Sampler, + sampler: HashMap<TextureFilter,wgpu::Sampler>, texture: wgpu::BindGroup, texture_version: usize, texture_atlas: Atlas, @@ -142,15 +144,32 @@ impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { use wgpu::util::DeviceExt; - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); + let to_wgpu = |method: FilterMethod| { + match method { + FilterMethod::Linear => wgpu::FilterMode::Linear, + FilterMethod::Nearest => wgpu::FilterMode::Nearest, + } + }; + + let mut sampler = HashMap::new(); + + let filter = [FilterMethod::Linear, FilterMethod::Nearest]; + for min in 0..filter.len() { + for mag in 0..filter.len() { + let _ = sampler.insert(TextureFilter {min: filter[min], mag: filter[mag]}, + device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: to_wgpu(filter[mag]), + min_filter: to_wgpu(filter[min]), + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + } + )); + } + } + let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -355,7 +374,7 @@ impl Pipeline { #[cfg(feature = "tracing")] let _ = info_span!("Wgpu::Image", "DRAW").entered(); - let instances: &mut Vec<Instance> = &mut Vec::new(); + let instances: &mut HashMap<TextureFilter,Vec<Instance>> = &mut HashMap::new(); #[cfg(feature = "image")] let mut raster_cache = self.raster_cache.borrow_mut(); @@ -377,7 +396,7 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], atlas_entry, - instances, + instances.entry(handle.filter().clone()).or_insert(Vec::new()), ); } } @@ -405,7 +424,7 @@ impl Pipeline { [bounds.x, bounds.y], size, atlas_entry, - instances, + instances.entry(TextureFilter::default()).or_insert(Vec::new()), ); } } @@ -438,18 +457,20 @@ impl Pipeline { self.texture_version = texture_version; } - if self.layers.len() <= self.prepare_layer { - self.layers.push(Layer::new( - device, - &self.constant_layout, - &self.sampler, - )); + for (filter, instances) in instances.iter_mut() { + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.constant_layout, + &self.sampler.get(filter).expect("Sampler is registered"), + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare(device, queue, &instances, transformation); + + self.prepare_layer += 1; } - - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, instances, transformation); - - self.prepare_layer += 1; } pub fn render<'a>( diff --git a/widget/src/image.rs b/widget/src/image.rs index a0e89920..9f0b16b7 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -13,7 +13,7 @@ use crate::core::{ use std::hash::Hash; -pub use image::Handle; +pub use image::{Handle, TextureFilter, FilterMethod}; /// Creates a new [`Viewer`] with the given image `Handle`. pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { -- cgit From 4b32a488808e371313ce78e727c9d98ab2eb759e Mon Sep 17 00:00:00 2001 From: Remmirad <remmirad@posteo.net> Date: Fri, 4 Aug 2023 13:50:16 +0200 Subject: Fix clippy + fmt --- core/src/image.rs | 2 +- tiny_skia/src/raster.rs | 10 +++++++--- wgpu/src/image.rs | 42 ++++++++++++++++++++++++------------------ widget/src/image.rs | 2 +- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 9a6011a3..69f19436 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -11,7 +11,7 @@ pub enum FilterMethod { /// Bilinear interpolation #[default] Linear, - /// Nearest Neighbor + /// Nearest Neighbor Nearest, } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 95f74ad1..3f35ee78 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -40,8 +40,12 @@ impl Pipeline { let transform = transform.pre_scale(width_scale, height_scale); let quality = match handle.filter().mag { - raster::FilterMethod::Linear => tiny_skia::FilterQuality::Bilinear, - raster::FilterMethod::Nearest => tiny_skia::FilterQuality::Nearest, + raster::FilterMethod::Linear => { + tiny_skia::FilterQuality::Bilinear + } + raster::FilterMethod::Nearest => { + tiny_skia::FilterQuality::Nearest + } }; pixels.draw_pixmap( @@ -49,7 +53,7 @@ impl Pipeline { (bounds.y / height_scale) as i32, image, &tiny_skia::PixmapPaint { - quality: quality, + quality, ..Default::default() }, transform, diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index a0fe7e83..a3168001 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -7,7 +7,7 @@ mod raster; mod vector; use atlas::Atlas; -use iced_graphics::core::image::{TextureFilter, FilterMethod}; +use iced_graphics::core::image::{FilterMethod, TextureFilter}; use crate::core::{Rectangle, Size}; use crate::graphics::Transformation; @@ -39,7 +39,7 @@ pub struct Pipeline { pipeline: wgpu::RenderPipeline, vertices: wgpu::Buffer, indices: wgpu::Buffer, - sampler: HashMap<TextureFilter,wgpu::Sampler>, + sampler: HashMap<TextureFilter, wgpu::Sampler>, texture: wgpu::BindGroup, texture_version: usize, texture_atlas: Atlas, @@ -144,11 +144,9 @@ impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { use wgpu::util::DeviceExt; - let to_wgpu = |method: FilterMethod| { - match method { - FilterMethod::Linear => wgpu::FilterMode::Linear, - FilterMethod::Nearest => wgpu::FilterMode::Nearest, - } + let to_wgpu = |method: FilterMethod| match method { + FilterMethod::Linear => wgpu::FilterMode::Linear, + FilterMethod::Nearest => wgpu::FilterMode::Nearest, }; let mut sampler = HashMap::new(); @@ -156,7 +154,11 @@ impl Pipeline { let filter = [FilterMethod::Linear, FilterMethod::Nearest]; for min in 0..filter.len() { for mag in 0..filter.len() { - let _ = sampler.insert(TextureFilter {min: filter[min], mag: filter[mag]}, + let _ = sampler.insert( + TextureFilter { + min: filter[min], + mag: filter[mag], + }, device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, @@ -165,12 +167,11 @@ impl Pipeline { min_filter: to_wgpu(filter[min]), mipmap_filter: wgpu::FilterMode::Linear, ..Default::default() - } - )); + }), + ); } } - let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::image constants layout"), @@ -374,7 +375,8 @@ impl Pipeline { #[cfg(feature = "tracing")] let _ = info_span!("Wgpu::Image", "DRAW").entered(); - let instances: &mut HashMap<TextureFilter,Vec<Instance>> = &mut HashMap::new(); + let instances: &mut HashMap<TextureFilter, Vec<Instance>> = + &mut HashMap::new(); #[cfg(feature = "image")] let mut raster_cache = self.raster_cache.borrow_mut(); @@ -396,7 +398,9 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], atlas_entry, - instances.entry(handle.filter().clone()).or_insert(Vec::new()), + instances + .entry(handle.filter().clone()) + .or_insert(Vec::new()), ); } } @@ -424,7 +428,9 @@ impl Pipeline { [bounds.x, bounds.y], size, atlas_entry, - instances.entry(TextureFilter::default()).or_insert(Vec::new()), + instances + .entry(TextureFilter::default()) + .or_insert(Vec::new()), ); } } @@ -462,13 +468,13 @@ impl Pipeline { self.layers.push(Layer::new( device, &self.constant_layout, - &self.sampler.get(filter).expect("Sampler is registered"), + self.sampler.get(filter).expect("Sampler is registered"), )); } - + let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, &instances, transformation); - + layer.prepare(device, queue, instances, transformation); + self.prepare_layer += 1; } } diff --git a/widget/src/image.rs b/widget/src/image.rs index 9f0b16b7..684f200c 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -13,7 +13,7 @@ use crate::core::{ use std::hash::Hash; -pub use image::{Handle, TextureFilter, FilterMethod}; +pub use image::{FilterMethod, Handle, TextureFilter}; /// Creates a new [`Viewer`] with the given image `Handle`. pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { -- cgit From e5d3e75d826e9fad8a0da5dd538aa542059dd034 Mon Sep 17 00:00:00 2001 From: Remmirad <remmirad@posteo.net> Date: Mon, 25 Sep 2023 21:54:50 +0200 Subject: fix design for wgpu backend --- wgpu/src/image.rs | 133 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 56 deletions(-) diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index a3168001..0aa7f899 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -8,6 +8,7 @@ mod vector; use atlas::Atlas; use iced_graphics::core::image::{FilterMethod, TextureFilter}; +use wgpu::Sampler; use crate::core::{Rectangle, Size}; use crate::graphics::Transformation; @@ -15,7 +16,6 @@ use crate::layer; use crate::Buffer; use std::cell::RefCell; -use std::collections::HashMap; use std::mem; use bytemuck::{Pod, Zeroable}; @@ -29,6 +29,8 @@ use crate::core::svg; #[cfg(feature = "tracing")] use tracing::info_span; +const SAMPLER_COUNT: usize = 4; + #[derive(Debug)] pub struct Pipeline { #[cfg(feature = "image")] @@ -39,14 +41,14 @@ pub struct Pipeline { pipeline: wgpu::RenderPipeline, vertices: wgpu::Buffer, indices: wgpu::Buffer, - sampler: HashMap<TextureFilter, wgpu::Sampler>, + sampler: [wgpu::Sampler; SAMPLER_COUNT], texture: wgpu::BindGroup, texture_version: usize, texture_atlas: Atlas, texture_layout: wgpu::BindGroupLayout, constant_layout: wgpu::BindGroupLayout, - layers: Vec<Layer>, + layers: Vec<[Option<Layer>; SAMPLER_COUNT]>, prepare_layer: usize, } @@ -149,28 +151,32 @@ impl Pipeline { FilterMethod::Nearest => wgpu::FilterMode::Nearest, }; - let mut sampler = HashMap::new(); + let mut sampler: [Option<Sampler>; SAMPLER_COUNT] = + [None, None, None, None]; let filter = [FilterMethod::Linear, FilterMethod::Nearest]; for min in 0..filter.len() { for mag in 0..filter.len() { - let _ = sampler.insert( - TextureFilter { - min: filter[min], - mag: filter[mag], - }, - device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: to_wgpu(filter[mag]), - min_filter: to_wgpu(filter[min]), - mipmap_filter: wgpu::FilterMode::Linear, - ..Default::default() - }), - ); + sampler[to_index(&TextureFilter { + min: filter[min], + mag: filter[mag], + })] = Some(device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: to_wgpu(filter[mag]), + min_filter: to_wgpu(filter[min]), + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + })); } } + let sampler = [ + sampler[0].take().unwrap(), + sampler[1].take().unwrap(), + sampler[2].take().unwrap(), + sampler[3].take().unwrap(), + ]; let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -375,8 +381,8 @@ impl Pipeline { #[cfg(feature = "tracing")] let _ = info_span!("Wgpu::Image", "DRAW").entered(); - let instances: &mut HashMap<TextureFilter, Vec<Instance>> = - &mut HashMap::new(); + let mut instances: [Vec<Instance>; SAMPLER_COUNT] = + [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; #[cfg(feature = "image")] let mut raster_cache = self.raster_cache.borrow_mut(); @@ -398,9 +404,7 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], atlas_entry, - instances - .entry(handle.filter().clone()) - .or_insert(Vec::new()), + &mut instances[to_index(handle.filter())], ); } } @@ -428,9 +432,7 @@ impl Pipeline { [bounds.x, bounds.y], size, atlas_entry, - instances - .entry(TextureFilter::default()) - .or_insert(Vec::new()), + &mut instances[to_index(&TextureFilter::default())], ); } } @@ -463,20 +465,26 @@ impl Pipeline { self.texture_version = texture_version; } - for (filter, instances) in instances.iter_mut() { - if self.layers.len() <= self.prepare_layer { - self.layers.push(Layer::new( - device, - &self.constant_layout, - self.sampler.get(filter).expect("Sampler is registered"), - )); + if self.layers.len() <= self.prepare_layer { + self.layers.push([None, None, None, None]); + } + for (i, instances) in instances.iter_mut().enumerate() { + let layer = &mut self.layers[self.prepare_layer][i]; + if !instances.is_empty() { + if layer.is_none() { + *layer = Some(Layer::new( + device, + &self.constant_layout, + &self.sampler[i], + )) + } } - let layer = &mut self.layers[self.prepare_layer]; - layer.prepare(device, queue, instances, transformation); - - self.prepare_layer += 1; + if let Some(layer) = layer { + layer.prepare(device, queue, instances, transformation); + } } + self.prepare_layer += 1; } pub fn render<'a>( @@ -485,24 +493,29 @@ impl Pipeline { bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, ) { - if let Some(layer) = self.layers.get(layer) { - render_pass.set_pipeline(&self.pipeline); - - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - render_pass.set_bind_group(1, &self.texture, &[]); - render_pass.set_index_buffer( - self.indices.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - - layer.render(render_pass); + if let Some(layer_group) = self.layers.get(layer) { + for (i, layer) in layer_group.iter().enumerate() { + if let Some(layer) = layer { + println!("Render {i}"); + render_pass.set_pipeline(&self.pipeline); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_index_buffer( + self.indices.slice(..), + wgpu::IndexFormat::Uint16, + ); + render_pass.set_vertex_buffer(0, self.vertices.slice(..)); + + layer.render(render_pass); + } + } } } @@ -517,6 +530,14 @@ impl Pipeline { } } +fn to_index(filter: &TextureFilter) -> usize { + let to_index = |m| match m { + FilterMethod::Linear => 0, + FilterMethod::Nearest => 1, + }; + return (to_index(filter.mag) << 1) | (to_index(filter.min)); +} + #[repr(C)] #[derive(Clone, Copy, Zeroable, Pod)] pub struct Vertex { -- cgit From 75c9afc608a4a9ff44d60a8fb6f4a5819f05bf79 Mon Sep 17 00:00:00 2001 From: Remmirad <remmirad@posteo.net> Date: Mon, 25 Sep 2023 22:03:22 +0200 Subject: Remove debug traces --- wgpu/src/image.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 0aa7f899..6768a714 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -494,9 +494,8 @@ impl Pipeline { render_pass: &mut wgpu::RenderPass<'a>, ) { if let Some(layer_group) = self.layers.get(layer) { - for (i, layer) in layer_group.iter().enumerate() { + for layer in layer_group.iter() { if let Some(layer) = layer { - println!("Render {i}"); render_pass.set_pipeline(&self.pipeline); render_pass.set_scissor_rect( -- cgit From a5125d6fea824df1191777fe3eb53a2f748208b9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 07:02:01 +0100 Subject: Refactor texture image filtering - Support only `Linear` or `Nearest` - Simplify `Layer` groups - Move `FilterMethod` to `Image` and `image::Viewer` --- core/src/image.rs | 49 +++------- examples/tour/src/main.rs | 44 +++++++-- graphics/src/primitive.rs | 2 + graphics/src/renderer.rs | 13 ++- renderer/src/lib.rs | 9 +- tiny_skia/src/backend.rs | 16 ++- tiny_skia/src/raster.rs | 3 +- wgpu/src/image.rs | 238 +++++++++++++++++++++++++-------------------- wgpu/src/layer.rs | 7 +- wgpu/src/layer/image.rs | 3 + widget/src/image.rs | 29 ++++-- widget/src/image/viewer.rs | 5 +- 12 files changed, 254 insertions(+), 164 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 69f19436..e9675316 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -5,31 +5,11 @@ use std::hash::{Hash, Hasher as _}; use std::path::PathBuf; use std::sync::Arc; -/// Image filter method -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum FilterMethod { - /// Bilinear interpolation - #[default] - Linear, - /// Nearest Neighbor - Nearest, -} - -/// Texture filter settings -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] -pub struct TextureFilter { - /// Filter for scaling the image down. - pub min: FilterMethod, - /// Filter for scaling the image up. - pub mag: FilterMethod, -} - /// A handle of some image data. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Handle { id: u64, data: Data, - filter: TextureFilter, } impl Handle { @@ -76,7 +56,6 @@ impl Handle { Handle { id: hasher.finish(), data, - filter: TextureFilter::default(), } } @@ -89,17 +68,6 @@ impl Handle { pub fn data(&self) -> &Data { &self.data } - - /// Returns a reference to the [`TextureFilter`]. - pub fn filter(&self) -> &TextureFilter { - &self.filter - } - - /// Sets the texture filtering methods. - pub fn set_filter(mut self, filter: TextureFilter) -> Self { - self.filter = filter; - self - } } impl<T> From<T> for Handle @@ -196,6 +164,16 @@ impl std::fmt::Debug for Data { } } +/// Image filtering strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum FilterMethod { + /// Bilinear interpolation. + #[default] + Linear, + /// Nearest neighbor. + Nearest, +} + /// A [`Renderer`] that can render raster graphics. /// /// [renderer]: crate::renderer @@ -210,5 +188,10 @@ pub trait Renderer: crate::Renderer { /// Draws an image with the given [`Handle`] and inside the provided /// `bounds`. - fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); + fn draw( + &mut self, + handle: Self::Handle, + filter_method: FilterMethod, + bounds: Rectangle, + ); } diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index d46e40d1..7003d8ae 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,4 +1,4 @@ -use iced::alignment; +use iced::alignment::{self, Alignment}; use iced::theme; use iced::widget::{ checkbox, column, container, horizontal_space, image, radio, row, @@ -126,7 +126,10 @@ impl Steps { Step::Toggler { can_continue: false, }, - Step::Image { width: 300 }, + Step::Image { + width: 300, + filter_method: image::FilterMethod::Linear, + }, Step::Scrollable, Step::TextInput { value: String::new(), @@ -195,6 +198,7 @@ enum Step { }, Image { width: u16, + filter_method: image::FilterMethod, }, Scrollable, TextInput { @@ -215,6 +219,7 @@ pub enum StepMessage { TextColorChanged(Color), LanguageSelected(Language), ImageWidthChanged(u16), + ImageUseNearestToggled(bool), InputChanged(String), ToggleSecureInput(bool), ToggleTextInputIcon(bool), @@ -265,6 +270,15 @@ impl<'a> Step { *width = new_width; } } + StepMessage::ImageUseNearestToggled(use_nearest) => { + if let Step::Image { filter_method, .. } = self { + *filter_method = if use_nearest { + image::FilterMethod::Nearest + } else { + image::FilterMethod::Linear + }; + } + } StepMessage::InputChanged(new_value) => { if let Step::TextInput { value, .. } = self { *value = new_value; @@ -330,7 +344,10 @@ impl<'a> Step { Step::Toggler { can_continue } => Self::toggler(*can_continue), Step::Slider { value } => Self::slider(*value), Step::Text { size, color } => Self::text(*size, *color), - Step::Image { width } => Self::image(*width), + Step::Image { + width, + filter_method, + } => Self::image(*width, *filter_method), Step::RowsAndColumns { layout, spacing } => { Self::rows_and_columns(*layout, *spacing) } @@ -525,16 +542,25 @@ impl<'a> Step { ) } - fn image(width: u16) -> Column<'a, StepMessage> { + fn image( + width: u16, + filter_method: image::FilterMethod, + ) -> Column<'a, StepMessage> { Self::container("Image") .push("An image that tries to keep its aspect ratio.") - .push(ferris(width)) + .push(ferris(width, filter_method)) .push(slider(100..=500, width, StepMessage::ImageWidthChanged)) .push( text(format!("Width: {width} px")) .width(Length::Fill) .horizontal_alignment(alignment::Horizontal::Center), ) + .push(checkbox( + "Use nearest interpolation", + filter_method == image::FilterMethod::Nearest, + StepMessage::ImageUseNearestToggled, + )) + .align_items(Alignment::Center) } fn scrollable() -> Column<'a, StepMessage> { @@ -555,7 +581,7 @@ impl<'a> Step { .horizontal_alignment(alignment::Horizontal::Center), ) .push(vertical_space(4096)) - .push(ferris(300)) + .push(ferris(300, image::FilterMethod::Linear)) .push( text("You made it!") .width(Length::Fill) @@ -646,7 +672,10 @@ impl<'a> Step { } } -fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { +fn ferris<'a>( + width: u16, + filter_method: image::FilterMethod, +) -> Container<'a, StepMessage> { container( // This should go away once we unify resource loading on native // platforms @@ -655,6 +684,7 @@ fn ferris<'a>(width: u16) -> Container<'a, StepMessage> { } else { image(format!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) } + .filter_method(filter_method) .width(width), ) .width(Length::Fill) diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index ce0b734b..4ed512c1 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -68,6 +68,8 @@ pub enum Primitive<T> { Image { /// The handle of the image handle: image::Handle, + /// The filter method of the image + filter_method: image::FilterMethod, /// The bounds of the image bounds: Rectangle, }, diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 93fff3b7..d7613e36 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -215,8 +215,17 @@ where self.backend().dimensions(handle) } - fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.primitives.push(Primitive::Image { handle, bounds }); + fn draw( + &mut self, + handle: image::Handle, + filter_method: image::FilterMethod, + bounds: Rectangle, + ) { + self.primitives.push(Primitive::Image { + handle, + filter_method, + bounds, + }); } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index cc81c6e2..43f9794b 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -214,8 +214,13 @@ impl<T> crate::core::image::Renderer for Renderer<T> { delegate!(self, renderer, renderer.dimensions(handle)) } - fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) { - delegate!(self, renderer, renderer.draw(handle, bounds)); + fn draw( + &mut self, + handle: crate::core::image::Handle, + filter_method: crate::core::image::FilterMethod, + bounds: Rectangle, + ) { + delegate!(self, renderer, renderer.draw(handle, filter_method, bounds)); } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 3c6fe288..f2905b00 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -445,7 +445,11 @@ impl Backend { ); } #[cfg(feature = "image")] - Primitive::Image { handle, bounds } => { + Primitive::Image { + handle, + filter_method, + bounds, + } => { let physical_bounds = (*bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { @@ -461,8 +465,14 @@ impl Backend { ) .post_scale(scale_factor, scale_factor); - self.raster_pipeline - .draw(handle, *bounds, pixels, transform, clip_mask); + self.raster_pipeline.draw( + handle, + *filter_method, + *bounds, + pixels, + transform, + clip_mask, + ); } #[cfg(not(feature = "image"))] Primitive::Image { .. } => { diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index 3f35ee78..5f17ae60 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -28,6 +28,7 @@ impl Pipeline { pub fn draw( &mut self, handle: &raster::Handle, + filter_method: raster::FilterMethod, bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, @@ -39,7 +40,7 @@ impl Pipeline { let transform = transform.pre_scale(width_scale, height_scale); - let quality = match handle.filter().mag { + let quality = match filter_method { raster::FilterMethod::Linear => { tiny_skia::FilterQuality::Bilinear } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 6768a714..1a88c6ae 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -7,8 +7,6 @@ mod raster; mod vector; use atlas::Atlas; -use iced_graphics::core::image::{FilterMethod, TextureFilter}; -use wgpu::Sampler; use crate::core::{Rectangle, Size}; use crate::graphics::Transformation; @@ -29,8 +27,6 @@ use crate::core::svg; #[cfg(feature = "tracing")] use tracing::info_span; -const SAMPLER_COUNT: usize = 4; - #[derive(Debug)] pub struct Pipeline { #[cfg(feature = "image")] @@ -41,30 +37,31 @@ pub struct Pipeline { pipeline: wgpu::RenderPipeline, vertices: wgpu::Buffer, indices: wgpu::Buffer, - sampler: [wgpu::Sampler; SAMPLER_COUNT], + nearest_sampler: wgpu::Sampler, + linear_sampler: wgpu::Sampler, texture: wgpu::BindGroup, texture_version: usize, texture_atlas: Atlas, texture_layout: wgpu::BindGroupLayout, constant_layout: wgpu::BindGroupLayout, - layers: Vec<[Option<Layer>; SAMPLER_COUNT]>, + layers: Vec<Layer>, prepare_layer: usize, } #[derive(Debug)] struct Layer { uniforms: wgpu::Buffer, - constants: wgpu::BindGroup, - instances: Buffer<Instance>, - instance_count: usize, + nearest: Data, + linear: Data, } impl Layer { fn new( device: &wgpu::Device, constant_layout: &wgpu::BindGroupLayout, - sampler: &wgpu::Sampler, + nearest_sampler: &wgpu::Sampler, + linear_sampler: &wgpu::Sampler, ) -> Self { let uniforms = device.create_buffer(&wgpu::BufferDescriptor { label: Some("iced_wgpu::image uniforms buffer"), @@ -73,6 +70,59 @@ impl Layer { mapped_at_creation: false, }); + let nearest = + Data::new(device, constant_layout, nearest_sampler, &uniforms); + + let linear = + Data::new(device, constant_layout, linear_sampler, &uniforms); + + Self { + uniforms, + nearest, + linear, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + nearest_instances: &[Instance], + linear_instances: &[Instance], + transformation: Transformation, + ) { + queue.write_buffer( + &self.uniforms, + 0, + bytemuck::bytes_of(&Uniforms { + transform: transformation.into(), + }), + ); + + self.nearest.upload(device, queue, nearest_instances); + self.linear.upload(device, queue, linear_instances); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + self.nearest.render(render_pass); + self.linear.render(render_pass); + } +} + +#[derive(Debug)] +struct Data { + constants: wgpu::BindGroup, + instances: Buffer<Instance>, + instance_count: usize, +} + +impl Data { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + uniforms: &wgpu::Buffer, + ) -> Self { let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::image constants bind group"), layout: constant_layout, @@ -102,28 +152,18 @@ impl Layer { ); Self { - uniforms, constants, instances, instance_count: 0, } } - fn prepare( + fn upload( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, instances: &[Instance], - transformation: Transformation, ) { - queue.write_buffer( - &self.uniforms, - 0, - bytemuck::bytes_of(&Uniforms { - transform: transformation.into(), - }), - ); - let _ = self.instances.resize(device, instances.len()); let _ = self.instances.write(queue, 0, instances); @@ -146,37 +186,25 @@ impl Pipeline { pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { use wgpu::util::DeviceExt; - let to_wgpu = |method: FilterMethod| match method { - FilterMethod::Linear => wgpu::FilterMode::Linear, - FilterMethod::Nearest => wgpu::FilterMode::Nearest, - }; - - let mut sampler: [Option<Sampler>; SAMPLER_COUNT] = - [None, None, None, None]; - - let filter = [FilterMethod::Linear, FilterMethod::Nearest]; - for min in 0..filter.len() { - for mag in 0..filter.len() { - sampler[to_index(&TextureFilter { - min: filter[min], - mag: filter[mag], - })] = Some(device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: to_wgpu(filter[mag]), - min_filter: to_wgpu(filter[min]), - mipmap_filter: wgpu::FilterMode::Linear, - ..Default::default() - })); - } - } - let sampler = [ - sampler[0].take().unwrap(), - sampler[1].take().unwrap(), - sampler[2].take().unwrap(), - sampler[3].take().unwrap(), - ]; + let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Nearest, + mag_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + min_filter: wgpu::FilterMode::Linear, + mag_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); let constant_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -338,7 +366,8 @@ impl Pipeline { pipeline, vertices, indices, - sampler, + nearest_sampler, + linear_sampler, texture, texture_version: texture_atlas.layer_count(), texture_atlas, @@ -381,8 +410,8 @@ impl Pipeline { #[cfg(feature = "tracing")] let _ = info_span!("Wgpu::Image", "DRAW").entered(); - let mut instances: [Vec<Instance>; SAMPLER_COUNT] = - [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; + let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); + let linear_instances: &mut Vec<Instance> = &mut Vec::new(); #[cfg(feature = "image")] let mut raster_cache = self.raster_cache.borrow_mut(); @@ -393,7 +422,11 @@ impl Pipeline { for image in images { match &image { #[cfg(feature = "image")] - layer::Image::Raster { handle, bounds } => { + layer::Image::Raster { + handle, + filter_method, + bounds, + } => { if let Some(atlas_entry) = raster_cache.upload( device, encoder, @@ -404,7 +437,12 @@ impl Pipeline { [bounds.x, bounds.y], [bounds.width, bounds.height], atlas_entry, - &mut instances[to_index(handle.filter())], + match filter_method { + image::FilterMethod::Nearest => { + nearest_instances + } + image::FilterMethod::Linear => linear_instances, + }, ); } } @@ -432,7 +470,7 @@ impl Pipeline { [bounds.x, bounds.y], size, atlas_entry, - &mut instances[to_index(&TextureFilter::default())], + nearest_instances, ); } } @@ -441,7 +479,7 @@ impl Pipeline { } } - if instances.is_empty() { + if nearest_instances.is_empty() && linear_instances.is_empty() { return; } @@ -466,24 +504,24 @@ impl Pipeline { } if self.layers.len() <= self.prepare_layer { - self.layers.push([None, None, None, None]); + self.layers.push(Layer::new( + device, + &self.constant_layout, + &self.nearest_sampler, + &self.linear_sampler, + )); } - for (i, instances) in instances.iter_mut().enumerate() { - let layer = &mut self.layers[self.prepare_layer][i]; - if !instances.is_empty() { - if layer.is_none() { - *layer = Some(Layer::new( - device, - &self.constant_layout, - &self.sampler[i], - )) - } - } - if let Some(layer) = layer { - layer.prepare(device, queue, instances, transformation); - } - } + let layer = &mut self.layers[self.prepare_layer]; + + layer.prepare( + device, + queue, + &nearest_instances, + &linear_instances, + transformation, + ); + self.prepare_layer += 1; } @@ -493,28 +531,24 @@ impl Pipeline { bounds: Rectangle<u32>, render_pass: &mut wgpu::RenderPass<'a>, ) { - if let Some(layer_group) = self.layers.get(layer) { - for layer in layer_group.iter() { - if let Some(layer) = layer { - render_pass.set_pipeline(&self.pipeline); - - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - - render_pass.set_bind_group(1, &self.texture, &[]); - render_pass.set_index_buffer( - self.indices.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - - layer.render(render_pass); - } - } + if let Some(layer) = self.layers.get(layer) { + render_pass.set_pipeline(&self.pipeline); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_index_buffer( + self.indices.slice(..), + wgpu::IndexFormat::Uint16, + ); + render_pass.set_vertex_buffer(0, self.vertices.slice(..)); + + layer.render(render_pass); } } @@ -529,14 +563,6 @@ impl Pipeline { } } -fn to_index(filter: &TextureFilter) -> usize { - let to_index = |m| match m { - FilterMethod::Linear => 0, - FilterMethod::Nearest => 1, - }; - return (to_index(filter.mag) << 1) | (to_index(filter.min)); -} - #[repr(C)] #[derive(Clone, Copy, Zeroable, Pod)] pub struct Vertex { @@ -571,7 +597,7 @@ struct Instance { } impl Instance { - pub const INITIAL: usize = 1_000; + pub const INITIAL: usize = 20; } #[repr(C)] diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index b251538e..286801e6 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -186,11 +186,16 @@ impl<'a> Layer<'a> { layer.quads.add(quad, background); } - Primitive::Image { handle, bounds } => { + Primitive::Image { + handle, + filter_method, + bounds, + } => { let layer = &mut layers[current_layer]; layer.images.push(Image::Raster { handle: handle.clone(), + filter_method: *filter_method, bounds: *bounds + translation, }); } diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs index 0de589f8..facbe192 100644 --- a/wgpu/src/layer/image.rs +++ b/wgpu/src/layer/image.rs @@ -10,6 +10,9 @@ pub enum Image { /// The handle of a raster image. handle: image::Handle, + /// The filter method of a raster image. + filter_method: image::FilterMethod, + /// The bounds of the image. bounds: Rectangle, }, diff --git a/widget/src/image.rs b/widget/src/image.rs index 684f200c..67699102 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -13,7 +13,7 @@ use crate::core::{ use std::hash::Hash; -pub use image::{FilterMethod, Handle, TextureFilter}; +pub use image::{FilterMethod, Handle}; /// Creates a new [`Viewer`] with the given image `Handle`. pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { @@ -37,6 +37,7 @@ pub struct Image<Handle> { width: Length, height: Length, content_fit: ContentFit, + filter_method: FilterMethod, } impl<Handle> Image<Handle> { @@ -47,6 +48,7 @@ impl<Handle> Image<Handle> { width: Length::Shrink, height: Length::Shrink, content_fit: ContentFit::Contain, + filter_method: FilterMethod::default(), } } @@ -65,11 +67,15 @@ impl<Handle> Image<Handle> { /// Sets the [`ContentFit`] of the [`Image`]. /// /// Defaults to [`ContentFit::Contain`] - pub fn content_fit(self, content_fit: ContentFit) -> Self { - Self { - content_fit, - ..self - } + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } + + /// Sets the [`FilterMethod`] of the [`Image`]. + pub fn filter_method(mut self, filter_method: FilterMethod) -> Self { + self.filter_method = filter_method; + self } } @@ -119,6 +125,7 @@ pub fn draw<Renderer, Handle>( layout: Layout<'_>, handle: &Handle, content_fit: ContentFit, + filter_method: FilterMethod, ) where Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, @@ -141,7 +148,7 @@ pub fn draw<Renderer, Handle>( ..bounds }; - renderer.draw(handle.clone(), drawing_bounds + offset); + renderer.draw(handle.clone(), filter_method, drawing_bounds + offset); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height @@ -191,7 +198,13 @@ where _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw(renderer, layout, &self.handle, self.content_fit); + draw( + renderer, + layout, + &self.handle, + self.content_fit, + self.filter_method, + ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 44624fc8..68015ba8 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -22,19 +22,21 @@ pub struct Viewer<Handle> { max_scale: f32, scale_step: f32, handle: Handle, + filter_method: image::FilterMethod, } impl<Handle> Viewer<Handle> { /// Creates a new [`Viewer`] with the given [`State`]. pub fn new(handle: Handle) -> Self { Viewer { + handle, padding: 0.0, width: Length::Shrink, height: Length::Shrink, min_scale: 0.25, max_scale: 10.0, scale_step: 0.10, - handle, + filter_method: image::FilterMethod::default(), } } @@ -329,6 +331,7 @@ where image::Renderer::draw( renderer, self.handle.clone(), + self.filter_method, Rectangle { x: bounds.x, y: bounds.y, -- cgit From 9d560c813566ba04be3e23ae1b14861365485b57 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 11 Nov 2023 07:27:38 +0100 Subject: Fix unnecessary references in `iced_wgpu::image` --- wgpu/src/image.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 1a88c6ae..b78802c7 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -131,7 +131,7 @@ impl Data { binding: 0, resource: wgpu::BindingResource::Buffer( wgpu::BufferBinding { - buffer: &uniforms, + buffer: uniforms, offset: 0, size: None, }, @@ -517,8 +517,8 @@ impl Pipeline { layer.prepare( device, queue, - &nearest_instances, - &linear_instances, + nearest_instances, + linear_instances, transformation, ); -- cgit From ae2d59ae96ba1ec2daf6979beff0e3913ba9e0b8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 12 Nov 2023 03:17:02 +0100 Subject: Add `check` workflow to ensure `iced_widget` crate compiles --- .github/workflows/check.yml | 29 +++++++++++++++++++++++++++++ .github/workflows/test.yml | 21 +-------------------- 2 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..df9c480f --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,29 @@ +name: Check +on: [push, pull_request] +jobs: + widget: + runs-on: ubuntu-latest + steps: + - uses: hecrj/setup-rust-action@v1 + - uses: actions/checkout@master + - name: Check standalone `iced_widget` crate + run: cargo check --package iced_widget --features image,svg,canvas + + wasm: + runs-on: ubuntu-latest + env: + RUSTFLAGS: --cfg=web_sys_unstable_apis + steps: + - uses: hecrj/setup-rust-action@v1 + with: + rust-version: stable + targets: wasm32-unknown-unknown + - uses: actions/checkout@master + - name: Run checks + run: cargo check --package iced --target wasm32-unknown-unknown + - name: Check compilation of `tour` example + run: cargo build --package tour --target wasm32-unknown-unknown + - name: Check compilation of `todos` example + run: cargo build --package todos --target wasm32-unknown-unknown + - name: Check compilation of `integration` example + run: cargo build --package integration --target wasm32-unknown-unknown diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 215b616b..a08033c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: Test on: [push, pull_request] jobs: - native: + all: runs-on: ${{ matrix.os }} strategy: matrix: @@ -22,22 +22,3 @@ jobs: run: | cargo test --verbose --workspace cargo test --verbose --workspace --all-features - - web: - runs-on: ubuntu-latest - env: - RUSTFLAGS: --cfg=web_sys_unstable_apis - steps: - - uses: hecrj/setup-rust-action@v1 - with: - rust-version: stable - targets: wasm32-unknown-unknown - - uses: actions/checkout@master - - name: Run checks - run: cargo check --package iced --target wasm32-unknown-unknown - - name: Check compilation of `tour` example - run: cargo build --package tour --target wasm32-unknown-unknown - - name: Check compilation of `todos` example - run: cargo build --package todos --target wasm32-unknown-unknown - - name: Check compilation of `integration` example - run: cargo build --package integration --target wasm32-unknown-unknown -- cgit From 9d5ff12063e05158ede74f1aec4167bf910c8730 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 12 Nov 2023 03:22:43 +0100 Subject: Fix conditional compilation in `iced_renderer` --- renderer/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 43f9794b..78dec847 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -252,6 +252,7 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> { crate::Geometry::TinySkia(primitive) => { renderer.draw_primitive(primitive); } + #[cfg(feature = "wgpu")] crate::Geometry::Wgpu(_) => unreachable!(), } } -- cgit From 93416cbebd1dad04d250bc39ee7db9482d1e5e72 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 12 Nov 2023 03:33:09 +0100 Subject: Deny warnings in `test` workflow --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 215b616b..e9e1d86b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,8 @@ on: [push, pull_request] jobs: native: runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: --deny warnings strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] -- cgit From f98627a317615151681ca8b324052eb4a170b789 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sun, 12 Nov 2023 03:40:32 +0100 Subject: Add missing `'static` lifetimes to constant slices --- examples/lazy/src/main.rs | 2 +- examples/modal/src/main.rs | 3 ++- examples/toast/src/main.rs | 2 +- highlighter/src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 9bf17c56..01560598 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -46,7 +46,7 @@ enum Color { } impl Color { - const ALL: &[Color] = &[ + const ALL: &'static [Color] = &[ Color::Black, Color::Red, Color::Orange, diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index b0e2c81b..3b69f5e6 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -205,7 +205,8 @@ enum Plan { } impl Plan { - pub const ALL: &[Self] = &[Self::Basic, Self::Pro, Self::Enterprise]; + pub const ALL: &'static [Self] = + &[Self::Basic, Self::Pro, Self::Enterprise]; } impl fmt::Display for Plan { diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 20c3dd42..5b089e8a 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -210,7 +210,7 @@ mod toast { } impl Status { - pub const ALL: &[Self] = + pub const ALL: &'static [Self] = &[Self::Primary, Self::Secondary, Self::Success, Self::Danger]; } diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 5630756e..63f21fc0 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -168,7 +168,7 @@ pub enum Theme { } impl Theme { - pub const ALL: &[Self] = &[ + pub const ALL: &'static [Self] = &[ Self::SolarizedDark, Self::Base16Mocha, Self::Base16Ocean, -- cgit From 781ef1f94c4859aeeb852f801b72be095b8ff82b Mon Sep 17 00:00:00 2001 From: Bingus <shankern@protonmail.com> Date: Thu, 14 Sep 2023 13:58:36 -0700 Subject: Added support for custom shader widget for iced_wgpu backend. --- Cargo.toml | 2 +- core/src/rectangle.rs | 11 + examples/custom_shader/Cargo.toml | 13 + examples/custom_shader/src/camera.rs | 53 ++ examples/custom_shader/src/cubes.rs | 99 ++++ examples/custom_shader/src/main.rs | 174 ++++++ examples/custom_shader/src/pipeline.rs | 600 +++++++++++++++++++++ examples/custom_shader/src/primitive.rs | 95 ++++ examples/custom_shader/src/primitive/buffer.rs | 39 ++ examples/custom_shader/src/primitive/cube.rs | 324 +++++++++++ examples/custom_shader/src/primitive/uniforms.rs | 22 + examples/custom_shader/src/primitive/vertex.rs | 29 + examples/custom_shader/src/shaders/cubes.wgsl | 123 +++++ examples/custom_shader/src/shaders/depth.wgsl | 48 ++ .../src/textures/ice_cube_normal_map.png | Bin 0 -> 1773656 bytes .../custom_shader/src/textures/skybox/neg_x.jpg | Bin 0 -> 7549 bytes .../custom_shader/src/textures/skybox/neg_y.jpg | Bin 0 -> 2722 bytes .../custom_shader/src/textures/skybox/neg_z.jpg | Bin 0 -> 3986 bytes .../custom_shader/src/textures/skybox/pos_x.jpg | Bin 0 -> 5522 bytes .../custom_shader/src/textures/skybox/pos_y.jpg | Bin 0 -> 3382 bytes .../custom_shader/src/textures/skybox/pos_z.jpg | Bin 0 -> 5205 bytes examples/integration/src/main.rs | 1 + graphics/Cargo.toml | 1 - renderer/src/lib.rs | 21 + renderer/src/widget.rs | 3 + renderer/src/widget/shader.rs | 215 ++++++++ renderer/src/widget/shader/event.rs | 21 + renderer/src/widget/shader/program.rs | 60 +++ style/src/theme.rs | 2 +- wgpu/src/backend.rs | 64 ++- wgpu/src/custom.rs | 66 +++ wgpu/src/layer.rs | 16 + wgpu/src/lib.rs | 1 + wgpu/src/primitive.rs | 36 ++ wgpu/src/window/compositor.rs | 2 + widget/Cargo.toml | 1 + widget/src/lib.rs | 3 + 37 files changed, 2139 insertions(+), 6 deletions(-) create mode 100644 examples/custom_shader/Cargo.toml create mode 100644 examples/custom_shader/src/camera.rs create mode 100644 examples/custom_shader/src/cubes.rs create mode 100644 examples/custom_shader/src/main.rs create mode 100644 examples/custom_shader/src/pipeline.rs create mode 100644 examples/custom_shader/src/primitive.rs create mode 100644 examples/custom_shader/src/primitive/buffer.rs create mode 100644 examples/custom_shader/src/primitive/cube.rs create mode 100644 examples/custom_shader/src/primitive/uniforms.rs create mode 100644 examples/custom_shader/src/primitive/vertex.rs create mode 100644 examples/custom_shader/src/shaders/cubes.wgsl create mode 100644 examples/custom_shader/src/shaders/depth.wgsl create mode 100644 examples/custom_shader/src/textures/ice_cube_normal_map.png create mode 100644 examples/custom_shader/src/textures/skybox/neg_x.jpg create mode 100644 examples/custom_shader/src/textures/skybox/neg_y.jpg create mode 100644 examples/custom_shader/src/textures/skybox/neg_z.jpg create mode 100644 examples/custom_shader/src/textures/skybox/pos_x.jpg create mode 100644 examples/custom_shader/src/textures/skybox/pos_y.jpg create mode 100644 examples/custom_shader/src/textures/skybox/pos_z.jpg create mode 100644 renderer/src/widget/shader.rs create mode 100644 renderer/src/widget/shader/event.rs create mode 100644 renderer/src/widget/shader/program.rs create mode 100644 wgpu/src/custom.rs diff --git a/Cargo.toml b/Cargo.toml index d69c95cf..ad4cd1bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ maintenance = { status = "actively-developed" } [features] default = ["wgpu"] # Enable the `wgpu` GPU-accelerated renderer backend -wgpu = ["iced_renderer/wgpu"] +wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] # Enables the `Image` widget image = ["iced_widget/image", "dep:image"] # Enables the `Svg` widget diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index c1c2eeac..d5437d51 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -183,6 +183,17 @@ impl From<Rectangle<u32>> for Rectangle<f32> { } } +impl From<Rectangle<f32>> for Rectangle<u32> { + fn from(rectangle: Rectangle<f32>) -> Self { + Rectangle { + x: rectangle.x as u32, + y: rectangle.y as u32, + width: rectangle.width as u32, + height: rectangle.height as u32, + } + } +} + impl<T> std::ops::Add<Vector<T>> for Rectangle<T> where T: std::ops::Add<Output = T>, diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml new file mode 100644 index 00000000..7a927811 --- /dev/null +++ b/examples/custom_shader/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "custom_shader" +version = "0.1.0" +authors = ["Bingus <shankern@protonmail.com>"] +edition = "2021" + +[dependencies] +iced = { path = "../..", features = ["debug", "advanced"]} +image = { version = "0.24.6"} +wgpu = "0.17" +bytemuck = { version = "1.13.1" } +glam = { version = "0.24.0", features = ["bytemuck"] } +rand = "0.8.5" diff --git a/examples/custom_shader/src/camera.rs b/examples/custom_shader/src/camera.rs new file mode 100644 index 00000000..2a49c102 --- /dev/null +++ b/examples/custom_shader/src/camera.rs @@ -0,0 +1,53 @@ +use glam::{mat4, vec3, vec4}; +use iced::Rectangle; + +#[derive(Copy, Clone)] +pub struct Camera { + eye: glam::Vec3, + target: glam::Vec3, + up: glam::Vec3, + fov_y: f32, + near: f32, + far: f32, +} + +impl Default for Camera { + fn default() -> Self { + Self { + eye: vec3(0.0, 2.0, 3.0), + target: glam::Vec3::ZERO, + up: glam::Vec3::Y, + fov_y: 45.0, + near: 0.1, + far: 100.0, + } + } +} + +pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 0.5, 0.0), + vec4(0.0, 0.0, 0.5, 1.0), +); + +impl Camera { + pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 { + //TODO looks distorted without padding; base on surface texture size instead? + let aspect_ratio = bounds.width / (bounds.height + 150.0); + + let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up); + let proj = glam::Mat4::perspective_rh( + self.fov_y, + aspect_ratio, + self.near, + self.far, + ); + + OPENGL_TO_WGPU_MATRIX * proj * view + } + + pub fn position(&self) -> glam::Vec4 { + glam::Vec4::from((self.eye, 0.0)) + } +} diff --git a/examples/custom_shader/src/cubes.rs b/examples/custom_shader/src/cubes.rs new file mode 100644 index 00000000..8dbba4b1 --- /dev/null +++ b/examples/custom_shader/src/cubes.rs @@ -0,0 +1,99 @@ +use crate::camera::Camera; +use crate::primitive; +use crate::primitive::cube::Cube; +use glam::Vec3; +use iced::widget::shader; +use iced::{mouse, Color, Rectangle}; +use rand::Rng; +use std::cmp::Ordering; +use std::iter; +use std::time::Duration; + +pub const MAX: u32 = 500; + +#[derive(Clone)] +pub struct Cubes { + pub size: f32, + pub cubes: Vec<Cube>, + pub camera: Camera, + pub show_depth_buffer: bool, + pub light_color: Color, +} + +impl Cubes { + pub fn new() -> Self { + let mut cubes = Self { + size: 0.2, + cubes: vec![], + camera: Camera::default(), + show_depth_buffer: false, + light_color: Color::WHITE, + }; + + cubes.adjust_num_cubes(MAX); + + cubes + } + + pub fn update(&mut self, time: Duration) { + for cube in self.cubes.iter_mut() { + cube.update(self.size, time.as_secs_f32()); + } + } + + pub fn adjust_num_cubes(&mut self, num_cubes: u32) { + let curr_cubes = self.cubes.len() as u32; + + match num_cubes.cmp(&curr_cubes) { + Ordering::Greater => { + // spawn + let cubes_2_spawn = (num_cubes - curr_cubes) as usize; + + let mut cubes = 0; + self.cubes.extend(iter::from_fn(|| { + if cubes < cubes_2_spawn { + cubes += 1; + Some(Cube::new(self.size, rnd_origin())) + } else { + None + } + })); + } + Ordering::Less => { + // chop + let cubes_2_cut = curr_cubes - num_cubes; + let new_len = self.cubes.len() - cubes_2_cut as usize; + self.cubes.truncate(new_len); + } + _ => {} + } + } +} + +impl<Message> shader::Program<Message> for Cubes { + type State = (); + type Primitive = primitive::Primitive; + + fn draw( + &self, + _state: &Self::State, + _cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + primitive::Primitive::new( + &self.cubes, + &self.camera, + bounds, + self.show_depth_buffer, + self.light_color, + ) + } +} + +fn rnd_origin() -> Vec3 { + Vec3::new( + rand::thread_rng().gen_range(-4.0..4.0), + rand::thread_rng().gen_range(-4.0..4.0), + rand::thread_rng().gen_range(-4.0..2.0), + ) +} diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs new file mode 100644 index 00000000..76fa1625 --- /dev/null +++ b/examples/custom_shader/src/main.rs @@ -0,0 +1,174 @@ +mod camera; +mod cubes; +mod pipeline; +mod primitive; + +use crate::cubes::Cubes; +use iced::widget::{ + checkbox, column, container, row, slider, text, vertical_space, Shader, +}; +use iced::{ + executor, window, Alignment, Application, Color, Command, Element, Length, + Renderer, Subscription, Theme, +}; +use std::time::Instant; + +fn main() -> iced::Result { + IcedCubes::run(iced::Settings::default()) +} + +struct IcedCubes { + start: Instant, + cubes: Cubes, + num_cubes_slider: u32, +} + +impl Default for IcedCubes { + fn default() -> Self { + Self { + start: Instant::now(), + cubes: Cubes::new(), + num_cubes_slider: cubes::MAX, + } + } +} + +#[derive(Debug, Clone)] +enum Message { + CubeAmountChanged(u32), + CubeSizeChanged(f32), + Tick(Instant), + ShowDepthBuffer(bool), + LightColorChanged(Color), +} + +impl Application for IcedCubes { + type Executor = executor::Default; + type Message = Message; + type Theme = Theme; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) { + (IcedCubes::default(), Command::none()) + } + + fn title(&self) -> String { + "Iced Cubes".to_string() + } + + fn update(&mut self, message: Self::Message) -> Command<Self::Message> { + match message { + Message::CubeAmountChanged(num) => { + self.num_cubes_slider = num; + self.cubes.adjust_num_cubes(num); + } + Message::CubeSizeChanged(size) => { + self.cubes.size = size; + } + Message::Tick(time) => { + self.cubes.update(time - self.start); + } + Message::ShowDepthBuffer(show) => { + self.cubes.show_depth_buffer = show; + } + Message::LightColorChanged(color) => { + self.cubes.light_color = color; + } + } + + Command::none() + } + + fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> { + let top_controls = row![ + control( + "Amount", + slider( + 1..=cubes::MAX, + self.num_cubes_slider, + Message::CubeAmountChanged + ) + .width(100) + ), + control( + "Size", + slider(0.1..=0.25, self.cubes.size, Message::CubeSizeChanged) + .step(0.01) + .width(100), + ), + checkbox( + "Show Depth Buffer", + self.cubes.show_depth_buffer, + Message::ShowDepthBuffer + ), + ] + .spacing(40); + + let bottom_controls = row![ + control( + "R", + slider(0.0..=1.0, self.cubes.light_color.r, move |r| { + Message::LightColorChanged(Color { + r, + ..self.cubes.light_color + }) + }) + .step(0.01) + .width(100) + ), + control( + "G", + slider(0.0..=1.0, self.cubes.light_color.g, move |g| { + Message::LightColorChanged(Color { + g, + ..self.cubes.light_color + }) + }) + .step(0.01) + .width(100) + ), + control( + "B", + slider(0.0..=1.0, self.cubes.light_color.b, move |b| { + Message::LightColorChanged(Color { + b, + ..self.cubes.light_color + }) + }) + .step(0.01) + .width(100) + ) + ] + .spacing(40); + + let controls = column![top_controls, bottom_controls,] + .spacing(10) + .align_items(Alignment::Center); + + let shader = Shader::new(&self.cubes) + .width(Length::Fill) + .height(Length::Fill); + + container( + column![shader, controls, vertical_space(20),] + .spacing(40) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } + + fn subscription(&self) -> Subscription<Self::Message> { + window::frames().map(Message::Tick) + } +} + +fn control<'a>( + label: &'static str, + control: impl Into<Element<'a, Message>>, +) -> Element<'a, Message> { + row![text(label), control.into()].spacing(10).into() +} diff --git a/examples/custom_shader/src/pipeline.rs b/examples/custom_shader/src/pipeline.rs new file mode 100644 index 00000000..9dd154e8 --- /dev/null +++ b/examples/custom_shader/src/pipeline.rs @@ -0,0 +1,600 @@ +use crate::primitive; +use crate::primitive::cube; +use crate::primitive::{Buffer, Uniforms}; +use iced::{Rectangle, Size}; +use wgpu::util::DeviceExt; + +const SKY_TEXTURE_SIZE: u32 = 128; + +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, + vertices: wgpu::Buffer, + cubes: Buffer, + uniforms: wgpu::Buffer, + uniform_bind_group: wgpu::BindGroup, + depth_texture_size: Size<u32>, + depth_view: wgpu::TextureView, + depth_pipeline: DepthPipeline, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + target_size: Size<u32>, + ) -> Self { + //vertices of one cube + let vertices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("cubes vertex buffer"), + contents: bytemuck::cast_slice(&cube::Raw::vertices()), + usage: wgpu::BufferUsages::VERTEX, + }); + + //cube instance data + let cubes_buffer = Buffer::new( + device, + "cubes instance buffer", + std::mem::size_of::<cube::Raw>() as u64, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + //uniforms for all cubes + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("cubes uniform buffer"), + size: std::mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + //depth buffer + let depth_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("cubes depth texture"), + size: wgpu::Extent3d { + width: target_size.width, + height: target_size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let depth_view = + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let normal_map_data = load_normal_map_data(); + + //normal map + let normal_texture = device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("cubes normal map texture"), + size: wgpu::Extent3d { + width: 1024, + height: 1024, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + &normal_map_data, + ); + + let normal_view = + normal_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + //skybox texture for reflection/refraction + let skybox_data = load_skybox_data(); + + let skybox_texture = device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("cubes skybox texture"), + size: wgpu::Extent3d { + width: SKY_TEXTURE_SIZE, + height: SKY_TEXTURE_SIZE, + depth_or_array_layers: 6, //one for each face of the cube + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + &skybox_data, + ); + + let sky_view = + skybox_texture.create_view(&wgpu::TextureViewDescriptor { + label: Some("cubes skybox texture view"), + dimension: Some(wgpu::TextureViewDimension::Cube), + ..Default::default() + }); + + let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("cubes skybox sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubes uniform bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::Filtering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ], + }); + + let uniform_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes uniform bind group"), + layout: &uniform_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniforms.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&sky_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&sky_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView( + &normal_view, + ), + }, + ], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubes pipeline layout"), + bind_group_layouts: &[&uniform_bind_group_layout], + push_constant_ranges: &[], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("cubes shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("shaders/cubes.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("cubes pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[primitive::Vertex::desc(), cube::Raw::desc()], + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Max, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let depth_pipeline = DepthPipeline::new( + device, + format, + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + + Self { + pipeline, + cubes: cubes_buffer, + uniforms, + uniform_bind_group, + vertices, + depth_texture_size: target_size, + depth_view, + depth_pipeline, + } + } + + fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) { + if self.depth_texture_size.height != size.height + || self.depth_texture_size.width != size.width + { + let text = device.create_texture(&wgpu::TextureDescriptor { + label: Some("cubes depth texture"), + size: wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + self.depth_view = + text.create_view(&wgpu::TextureViewDescriptor::default()); + self.depth_texture_size = size; + + self.depth_pipeline.update(device, &text); + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + uniforms: &Uniforms, + num_cubes: usize, + cubes: &[cube::Raw], + ) { + //recreate depth texture if surface texture size has changed + self.update_depth_texture(device, target_size); + + // update uniforms + queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms)); + + //resize cubes vertex buffer if cubes amount changed + let new_size = num_cubes * std::mem::size_of::<cube::Raw>(); + self.cubes.resize(device, new_size as u64); + + //always write new cube data since they are constantly rotating + queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes)); + } + + pub fn render( + &self, + target: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + bounds: Rectangle<u32>, + num_cubes: u32, + show_depth: bool, + ) { + { + let mut pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cubes.pipeline.pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + }, + )], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }, + ), + }); + + pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_vertex_buffer(0, self.vertices.slice(..)); + pass.set_vertex_buffer(1, self.cubes.raw.slice(..)); + pass.draw(0..36, 0..num_cubes); + } + + if show_depth { + self.depth_pipeline.render(encoder, target, bounds); + } + } +} + +struct DepthPipeline { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + bind_group: wgpu::BindGroup, + sampler: wgpu::Sampler, + depth_view: wgpu::TextureView, +} + +impl DepthPipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + depth_texture: wgpu::TextureView, + ) -> Self { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("cubes.depth_pipeline.sampler"), + ..Default::default() + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubes.depth_pipeline.bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes.depth_pipeline.bind_group"), + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView( + &depth_texture, + ), + }, + ], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubes.depth_pipeline.layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("cubes.depth_pipeline.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("shaders/depth.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("cubes.depth_pipeline.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + primitive: Default::default(), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Less, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: Default::default(), + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + Self { + pipeline, + bind_group_layout, + bind_group, + sampler, + depth_view: depth_texture, + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + depth_texture: &wgpu::Texture, + ) { + self.depth_view = + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + self.bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes.depth_pipeline.bind_group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView( + &self.depth_view, + ), + }, + ], + }); + } + + pub fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + bounds: Rectangle<u32>, + ) { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cubes.pipeline.depth_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: None, + stencil_ops: None, + }, + ), + }); + + pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..6, 0..1); + } +} + +fn load_skybox_data() -> Vec<u8> { + let pos_x: &[u8] = include_bytes!("textures/skybox/pos_x.jpg"); + let neg_x: &[u8] = include_bytes!("textures/skybox/neg_x.jpg"); + let pos_y: &[u8] = include_bytes!("textures/skybox/pos_y.jpg"); + let neg_y: &[u8] = include_bytes!("textures/skybox/neg_y.jpg"); + let pos_z: &[u8] = include_bytes!("textures/skybox/pos_z.jpg"); + let neg_z: &[u8] = include_bytes!("textures/skybox/neg_z.jpg"); + + let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z]; + + data.iter().fold(vec![], |mut acc, bytes| { + let i = image::load_from_memory_with_format( + bytes, + image::ImageFormat::Jpeg, + ) + .unwrap() + .to_rgba8() + .into_raw(); + + acc.extend(i); + acc + }) +} + +fn load_normal_map_data() -> Vec<u8> { + let bytes: &[u8] = include_bytes!("textures/ice_cube_normal_map.png"); + + image::load_from_memory_with_format(bytes, image::ImageFormat::Png) + .unwrap() + .to_rgba8() + .into_raw() +} diff --git a/examples/custom_shader/src/primitive.rs b/examples/custom_shader/src/primitive.rs new file mode 100644 index 00000000..2201218f --- /dev/null +++ b/examples/custom_shader/src/primitive.rs @@ -0,0 +1,95 @@ +pub mod cube; +pub mod vertex; + +mod buffer; +mod uniforms; + +use crate::camera::Camera; +use crate::pipeline::Pipeline; +use crate::primitive::cube::Cube; +use iced::advanced::graphics::Transformation; +use iced::widget::shader; +use iced::{Color, Rectangle, Size}; + +pub use crate::primitive::vertex::Vertex; +pub use buffer::Buffer; +pub use uniforms::Uniforms; + +/// A collection of `Cube`s that can be rendered. +#[derive(Debug)] +pub struct Primitive { + cubes: Vec<cube::Raw>, + uniforms: Uniforms, + show_depth_buffer: bool, +} + +impl Primitive { + pub fn new( + cubes: &[Cube], + camera: &Camera, + bounds: Rectangle, + show_depth_buffer: bool, + light_color: Color, + ) -> Self { + let uniforms = Uniforms::new(camera, bounds, light_color); + + Self { + cubes: cubes + .iter() + .map(cube::Raw::from_cube) + .collect::<Vec<cube::Raw>>(), + uniforms, + show_depth_buffer, + } + } +} + +impl shader::Primitive for Primitive { + fn prepare( + &self, + format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + _scale_factor: f32, + _transform: Transformation, + storage: &mut shader::Storage, + ) { + if !storage.has::<Pipeline>() { + storage.store(Pipeline::new(device, queue, format, target_size)) + } + + let pipeline = storage.get_mut::<Pipeline>().unwrap(); + + //upload data to GPU + pipeline.update( + device, + queue, + target_size, + &self.uniforms, + self.cubes.len(), + &self.cubes, + ); + } + + fn render( + &self, + storage: &shader::Storage, + bounds: Rectangle<u32>, + target: &wgpu::TextureView, + _target_size: Size<u32>, + encoder: &mut wgpu::CommandEncoder, + ) { + //at this point our pipeline should always be initialized + let pipeline = storage.get::<Pipeline>().unwrap(); + + //render primitive + pipeline.render( + target, + encoder, + bounds, + self.cubes.len() as u32, + self.show_depth_buffer, + ) + } +} diff --git a/examples/custom_shader/src/primitive/buffer.rs b/examples/custom_shader/src/primitive/buffer.rs new file mode 100644 index 00000000..377ce1bb --- /dev/null +++ b/examples/custom_shader/src/primitive/buffer.rs @@ -0,0 +1,39 @@ +// A custom buffer container for dynamic resizing. +pub struct Buffer { + pub raw: wgpu::Buffer, + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, +} + +impl Buffer { + pub fn new( + device: &wgpu::Device, + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, + ) -> Self { + Self { + raw: device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage, + mapped_at_creation: false, + }), + label, + size, + usage, + } + } + + pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) { + if new_size > self.size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(self.label), + size: new_size, + usage: self.usage, + mapped_at_creation: false, + }); + } + } +} diff --git a/examples/custom_shader/src/primitive/cube.rs b/examples/custom_shader/src/primitive/cube.rs new file mode 100644 index 00000000..c23f2132 --- /dev/null +++ b/examples/custom_shader/src/primitive/cube.rs @@ -0,0 +1,324 @@ +use crate::primitive::Vertex; +use glam::{vec2, vec3, Vec3}; +use rand::{thread_rng, Rng}; + +/// A single instance of a cube. +#[derive(Debug, Clone)] +pub struct Cube { + pub rotation: glam::Quat, + pub position: Vec3, + pub size: f32, + rotation_dir: f32, + rotation_axis: glam::Vec3, +} + +impl Default for Cube { + fn default() -> Self { + Self { + rotation: glam::Quat::IDENTITY, + position: glam::Vec3::ZERO, + size: 0.1, + rotation_dir: 1.0, + rotation_axis: glam::Vec3::Y, + } + } +} + +impl Cube { + pub fn new(size: f32, origin: Vec3) -> Self { + let rnd = thread_rng().gen_range(0.0..=1.0f32); + + Self { + rotation: glam::Quat::IDENTITY, + position: origin + Vec3::new(0.1, 0.1, 0.1), + size, + rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 }, + rotation_axis: if rnd <= 0.33 { + glam::Vec3::Y + } else if rnd <= 0.66 { + glam::Vec3::X + } else { + glam::Vec3::Z + }, + } + } + + pub fn update(&mut self, size: f32, time: f32) { + self.rotation = glam::Quat::from_axis_angle( + self.rotation_axis, + time / 2.0 * self.rotation_dir, + ); + self.size = size; + } +} + +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)] +#[repr(C)] +pub struct Raw { + transformation: glam::Mat4, + normal: glam::Mat3, + _padding: [f32; 3], +} + +impl Raw { + const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![ + //cube transformation matrix + 4 => Float32x4, + 5 => Float32x4, + 6 => Float32x4, + 7 => Float32x4, + //normal rotation matrix + 8 => Float32x3, + 9 => Float32x3, + 10 => Float32x3, + ]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &Self::ATTRIBS, + } + } +} + +impl Raw { + pub fn from_cube(cube: &Cube) -> Raw { + Raw { + transformation: glam::Mat4::from_scale_rotation_translation( + glam::vec3(cube.size, cube.size, cube.size), + cube.rotation, + cube.position, + ), + normal: glam::Mat3::from_quat(cube.rotation), + _padding: [0.0; 3], + } + } + + pub fn vertices() -> [Vertex; 36] { + [ + //face 1 + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 2 + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 3 + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + //face 4 + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + //face 5 + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 6 + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + ] + } +} diff --git a/examples/custom_shader/src/primitive/uniforms.rs b/examples/custom_shader/src/primitive/uniforms.rs new file mode 100644 index 00000000..4fcb413b --- /dev/null +++ b/examples/custom_shader/src/primitive/uniforms.rs @@ -0,0 +1,22 @@ +use crate::camera::Camera; +use iced::{Color, Rectangle}; + +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Uniforms { + camera_proj: glam::Mat4, + camera_pos: glam::Vec4, + light_color: glam::Vec4, +} + +impl Uniforms { + pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self { + let camera_proj = camera.build_view_proj_matrix(bounds); + + Self { + camera_proj, + camera_pos: camera.position(), + light_color: glam::Vec4::from(light_color.into_linear()), + } + } +} diff --git a/examples/custom_shader/src/primitive/vertex.rs b/examples/custom_shader/src/primitive/vertex.rs new file mode 100644 index 00000000..6d17aa0f --- /dev/null +++ b/examples/custom_shader/src/primitive/vertex.rs @@ -0,0 +1,29 @@ +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Vertex { + pub pos: glam::Vec3, + pub normal: glam::Vec3, + pub tangent: glam::Vec3, + pub uv: glam::Vec2, +} + +impl Vertex { + const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ + //position + 0 => Float32x3, + //normal + 1 => Float32x3, + //tangent + 2 => Float32x3, + //uv + 3 => Float32x2, + ]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBS, + } + } +} diff --git a/examples/custom_shader/src/shaders/cubes.wgsl b/examples/custom_shader/src/shaders/cubes.wgsl new file mode 100644 index 00000000..cd7f94d8 --- /dev/null +++ b/examples/custom_shader/src/shaders/cubes.wgsl @@ -0,0 +1,123 @@ +struct Uniforms { + projection: mat4x4<f32>, + camera_pos: vec4<f32>, + light_color: vec4<f32>, +} + +const LIGHT_POS: vec3<f32> = vec3<f32>(0.0, 3.0, 3.0); + +@group(0) @binding(0) var<uniform> uniforms: Uniforms; +@group(0) @binding(1) var sky_texture: texture_cube<f32>; +@group(0) @binding(2) var tex_sampler: sampler; +@group(0) @binding(3) var normal_texture: texture_2d<f32>; + +struct Vertex { + @location(0) position: vec3<f32>, + @location(1) normal: vec3<f32>, + @location(2) tangent: vec3<f32>, + @location(3) uv: vec2<f32>, +} + +struct Cube { + @location(4) matrix_0: vec4<f32>, + @location(5) matrix_1: vec4<f32>, + @location(6) matrix_2: vec4<f32>, + @location(7) matrix_3: vec4<f32>, + @location(8) normal_matrix_0: vec3<f32>, + @location(9) normal_matrix_1: vec3<f32>, + @location(10) normal_matrix_2: vec3<f32>, +} + +struct Output { + @builtin(position) clip_pos: vec4<f32>, + @location(0) uv: vec2<f32>, + @location(1) tangent_pos: vec3<f32>, + @location(2) tangent_camera_pos: vec3<f32>, + @location(3) tangent_light_pos: vec3<f32>, +} + +@vertex +fn vs_main(vertex: Vertex, cube: Cube) -> Output { + let cube_matrix = mat4x4<f32>( + cube.matrix_0, + cube.matrix_1, + cube.matrix_2, + cube.matrix_3, + ); + + let normal_matrix = mat3x3<f32>( + cube.normal_matrix_0, + cube.normal_matrix_1, + cube.normal_matrix_2, + ); + + //convert to tangent space to calculate lighting in same coordinate space as normal map sample + let tangent = normalize(normal_matrix * vertex.tangent); + let normal = normalize(normal_matrix * vertex.normal); + let bitangent = cross(tangent, normal); + + //shift everything into tangent space + let tbn = transpose(mat3x3<f32>(tangent, bitangent, normal)); + + let world_pos = cube_matrix * vec4<f32>(vertex.position, 1.0); + + var out: Output; + out.clip_pos = uniforms.projection * world_pos; + out.uv = vertex.uv; + out.tangent_pos = tbn * world_pos.xyz; + out.tangent_camera_pos = tbn * uniforms.camera_pos.xyz; + out.tangent_light_pos = tbn * LIGHT_POS; + + return out; +} + +//cube properties +const CUBE_BASE_COLOR: vec4<f32> = vec4<f32>(0.294118, 0.462745, 0.611765, 0.6); +const SHINE_DAMPER: f32 = 1.0; +const REFLECTIVITY: f32 = 0.8; +const REFRACTION_INDEX: f32 = 1.31; + +//fog, for the ~* cinematic effect *~ +const FOG_DENSITY: f32 = 0.15; +const FOG_GRADIENT: f32 = 8.0; +const FOG_COLOR: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0); + +@fragment +fn fs_main(in: Output) -> @location(0) vec4<f32> { + let to_camera = in.tangent_camera_pos - in.tangent_pos; + + //normal sample from texture + var normal = textureSample(normal_texture, tex_sampler, in.uv).xyz; + normal = normal * 2.0 - 1.0; + + //diffuse + let dir_to_light: vec3<f32> = normalize(in.tangent_light_pos - in.tangent_pos); + let brightness = max(dot(normal, dir_to_light), 0.0); + let diffuse: vec3<f32> = brightness * uniforms.light_color.xyz; + + //specular + let dir_to_camera = normalize(to_camera); + let light_dir = -dir_to_light; + let reflected_light_dir = reflect(light_dir, normal); + let specular_factor = max(dot(reflected_light_dir, dir_to_camera), 0.0); + let damped_factor = pow(specular_factor, SHINE_DAMPER); + let specular: vec3<f32> = damped_factor * uniforms.light_color.xyz * REFLECTIVITY; + + //fog + let distance = length(to_camera); + let visibility = clamp(exp(-pow((distance * FOG_DENSITY), FOG_GRADIENT)), 0.0, 1.0); + + //reflection + let reflection_dir = reflect(dir_to_camera, normal); + let reflection_color = textureSample(sky_texture, tex_sampler, reflection_dir); + let refraction_dir = refract(dir_to_camera, normal, REFRACTION_INDEX); + let refraction_color = textureSample(sky_texture, tex_sampler, refraction_dir); + let final_reflect_color = mix(reflection_color, refraction_color, 0.5); + + //mix it all together! + var color = vec4<f32>(CUBE_BASE_COLOR.xyz * diffuse + specular, CUBE_BASE_COLOR.w); + color = mix(color, final_reflect_color, 0.8); + color = mix(FOG_COLOR, color, visibility); + + return color; +} diff --git a/examples/custom_shader/src/shaders/depth.wgsl b/examples/custom_shader/src/shaders/depth.wgsl new file mode 100644 index 00000000..a3f7e5ec --- /dev/null +++ b/examples/custom_shader/src/shaders/depth.wgsl @@ -0,0 +1,48 @@ +var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>( + vec2<f32>(-1.0, 1.0), + vec2<f32>(-1.0, -1.0), + vec2<f32>(1.0, -1.0), + vec2<f32>(-1.0, 1.0), + vec2<f32>(1.0, 1.0), + vec2<f32>(1.0, -1.0) +); + +var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>( + vec2<f32>(0.0, 0.0), + vec2<f32>(0.0, 1.0), + vec2<f32>(1.0, 1.0), + vec2<f32>(0.0, 0.0), + vec2<f32>(1.0, 0.0), + vec2<f32>(1.0, 1.0) +); + +@group(0) @binding(0) var depth_sampler: sampler; +@group(0) @binding(1) var depth_texture: texture_2d<f32>; + +struct Output { + @builtin(position) position: vec4<f32>, + @location(0) uv: vec2<f32>, +} + +@vertex +fn vs_main(@builtin(vertex_index) v_index: u32) -> Output { + var out: Output; + + out.position = vec4<f32>(positions[v_index], 0.0, 1.0); + out.uv = uvs[v_index]; + + return out; +} + +@fragment +fn fs_main(input: Output) -> @location(0) vec4<f32> { + let depth = textureSample(depth_texture, depth_sampler, input.uv).r; + + if (depth > .9999) { + discard; + } + + let c = 1.0 - depth; + + return vec4<f32>(c, c, c, 1.0); +} diff --git a/examples/custom_shader/src/textures/ice_cube_normal_map.png b/examples/custom_shader/src/textures/ice_cube_normal_map.png new file mode 100644 index 00000000..7b4b7228 Binary files /dev/null and b/examples/custom_shader/src/textures/ice_cube_normal_map.png differ diff --git a/examples/custom_shader/src/textures/skybox/neg_x.jpg b/examples/custom_shader/src/textures/skybox/neg_x.jpg new file mode 100644 index 00000000..00cc783d Binary files /dev/null and b/examples/custom_shader/src/textures/skybox/neg_x.jpg differ diff --git a/examples/custom_shader/src/textures/skybox/neg_y.jpg b/examples/custom_shader/src/textures/skybox/neg_y.jpg new file mode 100644 index 00000000..548f6445 Binary files /dev/null and b/examples/custom_shader/src/textures/skybox/neg_y.jpg differ diff --git a/examples/custom_shader/src/textures/skybox/neg_z.jpg b/examples/custom_shader/src/textures/skybox/neg_z.jpg new file mode 100644 index 00000000..5698512e Binary files /dev/null and b/examples/custom_shader/src/textures/skybox/neg_z.jpg differ diff --git a/examples/custom_shader/src/textures/skybox/pos_x.jpg b/examples/custom_shader/src/textures/skybox/pos_x.jpg new file mode 100644 index 00000000..dddecba7 Binary files /dev/null and b/examples/custom_shader/src/textures/skybox/pos_x.jpg differ diff --git a/examples/custom_shader/src/textures/skybox/pos_y.jpg b/examples/custom_shader/src/textures/skybox/pos_y.jpg new file mode 100644 index 00000000..361427fd Binary files /dev/null and b/examples/custom_shader/src/textures/skybox/pos_y.jpg differ diff --git a/examples/custom_shader/src/textures/skybox/pos_z.jpg b/examples/custom_shader/src/textures/skybox/pos_z.jpg new file mode 100644 index 00000000..0085a49e Binary files /dev/null and b/examples/custom_shader/src/textures/skybox/pos_z.jpg differ diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index c26d52fe..0f32fca0 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -271,6 +271,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { &queue, &mut encoder, None, + frame.texture.format(), &view, primitive, &viewport, diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index a7aea352..6741d7cf 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -16,7 +16,6 @@ all-features = true [features] geometry = ["lyon_path"] -opengl = [] image = ["dep:image", "kamadak-exif"] web-colors = [] diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 78dec847..8c5ee2f0 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -7,6 +7,7 @@ pub mod compositor; pub mod geometry; mod settings; +pub mod widget; pub use iced_graphics as graphics; pub use iced_graphics::core; @@ -59,6 +60,26 @@ impl<T> Renderer<T> { } } } + + pub fn draw_custom<P: widget::shader::Primitive>( + &mut self, + bounds: Rectangle, + primitive: P, + ) { + match self { + Renderer::TinySkia(_) => { + log::warn!( + "Custom shader primitive is unavailable with tiny-skia." + ); + } + #[cfg(feature = "wgpu")] + Renderer::Wgpu(renderer) => { + renderer.draw_primitive(iced_wgpu::Primitive::Custom( + iced_wgpu::primitive::Custom::shader(bounds, primitive), + )) + } + } + } } impl<T> core::Renderer for Renderer<T> { diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs index 6c0c2a83..0422c99c 100644 --- a/renderer/src/widget.rs +++ b/renderer/src/widget.rs @@ -9,3 +9,6 @@ pub mod qr_code; #[cfg(feature = "qr_code")] pub use qr_code::QRCode; + +#[cfg(feature = "wgpu")] +pub mod shader; diff --git a/renderer/src/widget/shader.rs b/renderer/src/widget/shader.rs new file mode 100644 index 00000000..da42a7dd --- /dev/null +++ b/renderer/src/widget/shader.rs @@ -0,0 +1,215 @@ +//! A custom shader widget for wgpu applications. +use crate::core::event::Status; +use crate::core::layout::{Limits, Node}; +use crate::core::mouse::{Cursor, Interaction}; +use crate::core::renderer::Style; +use crate::core::widget::tree::{State, Tag}; +use crate::core::widget::{tree, Tree}; +use crate::core::{ + self, layout, mouse, widget, Clipboard, Element, Layout, Length, Rectangle, + Shell, Size, Widget, +}; +use std::marker::PhantomData; + +mod event; +mod program; + +pub use event::Event; +pub use iced_wgpu::custom::Primitive; +pub use iced_wgpu::custom::Storage; +pub use program::Program; + +/// A widget which can render custom shaders with Iced's `wgpu` backend. +/// +/// Must be initialized with a [`Program`], which describes the internal widget state & how +/// its [`Program::Primitive`]s are drawn. +#[allow(missing_debug_implementations)] +pub struct Shader<Message, P: Program<Message>> { + width: Length, + height: Length, + program: P, + _message: PhantomData<Message>, +} + +impl<Message, P: Program<Message>> Shader<Message, P> { + /// Create a new custom [`Shader`]. + pub fn new(program: P) -> Self { + Self { + width: Length::Fixed(100.0), + height: Length::Fixed(100.0), + program, + _message: PhantomData, + } + } + + /// Set the `width` of the custom [`Shader`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Set the `height` of the custom [`Shader`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } +} + +impl<P, Message, Theme> Widget<Message, crate::Renderer<Theme>> + for Shader<Message, P> +where + P: Program<Message>, +{ + fn tag(&self) -> Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<P::State>>() + } + + fn state(&self) -> State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _tree: &mut Tree, + _renderer: &crate::Renderer<Theme>, + limits: &Limits, + ) -> Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: crate::core::Event, + layout: Layout<'_>, + cursor: Cursor, + _renderer: &crate::Renderer<Theme>, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> Status { + let bounds = layout.bounds(); + + let custom_shader_event = match event { + core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), + core::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + _ => None, + }; + + if let Some(custom_shader_event) = custom_shader_event { + let state = tree.state.downcast_mut::<P::State>(); + + let (event_status, message) = self.program.update( + state, + custom_shader_event, + bounds, + cursor, + shell, + ); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer<Theme>, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut crate::Renderer<Theme>, + _theme: &Theme, + _style: &Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + renderer.draw_custom( + bounds, + self.program.draw(state, cursor_position, bounds), + ); + } +} + +impl<'a, M, P, Theme> From<Shader<M, P>> + for Element<'a, M, crate::Renderer<Theme>> +where + M: 'a, + P: Program<M> + 'a, +{ + fn from(custom: Shader<M, P>) -> Element<'a, M, crate::Renderer<Theme>> { + Element::new(custom) + } +} + +impl<Message, T> Program<Message> for &T +where + T: Program<Message>, +{ + type State = T::State; + type Primitive = T::Primitive; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + shell: &mut Shell<'_, Message>, + ) -> (Status, Option<Message>) { + T::update(self, state, event, bounds, cursor, shell) + } + + fn draw( + &self, + state: &Self::State, + cursor: Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + T::draw(self, state, cursor, bounds) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/renderer/src/widget/shader/event.rs b/renderer/src/widget/shader/event.rs new file mode 100644 index 00000000..981b30d7 --- /dev/null +++ b/renderer/src/widget/shader/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a custom shader widget. +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::touch; + +pub use crate::core::event::Status; + +/// A [`Shader`] event. +/// +/// [`Shader`]: crate::widget::shader::Shader; +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/renderer/src/widget/shader/program.rs b/renderer/src/widget/shader/program.rs new file mode 100644 index 00000000..b8871688 --- /dev/null +++ b/renderer/src/widget/shader/program.rs @@ -0,0 +1,60 @@ +use crate::core::{event, mouse, Rectangle, Shell}; +use crate::widget; +use widget::shader; + +/// The state and logic of a [`Shader`] widget. +/// +/// A [`Program`] can mutate the internal state of a [`Shader`] widget +/// and produce messages for an application. +/// +/// [`Shader`]: crate::widget::shader::Shader +pub trait Program<Message> { + /// The internal state of the [`Program`]. + type State: Default + 'static; + + /// The type of primitive this [`Program`] can draw. + type Primitive: shader::Primitive + 'static; + + /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes + /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a + /// redraw for the window, etc. + /// + /// By default, this method does and returns nothing. + /// + /// [`State`]: Self::State + fn update( + &self, + _state: &mut Self::State, + _event: shader::Event, + _bounds: Rectangle, + _cursor: mouse::Cursor, + _shell: &mut Shell<'_, Message>, + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, None) + } + + /// Draws the [`Primitive`]. + /// + /// [`Primitive`]: Self::Primitive + fn draw( + &self, + state: &Self::State, + cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position is out of + /// bounds of the [`Shader`]'s program. + /// + /// [`Shader`]: crate::widget::shader::Shader + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} diff --git a/style/src/theme.rs b/style/src/theme.rs index 47010728..cc31d72d 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -1,7 +1,7 @@ //! Use the built-in theme and styles. pub mod palette; -pub use palette::Palette; +pub use self::palette::Palette; use crate::application; use crate::button; diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 2bd29f42..907611d9 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -3,9 +3,7 @@ use crate::graphics::backend; use crate::graphics::color; use crate::graphics::{Transformation, Viewport}; use crate::primitive::{self, Primitive}; -use crate::quad; -use crate::text; -use crate::triangle; +use crate::{custom, quad, text, triangle}; use crate::{Layer, Settings}; #[cfg(feature = "tracing")] @@ -25,6 +23,7 @@ pub struct Backend { quad_pipeline: quad::Pipeline, text_pipeline: text::Pipeline, triangle_pipeline: triangle::Pipeline, + pipeline_storage: custom::Storage, #[cfg(any(feature = "image", feature = "svg"))] image_pipeline: image::Pipeline, @@ -50,6 +49,7 @@ impl Backend { quad_pipeline, text_pipeline, triangle_pipeline, + pipeline_storage: custom::Storage::default(), #[cfg(any(feature = "image", feature = "svg"))] image_pipeline, @@ -66,6 +66,7 @@ impl Backend { queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, clear_color: Option<Color>, + format: wgpu::TextureFormat, frame: &wgpu::TextureView, primitives: &[Primitive], viewport: &Viewport, @@ -88,6 +89,7 @@ impl Backend { self.prepare( device, queue, + format, encoder, scale_factor, target_size, @@ -117,6 +119,7 @@ impl Backend { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + format: wgpu::TextureFormat, _encoder: &mut wgpu::CommandEncoder, scale_factor: f32, target_size: Size<u32>, @@ -179,6 +182,20 @@ impl Backend { target_size, ); } + + if !layer.shaders.is_empty() { + for shader in &layer.shaders { + shader.primitive.prepare( + format, + device, + queue, + target_size, + scale_factor, + transformation, + &mut self.pipeline_storage, + ); + } + } } } @@ -302,6 +319,47 @@ impl Backend { text_layer += 1; } + + // kill render pass to let custom shaders get mut access to encoder + let _ = ManuallyDrop::into_inner(render_pass); + + if !layer.shaders.is_empty() { + for shader in &layer.shaders { + //This extra check is needed since each custom pipeline must set it's own + //scissor rect, which will panic if bounds.w/h < 1 + let bounds = shader.bounds * scale_factor; + + if bounds.width < 1.0 || bounds.height < 1.0 { + continue; + } + + shader.primitive.render( + &self.pipeline_storage, + bounds.into(), + target, + target_size, + encoder, + ); + } + } + + // recreate and continue processing layers + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::quad render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + }, + )], + depth_stencil_attachment: None, + }, + )); } let _ = ManuallyDrop::into_inner(render_pass); diff --git a/wgpu/src/custom.rs b/wgpu/src/custom.rs new file mode 100644 index 00000000..65dd0496 --- /dev/null +++ b/wgpu/src/custom.rs @@ -0,0 +1,66 @@ +use crate::core::{Rectangle, Size}; +use crate::graphics::Transformation; +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::fmt::Debug; + +/// Stores custom, user-provided pipelines. +#[derive(Default, Debug)] +pub struct Storage { + pipelines: HashMap<TypeId, Box<dyn Any>>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a pipeline with type `T`. + pub fn has<T: 'static>(&self) -> bool { + self.pipelines.get(&TypeId::of::<T>()).is_some() + } + + /// Inserts the pipeline `T` in to [`Storage`]. + pub fn store<T: 'static>(&mut self, pipeline: T) { + let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); + } + + /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. + pub fn get<T: 'static>(&self) -> Option<&T> { + self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_ref::<T>() + .expect("Pipeline with this type does not exist in Storage.") + }) + } + + /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. + pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { + self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_mut::<T>() + .expect("Pipeline with this type does not exist in Storage.") + }) + } +} + +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + Send + Sync + 'static { + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + scale_factor: f32, + transform: Transformation, + storage: &mut Storage, + ); + + /// Renders the [`Primitive`]. + fn render( + &self, + storage: &Storage, + bounds: Rectangle<u32>, + target: &wgpu::TextureView, + target_size: Size<u32>, + encoder: &mut wgpu::CommandEncoder, + ); +} diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 286801e6..d451cbfd 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -34,6 +34,9 @@ pub struct Layer<'a> { /// The images of the [`Layer`]. pub images: Vec<Image>, + + /// The custom shader primitives of this [`Layer`]. + pub shaders: Vec<primitive::Shader>, } impl<'a> Layer<'a> { @@ -45,6 +48,7 @@ impl<'a> Layer<'a> { meshes: Vec::new(), text: Vec::new(), images: Vec::new(), + shaders: Vec::new(), } } @@ -308,6 +312,18 @@ impl<'a> Layer<'a> { } } }, + primitive::Custom::Shader(shader) => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + shader.bounds.size(), + ); + + if layer.bounds.intersection(&bounds).is_some() { + layer.shaders.push(shader.clone()); + } + } }, } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 424dfeb3..13d8e886 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -29,6 +29,7 @@ rustdoc::broken_intra_doc_links )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +pub mod custom; pub mod layer; pub mod primitive; pub mod settings; diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 8dbf3008..4347dcda 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,6 +1,10 @@ //! Draw using different graphical primitives. use crate::core::Rectangle; +use crate::custom; use crate::graphics::{Damage, Mesh}; +use std::any::Any; +use std::fmt::Debug; +use std::sync::Arc; /// The graphical primitives supported by `iced_wgpu`. pub type Primitive = crate::graphics::Primitive<Custom>; @@ -10,12 +14,44 @@ pub type Primitive = crate::graphics::Primitive<Custom>; pub enum Custom { /// A mesh primitive. Mesh(Mesh), + /// A custom shader primitive + Shader(Shader), +} + +impl Custom { + /// Create a custom [`Shader`] primitive. + pub fn shader<P: custom::Primitive>( + bounds: Rectangle, + primitive: P, + ) -> Self { + Self::Shader(Shader { + bounds, + primitive: Arc::new(primitive), + }) + } } impl Damage for Custom { fn bounds(&self) -> Rectangle { match self { Self::Mesh(mesh) => mesh.bounds(), + Self::Shader(shader) => shader.bounds, } } } + +#[derive(Clone, Debug)] +/// A custom primitive which can be used to render primitives associated with a custom pipeline. +pub struct Shader { + /// The bounds of the [`Shader`]. + pub bounds: Rectangle, + + /// The [`custom::Primitive`] to render. + pub primitive: Arc<dyn custom::Primitive>, +} + +impl PartialEq for Shader { + fn eq(&self, other: &Self) -> bool { + self.primitive.type_id() == other.primitive.type_id() + } +} diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 1ddbe5fe..90d64e17 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -178,6 +178,7 @@ pub fn present<Theme, T: AsRef<str>>( &compositor.queue, &mut encoder, Some(background_color), + frame.texture.format(), view, primitives, viewport, @@ -357,6 +358,7 @@ pub fn screenshot<Theme, T: AsRef<str>>( &compositor.queue, &mut encoder, Some(background_color), + texture.format(), &view, primitives, viewport, diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 6d62c181..032f801c 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -20,6 +20,7 @@ image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] +wgpu = [] [dependencies] iced_renderer.workspace = true diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 2f959370..e052f398 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -97,6 +97,9 @@ pub use tooltip::Tooltip; #[doc(no_inline)] pub use vertical_slider::VerticalSlider; +#[cfg(feature = "wgpu")] +pub use renderer::widget::shader::{self, Shader}; + #[cfg(feature = "svg")] pub mod svg; -- cgit From 36e85215932079fa324cfefb620602ad79f7df3d Mon Sep 17 00:00:00 2001 From: Bingus <shankern@protonmail.com> Date: Mon, 18 Sep 2023 09:04:28 -0700 Subject: Removed `Into` for Rectangle<f32> from u32 --- core/src/rectangle.rs | 11 ----------- wgpu/src/backend.rs | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index d5437d51..c1c2eeac 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -183,17 +183,6 @@ impl From<Rectangle<u32>> for Rectangle<f32> { } } -impl From<Rectangle<f32>> for Rectangle<u32> { - fn from(rectangle: Rectangle<f32>) -> Self { - Rectangle { - x: rectangle.x as u32, - y: rectangle.y as u32, - width: rectangle.width as u32, - height: rectangle.height as u32, - } - } -} - impl<T> std::ops::Add<Vector<T>> for Rectangle<T> where T: std::ops::Add<Output = T>, diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 907611d9..ace2ef95 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -335,7 +335,7 @@ impl Backend { shader.primitive.render( &self.pipeline_storage, - bounds.into(), + bounds.snap(), target, target_size, encoder, -- cgit From 91fca024b629b7ddf4de533a75f01593954362f6 Mon Sep 17 00:00:00 2001 From: Bingus <shankern@protonmail.com> Date: Thu, 21 Sep 2023 13:24:54 -0700 Subject: Reexport Transformation from widget::shader --- renderer/src/widget/shader.rs | 1 + widget/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/renderer/src/widget/shader.rs b/renderer/src/widget/shader.rs index da42a7dd..85fc13c8 100644 --- a/renderer/src/widget/shader.rs +++ b/renderer/src/widget/shader.rs @@ -18,6 +18,7 @@ pub use event::Event; pub use iced_wgpu::custom::Primitive; pub use iced_wgpu::custom::Storage; pub use program::Program; +pub use iced_graphics::Transformation; /// A widget which can render custom shaders with Iced's `wgpu` backend. /// diff --git a/widget/src/lib.rs b/widget/src/lib.rs index e052f398..5220e83a 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -98,7 +98,7 @@ pub use tooltip::Tooltip; pub use vertical_slider::VerticalSlider; #[cfg(feature = "wgpu")] -pub use renderer::widget::shader::{self, Shader}; +pub use renderer::widget::shader::{self, Shader, Transformation}; #[cfg(feature = "svg")] pub mod svg; -- cgit From 65f4ff060a36c6dc3afea20d75f541d72460d333 Mon Sep 17 00:00:00 2001 From: Bingus <shankern@protonmail.com> Date: Thu, 28 Sep 2023 09:48:38 -0700 Subject: Added redraw request handling to widget events. --- renderer/src/widget/shader.rs | 8 ++++---- renderer/src/widget/shader/event.rs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/renderer/src/widget/shader.rs b/renderer/src/widget/shader.rs index 85fc13c8..385fa981 100644 --- a/renderer/src/widget/shader.rs +++ b/renderer/src/widget/shader.rs @@ -5,10 +5,7 @@ use crate::core::mouse::{Cursor, Interaction}; use crate::core::renderer::Style; use crate::core::widget::tree::{State, Tag}; use crate::core::widget::{tree, Tree}; -use crate::core::{ - self, layout, mouse, widget, Clipboard, Element, Layout, Length, Rectangle, - Shell, Size, Widget, -}; +use crate::core::{self, layout, mouse, widget, Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget, window}; use std::marker::PhantomData; mod event; @@ -109,6 +106,9 @@ where Some(Event::Keyboard(keyboard_event)) } core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + core::Event::Window(window::Event::RedrawRequested(instant)) => { + Some(Event::RedrawRequested(instant)) + } _ => None, }; diff --git a/renderer/src/widget/shader/event.rs b/renderer/src/widget/shader/event.rs index 981b30d7..c1696580 100644 --- a/renderer/src/widget/shader/event.rs +++ b/renderer/src/widget/shader/event.rs @@ -1,4 +1,5 @@ //! Handle events of a custom shader widget. +use std::time::Instant; use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -18,4 +19,7 @@ pub enum Event { /// A keyboard event. Keyboard(keyboard::Event), + + /// A window requested a redraw. + RedrawRequested(Instant), } -- cgit From de9420e7df7d909bca611c360182dec54c5b1aae Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 11:33:04 +0100 Subject: Fix latest `wgpu` changes --- wgpu/src/backend.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index ace2ef95..27ef0b3c 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -353,11 +353,13 @@ impl Backend { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, }, )], depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, }, )); } -- cgit From 46a48af97fa472e1158e07d4deb988c5601197e0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 11:34:15 +0100 Subject: Write missing documentation for `custom` module in `iced_wgpu` --- wgpu/src/custom.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/wgpu/src/custom.rs b/wgpu/src/custom.rs index 65dd0496..65a6f133 100644 --- a/wgpu/src/custom.rs +++ b/wgpu/src/custom.rs @@ -1,3 +1,4 @@ +//! Draw custom primitives. use crate::core::{Rectangle, Size}; use crate::graphics::Transformation; use std::any::{Any, TypeId}; -- cgit From 3e8ed05356dde17a6e31a0dc927a3c19b593b09a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 11:38:16 +0100 Subject: Update `wgpu` in `custom_shader` example --- examples/custom_shader/Cargo.toml | 15 ++++++++++----- examples/custom_shader/src/pipeline.rs | 10 +++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml index 7a927811..0b8466a9 100644 --- a/examples/custom_shader/Cargo.toml +++ b/examples/custom_shader/Cargo.toml @@ -5,9 +5,14 @@ authors = ["Bingus <shankern@protonmail.com>"] edition = "2021" [dependencies] -iced = { path = "../..", features = ["debug", "advanced"]} -image = { version = "0.24.6"} -wgpu = "0.17" -bytemuck = { version = "1.13.1" } -glam = { version = "0.24.0", features = ["bytemuck"] } +iced.workspace = true +iced.features = ["debug", "advanced"] + +image.workspace = true +wgpu.workspace = true +bytemuck.workspace = true + +glam.workspace = true +glam.features = ["bytemuck"] + rand = "0.8.5" diff --git a/examples/custom_shader/src/pipeline.rs b/examples/custom_shader/src/pipeline.rs index 9dd154e8..eef1081d 100644 --- a/examples/custom_shader/src/pipeline.rs +++ b/examples/custom_shader/src/pipeline.rs @@ -355,7 +355,7 @@ impl Pipeline { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, }, )], @@ -364,11 +364,13 @@ impl Pipeline { view: &self.depth_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), - store: true, + store: wgpu::StoreOp::Store, }), stencil_ops: None, }, ), + timestamp_writes: None, + occlusion_query_set: None, }); pass.set_scissor_rect( @@ -547,7 +549,7 @@ impl DepthPipeline { resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: Some( @@ -557,6 +559,8 @@ impl DepthPipeline { stencil_ops: None, }, ), + timestamp_writes: None, + occlusion_query_set: None, }); pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height); -- cgit From 33f626294452aefd1cd04f455fa1d1dfcb7f549e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 11:43:38 +0100 Subject: Fix `clippy` lints :crab: --- examples/custom_shader/src/cubes.rs | 2 +- examples/custom_shader/src/pipeline.rs | 8 ++++---- examples/custom_shader/src/primitive.rs | 4 ++-- renderer/src/lib.rs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/custom_shader/src/cubes.rs b/examples/custom_shader/src/cubes.rs index 8dbba4b1..00e608e3 100644 --- a/examples/custom_shader/src/cubes.rs +++ b/examples/custom_shader/src/cubes.rs @@ -65,7 +65,7 @@ impl Cubes { let new_len = self.cubes.len() - cubes_2_cut as usize; self.cubes.truncate(new_len); } - _ => {} + Ordering::Equal => {} } } } diff --git a/examples/custom_shader/src/pipeline.rs b/examples/custom_shader/src/pipeline.rs index eef1081d..44ad17a2 100644 --- a/examples/custom_shader/src/pipeline.rs +++ b/examples/custom_shader/src/pipeline.rs @@ -479,15 +479,15 @@ impl DepthPipeline { entry_point: "vs_main", buffers: &[], }, - primitive: Default::default(), + primitive: wgpu::PrimitiveState::default(), depth_stencil: Some(wgpu::DepthStencilState { format: wgpu::TextureFormat::Depth32Float, depth_write_enabled: false, depth_compare: wgpu::CompareFunction::Less, - stencil: Default::default(), - bias: Default::default(), + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), }), - multisample: Default::default(), + multisample: wgpu::MultisampleState::default(), fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", diff --git a/examples/custom_shader/src/primitive.rs b/examples/custom_shader/src/primitive.rs index 2201218f..f81ce95d 100644 --- a/examples/custom_shader/src/primitive.rs +++ b/examples/custom_shader/src/primitive.rs @@ -56,7 +56,7 @@ impl shader::Primitive for Primitive { storage: &mut shader::Storage, ) { if !storage.has::<Pipeline>() { - storage.store(Pipeline::new(device, queue, format, target_size)) + storage.store(Pipeline::new(device, queue, format, target_size)); } let pipeline = storage.get_mut::<Pipeline>().unwrap(); @@ -90,6 +90,6 @@ impl shader::Primitive for Primitive { bounds, self.cubes.len() as u32, self.show_depth_buffer, - ) + ); } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 8c5ee2f0..e4b1eda9 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -76,7 +76,7 @@ impl<T> Renderer<T> { Renderer::Wgpu(renderer) => { renderer.draw_primitive(iced_wgpu::Primitive::Custom( iced_wgpu::primitive::Custom::shader(bounds, primitive), - )) + )); } } } -- cgit From 226eac35c3fa35be328f6390fdf2a52a38ed2b0f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 11:51:04 +0100 Subject: Remove old `widget` modules in `iced_renderer` --- renderer/src/widget.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs index 0422c99c..4b7dad5d 100644 --- a/renderer/src/widget.rs +++ b/renderer/src/widget.rs @@ -1,14 +1,2 @@ -#[cfg(feature = "canvas")] -pub mod canvas; - -#[cfg(feature = "canvas")] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -pub use qr_code::QRCode; - #[cfg(feature = "wgpu")] pub mod shader; -- cgit From 2dda9132cda6d2a2279759f3447bae4e1c277555 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 11:52:34 +0100 Subject: Run `cargo fmt` --- renderer/src/widget/shader.rs | 7 +++++-- renderer/src/widget/shader/event.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/renderer/src/widget/shader.rs b/renderer/src/widget/shader.rs index 385fa981..e218f35a 100644 --- a/renderer/src/widget/shader.rs +++ b/renderer/src/widget/shader.rs @@ -5,17 +5,20 @@ use crate::core::mouse::{Cursor, Interaction}; use crate::core::renderer::Style; use crate::core::widget::tree::{State, Tag}; use crate::core::widget::{tree, Tree}; -use crate::core::{self, layout, mouse, widget, Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget, window}; +use crate::core::{ + self, layout, mouse, widget, window, Clipboard, Element, Layout, Length, + Rectangle, Shell, Size, Widget, +}; use std::marker::PhantomData; mod event; mod program; pub use event::Event; +pub use iced_graphics::Transformation; pub use iced_wgpu::custom::Primitive; pub use iced_wgpu::custom::Storage; pub use program::Program; -pub use iced_graphics::Transformation; /// A widget which can render custom shaders with Iced's `wgpu` backend. /// diff --git a/renderer/src/widget/shader/event.rs b/renderer/src/widget/shader/event.rs index c1696580..8901fb31 100644 --- a/renderer/src/widget/shader/event.rs +++ b/renderer/src/widget/shader/event.rs @@ -1,8 +1,8 @@ //! Handle events of a custom shader widget. -use std::time::Instant; use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; +use std::time::Instant; pub use crate::core::event::Status; -- cgit From 9489e29e6619b14ed9f41a8887c4b34158266f71 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 12:49:49 +0100 Subject: Re-organize `custom` module as `pipeline` module ... and move `Shader` widget to `iced_widget` crate --- examples/custom_shader/src/main.rs | 11 +- examples/custom_shader/src/primitive.rs | 15 ++- renderer/src/lib.rs | 44 ++++--- renderer/src/widget.rs | 2 - renderer/src/widget/shader.rs | 219 -------------------------------- renderer/src/widget/shader/event.rs | 25 ---- renderer/src/widget/shader/program.rs | 60 --------- wgpu/src/backend.rs | 29 +++-- wgpu/src/custom.rs | 63 +-------- wgpu/src/layer.rs | 19 ++- wgpu/src/lib.rs | 1 - wgpu/src/primitive.rs | 43 ++----- wgpu/src/primitive/pipeline.rs | 117 +++++++++++++++++ widget/src/lib.rs | 6 +- widget/src/shader.rs | 219 ++++++++++++++++++++++++++++++++ widget/src/shader/event.rs | 26 ++++ widget/src/shader/program.rs | 62 +++++++++ 17 files changed, 505 insertions(+), 456 deletions(-) delete mode 100644 renderer/src/widget.rs delete mode 100644 renderer/src/widget/shader.rs delete mode 100644 renderer/src/widget/shader/event.rs delete mode 100644 renderer/src/widget/shader/program.rs create mode 100644 wgpu/src/primitive/pipeline.rs create mode 100644 widget/src/shader.rs create mode 100644 widget/src/shader/event.rs create mode 100644 widget/src/shader/program.rs diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 76fa1625..f6370f46 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -3,15 +3,20 @@ mod cubes; mod pipeline; mod primitive; +use crate::camera::Camera; use crate::cubes::Cubes; +use crate::pipeline::Pipeline; + +use iced::executor; +use iced::time::Instant; use iced::widget::{ checkbox, column, container, row, slider, text, vertical_space, Shader, }; +use iced::window; use iced::{ - executor, window, Alignment, Application, Color, Command, Element, Length, - Renderer, Subscription, Theme, + Alignment, Application, Color, Command, Element, Length, Renderer, + Subscription, Theme, }; -use std::time::Instant; fn main() -> iced::Result { IcedCubes::run(iced::Settings::default()) diff --git a/examples/custom_shader/src/primitive.rs b/examples/custom_shader/src/primitive.rs index f81ce95d..520ceb8e 100644 --- a/examples/custom_shader/src/primitive.rs +++ b/examples/custom_shader/src/primitive.rs @@ -4,17 +4,18 @@ pub mod vertex; mod buffer; mod uniforms; -use crate::camera::Camera; -use crate::pipeline::Pipeline; -use crate::primitive::cube::Cube; +pub use buffer::Buffer; +pub use cube::Cube; +pub use uniforms::Uniforms; +pub use vertex::Vertex; + +use crate::Camera; +use crate::Pipeline; + use iced::advanced::graphics::Transformation; use iced::widget::shader; use iced::{Color, Rectangle, Size}; -pub use crate::primitive::vertex::Vertex; -pub use buffer::Buffer; -pub use uniforms::Uniforms; - /// A collection of `Cube`s that can be rendered. #[derive(Debug)] pub struct Primitive { diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index e4b1eda9..1fc4c86b 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,13 +1,15 @@ #![forbid(rust_2018_idioms)] #![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#[cfg(feature = "wgpu")] +pub use iced_wgpu as wgpu; + pub mod compositor; #[cfg(feature = "geometry")] pub mod geometry; mod settings; -pub mod widget; pub use iced_graphics as graphics; pub use iced_graphics::core; @@ -60,26 +62,6 @@ impl<T> Renderer<T> { } } } - - pub fn draw_custom<P: widget::shader::Primitive>( - &mut self, - bounds: Rectangle, - primitive: P, - ) { - match self { - Renderer::TinySkia(_) => { - log::warn!( - "Custom shader primitive is unavailable with tiny-skia." - ); - } - #[cfg(feature = "wgpu")] - Renderer::Wgpu(renderer) => { - renderer.draw_primitive(iced_wgpu::Primitive::Custom( - iced_wgpu::primitive::Custom::shader(bounds, primitive), - )); - } - } - } } impl<T> core::Renderer for Renderer<T> { @@ -292,3 +274,23 @@ impl<T> crate::graphics::geometry::Renderer for Renderer<T> { } } } + +#[cfg(feature = "wgpu")] +impl<T> iced_wgpu::primitive::pipeline::Renderer for Renderer<T> { + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl wgpu::primitive::pipeline::Primitive, + ) { + match self { + Self::TinySkia(_renderer) => { + log::warn!( + "Custom shader primitive is unavailable with tiny-skia." + ); + } + Self::Wgpu(renderer) => { + renderer.draw_pipeline_primitive(bounds, primitive); + } + } + } +} diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs deleted file mode 100644 index 4b7dad5d..00000000 --- a/renderer/src/widget.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "wgpu")] -pub mod shader; diff --git a/renderer/src/widget/shader.rs b/renderer/src/widget/shader.rs deleted file mode 100644 index e218f35a..00000000 --- a/renderer/src/widget/shader.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! A custom shader widget for wgpu applications. -use crate::core::event::Status; -use crate::core::layout::{Limits, Node}; -use crate::core::mouse::{Cursor, Interaction}; -use crate::core::renderer::Style; -use crate::core::widget::tree::{State, Tag}; -use crate::core::widget::{tree, Tree}; -use crate::core::{ - self, layout, mouse, widget, window, Clipboard, Element, Layout, Length, - Rectangle, Shell, Size, Widget, -}; -use std::marker::PhantomData; - -mod event; -mod program; - -pub use event::Event; -pub use iced_graphics::Transformation; -pub use iced_wgpu::custom::Primitive; -pub use iced_wgpu::custom::Storage; -pub use program::Program; - -/// A widget which can render custom shaders with Iced's `wgpu` backend. -/// -/// Must be initialized with a [`Program`], which describes the internal widget state & how -/// its [`Program::Primitive`]s are drawn. -#[allow(missing_debug_implementations)] -pub struct Shader<Message, P: Program<Message>> { - width: Length, - height: Length, - program: P, - _message: PhantomData<Message>, -} - -impl<Message, P: Program<Message>> Shader<Message, P> { - /// Create a new custom [`Shader`]. - pub fn new(program: P) -> Self { - Self { - width: Length::Fixed(100.0), - height: Length::Fixed(100.0), - program, - _message: PhantomData, - } - } - - /// Set the `width` of the custom [`Shader`]. - pub fn width(mut self, width: impl Into<Length>) -> Self { - self.width = width.into(); - self - } - - /// Set the `height` of the custom [`Shader`]. - pub fn height(mut self, height: impl Into<Length>) -> Self { - self.height = height.into(); - self - } -} - -impl<P, Message, Theme> Widget<Message, crate::Renderer<Theme>> - for Shader<Message, P> -where - P: Program<Message>, -{ - fn tag(&self) -> Tag { - struct Tag<T>(T); - tree::Tag::of::<Tag<P::State>>() - } - - fn state(&self) -> State { - tree::State::new(P::State::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _tree: &mut Tree, - _renderer: &crate::Renderer<Theme>, - limits: &Limits, - ) -> Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: crate::core::Event, - layout: Layout<'_>, - cursor: Cursor, - _renderer: &crate::Renderer<Theme>, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) -> Status { - let bounds = layout.bounds(); - - let custom_shader_event = match event { - core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - core::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Window(window::Event::RedrawRequested(instant)) => { - Some(Event::RedrawRequested(instant)) - } - _ => None, - }; - - if let Some(custom_shader_event) = custom_shader_event { - let state = tree.state.downcast_mut::<P::State>(); - - let (event_status, message) = self.program.update( - state, - custom_shader_event, - bounds, - cursor, - shell, - ); - - if let Some(message) = message { - shell.publish(message); - } - - return event_status; - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: Cursor, - _viewport: &Rectangle, - _renderer: &crate::Renderer<Theme>, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let state = tree.state.downcast_ref::<P::State>(); - - self.program.mouse_interaction(state, bounds, cursor) - } - - fn draw( - &self, - tree: &widget::Tree, - renderer: &mut crate::Renderer<Theme>, - _theme: &Theme, - _style: &Style, - layout: Layout<'_>, - cursor_position: mouse::Cursor, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let state = tree.state.downcast_ref::<P::State>(); - - renderer.draw_custom( - bounds, - self.program.draw(state, cursor_position, bounds), - ); - } -} - -impl<'a, M, P, Theme> From<Shader<M, P>> - for Element<'a, M, crate::Renderer<Theme>> -where - M: 'a, - P: Program<M> + 'a, -{ - fn from(custom: Shader<M, P>) -> Element<'a, M, crate::Renderer<Theme>> { - Element::new(custom) - } -} - -impl<Message, T> Program<Message> for &T -where - T: Program<Message>, -{ - type State = T::State; - type Primitive = T::Primitive; - - fn update( - &self, - state: &mut Self::State, - event: Event, - bounds: Rectangle, - cursor: Cursor, - shell: &mut Shell<'_, Message>, - ) -> (Status, Option<Message>) { - T::update(self, state, event, bounds, cursor, shell) - } - - fn draw( - &self, - state: &Self::State, - cursor: Cursor, - bounds: Rectangle, - ) -> Self::Primitive { - T::draw(self, state, cursor, bounds) - } - - fn mouse_interaction( - &self, - state: &Self::State, - bounds: Rectangle, - cursor: Cursor, - ) -> Interaction { - T::mouse_interaction(self, state, bounds, cursor) - } -} diff --git a/renderer/src/widget/shader/event.rs b/renderer/src/widget/shader/event.rs deleted file mode 100644 index 8901fb31..00000000 --- a/renderer/src/widget/shader/event.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Handle events of a custom shader widget. -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::touch; -use std::time::Instant; - -pub use crate::core::event::Status; - -/// A [`Shader`] event. -/// -/// [`Shader`]: crate::widget::shader::Shader; -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), - - /// A window requested a redraw. - RedrawRequested(Instant), -} diff --git a/renderer/src/widget/shader/program.rs b/renderer/src/widget/shader/program.rs deleted file mode 100644 index b8871688..00000000 --- a/renderer/src/widget/shader/program.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::core::{event, mouse, Rectangle, Shell}; -use crate::widget; -use widget::shader; - -/// The state and logic of a [`Shader`] widget. -/// -/// A [`Program`] can mutate the internal state of a [`Shader`] widget -/// and produce messages for an application. -/// -/// [`Shader`]: crate::widget::shader::Shader -pub trait Program<Message> { - /// The internal state of the [`Program`]. - type State: Default + 'static; - - /// The type of primitive this [`Program`] can draw. - type Primitive: shader::Primitive + 'static; - - /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes - /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a - /// redraw for the window, etc. - /// - /// By default, this method does and returns nothing. - /// - /// [`State`]: Self::State - fn update( - &self, - _state: &mut Self::State, - _event: shader::Event, - _bounds: Rectangle, - _cursor: mouse::Cursor, - _shell: &mut Shell<'_, Message>, - ) -> (event::Status, Option<Message>) { - (event::Status::Ignored, None) - } - - /// Draws the [`Primitive`]. - /// - /// [`Primitive`]: Self::Primitive - fn draw( - &self, - state: &Self::State, - cursor: mouse::Cursor, - bounds: Rectangle, - ) -> Self::Primitive; - - /// Returns the current mouse interaction of the [`Program`]. - /// - /// The interaction returned will be in effect even if the cursor position is out of - /// bounds of the [`Shader`]'s program. - /// - /// [`Shader`]: crate::widget::shader::Shader - fn mouse_interaction( - &self, - _state: &Self::State, - _bounds: Rectangle, - _cursor: mouse::Cursor, - ) -> mouse::Interaction { - mouse::Interaction::default() - } -} diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 27ef0b3c..f89bcee1 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -2,8 +2,11 @@ use crate::core::{Color, Size}; use crate::graphics::backend; use crate::graphics::color; use crate::graphics::{Transformation, Viewport}; +use crate::primitive::pipeline; use crate::primitive::{self, Primitive}; -use crate::{custom, quad, text, triangle}; +use crate::quad; +use crate::text; +use crate::triangle; use crate::{Layer, Settings}; #[cfg(feature = "tracing")] @@ -23,7 +26,7 @@ pub struct Backend { quad_pipeline: quad::Pipeline, text_pipeline: text::Pipeline, triangle_pipeline: triangle::Pipeline, - pipeline_storage: custom::Storage, + pipeline_storage: pipeline::Storage, #[cfg(any(feature = "image", feature = "svg"))] image_pipeline: image::Pipeline, @@ -49,7 +52,7 @@ impl Backend { quad_pipeline, text_pipeline, triangle_pipeline, - pipeline_storage: custom::Storage::default(), + pipeline_storage: pipeline::Storage::default(), #[cfg(any(feature = "image", feature = "svg"))] image_pipeline, @@ -183,9 +186,9 @@ impl Backend { ); } - if !layer.shaders.is_empty() { - for shader in &layer.shaders { - shader.primitive.prepare( + if !layer.pipelines.is_empty() { + for pipeline in &layer.pipelines { + pipeline.primitive.prepare( format, device, queue, @@ -323,19 +326,17 @@ impl Backend { // kill render pass to let custom shaders get mut access to encoder let _ = ManuallyDrop::into_inner(render_pass); - if !layer.shaders.is_empty() { - for shader in &layer.shaders { - //This extra check is needed since each custom pipeline must set it's own - //scissor rect, which will panic if bounds.w/h < 1 - let bounds = shader.bounds * scale_factor; + if !layer.pipelines.is_empty() { + for pipeline in &layer.pipelines { + let bounds = (pipeline.bounds * scale_factor).snap(); - if bounds.width < 1.0 || bounds.height < 1.0 { + if bounds.width < 1 || bounds.height < 1 { continue; } - shader.primitive.render( + pipeline.primitive.render( &self.pipeline_storage, - bounds.snap(), + bounds, target, target_size, encoder, diff --git a/wgpu/src/custom.rs b/wgpu/src/custom.rs index 65a6f133..98e2b396 100644 --- a/wgpu/src/custom.rs +++ b/wgpu/src/custom.rs @@ -1,67 +1,8 @@ //! Draw custom primitives. use crate::core::{Rectangle, Size}; use crate::graphics::Transformation; +use crate::primitive; + use std::any::{Any, TypeId}; use std::collections::HashMap; use std::fmt::Debug; - -/// Stores custom, user-provided pipelines. -#[derive(Default, Debug)] -pub struct Storage { - pipelines: HashMap<TypeId, Box<dyn Any>>, -} - -impl Storage { - /// Returns `true` if `Storage` contains a pipeline with type `T`. - pub fn has<T: 'static>(&self) -> bool { - self.pipelines.get(&TypeId::of::<T>()).is_some() - } - - /// Inserts the pipeline `T` in to [`Storage`]. - pub fn store<T: 'static>(&mut self, pipeline: T) { - let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); - } - - /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. - pub fn get<T: 'static>(&self) -> Option<&T> { - self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { - pipeline - .downcast_ref::<T>() - .expect("Pipeline with this type does not exist in Storage.") - }) - } - - /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. - pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { - self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { - pipeline - .downcast_mut::<T>() - .expect("Pipeline with this type does not exist in Storage.") - }) - } -} - -/// A set of methods which allows a [`Primitive`] to be rendered. -pub trait Primitive: Debug + Send + Sync + 'static { - /// Processes the [`Primitive`], allowing for GPU buffer allocation. - fn prepare( - &self, - format: wgpu::TextureFormat, - device: &wgpu::Device, - queue: &wgpu::Queue, - target_size: Size<u32>, - scale_factor: f32, - transform: Transformation, - storage: &mut Storage, - ); - - /// Renders the [`Primitive`]. - fn render( - &self, - storage: &Storage, - bounds: Rectangle<u32>, - target: &wgpu::TextureView, - target_size: Size<u32>, - encoder: &mut wgpu::CommandEncoder, - ); -} diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index d451cbfd..33aaf670 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -35,8 +35,8 @@ pub struct Layer<'a> { /// The images of the [`Layer`]. pub images: Vec<Image>, - /// The custom shader primitives of this [`Layer`]. - pub shaders: Vec<primitive::Shader>, + /// The custom pipelines of this [`Layer`]. + pub pipelines: Vec<primitive::Pipeline>, } impl<'a> Layer<'a> { @@ -48,7 +48,7 @@ impl<'a> Layer<'a> { meshes: Vec::new(), text: Vec::new(), images: Vec::new(), - shaders: Vec::new(), + pipelines: Vec::new(), } } @@ -312,16 +312,21 @@ impl<'a> Layer<'a> { } } }, - primitive::Custom::Shader(shader) => { + primitive::Custom::Pipeline(pipeline) => { let layer = &mut layers[current_layer]; let bounds = Rectangle::new( Point::new(translation.x, translation.y), - shader.bounds.size(), + pipeline.bounds.size(), ); - if layer.bounds.intersection(&bounds).is_some() { - layer.shaders.push(shader.clone()); + if let Some(clip_bounds) = + layer.bounds.intersection(&bounds) + { + layer.pipelines.push(primitive::Pipeline { + bounds: clip_bounds, + primitive: pipeline.primitive.clone(), + }); } } }, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 13d8e886..424dfeb3 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -29,7 +29,6 @@ rustdoc::broken_intra_doc_links )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -pub mod custom; pub mod layer; pub mod primitive; pub mod settings; diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs index 4347dcda..fff927ea 100644 --- a/wgpu/src/primitive.rs +++ b/wgpu/src/primitive.rs @@ -1,10 +1,12 @@ //! Draw using different graphical primitives. +pub mod pipeline; + +pub use pipeline::Pipeline; + use crate::core::Rectangle; -use crate::custom; use crate::graphics::{Damage, Mesh}; -use std::any::Any; + use std::fmt::Debug; -use std::sync::Arc; /// The graphical primitives supported by `iced_wgpu`. pub type Primitive = crate::graphics::Primitive<Custom>; @@ -14,44 +16,15 @@ pub type Primitive = crate::graphics::Primitive<Custom>; pub enum Custom { /// A mesh primitive. Mesh(Mesh), - /// A custom shader primitive - Shader(Shader), -} - -impl Custom { - /// Create a custom [`Shader`] primitive. - pub fn shader<P: custom::Primitive>( - bounds: Rectangle, - primitive: P, - ) -> Self { - Self::Shader(Shader { - bounds, - primitive: Arc::new(primitive), - }) - } + /// A custom pipeline primitive. + Pipeline(Pipeline), } impl Damage for Custom { fn bounds(&self) -> Rectangle { match self { Self::Mesh(mesh) => mesh.bounds(), - Self::Shader(shader) => shader.bounds, + Self::Pipeline(pipeline) => pipeline.bounds, } } } - -#[derive(Clone, Debug)] -/// A custom primitive which can be used to render primitives associated with a custom pipeline. -pub struct Shader { - /// The bounds of the [`Shader`]. - pub bounds: Rectangle, - - /// The [`custom::Primitive`] to render. - pub primitive: Arc<dyn custom::Primitive>, -} - -impl PartialEq for Shader { - fn eq(&self, other: &Self) -> bool { - self.primitive.type_id() == other.primitive.type_id() - } -} diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs new file mode 100644 index 00000000..b59aeff1 --- /dev/null +++ b/wgpu/src/primitive/pipeline.rs @@ -0,0 +1,117 @@ +//! Draw primitives using custom pipelines. +use crate::core::{Rectangle, Size}; +use crate::graphics::Transformation; + +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Arc; + +#[derive(Clone, Debug)] +/// A custom primitive which can be used to render primitives associated with a custom pipeline. +pub struct Pipeline { + /// The bounds of the [`Shader`]. + pub bounds: Rectangle, + + /// The [`custom::Primitive`] to render. + pub primitive: Arc<dyn Primitive>, +} + +impl Pipeline { + /// Creates a new [`Pipeline`] with the given [`Primitive`]. + pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self { + Pipeline { + bounds, + primitive: Arc::new(primitive), + } + } +} + +impl PartialEq for Pipeline { + fn eq(&self, other: &Self) -> bool { + self.primitive.type_id() == other.primitive.type_id() + } +} + +/// A set of methods which allows a [`Primitive`] to be rendered. +pub trait Primitive: Debug + Send + Sync + 'static { + /// Processes the [`Primitive`], allowing for GPU buffer allocation. + fn prepare( + &self, + format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + scale_factor: f32, + transform: Transformation, + storage: &mut Storage, + ); + + /// Renders the [`Primitive`]. + fn render( + &self, + storage: &Storage, + bounds: Rectangle<u32>, + target: &wgpu::TextureView, + target_size: Size<u32>, + encoder: &mut wgpu::CommandEncoder, + ); +} + +/// A renderer than can draw custom pipeline primitives. +pub trait Renderer: crate::core::Renderer { + /// Draws a custom pipeline primitive. + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl Primitive, + ); +} + +impl<Theme> Renderer for crate::Renderer<Theme> { + fn draw_pipeline_primitive( + &mut self, + bounds: Rectangle, + primitive: impl Primitive, + ) { + self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline( + Pipeline::new(bounds, primitive), + ))); + } +} + +/// Stores custom, user-provided pipelines. +#[derive(Default, Debug)] +pub struct Storage { + pipelines: HashMap<TypeId, Box<dyn Any>>, +} + +impl Storage { + /// Returns `true` if `Storage` contains a pipeline with type `T`. + pub fn has<T: 'static>(&self) -> bool { + self.pipelines.get(&TypeId::of::<T>()).is_some() + } + + /// Inserts the pipeline `T` in to [`Storage`]. + pub fn store<T: 'static>(&mut self, pipeline: T) { + let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); + } + + /// Returns a reference to pipeline with type `T` if it exists in [`Storage`]. + pub fn get<T: 'static>(&self) -> Option<&T> { + self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_ref::<T>() + .expect("Pipeline with this type does not exist in Storage.") + }) + } + + /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`]. + pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { + self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| { + pipeline + .downcast_mut::<T>() + .expect("Pipeline with this type does not exist in Storage.") + }) + } +} diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 5220e83a..07378d83 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -98,7 +98,11 @@ pub use tooltip::Tooltip; pub use vertical_slider::VerticalSlider; #[cfg(feature = "wgpu")] -pub use renderer::widget::shader::{self, Shader, Transformation}; +pub mod shader; + +#[cfg(feature = "wgpu")] +#[doc(no_inline)] +pub use shader::Shader; #[cfg(feature = "svg")] pub mod svg; diff --git a/widget/src/shader.rs b/widget/src/shader.rs new file mode 100644 index 00000000..9d482537 --- /dev/null +++ b/widget/src/shader.rs @@ -0,0 +1,219 @@ +//! A custom shader widget for wgpu applications. +mod event; +mod program; + +pub use event::Event; +pub use program::Program; + +use crate::core; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget::tree::{self, Tree}; +use crate::core::widget::{self, Widget}; +use crate::core::window; +use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; +use crate::renderer::wgpu::primitive::pipeline; + +use std::marker::PhantomData; + +pub use pipeline::{Primitive, Storage}; + +/// A widget which can render custom shaders with Iced's `wgpu` backend. +/// +/// Must be initialized with a [`Program`], which describes the internal widget state & how +/// its [`Program::Primitive`]s are drawn. +#[allow(missing_debug_implementations)] +pub struct Shader<Message, P: Program<Message>> { + width: Length, + height: Length, + program: P, + _message: PhantomData<Message>, +} + +impl<Message, P: Program<Message>> Shader<Message, P> { + /// Create a new custom [`Shader`]. + pub fn new(program: P) -> Self { + Self { + width: Length::Fixed(100.0), + height: Length::Fixed(100.0), + program, + _message: PhantomData, + } + } + + /// Set the `width` of the custom [`Shader`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Set the `height` of the custom [`Shader`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } +} + +impl<P, Message, Renderer> Widget<Message, Renderer> for Shader<Message, P> +where + P: Program<Message>, + Renderer: pipeline::Renderer, +{ + fn tag(&self) -> tree::Tag { + struct Tag<T>(T); + tree::Tag::of::<Tag<P::State>>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: crate::core::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let bounds = layout.bounds(); + + let custom_shader_event = match event { + core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), + core::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + core::Event::Window(window::Event::RedrawRequested(instant)) => { + Some(Event::RedrawRequested(instant)) + } + _ => None, + }; + + if let Some(custom_shader_event) = custom_shader_event { + let state = tree.state.downcast_mut::<P::State>(); + + let (event_status, message) = self.program.update( + state, + custom_shader_event, + bounds, + cursor, + shell, + ); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + _theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let state = tree.state.downcast_ref::<P::State>(); + + renderer.draw_pipeline_primitive( + bounds, + self.program.draw(state, cursor_position, bounds), + ); + } +} + +impl<'a, Message, Renderer, P> From<Shader<Message, P>> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: pipeline::Renderer, + P: Program<Message> + 'a, +{ + fn from(custom: Shader<Message, P>) -> Element<'a, Message, Renderer> { + Element::new(custom) + } +} + +impl<Message, T> Program<Message> for &T +where + T: Program<Message>, +{ + type State = T::State; + type Primitive = T::Primitive; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + ) -> (event::Status, Option<Message>) { + T::update(self, state, event, bounds, cursor, shell) + } + + fn draw( + &self, + state: &Self::State, + cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + T::draw(self, state, cursor, bounds) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs new file mode 100644 index 00000000..e4d2b03d --- /dev/null +++ b/widget/src/shader/event.rs @@ -0,0 +1,26 @@ +//! Handle events of a custom shader widget. +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::touch; + +use std::time::Instant; + +pub use crate::core::event::Status; + +/// A [`Shader`] event. +/// +/// [`Shader`]: crate::widget::shader::Shader; +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), + + /// A window requested a redraw. + RedrawRequested(Instant), +} diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs new file mode 100644 index 00000000..0319844d --- /dev/null +++ b/widget/src/shader/program.rs @@ -0,0 +1,62 @@ +use crate::core::event; +use crate::core::mouse; +use crate::core::{Rectangle, Shell}; +use crate::renderer::wgpu::primitive::pipeline; +use crate::shader; + +/// The state and logic of a [`Shader`] widget. +/// +/// A [`Program`] can mutate the internal state of a [`Shader`] widget +/// and produce messages for an application. +/// +/// [`Shader`]: crate::widget::shader::Shader +pub trait Program<Message> { + /// The internal state of the [`Program`]. + type State: Default + 'static; + + /// The type of primitive this [`Program`] can draw. + type Primitive: pipeline::Primitive + 'static; + + /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes + /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a + /// redraw for the window, etc. + /// + /// By default, this method does and returns nothing. + /// + /// [`State`]: Self::State + fn update( + &self, + _state: &mut Self::State, + _event: shader::Event, + _bounds: Rectangle, + _cursor: mouse::Cursor, + _shell: &mut Shell<'_, Message>, + ) -> (event::Status, Option<Message>) { + (event::Status::Ignored, None) + } + + /// Draws the [`Primitive`]. + /// + /// [`Primitive`]: Self::Primitive + fn draw( + &self, + state: &Self::State, + cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position is out of + /// bounds of the [`Shader`]'s program. + /// + /// [`Shader`]: crate::widget::shader::Shader + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} -- cgit From 882ae304ac8da899d7d83aed433b7dd3311a0496 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 12:51:08 +0100 Subject: Enable `iced_renderer/wgpu` feature in `iced_widget` --- widget/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 032f801c..e8e363c4 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -20,7 +20,7 @@ image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] qr_code = ["canvas", "qrcode"] -wgpu = [] +wgpu = ["iced_renderer/wgpu"] [dependencies] iced_renderer.workspace = true -- cgit From c2baf18cbff721e25c3103b6235292530b419c54 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 12:52:03 +0100 Subject: Use `Instant` from `iced_core` instead of `std` This is needed for Wasm compatibility. --- widget/src/shader/event.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs index e4d2b03d..a5a7acaa 100644 --- a/widget/src/shader/event.rs +++ b/widget/src/shader/event.rs @@ -1,10 +1,9 @@ //! Handle events of a custom shader widget. use crate::core::keyboard; use crate::core::mouse; +use crate::core::time::Instant; use crate::core::touch; -use std::time::Instant; - pub use crate::core::event::Status; /// A [`Shader`] event. -- cgit From 280d3736d59b62c4087fe980c187953cc2be83d2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 13:23:28 +0100 Subject: Fix broken intra-doc links --- wgpu/src/primitive/pipeline.rs | 4 ++-- widget/src/shader/event.rs | 2 +- widget/src/shader/program.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs index b59aeff1..5dbd6697 100644 --- a/wgpu/src/primitive/pipeline.rs +++ b/wgpu/src/primitive/pipeline.rs @@ -10,10 +10,10 @@ use std::sync::Arc; #[derive(Clone, Debug)] /// A custom primitive which can be used to render primitives associated with a custom pipeline. pub struct Pipeline { - /// The bounds of the [`Shader`]. + /// The bounds of the [`Pipeline`]. pub bounds: Rectangle, - /// The [`custom::Primitive`] to render. + /// The [`Primitive`] to render. pub primitive: Arc<dyn Primitive>, } diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs index a5a7acaa..1cc484fb 100644 --- a/widget/src/shader/event.rs +++ b/widget/src/shader/event.rs @@ -8,7 +8,7 @@ pub use crate::core::event::Status; /// A [`Shader`] event. /// -/// [`Shader`]: crate::widget::shader::Shader; +/// [`Shader`]: crate::Shader #[derive(Debug, Clone, Copy, PartialEq)] pub enum Event { /// A mouse event. diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 0319844d..6dd50404 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -9,7 +9,7 @@ use crate::shader; /// A [`Program`] can mutate the internal state of a [`Shader`] widget /// and produce messages for an application. /// -/// [`Shader`]: crate::widget::shader::Shader +/// [`Shader`]: crate::Shader pub trait Program<Message> { /// The internal state of the [`Program`]. type State: Default + 'static; @@ -50,7 +50,7 @@ pub trait Program<Message> { /// The interaction returned will be in effect even if the cursor position is out of /// bounds of the [`Shader`]'s program. /// - /// [`Shader`]: crate::widget::shader::Shader + /// [`Shader`]: crate::Shader fn mouse_interaction( &self, _state: &Self::State, -- cgit From 91d7df52cdedd1b5c431fdb51a6356e827765b60 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 13:25:49 +0100 Subject: Create `shader` function helper in `iced_widget` --- examples/custom_shader/src/main.rs | 7 +++---- widget/src/helpers.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index f6370f46..e9b6776f 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -10,7 +10,7 @@ use crate::pipeline::Pipeline; use iced::executor; use iced::time::Instant; use iced::widget::{ - checkbox, column, container, row, slider, text, vertical_space, Shader, + checkbox, column, container, row, shader, slider, text, vertical_space, }; use iced::window; use iced::{ @@ -150,9 +150,8 @@ impl Application for IcedCubes { .spacing(10) .align_items(Alignment::Center); - let shader = Shader::new(&self.cubes) - .width(Length::Fill) - .height(Length::Fill); + let shader = + shader(&self.cubes).width(Length::Fill).height(Length::Fill); container( column![shader, controls, vertical_space(20),] diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e0b58722..115198fb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -385,6 +385,17 @@ where crate::Canvas::new(program) } +/// Creates a new [`Shader`]. +/// +/// [`Shader`]: crate::Shader +#[cfg(feature = "wgpu")] +pub fn shader<Message, P>(program: P) -> crate::Shader<Message, P> +where + P: crate::shader::Program<Message>, +{ + crate::Shader::new(program) +} + /// Focuses the previous focusable widget. pub fn focus_previous<Message>() -> Command<Message> where -- cgit From 63f36b04638f14af3455ead8b82d581a438a28a3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 14:04:54 +0100 Subject: Export `wgpu` crate in `shader` module in `iced_widget` --- examples/custom_shader/Cargo.toml | 1 - examples/custom_shader/src/main.rs | 1 + examples/custom_shader/src/pipeline.rs | 4 +++- examples/custom_shader/src/primitive.rs | 1 + examples/custom_shader/src/primitive/buffer.rs | 2 ++ examples/custom_shader/src/primitive/cube.rs | 2 ++ examples/custom_shader/src/primitive/vertex.rs | 2 ++ widget/src/shader.rs | 1 + 8 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/custom_shader/Cargo.toml b/examples/custom_shader/Cargo.toml index 0b8466a9..b602f98d 100644 --- a/examples/custom_shader/Cargo.toml +++ b/examples/custom_shader/Cargo.toml @@ -9,7 +9,6 @@ iced.workspace = true iced.features = ["debug", "advanced"] image.workspace = true -wgpu.workspace = true bytemuck.workspace = true glam.workspace = true diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index e9b6776f..e7b07d78 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -9,6 +9,7 @@ use crate::pipeline::Pipeline; use iced::executor; use iced::time::Instant; +use iced::widget::shader::wgpu; use iced::widget::{ checkbox, column, container, row, shader, slider, text, vertical_space, }; diff --git a/examples/custom_shader/src/pipeline.rs b/examples/custom_shader/src/pipeline.rs index 44ad17a2..9343e5e0 100644 --- a/examples/custom_shader/src/pipeline.rs +++ b/examples/custom_shader/src/pipeline.rs @@ -1,8 +1,10 @@ use crate::primitive; use crate::primitive::cube; use crate::primitive::{Buffer, Uniforms}; +use crate::wgpu; +use crate::wgpu::util::DeviceExt; + use iced::{Rectangle, Size}; -use wgpu::util::DeviceExt; const SKY_TEXTURE_SIZE: u32 = 128; diff --git a/examples/custom_shader/src/primitive.rs b/examples/custom_shader/src/primitive.rs index 520ceb8e..f5862ab3 100644 --- a/examples/custom_shader/src/primitive.rs +++ b/examples/custom_shader/src/primitive.rs @@ -9,6 +9,7 @@ pub use cube::Cube; pub use uniforms::Uniforms; pub use vertex::Vertex; +use crate::wgpu; use crate::Camera; use crate::Pipeline; diff --git a/examples/custom_shader/src/primitive/buffer.rs b/examples/custom_shader/src/primitive/buffer.rs index 377ce1bb..ef4c41c9 100644 --- a/examples/custom_shader/src/primitive/buffer.rs +++ b/examples/custom_shader/src/primitive/buffer.rs @@ -1,3 +1,5 @@ +use crate::wgpu; + // A custom buffer container for dynamic resizing. pub struct Buffer { pub raw: wgpu::Buffer, diff --git a/examples/custom_shader/src/primitive/cube.rs b/examples/custom_shader/src/primitive/cube.rs index c23f2132..7ece685d 100644 --- a/examples/custom_shader/src/primitive/cube.rs +++ b/examples/custom_shader/src/primitive/cube.rs @@ -1,4 +1,6 @@ use crate::primitive::Vertex; +use crate::wgpu; + use glam::{vec2, vec3, Vec3}; use rand::{thread_rng, Rng}; diff --git a/examples/custom_shader/src/primitive/vertex.rs b/examples/custom_shader/src/primitive/vertex.rs index 6d17aa0f..e64cd926 100644 --- a/examples/custom_shader/src/primitive/vertex.rs +++ b/examples/custom_shader/src/primitive/vertex.rs @@ -1,3 +1,5 @@ +use crate::wgpu; + #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] pub struct Vertex { diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 9d482537..fe6214db 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -17,6 +17,7 @@ use crate::renderer::wgpu::primitive::pipeline; use std::marker::PhantomData; +pub use crate::renderer::wgpu::wgpu; pub use pipeline::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. -- cgit From 78a06384b1e6fc5e0c472dc19169fcaf11fe27b2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 14:36:38 +0100 Subject: Use a single source for amount of cubes in `custom_shader` example --- examples/custom_shader/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index e7b07d78..3336e7f5 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -26,7 +26,6 @@ fn main() -> iced::Result { struct IcedCubes { start: Instant, cubes: Cubes, - num_cubes_slider: u32, } impl Default for IcedCubes { @@ -34,7 +33,6 @@ impl Default for IcedCubes { Self { start: Instant::now(), cubes: Cubes::new(), - num_cubes_slider: cubes::MAX, } } } @@ -65,7 +63,6 @@ impl Application for IcedCubes { fn update(&mut self, message: Self::Message) -> Command<Self::Message> { match message { Message::CubeAmountChanged(num) => { - self.num_cubes_slider = num; self.cubes.adjust_num_cubes(num); } Message::CubeSizeChanged(size) => { @@ -91,7 +88,7 @@ impl Application for IcedCubes { "Amount", slider( 1..=cubes::MAX, - self.num_cubes_slider, + self.cubes.cubes.len() as u32, Message::CubeAmountChanged ) .width(100) -- cgit From 9ddfaf3ee73cef3e7bd122f4d11893f77647c2c3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 14:41:48 +0100 Subject: Rename `cubes` to `scene` in `custom_shader` example --- examples/custom_shader/src/cubes.rs | 99 ------------------------------------- examples/custom_shader/src/main.rs | 42 ++++++++-------- examples/custom_shader/src/scene.rs | 99 +++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 120 deletions(-) delete mode 100644 examples/custom_shader/src/cubes.rs create mode 100644 examples/custom_shader/src/scene.rs diff --git a/examples/custom_shader/src/cubes.rs b/examples/custom_shader/src/cubes.rs deleted file mode 100644 index 00e608e3..00000000 --- a/examples/custom_shader/src/cubes.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::camera::Camera; -use crate::primitive; -use crate::primitive::cube::Cube; -use glam::Vec3; -use iced::widget::shader; -use iced::{mouse, Color, Rectangle}; -use rand::Rng; -use std::cmp::Ordering; -use std::iter; -use std::time::Duration; - -pub const MAX: u32 = 500; - -#[derive(Clone)] -pub struct Cubes { - pub size: f32, - pub cubes: Vec<Cube>, - pub camera: Camera, - pub show_depth_buffer: bool, - pub light_color: Color, -} - -impl Cubes { - pub fn new() -> Self { - let mut cubes = Self { - size: 0.2, - cubes: vec![], - camera: Camera::default(), - show_depth_buffer: false, - light_color: Color::WHITE, - }; - - cubes.adjust_num_cubes(MAX); - - cubes - } - - pub fn update(&mut self, time: Duration) { - for cube in self.cubes.iter_mut() { - cube.update(self.size, time.as_secs_f32()); - } - } - - pub fn adjust_num_cubes(&mut self, num_cubes: u32) { - let curr_cubes = self.cubes.len() as u32; - - match num_cubes.cmp(&curr_cubes) { - Ordering::Greater => { - // spawn - let cubes_2_spawn = (num_cubes - curr_cubes) as usize; - - let mut cubes = 0; - self.cubes.extend(iter::from_fn(|| { - if cubes < cubes_2_spawn { - cubes += 1; - Some(Cube::new(self.size, rnd_origin())) - } else { - None - } - })); - } - Ordering::Less => { - // chop - let cubes_2_cut = curr_cubes - num_cubes; - let new_len = self.cubes.len() - cubes_2_cut as usize; - self.cubes.truncate(new_len); - } - Ordering::Equal => {} - } - } -} - -impl<Message> shader::Program<Message> for Cubes { - type State = (); - type Primitive = primitive::Primitive; - - fn draw( - &self, - _state: &Self::State, - _cursor: mouse::Cursor, - bounds: Rectangle, - ) -> Self::Primitive { - primitive::Primitive::new( - &self.cubes, - &self.camera, - bounds, - self.show_depth_buffer, - self.light_color, - ) - } -} - -fn rnd_origin() -> Vec3 { - Vec3::new( - rand::thread_rng().gen_range(-4.0..4.0), - rand::thread_rng().gen_range(-4.0..4.0), - rand::thread_rng().gen_range(-4.0..2.0), - ) -} diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 3336e7f5..f90061d7 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -1,11 +1,11 @@ mod camera; -mod cubes; mod pipeline; mod primitive; +mod scene; use crate::camera::Camera; -use crate::cubes::Cubes; use crate::pipeline::Pipeline; +use crate::scene::Scene; use iced::executor; use iced::time::Instant; @@ -25,14 +25,14 @@ fn main() -> iced::Result { struct IcedCubes { start: Instant, - cubes: Cubes, + scene: Scene, } impl Default for IcedCubes { fn default() -> Self { Self { start: Instant::now(), - cubes: Cubes::new(), + scene: Scene::new(), } } } @@ -62,20 +62,20 @@ impl Application for IcedCubes { fn update(&mut self, message: Self::Message) -> Command<Self::Message> { match message { - Message::CubeAmountChanged(num) => { - self.cubes.adjust_num_cubes(num); + Message::CubeAmountChanged(amount) => { + self.scene.change_amount(amount); } Message::CubeSizeChanged(size) => { - self.cubes.size = size; + self.scene.size = size; } Message::Tick(time) => { - self.cubes.update(time - self.start); + self.scene.update(time - self.start); } Message::ShowDepthBuffer(show) => { - self.cubes.show_depth_buffer = show; + self.scene.show_depth_buffer = show; } Message::LightColorChanged(color) => { - self.cubes.light_color = color; + self.scene.light_color = color; } } @@ -87,21 +87,21 @@ impl Application for IcedCubes { control( "Amount", slider( - 1..=cubes::MAX, - self.cubes.cubes.len() as u32, + 1..=scene::MAX, + self.scene.cubes.len() as u32, Message::CubeAmountChanged ) .width(100) ), control( "Size", - slider(0.1..=0.25, self.cubes.size, Message::CubeSizeChanged) + slider(0.1..=0.25, self.scene.size, Message::CubeSizeChanged) .step(0.01) .width(100), ), checkbox( "Show Depth Buffer", - self.cubes.show_depth_buffer, + self.scene.show_depth_buffer, Message::ShowDepthBuffer ), ] @@ -110,10 +110,10 @@ impl Application for IcedCubes { let bottom_controls = row![ control( "R", - slider(0.0..=1.0, self.cubes.light_color.r, move |r| { + slider(0.0..=1.0, self.scene.light_color.r, move |r| { Message::LightColorChanged(Color { r, - ..self.cubes.light_color + ..self.scene.light_color }) }) .step(0.01) @@ -121,10 +121,10 @@ impl Application for IcedCubes { ), control( "G", - slider(0.0..=1.0, self.cubes.light_color.g, move |g| { + slider(0.0..=1.0, self.scene.light_color.g, move |g| { Message::LightColorChanged(Color { g, - ..self.cubes.light_color + ..self.scene.light_color }) }) .step(0.01) @@ -132,10 +132,10 @@ impl Application for IcedCubes { ), control( "B", - slider(0.0..=1.0, self.cubes.light_color.b, move |b| { + slider(0.0..=1.0, self.scene.light_color.b, move |b| { Message::LightColorChanged(Color { b, - ..self.cubes.light_color + ..self.scene.light_color }) }) .step(0.01) @@ -149,7 +149,7 @@ impl Application for IcedCubes { .align_items(Alignment::Center); let shader = - shader(&self.cubes).width(Length::Fill).height(Length::Fill); + shader(&self.scene).width(Length::Fill).height(Length::Fill); container( column![shader, controls, vertical_space(20),] diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs new file mode 100644 index 00000000..ab923093 --- /dev/null +++ b/examples/custom_shader/src/scene.rs @@ -0,0 +1,99 @@ +use crate::camera::Camera; +use crate::primitive; +use crate::primitive::cube::Cube; +use glam::Vec3; +use iced::widget::shader; +use iced::{mouse, Color, Rectangle}; +use rand::Rng; +use std::cmp::Ordering; +use std::iter; +use std::time::Duration; + +pub const MAX: u32 = 500; + +#[derive(Clone)] +pub struct Scene { + pub size: f32, + pub cubes: Vec<Cube>, + pub camera: Camera, + pub show_depth_buffer: bool, + pub light_color: Color, +} + +impl Scene { + pub fn new() -> Self { + let mut scene = Self { + size: 0.2, + cubes: vec![], + camera: Camera::default(), + show_depth_buffer: false, + light_color: Color::WHITE, + }; + + scene.change_amount(MAX); + + scene + } + + pub fn update(&mut self, time: Duration) { + for cube in self.cubes.iter_mut() { + cube.update(self.size, time.as_secs_f32()); + } + } + + pub fn change_amount(&mut self, amount: u32) { + let curr_cubes = self.cubes.len() as u32; + + match amount.cmp(&curr_cubes) { + Ordering::Greater => { + // spawn + let cubes_2_spawn = (amount - curr_cubes) as usize; + + let mut cubes = 0; + self.cubes.extend(iter::from_fn(|| { + if cubes < cubes_2_spawn { + cubes += 1; + Some(Cube::new(self.size, rnd_origin())) + } else { + None + } + })); + } + Ordering::Less => { + // chop + let cubes_2_cut = curr_cubes - amount; + let new_len = self.cubes.len() - cubes_2_cut as usize; + self.cubes.truncate(new_len); + } + Ordering::Equal => {} + } + } +} + +impl<Message> shader::Program<Message> for Scene { + type State = (); + type Primitive = primitive::Primitive; + + fn draw( + &self, + _state: &Self::State, + _cursor: mouse::Cursor, + bounds: Rectangle, + ) -> Self::Primitive { + primitive::Primitive::new( + &self.cubes, + &self.camera, + bounds, + self.show_depth_buffer, + self.light_color, + ) + } +} + +fn rnd_origin() -> Vec3 { + Vec3::new( + rand::thread_rng().gen_range(-4.0..4.0), + rand::thread_rng().gen_range(-4.0..4.0), + rand::thread_rng().gen_range(-4.0..2.0), + ) +} -- cgit From 34b5cb75ef9f97076dd9e306d8afb68058176883 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 14:43:02 +0100 Subject: Remove `Default` implementation in `custom_shader` example --- examples/custom_shader/src/main.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index f90061d7..f4853507 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -28,15 +28,6 @@ struct IcedCubes { scene: Scene, } -impl Default for IcedCubes { - fn default() -> Self { - Self { - start: Instant::now(), - scene: Scene::new(), - } - } -} - #[derive(Debug, Clone)] enum Message { CubeAmountChanged(u32), @@ -53,7 +44,13 @@ impl Application for IcedCubes { type Flags = (); fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) { - (IcedCubes::default(), Command::none()) + ( + Self { + start: Instant::now(), + scene: Scene::new(), + }, + Command::none(), + ) } fn title(&self) -> String { -- cgit From fee3bf0df4e3099ea74def738be8743b2b72d78a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 14:47:29 +0100 Subject: Kill current render pass only when custom pipelines are present in layer --- wgpu/src/backend.rs | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index f89bcee1..91ae777b 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -323,10 +323,9 @@ impl Backend { text_layer += 1; } - // kill render pass to let custom shaders get mut access to encoder - let _ = ManuallyDrop::into_inner(render_pass); - if !layer.pipelines.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + for pipeline in &layer.pipelines { let bounds = (pipeline.bounds * scale_factor).snap(); @@ -342,27 +341,26 @@ impl Backend { encoder, ); } - } - // recreate and continue processing layers - render_pass = ManuallyDrop::new(encoder.begin_render_pass( - &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::quad render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, }, - }, - )], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }, - )); + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }, + )); + } } let _ = ManuallyDrop::into_inner(render_pass); -- cgit From b1b2467b45e16185cc17df00b4c75700435cd46e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 14:50:57 +0100 Subject: Fix render pass label in `iced_wgpu` --- wgpu/src/backend.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 91ae777b..88caad06 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -222,7 +222,7 @@ impl Backend { let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass( &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), + label: Some("iced_wgpu render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: target, resolve_target: None, @@ -285,7 +285,7 @@ impl Backend { render_pass = ManuallyDrop::new(encoder.begin_render_pass( &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), + label: Some("iced_wgpu render pass"), color_attachments: &[Some( wgpu::RenderPassColorAttachment { view: target, @@ -344,7 +344,7 @@ impl Backend { render_pass = ManuallyDrop::new(encoder.begin_render_pass( &wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), + label: Some("iced_wgpu render pass"), color_attachments: &[Some( wgpu::RenderPassColorAttachment { view: target, -- cgit From 811aa673e9e832ebe38bf56a087f32fdc7aba59c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 15:48:01 +0100 Subject: Improve module hierarchy of `custom_shader` example --- examples/custom_shader/src/camera.rs | 53 -- examples/custom_shader/src/main.rs | 7 +- examples/custom_shader/src/pipeline.rs | 606 -------------------- examples/custom_shader/src/primitive.rs | 97 ---- examples/custom_shader/src/primitive/buffer.rs | 41 -- examples/custom_shader/src/primitive/cube.rs | 326 ----------- examples/custom_shader/src/primitive/uniforms.rs | 22 - examples/custom_shader/src/primitive/vertex.rs | 31 -- examples/custom_shader/src/scene.rs | 103 +++- examples/custom_shader/src/scene/camera.rs | 53 ++ examples/custom_shader/src/scene/pipeline.rs | 615 +++++++++++++++++++++ .../custom_shader/src/scene/pipeline/buffer.rs | 41 ++ examples/custom_shader/src/scene/pipeline/cube.rs | 326 +++++++++++ .../custom_shader/src/scene/pipeline/uniforms.rs | 23 + .../custom_shader/src/scene/pipeline/vertex.rs | 31 ++ widget/src/shader.rs | 1 + 16 files changed, 1186 insertions(+), 1190 deletions(-) delete mode 100644 examples/custom_shader/src/camera.rs delete mode 100644 examples/custom_shader/src/pipeline.rs delete mode 100644 examples/custom_shader/src/primitive.rs delete mode 100644 examples/custom_shader/src/primitive/buffer.rs delete mode 100644 examples/custom_shader/src/primitive/cube.rs delete mode 100644 examples/custom_shader/src/primitive/uniforms.rs delete mode 100644 examples/custom_shader/src/primitive/vertex.rs create mode 100644 examples/custom_shader/src/scene/camera.rs create mode 100644 examples/custom_shader/src/scene/pipeline.rs create mode 100644 examples/custom_shader/src/scene/pipeline/buffer.rs create mode 100644 examples/custom_shader/src/scene/pipeline/cube.rs create mode 100644 examples/custom_shader/src/scene/pipeline/uniforms.rs create mode 100644 examples/custom_shader/src/scene/pipeline/vertex.rs diff --git a/examples/custom_shader/src/camera.rs b/examples/custom_shader/src/camera.rs deleted file mode 100644 index 2a49c102..00000000 --- a/examples/custom_shader/src/camera.rs +++ /dev/null @@ -1,53 +0,0 @@ -use glam::{mat4, vec3, vec4}; -use iced::Rectangle; - -#[derive(Copy, Clone)] -pub struct Camera { - eye: glam::Vec3, - target: glam::Vec3, - up: glam::Vec3, - fov_y: f32, - near: f32, - far: f32, -} - -impl Default for Camera { - fn default() -> Self { - Self { - eye: vec3(0.0, 2.0, 3.0), - target: glam::Vec3::ZERO, - up: glam::Vec3::Y, - fov_y: 45.0, - near: 0.1, - far: 100.0, - } - } -} - -pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4( - vec4(1.0, 0.0, 0.0, 0.0), - vec4(0.0, 1.0, 0.0, 0.0), - vec4(0.0, 0.0, 0.5, 0.0), - vec4(0.0, 0.0, 0.5, 1.0), -); - -impl Camera { - pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 { - //TODO looks distorted without padding; base on surface texture size instead? - let aspect_ratio = bounds.width / (bounds.height + 150.0); - - let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up); - let proj = glam::Mat4::perspective_rh( - self.fov_y, - aspect_ratio, - self.near, - self.far, - ); - - OPENGL_TO_WGPU_MATRIX * proj * view - } - - pub fn position(&self) -> glam::Vec4 { - glam::Vec4::from((self.eye, 0.0)) - } -} diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index f4853507..2eb1ac4a 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -1,11 +1,6 @@ -mod camera; -mod pipeline; -mod primitive; mod scene; -use crate::camera::Camera; -use crate::pipeline::Pipeline; -use crate::scene::Scene; +use scene::Scene; use iced::executor; use iced::time::Instant; diff --git a/examples/custom_shader/src/pipeline.rs b/examples/custom_shader/src/pipeline.rs deleted file mode 100644 index 9343e5e0..00000000 --- a/examples/custom_shader/src/pipeline.rs +++ /dev/null @@ -1,606 +0,0 @@ -use crate::primitive; -use crate::primitive::cube; -use crate::primitive::{Buffer, Uniforms}; -use crate::wgpu; -use crate::wgpu::util::DeviceExt; - -use iced::{Rectangle, Size}; - -const SKY_TEXTURE_SIZE: u32 = 128; - -pub struct Pipeline { - pipeline: wgpu::RenderPipeline, - vertices: wgpu::Buffer, - cubes: Buffer, - uniforms: wgpu::Buffer, - uniform_bind_group: wgpu::BindGroup, - depth_texture_size: Size<u32>, - depth_view: wgpu::TextureView, - depth_pipeline: DepthPipeline, -} - -impl Pipeline { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - format: wgpu::TextureFormat, - target_size: Size<u32>, - ) -> Self { - //vertices of one cube - let vertices = - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("cubes vertex buffer"), - contents: bytemuck::cast_slice(&cube::Raw::vertices()), - usage: wgpu::BufferUsages::VERTEX, - }); - - //cube instance data - let cubes_buffer = Buffer::new( - device, - "cubes instance buffer", - std::mem::size_of::<cube::Raw>() as u64, - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - //uniforms for all cubes - let uniforms = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("cubes uniform buffer"), - size: std::mem::size_of::<Uniforms>() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - //depth buffer - let depth_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("cubes depth texture"), - size: wgpu::Extent3d { - width: target_size.width, - height: target_size.height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - - let depth_view = - depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - let normal_map_data = load_normal_map_data(); - - //normal map - let normal_texture = device.create_texture_with_data( - queue, - &wgpu::TextureDescriptor { - label: Some("cubes normal map texture"), - size: wgpu::Extent3d { - width: 1024, - height: 1024, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - &normal_map_data, - ); - - let normal_view = - normal_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - //skybox texture for reflection/refraction - let skybox_data = load_skybox_data(); - - let skybox_texture = device.create_texture_with_data( - queue, - &wgpu::TextureDescriptor { - label: Some("cubes skybox texture"), - size: wgpu::Extent3d { - width: SKY_TEXTURE_SIZE, - height: SKY_TEXTURE_SIZE, - depth_or_array_layers: 6, //one for each face of the cube - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - &skybox_data, - ); - - let sky_view = - skybox_texture.create_view(&wgpu::TextureViewDescriptor { - label: Some("cubes skybox texture view"), - dimension: Some(wgpu::TextureViewDimension::Cube), - ..Default::default() - }); - - let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("cubes skybox sampler"), - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Linear, - ..Default::default() - }); - - let uniform_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("cubes uniform bind group layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { - filterable: true, - }, - view_dimension: wgpu::TextureViewDimension::Cube, - multisampled: false, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::Filtering, - ), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { - filterable: true, - }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - ], - }); - - let uniform_bind_group = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("cubes uniform bind group"), - layout: &uniform_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: uniforms.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&sky_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&sky_sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView( - &normal_view, - ), - }, - ], - }); - - let layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("cubes pipeline layout"), - bind_group_layouts: &[&uniform_bind_group_layout], - push_constant_ranges: &[], - }); - - let shader = - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("cubes shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( - include_str!("shaders/cubes.wgsl"), - )), - }); - - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("cubes pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[primitive::Vertex::desc(), cube::Raw::desc()], - }, - primitive: wgpu::PrimitiveState::default(), - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Less, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::Max, - }, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }); - - let depth_pipeline = DepthPipeline::new( - device, - format, - depth_texture.create_view(&wgpu::TextureViewDescriptor::default()), - ); - - Self { - pipeline, - cubes: cubes_buffer, - uniforms, - uniform_bind_group, - vertices, - depth_texture_size: target_size, - depth_view, - depth_pipeline, - } - } - - fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) { - if self.depth_texture_size.height != size.height - || self.depth_texture_size.width != size.width - { - let text = device.create_texture(&wgpu::TextureDescriptor { - label: Some("cubes depth texture"), - size: wgpu::Extent3d { - width: size.width, - height: size.height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Depth32Float, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - - self.depth_view = - text.create_view(&wgpu::TextureViewDescriptor::default()); - self.depth_texture_size = size; - - self.depth_pipeline.update(device, &text); - } - } - - pub fn update( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - target_size: Size<u32>, - uniforms: &Uniforms, - num_cubes: usize, - cubes: &[cube::Raw], - ) { - //recreate depth texture if surface texture size has changed - self.update_depth_texture(device, target_size); - - // update uniforms - queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms)); - - //resize cubes vertex buffer if cubes amount changed - let new_size = num_cubes * std::mem::size_of::<cube::Raw>(); - self.cubes.resize(device, new_size as u64); - - //always write new cube data since they are constantly rotating - queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes)); - } - - pub fn render( - &self, - target: &wgpu::TextureView, - encoder: &mut wgpu::CommandEncoder, - bounds: Rectangle<u32>, - num_cubes: u32, - show_depth: bool, - ) { - { - let mut pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("cubes.pipeline.pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - }, - )], - depth_stencil_attachment: Some( - wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }, - ), - timestamp_writes: None, - occlusion_query_set: None, - }); - - pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ); - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &self.uniform_bind_group, &[]); - pass.set_vertex_buffer(0, self.vertices.slice(..)); - pass.set_vertex_buffer(1, self.cubes.raw.slice(..)); - pass.draw(0..36, 0..num_cubes); - } - - if show_depth { - self.depth_pipeline.render(encoder, target, bounds); - } - } -} - -struct DepthPipeline { - pipeline: wgpu::RenderPipeline, - bind_group_layout: wgpu::BindGroupLayout, - bind_group: wgpu::BindGroup, - sampler: wgpu::Sampler, - depth_view: wgpu::TextureView, -} - -impl DepthPipeline { - pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - depth_texture: wgpu::TextureView, - ) -> Self { - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: Some("cubes.depth_pipeline.sampler"), - ..Default::default() - }); - - let bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("cubes.depth_pipeline.bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler( - wgpu::SamplerBindingType::NonFiltering, - ), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { - filterable: false, - }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - count: None, - }, - ], - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("cubes.depth_pipeline.bind_group"), - layout: &bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&sampler), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView( - &depth_texture, - ), - }, - ], - }); - - let layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("cubes.depth_pipeline.layout"), - bind_group_layouts: &[&bind_group_layout], - push_constant_ranges: &[], - }); - - let shader = - device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("cubes.depth_pipeline.shader"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( - include_str!("shaders/depth.wgsl"), - )), - }); - - let pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("cubes.depth_pipeline.pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[], - }, - primitive: wgpu::PrimitiveState::default(), - depth_stencil: Some(wgpu::DepthStencilState { - format: wgpu::TextureFormat::Depth32Float, - depth_write_enabled: false, - depth_compare: wgpu::CompareFunction::Less, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState::default(), - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }); - - Self { - pipeline, - bind_group_layout, - bind_group, - sampler, - depth_view: depth_texture, - } - } - - pub fn update( - &mut self, - device: &wgpu::Device, - depth_texture: &wgpu::Texture, - ) { - self.depth_view = - depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); - - self.bind_group = - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("cubes.depth_pipeline.bind_group"), - layout: &self.bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Sampler(&self.sampler), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView( - &self.depth_view, - ), - }, - ], - }); - } - - pub fn render( - &self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - bounds: Rectangle<u32>, - ) { - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("cubes.pipeline.depth_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some( - wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_view, - depth_ops: None, - stencil_ops: None, - }, - ), - timestamp_writes: None, - occlusion_query_set: None, - }); - - pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height); - pass.set_pipeline(&self.pipeline); - pass.set_bind_group(0, &self.bind_group, &[]); - pass.draw(0..6, 0..1); - } -} - -fn load_skybox_data() -> Vec<u8> { - let pos_x: &[u8] = include_bytes!("textures/skybox/pos_x.jpg"); - let neg_x: &[u8] = include_bytes!("textures/skybox/neg_x.jpg"); - let pos_y: &[u8] = include_bytes!("textures/skybox/pos_y.jpg"); - let neg_y: &[u8] = include_bytes!("textures/skybox/neg_y.jpg"); - let pos_z: &[u8] = include_bytes!("textures/skybox/pos_z.jpg"); - let neg_z: &[u8] = include_bytes!("textures/skybox/neg_z.jpg"); - - let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z]; - - data.iter().fold(vec![], |mut acc, bytes| { - let i = image::load_from_memory_with_format( - bytes, - image::ImageFormat::Jpeg, - ) - .unwrap() - .to_rgba8() - .into_raw(); - - acc.extend(i); - acc - }) -} - -fn load_normal_map_data() -> Vec<u8> { - let bytes: &[u8] = include_bytes!("textures/ice_cube_normal_map.png"); - - image::load_from_memory_with_format(bytes, image::ImageFormat::Png) - .unwrap() - .to_rgba8() - .into_raw() -} diff --git a/examples/custom_shader/src/primitive.rs b/examples/custom_shader/src/primitive.rs deleted file mode 100644 index f5862ab3..00000000 --- a/examples/custom_shader/src/primitive.rs +++ /dev/null @@ -1,97 +0,0 @@ -pub mod cube; -pub mod vertex; - -mod buffer; -mod uniforms; - -pub use buffer::Buffer; -pub use cube::Cube; -pub use uniforms::Uniforms; -pub use vertex::Vertex; - -use crate::wgpu; -use crate::Camera; -use crate::Pipeline; - -use iced::advanced::graphics::Transformation; -use iced::widget::shader; -use iced::{Color, Rectangle, Size}; - -/// A collection of `Cube`s that can be rendered. -#[derive(Debug)] -pub struct Primitive { - cubes: Vec<cube::Raw>, - uniforms: Uniforms, - show_depth_buffer: bool, -} - -impl Primitive { - pub fn new( - cubes: &[Cube], - camera: &Camera, - bounds: Rectangle, - show_depth_buffer: bool, - light_color: Color, - ) -> Self { - let uniforms = Uniforms::new(camera, bounds, light_color); - - Self { - cubes: cubes - .iter() - .map(cube::Raw::from_cube) - .collect::<Vec<cube::Raw>>(), - uniforms, - show_depth_buffer, - } - } -} - -impl shader::Primitive for Primitive { - fn prepare( - &self, - format: wgpu::TextureFormat, - device: &wgpu::Device, - queue: &wgpu::Queue, - target_size: Size<u32>, - _scale_factor: f32, - _transform: Transformation, - storage: &mut shader::Storage, - ) { - if !storage.has::<Pipeline>() { - storage.store(Pipeline::new(device, queue, format, target_size)); - } - - let pipeline = storage.get_mut::<Pipeline>().unwrap(); - - //upload data to GPU - pipeline.update( - device, - queue, - target_size, - &self.uniforms, - self.cubes.len(), - &self.cubes, - ); - } - - fn render( - &self, - storage: &shader::Storage, - bounds: Rectangle<u32>, - target: &wgpu::TextureView, - _target_size: Size<u32>, - encoder: &mut wgpu::CommandEncoder, - ) { - //at this point our pipeline should always be initialized - let pipeline = storage.get::<Pipeline>().unwrap(); - - //render primitive - pipeline.render( - target, - encoder, - bounds, - self.cubes.len() as u32, - self.show_depth_buffer, - ); - } -} diff --git a/examples/custom_shader/src/primitive/buffer.rs b/examples/custom_shader/src/primitive/buffer.rs deleted file mode 100644 index ef4c41c9..00000000 --- a/examples/custom_shader/src/primitive/buffer.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::wgpu; - -// A custom buffer container for dynamic resizing. -pub struct Buffer { - pub raw: wgpu::Buffer, - label: &'static str, - size: u64, - usage: wgpu::BufferUsages, -} - -impl Buffer { - pub fn new( - device: &wgpu::Device, - label: &'static str, - size: u64, - usage: wgpu::BufferUsages, - ) -> Self { - Self { - raw: device.create_buffer(&wgpu::BufferDescriptor { - label: Some(label), - size, - usage, - mapped_at_creation: false, - }), - label, - size, - usage, - } - } - - pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) { - if new_size > self.size { - self.raw = device.create_buffer(&wgpu::BufferDescriptor { - label: Some(self.label), - size: new_size, - usage: self.usage, - mapped_at_creation: false, - }); - } - } -} diff --git a/examples/custom_shader/src/primitive/cube.rs b/examples/custom_shader/src/primitive/cube.rs deleted file mode 100644 index 7ece685d..00000000 --- a/examples/custom_shader/src/primitive/cube.rs +++ /dev/null @@ -1,326 +0,0 @@ -use crate::primitive::Vertex; -use crate::wgpu; - -use glam::{vec2, vec3, Vec3}; -use rand::{thread_rng, Rng}; - -/// A single instance of a cube. -#[derive(Debug, Clone)] -pub struct Cube { - pub rotation: glam::Quat, - pub position: Vec3, - pub size: f32, - rotation_dir: f32, - rotation_axis: glam::Vec3, -} - -impl Default for Cube { - fn default() -> Self { - Self { - rotation: glam::Quat::IDENTITY, - position: glam::Vec3::ZERO, - size: 0.1, - rotation_dir: 1.0, - rotation_axis: glam::Vec3::Y, - } - } -} - -impl Cube { - pub fn new(size: f32, origin: Vec3) -> Self { - let rnd = thread_rng().gen_range(0.0..=1.0f32); - - Self { - rotation: glam::Quat::IDENTITY, - position: origin + Vec3::new(0.1, 0.1, 0.1), - size, - rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 }, - rotation_axis: if rnd <= 0.33 { - glam::Vec3::Y - } else if rnd <= 0.66 { - glam::Vec3::X - } else { - glam::Vec3::Z - }, - } - } - - pub fn update(&mut self, size: f32, time: f32) { - self.rotation = glam::Quat::from_axis_angle( - self.rotation_axis, - time / 2.0 * self.rotation_dir, - ); - self.size = size; - } -} - -#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)] -#[repr(C)] -pub struct Raw { - transformation: glam::Mat4, - normal: glam::Mat3, - _padding: [f32; 3], -} - -impl Raw { - const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![ - //cube transformation matrix - 4 => Float32x4, - 5 => Float32x4, - 6 => Float32x4, - 7 => Float32x4, - //normal rotation matrix - 8 => Float32x3, - 9 => Float32x3, - 10 => Float32x3, - ]; - - pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &Self::ATTRIBS, - } - } -} - -impl Raw { - pub fn from_cube(cube: &Cube) -> Raw { - Raw { - transformation: glam::Mat4::from_scale_rotation_translation( - glam::vec3(cube.size, cube.size, cube.size), - cube.rotation, - cube.position, - ), - normal: glam::Mat3::from_quat(cube.rotation), - _padding: [0.0; 3], - } - } - - pub fn vertices() -> [Vertex; 36] { - [ - //face 1 - Vertex { - pos: vec3(-0.5, -0.5, -0.5), - normal: vec3(0.0, 0.0, -1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - Vertex { - pos: vec3(0.5, -0.5, -0.5), - normal: vec3(0.0, 0.0, -1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 1.0), - }, - Vertex { - pos: vec3(0.5, 0.5, -0.5), - normal: vec3(0.0, 0.0, -1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(0.5, 0.5, -0.5), - normal: vec3(0.0, 0.0, -1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, 0.5, -0.5), - normal: vec3(0.0, 0.0, -1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, -0.5), - normal: vec3(0.0, 0.0, -1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - //face 2 - Vertex { - pos: vec3(-0.5, -0.5, 0.5), - normal: vec3(0.0, 0.0, 1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - Vertex { - pos: vec3(0.5, -0.5, 0.5), - normal: vec3(0.0, 0.0, 1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 1.0), - }, - Vertex { - pos: vec3(0.5, 0.5, 0.5), - normal: vec3(0.0, 0.0, 1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(0.5, 0.5, 0.5), - normal: vec3(0.0, 0.0, 1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, 0.5, 0.5), - normal: vec3(0.0, 0.0, 1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, 0.5), - normal: vec3(0.0, 0.0, 1.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - //face 3 - Vertex { - pos: vec3(-0.5, 0.5, 0.5), - normal: vec3(-1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(0.0, 1.0), - }, - Vertex { - pos: vec3(-0.5, 0.5, -0.5), - normal: vec3(-1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(1.0, 1.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, -0.5), - normal: vec3(-1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, -0.5), - normal: vec3(-1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, 0.5), - normal: vec3(-1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(0.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, 0.5, 0.5), - normal: vec3(-1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(0.0, 1.0), - }, - //face 4 - Vertex { - pos: vec3(0.5, 0.5, 0.5), - normal: vec3(1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(0.0, 1.0), - }, - Vertex { - pos: vec3(0.5, 0.5, -0.5), - normal: vec3(1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(1.0, 1.0), - }, - Vertex { - pos: vec3(0.5, -0.5, -0.5), - normal: vec3(1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(0.5, -0.5, -0.5), - normal: vec3(1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(0.5, -0.5, 0.5), - normal: vec3(1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(0.0, 0.0), - }, - Vertex { - pos: vec3(0.5, 0.5, 0.5), - normal: vec3(1.0, 0.0, 0.0), - tangent: vec3(0.0, 0.0, -1.0), - uv: vec2(0.0, 1.0), - }, - //face 5 - Vertex { - pos: vec3(-0.5, -0.5, -0.5), - normal: vec3(0.0, -1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - Vertex { - pos: vec3(0.5, -0.5, -0.5), - normal: vec3(0.0, -1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 1.0), - }, - Vertex { - pos: vec3(0.5, -0.5, 0.5), - normal: vec3(0.0, -1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(0.5, -0.5, 0.5), - normal: vec3(0.0, -1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, 0.5), - normal: vec3(0.0, -1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, -0.5, -0.5), - normal: vec3(0.0, -1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - //face 6 - Vertex { - pos: vec3(-0.5, 0.5, -0.5), - normal: vec3(0.0, 1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - Vertex { - pos: vec3(0.5, 0.5, -0.5), - normal: vec3(0.0, 1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 1.0), - }, - Vertex { - pos: vec3(0.5, 0.5, 0.5), - normal: vec3(0.0, 1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(0.5, 0.5, 0.5), - normal: vec3(0.0, 1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(1.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, 0.5, 0.5), - normal: vec3(0.0, 1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 0.0), - }, - Vertex { - pos: vec3(-0.5, 0.5, -0.5), - normal: vec3(0.0, 1.0, 0.0), - tangent: vec3(1.0, 0.0, 0.0), - uv: vec2(0.0, 1.0), - }, - ] - } -} diff --git a/examples/custom_shader/src/primitive/uniforms.rs b/examples/custom_shader/src/primitive/uniforms.rs deleted file mode 100644 index 4fcb413b..00000000 --- a/examples/custom_shader/src/primitive/uniforms.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::camera::Camera; -use iced::{Color, Rectangle}; - -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -pub struct Uniforms { - camera_proj: glam::Mat4, - camera_pos: glam::Vec4, - light_color: glam::Vec4, -} - -impl Uniforms { - pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self { - let camera_proj = camera.build_view_proj_matrix(bounds); - - Self { - camera_proj, - camera_pos: camera.position(), - light_color: glam::Vec4::from(light_color.into_linear()), - } - } -} diff --git a/examples/custom_shader/src/primitive/vertex.rs b/examples/custom_shader/src/primitive/vertex.rs deleted file mode 100644 index e64cd926..00000000 --- a/examples/custom_shader/src/primitive/vertex.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::wgpu; - -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -pub struct Vertex { - pub pos: glam::Vec3, - pub normal: glam::Vec3, - pub tangent: glam::Vec3, - pub uv: glam::Vec2, -} - -impl Vertex { - const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ - //position - 0 => Float32x3, - //normal - 1 => Float32x3, - //tangent - 2 => Float32x3, - //uv - 3 => Float32x2, - ]; - - pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &Self::ATTRIBS, - } - } -} diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs index ab923093..3b291ce2 100644 --- a/examples/custom_shader/src/scene.rs +++ b/examples/custom_shader/src/scene.rs @@ -1,13 +1,21 @@ -use crate::camera::Camera; -use crate::primitive; -use crate::primitive::cube::Cube; -use glam::Vec3; +mod camera; +mod pipeline; + +use camera::Camera; +use pipeline::Pipeline; + +use crate::wgpu; +use pipeline::cube::{self, Cube}; + +use iced::mouse; +use iced::time::Duration; use iced::widget::shader; -use iced::{mouse, Color, Rectangle}; +use iced::{Color, Rectangle, Size}; + +use glam::Vec3; use rand::Rng; use std::cmp::Ordering; use std::iter; -use std::time::Duration; pub const MAX: u32 = 500; @@ -72,7 +80,7 @@ impl Scene { impl<Message> shader::Program<Message> for Scene { type State = (); - type Primitive = primitive::Primitive; + type Primitive = Primitive; fn draw( &self, @@ -80,7 +88,7 @@ impl<Message> shader::Program<Message> for Scene { _cursor: mouse::Cursor, bounds: Rectangle, ) -> Self::Primitive { - primitive::Primitive::new( + Primitive::new( &self.cubes, &self.camera, bounds, @@ -90,6 +98,85 @@ impl<Message> shader::Program<Message> for Scene { } } +/// A collection of `Cube`s that can be rendered. +#[derive(Debug)] +pub struct Primitive { + cubes: Vec<cube::Raw>, + uniforms: pipeline::Uniforms, + show_depth_buffer: bool, +} + +impl Primitive { + pub fn new( + cubes: &[Cube], + camera: &Camera, + bounds: Rectangle, + show_depth_buffer: bool, + light_color: Color, + ) -> Self { + let uniforms = pipeline::Uniforms::new(camera, bounds, light_color); + + Self { + cubes: cubes + .iter() + .map(cube::Raw::from_cube) + .collect::<Vec<cube::Raw>>(), + uniforms, + show_depth_buffer, + } + } +} + +impl shader::Primitive for Primitive { + fn prepare( + &self, + format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + _scale_factor: f32, + _transform: shader::Transformation, + storage: &mut shader::Storage, + ) { + if !storage.has::<Pipeline>() { + storage.store(Pipeline::new(device, queue, format, target_size)); + } + + let pipeline = storage.get_mut::<Pipeline>().unwrap(); + + //upload data to GPU + pipeline.update( + device, + queue, + target_size, + &self.uniforms, + self.cubes.len(), + &self.cubes, + ); + } + + fn render( + &self, + storage: &shader::Storage, + bounds: Rectangle<u32>, + target: &wgpu::TextureView, + _target_size: Size<u32>, + encoder: &mut wgpu::CommandEncoder, + ) { + //at this point our pipeline should always be initialized + let pipeline = storage.get::<Pipeline>().unwrap(); + + //render primitive + pipeline.render( + target, + encoder, + bounds, + self.cubes.len() as u32, + self.show_depth_buffer, + ); + } +} + fn rnd_origin() -> Vec3 { Vec3::new( rand::thread_rng().gen_range(-4.0..4.0), diff --git a/examples/custom_shader/src/scene/camera.rs b/examples/custom_shader/src/scene/camera.rs new file mode 100644 index 00000000..2a49c102 --- /dev/null +++ b/examples/custom_shader/src/scene/camera.rs @@ -0,0 +1,53 @@ +use glam::{mat4, vec3, vec4}; +use iced::Rectangle; + +#[derive(Copy, Clone)] +pub struct Camera { + eye: glam::Vec3, + target: glam::Vec3, + up: glam::Vec3, + fov_y: f32, + near: f32, + far: f32, +} + +impl Default for Camera { + fn default() -> Self { + Self { + eye: vec3(0.0, 2.0, 3.0), + target: glam::Vec3::ZERO, + up: glam::Vec3::Y, + fov_y: 45.0, + near: 0.1, + far: 100.0, + } + } +} + +pub const OPENGL_TO_WGPU_MATRIX: glam::Mat4 = mat4( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 0.5, 0.0), + vec4(0.0, 0.0, 0.5, 1.0), +); + +impl Camera { + pub fn build_view_proj_matrix(&self, bounds: Rectangle) -> glam::Mat4 { + //TODO looks distorted without padding; base on surface texture size instead? + let aspect_ratio = bounds.width / (bounds.height + 150.0); + + let view = glam::Mat4::look_at_rh(self.eye, self.target, self.up); + let proj = glam::Mat4::perspective_rh( + self.fov_y, + aspect_ratio, + self.near, + self.far, + ); + + OPENGL_TO_WGPU_MATRIX * proj * view + } + + pub fn position(&self) -> glam::Vec4 { + glam::Vec4::from((self.eye, 0.0)) + } +} diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs new file mode 100644 index 00000000..0967e139 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -0,0 +1,615 @@ +pub mod cube; + +mod buffer; +mod uniforms; +mod vertex; + +pub use cube::Cube; +pub use uniforms::Uniforms; + +use buffer::Buffer; +use vertex::Vertex; + +use crate::wgpu; +use crate::wgpu::util::DeviceExt; + +use iced::{Rectangle, Size}; + +const SKY_TEXTURE_SIZE: u32 = 128; + +pub struct Pipeline { + pipeline: wgpu::RenderPipeline, + vertices: wgpu::Buffer, + cubes: Buffer, + uniforms: wgpu::Buffer, + uniform_bind_group: wgpu::BindGroup, + depth_texture_size: Size<u32>, + depth_view: wgpu::TextureView, + depth_pipeline: DepthPipeline, +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, + target_size: Size<u32>, + ) -> Self { + //vertices of one cube + let vertices = + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("cubes vertex buffer"), + contents: bytemuck::cast_slice(&cube::Raw::vertices()), + usage: wgpu::BufferUsages::VERTEX, + }); + + //cube instance data + let cubes_buffer = Buffer::new( + device, + "cubes instance buffer", + std::mem::size_of::<cube::Raw>() as u64, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + //uniforms for all cubes + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("cubes uniform buffer"), + size: std::mem::size_of::<Uniforms>() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + //depth buffer + let depth_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("cubes depth texture"), + size: wgpu::Extent3d { + width: target_size.width, + height: target_size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let depth_view = + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let normal_map_data = load_normal_map_data(); + + //normal map + let normal_texture = device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("cubes normal map texture"), + size: wgpu::Extent3d { + width: 1024, + height: 1024, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + &normal_map_data, + ); + + let normal_view = + normal_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + //skybox texture for reflection/refraction + let skybox_data = load_skybox_data(); + + let skybox_texture = device.create_texture_with_data( + queue, + &wgpu::TextureDescriptor { + label: Some("cubes skybox texture"), + size: wgpu::Extent3d { + width: SKY_TEXTURE_SIZE, + height: SKY_TEXTURE_SIZE, + depth_or_array_layers: 6, //one for each face of the cube + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + &skybox_data, + ); + + let sky_view = + skybox_texture.create_view(&wgpu::TextureViewDescriptor { + label: Some("cubes skybox texture view"), + dimension: Some(wgpu::TextureViewDimension::Cube), + ..Default::default() + }); + + let sky_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("cubes skybox sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubes uniform bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::Cube, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::Filtering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ], + }); + + let uniform_bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes uniform bind group"), + layout: &uniform_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniforms.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&sky_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&sky_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView( + &normal_view, + ), + }, + ], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubes pipeline layout"), + bind_group_layouts: &[&uniform_bind_group_layout], + push_constant_ranges: &[], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("cubes shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shaders/cubes.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("cubes pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc(), cube::Raw::desc()], + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Max, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + let depth_pipeline = DepthPipeline::new( + device, + format, + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + + Self { + pipeline, + cubes: cubes_buffer, + uniforms, + uniform_bind_group, + vertices, + depth_texture_size: target_size, + depth_view, + depth_pipeline, + } + } + + fn update_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) { + if self.depth_texture_size.height != size.height + || self.depth_texture_size.width != size.width + { + let text = device.create_texture(&wgpu::TextureDescriptor { + label: Some("cubes depth texture"), + size: wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + self.depth_view = + text.create_view(&wgpu::TextureViewDescriptor::default()); + self.depth_texture_size = size; + + self.depth_pipeline.update(device, &text); + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + target_size: Size<u32>, + uniforms: &Uniforms, + num_cubes: usize, + cubes: &[cube::Raw], + ) { + //recreate depth texture if surface texture size has changed + self.update_depth_texture(device, target_size); + + // update uniforms + queue.write_buffer(&self.uniforms, 0, bytemuck::bytes_of(uniforms)); + + //resize cubes vertex buffer if cubes amount changed + let new_size = num_cubes * std::mem::size_of::<cube::Raw>(); + self.cubes.resize(device, new_size as u64); + + //always write new cube data since they are constantly rotating + queue.write_buffer(&self.cubes.raw, 0, bytemuck::cast_slice(cubes)); + } + + pub fn render( + &self, + target: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + bounds: Rectangle<u32>, + num_cubes: u32, + show_depth: bool, + ) { + { + let mut pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cubes.pipeline.pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + }, + )], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }, + ), + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_vertex_buffer(0, self.vertices.slice(..)); + pass.set_vertex_buffer(1, self.cubes.raw.slice(..)); + pass.draw(0..36, 0..num_cubes); + } + + if show_depth { + self.depth_pipeline.render(encoder, target, bounds); + } + } +} + +struct DepthPipeline { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + bind_group: wgpu::BindGroup, + sampler: wgpu::Sampler, + depth_view: wgpu::TextureView, +} + +impl DepthPipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + depth_texture: wgpu::TextureView, + ) -> Self { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("cubes.depth_pipeline.sampler"), + ..Default::default() + }); + + let bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("cubes.depth_pipeline.bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler( + wgpu::SamplerBindingType::NonFiltering, + ), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: false, + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + ], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes.depth_pipeline.bind_group"), + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView( + &depth_texture, + ), + }, + ], + }); + + let layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("cubes.depth_pipeline.layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let shader = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("cubes.depth_pipeline.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( + include_str!("../shaders/depth.wgsl"), + )), + }); + + let pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("cubes.depth_pipeline.pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + primitive: wgpu::PrimitiveState::default(), + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState::default(), + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + }); + + Self { + pipeline, + bind_group_layout, + bind_group, + sampler, + depth_view: depth_texture, + } + } + + pub fn update( + &mut self, + device: &wgpu::Device, + depth_texture: &wgpu::Texture, + ) { + self.depth_view = + depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + self.bind_group = + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("cubes.depth_pipeline.bind_group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView( + &self.depth_view, + ), + }, + ], + }); + } + + pub fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + bounds: Rectangle<u32>, + ) { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cubes.pipeline.depth_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some( + wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: None, + stencil_ops: None, + }, + ), + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..6, 0..1); + } +} + +fn load_skybox_data() -> Vec<u8> { + let pos_x: &[u8] = include_bytes!("../textures/skybox/pos_x.jpg"); + let neg_x: &[u8] = include_bytes!("../textures/skybox/neg_x.jpg"); + let pos_y: &[u8] = include_bytes!("../textures/skybox/pos_y.jpg"); + let neg_y: &[u8] = include_bytes!("../textures/skybox/neg_y.jpg"); + let pos_z: &[u8] = include_bytes!("../textures/skybox/pos_z.jpg"); + let neg_z: &[u8] = include_bytes!("../textures/skybox/neg_z.jpg"); + + let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z]; + + data.iter().fold(vec![], |mut acc, bytes| { + let i = image::load_from_memory_with_format( + bytes, + image::ImageFormat::Jpeg, + ) + .unwrap() + .to_rgba8() + .into_raw(); + + acc.extend(i); + acc + }) +} + +fn load_normal_map_data() -> Vec<u8> { + let bytes: &[u8] = include_bytes!("../textures/ice_cube_normal_map.png"); + + image::load_from_memory_with_format(bytes, image::ImageFormat::Png) + .unwrap() + .to_rgba8() + .into_raw() +} diff --git a/examples/custom_shader/src/scene/pipeline/buffer.rs b/examples/custom_shader/src/scene/pipeline/buffer.rs new file mode 100644 index 00000000..ef4c41c9 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/buffer.rs @@ -0,0 +1,41 @@ +use crate::wgpu; + +// A custom buffer container for dynamic resizing. +pub struct Buffer { + pub raw: wgpu::Buffer, + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, +} + +impl Buffer { + pub fn new( + device: &wgpu::Device, + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, + ) -> Self { + Self { + raw: device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage, + mapped_at_creation: false, + }), + label, + size, + usage, + } + } + + pub fn resize(&mut self, device: &wgpu::Device, new_size: u64) { + if new_size > self.size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(self.label), + size: new_size, + usage: self.usage, + mapped_at_creation: false, + }); + } + } +} diff --git a/examples/custom_shader/src/scene/pipeline/cube.rs b/examples/custom_shader/src/scene/pipeline/cube.rs new file mode 100644 index 00000000..de8bad6c --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/cube.rs @@ -0,0 +1,326 @@ +use crate::scene::pipeline::Vertex; +use crate::wgpu; + +use glam::{vec2, vec3, Vec3}; +use rand::{thread_rng, Rng}; + +/// A single instance of a cube. +#[derive(Debug, Clone)] +pub struct Cube { + pub rotation: glam::Quat, + pub position: Vec3, + pub size: f32, + rotation_dir: f32, + rotation_axis: glam::Vec3, +} + +impl Default for Cube { + fn default() -> Self { + Self { + rotation: glam::Quat::IDENTITY, + position: glam::Vec3::ZERO, + size: 0.1, + rotation_dir: 1.0, + rotation_axis: glam::Vec3::Y, + } + } +} + +impl Cube { + pub fn new(size: f32, origin: Vec3) -> Self { + let rnd = thread_rng().gen_range(0.0..=1.0f32); + + Self { + rotation: glam::Quat::IDENTITY, + position: origin + Vec3::new(0.1, 0.1, 0.1), + size, + rotation_dir: if rnd <= 0.5 { -1.0 } else { 1.0 }, + rotation_axis: if rnd <= 0.33 { + glam::Vec3::Y + } else if rnd <= 0.66 { + glam::Vec3::X + } else { + glam::Vec3::Z + }, + } + } + + pub fn update(&mut self, size: f32, time: f32) { + self.rotation = glam::Quat::from_axis_angle( + self.rotation_axis, + time / 2.0 * self.rotation_dir, + ); + self.size = size; + } +} + +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable, Debug)] +#[repr(C)] +pub struct Raw { + transformation: glam::Mat4, + normal: glam::Mat3, + _padding: [f32; 3], +} + +impl Raw { + const ATTRIBS: [wgpu::VertexAttribute; 7] = wgpu::vertex_attr_array![ + //cube transformation matrix + 4 => Float32x4, + 5 => Float32x4, + 6 => Float32x4, + 7 => Float32x4, + //normal rotation matrix + 8 => Float32x3, + 9 => Float32x3, + 10 => Float32x3, + ]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &Self::ATTRIBS, + } + } +} + +impl Raw { + pub fn from_cube(cube: &Cube) -> Raw { + Raw { + transformation: glam::Mat4::from_scale_rotation_translation( + glam::vec3(cube.size, cube.size, cube.size), + cube.rotation, + cube.position, + ), + normal: glam::Mat3::from_quat(cube.rotation), + _padding: [0.0; 3], + } + } + + pub fn vertices() -> [Vertex; 36] { + [ + //face 1 + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, 0.0, -1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 2 + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, 0.0, 1.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 3 + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(-1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + //face 4 + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(1.0, 0.0, 0.0), + tangent: vec3(0.0, 0.0, -1.0), + uv: vec2(0.0, 1.0), + }, + //face 5 + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, 0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, -0.5, -0.5), + normal: vec3(0.0, -1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + //face 6 + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 1.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(1.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, 0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 0.0), + }, + Vertex { + pos: vec3(-0.5, 0.5, -0.5), + normal: vec3(0.0, 1.0, 0.0), + tangent: vec3(1.0, 0.0, 0.0), + uv: vec2(0.0, 1.0), + }, + ] + } +} diff --git a/examples/custom_shader/src/scene/pipeline/uniforms.rs b/examples/custom_shader/src/scene/pipeline/uniforms.rs new file mode 100644 index 00000000..1eac8292 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/uniforms.rs @@ -0,0 +1,23 @@ +use crate::scene::Camera; + +use iced::{Color, Rectangle}; + +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Uniforms { + camera_proj: glam::Mat4, + camera_pos: glam::Vec4, + light_color: glam::Vec4, +} + +impl Uniforms { + pub fn new(camera: &Camera, bounds: Rectangle, light_color: Color) -> Self { + let camera_proj = camera.build_view_proj_matrix(bounds); + + Self { + camera_proj, + camera_pos: camera.position(), + light_color: glam::Vec4::from(light_color.into_linear()), + } + } +} diff --git a/examples/custom_shader/src/scene/pipeline/vertex.rs b/examples/custom_shader/src/scene/pipeline/vertex.rs new file mode 100644 index 00000000..e64cd926 --- /dev/null +++ b/examples/custom_shader/src/scene/pipeline/vertex.rs @@ -0,0 +1,31 @@ +use crate::wgpu; + +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub struct Vertex { + pub pos: glam::Vec3, + pub normal: glam::Vec3, + pub tangent: glam::Vec3, + pub uv: glam::Vec2, +} + +impl Vertex { + const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ + //position + 0 => Float32x3, + //normal + 1 => Float32x3, + //tangent + 2 => Float32x3, + //uv + 3 => Float32x2, + ]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBS, + } + } +} diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fe6214db..ca140627 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -17,6 +17,7 @@ use crate::renderer::wgpu::primitive::pipeline; use std::marker::PhantomData; +pub use crate::graphics::Transformation; pub use crate::renderer::wgpu::wgpu; pub use pipeline::{Primitive, Storage}; -- cgit From 77dfa60c9640236df8b56084a6b1c58826b51e7e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 15:49:09 +0100 Subject: Move `textures` directory outside of `src` in `custom_shader` example --- examples/custom_shader/src/scene/pipeline.rs | 14 +++++++------- .../src/textures/ice_cube_normal_map.png | Bin 1773656 -> 0 bytes examples/custom_shader/src/textures/skybox/neg_x.jpg | Bin 7549 -> 0 bytes examples/custom_shader/src/textures/skybox/neg_y.jpg | Bin 2722 -> 0 bytes examples/custom_shader/src/textures/skybox/neg_z.jpg | Bin 3986 -> 0 bytes examples/custom_shader/src/textures/skybox/pos_x.jpg | Bin 5522 -> 0 bytes examples/custom_shader/src/textures/skybox/pos_y.jpg | Bin 3382 -> 0 bytes examples/custom_shader/src/textures/skybox/pos_z.jpg | Bin 5205 -> 0 bytes .../custom_shader/textures/ice_cube_normal_map.png | Bin 0 -> 1773656 bytes examples/custom_shader/textures/skybox/neg_x.jpg | Bin 0 -> 7549 bytes examples/custom_shader/textures/skybox/neg_y.jpg | Bin 0 -> 2722 bytes examples/custom_shader/textures/skybox/neg_z.jpg | Bin 0 -> 3986 bytes examples/custom_shader/textures/skybox/pos_x.jpg | Bin 0 -> 5522 bytes examples/custom_shader/textures/skybox/pos_y.jpg | Bin 0 -> 3382 bytes examples/custom_shader/textures/skybox/pos_z.jpg | Bin 0 -> 5205 bytes 15 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 examples/custom_shader/src/textures/ice_cube_normal_map.png delete mode 100644 examples/custom_shader/src/textures/skybox/neg_x.jpg delete mode 100644 examples/custom_shader/src/textures/skybox/neg_y.jpg delete mode 100644 examples/custom_shader/src/textures/skybox/neg_z.jpg delete mode 100644 examples/custom_shader/src/textures/skybox/pos_x.jpg delete mode 100644 examples/custom_shader/src/textures/skybox/pos_y.jpg delete mode 100644 examples/custom_shader/src/textures/skybox/pos_z.jpg create mode 100644 examples/custom_shader/textures/ice_cube_normal_map.png create mode 100644 examples/custom_shader/textures/skybox/neg_x.jpg create mode 100644 examples/custom_shader/textures/skybox/neg_y.jpg create mode 100644 examples/custom_shader/textures/skybox/neg_z.jpg create mode 100644 examples/custom_shader/textures/skybox/pos_x.jpg create mode 100644 examples/custom_shader/textures/skybox/pos_y.jpg create mode 100644 examples/custom_shader/textures/skybox/pos_z.jpg diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 0967e139..3956c12e 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -582,12 +582,12 @@ impl DepthPipeline { } fn load_skybox_data() -> Vec<u8> { - let pos_x: &[u8] = include_bytes!("../textures/skybox/pos_x.jpg"); - let neg_x: &[u8] = include_bytes!("../textures/skybox/neg_x.jpg"); - let pos_y: &[u8] = include_bytes!("../textures/skybox/pos_y.jpg"); - let neg_y: &[u8] = include_bytes!("../textures/skybox/neg_y.jpg"); - let pos_z: &[u8] = include_bytes!("../textures/skybox/pos_z.jpg"); - let neg_z: &[u8] = include_bytes!("../textures/skybox/neg_z.jpg"); + let pos_x: &[u8] = include_bytes!("../../textures/skybox/pos_x.jpg"); + let neg_x: &[u8] = include_bytes!("../../textures/skybox/neg_x.jpg"); + let pos_y: &[u8] = include_bytes!("../../textures/skybox/pos_y.jpg"); + let neg_y: &[u8] = include_bytes!("../../textures/skybox/neg_y.jpg"); + let pos_z: &[u8] = include_bytes!("../../textures/skybox/pos_z.jpg"); + let neg_z: &[u8] = include_bytes!("../../textures/skybox/neg_z.jpg"); let data: [&[u8]; 6] = [pos_x, neg_x, pos_y, neg_y, pos_z, neg_z]; @@ -606,7 +606,7 @@ fn load_skybox_data() -> Vec<u8> { } fn load_normal_map_data() -> Vec<u8> { - let bytes: &[u8] = include_bytes!("../textures/ice_cube_normal_map.png"); + let bytes: &[u8] = include_bytes!("../../textures/ice_cube_normal_map.png"); image::load_from_memory_with_format(bytes, image::ImageFormat::Png) .unwrap() diff --git a/examples/custom_shader/src/textures/ice_cube_normal_map.png b/examples/custom_shader/src/textures/ice_cube_normal_map.png deleted file mode 100644 index 7b4b7228..00000000 Binary files a/examples/custom_shader/src/textures/ice_cube_normal_map.png and /dev/null differ diff --git a/examples/custom_shader/src/textures/skybox/neg_x.jpg b/examples/custom_shader/src/textures/skybox/neg_x.jpg deleted file mode 100644 index 00cc783d..00000000 Binary files a/examples/custom_shader/src/textures/skybox/neg_x.jpg and /dev/null differ diff --git a/examples/custom_shader/src/textures/skybox/neg_y.jpg b/examples/custom_shader/src/textures/skybox/neg_y.jpg deleted file mode 100644 index 548f6445..00000000 Binary files a/examples/custom_shader/src/textures/skybox/neg_y.jpg and /dev/null differ diff --git a/examples/custom_shader/src/textures/skybox/neg_z.jpg b/examples/custom_shader/src/textures/skybox/neg_z.jpg deleted file mode 100644 index 5698512e..00000000 Binary files a/examples/custom_shader/src/textures/skybox/neg_z.jpg and /dev/null differ diff --git a/examples/custom_shader/src/textures/skybox/pos_x.jpg b/examples/custom_shader/src/textures/skybox/pos_x.jpg deleted file mode 100644 index dddecba7..00000000 Binary files a/examples/custom_shader/src/textures/skybox/pos_x.jpg and /dev/null differ diff --git a/examples/custom_shader/src/textures/skybox/pos_y.jpg b/examples/custom_shader/src/textures/skybox/pos_y.jpg deleted file mode 100644 index 361427fd..00000000 Binary files a/examples/custom_shader/src/textures/skybox/pos_y.jpg and /dev/null differ diff --git a/examples/custom_shader/src/textures/skybox/pos_z.jpg b/examples/custom_shader/src/textures/skybox/pos_z.jpg deleted file mode 100644 index 0085a49e..00000000 Binary files a/examples/custom_shader/src/textures/skybox/pos_z.jpg and /dev/null differ diff --git a/examples/custom_shader/textures/ice_cube_normal_map.png b/examples/custom_shader/textures/ice_cube_normal_map.png new file mode 100644 index 00000000..7b4b7228 Binary files /dev/null and b/examples/custom_shader/textures/ice_cube_normal_map.png differ diff --git a/examples/custom_shader/textures/skybox/neg_x.jpg b/examples/custom_shader/textures/skybox/neg_x.jpg new file mode 100644 index 00000000..00cc783d Binary files /dev/null and b/examples/custom_shader/textures/skybox/neg_x.jpg differ diff --git a/examples/custom_shader/textures/skybox/neg_y.jpg b/examples/custom_shader/textures/skybox/neg_y.jpg new file mode 100644 index 00000000..548f6445 Binary files /dev/null and b/examples/custom_shader/textures/skybox/neg_y.jpg differ diff --git a/examples/custom_shader/textures/skybox/neg_z.jpg b/examples/custom_shader/textures/skybox/neg_z.jpg new file mode 100644 index 00000000..5698512e Binary files /dev/null and b/examples/custom_shader/textures/skybox/neg_z.jpg differ diff --git a/examples/custom_shader/textures/skybox/pos_x.jpg b/examples/custom_shader/textures/skybox/pos_x.jpg new file mode 100644 index 00000000..dddecba7 Binary files /dev/null and b/examples/custom_shader/textures/skybox/pos_x.jpg differ diff --git a/examples/custom_shader/textures/skybox/pos_y.jpg b/examples/custom_shader/textures/skybox/pos_y.jpg new file mode 100644 index 00000000..361427fd Binary files /dev/null and b/examples/custom_shader/textures/skybox/pos_y.jpg differ diff --git a/examples/custom_shader/textures/skybox/pos_z.jpg b/examples/custom_shader/textures/skybox/pos_z.jpg new file mode 100644 index 00000000..0085a49e Binary files /dev/null and b/examples/custom_shader/textures/skybox/pos_z.jpg differ -- cgit From 74b920a708afc2407cb495a2953e97114f7773b1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 15:52:55 +0100 Subject: Remove unnecessary `self` in `iced_style::theme` --- style/src/theme.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style/src/theme.rs b/style/src/theme.rs index cc31d72d..47010728 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -1,7 +1,7 @@ //! Use the built-in theme and styles. pub mod palette; -pub use self::palette::Palette; +pub use palette::Palette; use crate::application; use crate::button; -- cgit From 8f384c83be242f9318685530ee52dd6c27b2bb62 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 15:54:10 +0100 Subject: Remove unsused `custom.rs` file in `iced_wgpu` --- wgpu/src/custom.rs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 wgpu/src/custom.rs diff --git a/wgpu/src/custom.rs b/wgpu/src/custom.rs deleted file mode 100644 index 98e2b396..00000000 --- a/wgpu/src/custom.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Draw custom primitives. -use crate::core::{Rectangle, Size}; -use crate::graphics::Transformation; -use crate::primitive; - -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt::Debug; -- cgit From 0968c5b64a528ff92a5a93f6586eef557546da25 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 14 Nov 2023 15:58:32 +0100 Subject: Remove unused import in `custom_shader` example --- examples/custom_shader/src/scene/pipeline.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 3956c12e..94c6c562 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -4,7 +4,6 @@ mod buffer; mod uniforms; mod vertex; -pub use cube::Cube; pub use uniforms::Uniforms; use buffer::Buffer; -- cgit From 7dd32f3be43c72e11dac5e07918e9ad6d36b6555 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 15 Nov 2023 10:27:26 +0100 Subject: Update `itertools` dependency for `game_of_life` example --- examples/game_of_life/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/game_of_life/Cargo.toml b/examples/game_of_life/Cargo.toml index 9b291de8..7596844c 100644 --- a/examples/game_of_life/Cargo.toml +++ b/examples/game_of_life/Cargo.toml @@ -9,7 +9,7 @@ publish = false iced.workspace = true iced.features = ["debug", "canvas", "tokio"] -itertools = "0.11" +itertools = "0.12" rustc-hash.workspace = true tokio = { workspace = true, features = ["sync"] } tracing-subscriber = "0.3" -- cgit From 25006b9c6f2ae909d86871d3a13631d518c07158 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 21 Nov 2023 14:41:22 +0100 Subject: Fix `Overlay` composition Translations were not easily composable. --- core/src/overlay.rs | 3 ++- core/src/overlay/element.rs | 20 ++++++++++++++++---- core/src/overlay/group.rs | 9 +++++---- runtime/src/overlay/nested.rs | 18 +++++++++++++----- runtime/src/user_interface.rs | 30 +++++++++++++++++++++++------- widget/src/lazy.rs | 5 +++-- widget/src/lazy/component.rs | 3 ++- widget/src/lazy/responsive.rs | 6 ++++-- widget/src/overlay/menu.rs | 1 + widget/src/tooltip.rs | 1 + 10 files changed, 70 insertions(+), 26 deletions(-) diff --git a/core/src/overlay.rs b/core/src/overlay.rs index f71f25f7..af10afee 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -11,7 +11,7 @@ use crate::mouse; use crate::renderer; use crate::widget; use crate::widget::Tree; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; /// An interactive component that can be displayed on top of other widgets. pub trait Overlay<Message, Renderer> @@ -29,6 +29,7 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node; /// Draws the [`Overlay`] using the associated `Renderer`. diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 3dd58f9b..a279fe28 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -13,6 +13,7 @@ use std::any::Any; #[allow(missing_debug_implementations)] pub struct Element<'a, Message, Renderer> { position: Point, + translation: Vector, overlay: Box<dyn Overlay<Message, Renderer> + 'a>, } @@ -25,7 +26,11 @@ where position: Point, overlay: Box<dyn Overlay<Message, Renderer> + 'a>, ) -> Self { - Self { position, overlay } + Self { + position, + overlay, + translation: Vector::ZERO, + } } /// Returns the position of the [`Element`]. @@ -36,6 +41,7 @@ where /// Translates the [`Element`]. pub fn translate(mut self, translation: Vector) -> Self { self.position = self.position + translation; + self.translation = self.translation + translation; self } @@ -48,6 +54,7 @@ where { Element { position: self.position, + translation: self.translation, overlay: Box::new(Map::new(self.overlay, f)), } } @@ -59,8 +66,12 @@ where bounds: Size, translation: Vector, ) -> layout::Node { - self.overlay - .layout(renderer, bounds, self.position + translation) + self.overlay.layout( + renderer, + bounds, + self.position + translation, + self.translation + translation, + ) } /// Processes a runtime [`Event`]. @@ -154,8 +165,9 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { - self.content.layout(renderer, bounds, position) + self.content.layout(renderer, bounds, position, translation) } fn operate( diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index dccf6dba..e1e9727a 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -4,7 +4,9 @@ use crate::mouse; use crate::overlay; use crate::renderer; use crate::widget; -use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; +use crate::{ + Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size, Vector, +}; /// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] /// children. @@ -64,10 +66,9 @@ where &mut self, renderer: &Renderer, bounds: Size, - position: Point, + _position: Point, + translation: Vector, ) -> layout::Node { - let translation = position - Point::ORIGIN; - layout::Node::with_children( bounds, self.children diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index 062ccc72..b7cfc918 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -4,7 +4,9 @@ use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget; -use crate::core::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size}; +use crate::core::{ + Clipboard, Event, Layout, Point, Rectangle, Shell, Size, Vector, +}; /// An overlay container that displays nested overlays #[allow(missing_debug_implementations)] @@ -34,18 +36,18 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { fn recurse<Message, Renderer>( element: &mut overlay::Element<'_, Message, Renderer>, renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node where Renderer: renderer::Renderer, { - let translation = position - Point::ORIGIN; - let node = element.layout(renderer, bounds, translation); if let Some(mut nested) = @@ -55,7 +57,13 @@ where node.size(), vec![ node, - recurse(&mut nested, renderer, bounds, position), + recurse( + &mut nested, + renderer, + bounds, + position, + translation, + ), ], ) } else { @@ -63,7 +71,7 @@ where } } - recurse(&mut self.overlay, renderer, bounds, position) + recurse(&mut self.overlay, renderer, bounds, position, translation) } /// Draws the [`Nested`] overlay using the associated `Renderer`. diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index dae9e0ac..3594ac18 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -5,7 +5,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget; use crate::core::window; -use crate::core::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; +use crate::core::{ + Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, +}; use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -199,7 +201,8 @@ where let bounds = self.bounds; let mut overlay = manual_overlay.as_mut().unwrap(); - let mut layout = overlay.layout(renderer, bounds, Point::ORIGIN); + let mut layout = + overlay.layout(renderer, bounds, Point::ORIGIN, Vector::ZERO); let mut event_statuses = Vec::new(); for event in events.iter().cloned() { @@ -253,8 +256,12 @@ where overlay = manual_overlay.as_mut().unwrap(); shell.revalidate_layout(|| { - layout = - overlay.layout(renderer, bounds, Point::ORIGIN); + layout = overlay.layout( + renderer, + bounds, + Point::ORIGIN, + Vector::ZERO, + ); }); } @@ -448,7 +455,12 @@ where .map(overlay::Nested::new) { let overlay_layout = self.overlay.take().unwrap_or_else(|| { - overlay.layout(renderer, self.bounds, Point::ORIGIN) + overlay.layout( + renderer, + self.bounds, + Point::ORIGIN, + Vector::ZERO, + ) }); let cursor = if cursor @@ -566,8 +578,12 @@ where .map(overlay::Nested::new) { if self.overlay.is_none() { - self.overlay = - Some(overlay.layout(renderer, self.bounds, Point::ORIGIN)); + self.overlay = Some(overlay.layout( + renderer, + self.bounds, + Point::ORIGIN, + Vector::ZERO, + )); } overlay.operate( diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 589dd938..167a055d 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -18,7 +18,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; use crate::core::Element; use crate::core::{ - self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, + self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector, }; use crate::runtime::overlay::Nested; @@ -333,9 +333,10 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - overlay.layout(renderer, bounds, position) + overlay.layout(renderer, bounds, position, translation) }) .unwrap_or_default() } diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index d454b72b..ad0c3823 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -577,9 +577,10 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - overlay.layout(renderer, bounds, position) + overlay.layout(renderer, bounds, position, translation) }) .unwrap_or_default() } diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index ed471988..86d37b6c 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -6,7 +6,8 @@ use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, + self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, + Widget, }; use crate::horizontal_space; use crate::runtime::overlay::Nested; @@ -367,9 +368,10 @@ where renderer: &Renderer, bounds: Size, position: Point, + translation: Vector, ) -> layout::Node { self.with_overlay_maybe(|overlay| { - overlay.layout(renderer, bounds, position) + overlay.layout(renderer, bounds, position, translation) }) .unwrap_or_default() } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b293f9fa..5098fa17 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -236,6 +236,7 @@ where renderer: &Renderer, bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index b041d2e9..d5ee3de2 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -325,6 +325,7 @@ where renderer: &Renderer, bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let viewport = Rectangle::with_size(bounds); -- cgit From 921ddec1285027a6a2feb88b0a5c29ec8f942f8b Mon Sep 17 00:00:00 2001 From: arslee07 <mail@arslee.me> Date: Wed, 22 Nov 2023 00:32:01 +0900 Subject: Use the correct GIF for the progress bar example --- examples/progress_bar/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/progress_bar/README.md b/examples/progress_bar/README.md index 1268ac6b..a87829c6 100644 --- a/examples/progress_bar/README.md +++ b/examples/progress_bar/README.md @@ -5,7 +5,7 @@ A simple progress bar that can be filled by using a slider. The __[`main`]__ file contains all the code of the example. <div align="center"> - <img src="https://iced.rs/examples/pokedex.gif"> + <img src="https://iced.rs/examples/progress_bar.gif"> </div> You can run it with `cargo run`: -- cgit From a1439071d691be8096ce956df90d1553fe5b3694 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 21 Nov 2023 18:53:31 +0100 Subject: Remove unused `position` argument in `overlay::Nested` --- runtime/src/overlay/nested.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index b7cfc918..4256efb7 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -35,14 +35,13 @@ where &mut self, renderer: &Renderer, bounds: Size, - position: Point, + _position: Point, translation: Vector, ) -> layout::Node { fn recurse<Message, Renderer>( element: &mut overlay::Element<'_, Message, Renderer>, renderer: &Renderer, bounds: Size, - position: Point, translation: Vector, ) -> layout::Node where @@ -57,13 +56,7 @@ where node.size(), vec![ node, - recurse( - &mut nested, - renderer, - bounds, - position, - translation, - ), + recurse(&mut nested, renderer, bounds, translation), ], ) } else { @@ -71,7 +64,7 @@ where } } - recurse(&mut self.overlay, renderer, bounds, position, translation) + recurse(&mut self.overlay, renderer, bounds, translation) } /// Draws the [`Nested`] overlay using the associated `Renderer`. -- cgit From 89e3de7c08dc07eefbcc2617f0da63282aa1c8ef Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 21 Nov 2023 18:55:53 +0100 Subject: Fix `modal` and `toast` examples --- examples/modal/src/main.rs | 2 ++ examples/toast/src/main.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 3b69f5e6..acb14372 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -231,6 +231,7 @@ mod modal { use iced::mouse; use iced::{ BorderRadius, Color, Element, Event, Length, Point, Rectangle, Size, + Vector, }; /// A widget that centers a modal element over some base element @@ -413,6 +414,7 @@ mod modal { renderer: &Renderer, _bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let limits = layout::Limits::new(Size::ZERO, self.size) .width(Length::Fill) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 5b089e8a..934049d5 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -511,6 +511,7 @@ mod toast { renderer: &Renderer, bounds: Size, position: Point, + _translation: Vector, ) -> layout::Node { let limits = layout::Limits::new(Size::ZERO, bounds) .width(Length::Fill) -- cgit From f67387f2d80e744618a5f4f107509ba24802146c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 21 Nov 2023 18:11:31 +0100 Subject: Invalidate layout when `Tooltip` changes `overlay` --- widget/src/tooltip.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index b041d2e9..9745bc1c 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -157,11 +157,19 @@ where ) -> event::Status { let state = tree.state.downcast_mut::<State>(); + let was_idle = *state == State::Idle; + *state = cursor .position_over(layout.bounds()) .map(|cursor_position| State::Hovered { cursor_position }) .unwrap_or_default(); + let is_idle = *state == State::Idle; + + if was_idle != is_idle { + shell.invalidate_layout(); + } + self.content.as_widget_mut().on_event( &mut tree.children[0], event, @@ -289,7 +297,7 @@ pub enum Position { Right, } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] enum State { #[default] Idle, -- cgit From ab7dae554cac801aeed5d9aa4d3850d50be86263 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 28 Nov 2023 23:13:38 +0100 Subject: Provide actual bounds to `Shader` primitives ... and allow for proper translation and scissoring. --- examples/custom_shader/src/main.rs | 21 ++++++++------------- examples/custom_shader/src/scene.rs | 6 +++--- examples/custom_shader/src/scene/pipeline.rs | 21 +++++++++++++-------- wgpu/src/backend.rs | 8 ++++---- wgpu/src/layer.rs | 15 +++++++-------- wgpu/src/layer/pipeline.rs | 17 +++++++++++++++++ wgpu/src/primitive/pipeline.rs | 5 ++--- widget/src/shader.rs | 1 - 8 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 wgpu/src/layer/pipeline.rs diff --git a/examples/custom_shader/src/main.rs b/examples/custom_shader/src/main.rs index 2eb1ac4a..3bfa3a43 100644 --- a/examples/custom_shader/src/main.rs +++ b/examples/custom_shader/src/main.rs @@ -5,9 +5,7 @@ use scene::Scene; use iced::executor; use iced::time::Instant; use iced::widget::shader::wgpu; -use iced::widget::{ - checkbox, column, container, row, shader, slider, text, vertical_space, -}; +use iced::widget::{checkbox, column, container, row, shader, slider, text}; use iced::window; use iced::{ Alignment, Application, Color, Command, Element, Length, Renderer, @@ -138,21 +136,18 @@ impl Application for IcedCubes { let controls = column![top_controls, bottom_controls,] .spacing(10) + .padding(20) .align_items(Alignment::Center); let shader = shader(&self.scene).width(Length::Fill).height(Length::Fill); - container( - column![shader, controls, vertical_space(20),] - .spacing(40) - .align_items(Alignment::Center), - ) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + container(column![shader, controls].align_items(Alignment::Center)) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() } fn subscription(&self) -> Subscription<Self::Message> { diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs index 3b291ce2..a35efdd9 100644 --- a/examples/custom_shader/src/scene.rs +++ b/examples/custom_shader/src/scene.rs @@ -133,9 +133,9 @@ impl shader::Primitive for Primitive { format: wgpu::TextureFormat, device: &wgpu::Device, queue: &wgpu::Queue, + _bounds: Rectangle, target_size: Size<u32>, _scale_factor: f32, - _transform: shader::Transformation, storage: &mut shader::Storage, ) { if !storage.has::<Pipeline>() { @@ -158,9 +158,9 @@ impl shader::Primitive for Primitive { fn render( &self, storage: &shader::Storage, - bounds: Rectangle<u32>, target: &wgpu::TextureView, _target_size: Size<u32>, + viewport: Rectangle<u32>, encoder: &mut wgpu::CommandEncoder, ) { //at this point our pipeline should always be initialized @@ -170,7 +170,7 @@ impl shader::Primitive for Primitive { pipeline.render( target, encoder, - bounds, + viewport, self.cubes.len() as u32, self.show_depth_buffer, ); diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 94c6c562..124b421f 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -351,7 +351,7 @@ impl Pipeline { &self, target: &wgpu::TextureView, encoder: &mut wgpu::CommandEncoder, - bounds: Rectangle<u32>, + viewport: Rectangle<u32>, num_cubes: u32, show_depth: bool, ) { @@ -384,10 +384,10 @@ impl Pipeline { }); pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - bounds.height, + viewport.x, + viewport.y, + viewport.width, + viewport.height, ); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.uniform_bind_group, &[]); @@ -397,7 +397,7 @@ impl Pipeline { } if show_depth { - self.depth_pipeline.render(encoder, target, bounds); + self.depth_pipeline.render(encoder, target, viewport); } } } @@ -550,7 +550,7 @@ impl DepthPipeline { &self, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, - bounds: Rectangle<u32>, + viewport: Rectangle<u32>, ) { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("cubes.pipeline.depth_pass"), @@ -573,7 +573,12 @@ impl DepthPipeline { occlusion_query_set: None, }); - pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height); + pass.set_scissor_rect( + viewport.x, + viewport.y, + viewport.width, + viewport.height, + ); pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.bind_group, &[]); pass.draw(0..6, 0..1); diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 88caad06..25134d68 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -192,9 +192,9 @@ impl Backend { format, device, queue, + pipeline.bounds, target_size, scale_factor, - transformation, &mut self.pipeline_storage, ); } @@ -327,17 +327,17 @@ impl Backend { let _ = ManuallyDrop::into_inner(render_pass); for pipeline in &layer.pipelines { - let bounds = (pipeline.bounds * scale_factor).snap(); + let viewport = (pipeline.viewport * scale_factor).snap(); - if bounds.width < 1 || bounds.height < 1 { + if viewport.width < 1 || viewport.height < 1 { continue; } pipeline.primitive.render( &self.pipeline_storage, - bounds, target, target_size, + viewport, encoder, ); } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 33aaf670..98e49f1a 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,11 +1,13 @@ //! Organize rendering primitives into a flattened list of layers. mod image; +mod pipeline; mod text; pub mod mesh; pub use image::Image; pub use mesh::Mesh; +pub use pipeline::Pipeline; pub use text::Text; use crate::core; @@ -36,7 +38,7 @@ pub struct Layer<'a> { pub images: Vec<Image>, /// The custom pipelines of this [`Layer`]. - pub pipelines: Vec<primitive::Pipeline>, + pub pipelines: Vec<Pipeline>, } impl<'a> Layer<'a> { @@ -314,17 +316,14 @@ impl<'a> Layer<'a> { }, primitive::Custom::Pipeline(pipeline) => { let layer = &mut layers[current_layer]; - - let bounds = Rectangle::new( - Point::new(translation.x, translation.y), - pipeline.bounds.size(), - ); + let bounds = pipeline.bounds + translation; if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.pipelines.push(primitive::Pipeline { - bounds: clip_bounds, + layer.pipelines.push(Pipeline { + bounds, + viewport: clip_bounds, primitive: pipeline.primitive.clone(), }); } diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs new file mode 100644 index 00000000..6dfe6750 --- /dev/null +++ b/wgpu/src/layer/pipeline.rs @@ -0,0 +1,17 @@ +use crate::core::Rectangle; +use crate::primitive::pipeline::Primitive; + +use std::sync::Arc; + +#[derive(Clone, Debug)] +/// A custom primitive which can be used to render primitives associated with a custom pipeline. +pub struct Pipeline { + /// The bounds of the [`Pipeline`]. + pub bounds: Rectangle, + + /// The viewport of the [`Pipeline`]. + pub viewport: Rectangle, + + /// The [`Primitive`] to render. + pub primitive: Arc<dyn Primitive>, +} diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs index 5dbd6697..302e38f6 100644 --- a/wgpu/src/primitive/pipeline.rs +++ b/wgpu/src/primitive/pipeline.rs @@ -1,6 +1,5 @@ //! Draw primitives using custom pipelines. use crate::core::{Rectangle, Size}; -use crate::graphics::Transformation; use std::any::{Any, TypeId}; use std::collections::HashMap; @@ -41,9 +40,9 @@ pub trait Primitive: Debug + Send + Sync + 'static { format: wgpu::TextureFormat, device: &wgpu::Device, queue: &wgpu::Queue, + bounds: Rectangle, target_size: Size<u32>, scale_factor: f32, - transform: Transformation, storage: &mut Storage, ); @@ -51,9 +50,9 @@ pub trait Primitive: Debug + Send + Sync + 'static { fn render( &self, storage: &Storage, - bounds: Rectangle<u32>, target: &wgpu::TextureView, target_size: Size<u32>, + viewport: Rectangle<u32>, encoder: &mut wgpu::CommandEncoder, ); } diff --git a/widget/src/shader.rs b/widget/src/shader.rs index ca140627..fe6214db 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -17,7 +17,6 @@ use crate::renderer::wgpu::primitive::pipeline; use std::marker::PhantomData; -pub use crate::graphics::Transformation; pub use crate::renderer::wgpu::wgpu; pub use pipeline::{Primitive, Storage}; -- cgit From 3b7d479534d9114ed12bb5d9ccd910e85d5c13c7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 00:12:48 +0100 Subject: Implement `Command::run` for executing a `Stream` to completion --- futures/src/runtime.rs | 25 ++++++++++++++++++++++++- runtime/src/command.rs | 14 +++++++++++++- runtime/src/command/action.rs | 9 ++++++++- winit/src/application.rs | 3 +++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/futures/src/runtime.rs b/futures/src/runtime.rs index 16111b36..cac7b7e1 100644 --- a/futures/src/runtime.rs +++ b/futures/src/runtime.rs @@ -1,7 +1,7 @@ //! Run commands and keep track of subscriptions. use crate::core::event::{self, Event}; use crate::subscription; -use crate::{BoxFuture, Executor, MaybeSend}; +use crate::{BoxFuture, BoxStream, Executor, MaybeSend}; use futures::{channel::mpsc, Sink}; use std::marker::PhantomData; @@ -69,6 +69,29 @@ where self.executor.spawn(future); } + /// Runs a [`Stream`] in the [`Runtime`] until completion. + /// + /// The resulting `Message`s will be forwarded to the `Sender` of the + /// [`Runtime`]. + /// + /// [`Stream`]: BoxStream + pub fn run(&mut self, stream: BoxStream<Message>) { + use futures::{FutureExt, StreamExt}; + + let sender = self.sender.clone(); + let future = + stream.map(Ok).forward(sender).map(|result| match result { + Ok(()) => (), + Err(error) => { + log::warn!( + "Stream could not run until completion: {error}" + ); + } + }); + + self.executor.spawn(future); + } + /// Tracks a [`Subscription`] in the [`Runtime`]. /// /// It will spawn new streams or close old ones as necessary! See diff --git a/runtime/src/command.rs b/runtime/src/command.rs index b74097bd..b942f3ce 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -4,8 +4,10 @@ mod action; pub use action::Action; use crate::core::widget; +use crate::futures::futures; use crate::futures::MaybeSend; +use futures::Stream; use std::fmt; use std::future::Future; @@ -43,11 +45,21 @@ impl<T> Command<T> { future: impl Future<Output = A> + 'static + MaybeSend, f: impl FnOnce(A) -> T + 'static + MaybeSend, ) -> Command<T> { - use iced_futures::futures::FutureExt; + use futures::FutureExt; Command::single(Action::Future(Box::pin(future.map(f)))) } + /// Creates a [`Command`] that runs the given stream to completion. + pub fn run<A>( + stream: impl Stream<Item = A> + 'static + MaybeSend, + f: impl Fn(A) -> T + 'static + MaybeSend, + ) -> Command<T> { + use futures::StreamExt; + + Command::single(Action::Stream(Box::pin(stream.map(f)))) + } + /// Creates a [`Command`] that performs the actions of all the given /// commands. /// diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs index 6c74f0ef..6551e233 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -18,6 +18,11 @@ pub enum Action<T> { /// [`Future`]: iced_futures::BoxFuture Future(iced_futures::BoxFuture<T>), + /// Run a [`Stream`] to completion. + /// + /// [`Stream`]: iced_futures::BoxStream + Stream(iced_futures::BoxStream<T>), + /// Run a clipboard action. Clipboard(clipboard::Action<T>), @@ -52,10 +57,11 @@ impl<T> Action<T> { A: 'static, T: 'static, { - use iced_futures::futures::FutureExt; + use iced_futures::futures::{FutureExt, StreamExt}; match self { Self::Future(future) => Action::Future(Box::pin(future.map(f))), + Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))), Self::Clipboard(action) => Action::Clipboard(action.map(f)), Self::Window(window) => Action::Window(window.map(f)), Self::System(system) => Action::System(system.map(f)), @@ -74,6 +80,7 @@ impl<T> fmt::Debug for Action<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Future(_) => write!(f, "Action::Future"), + Self::Stream(_) => write!(f, "Action::Stream"), Self::Clipboard(action) => { write!(f, "Action::Clipboard({action:?})") } diff --git a/winit/src/application.rs b/winit/src/application.rs index 315e34d9..2c5c864a 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -736,6 +736,9 @@ pub fn run_command<A, C, E>( command::Action::Future(future) => { runtime.spawn(future); } + command::Action::Stream(stream) => { + runtime.run(stream); + } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { let message = tag(clipboard.read()); -- cgit From a761448858521d11dc646e2ef5217e9e06628932 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 00:14:27 +0100 Subject: Implement `command::channel` helper It is analogous to `subscription::channel`. --- runtime/src/command.rs | 21 +++++++++++++++++++++ src/lib.rs | 7 ++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/runtime/src/command.rs b/runtime/src/command.rs index b942f3ce..f70da915 100644 --- a/runtime/src/command.rs +++ b/runtime/src/command.rs @@ -7,6 +7,7 @@ use crate::core::widget; use crate::futures::futures; use crate::futures::MaybeSend; +use futures::channel::mpsc; use futures::Stream; use std::fmt; use std::future::Future; @@ -118,3 +119,23 @@ impl<T> fmt::Debug for Command<T> { command.fmt(f) } } + +/// Creates a [`Command`] that produces the `Message`s published from a [`Future`] +/// to an [`mpsc::Sender`] with the given bounds. +pub fn channel<Fut, Message>( + size: usize, + f: impl FnOnce(mpsc::Sender<Message>) -> Fut + MaybeSend + 'static, +) -> Command<Message> +where + Fut: Future<Output = ()> + MaybeSend + 'static, + Message: 'static + MaybeSend, +{ + use futures::future; + use futures::stream::{self, StreamExt}; + + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).filter_map(|_| future::ready(None)); + + Command::single(Action::Stream(Box::pin(stream::select(receiver, runner)))) +} diff --git a/src/lib.rs b/src/lib.rs index f9f3952c..47766e6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,7 +190,6 @@ pub use crate::core::{ color, Alignment, Background, BorderRadius, Color, ContentFit, Degrees, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, Size, Vector, }; -pub use crate::runtime::Command; pub mod clipboard { //! Access the clipboard. @@ -239,6 +238,11 @@ pub mod mouse { }; } +pub mod command { + //! Run asynchronous actions. + pub use crate::runtime::command::{channel, Command}; +} + pub mod subscription { //! Listen to external events in your application. pub use iced_futures::subscription::{ @@ -287,6 +291,7 @@ pub mod widget { } pub use application::Application; +pub use command::Command; pub use error::Error; pub use event::Event; pub use executor::Executor; -- cgit From 7e7d65a23d864e4662c43028a41244ca30cac540 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 01:06:58 +0100 Subject: Fix Discourse badge in `README` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 825219aa..eb2befbc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![License](https://img.shields.io/crates/l/iced.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) [![Downloads](https://img.shields.io/crates/d/iced.svg)](https://crates.io/crates/iced) [![Test Status](https://img.shields.io/github/actions/workflow/status/iced-rs/iced/test.yml?branch=master&event=push&label=test)](https://github.com/iced-rs/iced/actions) -[![Discourse](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.iced.rs&color=5e7ce2)](https://discourse.iced.rs/) +[![Discourse](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscourse.iced.rs%2Fsite%2Fstatistics.json&query=%24.users_count&suffix=%20users&label=discourse&color=5e7ce2)](https://discourse.iced.rs/) [![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) A cross-platform GUI library for Rust focused on simplicity and type-safety. -- cgit From 6dca076c8b18c3cdb702fa55045866cbd413cc55 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:32:41 +0100 Subject: Use `workspace` dependency for `raw-window-handle` --- core/Cargo.toml | 4 ++-- winit/Cargo.toml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 7db4fa53..4672c754 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,8 +23,8 @@ palette.optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant.workspace = true -[target.'cfg(windows)'.dependencies.raw-window-handle] -version = "0.5.2" +[target.'cfg(windows)'.dependencies] +raw-window-handle.workspace = true [dev-dependencies] approx = "0.5" diff --git a/winit/Cargo.toml b/winit/Cargo.toml index bab05b91..87e600ae 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -27,7 +27,6 @@ iced_runtime.workspace = true iced_style.workspace = true log.workspace = true -raw-window-handle.workspace = true thiserror.workspace = true tracing.workspace = true window_clipboard.workspace = true -- cgit From abe13b530514c8ce63a0ef90172082258e35a43d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:33:20 +0100 Subject: Move `multi-window` feature before the `advanced` one --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aba66f39..0afbcd51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,10 +49,10 @@ web-colors = ["iced_renderer/web-colors"] webgl = ["iced_renderer/webgl"] # Enables the syntax `highlighter` module highlighter = ["iced_highlighter"] -# Enables the advanced module -advanced = [] # Enables experimental multi-window support. multi-window = ["iced_winit/multi-window"] +# Enables the advanced module +advanced = [] [dependencies] iced_core.workspace = true -- cgit From 9b34b2ac19a8fdd424581d160bc702e096a2b46a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:33:41 +0100 Subject: Run `cargo fmt` --- winit/src/multi_window/windows.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/winit/src/multi_window/windows.rs b/winit/src/multi_window/windows.rs index 1f606b31..6846abb3 100644 --- a/winit/src/multi_window/windows.rs +++ b/winit/src/multi_window/windows.rs @@ -176,11 +176,12 @@ where /// Returns the windows that need to be requested to closed, and also the windows that can be /// closed immediately. - pub fn partition_close_requests(&self) -> (Vec<window::Id>, Vec<window::Id>) { + pub fn partition_close_requests( + &self, + ) -> (Vec<window::Id>, Vec<window::Id>) { self.exit_on_close_requested.iter().enumerate().fold( (vec![], vec![]), - |(mut close_immediately, mut needs_request_closed), - (i, close)| { + |(mut close_immediately, mut needs_request_closed), (i, close)| { let id = self.ids[i]; if *close { -- cgit From 7def3ee38a3f0f24a331d722b09f325fc9584625 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:37:54 +0100 Subject: Fix `clippy` lints --- runtime/src/multi_window/state.rs | 2 +- winit/src/application.rs | 7 ++++--- winit/src/multi_window.rs | 24 ++++++++++++++---------- winit/src/multi_window/windows.rs | 14 +++++++++----- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 78c35e6c..05036a07 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -228,7 +228,7 @@ where match operation.finish() { operation::Outcome::None => {} operation::Outcome::Some(message) => { - self.queued_messages.push(message) + self.queued_messages.push(message); } operation::Outcome::Chain(next) => { current_operation = Some(next); diff --git a/winit/src/application.rs b/winit/src/application.rs index 8457fd92..b197c4ed 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -720,9 +720,10 @@ pub fn run_command<A, C, E>( let _res = window.drag_window(); } window::Action::Spawn { .. } => { - log::info!( - "Spawning a window is only available with multi-window applications." - ) + log::warn!( + "Spawning a window is only available with \ + multi-window applications." + ); } window::Action::Resize(size) => { window.set_inner_size(winit::dpi::LogicalSize { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index f2452eb3..b233564a 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -912,7 +912,7 @@ pub fn run_command<A, C, E>( size.width, size.height, )))) - .expect("Send message to event loop") + .expect("Send message to event loop"); } window::Action::Maximize(maximized) => { windows.with_raw(id).set_maximized(maximized); @@ -934,7 +934,9 @@ pub fn run_command<A, C, E>( )); } window::Action::ChangeIcon(icon) => { - windows.with_raw(id).set_window_icon(conversion::icon(icon)) + windows + .with_raw(id) + .set_window_icon(conversion::icon(icon)); } window::Action::FetchMode(tag) => { let window = windows.with_raw(id); @@ -969,12 +971,14 @@ pub fn run_command<A, C, E>( .with_raw(id) .set_window_level(conversion::window_level(level)); } - window::Action::FetchId(tag) => proxy - .send_event(Event::Application(tag(windows - .with_raw(id) - .id() - .into()))) - .expect("Event loop doesn't exist."), + window::Action::FetchId(tag) => { + proxy + .send_event(Event::Application(tag(windows + .with_raw(id) + .id() + .into()))) + .expect("Event loop doesn't exist."); + } window::Action::Screenshot(tag) => { let i = windows.index_from_id(id); let state = &windows.states[i]; @@ -996,7 +1000,7 @@ pub fn run_command<A, C, E>( state.physical_size(), ), ))) - .expect("Event loop doesn't exist.") + .expect("Event loop doesn't exist."); } }, command::Action::System(action) => match action { @@ -1014,7 +1018,7 @@ pub fn run_command<A, C, E>( proxy .send_event(Event::Application(message)) - .expect("Event loop doesn't exist.") + .expect("Event loop doesn't exist."); }); } } diff --git a/winit/src/multi_window/windows.rs b/winit/src/multi_window/windows.rs index 6846abb3..a4841a45 100644 --- a/winit/src/multi_window/windows.rs +++ b/winit/src/multi_window/windows.rs @@ -1,10 +1,12 @@ use crate::core::{window, Size}; +use crate::graphics::Compositor; use crate::multi_window::{Application, State}; -use iced_graphics::Compositor; -use iced_style::application::StyleSheet; -use std::fmt::{Debug, Formatter}; +use crate::style::application::StyleSheet; + use winit::monitor::MonitorHandle; +use std::fmt::{Debug, Formatter}; + pub struct Windows<A: Application, C: Compositor> where <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, @@ -33,7 +35,7 @@ where &self .raw .iter() - .map(|raw| raw.id()) + .map(winit::window::Window::id) .collect::<Vec<winit::window::WindowId>>(), ) .field("states", &self.states) @@ -131,7 +133,9 @@ where } pub fn last_monitor(&self) -> Option<MonitorHandle> { - self.raw.last().and_then(|w| w.current_monitor()) + self.raw + .last() + .and_then(winit::window::Window::current_monitor) } pub fn last(&self) -> usize { -- cgit From 6740c2c5d6b24399dab1343abdfec5daf4b03c98 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:46:47 +0100 Subject: Fix broken intra-doc links --- graphics/src/compositor.rs | 2 +- runtime/src/multi_window/state.rs | 2 +- runtime/src/window/action.rs | 2 +- src/multi_window/application.rs | 2 ++ winit/src/conversion.rs | 2 +- winit/src/multi_window.rs | 23 ++++------------------- winit/src/multi_window/state.rs | 4 +--- winit/src/settings.rs | 2 +- 8 files changed, 12 insertions(+), 27 deletions(-) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index e0b1e20f..78731a98 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -24,7 +24,7 @@ pub trait Compositor: Sized { compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error>; - /// Creates a [`Renderer`] for the [`Compositor`]. + /// Creates a [`Self::Renderer`] for the [`Compositor`]. fn renderer(&self) -> Self::Renderer; /// Crates a new [`Surface`] for the given window. diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs index 05036a07..49f72c39 100644 --- a/runtime/src/multi_window/state.rs +++ b/runtime/src/multi_window/state.rs @@ -201,7 +201,7 @@ where (uncaptured_events, commands) } - /// Applies [`widget::Operation`]s to the [`State`] + /// Applies widget [`Operation`]s to the [`State`]. pub fn operate( &mut self, renderer: &mut P::Renderer, diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index d631cee1..2a31bbd6 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -17,7 +17,7 @@ pub enum Action<T> { Drag, /// Spawns a new window. Spawn { - /// The settings of the [`Window`]. + /// The settings of the window. settings: Settings, }, /// Resize the window. diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index 0486159e..b6f15149 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -62,6 +62,8 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// } /// } /// ``` +/// +/// [`Sandbox`]: crate::Sandbox pub trait Application: Sized { /// The [`Executor`] that will run commands and subscriptions. /// diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 22e6b9be..68c2b905 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -272,7 +272,7 @@ 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 pub fn position( diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index b233564a..0e08a081 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -23,34 +23,19 @@ use std::mem::ManuallyDrop; use std::time::Instant; use winit::monitor::MonitorHandle; -/// This is a wrapper around the `Application::Message` associate type -/// to allows the `shell` to create internal messages, while still having -/// the current user-specified custom messages. #[derive(Debug)] -pub enum Event<Message> { - /// An internal event which contains an [`Application`] generated message. +enum Event<Message> { Application(Message), - /// An internal event which spawns a new window. NewWindow { - /// The [window::Id] of the newly spawned [`Window`]. id: window::Id, - /// The [settings::Window] of the newly spawned [`Window`]. settings: window::Settings, - /// The title of the newly spawned [`Window`]. title: String, - /// The monitor on which to spawn the window. If `None`, will use monitor of the last window - /// spawned. monitor: Option<MonitorHandle>, }, - /// An internal event for closing a window. CloseWindow(window::Id), - /// An internal event for when the window has finished being created. WindowCreated { - /// The internal ID of the window. id: window::Id, - /// The raw window. window: winit::window::Window, - /// Whether or not the window should close when a user requests it does. exit_on_close_request: bool, }, } @@ -771,7 +756,7 @@ async fn run_instance<A, E, C>( } /// Builds a window's [`UserInterface`] for the [`Application`]. -pub fn build_user_interface<'a, A: Application>( +fn build_user_interface<'a, A: Application>( application: &'a A, cache: user_interface::Cache, renderer: &mut A::Renderer, @@ -795,7 +780,7 @@ where /// Updates a multi-window [`Application`] by feeding it messages, spawning any /// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update<A: Application, C, E: Executor>( +fn update<A: Application, C, E: Executor>( application: &mut A, compositor: &mut C, runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, @@ -834,7 +819,7 @@ pub fn update<A: Application, C, E: Executor>( } /// Runs the actions of a [`Command`]. -pub fn run_command<A, C, E>( +fn run_command<A, C, E>( application: &A, compositor: &mut C, command: Command<A::Message>, diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index f2741c3c..e9a9f91a 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -200,9 +200,7 @@ where /// window. /// /// Normally, an [`Application`] should be synchronized with its [`State`] - /// and window after calling [`Application::update`]. - /// - /// [`Application::update`]: crate::Program::update + /// and window after calling [`State::update`]. pub fn synchronize( &mut self, application: &A, diff --git a/winit/src/settings.rs b/winit/src/settings.rs index dc0f65a5..2e541128 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -12,7 +12,7 @@ pub struct Settings<Flags> { /// communicate with it through the windowing system. pub id: Option<String>, - /// The [`Window`] settings. + /// The [`window::Settings`]. pub window: window::Settings, /// The data needed to initialize an [`Application`]. -- cgit From 8c4e7d80a1ba128864ee82770a4670b8dbba619a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:47:46 +0100 Subject: Fix `renderer` method in `iced_renderer::Compositor` --- renderer/src/compositor.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 5fc5a459..5bec1639 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -55,10 +55,6 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { Compositor::Wgpu(compositor) => { Renderer::Wgpu(compositor.renderer()) } - #[cfg(not(feature = "wgpu"))] - Self::Wgpu => { - panic!("`wgpu` feature was not enabled in `iced_renderer`") - } } } -- cgit From ac12d2d099d9ae996d0ccfdc7e5b82d9cef990ee Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:50:35 +0100 Subject: Remove unnecessary unsafe `Send` marker in `iced_winit` --- winit/src/multi_window.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 0e08a081..ef142c77 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -40,9 +40,6 @@ enum Event<Message> { }, } -#[allow(unsafe_code)] -unsafe impl<Message> std::marker::Send for Event<Message> {} - /// An interactive, native, cross-platform, multi-windowed application. /// /// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run -- cgit From 3b39ba7029832cab5235fb5538b46148d599daa7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 22:52:46 +0100 Subject: Fix unused import in `multi_window::application` --- src/multi_window/application.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs index b6f15149..4a91bdf4 100644 --- a/src/multi_window/application.rs +++ b/src/multi_window/application.rs @@ -1,8 +1,7 @@ +use crate::style::application::StyleSheet; use crate::window; use crate::{Command, Element, Executor, Settings, Subscription}; -pub use crate::style::application::{Appearance, StyleSheet}; - /// An interactive cross-platform multi-window application. /// /// This trait is the main entrypoint of Iced. Once implemented, you can run -- cgit From d34bc4e4a251bb28854770575d379d4a53f2db12 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 29 Nov 2023 23:55:17 +0100 Subject: Refactor event loop <-> instance communication in `multi_window` --- winit/src/multi_window.rs | 871 ++++++++++++++++++++++++---------------------- 1 file changed, 452 insertions(+), 419 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index ef142c77..f4ebbe09 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -8,7 +8,7 @@ use crate::conversion; use crate::core::widget::operation; use crate::core::{self, mouse, renderer, window, Size}; use crate::futures::futures::channel::mpsc; -use crate::futures::futures::{task, Future, FutureExt, StreamExt}; +use crate::futures::futures::{task, Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; use crate::graphics::{compositor, Compositor}; use crate::multi_window::windows::Windows; @@ -21,23 +21,24 @@ use crate::{Clipboard, Error, Proxy, Settings}; use std::mem::ManuallyDrop; use std::time::Instant; -use winit::monitor::MonitorHandle; -#[derive(Debug)] -enum Event<Message> { - Application(Message), - NewWindow { - id: window::Id, - settings: window::Settings, - title: String, - monitor: Option<MonitorHandle>, - }, - CloseWindow(window::Id), +enum Event<Message: 'static> { WindowCreated { id: window::Id, window: winit::window::Window, exit_on_close_request: bool, }, + EventLoopAwakened(winit::event::Event<'static, Message>), +} + +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option<winit::monitor::MonitorHandle>, + }, } /// An interactive, native, cross-platform, multi-windowed application. @@ -243,44 +244,57 @@ where event: winit::event::WindowEvent::Resized(*new_inner_size), window_id, }), - winit::event::Event::UserEvent(Event::NewWindow { - id, - settings, - title, - monitor, - }) => { - let exit_on_close_request = settings.exit_on_close_request; - - let window = conversion::window_settings( - settings, &title, monitor, None, - ) - .build(window_target) - .expect("Failed to build window"); - - Some(winit::event::Event::UserEvent(Event::WindowCreated { - id, - window, - exit_on_close_request, - })) - } _ => event.to_static(), }; if let Some(event) = event { - event_sender.start_send(event).expect("Send event"); - - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = control_receiver.try_next() { - *control_flow = flow; + event_sender + .start_send(Event::EventLoopAwakened(event)) + .expect("Send event"); + + loop { + let poll = instance.as_mut().poll(&mut context); + + match poll { + task::Poll::Pending => match control_receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + *control_flow = flow; + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = conversion::window_settings( + settings, &title, monitor, None, + ) + .build(window_target) + .expect("Failed to build window"); + + event_sender + .start_send(Event::WindowCreated { + id, + window, + exit_on_close_request, + }) + .expect("Send event"); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + *control_flow = ControlFlow::Exit; + break; } - } - task::Poll::Ready(_) => { - *control_flow = ControlFlow::Exit; - } - }; + }; + } } }) } @@ -288,13 +302,11 @@ where async fn run_instance<A, E, C>( mut application: A, mut compositor: C, - mut runtime: Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, - mut proxy: winit::event_loop::EventLoopProxy<Event<A::Message>>, + mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, + mut proxy: winit::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, - mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<'_, Event<A::Message>>, - >, - mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, + mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, + mut control_sender: mpsc::UnboundedSender<Control>, init_command: Command<A::Message>, mut windows: Windows<A, C>, should_main_window_be_visible: bool, @@ -327,18 +339,14 @@ async fn run_instance<A, E, C>( init_command, &mut runtime, &mut clipboard, + &mut control_sender, &mut proxy, &mut debug, &mut windows, &mut ui_caches, ); - runtime.track( - application - .subscription() - .map(Event::Application) - .into_recipes(), - ); + runtime.track(application.subscription().into_recipes()); let mut mouse_interaction = mouse::Interaction::default(); @@ -361,391 +369,409 @@ async fn run_instance<A, E, C>( 'main: while let Some(event) = event_receiver.next().await { match event { - event::Event::NewEvents(start_cause) => { - redraw_pending = matches!( - start_cause, - event::StartCause::Init - | event::StartCause::Poll - | event::StartCause::ResumeTimeReached { .. } - ); - } - event::Event::MainEventsCleared => { - debug.event_processing_started(); - let mut uis_stale = false; - - for (i, id) in windows.ids.iter().enumerate() { - let mut window_events = vec![]; - - events.retain(|(window_id, event)| { - if *window_id == Some(*id) || window_id.is_none() { - window_events.push(event.clone()); - false - } else { - true - } - }); - - if !redraw_pending - && window_events.is_empty() - && messages.is_empty() - { - continue; - } + Event::WindowCreated { + id, + window, + exit_on_close_request, + } => { + let bounds = logical_bounds_of(&window); - let (ui_state, statuses) = user_interfaces[i].update( - &window_events, - windows.states[i].cursor(), - &mut windows.renderers[i], - &mut clipboard, - &mut messages, - ); + let (inner_size, i) = windows.add( + &application, + &mut compositor, + id, + window, + exit_on_close_request, + ); - if !uis_stale { - uis_stale = - matches!(ui_state, user_interface::State::Outdated); - } + user_interfaces.push(build_user_interface( + &application, + user_interface::Cache::default(), + &mut windows.renderers[i], + inner_size, + &mut debug, + id, + )); + ui_caches.push(user_interface::Cache::default()); - for (event, status) in - window_events.into_iter().zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } + if let Some(bounds) = bounds { + events.push(( + Some(id), + core::Event::Window( + id, + window::Event::Created { + position: bounds.0, + size: bounds.1, + }, + ), + )); } - - debug.event_processing_finished(); - - // TODO mw application update returns which window IDs to update - if !messages.is_empty() || uis_stale { - let mut cached_interfaces: Vec<user_interface::Cache> = - ManuallyDrop::into_inner(user_interfaces) - .drain(..) - .map(UserInterface::into_cache) - .collect(); - - // Update application - update( - &mut application, - &mut compositor, - &mut runtime, - &mut clipboard, - &mut proxy, - &mut debug, - &mut messages, - &mut windows, - &mut cached_interfaces, - ); - - // we must synchronize all window states with application state after an - // application update since we don't know what changed - for (state, (id, window)) in windows - .states - .iter_mut() - .zip(windows.ids.iter().zip(windows.raw.iter())) - { - state.synchronize(&application, *id, window); + } + Event::EventLoopAwakened(event) => { + match event { + event::Event::NewEvents(start_cause) => { + redraw_pending = matches!( + start_cause, + event::StartCause::Init + | event::StartCause::Poll + | event::StartCause::ResumeTimeReached { .. } + ); } + event::Event::MainEventsCleared => { + debug.event_processing_started(); + let mut uis_stale = false; + + for (i, id) in windows.ids.iter().enumerate() { + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == Some(*id) + || window_id.is_none() + { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if !redraw_pending + && window_events.is_empty() + && messages.is_empty() + { + continue; + } - // rebuild UIs with the synchronized states - user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut debug, - &mut windows, - cached_interfaces, - )); - } + let (ui_state, statuses) = user_interfaces[i] + .update( + &window_events, + windows.states[i].cursor(), + &mut windows.renderers[i], + &mut clipboard, + &mut messages, + ); + + if !uis_stale { + uis_stale = matches!( + ui_state, + user_interface::State::Outdated + ); + } - debug.draw_started(); - - for (i, id) in windows.ids.iter().enumerate() { - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = core::Event::Window( - *id, - window::Event::RedrawRequested(Instant::now()), - ); + for (event, status) in window_events + .into_iter() + .zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + } - let cursor = windows.states[i].cursor(); + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let mut cached_interfaces: Vec< + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain(..) + .map(UserInterface::into_cache) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut messages, + &mut windows, + &mut cached_interfaces, + ); - let (ui_state, _) = user_interfaces[i].update( - &[redraw_event.clone()], - cursor, - &mut windows.renderers[i], - &mut clipboard, - &mut messages, - ); + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (state, (id, window)) in windows + .states + .iter_mut() + .zip(windows.ids.iter().zip(windows.raw.iter())) + { + state.synchronize(&application, *id, window); + } - let new_mouse_interaction = { - let state = &windows.states[i]; + // rebuild UIs with the synchronized states + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut windows, + cached_interfaces, + )); + } - user_interfaces[i].draw( - &mut windows.renderers[i], - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - cursor, - ) - }; + debug.draw_started(); + + for (i, id) in windows.ids.iter().enumerate() { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + *id, + window::Event::RedrawRequested(Instant::now()), + ); - if new_mouse_interaction != mouse_interaction { - windows.raw[i].set_cursor_icon( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); + let cursor = windows.states[i].cursor(); - mouse_interaction = new_mouse_interaction; - } + let (ui_state, _) = user_interfaces[i].update( + &[redraw_event.clone()], + cursor, + &mut windows.renderers[i], + &mut clipboard, + &mut messages, + ); - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - windows.raw[i].request_redraw(); + let new_mouse_interaction = { + let state = &windows.states[i]; + + user_interfaces[i].draw( + &mut windows.renderers[i], + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + cursor, + ) + }; + + if new_mouse_interaction != mouse_interaction { + windows.raw[i].set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + mouse_interaction = new_mouse_interaction; + } - runtime.broadcast( - redraw_event.clone(), - core::event::Status::Ignored, - ); + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + windows.raw[i].request_redraw(); - let _ = control_sender.start_send(match ui_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - ControlFlow::Poll - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - } + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); - redraw_pending = false; + let _ = control_sender.start_send( + Control::ChangeFlow(match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }), + ); + } - debug.draw_finished(); - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use crate::core::event; + redraw_pending = false; - events.push(( - None, + debug.draw_finished(); + } event::Event::PlatformSpecific( event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), ), - ), - )); - } - event::Event::UserEvent(event) => match event { - Event::Application(message) => { - messages.push(message); - } - Event::WindowCreated { - id, - window, - exit_on_close_request, - } => { - let bounds = logical_bounds_of(&window); - - let (inner_size, i) = windows.add( - &application, - &mut compositor, - id, - window, - exit_on_close_request, - ); + ) => { + use crate::core::event; - user_interfaces.push(build_user_interface( - &application, - user_interface::Cache::default(), - &mut windows.renderers[i], - inner_size, - &mut debug, - id, - )); - ui_caches.push(user_interface::Cache::default()); - - if let Some(bounds) = bounds { events.push(( - Some(id), - core::Event::Window( - id, - window::Event::Created { - position: bounds.0, - size: bounds.1, - }, + None, + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), ), )); } - } - Event::CloseWindow(id) => { - let i = windows.delete(id); - let _ = user_interfaces.remove(i); - let _ = ui_caches.remove(i); - - if windows.is_empty() { - break 'main; + event::Event::UserEvent(message) => { + messages.push(message); } - } - Event::NewWindow { .. } => unreachable!(), - }, - event::Event::RedrawRequested(id) => { - let i = windows.index_from_raw(id); - let state = &windows.states[i]; - let physical_size = state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 { - continue; - } - - debug.render_started(); - let current_viewport_version = state.viewport_version(); - let window_viewport_version = windows.viewport_versions[i]; + event::Event::RedrawRequested(id) => { + let i = windows.index_from_raw(id); + let state = &windows.states[i]; + let physical_size = state.physical_size(); - if window_viewport_version != current_viewport_version { - let logical_size = state.logical_size(); + if physical_size.width == 0 || physical_size.height == 0 + { + continue; + } - debug.layout_started(); + debug.render_started(); + let current_viewport_version = state.viewport_version(); + let window_viewport_version = + windows.viewport_versions[i]; - let renderer = &mut windows.renderers[i]; - let ui = user_interfaces.remove(i); + if window_viewport_version != current_viewport_version { + let logical_size = state.logical_size(); - user_interfaces - .insert(i, ui.relayout(logical_size, renderer)); + debug.layout_started(); - debug.layout_finished(); + let renderer = &mut windows.renderers[i]; + let ui = user_interfaces.remove(i); - debug.draw_started(); - let new_mouse_interaction = user_interfaces[i].draw( - renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor(), - ); + user_interfaces + .insert(i, ui.relayout(logical_size, renderer)); - if new_mouse_interaction != mouse_interaction { - windows.raw[i].set_cursor_icon( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); + debug.layout_finished(); - mouse_interaction = new_mouse_interaction; - } - debug.draw_finished(); + debug.draw_started(); + let new_mouse_interaction = user_interfaces[i] + .draw( + renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + state.cursor(), + ); - compositor.configure_surface( - &mut windows.surfaces[i], - physical_size.width, - physical_size.height, - ); + if new_mouse_interaction != mouse_interaction { + windows.raw[i].set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); - windows.viewport_versions[i] = current_viewport_version; - } + mouse_interaction = new_mouse_interaction; + } + debug.draw_finished(); - match compositor.present( - &mut windows.renderers[i], - &mut windows.surfaces[i], - state.viewport(), - state.background_color(), - &debug.overlay(), - ) { - Ok(()) => { - debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{:?}", error); - } - _ => { - debug.render_finished(); - log::error!( - "Error {error:?} when presenting surface." + compositor.configure_surface( + &mut windows.surfaces[i], + physical_size.width, + physical_size.height, ); - // Try rendering all windows again next frame. - for window in &windows.raw { - window.request_redraw(); - } + windows.viewport_versions[i] = + current_viewport_version; } - }, - } - } - event::Event::WindowEvent { - event: window_event, - window_id, - } => { - let window_index = - windows.raw.iter().position(|w| w.id() == window_id); - - match window_index { - Some(i) => { - let id = windows.ids[i]; - let raw = &windows.raw[i]; - let exit_on_close_request = - windows.exit_on_close_requested[i]; - - if matches!( - window_event, - winit::event::WindowEvent::CloseRequested - ) && exit_on_close_request - { - let i = windows.delete(id); - let _ = user_interfaces.remove(i); - let _ = ui_caches.remove(i); - if windows.is_empty() { - break 'main; - } - } else { - let state = &mut windows.states[i]; - state.update(raw, &window_event, &mut debug); + match compositor.present( + &mut windows.renderers[i], + &mut windows.surfaces[i], + state.viewport(), + state.background_color(), + &debug.overlay(), + ) { + Ok(()) => { + debug.render_finished(); - if let Some(event) = conversion::window_event( - id, - &window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push((Some(id), event)); + // TODO: Handle animations! + // Maybe we can use `ControlFlow::WaitUntil` for this. } + Err(error) => match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + _ => { + debug.render_finished(); + log::error!( + "Error {error:?} when presenting surface." + ); + + // Try rendering all windows again next frame. + for window in &windows.raw { + window.request_redraw(); + } + } + }, } } - None => { - // This is the only special case, since in order to trigger the Destroyed event the - // window reference from winit must be dropped, but we still want to inform the - // user that the window was destroyed so they can clean up any specific window - // code for this window - if matches!( - window_event, - winit::event::WindowEvent::Destroyed - ) { - let id = windows.get_pending_destroy(window_id); - - events.push(( - None, - core::Event::Window( - id, - window::Event::Destroyed, - ), - )); + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + let window_index = windows + .raw + .iter() + .position(|w| w.id() == window_id); + + match window_index { + Some(i) => { + let id = windows.ids[i]; + let raw = &windows.raw[i]; + let exit_on_close_request = + windows.exit_on_close_requested[i]; + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && exit_on_close_request + { + let i = windows.delete(id); + let _ = user_interfaces.remove(i); + let _ = ui_caches.remove(i); + + if windows.is_empty() { + break 'main; + } + } else { + let state = &mut windows.states[i]; + state.update( + raw, + &window_event, + &mut debug, + ); + + if let Some(event) = + conversion::window_event( + id, + &window_event, + state.scale_factor(), + state.modifiers(), + ) + { + events.push((Some(id), event)); + } + } + } + None => { + // This is the only special case, since in order to trigger the Destroyed event the + // window reference from winit must be dropped, but we still want to inform the + // user that the window was destroyed so they can clean up any specific window + // code for this window + if matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) { + let id = + windows.get_pending_destroy(window_id); + + events.push(( + None, + core::Event::Window( + id, + window::Event::Destroyed, + ), + )); + } + } } } + _ => {} } } - _ => {} } } @@ -780,9 +806,10 @@ where fn update<A: Application, C, E: Executor>( application: &mut A, compositor: &mut C, - runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy<Event<A::Message>>, + control_sender: &mut mpsc::UnboundedSender<Control>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, messages: &mut Vec<A::Message>, windows: &mut Windows<A, C>, @@ -804,6 +831,7 @@ fn update<A: Application, C, E: Executor>( command, runtime, clipboard, + control_sender, proxy, debug, windows, @@ -811,7 +839,7 @@ fn update<A: Application, C, E: Executor>( ); } - let subscription = application.subscription().map(Event::Application); + let subscription = application.subscription(); runtime.track(subscription.into_recipes()); } @@ -820,9 +848,10 @@ fn run_command<A, C, E>( application: &A, compositor: &mut C, command: Command<A::Message>, - runtime: &mut Runtime<E, Proxy<Event<A::Message>>, Event<A::Message>>, + runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, - proxy: &mut winit::event_loop::EventLoopProxy<Event<A::Message>>, + control_sender: &mut mpsc::UnboundedSender<Control>, + proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, windows: &mut Windows<A, C>, ui_caches: &mut Vec<user_interface::Cache>, @@ -839,17 +868,17 @@ fn run_command<A, C, E>( for action in command.actions() { match action { command::Action::Future(future) => { - runtime.spawn(Box::pin(future.map(Event::Application))); + runtime.spawn(Box::pin(future)); } command::Action::Stream(stream) => { - runtime.run(Box::pin(stream.map(Event::Application))); + runtime.run(Box::pin(stream)); } command::Action::Clipboard(action) => match action { clipboard::Action::Read(tag) => { let message = tag(clipboard.read()); proxy - .send_event(Event::Application(message)) + .send_event(message) .expect("Send message to event loop"); } clipboard::Action::Write(contents) => { @@ -860,19 +889,28 @@ fn run_command<A, C, E>( window::Action::Spawn { settings } => { let monitor = windows.last_monitor(); - proxy - .send_event(Event::NewWindow { + control_sender + .start_send(Control::CreateWindow { id, settings, title: application.title(id), monitor, }) - .expect("Send message to event loop"); + .expect("Send control action"); } window::Action::Close => { - proxy - .send_event(Event::CloseWindow(id)) - .expect("Send message to event loop"); + use winit::event_loop::ControlFlow; + + let i = windows.delete(id); + let _ = ui_caches.remove(i); + + if windows.is_empty() { + control_sender + .start_send(Control::ChangeFlow( + ControlFlow::ExitWithCode(0), + )) + .expect("Send control action"); + } } window::Action::Drag => { let _ = windows.with_raw(id).drag_window(); @@ -890,10 +928,10 @@ fn run_command<A, C, E>( let size = window.inner_size(); proxy - .send_event(Event::Application(callback(Size::new( + .send_event(callback(Size::new( size.width, size.height, - )))) + ))) .expect("Send message to event loop"); } window::Action::Maximize(maximized) => { @@ -929,7 +967,7 @@ fn run_command<A, C, E>( }; proxy - .send_event(Event::Application(tag(mode))) + .send_event(tag(mode)) .expect("Event loop doesn't exist."); } window::Action::ToggleMaximize => { @@ -955,10 +993,7 @@ fn run_command<A, C, E>( } window::Action::FetchId(tag) => { proxy - .send_event(Event::Application(tag(windows - .with_raw(id) - .id() - .into()))) + .send_event(tag(windows.with_raw(id).id().into())) .expect("Event loop doesn't exist."); } window::Action::Screenshot(tag) => { @@ -976,11 +1011,9 @@ fn run_command<A, C, E>( ); proxy - .send_event(Event::Application(tag( - window::Screenshot::new( - bytes, - state.physical_size(), - ), + .send_event(tag(window::Screenshot::new( + bytes, + state.physical_size(), ))) .expect("Event loop doesn't exist."); } @@ -999,7 +1032,7 @@ fn run_command<A, C, E>( let message = _tag(information); proxy - .send_event(Event::Application(message)) + .send_event(message) .expect("Event loop doesn't exist."); }); } @@ -1025,7 +1058,7 @@ fn run_command<A, C, E>( operation::Outcome::None => {} operation::Outcome::Some(message) => { proxy - .send_event(Event::Application(message)) + .send_event(message) .expect("Event loop doesn't exist."); // operation completed, don't need to try to operate on rest of UIs @@ -1051,7 +1084,7 @@ fn run_command<A, C, E>( } proxy - .send_event(Event::Application(tagger(Ok(())))) + .send_event(tagger(Ok(()))) .expect("Send message to event loop"); } } -- cgit From 9f29aec128ccf51c620a8b69a9fbd64186ab8c65 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 30 Nov 2023 00:01:32 +0100 Subject: Move `Event` and `Control` types after `multi_window::run` --- winit/src/multi_window.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index f4ebbe09..f8cedcb8 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -22,25 +22,6 @@ use crate::{Clipboard, Error, Proxy, Settings}; use std::mem::ManuallyDrop; use std::time::Instant; -enum Event<Message: 'static> { - WindowCreated { - id: window::Id, - window: winit::window::Window, - exit_on_close_request: bool, - }, - EventLoopAwakened(winit::event::Event<'static, Message>), -} - -enum Control { - ChangeFlow(winit::event_loop::ControlFlow), - CreateWindow { - id: window::Id, - settings: window::Settings, - title: String, - monitor: Option<winit::monitor::MonitorHandle>, - }, -} - /// An interactive, native, cross-platform, multi-windowed application. /// /// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run @@ -299,6 +280,25 @@ where }) } +enum Event<Message: 'static> { + WindowCreated { + id: window::Id, + window: winit::window::Window, + exit_on_close_request: bool, + }, + EventLoopAwakened(winit::event::Event<'static, Message>), +} + +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option<winit::monitor::MonitorHandle>, + }, +} + async fn run_instance<A, E, C>( mut application: A, mut compositor: C, -- cgit From 67408311f45d341509538f8cc185978da66b6ace Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 30 Nov 2023 23:40:33 +0100 Subject: Use actual floats for logical coordinates --- core/src/point.rs | 58 ++++++++++++++++++------- core/src/window/event.rs | 18 ++++---- core/src/window/position.rs | 6 ++- core/src/window/settings.rs | 11 ++--- examples/multi_window/src/main.rs | 15 ++++--- examples/solar_system/src/main.rs | 14 +++--- examples/todos/src/main.rs | 4 +- runtime/src/window.rs | 13 +++--- runtime/src/window/action.rs | 25 +++++------ winit/src/application.rs | 9 ++-- winit/src/conversion.rs | 39 ++++++++++------- winit/src/multi_window.rs | 89 ++++++++++++++++++++------------------- 12 files changed, 165 insertions(+), 136 deletions(-) diff --git a/core/src/point.rs b/core/src/point.rs index 9bf7726b..ef42852f 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -1,26 +1,34 @@ use crate::Vector; +use num_traits::{Float, Num}; +use std::fmt; + /// A 2D point. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Point { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Point<T = f32> { /// The X coordinate. - pub x: f32, + pub x: T, /// The Y coordinate. - pub y: f32, + pub y: T, } impl Point { /// 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. - pub const fn new(x: f32, y: f32) -> Self { + pub const fn new(x: T, y: T) -> Self { Self { x, y } } /// 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 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 { - 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; - fn add(self, vector: Vector) -> Self { + fn add(self, vector: Vector<T>) -> Self { Self { x: self.x + vector.x, 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; - fn sub(self, vector: Vector) -> Self { + fn sub(self, vector: Vector<T>) -> Self { Self { x: self.x - vector.x, y: self.y - vector.y, @@ -68,10 +82,22 @@ impl std::ops::Sub<Vector> for Point { } } -impl std::ops::Sub<Point> for Point { - type Output = Vector; +impl<T> std::ops::Sub<Point<T>> for Point<T> +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) } } + +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) + } +} diff --git a/core/src/window/event.rs b/core/src/window/event.rs index f7759435..3ab7cd81 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -1,10 +1,10 @@ use crate::time::Instant; -use crate::Size; +use crate::{Point, Size}; use std::path::PathBuf; /// A window-related event. -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug)] pub enum Event { /// A window was moved. Moved { @@ -30,22 +30,22 @@ pub enum Event { /// The user has requested for the window to close. CloseRequested, - /// A window was destroyed by the runtime. - Destroyed, - /// A window was created. - /// - /// **Note:** this event is not supported on Wayland. Created { /// The position of the created 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. - position: (i32, i32), + /// + /// **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<u32>, + size: Size, }, + /// A window was destroyed by the runtime. + Destroyed, + /// A window was focused. Focused, diff --git a/core/src/window/position.rs b/core/src/window/position.rs index c260c29e..73391e75 100644 --- a/core/src/window/position.rs +++ b/core/src/window/position.rs @@ -1,5 +1,7 @@ +use crate::Point; + /// The position of a window in a given screen. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Position { /// The platform-specific default position for a new window. Default, @@ -12,7 +14,7 @@ pub enum Position { /// 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), + Specific(Point), } impl Default for Position { diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index 25df8159..fbbf86ab 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -25,22 +25,23 @@ mod platform; mod platform; use crate::window::{Icon, Level, Position}; +use crate::Size; pub use platform::PlatformSpecific; /// The window settings of an application. #[derive(Debug, Clone)] pub struct Settings { - /// The initial size of the window. - pub size: (u32, u32), + /// The initial logical dimensions of the window. + pub size: Size, /// The initial position of the window. pub position: Position, /// The minimum size of the window. - pub min_size: Option<(u32, u32)>, + pub min_size: Option<Size>, /// 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. pub visible: bool, @@ -77,7 +78,7 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - size: (1024, 768), + size: Size::new(1024.0, 768.0), position: Position::default(), min_size: None, max_size: None, diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 7d1f1e91..16beb46e 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -4,8 +4,10 @@ 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, Settings, Subscription, Theme, + Alignment, Command, Element, Length, Point, Settings, Subscription, Theme, + Vector, }; + use std::collections::HashMap; fn main() -> iced::Result { @@ -33,8 +35,8 @@ enum Message { ScaleChanged(window::Id, String), TitleChanged(window::Id, String), CloseWindow(window::Id), + WindowCreated(window::Id, Option<Point>), WindowDestroyed(window::Id), - WindowCreated(window::Id, (i32, i32)), NewWindow, } @@ -90,10 +92,11 @@ impl multi_window::Application for Example { self.windows.remove(&id); } Message::WindowCreated(id, position) => { - self.next_window_pos = window::Position::Specific( - position.0 + 20, - position.1 + 20, - ); + 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) { return text_input::focus(window.input_id.clone()); diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 8295dded..82421a86 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -114,14 +114,14 @@ impl State { pub fn new() -> State { let now = Instant::now(); - let (width, height) = window::Settings::default().size; + let size = window::Settings::default().size; State { space_cache: canvas::Cache::default(), system_cache: canvas::Cache::default(), start: 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(); } - fn generate_stars(width: u32, height: u32) -> Vec<(Point, f32)> { + fn generate_stars(width: f32, height: f32) -> Vec<(Point, f32)> { use rand::Rng; let mut rng = rand::thread_rng(); @@ -139,12 +139,8 @@ impl State { .map(|_| { ( Point::new( - rng.gen_range( - (-(width as f32) / 2.0)..(width as f32 / 2.0), - ), - rng.gen_range( - (-(height as f32) / 2.0)..(height as f32 / 2.0), - ), + rng.gen_range((-width / 2.0)..(width / 2.0)), + rng.gen_range((-height / 2.0)..(height / 2.0)), ), rng.gen_range(0.5..1.0), ) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index a7ba69b9..4dac032c 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -8,7 +8,7 @@ use iced::widget::{ }; use iced::window; 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 serde::{Deserialize, Serialize}; @@ -22,7 +22,7 @@ pub fn main() -> iced::Result { Todos::run(Settings { window: window::Settings { - size: (500, 800), + size: Size::new(500.0, 800.0), ..window::Settings::default() }, ..Settings::default() diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 375ce889..f46ac1b8 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -10,7 +10,7 @@ pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; use crate::core::window::{self, Event, Icon, Level, Mode, UserAttention}; -use crate::core::Size; +use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::Subscription; @@ -48,17 +48,14 @@ pub fn drag<Message>(id: window::Id) -> Command<Message> { } /// Resizes the window to the given logical dimensions. -pub fn resize<Message>( - id: window::Id, - new_size: Size<u32>, -) -> Command<Message> { +pub fn resize<Message>(id: window::Id, new_size: Size) -> Command<Message> { Command::single(command::Action::Window(id, Action::Resize(new_size))) } /// Fetches the window's size in logical dimensions. pub fn fetch_size<Message>( id: window::Id, - f: impl FnOnce(Size<u32>) -> Message + 'static, + f: impl FnOnce(Size) -> Message + 'static, ) -> Command<Message> { Command::single(command::Action::Window(id, Action::FetchSize(Box::new(f)))) } @@ -74,8 +71,8 @@ pub fn minimize<Message>(id: window::Id, minimized: bool) -> Command<Message> { } /// Moves the window to the given logical coordinates. -pub fn move_to<Message>(id: window::Id, x: i32, y: i32) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Move { x, y })) +pub fn move_to<Message>(id: window::Id, position: Point) -> Command<Message> { + Command::single(command::Action::Window(id, Action::Move(position))) } /// Changes the [`Mode`] of the window. diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 2a31bbd6..5afe0389 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,5 +1,5 @@ use crate::core::window::{Icon, Level, Mode, Settings, UserAttention}; -use crate::core::Size; +use crate::core::{Point, Size}; use crate::futures::MaybeSend; use crate::window::Screenshot; @@ -20,23 +20,18 @@ pub enum Action<T> { /// The settings of the window. settings: Settings, }, - /// Resize the window. - Resize(Size<u32>), - /// Fetch the current size of the window. - FetchSize(Box<dyn FnOnce(Size<u32>) -> T + 'static>), + /// Resize the window to the given logical dimensions. + Resize(Size), + /// Fetch the current logical dimensions of the window. + FetchSize(Box<dyn FnOnce(Size) -> T + 'static>), /// Set the window to maximized or back Maximize(bool), /// Set the window to minimized or back Minimize(bool), - /// Move the window. + /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. - Move { - /// The new logical x location of the window - x: i32, - /// The new logical y location of the window - y: i32, - }, + Move(Point), /// Change the [`Mode`] of the window. ChangeMode(Mode), /// Fetch the current [`Mode`] of the window. @@ -114,7 +109,7 @@ impl<T> Action<T> { Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))), Self::Maximize(maximized) => Action::Maximize(maximized), Self::Minimize(minimized) => Action::Minimize(minimized), - Self::Move { x, y } => Action::Move { x, y }, + Self::Move(position) => Action::Move(position), Self::ChangeMode(mode) => Action::ChangeMode(mode), Self::FetchMode(o) => Action::FetchMode(Box::new(move |s| f(o(s)))), Self::ToggleMaximize => Action::ToggleMaximize, @@ -151,8 +146,8 @@ impl<T> fmt::Debug for Action<T> { Self::Minimize(minimized) => { write!(f, "Action::Minimize({minimized}") } - Self::Move { x, y } => { - write!(f, "Action::Move {{ x: {x}, y: {y} }}") + Self::Move(position) => { + write!(f, "Action::Move({position})") } Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"), Self::FetchMode(_) => write!(f, "Action::FetchMode"), diff --git a/winit/src/application.rs b/winit/src/application.rs index b197c4ed..4e6a879f 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -732,7 +732,8 @@ pub fn run_command<A, C, E>( }); } window::Action::FetchSize(callback) => { - let size = window.inner_size(); + let size = + window.inner_size().to_logical(window.scale_factor()); proxy .send_event(callback(Size::new( @@ -747,10 +748,10 @@ pub fn run_command<A, C, E>( window::Action::Minimize(minimized) => { window.set_minimized(minimized); } - window::Action::Move { x, y } => { + window::Action::Move(position) => { window.set_outer_position(winit::dpi::LogicalPosition { - x, - y, + x: position.x, + y: position.y, }); } window::Action::ChangeMode(mode) => { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 68c2b905..7e51a2d4 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -6,7 +6,7 @@ use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; use crate::core::window; -use crate::core::{Event, Point}; +use crate::core::{Event, Point, Size}; /// Converts some [`window::Settings`] into a `WindowBuilder` from `winit`. pub fn window_settings( @@ -17,11 +17,12 @@ pub fn window_settings( ) -> winit::window::WindowBuilder { let mut window_builder = winit::window::WindowBuilder::new(); - let (width, height) = settings.size; - window_builder = window_builder .with_title(title) - .with_inner_size(winit::dpi::LogicalSize { width, height }) + .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() @@ -41,14 +42,20 @@ pub fn window_settings( window_builder = window_builder.with_position(position); } - if let Some((width, height)) = settings.min_size { - window_builder = window_builder - .with_min_inner_size(winit::dpi::LogicalSize { width, height }); + 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((width, height)) = settings.max_size { - window_builder = window_builder - .with_max_inner_size(winit::dpi::LogicalSize { width, 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( @@ -277,15 +284,15 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel { /// [`winit`]: https://github.com/rust-windowing/winit pub fn position( monitor: Option<&winit::monitor::MonitorHandle>, - (width, height): (u32, u32), + size: Size, position: window::Position, ) -> Option<winit::dpi::Position> { match position { window::Position::Default => None, - window::Position::Specific(x, y) => { + window::Position::Specific(position) => { Some(winit::dpi::Position::Logical(winit::dpi::LogicalPosition { - x: f64::from(x), - y: f64::from(y), + x: f64::from(position.x), + y: f64::from(position.y), })) } window::Position::Centered => { @@ -297,8 +304,8 @@ pub fn position( let centered: winit::dpi::PhysicalPosition<i32> = winit::dpi::LogicalPosition { - x: (resolution.width - f64::from(width)) / 2.0, - y: (resolution.height - f64::from(height)) / 2.0, + x: (resolution.width - f64::from(size.width)) / 2.0, + y: (resolution.height - f64::from(size.height)) / 2.0, } .to_physical(monitor.scale_factor()); diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index f8cedcb8..73476452 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -5,8 +5,12 @@ mod windows; pub use state::State; use crate::conversion; +use crate::core; +use crate::core::mouse; +use crate::core::renderer; use crate::core::widget::operation; -use crate::core::{self, mouse, renderer, window, Size}; +use crate::core::window; +use crate::core::{Point, Size}; use crate::futures::futures::channel::mpsc; use crate::futures::futures::{task, Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; @@ -350,18 +354,18 @@ async fn run_instance<A, E, C>( let mut mouse_interaction = mouse::Interaction::default(); - let mut events = - if let Some((position, size)) = logical_bounds_of(windows.main()) { - vec![( - Some(window::Id::MAIN), - core::Event::Window( - window::Id::MAIN, - window::Event::Created { position, size }, - ), - )] - } else { - Vec::new() - }; + let mut events = { + let (position, size) = logical_bounds_of(windows.main()); + + vec![( + Some(window::Id::MAIN), + core::Event::Window( + window::Id::MAIN, + window::Event::Created { position, size }, + ), + )] + }; + let mut messages = Vec::new(); let mut redraw_pending = false; @@ -374,7 +378,7 @@ async fn run_instance<A, E, C>( window, exit_on_close_request, } => { - let bounds = logical_bounds_of(&window); + let (position, size) = logical_bounds_of(&window); let (inner_size, i) = windows.add( &application, @@ -394,18 +398,13 @@ async fn run_instance<A, E, C>( )); ui_caches.push(user_interface::Cache::default()); - if let Some(bounds) = bounds { - events.push(( - Some(id), - core::Event::Window( - id, - window::Event::Created { - position: bounds.0, - size: bounds.1, - }, - ), - )); - } + events.push(( + Some(id), + core::Event::Window( + id, + window::Event::Created { position, size }, + ), + )); } Event::EventLoopAwakened(event) => { match event { @@ -925,7 +924,8 @@ fn run_command<A, C, E>( } window::Action::FetchSize(callback) => { let window = windows.with_raw(id); - let size = window.inner_size(); + let size = + window.inner_size().to_logical(window.scale_factor()); proxy .send_event(callback(Size::new( @@ -940,9 +940,12 @@ fn run_command<A, C, E>( window::Action::Minimize(minimized) => { windows.with_raw(id).set_minimized(minimized); } - window::Action::Move { x, y } => { + window::Action::Move(position) => { windows.with_raw(id).set_outer_position( - winit::dpi::LogicalPosition { x, y }, + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, ); } window::Action::ChangeMode(mode) => { @@ -1145,25 +1148,23 @@ pub fn user_force_quit( } } -fn logical_bounds_of( - window: &winit::window::Window, -) -> Option<((i32, i32), Size<u32>)> { - let scale = window.scale_factor(); - let pos = window +fn logical_bounds_of(window: &winit::window::Window) -> (Option<Point>, Size) { + let position = window .inner_position() - .map(|pos| { - ((pos.x as f64 / scale) as i32, (pos.y as f64 / scale) as i32) - }) - .ok()?; + .ok() + .map(|position| position.to_logical(window.scale_factor())) + .map(|position| Point { + x: position.x, + y: position.y, + }); + let size = { - let size = window.inner_size(); - Size::new( - (size.width as f64 / scale) as u32, - (size.height as f64 / scale) as u32, - ) + let size = window.inner_size().to_logical(window.scale_factor()); + + Size::new(size.width, size.height) }; - Some((pos, size)) + (position, size) } #[cfg(not(target_arch = "wasm32"))] -- cgit From 99899d49cc93cdec3832f7b5ecad867fdd421e07 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 1 Dec 2023 15:04:08 +0100 Subject: Clamp `text::measure` to `Buffer::size` --- graphics/src/text.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 7261900e..fc7694c2 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -76,7 +76,12 @@ pub fn measure(buffer: &cosmic_text::Buffer) -> Size { (run.line_w.max(width), total_lines + 1) }); - Size::new(width, total_lines as f32 * buffer.metrics().line_height) + let (max_width, max_height) = buffer.size(); + + Size::new( + width.min(max_width), + (total_lines as f32 * buffer.metrics().line_height).min(max_height), + ) } /// Returns the attributes of the given [`Font`]. -- cgit From 936d480267578d7e80675e78ec1880aaaaab72d6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 1 Dec 2023 16:04:27 +0100 Subject: Clip text to `viewport` bounds instead of layout bounds --- core/src/renderer/null.rs | 3 +++ core/src/text.rs | 5 +++- core/src/widget/text.rs | 5 +++- graphics/src/primitive.rs | 26 ++++++++++++-------- graphics/src/renderer.rs | 6 +++++ renderer/src/lib.rs | 13 +++++++--- tiny_skia/src/backend.rs | 17 ++++++------- tiny_skia/src/geometry.rs | 15 +++++++----- wgpu/src/geometry.rs | 15 +++++++----- wgpu/src/layer.rs | 7 ++++++ wgpu/src/layer/text.rs | 5 ++++ wgpu/src/text.rs | 23 +++++++++++------- widget/src/checkbox.rs | 4 +++- widget/src/column.rs | 20 +++++++++------- widget/src/combo_box.rs | 3 ++- widget/src/container.rs | 32 +++++++++++++------------ widget/src/overlay/menu.rs | 1 + widget/src/pick_list.rs | 6 ++++- widget/src/radio.rs | 3 ++- widget/src/row.rs | 20 +++++++++------- widget/src/text_editor.rs | 3 ++- widget/src/text_input.rs | 59 ++++++++++++++++++++++------------------------ widget/src/toggler.rs | 3 ++- 23 files changed, 178 insertions(+), 116 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index da0f32de..3ce6a4f5 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -64,6 +64,7 @@ impl text::Renderer for Null { _paragraph: &Self::Paragraph, _position: Point, _color: Color, + _viewport: Rectangle, ) { } @@ -72,6 +73,7 @@ impl text::Renderer for Null { _editor: &Self::Editor, _position: Point, _color: Color, + _viewport: Rectangle, ) { } @@ -80,6 +82,7 @@ impl text::Renderer for Null { _paragraph: Text<'_, Self::Font>, _position: Point, _color: Color, + _viewport: Rectangle, ) { } } diff --git a/core/src/text.rs b/core/src/text.rs index 546d0b5c..697fa628 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -9,7 +9,7 @@ pub use highlighter::Highlighter; pub use paragraph::Paragraph; use crate::alignment; -use crate::{Color, Pixels, Point, Size}; +use crate::{Color, Pixels, Point, Rectangle, Size}; use std::borrow::Cow; use std::hash::{Hash, Hasher}; @@ -202,6 +202,7 @@ pub trait Renderer: crate::Renderer { text: &Self::Paragraph, position: Point, color: Color, + viewport: Rectangle, ); /// Draws the given [`Editor`] at the given position and with the given @@ -211,6 +212,7 @@ pub trait Renderer: crate::Renderer { editor: &Self::Editor, position: Point, color: Color, + viewport: Rectangle, ); /// Draws the given [`Text`] at the given position and with the given @@ -220,5 +222,6 @@ pub trait Renderer: crate::Renderer { text: Text<'_, Self::Font>, position: Point, color: Color, + viewport: Rectangle, ); } diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 97e0acac..e020b030 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -172,7 +172,7 @@ where style: &renderer::Style, layout: Layout<'_>, _cursor_position: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); @@ -182,6 +182,7 @@ where layout, state, theme.appearance(self.style.clone()), + viewport, ); } } @@ -244,6 +245,7 @@ pub fn draw<Renderer>( layout: Layout<'_>, state: &State<Renderer::Paragraph>, appearance: Appearance, + viewport: &Rectangle, ) where Renderer: text::Renderer, { @@ -266,6 +268,7 @@ pub fn draw<Renderer>( paragraph, Point::new(x, y), appearance.color.unwrap_or(style.text_color), + *viewport, ); } diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 4ed512c1..837eb77a 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -14,24 +14,26 @@ use std::sync::Arc; pub enum Primitive<T> { /// A text primitive Text { - /// The contents of the text + /// The contents of the text. content: String, - /// The bounds of the text + /// The bounds of the text. bounds: Rectangle, - /// The color of the text + /// The color of the text. color: Color, - /// The size of the text in logical pixels + /// The size of the text in logical pixels. size: Pixels, - /// The line height of the text + /// The line height of the text. line_height: text::LineHeight, - /// The font of the text + /// The font of the text. font: Font, - /// The horizontal alignment of the text + /// The horizontal alignment of the text. horizontal_alignment: alignment::Horizontal, - /// The vertical alignment of the text + /// The vertical alignment of the text. vertical_alignment: alignment::Vertical, /// The shaping strategy of the text. shaping: text::Shaping, + /// The viewport of the text. + viewport: Rectangle, }, /// A paragraph primitive Paragraph { @@ -41,15 +43,19 @@ pub enum Primitive<T> { position: Point, /// The color of the paragraph. color: Color, + /// The viewport of the paragraph. + viewport: Rectangle, }, /// An editor primitive Editor { /// The [`editor::Weak`] reference. editor: editor::Weak, - /// The position of the paragraph. + /// The position of the editor. position: Point, - /// The color of the paragraph. + /// The color of the editor. color: Color, + /// The viewport of the editor. + viewport: Rectangle, }, /// A quad primitive Quad { diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index d7613e36..0d3b11a7 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -164,11 +164,13 @@ where paragraph: &Self::Paragraph, position: Point, color: Color, + viewport: Rectangle, ) { self.primitives.push(Primitive::Paragraph { paragraph: paragraph.downgrade(), position, color, + viewport, }); } @@ -177,11 +179,13 @@ where editor: &Self::Editor, position: Point, color: Color, + viewport: Rectangle, ) { self.primitives.push(Primitive::Editor { editor: editor.downgrade(), position, color, + viewport, }); } @@ -190,6 +194,7 @@ where text: Text<'_, Self::Font>, position: Point, color: Color, + viewport: Rectangle, ) { self.primitives.push(Primitive::Text { content: text.content.to_string(), @@ -201,6 +206,7 @@ where horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + viewport, }); } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 1fc4c86b..90a7262b 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -175,11 +175,12 @@ impl<T> text::Renderer for Renderer<T> { paragraph: &Self::Paragraph, position: Point, color: Color, + viewport: Rectangle, ) { delegate!( self, renderer, - renderer.fill_paragraph(paragraph, position, color) + renderer.fill_paragraph(paragraph, position, color, viewport) ); } @@ -188,11 +189,12 @@ impl<T> text::Renderer for Renderer<T> { editor: &Self::Editor, position: Point, color: Color, + viewport: Rectangle, ) { delegate!( self, renderer, - renderer.fill_editor(editor, position, color) + renderer.fill_editor(editor, position, color, viewport) ); } @@ -201,8 +203,13 @@ impl<T> text::Renderer for Renderer<T> { text: Text<'_, Self::Font>, position: Point, color: Color, + viewport: Rectangle, ) { - delegate!(self, renderer, renderer.fill_text(text, position, color)); + delegate!( + self, + renderer, + renderer.fill_text(text, position, color, viewport) + ); } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index f2905b00..cc0f72d1 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,6 +1,6 @@ use crate::core::{Background, Color, Gradient, Rectangle, Vector}; use crate::graphics::backend; -use crate::graphics::{Damage, Viewport}; +use crate::graphics::Viewport; use crate::primitive::{self, Primitive}; use std::borrow::Cow; @@ -361,11 +361,9 @@ impl Backend { paragraph, position, color, + viewport, } => { - let physical_bounds = - (Rectangle::new(*position, paragraph.min_bounds) - + translation) - * scale_factor; + let physical_bounds = (*viewport + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; @@ -387,10 +385,9 @@ impl Backend { editor, position, color, + viewport, } => { - let physical_bounds = - (Rectangle::new(*position, editor.bounds) + translation) - * scale_factor; + let physical_bounds = (*viewport + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; @@ -418,9 +415,9 @@ impl Backend { horizontal_alignment, vertical_alignment, shaping, + viewport, } => { - let physical_bounds = - (primitive.bounds() + translation) * scale_factor; + let physical_bounds = (*viewport + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 1d14aa03..b73f84a9 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -109,15 +109,17 @@ impl Frame { Point::new(transformed[0].x, transformed[0].y) }; + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }; + // TODO: Use vectorial text instead of primitive self.primitives.push(Primitive::Text { content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, + bounds, color: text.color, size: text.size, line_height: text.line_height, @@ -125,6 +127,7 @@ impl Frame { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + viewport: bounds, }); } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 655362b7..c82b9ffb 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -328,15 +328,17 @@ impl Frame { Point::new(transformed.x, transformed.y) }; + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }; + // TODO: Use vectorial text instead of primitive self.primitives.push(Primitive::Text { content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, + bounds, color: text.color, size: text.size, line_height: text.line_height, @@ -344,6 +346,7 @@ impl Frame { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + viewport: bounds, }); } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 98e49f1a..60da3543 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -75,6 +75,7 @@ impl<'a> Layer<'a> { horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: core::text::Shaping::Basic, + viewport: Rectangle::with_size(Size::INFINITY), }; overlay.text.push(Text::Cached(text.clone())); @@ -123,6 +124,7 @@ impl<'a> Layer<'a> { paragraph, position, color, + viewport, } => { let layer = &mut layers[current_layer]; @@ -130,12 +132,14 @@ impl<'a> Layer<'a> { paragraph: paragraph.clone(), position: *position + translation, color: *color, + viewport: *viewport + translation, }); } Primitive::Editor { editor, position, color, + viewport, } => { let layer = &mut layers[current_layer]; @@ -143,6 +147,7 @@ impl<'a> Layer<'a> { editor: editor.clone(), position: *position + translation, color: *color, + viewport: *viewport + translation, }); } Primitive::Text { @@ -155,6 +160,7 @@ impl<'a> Layer<'a> { horizontal_alignment, vertical_alignment, shaping, + viewport, } => { let layer = &mut layers[current_layer]; @@ -168,6 +174,7 @@ impl<'a> Layer<'a> { horizontal_alignment: *horizontal_alignment, vertical_alignment: *vertical_alignment, shaping: *shaping, + viewport: *viewport + translation, })); } Primitive::Quad { diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index 66417cec..c4ea9185 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -13,6 +13,7 @@ pub enum Text<'a> { paragraph: paragraph::Weak, position: Point, color: Color, + viewport: Rectangle, }, /// An editor. #[allow(missing_docs)] @@ -20,6 +21,7 @@ pub enum Text<'a> { editor: editor::Weak, position: Point, color: Color, + viewport: Rectangle, }, /// A cached text. Cached(Cached<'a>), @@ -53,4 +55,7 @@ pub struct Cached<'a> { /// The shaping strategy of the text. pub shaping: text::Shaping, + + /// The viewport of the text. + pub viewport: Rectangle, } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 08a8bea6..7d73c87b 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -120,9 +120,13 @@ impl Pipeline { horizontal_alignment, vertical_alignment, color, + viewport, ) = match section { Text::Paragraph { - position, color, .. + position, + color, + viewport, + .. } => { use crate::core::text::Paragraph as _; @@ -137,10 +141,14 @@ impl Pipeline { paragraph.horizontal_alignment(), paragraph.vertical_alignment(), *color, + *viewport, ) } Text::Editor { - position, color, .. + position, + color, + viewport, + .. } => { use crate::core::text::Editor as _; @@ -155,6 +163,7 @@ impl Pipeline { alignment::Horizontal::Left, alignment::Vertical::Top, *color, + *viewport, ) } Text::Cached(text) => { @@ -173,6 +182,7 @@ impl Pipeline { text.horizontal_alignment, text.vertical_alignment, text.color, + text.viewport, ) } }; @@ -195,13 +205,8 @@ impl Pipeline { alignment::Vertical::Bottom => bounds.y - bounds.height, }; - let section_bounds = Rectangle { - x: left, - y: top, - ..bounds - }; - - let clip_bounds = layer_bounds.intersection(§ion_bounds)?; + let clip_bounds = + layer_bounds.intersection(&(viewport * scale_factor))?; Some(glyphon::TextArea { buffer, diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index d7fdf339..a0d9559b 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -266,7 +266,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let is_mouse_over = cursor.is_over(layout.bounds()); @@ -315,6 +315,7 @@ where }, bounds.center(), custom_style.icon_color, + *viewport, ); } } @@ -330,6 +331,7 @@ where crate::text::Appearance { color: custom_style.text_color, }, + viewport, ); } } diff --git a/widget/src/column.rs b/widget/src/column.rs index 42e90ac1..abb522be 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -224,15 +224,17 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child - .as_widget() - .draw(state, renderer, theme, style, layout, cursor, viewport); + if let Some(viewport) = layout.bounds().intersection(viewport) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, layout, cursor, &viewport, + ); + } } } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 768c2402..31ec27fc 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -622,7 +622,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let is_focused = { let text_input_state = tree.children[0] @@ -645,6 +645,7 @@ where layout, cursor, selection, + viewport, ); } diff --git a/widget/src/container.rs b/widget/src/container.rs index ee7a4965..5dd7705b 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -252,21 +252,23 @@ where ) { let style = theme.appearance(&self.style); - draw_background(renderer, &style, layout.bounds()); - - self.content.as_widget().draw( - tree, - renderer, - theme, - &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), - }, - layout.children().next().unwrap(), - cursor, - viewport, - ); + if let Some(viewport) = layout.bounds().intersection(viewport) { + draw_background(renderer, &style, layout.bounds()); + + self.content.as_widget().draw( + tree, + renderer, + theme, + &renderer::Style { + text_color: style + .text_color + .unwrap_or(renderer_style.text_color), + }, + layout.children().next().unwrap(), + cursor, + &viewport, + ); + } } fn overlay<'b>( diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 5098fa17..e45b44ae 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -544,6 +544,7 @@ where } else { appearance.text_color }, + *viewport, ); } } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 00c1a7ff..022ca8d9 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -235,7 +235,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); draw( @@ -253,6 +253,7 @@ where &self.handle, &self.style, || tree.state.downcast_ref::<State<Renderer::Paragraph>>(), + viewport, ); } @@ -631,6 +632,7 @@ pub fn draw<'a, T, Renderer>( handle: &Handle<Renderer::Font>, style: &<Renderer::Theme as StyleSheet>::Style, state: impl FnOnce() -> &'a State<Renderer::Paragraph>, + viewport: &Rectangle, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -715,6 +717,7 @@ pub fn draw<'a, T, Renderer>( bounds.center_y(), ), style.handle_color, + *viewport, ); } @@ -743,6 +746,7 @@ pub fn draw<'a, T, Renderer>( } else { style.placeholder_color }, + *viewport, ); } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 57acc033..ae2365dd 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -291,7 +291,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let is_mouse_over = cursor.is_over(layout.bounds()); @@ -349,6 +349,7 @@ where crate::text::Appearance { color: custom_style.text_color, }, + viewport, ); } } diff --git a/widget/src/row.rs b/widget/src/row.rs index 7ca90fbb..d52b8c43 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -213,15 +213,17 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child - .as_widget() - .draw(state, renderer, theme, style, layout, cursor, viewport); + if let Some(viewport) = layout.bounds().intersection(viewport) { + for ((child, state), layout) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + { + child.as_widget().draw( + state, renderer, theme, style, layout, cursor, &viewport, + ); + } } } diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 1708a2e5..63d48868 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -429,7 +429,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let bounds = layout.bounds(); @@ -470,6 +470,7 @@ where bounds.position() + Vector::new(self.padding.left, self.padding.top), style.text_color, + *viewport, ); let translation = Vector::new( diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 27efe755..b56e3167 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -238,6 +238,7 @@ where layout: Layout<'_>, cursor: mouse::Cursor, value: Option<&Value>, + viewport: &Rectangle, ) { draw( renderer, @@ -250,6 +251,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + viewport, ); } } @@ -362,7 +364,7 @@ where _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { draw( renderer, @@ -375,6 +377,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + viewport, ); } @@ -1055,6 +1058,7 @@ pub fn draw<Renderer>( is_secure: bool, icon: Option<&Icon<Renderer::Font>>, style: &<Renderer::Theme as StyleSheet>::Style, + viewport: &Rectangle, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -1096,6 +1100,7 @@ pub fn draw<Renderer>( &state.icon, icon_layout.bounds().center(), appearance.icon_color, + *viewport, ); } @@ -1189,39 +1194,31 @@ pub fn draw<Renderer>( (None, 0.0) }; - let text_width = state.value.min_width(); - - let render = |renderer: &mut Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); - } else { - renderer.with_translation(Vector::ZERO, |_| {}); - } - - renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, - Point::new(text_bounds.x, text_bounds.center_y()), - if text.is_empty() { - theme.placeholder_color(style) - } else if is_disabled { - theme.disabled_color(style) - } else { - theme.value_color(style) - }, - ); - }; - - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render); + if let Some((cursor, color)) = cursor { + renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { + renderer.fill_quad(cursor, color) }); } else { - render(renderer); + renderer.with_translation(Vector::ZERO, |_| {}); } + + renderer.fill_paragraph( + if text.is_empty() { + &state.placeholder + } else { + &state.value + }, + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), + if text.is_empty() { + theme.placeholder_color(style) + } else if is_disabled { + theme.disabled_color(style) + } else { + theme.value_color(style) + }, + text_bounds, + ); } /// Computes the current [`mouse::Interaction`] of the [`TextInput`]. diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 476c8330..d8723080 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -266,7 +266,7 @@ where style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { /// Makes sure that the border radius of the toggler looks good at every size. const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; @@ -287,6 +287,7 @@ where label_layout, tree.state.downcast_ref(), crate::text::Appearance::default(), + viewport, ); } -- cgit From 43a7cc2222750b1cada1663b29278b29d3ea232c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 1 Dec 2023 16:10:37 +0100 Subject: Fix `clippy` lint --- widget/src/text_input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index b56e3167..ab0e2412 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1196,7 +1196,7 @@ pub fn draw<Renderer>( if let Some((cursor, color)) = cursor { renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { - renderer.fill_quad(cursor, color) + renderer.fill_quad(cursor, color); }); } else { renderer.with_translation(Vector::ZERO, |_| {}); -- cgit From b526ce4958b28208395276dd4078ffe0d780e1d7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 2 Dec 2023 15:53:02 +0100 Subject: Rename `viewport` to `clip_bounds` --- core/src/renderer/null.rs | 6 +++--- core/src/text.rs | 6 +++--- graphics/src/primitive.rs | 12 ++++++------ graphics/src/renderer.rs | 12 ++++++------ renderer/src/lib.rs | 12 ++++++------ tiny_skia/src/backend.rs | 15 +++++++++------ tiny_skia/src/geometry.rs | 2 +- wgpu/src/geometry.rs | 2 +- wgpu/src/layer.rs | 14 +++++++------- wgpu/src/layer/text.rs | 8 ++++---- wgpu/src/text.rs | 14 +++++++------- 11 files changed, 53 insertions(+), 50 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 3ce6a4f5..7accd34e 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -64,7 +64,7 @@ impl text::Renderer for Null { _paragraph: &Self::Paragraph, _position: Point, _color: Color, - _viewport: Rectangle, + _clip_bounds: Rectangle, ) { } @@ -73,7 +73,7 @@ impl text::Renderer for Null { _editor: &Self::Editor, _position: Point, _color: Color, - _viewport: Rectangle, + _clip_bounds: Rectangle, ) { } @@ -82,7 +82,7 @@ impl text::Renderer for Null { _paragraph: Text<'_, Self::Font>, _position: Point, _color: Color, - _viewport: Rectangle, + _clip_bounds: Rectangle, ) { } } diff --git a/core/src/text.rs b/core/src/text.rs index 697fa628..edef79c2 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -202,7 +202,7 @@ pub trait Renderer: crate::Renderer { text: &Self::Paragraph, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ); /// Draws the given [`Editor`] at the given position and with the given @@ -212,7 +212,7 @@ pub trait Renderer: crate::Renderer { editor: &Self::Editor, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ); /// Draws the given [`Text`] at the given position and with the given @@ -222,6 +222,6 @@ pub trait Renderer: crate::Renderer { text: Text<'_, Self::Font>, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ); } diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 837eb77a..ed75776c 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -32,8 +32,8 @@ pub enum Primitive<T> { vertical_alignment: alignment::Vertical, /// The shaping strategy of the text. shaping: text::Shaping, - /// The viewport of the text. - viewport: Rectangle, + /// The clip bounds of the text. + clip_bounds: Rectangle, }, /// A paragraph primitive Paragraph { @@ -43,8 +43,8 @@ pub enum Primitive<T> { position: Point, /// The color of the paragraph. color: Color, - /// The viewport of the paragraph. - viewport: Rectangle, + /// The clip bounds of the paragraph. + clip_bounds: Rectangle, }, /// An editor primitive Editor { @@ -54,8 +54,8 @@ pub enum Primitive<T> { position: Point, /// The color of the editor. color: Color, - /// The viewport of the editor. - viewport: Rectangle, + /// The clip bounds of the editor. + clip_bounds: Rectangle, }, /// A quad primitive Quad { diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 0d3b11a7..1b0f5c5b 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -164,13 +164,13 @@ where paragraph: &Self::Paragraph, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ) { self.primitives.push(Primitive::Paragraph { paragraph: paragraph.downgrade(), position, color, - viewport, + clip_bounds, }); } @@ -179,13 +179,13 @@ where editor: &Self::Editor, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ) { self.primitives.push(Primitive::Editor { editor: editor.downgrade(), position, color, - viewport, + clip_bounds, }); } @@ -194,7 +194,7 @@ where text: Text<'_, Self::Font>, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ) { self.primitives.push(Primitive::Text { content: text.content.to_string(), @@ -206,7 +206,7 @@ where horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, - viewport, + clip_bounds, }); } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 90a7262b..f2acfa00 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -175,12 +175,12 @@ impl<T> text::Renderer for Renderer<T> { paragraph: &Self::Paragraph, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ) { delegate!( self, renderer, - renderer.fill_paragraph(paragraph, position, color, viewport) + renderer.fill_paragraph(paragraph, position, color, clip_bounds) ); } @@ -189,12 +189,12 @@ impl<T> text::Renderer for Renderer<T> { editor: &Self::Editor, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ) { delegate!( self, renderer, - renderer.fill_editor(editor, position, color, viewport) + renderer.fill_editor(editor, position, color, clip_bounds) ); } @@ -203,12 +203,12 @@ impl<T> text::Renderer for Renderer<T> { text: Text<'_, Self::Font>, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, ) { delegate!( self, renderer, - renderer.fill_text(text, position, color, viewport) + renderer.fill_text(text, position, color, clip_bounds) ); } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index cc0f72d1..3e9bd2a5 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -361,9 +361,10 @@ impl Backend { paragraph, position, color, - viewport, + clip_bounds: text_clip_bounds, } => { - let physical_bounds = (*viewport + translation) * scale_factor; + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; @@ -385,9 +386,10 @@ impl Backend { editor, position, color, - viewport, + clip_bounds: text_clip_bounds, } => { - let physical_bounds = (*viewport + translation) * scale_factor; + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; @@ -415,9 +417,10 @@ impl Backend { horizontal_alignment, vertical_alignment, shaping, - viewport, + clip_bounds: text_clip_bounds, } => { - let physical_bounds = (*viewport + translation) * scale_factor; + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { return; diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index b73f84a9..5f28b737 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -127,7 +127,7 @@ impl Frame { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, - viewport: bounds, + clip_bounds: Rectangle::with_size(Size::INFINITY), }); } diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index c82b9ffb..e0bff67e 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -346,7 +346,7 @@ impl Frame { horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, - viewport: bounds, + clip_bounds: Rectangle::with_size(Size::INFINITY), }); } diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 60da3543..557a7633 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -75,7 +75,7 @@ impl<'a> Layer<'a> { horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: core::text::Shaping::Basic, - viewport: Rectangle::with_size(Size::INFINITY), + clip_bounds: Rectangle::with_size(Size::INFINITY), }; overlay.text.push(Text::Cached(text.clone())); @@ -124,7 +124,7 @@ impl<'a> Layer<'a> { paragraph, position, color, - viewport, + clip_bounds, } => { let layer = &mut layers[current_layer]; @@ -132,14 +132,14 @@ impl<'a> Layer<'a> { paragraph: paragraph.clone(), position: *position + translation, color: *color, - viewport: *viewport + translation, + clip_bounds: *clip_bounds + translation, }); } Primitive::Editor { editor, position, color, - viewport, + clip_bounds, } => { let layer = &mut layers[current_layer]; @@ -147,7 +147,7 @@ impl<'a> Layer<'a> { editor: editor.clone(), position: *position + translation, color: *color, - viewport: *viewport + translation, + clip_bounds: *clip_bounds + translation, }); } Primitive::Text { @@ -160,7 +160,7 @@ impl<'a> Layer<'a> { horizontal_alignment, vertical_alignment, shaping, - viewport, + clip_bounds, } => { let layer = &mut layers[current_layer]; @@ -174,7 +174,7 @@ impl<'a> Layer<'a> { horizontal_alignment: *horizontal_alignment, vertical_alignment: *vertical_alignment, shaping: *shaping, - viewport: *viewport + translation, + clip_bounds: *clip_bounds + translation, })); } Primitive::Quad { diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index c4ea9185..df2f2875 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -13,7 +13,7 @@ pub enum Text<'a> { paragraph: paragraph::Weak, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, }, /// An editor. #[allow(missing_docs)] @@ -21,7 +21,7 @@ pub enum Text<'a> { editor: editor::Weak, position: Point, color: Color, - viewport: Rectangle, + clip_bounds: Rectangle, }, /// A cached text. Cached(Cached<'a>), @@ -56,6 +56,6 @@ pub struct Cached<'a> { /// The shaping strategy of the text. pub shaping: text::Shaping, - /// The viewport of the text. - pub viewport: Rectangle, + /// The clip bounds of the text. + pub clip_bounds: Rectangle, } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 7d73c87b..888b1924 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -120,12 +120,12 @@ impl Pipeline { horizontal_alignment, vertical_alignment, color, - viewport, + clip_bounds, ) = match section { Text::Paragraph { position, color, - viewport, + clip_bounds, .. } => { use crate::core::text::Paragraph as _; @@ -141,13 +141,13 @@ impl Pipeline { paragraph.horizontal_alignment(), paragraph.vertical_alignment(), *color, - *viewport, + *clip_bounds, ) } Text::Editor { position, color, - viewport, + clip_bounds, .. } => { use crate::core::text::Editor as _; @@ -163,7 +163,7 @@ impl Pipeline { alignment::Horizontal::Left, alignment::Vertical::Top, *color, - *viewport, + *clip_bounds, ) } Text::Cached(text) => { @@ -182,7 +182,7 @@ impl Pipeline { text.horizontal_alignment, text.vertical_alignment, text.color, - text.viewport, + text.clip_bounds, ) } }; @@ -206,7 +206,7 @@ impl Pipeline { }; let clip_bounds = - layer_bounds.intersection(&(viewport * scale_factor))?; + layer_bounds.intersection(&(clip_bounds * scale_factor))?; Some(glyphon::TextArea { buffer, -- cgit From ea42af766f345715ff7a7168182d3896ee79cfbc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 2 Dec 2023 20:41:58 +0100 Subject: Use `AtomicU64` for `window::Id` --- core/src/window/event.rs | 32 ++++---- core/src/window/id.rs | 14 ++-- examples/multi_window/src/main.rs | 49 ++++++------ runtime/src/command/action.rs | 8 +- runtime/src/multi_window/program.rs | 4 +- runtime/src/window.rs | 99 ++++++++++++----------- runtime/src/window/action.rs | 154 +++++++++++++++++++----------------- winit/src/application.rs | 36 ++++----- winit/src/multi_window.rs | 44 +++++------ 9 files changed, 228 insertions(+), 212 deletions(-) diff --git a/core/src/window/event.rs b/core/src/window/event.rs index 3ab7cd81..b9ee7aca 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -6,6 +6,22 @@ use std::path::PathBuf; /// A window-related event. #[derive(PartialEq, Clone, Debug)] 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. Moved { /// The new logical x location of the window @@ -30,22 +46,6 @@ pub enum Event { /// The user has requested for the window to close. CloseRequested, - /// A window was created. - Created { - /// The position of the created 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 destroyed by the runtime. - Destroyed, - /// A window was focused. Focused, diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 65002d43..20474c8f 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -1,5 +1,6 @@ -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; + +use std::sync::atomic::{self, AtomicU64}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] /// The id of the window. @@ -7,15 +8,14 @@ use std::hash::{Hash, Hasher}; /// 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 new(id: impl Hash) -> Id { - let mut hasher = DefaultHasher::new(); - id.hash(&mut hasher); - - Id(hasher.finish()) + pub fn unique() -> Id { + Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed)) } } diff --git a/examples/multi_window/src/main.rs b/examples/multi_window/src/main.rs index 16beb46e..5a5e70c1 100644 --- a/examples/multi_window/src/main.rs +++ b/examples/multi_window/src/main.rs @@ -35,8 +35,8 @@ enum Message { ScaleChanged(window::Id, String), TitleChanged(window::Id, String), CloseWindow(window::Id), - WindowCreated(window::Id, Option<Point>), - WindowDestroyed(window::Id), + WindowOpened(window::Id, Option<Point>), + WindowClosed(window::Id), NewWindow, } @@ -69,6 +69,8 @@ impl multi_window::Application for Example { let window = self.windows.get_mut(&id).expect("Window not found!"); window.scale_input = scale; + + Command::none() } Message::ScaleChanged(id, scale) => { let window = @@ -78,20 +80,23 @@ impl multi_window::Application for Example { .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) => { - return window::close(id); - } - Message::WindowDestroyed(id) => { + Message::CloseWindow(id) => window::close(id), + Message::WindowClosed(id) => { self.windows.remove(&id); + Command::none() } - Message::WindowCreated(id, position) => { + Message::WindowOpened(id, position) => { if let Some(position) = position { self.next_window_pos = window::Position::Specific( position + Vector::new(20.0, 20.0), @@ -99,27 +104,25 @@ impl multi_window::Application for Example { } if let Some(window) = self.windows.get(&id) { - return text_input::focus(window.input_id.clone()); + text_input::focus(window.input_id.clone()) + } else { + Command::none() } } Message::NewWindow => { let count = self.windows.len() + 1; - let id = window::Id::new(count); + + 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)); - return window::spawn( - id, - window::Settings { - position: self.next_window_pos, - exit_on_close_request: count % 2 == 0, - ..Default::default() - }, - ); + spawn_window } } - - Command::none() } fn view(&self, window: window::Id) -> Element<Message> { @@ -151,12 +154,10 @@ impl multi_window::Application for Example { window::Event::CloseRequested => { Some(Message::CloseWindow(id)) } - window::Event::Destroyed => { - Some(Message::WindowDestroyed(id)) - } - window::Event::Created { position, .. } => { - Some(Message::WindowCreated(id, position)) + window::Event::Opened { position, .. } => { + Some(Message::WindowOpened(id, position)) } + window::Event::Closed => Some(Message::WindowClosed(id)), _ => None, } } else { diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs index 7a70920e..cb0936df 100644 --- a/runtime/src/command/action.rs +++ b/runtime/src/command/action.rs @@ -27,7 +27,7 @@ pub enum Action<T> { Clipboard(clipboard::Action<T>), /// Run a window action. - Window(window::Id, window::Action<T>), + Window(window::Action<T>), /// Run a system action. System(system::Action<T>), @@ -63,7 +63,7 @@ impl<T> Action<T> { Self::Future(future) => Action::Future(Box::pin(future.map(f))), Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))), Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(id, window) => Action::Window(id, window.map(f)), + Self::Window(window) => Action::Window(window.map(f)), Self::System(system) => Action::System(system.map(f)), Self::Widget(operation) => { Action::Widget(Box::new(widget::operation::map(operation, f))) @@ -84,8 +84,8 @@ impl<T> fmt::Debug for Action<T> { Self::Clipboard(action) => { write!(f, "Action::Clipboard({action:?})") } - Self::Window(id, action) => { - write!(f, "Action::Window({id:?}, {action:?})") + Self::Window(action) => { + write!(f, "Action::Window({action:?})") } Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs index c3989d0d..591b3e9a 100644 --- a/runtime/src/multi_window/program.rs +++ b/runtime/src/multi_window/program.rs @@ -1,8 +1,8 @@ //! Build interactive programs using The Elm Architecture. -use crate::{window, Command}; - 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 { diff --git a/runtime/src/window.rs b/runtime/src/window.rs index f46ac1b8..f9d943f6 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -3,13 +3,14 @@ mod action; pub mod screenshot; -pub use crate::core::window::Id; pub use action::Action; pub use screenshot::Screenshot; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{self, Event, Icon, Level, Mode, UserAttention}; +use crate::core::window::{ + Event, Icon, Id, Level, Mode, Settings, UserAttention, +}; use crate::core::{Point, Size}; use crate::futures::event; use crate::futures::Subscription; @@ -29,73 +30,77 @@ pub fn frames() -> Subscription<Instant> { }) } -/// Spawns a new window with the given `id` and `settings`. -pub fn spawn<Message>( - id: window::Id, - settings: window::Settings, -) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Spawn { settings })) +/// Spawns a new window with the given `settings`. +/// +/// 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: window::Id) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Close)) +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. -pub fn drag<Message>(id: window::Id) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Drag)) +pub fn drag<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::Drag(id))) } /// Resizes the window to the given logical dimensions. -pub fn resize<Message>(id: window::Id, new_size: Size) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Resize(new_size))) +pub fn resize<Message>(id: Id, new_size: Size) -> Command<Message> { + Command::single(command::Action::Window(Action::Resize(id, new_size))) } /// Fetches the window's size in logical dimensions. pub fn fetch_size<Message>( - id: window::Id, + id: Id, f: impl FnOnce(Size) -> Message + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(id, Action::FetchSize(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) } /// Maximizes the window. -pub fn maximize<Message>(id: window::Id, maximized: bool) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Maximize(maximized))) +pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::Maximize(id, maximized))) } /// Minimizes the window. -pub fn minimize<Message>(id: window::Id, minimized: bool) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Minimize(minimized))) +pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> { + Command::single(command::Action::Window(Action::Minimize(id, minimized))) } /// Moves the window to the given logical coordinates. -pub fn move_to<Message>(id: window::Id, position: Point) -> Command<Message> { - Command::single(command::Action::Window(id, Action::Move(position))) +pub fn move_to<Message>(id: Id, position: Point) -> Command<Message> { + Command::single(command::Action::Window(Action::Move(id, position))) } /// Changes the [`Mode`] of the window. -pub fn change_mode<Message>(id: window::Id, mode: Mode) -> Command<Message> { - Command::single(command::Action::Window(id, Action::ChangeMode(mode))) +pub fn change_mode<Message>(id: Id, mode: Mode) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeMode(id, mode))) } /// Fetches the current [`Mode`] of the window. pub fn fetch_mode<Message>( - id: window::Id, + id: Id, f: impl FnOnce(Mode) -> Message + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(id, Action::FetchMode(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f)))) } /// Toggles the window to maximized or back. -pub fn toggle_maximize<Message>(id: window::Id) -> Command<Message> { - Command::single(command::Action::Window(id, Action::ToggleMaximize)) +pub fn toggle_maximize<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. -pub fn toggle_decorations<Message>(id: window::Id) -> Command<Message> { - Command::single(command::Action::Window(id, Action::ToggleDecorations)) +pub fn toggle_decorations<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -105,13 +110,13 @@ pub fn toggle_decorations<Message>(id: window::Id) -> Command<Message> { /// 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. pub fn request_user_attention<Message>( - id: window::Id, + id: Id, user_attention: Option<UserAttention>, ) -> Command<Message> { - Command::single(command::Action::Window( + Command::single(command::Action::Window(Action::RequestUserAttention( id, - Action::RequestUserAttention(user_attention), - )) + user_attention, + ))) } /// Brings the window to the front and sets input focus. Has no effect if the window is @@ -120,36 +125,36 @@ pub fn request_user_attention<Message>( /// 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 /// user experience. -pub fn gain_focus<Message>(id: window::Id) -> Command<Message> { - Command::single(command::Action::Window(id, Action::GainFocus)) +pub fn gain_focus<Message>(id: Id) -> Command<Message> { + Command::single(command::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. -pub fn change_level<Message>(id: window::Id, level: Level) -> Command<Message> { - Command::single(command::Action::Window(id, Action::ChangeLevel(level))) +pub fn change_level<Message>(id: Id, level: Level) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeLevel(id, level))) } /// Fetches an identifier unique to the window, provided by the underlying windowing system. This is -/// not to be confused with [`window::Id`]. +/// not to be confused with [`Id`]. pub fn fetch_id<Message>( - id: window::Id, + id: Id, f: impl FnOnce(u64) -> Message + 'static, ) -> Command<Message> { - Command::single(command::Action::Window(id, Action::FetchId(Box::new(f)))) + Command::single(command::Action::Window(Action::FetchId(id, Box::new(f)))) } /// Changes the [`Icon`] of the window. -pub fn change_icon<Message>(id: window::Id, icon: Icon) -> Command<Message> { - Command::single(command::Action::Window(id, Action::ChangeIcon(icon))) +pub fn change_icon<Message>(id: Id, icon: Icon) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeIcon(id, icon))) } /// Captures a [`Screenshot`] from the window. pub fn screenshot<Message>( - id: window::Id, + id: Id, f: impl FnOnce(Screenshot) -> Message + Send + 'static, ) -> Command<Message> { - Command::single(command::Action::Window( + Command::single(command::Action::Window(Action::Screenshot( id, - Action::Screenshot(Box::new(f)), - )) + Box::new(f), + ))) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 5afe0389..2d98b607 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::core::window::{Icon, Level, Mode, Settings, UserAttention}; +use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention}; use crate::core::{Point, Size}; use crate::futures::MaybeSend; use crate::window::Screenshot; @@ -7,43 +7,40 @@ use std::fmt; /// An operation to be performed on some window. pub enum Action<T> { - /// Close the current window and exits the application. - Close, + /// Spawns a new window with some [`Settings`]. + Spawn(Id, Settings), + /// Close the window and exits the application. + Close(Id), /// Move the window with the left mouse button until the button is /// released. /// /// There’s no guarantee that this will work unless the left mouse /// button was pressed immediately before this function is called. - Drag, - /// Spawns a new window. - Spawn { - /// The settings of the window. - settings: Settings, - }, + Drag(Id), /// Resize the window to the given logical dimensions. - Resize(Size), + Resize(Id, Size), /// Fetch the current logical dimensions of the window. - FetchSize(Box<dyn FnOnce(Size) -> T + 'static>), + FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>), /// Set the window to maximized or back - Maximize(bool), + Maximize(Id, bool), /// Set the window to minimized or back - Minimize(bool), + Minimize(Id, bool), /// Move the window to the given logical coordinates. /// /// Unsupported on Wayland. - Move(Point), + Move(Id, Point), /// Change the [`Mode`] of the window. - ChangeMode(Mode), + ChangeMode(Id, Mode), /// 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 - ToggleMaximize, + ToggleMaximize(Id), /// Toggle whether window has decorations. /// /// ## Platform-specific /// - **X11:** Not implemented. /// - **Web:** Unsupported. - ToggleDecorations, + ToggleDecorations(Id), /// 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, /// see [`UserAttention`] for details. @@ -57,7 +54,7 @@ pub enum Action<T> { /// - **macOS:** `None` has no effect. /// - **X11:** Requests for user attention must be manually cleared. /// - **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 /// already in focus, minimized, or not visible. /// @@ -68,11 +65,11 @@ pub enum Action<T> { /// ## Platform-specific /// /// - **Web / Wayland:** Unsupported. - GainFocus, + GainFocus(Id), /// Change the window [`Level`]. - ChangeLevel(Level), - /// Fetch an identifier unique to the window. - FetchId(Box<dyn FnOnce(u64) -> T + 'static>), + ChangeLevel(Id, Level), + /// Fetch the raw identifier unique to the window. + FetchId(Id, Box<dyn FnOnce(u64) -> T + 'static>), /// Change the window [`Icon`]. /// /// 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 /// said, it's usually in the same ballpark as on Windows. - ChangeIcon(Icon), + ChangeIcon(Id, Icon), /// 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> { @@ -102,30 +99,35 @@ impl<T> Action<T> { T: 'static, { match self { - Self::Close => Action::Close, - Self::Drag => Action::Drag, - Self::Spawn { settings } => Action::Spawn { settings }, - Self::Resize(size) => Action::Resize(size), - Self::FetchSize(o) => Action::FetchSize(Box::new(move |s| f(o(s)))), - Self::Maximize(maximized) => Action::Maximize(maximized), - Self::Minimize(minimized) => Action::Minimize(minimized), - Self::Move(position) => Action::Move(position), - 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::Spawn(id, settings) => Action::Spawn(id, settings), + Self::Close(id) => Action::Close(id), + Self::Drag(id) => Action::Drag(id), + Self::Resize(id, size) => Action::Resize(id, size), + Self::FetchSize(id, o) => { + Action::FetchSize(id, Box::new(move |s| f(o(s)))) } - Self::GainFocus => Action::GainFocus, - Self::ChangeLevel(level) => Action::ChangeLevel(level), - Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), - Self::ChangeIcon(icon) => Action::ChangeIcon(icon), - Self::Screenshot(tag) => { - Action::Screenshot(Box::new(move |screenshot| { - f(tag(screenshot)) - })) + Self::Maximize(id, maximized) => Action::Maximize(id, maximized), + Self::Minimize(id, minimized) => Action::Minimize(id, minimized), + Self::Move(id, position) => Action::Move(id, position), + Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), + Self::FetchMode(id, o) => { + Action::FetchMode(id, Box::new(move |s| f(o(s)))) } + 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))), + ), } } } @@ -133,38 +135,46 @@ impl<T> Action<T> { impl<T> fmt::Debug for Action<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Close => write!(f, "Action::Close"), - Self::Drag => write!(f, "Action::Drag"), - Self::Spawn { settings } => { - write!(f, "Action::Spawn {{ settings: {:?} }}", settings) + Self::Spawn(id, settings) => { + write!(f, "Action::Spawn({id:?}, {settings:?})") + } + Self::Close(id) => write!(f, "Action::Close({id:?})"), + Self::Drag(id) => write!(f, "Action::Drag({id:?})"), + Self::Resize(id, size) => { + write!(f, "Action::Resize({id:?}, {size:?})") + } + Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), + Self::Maximize(id, maximized) => { + write!(f, "Action::Maximize({id:?}, {maximized})") + } + Self::Minimize(id, minimized) => { + write!(f, "Action::Minimize({id:?}, {minimized}") + } + Self::Move(id, position) => { + write!(f, "Action::Move({id:?}, {position})") } - Self::Resize(size) => write!(f, "Action::Resize({size:?})"), - Self::FetchSize(_) => write!(f, "Action::FetchSize"), - Self::Maximize(maximized) => { - write!(f, "Action::Maximize({maximized})") + Self::ChangeMode(id, mode) => { + write!(f, "Action::SetMode({id:?}, {mode:?})") } - Self::Minimize(minimized) => { - write!(f, "Action::Minimize({minimized}") + Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"), + Self::ToggleMaximize(id) => { + write!(f, "Action::ToggleMaximize({id:?})") } - Self::Move(position) => { - write!(f, "Action::Move({position})") + Self::ToggleDecorations(id) => { + write!(f, "Action::ToggleDecorations({id:?})") } - Self::ChangeMode(mode) => write!(f, "Action::SetMode({mode:?})"), - Self::FetchMode(_) => write!(f, "Action::FetchMode"), - Self::ToggleMaximize => write!(f, "Action::ToggleMaximize"), - Self::ToggleDecorations => write!(f, "Action::ToggleDecorations"), - Self::RequestUserAttention(_) => { - write!(f, "Action::RequestUserAttention") + Self::RequestUserAttention(id, _) => { + write!(f, "Action::RequestUserAttention({id:?})") } - Self::GainFocus => write!(f, "Action::GainFocus"), - Self::ChangeLevel(level) => { - write!(f, "Action::ChangeLevel({level:?})") + Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"), + Self::ChangeLevel(id, level) => { + write!(f, "Action::ChangeLevel({id:?}, {level:?})") } - Self::FetchId(_) => write!(f, "Action::FetchId"), - Self::ChangeIcon(_icon) => { - write!(f, "Action::ChangeIcon(icon)") + Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), + Self::ChangeIcon(id, _icon) => { + write!(f, "Action::ChangeIcon({id:?})") } - Self::Screenshot(_) => write!(f, "Action::Screenshot"), + Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"), } } } diff --git a/winit/src/application.rs b/winit/src/application.rs index 4e6a879f..cc1db8cb 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -712,11 +712,11 @@ pub fn run_command<A, C, E>( clipboard.write(contents); } }, - command::Action::Window(_, action) => match action { - window::Action::Close => { + command::Action::Window(action) => match action { + window::Action::Close(_id) => { *should_exit = true; } - window::Action::Drag => { + window::Action::Drag(_id) => { let _res = window.drag_window(); } window::Action::Spawn { .. } => { @@ -725,13 +725,13 @@ pub fn run_command<A, C, E>( multi-window applications." ); } - window::Action::Resize(size) => { + window::Action::Resize(_id, size) => { window.set_inner_size(winit::dpi::LogicalSize { width: size.width, height: size.height, }); } - window::Action::FetchSize(callback) => { + window::Action::FetchSize(_id, callback) => { let size = window.inner_size().to_logical(window.scale_factor()); @@ -742,29 +742,29 @@ pub fn run_command<A, C, E>( ))) .expect("Send message to event loop"); } - window::Action::Maximize(maximized) => { + window::Action::Maximize(_id, maximized) => { window.set_maximized(maximized); } - window::Action::Minimize(minimized) => { + window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); } - window::Action::Move(position) => { + window::Action::Move(_id, position) => { window.set_outer_position(winit::dpi::LogicalPosition { x: position.x, y: position.y, }); } - window::Action::ChangeMode(mode) => { + window::Action::ChangeMode(_id, mode) => { window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( window.current_monitor(), mode, )); } - window::Action::ChangeIcon(icon) => { + window::Action::ChangeIcon(_id, 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) { conversion::mode(window.fullscreen()) } else { @@ -775,29 +775,29 @@ pub fn run_command<A, C, E>( .send_event(tag(mode)) .expect("Send message to event loop"); } - window::Action::ToggleMaximize => { + window::Action::ToggleMaximize(_id) => { window.set_maximized(!window.is_maximized()); } - window::Action::ToggleDecorations => { + window::Action::ToggleDecorations(_id) => { window.set_decorations(!window.is_decorated()); } - window::Action::RequestUserAttention(user_attention) => { + window::Action::RequestUserAttention(_id, user_attention) => { window.request_user_attention( user_attention.map(conversion::user_attention), ); } - window::Action::GainFocus => { + window::Action::GainFocus(_id) => { window.focus_window(); } - window::Action::ChangeLevel(level) => { + window::Action::ChangeLevel(_id, level) => { window.set_window_level(conversion::window_level(level)); } - window::Action::FetchId(tag) => { + window::Action::FetchId(_id, tag) => { proxy .send_event(tag(window.id().into())) .expect("Send message to event loop"); } - window::Action::Screenshot(tag) => { + window::Action::Screenshot(_id, tag) => { let bytes = compositor.screenshot( renderer, surface, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 73476452..aeb2c5e1 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -361,7 +361,7 @@ async fn run_instance<A, E, C>( Some(window::Id::MAIN), core::Event::Window( window::Id::MAIN, - window::Event::Created { position, size }, + window::Event::Opened { position, size }, ), )] }; @@ -402,7 +402,7 @@ async fn run_instance<A, E, C>( Some(id), core::Event::Window( id, - window::Event::Created { position, size }, + window::Event::Opened { position, size }, ), )); } @@ -761,7 +761,7 @@ async fn run_instance<A, E, C>( None, core::Event::Window( id, - window::Event::Destroyed, + window::Event::Closed, ), )); } @@ -884,8 +884,8 @@ fn run_command<A, C, E>( clipboard.write(contents); } }, - command::Action::Window(id, action) => match action { - window::Action::Spawn { settings } => { + command::Action::Window(action) => match action { + window::Action::Spawn(id, settings) => { let monitor = windows.last_monitor(); control_sender @@ -897,7 +897,7 @@ fn run_command<A, C, E>( }) .expect("Send control action"); } - window::Action::Close => { + window::Action::Close(id) => { use winit::event_loop::ControlFlow; let i = windows.delete(id); @@ -911,10 +911,10 @@ fn run_command<A, C, E>( .expect("Send control action"); } } - window::Action::Drag => { + window::Action::Drag(id) => { let _ = windows.with_raw(id).drag_window(); } - window::Action::Resize(size) => { + window::Action::Resize(id, size) => { windows.with_raw(id).set_inner_size( winit::dpi::LogicalSize { width: size.width, @@ -922,7 +922,7 @@ fn run_command<A, C, E>( }, ); } - window::Action::FetchSize(callback) => { + window::Action::FetchSize(id, callback) => { let window = windows.with_raw(id); let size = window.inner_size().to_logical(window.scale_factor()); @@ -934,13 +934,13 @@ fn run_command<A, C, E>( ))) .expect("Send message to event loop"); } - window::Action::Maximize(maximized) => { + window::Action::Maximize(id, maximized) => { windows.with_raw(id).set_maximized(maximized); } - window::Action::Minimize(minimized) => { + window::Action::Minimize(id, minimized) => { windows.with_raw(id).set_minimized(minimized); } - window::Action::Move(position) => { + window::Action::Move(id, position) => { windows.with_raw(id).set_outer_position( winit::dpi::LogicalPosition { x: position.x, @@ -948,7 +948,7 @@ fn run_command<A, C, E>( }, ); } - window::Action::ChangeMode(mode) => { + window::Action::ChangeMode(id, mode) => { let window = windows.with_raw(id); window.set_visible(conversion::visible(mode)); window.set_fullscreen(conversion::fullscreen( @@ -956,12 +956,12 @@ fn run_command<A, C, E>( mode, )); } - window::Action::ChangeIcon(icon) => { + window::Action::ChangeIcon(id, icon) => { windows .with_raw(id) .set_window_icon(conversion::icon(icon)); } - window::Action::FetchMode(tag) => { + window::Action::FetchMode(id, tag) => { let window = windows.with_raw(id); let mode = if window.is_visible().unwrap_or(true) { conversion::mode(window.fullscreen()) @@ -973,33 +973,33 @@ fn run_command<A, C, E>( .send_event(tag(mode)) .expect("Event loop doesn't exist."); } - window::Action::ToggleMaximize => { + window::Action::ToggleMaximize(id) => { let window = windows.with_raw(id); window.set_maximized(!window.is_maximized()); } - window::Action::ToggleDecorations => { + window::Action::ToggleDecorations(id) => { let window = windows.with_raw(id); window.set_decorations(!window.is_decorated()); } - window::Action::RequestUserAttention(attention_type) => { + window::Action::RequestUserAttention(id, attention_type) => { windows.with_raw(id).request_user_attention( attention_type.map(conversion::user_attention), ); } - window::Action::GainFocus => { + window::Action::GainFocus(id) => { windows.with_raw(id).focus_window(); } - window::Action::ChangeLevel(level) => { + window::Action::ChangeLevel(id, level) => { windows .with_raw(id) .set_window_level(conversion::window_level(level)); } - window::Action::FetchId(tag) => { + window::Action::FetchId(id, tag) => { proxy .send_event(tag(windows.with_raw(id).id().into())) .expect("Event loop doesn't exist."); } - window::Action::Screenshot(tag) => { + window::Action::Screenshot(id, tag) => { let i = windows.index_from_id(id); let state = &windows.states[i]; let surface = &mut windows.surfaces[i]; -- cgit From b152ecda63238136f77b6eda3c582fa1eff99737 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 2 Dec 2023 20:49:47 +0100 Subject: Separate `Compositor::new` from `Compositor::create_renderer` --- graphics/src/compositor.rs | 4 ++-- renderer/src/compositor.rs | 47 ++++++++++++-------------------------- tiny_skia/src/window/compositor.rs | 28 +++++++---------------- wgpu/src/window/compositor.rs | 21 ++++------------- winit/src/application.rs | 4 ++-- winit/src/multi_window.rs | 4 ++-- winit/src/multi_window/windows.rs | 2 +- 7 files changed, 35 insertions(+), 75 deletions(-) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 78731a98..b8b575b4 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -22,10 +22,10 @@ pub trait Compositor: Sized { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error>; + ) -> Result<Self, Error>; /// Creates a [`Self::Renderer`] for the [`Compositor`]. - fn renderer(&self) -> Self::Renderer; + fn create_renderer(&self) -> Self::Renderer; /// Crates a new [`Surface`] for the given window. /// diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 5bec1639..9d0ff9ab 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -26,7 +26,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { + ) -> Result<Self, Error> { let candidates = 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 { match candidate.build(settings, compatible_window) { - Ok((compositor, renderer)) => { - return Ok((compositor, renderer)) - } + Ok(compositor) => return Ok(compositor), Err(new_error) => { error = new_error; } @@ -46,14 +44,14 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { Err(error) } - fn renderer(&self) -> Self::Renderer { + fn create_renderer(&self) -> Self::Renderer { match self { Compositor::TinySkia(compositor) => { - Renderer::TinySkia(compositor.renderer()) + Renderer::TinySkia(compositor.create_renderer()) } #[cfg(feature = "wgpu")] Compositor::Wgpu(compositor) => { - Renderer::Wgpu(compositor.renderer()) + Renderer::Wgpu(compositor.create_renderer()) } } } @@ -232,29 +230,21 @@ impl Candidate { self, settings: Settings, _compatible_window: Option<&W>, - ) -> Result<(Compositor<Theme>, Renderer<Theme>), Error> { + ) -> Result<Compositor<Theme>, Error> { match self { Self::TinySkia => { - let (compositor, backend) = - iced_tiny_skia::window::compositor::new( - iced_tiny_skia::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - }, - ); + let compositor = iced_tiny_skia::window::compositor::new( + iced_tiny_skia::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + }, + ); - Ok(( - Compositor::TinySkia(compositor), - Renderer::TinySkia(iced_tiny_skia::Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - )), - )) + Ok(Compositor::TinySkia(compositor)) } #[cfg(feature = "wgpu")] Self::Wgpu => { - let (compositor, backend) = iced_wgpu::window::compositor::new( + let compositor = iced_wgpu::window::compositor::new( iced_wgpu::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, @@ -264,14 +254,7 @@ impl Candidate { _compatible_window, )?; - Ok(( - Compositor::Wgpu(compositor), - Renderer::Wgpu(iced_wgpu::Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - )), - )) + Ok(Compositor::Wgpu(compositor)) } #[cfg(not(feature = "wgpu"))] Self::Wgpu => { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 32095e23..87ded746 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -28,20 +28,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, _compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = new(settings); - - Ok(( - compositor, - Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - ), - )) + ) -> Result<Self, Error> { + Ok(new(settings)) } - fn renderer(&self) -> Self::Renderer { + fn create_renderer(&self) -> Self::Renderer { Renderer::new( Backend::new(), self.settings.default_font, @@ -130,14 +121,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } } -pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) { - ( - Compositor { - settings, - _theme: PhantomData, - }, - Backend::new(), - ) +pub fn new<Theme>(settings: Settings) -> Compositor<Theme> { + Compositor { + settings, + _theme: PhantomData, + } } pub fn present<T: AsRef<str>>( diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 21406134..090e0e9f 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -139,16 +139,14 @@ impl<Theme> Compositor<Theme> { pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Settings, compatible_window: Option<&W>, -) -> Result<(Compositor<Theme>, Backend), Error> { +) -> Result<Compositor<Theme>, Error> { let compositor = futures::executor::block_on(Compositor::request( settings, compatible_window, )) .ok_or(Error::GraphicsAdapterNotFound)?; - let backend = compositor.create_backend(); - - Ok((compositor, backend)) + Ok(compositor) } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. @@ -214,20 +212,11 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = new(settings, compatible_window)?; - - Ok(( - compositor, - Renderer::new( - backend, - settings.default_font, - settings.default_text_size, - ), - )) + ) -> Result<Self, Error> { + new(settings, compatible_window) } - fn renderer(&self) -> Self::Renderer { + fn create_renderer(&self) -> Self::Renderer { Renderer::new( self.create_backend(), self.settings.default_font, diff --git a/winit/src/application.rs b/winit/src/application.rs index cc1db8cb..d9700075 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -181,8 +181,8 @@ where }; } - let (compositor, mut renderer) = - C::new(compositor_settings, Some(&window))?; + let compositor = C::new(compositor_settings, Some(&window))?; + let mut renderer = compositor.create_renderer(); for font in settings.fonts { use crate::core::text::Renderer; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index aeb2c5e1..31c27a6d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -181,8 +181,8 @@ where }; } - let (mut compositor, renderer) = - C::new(compositor_settings, Some(&main_window))?; + let mut compositor = C::new(compositor_settings, Some(&main_window))?; + let renderer = compositor.create_renderer(); let windows = Windows::new( &application, diff --git a/winit/src/multi_window/windows.rs b/winit/src/multi_window/windows.rs index a4841a45..5a33b7b4 100644 --- a/winit/src/multi_window/windows.rs +++ b/winit/src/multi_window/windows.rs @@ -97,7 +97,7 @@ where physical_size.width, physical_size.height, ); - let renderer = compositor.renderer(); + let renderer = compositor.create_renderer(); self.ids.push(id); self.raw.push(window); -- cgit From 31cccd8f7b9b92d486cdcbf39ede112652c9dafa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 2 Dec 2023 20:56:55 +0100 Subject: Remove unnecessary re-exports in `iced_winit` --- winit/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/winit/src/lib.rs b/winit/src/lib.rs index cc886354..948576a2 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -54,6 +54,3 @@ pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; pub use settings::Settings; - -pub use crate::core::window::*; -pub use iced_graphics::Viewport; -- cgit From 5c5e7653bed248ba63faa6563e4d673e4441415e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Sat, 2 Dec 2023 22:26:01 +0100 Subject: Refactor `Windows` abstraction into `WindowManager` --- winit/src/multi_window.rs | 620 ++++++++++++++++--------------- winit/src/multi_window/state.rs | 4 +- winit/src/multi_window/window_manager.rs | 156 ++++++++ winit/src/multi_window/windows.rs | 201 ---------- 4 files changed, 470 insertions(+), 511 deletions(-) create mode 100644 winit/src/multi_window/window_manager.rs delete mode 100644 winit/src/multi_window/windows.rs diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 31c27a6d..84651d40 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -1,21 +1,20 @@ //! Create interactive, native cross-platform applications for WGPU. mod state; -mod windows; +mod window_manager; pub use state::State; use crate::conversion; use crate::core; -use crate::core::mouse; use crate::core::renderer; use crate::core::widget::operation; use crate::core::window; -use crate::core::{Point, Size}; +use crate::core::Size; use crate::futures::futures::channel::mpsc; use crate::futures::futures::{task, Future, StreamExt}; use crate::futures::{Executor, Runtime, Subscription}; use crate::graphics::{compositor, Compositor}; -use crate::multi_window::windows::Windows; +use crate::multi_window::window_manager::WindowManager; use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; @@ -23,6 +22,7 @@ use crate::runtime::Debug; use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; +use std::collections::HashMap; use std::mem::ManuallyDrop; use std::time::Instant; @@ -182,13 +182,13 @@ where } let mut compositor = C::new(compositor_settings, Some(&main_window))?; - let renderer = compositor.create_renderer(); - let windows = Windows::new( + let mut window_manager = WindowManager::new(); + let _ = window_manager.insert( + window::Id::MAIN, + main_window, &application, &mut compositor, - renderer, - main_window, exit_on_close_request, ); @@ -204,7 +204,7 @@ where event_receiver, control_sender, init_command, - windows, + window_manager, should_main_be_visible, )); @@ -312,7 +312,7 @@ async fn run_instance<A, E, C>( mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, mut control_sender: mpsc::UnboundedSender<Control>, init_command: Command<A::Message>, - mut windows: Windows<A, C>, + mut window_manager: WindowManager<A, C>, should_main_window_be_visible: bool, ) where A: Application + 'static, @@ -323,20 +323,39 @@ async fn run_instance<A, E, C>( use winit::event; use winit::event_loop::ControlFlow; - let mut clipboard = Clipboard::connect(windows.main()); + let main_window = window_manager + .get_mut(window::Id::MAIN) + .expect("Get main window"); + + if should_main_window_be_visible { + main_window.raw.set_visible(true); + } + + let mut clipboard = Clipboard::connect(&main_window.raw); + let mut events = { + vec![( + Some(window::Id::MAIN), + core::Event::Window( + window::Id::MAIN, + window::Event::Opened { + position: main_window.position(), + size: main_window.size(), + }, + ), + )] + }; - let mut ui_caches = vec![user_interface::Cache::default()]; + let mut ui_caches = HashMap::new(); let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( &application, &mut debug, - &mut windows, - vec![user_interface::Cache::default()], + &mut window_manager, + HashMap::from_iter([( + window::Id::MAIN, + user_interface::Cache::default(), + )]), )); - if should_main_window_be_visible { - windows.main().set_visible(true); - } - run_command( &application, &mut compositor, @@ -346,26 +365,12 @@ async fn run_instance<A, E, C>( &mut control_sender, &mut proxy, &mut debug, - &mut windows, + &mut window_manager, &mut ui_caches, ); runtime.track(application.subscription().into_recipes()); - let mut mouse_interaction = mouse::Interaction::default(); - - let mut events = { - let (position, size) = logical_bounds_of(windows.main()); - - vec![( - Some(window::Id::MAIN), - core::Event::Window( - window::Id::MAIN, - window::Event::Opened { position, size }, - ), - )] - }; - let mut messages = Vec::new(); let mut redraw_pending = false; @@ -378,31 +383,37 @@ async fn run_instance<A, E, C>( window, exit_on_close_request, } => { - let (position, size) = logical_bounds_of(&window); - - let (inner_size, i) = windows.add( - &application, - &mut compositor, + let window = window_manager.insert( id, window, + &application, + &mut compositor, exit_on_close_request, ); - user_interfaces.push(build_user_interface( - &application, - user_interface::Cache::default(), - &mut windows.renderers[i], - inner_size, - &mut debug, + let logical_size = window.state.logical_size(); + + let _ = user_interfaces.insert( id, - )); - ui_caches.push(user_interface::Cache::default()); + build_user_interface( + &application, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + &mut debug, + id, + ), + ); + let _ = ui_caches.insert(id, user_interface::Cache::default()); events.push(( Some(id), core::Event::Window( id, - window::Event::Opened { position, size }, + window::Event::Opened { + position: window.position(), + size: window.size(), + }, ), )); } @@ -420,12 +431,11 @@ async fn run_instance<A, E, C>( debug.event_processing_started(); let mut uis_stale = false; - for (i, id) in windows.ids.iter().enumerate() { + for (id, window) in window_manager.iter_mut() { let mut window_events = vec![]; events.retain(|(window_id, event)| { - if *window_id == Some(*id) - || window_id.is_none() + if *window_id == Some(id) || window_id.is_none() { window_events.push(event.clone()); false @@ -441,11 +451,13 @@ async fn run_instance<A, E, C>( continue; } - let (ui_state, statuses) = user_interfaces[i] + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") .update( &window_events, - windows.states[i].cursor(), - &mut windows.renderers[i], + window.state.cursor(), + &mut window.renderer, &mut clipboard, &mut messages, ); @@ -469,11 +481,12 @@ async fn run_instance<A, E, C>( // TODO mw application update returns which window IDs to update if !messages.is_empty() || uis_stale { - let mut cached_interfaces: Vec< + let mut cached_interfaces: HashMap< + window::Id, user_interface::Cache, > = ManuallyDrop::into_inner(user_interfaces) - .drain(..) - .map(UserInterface::into_cache) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) .collect(); // Update application @@ -486,18 +499,18 @@ async fn run_instance<A, E, C>( &mut proxy, &mut debug, &mut messages, - &mut windows, + &mut window_manager, &mut cached_interfaces, ); // we must synchronize all window states with application state after an // application update since we don't know what changed - for (state, (id, window)) in windows - .states - .iter_mut() - .zip(windows.ids.iter().zip(windows.raw.iter())) - { - state.synchronize(&application, *id, window); + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &application, + id, + &window.raw, + ); } // rebuild UIs with the synchronized states @@ -505,39 +518,43 @@ async fn run_instance<A, E, C>( ManuallyDrop::new(build_user_interfaces( &application, &mut debug, - &mut windows, + &mut window_manager, cached_interfaces, )); } debug.draw_started(); - for (i, id) in windows.ids.iter().enumerate() { + for (id, window) in window_manager.iter_mut() { // TODO: Avoid redrawing all the time by forcing widgets to // request redraws on state changes // // Then, we can use the `interface_state` here to decide if a redraw // is needed right away, or simply wait until a specific time. let redraw_event = core::Event::Window( - *id, + id, window::Event::RedrawRequested(Instant::now()), ); - let cursor = windows.states[i].cursor(); + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); - let (ui_state, _) = user_interfaces[i].update( + let (ui_state, _) = ui.update( &[redraw_event.clone()], cursor, - &mut windows.renderers[i], + &mut window.renderer, &mut clipboard, &mut messages, ); let new_mouse_interaction = { - let state = &windows.states[i]; + let state = &window.state; - user_interfaces[i].draw( - &mut windows.renderers[i], + ui.draw( + &mut window.renderer, state.theme(), &renderer::Style { text_color: state.text_color(), @@ -546,19 +563,21 @@ async fn run_instance<A, E, C>( ) }; - if new_mouse_interaction != mouse_interaction { - windows.raw[i].set_cursor_icon( + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( conversion::mouse_interaction( new_mouse_interaction, ), ); - mouse_interaction = new_mouse_interaction; + window.mouse_interaction = + new_mouse_interaction; } // TODO once widgets can request to be redrawn, we can avoid always requesting a // redraw - windows.raw[i].request_redraw(); + window.raw.request_redraw(); runtime.broadcast( redraw_event.clone(), @@ -606,9 +625,13 @@ async fn run_instance<A, E, C>( messages.push(message); } event::Event::RedrawRequested(id) => { - let i = windows.index_from_raw(id); - let state = &windows.states[i]; - let physical_size = state.physical_size(); + let Some((id, window)) = + window_manager.get_mut_alias(id) + else { + continue; + }; + + let physical_size = window.state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -616,60 +639,65 @@ async fn run_instance<A, E, C>( } debug.render_started(); - let current_viewport_version = state.viewport_version(); - let window_viewport_version = - windows.viewport_versions[i]; - - if window_viewport_version != current_viewport_version { - let logical_size = state.logical_size(); + if window.viewport_version + != window.state.viewport_version() + { + let logical_size = window.state.logical_size(); debug.layout_started(); - let renderer = &mut windows.renderers[i]; - let ui = user_interfaces.remove(i); + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); - user_interfaces - .insert(i, ui.relayout(logical_size, renderer)); + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); debug.layout_finished(); debug.draw_started(); - let new_mouse_interaction = user_interfaces[i] + let new_mouse_interaction = user_interfaces + .get_mut(&id) + .expect("Get user interface") .draw( - renderer, - state.theme(), + &mut window.renderer, + window.state.theme(), &renderer::Style { - text_color: state.text_color(), + text_color: window.state.text_color(), }, - state.cursor(), + window.state.cursor(), ); - if new_mouse_interaction != mouse_interaction { - windows.raw[i].set_cursor_icon( + if new_mouse_interaction != window.mouse_interaction + { + window.raw.set_cursor_icon( conversion::mouse_interaction( new_mouse_interaction, ), ); - mouse_interaction = new_mouse_interaction; + window.mouse_interaction = + new_mouse_interaction; } debug.draw_finished(); compositor.configure_surface( - &mut windows.surfaces[i], + &mut window.surface, physical_size.width, physical_size.height, ); - windows.viewport_versions[i] = - current_viewport_version; + window.viewport_version = + window.state.viewport_version(); } match compositor.present( - &mut windows.renderers[i], - &mut windows.surfaces[i], - state.viewport(), - state.background_color(), + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), &debug.overlay(), ) { Ok(()) => { @@ -690,8 +718,10 @@ async fn run_instance<A, E, C>( ); // Try rendering all windows again next frame. - for window in &windows.raw { - window.request_redraw(); + for (_id, window) in + window_manager.iter_mut() + { + window.raw.request_redraw(); } } }, @@ -701,70 +731,43 @@ async fn run_instance<A, E, C>( event: window_event, window_id, } => { - let window_index = windows - .raw - .iter() - .position(|w| w.id() == window_id); + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; - match window_index { - Some(i) => { - let id = windows.ids[i]; - let raw = &windows.raw[i]; - let exit_on_close_request = - windows.exit_on_close_requested[i]; + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && window.exit_on_close_request + { + let _ = window_manager.remove(id); + let _ = user_interfaces.remove(&id); + let _ = ui_caches.remove(&id); - if matches!( - window_event, - winit::event::WindowEvent::CloseRequested - ) && exit_on_close_request - { - let i = windows.delete(id); - let _ = user_interfaces.remove(i); - let _ = ui_caches.remove(i); + events.push(( + None, + core::Event::Window(id, window::Event::Closed), + )); - if windows.is_empty() { - break 'main; - } - } else { - let state = &mut windows.states[i]; - state.update( - raw, - &window_event, - &mut debug, - ); - - if let Some(event) = - conversion::window_event( - id, - &window_event, - state.scale_factor(), - state.modifiers(), - ) - { - events.push((Some(id), event)); - } - } + if window_manager.is_empty() { + break 'main; } - None => { - // This is the only special case, since in order to trigger the Destroyed event the - // window reference from winit must be dropped, but we still want to inform the - // user that the window was destroyed so they can clean up any specific window - // code for this window - if matches!( - window_event, - winit::event::WindowEvent::Destroyed - ) { - let id = - windows.get_pending_destroy(window_id); - - events.push(( - None, - core::Event::Window( - id, - window::Event::Closed, - ), - )); - } + } else { + window.state.update( + &window.raw, + &window_event, + &mut debug, + ); + + if let Some(event) = conversion::window_event( + id, + &window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((Some(id), event)); } } } @@ -811,8 +814,8 @@ fn update<A: Application, C, E: Executor>( proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, messages: &mut Vec<A::Message>, - windows: &mut Windows<A, C>, - ui_caches: &mut Vec<user_interface::Cache>, + window_manager: &mut WindowManager<A, C>, + ui_caches: &mut HashMap<window::Id, user_interface::Cache>, ) where C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, @@ -833,7 +836,7 @@ fn update<A: Application, C, E: Executor>( control_sender, proxy, debug, - windows, + window_manager, ui_caches, ); } @@ -852,8 +855,8 @@ fn run_command<A, C, E>( control_sender: &mut mpsc::UnboundedSender<Control>, proxy: &mut winit::event_loop::EventLoopProxy<A::Message>, debug: &mut Debug, - windows: &mut Windows<A, C>, - ui_caches: &mut Vec<user_interface::Cache>, + window_manager: &mut WindowManager<A, C>, + ui_caches: &mut HashMap<window::Id, user_interface::Cache>, ) where A: Application, E: Executor, @@ -886,7 +889,7 @@ fn run_command<A, C, E>( }, command::Action::Window(action) => match action { window::Action::Spawn(id, settings) => { - let monitor = windows.last_monitor(); + let monitor = window_manager.last_monitor(); control_sender .start_send(Control::CreateWindow { @@ -900,10 +903,10 @@ fn run_command<A, C, E>( window::Action::Close(id) => { use winit::event_loop::ControlFlow; - let i = windows.delete(id); - let _ = ui_caches.remove(i); + let _ = window_manager.remove(id); + let _ = ui_caches.remove(&id); - if windows.is_empty() { + if window_manager.is_empty() { control_sender .start_send(Control::ChangeFlow( ControlFlow::ExitWithCode(0), @@ -912,113 +915,133 @@ fn run_command<A, C, E>( } } window::Action::Drag(id) => { - let _ = windows.with_raw(id).drag_window(); + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } } window::Action::Resize(id, size) => { - windows.with_raw(id).set_inner_size( - winit::dpi::LogicalSize { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_inner_size(winit::dpi::LogicalSize { width: size.width, height: size.height, - }, - ); + }); + } } window::Action::FetchSize(id, callback) => { - let window = windows.with_raw(id); - let size = - window.inner_size().to_logical(window.scale_factor()); - - proxy - .send_event(callback(Size::new( - size.width, - size.height, - ))) - .expect("Send message to event loop"); + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + proxy + .send_event(callback(Size::new( + size.width, + size.height, + ))) + .expect("Send message to event loop"); + } } window::Action::Maximize(id, maximized) => { - windows.with_raw(id).set_maximized(maximized); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } } window::Action::Minimize(id, minimized) => { - windows.with_raw(id).set_minimized(minimized); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } } window::Action::Move(id, position) => { - windows.with_raw(id).set_outer_position( - winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }, - ); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, + ); + } } window::Action::ChangeMode(id, mode) => { - let window = windows.with_raw(id); - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - mode, - )); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); + } } window::Action::ChangeIcon(id, icon) => { - windows - .with_raw(id) - .set_window_icon(conversion::icon(icon)); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } } window::Action::FetchMode(id, tag) => { - let window = windows.with_raw(id); - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - proxy - .send_event(tag(mode)) - .expect("Event loop doesn't exist."); + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + proxy + .send_event(tag(mode)) + .expect("Event loop doesn't exist."); + } } window::Action::ToggleMaximize(id) => { - let window = windows.with_raw(id); - window.set_maximized(!window.is_maximized()); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } } window::Action::ToggleDecorations(id) => { - let window = windows.with_raw(id); - window.set_decorations(!window.is_decorated()); + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } } window::Action::RequestUserAttention(id, attention_type) => { - windows.with_raw(id).request_user_attention( - attention_type.map(conversion::user_attention), - ); + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } } window::Action::GainFocus(id) => { - windows.with_raw(id).focus_window(); + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } } window::Action::ChangeLevel(id, level) => { - windows - .with_raw(id) - .set_window_level(conversion::window_level(level)); + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } } window::Action::FetchId(id, tag) => { - proxy - .send_event(tag(windows.with_raw(id).id().into())) - .expect("Event loop doesn't exist."); + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(tag(window.raw.id().into())) + .expect("Event loop doesn't exist."); + } } window::Action::Screenshot(id, tag) => { - let i = windows.index_from_id(id); - let state = &windows.states[i]; - let surface = &mut windows.surfaces[i]; - let renderer = &mut windows.renderers[i]; - - let bytes = compositor.screenshot( - renderer, - surface, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); + if let Some(window) = window_manager.get_mut(id) { + let bytes = compositor.screenshot( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + &debug.overlay(), + ); - proxy - .send_event(tag(window::Screenshot::new( - bytes, - state.physical_size(), - ))) - .expect("Event loop doesn't exist."); + proxy + .send_event(tag(window::Screenshot::new( + bytes, + window.state.physical_size(), + ))) + .expect("Event loop doesn't exist."); + } } }, command::Action::System(action) => match action { @@ -1047,43 +1070,45 @@ fn run_command<A, C, E>( let mut uis = build_user_interfaces( application, debug, - windows, + window_manager, std::mem::take(ui_caches), ); 'operate: while let Some(mut operation) = current_operation.take() { - for (i, ui) in uis.iter_mut().enumerate() { - ui.operate(&windows.renderers[i], operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy - .send_event(message) - .expect("Event loop doesn't exist."); - - // operation completed, don't need to try to operate on rest of UIs - break 'operate; - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); + for (id, ui) in uis.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(message) => { + proxy + .send_event(message) + .expect("Event loop doesn't exist."); + + // operation completed, don't need to try to operate on rest of UIs + break 'operate; + } + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } } } } } *ui_caches = - uis.drain(..).map(UserInterface::into_cache).collect(); + uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); } command::Action::LoadFont { bytes, tagger } => { use crate::core::text::Renderer; // TODO change this once we change each renderer to having a single backend reference.. :pain: // TODO: Error handling (?) - for renderer in &mut windows.renderers { - renderer.load_font(bytes.clone()); + for (_, window) in window_manager.iter_mut() { + window.renderer.load_font(bytes.clone()); } proxy @@ -1098,33 +1123,31 @@ fn run_command<A, C, E>( pub fn build_user_interfaces<'a, A: Application, C: Compositor>( application: &'a A, debug: &mut Debug, - windows: &mut Windows<A, C>, - mut cached_user_interfaces: Vec<user_interface::Cache>, -) -> Vec<UserInterface<'a, A::Message, A::Renderer>> + window_manager: &mut WindowManager<A, C>, + mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, +) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Renderer>> where <A::Renderer as core::Renderer>::Theme: StyleSheet, C: Compositor<Renderer = A::Renderer>, { cached_user_interfaces - .drain(..) - .zip( - windows - .ids - .iter() - .zip(windows.states.iter().zip(windows.renderers.iter_mut())), - ) - .fold(vec![], |mut uis, (cache, (id, (state, renderer)))| { - uis.push(build_user_interface( - application, - cache, - renderer, - state.logical_size(), - debug, - *id, - )); - - uis + .drain() + .filter_map(|(id, cache)| { + let window = window_manager.get_mut(id)?; + + Some(( + id, + build_user_interface( + application, + cache, + &mut window.renderer, + window.state.logical_size(), + debug, + id, + ), + )) }) + .collect() } /// Returns true if the provided event should cause an [`Application`] to @@ -1148,25 +1171,6 @@ pub fn user_force_quit( } } -fn logical_bounds_of(window: &winit::window::Window) -> (Option<Point>, Size) { - let position = window - .inner_position() - .ok() - .map(|position| position.to_logical(window.scale_factor())) - .map(|position| Point { - x: position.x, - y: position.y, - }); - - let size = { - let size = window.inner_size().to_logical(window.scale_factor()); - - Size::new(size.width, size.height) - }; - - (position, size) -} - #[cfg(not(target_arch = "wasm32"))] mod platform { pub fn run<T, F>( diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index e9a9f91a..03da5ad7 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -19,7 +19,7 @@ where title: String, scale_factor: f64, viewport: Viewport, - viewport_version: usize, + viewport_version: u64, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::event::ModifiersState, theme: <A::Renderer as core::Renderer>::Theme, @@ -86,7 +86,7 @@ where /// Returns the version of the [`Viewport`] of the [`State`]. /// /// The version is incremented every time the [`Viewport`] changes. - pub fn viewport_version(&self) -> usize { + pub fn viewport_version(&self) -> u64 { self.viewport_version } diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs new file mode 100644 index 00000000..d54156e7 --- /dev/null +++ b/winit/src/multi_window/window_manager.rs @@ -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) + } +} diff --git a/winit/src/multi_window/windows.rs b/winit/src/multi_window/windows.rs deleted file mode 100644 index 5a33b7b4..00000000 --- a/winit/src/multi_window/windows.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::core::{window, Size}; -use crate::graphics::Compositor; -use crate::multi_window::{Application, State}; -use crate::style::application::StyleSheet; - -use winit::monitor::MonitorHandle; - -use std::fmt::{Debug, Formatter}; - -pub struct Windows<A: Application, C: Compositor> -where - <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, - C: Compositor<Renderer = A::Renderer>, -{ - pub ids: Vec<window::Id>, - pub raw: Vec<winit::window::Window>, - pub states: Vec<State<A>>, - pub viewport_versions: Vec<usize>, - pub exit_on_close_requested: Vec<bool>, - pub surfaces: Vec<C::Surface>, - pub renderers: Vec<A::Renderer>, - pub pending_destroy: Vec<(window::Id, winit::window::WindowId)>, -} - -impl<A: Application, C: Compositor> Debug for Windows<A, C> -where - <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, - C: Compositor<Renderer = A::Renderer>, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Windows") - .field("ids", &self.ids) - .field( - "raw", - &self - .raw - .iter() - .map(winit::window::Window::id) - .collect::<Vec<winit::window::WindowId>>(), - ) - .field("states", &self.states) - .field("viewport_versions", &self.viewport_versions) - .finish() - } -} - -impl<A: Application, C: Compositor> Windows<A, C> -where - <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, - C: Compositor<Renderer = A::Renderer>, -{ - /// Creates a new [`Windows`] with a single `window::Id::MAIN` window. - pub fn new( - application: &A, - compositor: &mut C, - renderer: A::Renderer, - main: winit::window::Window, - exit_on_close_requested: bool, - ) -> Self { - let state = State::new(application, window::Id::MAIN, &main); - let viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - let surface = compositor.create_surface( - &main, - physical_size.width, - physical_size.height, - ); - - Self { - ids: vec![window::Id::MAIN], - raw: vec![main], - states: vec![state], - viewport_versions: vec![viewport_version], - exit_on_close_requested: vec![exit_on_close_requested], - surfaces: vec![surface], - renderers: vec![renderer], - pending_destroy: vec![], - } - } - - /// Adds a new window to [`Windows`]. Returns the size of the newly created window in logical - /// pixels & the index of the window within [`Windows`]. - pub fn add( - &mut self, - application: &A, - compositor: &mut C, - id: window::Id, - window: winit::window::Window, - exit_on_close_requested: bool, - ) -> (Size, usize) { - let state = State::new(application, id, &window); - let window_size = state.logical_size(); - 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(); - - self.ids.push(id); - self.raw.push(window); - self.states.push(state); - self.exit_on_close_requested.push(exit_on_close_requested); - self.viewport_versions.push(viewport_version); - self.surfaces.push(surface); - self.renderers.push(renderer); - - (window_size, self.ids.len() - 1) - } - - pub fn is_empty(&self) -> bool { - self.ids.is_empty() - } - - pub fn main(&self) -> &winit::window::Window { - &self.raw[0] - } - - pub fn index_from_raw(&self, id: winit::window::WindowId) -> usize { - self.raw - .iter() - .position(|window| window.id() == id) - .expect("No raw window in multi_window::Windows") - } - - pub fn index_from_id(&self, id: window::Id) -> usize { - self.ids - .iter() - .position(|window_id| *window_id == id) - .expect("No window in multi_window::Windows") - } - - pub fn last_monitor(&self) -> Option<MonitorHandle> { - self.raw - .last() - .and_then(winit::window::Window::current_monitor) - } - - pub fn last(&self) -> usize { - self.ids.len() - 1 - } - - pub fn with_raw(&self, id: window::Id) -> &winit::window::Window { - let i = self.index_from_id(id); - &self.raw[i] - } - - /// Deletes the window with `id` from [`Windows`]. Returns the index that the window had. - pub fn delete(&mut self, id: window::Id) -> usize { - let i = self.index_from_id(id); - - let id = self.ids.remove(i); - let window = self.raw.remove(i); - let _ = self.states.remove(i); - let _ = self.exit_on_close_requested.remove(i); - let _ = self.viewport_versions.remove(i); - let _ = self.surfaces.remove(i); - - self.pending_destroy.push((id, window.id())); - - i - } - - /// Gets the winit `window` that is pending to be destroyed if it exists. - pub fn get_pending_destroy( - &mut self, - window: winit::window::WindowId, - ) -> window::Id { - let i = self - .pending_destroy - .iter() - .position(|(_, window_id)| window == *window_id) - .unwrap(); - - let (id, _) = self.pending_destroy.remove(i); - id - } - - /// Returns the windows that need to be requested to closed, and also the windows that can be - /// closed immediately. - pub fn partition_close_requests( - &self, - ) -> (Vec<window::Id>, Vec<window::Id>) { - self.exit_on_close_requested.iter().enumerate().fold( - (vec![], vec![]), - |(mut close_immediately, mut needs_request_closed), (i, close)| { - let id = self.ids[i]; - - if *close { - close_immediately.push(id); - } else { - needs_request_closed.push(id); - } - - (close_immediately, needs_request_closed) - }, - ) - } -} -- cgit From 603832e66c710ea39a95009ddc905de20c6856bd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 5 Dec 2023 02:19:17 +0100 Subject: Introduce `RawText` to `Primitive` in `iced_graphics` This should allow users to directly render a `cosmic_text::Buffer`. --- graphics/src/damage.rs | 5 +++++ graphics/src/primitive.rs | 2 ++ graphics/src/text.rs | 27 +++++++++++++++++++++++++-- tiny_skia/src/backend.rs | 30 ++++++++++++++++++++++++++++++ tiny_skia/src/text.rs | 29 ++++++++++++++++++++++++++++- wgpu/src/layer.rs | 15 +++++++++++++++ wgpu/src/layer/text.rs | 5 ++++- wgpu/src/text.rs | 22 ++++++++++++++++++++++ 8 files changed, 131 insertions(+), 4 deletions(-) diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 595cc274..59e9f5b4 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -73,6 +73,11 @@ impl<T: Damage> Damage for Primitive<T> { bounds.expand(1.5) } + Self::RawText(raw) => { + // TODO: Add `size` field to `raw` to compute more accurate + // damage bounds (?) + raw.clip_bounds.expand(1.5) + } Self::Quad { bounds, .. } | Self::Image { bounds, .. } | Self::Svg { bounds, .. } => bounds.expand(1.0), diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index ed75776c..20affaaf 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -57,6 +57,8 @@ pub enum Primitive<T> { /// The clip bounds of the editor. clip_bounds: Rectangle, }, + /// A raw `cosmic-text` primitive + RawText(crate::text::Raw), /// A quad primitive Quad { /// The bounds of the quad diff --git a/graphics/src/text.rs b/graphics/src/text.rs index fc7694c2..8fd037fe 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -12,11 +12,11 @@ pub use cosmic_text; use crate::color; use crate::core::font::{self, Font}; use crate::core::text::Shaping; -use crate::core::{Color, Size}; +use crate::core::{Color, Point, Rectangle, Size}; use once_cell::sync::OnceCell; use std::borrow::Cow; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Weak}; /// Returns the global [`FontSystem`]. pub fn font_system() -> &'static RwLock<FontSystem> { @@ -68,6 +68,29 @@ impl FontSystem { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Version(u32); +/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn. +#[derive(Debug, Clone)] +pub struct Raw { + /// A weak reference to a [`cosmic_text::Buffer`]. + pub buffer: Weak<cosmic_text::Buffer>, + /// The position of the text. + pub position: Point, + /// The color of the text. + pub color: Color, + /// The clip bounds of the text. + pub clip_bounds: Rectangle, +} + +impl PartialEq for Raw { + fn eq(&self, _other: &Self) -> bool { + // TODO: There is no proper way to compare raw buffers + // For now, no two instances of `Raw` text will be equal. + // This should be fine, but could trigger unnecessary redraws + // in the future. + false + } +} + /// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { let (width, total_lines) = buffer diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 3e9bd2a5..706db40e 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,5 +1,6 @@ use crate::core::{Background, Color, Gradient, Rectangle, Vector}; use crate::graphics::backend; +use crate::graphics::text; use crate::graphics::Viewport; use crate::primitive::{self, Primitive}; @@ -444,6 +445,35 @@ impl Backend { clip_mask, ); } + Primitive::RawText(text::Raw { + buffer, + position, + color, + clip_bounds: text_clip_bounds, + }) => { + let Some(buffer) = buffer.upgrade() else { + return; + }; + + let physical_bounds = + (*text_clip_bounds + translation) * scale_factor; + + if !clip_bounds.intersects(&physical_bounds) { + return; + } + + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + self.text_pipeline.draw_raw( + &buffer, + *position + translation, + *color, + scale_factor, + pixels, + clip_mask, + ); + } #[cfg(feature = "image")] Primitive::Image { handle, diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 70e95d01..a5a0a1b6 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,6 +1,6 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::core::{Color, Font, Pixels, Point, Rectangle, Size}; use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; use crate::graphics::text::editor; @@ -149,6 +149,33 @@ impl Pipeline { ); } + pub fn draw_raw( + &mut self, + buffer: &cosmic_text::Buffer, + position: Point, + color: Color, + scale_factor: f32, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::Mask>, + ) { + let mut font_system = font_system().write().expect("Write font system"); + + let (width, height) = buffer.size(); + + draw( + font_system.raw(), + &mut self.glyph_cache, + buffer, + Rectangle::new(position, Size::new(width, height)), + color, + alignment::Horizontal::Left, + alignment::Vertical::Top, + scale_factor, + pixels, + clip_mask, + ); + } + pub fn trim_cache(&mut self) { self.cache.get_mut().trim(); self.glyph_cache.trim(); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 557a7633..4ad12a88 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -177,6 +177,21 @@ impl<'a> Layer<'a> { clip_bounds: *clip_bounds + translation, })); } + graphics::Primitive::RawText(graphics::text::Raw { + buffer, + position, + color, + clip_bounds, + }) => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text::Raw(graphics::text::Raw { + buffer: buffer.clone(), + position: *position + translation, + color: *color, + clip_bounds: *clip_bounds + translation, + })); + } Primitive::Quad { bounds, background, diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs index df2f2875..37ee5247 100644 --- a/wgpu/src/layer/text.rs +++ b/wgpu/src/layer/text.rs @@ -1,6 +1,7 @@ use crate::core::alignment; use crate::core::text; use crate::core::{Color, Font, Pixels, Point, Rectangle}; +use crate::graphics; use crate::graphics::text::editor; use crate::graphics::text::paragraph; @@ -23,8 +24,10 @@ pub enum Text<'a> { color: Color, clip_bounds: Rectangle, }, - /// A cached text. + /// Some cached text. Cached(Cached<'a>), + /// Some raw text. + Raw(graphics::text::Raw), } #[derive(Debug, Clone)] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 888b1924..dca09cb8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -7,6 +7,7 @@ use crate::layer::Text; use std::borrow::Cow; use std::cell::RefCell; +use std::sync::Arc; #[allow(missing_debug_implementations)] pub struct Pipeline { @@ -76,6 +77,7 @@ impl Pipeline { Paragraph(Paragraph), Editor(Editor), Cache(cache::KeyHash), + Raw(Arc<glyphon::Buffer>), } let allocations: Vec<_> = sections @@ -107,6 +109,7 @@ impl Pipeline { Some(Allocation::Cache(key)) } + Text::Raw(text) => text.buffer.upgrade().map(Allocation::Raw), }) .collect(); @@ -185,6 +188,25 @@ impl Pipeline { text.clip_bounds, ) } + Text::Raw(text) => { + let Some(Allocation::Raw(buffer)) = allocation else { + return None; + }; + + let (width, height) = buffer.size(); + + ( + buffer.as_ref(), + Rectangle::new( + text.position, + Size::new(width, height), + ), + alignment::Horizontal::Left, + alignment::Vertical::Top, + text.color, + text.clip_bounds, + ) + } }; let bounds = bounds * scale_factor; -- cgit From 07b0aed5d35013a17deea7ce7824c744decef568 Mon Sep 17 00:00:00 2001 From: Bingus <shankern@protonmail.com> Date: Wed, 6 Dec 2023 14:52:53 -0800 Subject: Added the ability to change the style of a TextEditor --- widget/src/text_editor.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 63d48868..a2a186f0 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -128,6 +128,15 @@ where highlighter_format: to_format, } } + + /// Sets the style of the [`TextEditor`]. + pub fn style( + mut self, + style: impl Into<<Renderer::Theme as StyleSheet>::Style>, + ) -> Self { + self.style = style.into(); + self + } } /// The content of a [`TextEditor`]. -- cgit From a2a96adf7a19f8b2f7879fc19ff139b930fb102e Mon Sep 17 00:00:00 2001 From: Cory Frenette <cory@frenette.dev> Date: Sun, 10 Dec 2023 22:12:46 -0500 Subject: implement svg text fix for native renderer Signed-off-by: Cory Frenette <cory@frenette.dev> --- wgpu/src/image/vector.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 6582bb82..1efc5342 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -2,8 +2,9 @@ use crate::core::svg; use crate::core::{Color, Size}; use crate::image::atlas::{self, Atlas}; +use iced_graphics::text; use resvg::tiny_skia; -use resvg::usvg; +use resvg::usvg::{self, TreeTextToPath}; use std::collections::{HashMap, HashSet}; use std::fs; @@ -51,11 +52,23 @@ impl Cache { let svg = match handle.data() { svg::Data::Path(path) => { - let tree = fs::read_to_string(path).ok().and_then(|contents| { - usvg::Tree::from_str(&contents, &usvg::Options::default()) + let mut tree = + fs::read_to_string(path).ok().and_then(|contents| { + usvg::Tree::from_str( + &contents, + &usvg::Options::default(), + ) .ok() - }); - + }); + // If there are text nodes in the tree load fonts and convert the text to paths + if let Some(svg_tree) = &mut tree { + if svg_tree.has_text_nodes() { + let mut font_system = text::font_system() + .write() + .expect("Read font system"); + svg_tree.convert_text(font_system.raw().db_mut()); + } + } tree.map(Svg::Loaded).unwrap_or(Svg::NotFound) } svg::Data::Bytes(bytes) => { -- cgit From bb30b137d8b836b7e877d938c4cc62feefb9113f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 11 Dec 2023 10:47:17 +0100 Subject: Fix `expect` message in `iced_tiny_skia::vector` --- tiny_skia/src/vector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 9c2893a2..fd1ab3de 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -96,7 +96,7 @@ impl Cache { if let Some(svg) = &mut svg { if svg.has_text_nodes() { let mut font_system = - text::font_system().write().expect("Read font system"); + text::font_system().write().expect("Write font system"); svg.convert_text(font_system.raw().db_mut()); } -- cgit From 33f92b1be78e2f09290e36f0c9b77af899609bd8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 11 Dec 2023 10:47:53 +0100 Subject: Fix import styling in `iced_wgpu::image::vector` --- wgpu/src/image/vector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 1efc5342..f4819095 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -1,8 +1,8 @@ use crate::core::svg; use crate::core::{Color, Size}; +use crate::graphics::text; use crate::image::atlas::{self, Atlas}; -use iced_graphics::text; use resvg::tiny_skia; use resvg::usvg::{self, TreeTextToPath}; use std::collections::{HashMap, HashSet}; -- cgit From 04e8e529a0e80499b129395664f1806de8102d01 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 11 Dec 2023 10:48:07 +0100 Subject: Convert SVG text nodes for in-memory SVGs in `iced_wgpu` --- wgpu/src/image/vector.rs | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index f4819095..d9be50d7 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -50,27 +50,15 @@ impl Cache { return self.svgs.get(&handle.id()).unwrap(); } - let svg = match handle.data() { - svg::Data::Path(path) => { - let mut tree = - fs::read_to_string(path).ok().and_then(|contents| { - usvg::Tree::from_str( - &contents, - &usvg::Options::default(), - ) + let mut svg = match handle.data() { + svg::Data::Path(path) => fs::read_to_string(path) + .ok() + .and_then(|contents| { + usvg::Tree::from_str(&contents, &usvg::Options::default()) .ok() - }); - // If there are text nodes in the tree load fonts and convert the text to paths - if let Some(svg_tree) = &mut tree { - if svg_tree.has_text_nodes() { - let mut font_system = text::font_system() - .write() - .expect("Read font system"); - svg_tree.convert_text(font_system.raw().db_mut()); - } - } - tree.map(Svg::Loaded).unwrap_or(Svg::NotFound) - } + }) + .map(Svg::Loaded) + .unwrap_or(Svg::NotFound), svg::Data::Bytes(bytes) => { match usvg::Tree::from_data(bytes, &usvg::Options::default()) { Ok(tree) => Svg::Loaded(tree), @@ -79,6 +67,15 @@ impl Cache { } }; + if let Svg::Loaded(svg) = &mut svg { + if svg.has_text_nodes() { + let mut font_system = + text::font_system().write().expect("Write font system"); + + svg.convert_text(font_system.raw().db_mut()); + } + } + let _ = self.svgs.insert(handle.id(), svg); self.svgs.get(&handle.id()).unwrap() } -- cgit From dd249a1d11c68b8fee1828d58bae158946ee2ebd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 11 Dec 2023 10:57:48 +0100 Subject: Update `async-tungstenite` in `websocket` example --- examples/websocket/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index 2756e8e0..8f1b876a 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -13,7 +13,7 @@ once_cell.workspace = true warp = "0.3" [dependencies.async-tungstenite] -version = "0.23" +version = "0.24" features = ["tokio-rustls-webpki-roots"] [dependencies.tokio] -- cgit From b54f27d30deec672012c4a63c65d34641b40a9d5 Mon Sep 17 00:00:00 2001 From: hicaru <lich666black@gmail.com> Date: Tue, 12 Dec 2023 14:02:15 +0500 Subject: added svg hover, for styles impl --- style/src/svg.rs | 3 +++ style/src/theme.rs | 8 ++++++++ widget/src/svg.rs | 9 +++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/style/src/svg.rs b/style/src/svg.rs index 9378c1a7..5053f9f8 100644 --- a/style/src/svg.rs +++ b/style/src/svg.rs @@ -20,4 +20,7 @@ pub trait StyleSheet { /// Produces the [`Appearance`] of the svg. fn appearance(&self, style: &Self::Style) -> Appearance; + + /// Produces the hovered [`Appearance`] of a svg content. + fn hovered(&self, style: &Self::Style) -> Appearance; } diff --git a/style/src/theme.rs b/style/src/theme.rs index 47010728..4af07794 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -909,6 +909,10 @@ impl svg::StyleSheet for Theme { Svg::Custom(custom) => custom.appearance(self), } } + + fn hovered(&self, style: &Self::Style) -> svg::Appearance { + self.appearance(style) + } } impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { @@ -917,6 +921,10 @@ impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { fn appearance(&self, style: &Self::Style) -> svg::Appearance { (self)(style) } + + fn hovered(&self, style: &Self::Style) -> svg::Appearance { + self.appearance(style) + } } /// The style of a scrollable. diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 2d01d1ab..f9b16e1a 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -145,7 +145,7 @@ where theme: &Renderer::Theme, _style: &renderer::Style, layout: Layout<'_>, - _cursor: mouse::Cursor, + cursor: mouse::Cursor, _viewport: &Rectangle, ) { let Size { width, height } = renderer.dimensions(&self.handle); @@ -153,6 +153,7 @@ where let bounds = layout.bounds(); let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let is_mouse_over = cursor.is_over(bounds); let render = |renderer: &mut Renderer| { let offset = Vector::new( @@ -166,7 +167,11 @@ where ..bounds }; - let appearance = theme.appearance(&self.style); + let appearance = if is_mouse_over { + theme.appearance(&self.style) + } else { + theme.hovered(&self.style) + }; renderer.draw( self.handle.clone(), -- cgit From 116fb666b05d57df6f70631b11fc8732ed33f71b Mon Sep 17 00:00:00 2001 From: Joao Freitas <51237625+jhff@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:02:13 +0000 Subject: Add deadband distance before initiating drag action on pane grid --- widget/src/pane_grid.rs | 69 ++++++++++++++++++++++++++++++++----------- widget/src/pane_grid/state.rs | 17 +++++++++++ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 2d25a543..7057fe59 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -531,6 +531,8 @@ pub fn update<'a, Message, T: Draggable>( on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, ) -> event::Status { + const DRAG_DEADBAND_DISTANCE: f32 = 10.0; + let mut event_status = event::Status::Ignored; match event { @@ -572,7 +574,6 @@ pub fn update<'a, Message, T: Draggable>( shell, contents, on_click, - on_drag, ); } } @@ -584,7 +585,6 @@ pub fn update<'a, Message, T: Draggable>( shell, contents, on_click, - on_drag, ); } } @@ -637,7 +637,49 @@ pub fn update<'a, Message, T: Draggable>( } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some((_, on_resize)) = on_resize { + if let Some((_, origin)) = action.clicked_pane() { + if let Some(on_drag) = &on_drag { + let bounds = layout.bounds(); + + if let Some(cursor_position) = cursor.position_over(bounds) + { + let mut clicked_region = contents + .zip(layout.children()) + .filter(|(_, layout)| { + layout.bounds().contains(cursor_position) + }); + + if let Some(((pane, content), layout)) = + clicked_region.next() + { + if content + .can_be_dragged_at(layout, cursor_position) + { + let pane_position = layout.position(); + + let new_origin = cursor_position + - Vector::new( + pane_position.x, + pane_position.y, + ); + + if new_origin.distance(origin) + > DRAG_DEADBAND_DISTANCE + { + *action = state::Action::Dragging { + pane, + origin, + }; + + shell.publish(on_drag(DragEvent::Picked { + pane, + })); + } + } + } + } + } + } else if let Some((_, on_resize)) = on_resize { if let Some((split, _)) = action.picked_split() { let bounds = layout.bounds(); @@ -712,7 +754,6 @@ fn click_pane<'a, Message, T>( shell: &mut Shell<'_, Message>, contents: impl Iterator<Item = (Pane, T)>, on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, - on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, ) where T: Draggable, { @@ -720,23 +761,15 @@ fn click_pane<'a, Message, T>( .zip(layout.children()) .filter(|(_, layout)| layout.bounds().contains(cursor_position)); - if let Some(((pane, content), layout)) = clicked_region.next() { + if let Some(((pane, _), layout)) = clicked_region.next() { if let Some(on_click) = &on_click { shell.publish(on_click(pane)); } - if let Some(on_drag) = &on_drag { - if content.can_be_dragged_at(layout, cursor_position) { - let pane_position = layout.position(); - - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); - - *action = state::Action::Dragging { pane, origin }; - - shell.publish(on_drag(DragEvent::Picked { pane })); - } - } + let pane_position = layout.position(); + let origin = + cursor_position - Vector::new(pane_position.x, pane_position.y); + *action = state::Action::Clicking { pane, origin }; } } @@ -749,7 +782,7 @@ pub fn mouse_interaction( spacing: f32, resize_leeway: Option<f32>, ) -> Option<mouse::Interaction> { - if action.picked_pane().is_some() { + if action.clicked_pane().is_some() || action.picked_pane().is_some() { return Some(mouse::Interaction::Grabbing); } diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index 481cd770..5d1fe254 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -403,6 +403,15 @@ pub enum Action { /// /// [`PaneGrid`]: super::PaneGrid Idle, + /// A [`Pane`] in the [`PaneGrid`] is being clicked. + /// + /// [`PaneGrid`]: super::PaneGrid + Clicking { + /// The [`Pane`] being clicked. + pane: Pane, + /// The starting [`Point`] of the click interaction. + origin: Point, + }, /// A [`Pane`] in the [`PaneGrid`] is being dragged. /// /// [`PaneGrid`]: super::PaneGrid @@ -432,6 +441,14 @@ impl Action { } } + /// Returns the current [`Pane`] that is being clicked, if any. + pub fn clicked_pane(&self) -> Option<(Pane, Point)> { + match *self { + Action::Clicking { pane, origin, .. } => Some((pane, origin)), + _ => None, + } + } + /// Returns the current [`Split`] that is being dragged, if any. pub fn picked_split(&self) -> Option<(Split, Axis)> { match *self { -- cgit From e819c2390bad76e811265245bd5fab63fc30a8b2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 15 Dec 2023 13:15:44 +0100 Subject: Update `winit` to `0.29.4` --- Cargo.toml | 4 +- core/Cargo.toml | 4 +- core/src/keyboard/event.rs | 8 +- core/src/mouse/button.rs | 6 + core/src/time.rs | 13 +- examples/integration/src/main.rs | 124 ++++----- examples/modal/src/main.rs | 1 + examples/pokedex/Cargo.toml | 2 +- examples/toast/src/main.rs | 1 + futures/src/keyboard.rs | 1 + widget/src/canvas/event.rs | 2 +- widget/src/shader/event.rs | 2 +- widget/src/text_editor.rs | 12 +- widget/src/text_input.rs | 56 ++--- winit/src/application.rs | 341 +++++++++++-------------- winit/src/application/state.rs | 24 +- winit/src/conversion.rs | 352 +++++++++++--------------- winit/src/multi_window.rs | 527 +++++++++++++++++---------------------- winit/src/multi_window/state.rs | 24 +- 19 files changed, 656 insertions(+), 848 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0afbcd51..a78d0f8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,6 @@ glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "2caa9fc5e592 guillotiere = "0.6" half = "2.2" image = "0.24" -instant = "0.1" kamadak-exif = "0.5" kurbo = "0.9" log = "0.4" @@ -157,7 +156,8 @@ unicode-segmentation = "1.0" wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3" +web-time = "0.2" wgpu = "0.18" winapi = "0.3" window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "c52db2045d0a2f1b8d9923870de1d4ab1994146e", default-features = false } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "3bcdb9abcd7459978ec689523bc21943d38da0f9", default-features = false, features = ["rwh_05", "x11", "wayland"] } diff --git a/core/Cargo.toml b/core/Cargo.toml index 4672c754..c95477c4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,13 +16,11 @@ log.workspace = true thiserror.workspace = true xxhash-rust.workspace = true num-traits.workspace = true +web-time.workspace = true palette.workspace = true palette.optional = true -[target.'cfg(target_arch = "wasm32")'.dependencies] -instant.workspace = true - [target.'cfg(windows)'.dependencies] raw-window-handle.workspace = true diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 016761af..884fc502 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -6,7 +6,7 @@ use super::{KeyCode, Modifiers}; /// additional events, feel free to [open an issue] and share your use case!_ /// /// [open an issue]: https://github.com/iced-rs/iced/issues -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { /// A keyboard key was pressed. KeyPressed { @@ -15,6 +15,9 @@ pub enum Event { /// The state of the modifier keys modifiers: Modifiers, + + /// The text produced by the key press, if any. + text: Option<String>, }, /// A keyboard key was released. @@ -26,9 +29,6 @@ pub enum Event { modifiers: Modifiers, }, - /// A unicode character was received. - CharacterReceived(char), - /// The keyboard modifiers have changed. ModifiersChanged(Modifiers), } diff --git a/core/src/mouse/button.rs b/core/src/mouse/button.rs index 3eec7f42..a8f90329 100644 --- a/core/src/mouse/button.rs +++ b/core/src/mouse/button.rs @@ -10,6 +10,12 @@ pub enum Button { /// The middle (wheel) button. Middle, + /// The back mouse button. + Back, + + /// The forward mouse button. + Forward, + /// Some other button. Other(u16), } diff --git a/core/src/time.rs b/core/src/time.rs index 9355ae6d..dcfe4e41 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -1,13 +1,4 @@ //! Keep track of time, both in native and web platforms! -#[cfg(target_arch = "wasm32")] -pub use instant::Instant; - -#[cfg(target_arch = "wasm32")] -pub use instant::Duration; - -#[cfg(not(target_arch = "wasm32"))] -pub use std::time::Instant; - -#[cfg(not(target_arch = "wasm32"))] -pub use std::time::Duration; +pub use web_time::Duration; +pub use web_time::Instant; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 276794c8..fab81553 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -19,8 +19,9 @@ use iced_winit::winit; use iced_winit::Clipboard; use winit::{ - event::{Event, ModifiersState, WindowEvent}, + event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::ModifiersState, }; #[cfg(target_arch = "wasm32")] @@ -48,7 +49,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { tracing_subscriber::fmt::init(); // Initialize winit - let event_loop = EventLoop::new(); + let event_loop = EventLoop::new()?; #[cfg(target_arch = "wasm32")] let window = winit::window::WindowBuilder::new() @@ -160,67 +161,15 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { ); // Run event loop - event_loop.run(move |event, _, control_flow| { + event_loop.run(move |event, window_target| { // You should change this if you want to render continuosly - *control_flow = ControlFlow::Wait; + window_target.set_control_flow(ControlFlow::Wait); match event { - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CursorMoved { position, .. } => { - cursor_position = Some(position); - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers; - } - WindowEvent::Resized(_) => { - resized = true; - } - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - } - _ => {} - } - - // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( - window::Id::MAIN, - &event, - window.scale_factor(), - modifiers, - ) { - state.queue_event(event); - } - } - Event::MainEventsCleared => { - // If there are events pending - if !state.is_queue_empty() { - // We update iced - let _ = state.update( - viewport.logical_size(), - cursor_position - .map(|p| { - conversion::cursor_position( - p, - viewport.scale_factor(), - ) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable), - &mut renderer, - &Theme::Dark, - &renderer::Style { - text_color: Color::WHITE, - }, - &mut clipboard, - &mut debug, - ); - - // and request a redraw - window.request_redraw(); - } - } - Event::RedrawRequested(_) => { + Event::WindowEvent { + event: WindowEvent::RedrawRequested, + .. + } => { if resized { let size = window.inner_size(); @@ -309,7 +258,60 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { }, } } + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = Some(position); + } + WindowEvent::ModifiersChanged(new_modifiers) => { + modifiers = new_modifiers.state(); + } + WindowEvent::Resized(_) => { + resized = true; + } + WindowEvent::CloseRequested => { + window_target.exit(); + } + _ => {} + } + + // Map window event to iced event + if let Some(event) = iced_winit::conversion::window_event( + window::Id::MAIN, + &event, + window.scale_factor(), + modifiers, + ) { + state.queue_event(event); + } + } _ => {} } - }) + + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + cursor_position + .map(|p| { + conversion::cursor_position(p, viewport.scale_factor()) + }) + .map(mouse::Cursor::Available) + .unwrap_or(mouse::Cursor::Unavailable), + &mut renderer, + &Theme::Dark, + &renderer::Style { + text_color: Color::WHITE, + }, + &mut clipboard, + &mut debug, + ); + + // and request a redraw + window.request_redraw(); + } + })?; + + Ok(()) } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index acb14372..05461dab 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -87,6 +87,7 @@ impl Application for App { Event::Keyboard(keyboard::Event::KeyPressed { key_code: keyboard::KeyCode::Tab, modifiers, + .. }) => { if modifiers.shift() { widget::focus_previous() diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index bf7e1e35..4a55f943 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug", "tokio"] +iced.features = ["image", "debug", "tokio", "webgl"] serde_json = "1.0" diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 31b6f191..e8317589 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -95,6 +95,7 @@ impl Application for App { Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { key_code: keyboard::KeyCode::Tab, modifiers, + .. })) if modifiers.shift() => widget::focus_previous(), Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { key_code: keyboard::KeyCode::Tab, diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index af68e1f2..855eecd4 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -24,6 +24,7 @@ where core::Event::Keyboard(Event::KeyPressed { key_code, modifiers, + .. }), core::event::Status::Ignored, ) => f(key_code, modifiers), diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs index 1288365f..a8eb47f7 100644 --- a/widget/src/canvas/event.rs +++ b/widget/src/canvas/event.rs @@ -8,7 +8,7 @@ pub use crate::core::event::Status; /// A [`Canvas`] event. /// /// [`Canvas`]: crate::Canvas -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Event { /// A mouse event. Mouse(mouse::Event), diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs index 1cc484fb..005c8725 100644 --- a/widget/src/shader/event.rs +++ b/widget/src/shader/event.rs @@ -9,7 +9,7 @@ pub use crate::core::event::Status; /// A [`Shader`] event. /// /// [`Shader`]: crate::Shader -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Event { /// A mouse event. Mouse(mouse::Event), diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a2a186f0..3c0a1806 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -649,6 +649,7 @@ impl Update { keyboard::Event::KeyPressed { key_code, modifiers, + text, } if state.is_focused => { if let Some(motion) = motion(key_code) { let motion = @@ -678,12 +679,15 @@ impl Update { { Some(Self::Paste) } - _ => None, + _ => { + let text = text?; + + edit(Edit::Insert( + text.chars().next().unwrap_or_default(), + )) + } } } - keyboard::Event::CharacterReceived(c) if state.is_focused => { - edit(Edit::Insert(c)) - } _ => None, }, _ => None, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 65d3e1eb..8ba4bd71 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -752,34 +752,9 @@ where return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { - return event::Status::Ignored; - }; - - if state.is_pasting.is_none() - && !state.keyboard_modifiers.command() - && !c.is_control() - { - let mut editor = Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - update_cache(state, value); - - return event::Status::Captured; - } - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyPressed { + key_code, text, .. + }) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -971,7 +946,30 @@ where | keyboard::KeyCode::Down => { return event::Status::Ignored; } - _ => {} + _ => { + if let Some(text) = text { + let c = text.chars().next().unwrap_or_default(); + + if state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() + { + let mut editor = + Editor::new(value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, value); + + return event::Status::Captured; + } + } + } } return event::Status::Captured; diff --git a/winit/src/application.rs b/winit/src/application.rs index d9700075..ed6ba9eb 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -115,7 +115,9 @@ where let mut debug = Debug::new(); debug.startup_started(); - let event_loop = EventLoopBuilder::with_user_event().build(); + let event_loop = EventLoopBuilder::with_user_event() + .build() + .expect("Create event loop"); let proxy = event_loop.create_proxy(); let runtime = { @@ -155,7 +157,7 @@ where { use winit::platform::web::WindowExtWebSys; - let canvas = window.canvas(); + let canvas = window.canvas().expect("Get window canvas"); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); @@ -210,45 +212,28 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); - platform::run(event_loop, move |event, _, control_flow| { - use winit::event_loop::ControlFlow; - - if let ControlFlow::ExitWithCode(_) = control_flow { + let _ = event_loop.run(move |event, event_loop| { + if event_loop.exiting() { return; } - let event = match event { - winit::event::Event::WindowEvent { - event: - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - }, - window_id, - } => Some(winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(*new_inner_size), - window_id, - }), - _ => event.to_static(), - }; - - if let Some(event) = event { - event_sender.start_send(event).expect("Send event"); + event_sender.start_send(event).expect("Send event"); - let poll = instance.as_mut().poll(&mut context); + let poll = instance.as_mut().poll(&mut context); - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = control_receiver.try_next() { - *control_flow = flow; - } - } - task::Poll::Ready(_) => { - *control_flow = ControlFlow::Exit; + match poll { + task::Poll::Pending => { + if let Ok(Some(flow)) = control_receiver.try_next() { + event_loop.set_control_flow(flow); } - }; - } - }) + } + task::Poll::Ready(_) => { + event_loop.exit(); + } + }; + }); + + Ok(()) } async fn run_instance<A, E, C>( @@ -259,7 +244,7 @@ async fn run_instance<A, E, C>( mut proxy: winit::event_loop::EventLoopProxy<A::Message>, mut debug: Debug, mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<'_, A::Message>, + winit::event::Event<A::Message>, >, mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, init_command: Command<A::Message>, @@ -335,89 +320,24 @@ async fn run_instance<A, E, C>( | event::StartCause::ResumeTimeReached { .. } ); } - event::Event::MainEventsCleared => { - if !redraw_pending && events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - debug.event_processing_finished(); - - for (event, status) in - events.drain(..).zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - update( - &mut application, - &mut compositor, - &mut surface, - &mut cache, - &state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &mut messages, - &window, - ); - - // Update window - state.synchronize(&application, &window); - - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } - } - - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = Event::Window( - window::Id::MAIN, - window::Event::RedrawRequested(Instant::now()), - ); - - let (interface_state, _) = user_interface.update( - &[redraw_event.clone()], - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); + event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + )) => { + use crate::core::event; + events.push(Event::PlatformSpecific( + event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( + url, + )), + )); + } + event::Event::UserEvent(message) => { + messages.push(message); + } + event::Event::WindowEvent { + event: event::WindowEvent::RedrawRequested { .. }, + .. + } => { debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -437,38 +357,6 @@ async fn run_instance<A, E, C>( mouse_interaction = new_mouse_interaction; } - window.request_redraw(); - runtime.broadcast(redraw_event, core::event::Status::Ignored); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => ControlFlow::Poll, - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - redraw_pending = false; - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use crate::core::event; - - events.push(Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - )); - } - event::Event::UserEvent(message) => { - messages.push(message); - } - event::Event::RedrawRequested(_) => { let physical_size = state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 { @@ -566,6 +454,98 @@ async fn run_instance<A, E, C>( } _ => {} } + + if !redraw_pending && events.is_empty() && messages.is_empty() { + continue; + } + + debug.event_processing_started(); + + let (interface_state, statuses) = user_interface.update( + &events, + state.cursor(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + + debug.event_processing_finished(); + + for (event, status) in events.drain(..).zip(statuses.into_iter()) { + runtime.broadcast(event, status); + } + + if !messages.is_empty() + || matches!(interface_state, user_interface::State::Outdated) + { + let mut cache = + ManuallyDrop::into_inner(user_interface).into_cache(); + + // Update application + update( + &mut application, + &mut compositor, + &mut surface, + &mut cache, + &state, + &mut renderer, + &mut runtime, + &mut clipboard, + &mut should_exit, + &mut proxy, + &mut debug, + &mut messages, + &window, + ); + + // Update window + state.synchronize(&application, &window); + + user_interface = ManuallyDrop::new(build_user_interface( + &application, + cache, + &mut renderer, + state.logical_size(), + &mut debug, + )); + + if should_exit { + break; + } + } + + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = Event::Window( + window::Id::MAIN, + window::Event::RedrawRequested(Instant::now()), + ); + + let (interface_state, _) = user_interface.update( + &[redraw_event.clone()], + state.cursor(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + + window.request_redraw(); + runtime.broadcast(redraw_event, core::event::Status::Ignored); + + let _ = control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => ControlFlow::Poll, + window::RedrawRequest::At(at) => ControlFlow::WaitUntil(at), + }, + _ => ControlFlow::Wait, + }); + + redraw_pending = false; } // Manually drop the user interface @@ -575,8 +555,8 @@ async fn run_instance<A, E, C>( /// Returns true if the provided event should cause an [`Application`] to /// exit. pub fn requests_exit( - event: &winit::event::WindowEvent<'_>, - _modifiers: winit::event::ModifiersState, + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, ) -> bool { use winit::event::WindowEvent; @@ -584,14 +564,14 @@ pub fn requests_exit( WindowEvent::CloseRequested => true, #[cfg(target_os = "macos")] WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), state: winit::event::ElementState::Pressed, .. }, .. - } if _modifiers.logo() => true, + } if c == "q" && _modifiers.super_key() => true, _ => false, } } @@ -726,10 +706,11 @@ pub fn run_command<A, C, E>( ); } window::Action::Resize(_id, size) => { - window.set_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); + let _ = + window.request_inner_size(winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }); } window::Action::FetchSize(_id, callback) => { let size = @@ -878,43 +859,3 @@ pub fn run_command<A, C, E>( } } } - -#[cfg(not(target_arch = "wasm32"))] -mod platform { - pub fn run<T, F>( - mut event_loop: winit::event_loop::EventLoop<T>, - event_handler: F, - ) -> Result<(), super::Error> - where - F: 'static - + FnMut( - winit::event::Event<'_, T>, - &winit::event_loop::EventLoopWindowTarget<T>, - &mut winit::event_loop::ControlFlow, - ), - { - use winit::platform::run_return::EventLoopExtRunReturn; - - let _ = event_loop.run_return(event_handler); - - Ok(()) - } -} - -#[cfg(target_arch = "wasm32")] -mod platform { - pub fn run<T, F>( - event_loop: winit::event_loop::EventLoop<T>, - event_handler: F, - ) -> ! - where - F: 'static - + FnMut( - winit::event::Event<'_, T>, - &winit::event_loop::EventLoopWindowTarget<T>, - &mut winit::event_loop::ControlFlow, - ), - { - event_loop.run(event_handler) - } -} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index e655529a..8c9b20e0 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -22,7 +22,7 @@ where viewport: Viewport, viewport_version: usize, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, theme: <A::Renderer as core::Renderer>::Theme, appearance: application::Appearance, application: PhantomData<A>, @@ -54,7 +54,7 @@ where viewport, viewport_version: 0, cursor_position: None, - modifiers: winit::event::ModifiersState::default(), + modifiers: winit::keyboard::ModifiersState::default(), theme, appearance, application: PhantomData, @@ -102,7 +102,7 @@ where } /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> winit::event::ModifiersState { + pub fn modifiers(&self) -> winit::keyboard::ModifiersState { self.modifiers } @@ -126,7 +126,7 @@ where pub fn update( &mut self, window: &Window, - event: &WindowEvent<'_>, + event: &WindowEvent, _debug: &mut Debug, ) { match event { @@ -142,10 +142,9 @@ where } WindowEvent::ScaleFactorChanged { scale_factor: new_scale_factor, - new_inner_size, + .. } => { - let size = - Size::new(new_inner_size.width, new_inner_size.height); + let size = self.viewport.physical_size(); self.viewport = Viewport::with_physical_size( size, @@ -164,13 +163,16 @@ where self.cursor_position = None; } WindowEvent::ModifiersChanged(new_modifiers) => { - self.modifiers = *new_modifiers; + self.modifiers = new_modifiers.state(); } #[cfg(feature = "debug")] WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::F12), + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), state: winit::event::ElementState::Pressed, .. }, diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 7e51a2d4..ecc34320 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -128,9 +128,9 @@ pub fn window_settings( /// Converts a winit window event into an iced event. pub fn window_event( id: window::Id, - event: &winit::event::WindowEvent<'_>, + event: &winit::event::WindowEvent, scale_factor: f64, - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, ) -> Option<Event> { use winit::event::WindowEvent; @@ -146,17 +146,6 @@ pub fn window_event( }, )) } - WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { - let logical_size = new_inner_size.to_logical(scale_factor); - - Some(Event::Window( - id, - window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - }, - )) - } WindowEvent::CloseRequested => { Some(Event::Window(id, window::Event::CloseRequested)) } @@ -203,19 +192,17 @@ pub fn window_event( })) } }, - WindowEvent::ReceivedCharacter(c) if !is_private_use_character(*c) => { - Some(Event::Keyboard(keyboard::Event::CharacterReceived(*c))) - } WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(virtual_keycode), + event: + winit::event::KeyEvent { + logical_key, state, + text, .. }, .. } => Some(Event::Keyboard({ - let key_code = key_code(*virtual_keycode); + let key_code = key_code(logical_key); let modifiers = self::modifiers(modifiers); match state { @@ -223,6 +210,9 @@ pub fn window_event( keyboard::Event::KeyPressed { key_code, modifiers, + text: text + .as_ref() + .map(winit::keyboard::SmolStr::to_string), } } winit::event::ElementState::Released => { @@ -233,9 +223,11 @@ pub fn window_event( } } })), - WindowEvent::ModifiersChanged(new_modifiers) => Some(Event::Keyboard( - keyboard::Event::ModifiersChanged(self::modifiers(*new_modifiers)), - )), + WindowEvent::ModifiersChanged(new_modifiers) => { + Some(Event::Keyboard(keyboard::Event::ModifiersChanged( + self::modifiers(new_modifiers.state()), + ))) + } WindowEvent::Focused(focused) => Some(Event::Window( id, if *focused { @@ -365,7 +357,7 @@ pub fn mouse_interaction( match interaction { Interaction::Idle => winit::window::CursorIcon::Default, - Interaction::Pointer => winit::window::CursorIcon::Hand, + Interaction::Pointer => winit::window::CursorIcon::Pointer, Interaction::Working => winit::window::CursorIcon::Progress, Interaction::Grab => winit::window::CursorIcon::Grab, Interaction::Grabbing => winit::window::CursorIcon::Grabbing, @@ -388,6 +380,8 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { winit::event::MouseButton::Left => mouse::Button::Left, winit::event::MouseButton::Right => mouse::Button::Right, winit::event::MouseButton::Middle => mouse::Button::Middle, + winit::event::MouseButton::Back => mouse::Button::Back, + winit::event::MouseButton::Forward => mouse::Button::Forward, winit::event::MouseButton::Other(other) => mouse::Button::Other(other), } } @@ -398,14 +392,14 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 pub fn modifiers( - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, ) -> keyboard::Modifiers { let mut result = keyboard::Modifiers::empty(); - result.set(keyboard::Modifiers::SHIFT, modifiers.shift()); - result.set(keyboard::Modifiers::CTRL, modifiers.ctrl()); - result.set(keyboard::Modifiers::ALT, modifiers.alt()); - result.set(keyboard::Modifiers::LOGO, modifiers.logo()); + result.set(keyboard::Modifiers::SHIFT, modifiers.shift_key()); + result.set(keyboard::Modifiers::CTRL, modifiers.control_key()); + result.set(keyboard::Modifiers::ALT, modifiers.alt_key()); + result.set(keyboard::Modifiers::LOGO, modifiers.super_key()); result } @@ -455,179 +449,125 @@ pub fn touch_event( /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 -pub fn key_code( - virtual_keycode: winit::event::VirtualKeyCode, -) -> keyboard::KeyCode { +pub fn key_code(key: &winit::keyboard::Key) -> keyboard::KeyCode { use keyboard::KeyCode; - - match virtual_keycode { - winit::event::VirtualKeyCode::Key1 => KeyCode::Key1, - winit::event::VirtualKeyCode::Key2 => KeyCode::Key2, - winit::event::VirtualKeyCode::Key3 => KeyCode::Key3, - winit::event::VirtualKeyCode::Key4 => KeyCode::Key4, - winit::event::VirtualKeyCode::Key5 => KeyCode::Key5, - winit::event::VirtualKeyCode::Key6 => KeyCode::Key6, - winit::event::VirtualKeyCode::Key7 => KeyCode::Key7, - winit::event::VirtualKeyCode::Key8 => KeyCode::Key8, - winit::event::VirtualKeyCode::Key9 => KeyCode::Key9, - winit::event::VirtualKeyCode::Key0 => KeyCode::Key0, - winit::event::VirtualKeyCode::A => KeyCode::A, - winit::event::VirtualKeyCode::B => KeyCode::B, - winit::event::VirtualKeyCode::C => KeyCode::C, - winit::event::VirtualKeyCode::D => KeyCode::D, - winit::event::VirtualKeyCode::E => KeyCode::E, - winit::event::VirtualKeyCode::F => KeyCode::F, - winit::event::VirtualKeyCode::G => KeyCode::G, - winit::event::VirtualKeyCode::H => KeyCode::H, - winit::event::VirtualKeyCode::I => KeyCode::I, - winit::event::VirtualKeyCode::J => KeyCode::J, - winit::event::VirtualKeyCode::K => KeyCode::K, - winit::event::VirtualKeyCode::L => KeyCode::L, - winit::event::VirtualKeyCode::M => KeyCode::M, - winit::event::VirtualKeyCode::N => KeyCode::N, - winit::event::VirtualKeyCode::O => KeyCode::O, - winit::event::VirtualKeyCode::P => KeyCode::P, - winit::event::VirtualKeyCode::Q => KeyCode::Q, - winit::event::VirtualKeyCode::R => KeyCode::R, - winit::event::VirtualKeyCode::S => KeyCode::S, - winit::event::VirtualKeyCode::T => KeyCode::T, - winit::event::VirtualKeyCode::U => KeyCode::U, - winit::event::VirtualKeyCode::V => KeyCode::V, - winit::event::VirtualKeyCode::W => KeyCode::W, - winit::event::VirtualKeyCode::X => KeyCode::X, - winit::event::VirtualKeyCode::Y => KeyCode::Y, - winit::event::VirtualKeyCode::Z => KeyCode::Z, - winit::event::VirtualKeyCode::Escape => KeyCode::Escape, - winit::event::VirtualKeyCode::F1 => KeyCode::F1, - winit::event::VirtualKeyCode::F2 => KeyCode::F2, - winit::event::VirtualKeyCode::F3 => KeyCode::F3, - winit::event::VirtualKeyCode::F4 => KeyCode::F4, - winit::event::VirtualKeyCode::F5 => KeyCode::F5, - winit::event::VirtualKeyCode::F6 => KeyCode::F6, - winit::event::VirtualKeyCode::F7 => KeyCode::F7, - winit::event::VirtualKeyCode::F8 => KeyCode::F8, - winit::event::VirtualKeyCode::F9 => KeyCode::F9, - winit::event::VirtualKeyCode::F10 => KeyCode::F10, - winit::event::VirtualKeyCode::F11 => KeyCode::F11, - winit::event::VirtualKeyCode::F12 => KeyCode::F12, - winit::event::VirtualKeyCode::F13 => KeyCode::F13, - winit::event::VirtualKeyCode::F14 => KeyCode::F14, - winit::event::VirtualKeyCode::F15 => KeyCode::F15, - winit::event::VirtualKeyCode::F16 => KeyCode::F16, - winit::event::VirtualKeyCode::F17 => KeyCode::F17, - winit::event::VirtualKeyCode::F18 => KeyCode::F18, - winit::event::VirtualKeyCode::F19 => KeyCode::F19, - winit::event::VirtualKeyCode::F20 => KeyCode::F20, - winit::event::VirtualKeyCode::F21 => KeyCode::F21, - winit::event::VirtualKeyCode::F22 => KeyCode::F22, - winit::event::VirtualKeyCode::F23 => KeyCode::F23, - winit::event::VirtualKeyCode::F24 => KeyCode::F24, - winit::event::VirtualKeyCode::Snapshot => KeyCode::Snapshot, - winit::event::VirtualKeyCode::Scroll => KeyCode::Scroll, - winit::event::VirtualKeyCode::Pause => KeyCode::Pause, - winit::event::VirtualKeyCode::Insert => KeyCode::Insert, - winit::event::VirtualKeyCode::Home => KeyCode::Home, - winit::event::VirtualKeyCode::Delete => KeyCode::Delete, - winit::event::VirtualKeyCode::End => KeyCode::End, - winit::event::VirtualKeyCode::PageDown => KeyCode::PageDown, - winit::event::VirtualKeyCode::PageUp => KeyCode::PageUp, - winit::event::VirtualKeyCode::Left => KeyCode::Left, - winit::event::VirtualKeyCode::Up => KeyCode::Up, - winit::event::VirtualKeyCode::Right => KeyCode::Right, - winit::event::VirtualKeyCode::Down => KeyCode::Down, - winit::event::VirtualKeyCode::Back => KeyCode::Backspace, - winit::event::VirtualKeyCode::Return => KeyCode::Enter, - winit::event::VirtualKeyCode::Space => KeyCode::Space, - winit::event::VirtualKeyCode::Compose => KeyCode::Compose, - winit::event::VirtualKeyCode::Caret => KeyCode::Caret, - winit::event::VirtualKeyCode::Numlock => KeyCode::Numlock, - winit::event::VirtualKeyCode::Numpad0 => KeyCode::Numpad0, - winit::event::VirtualKeyCode::Numpad1 => KeyCode::Numpad1, - winit::event::VirtualKeyCode::Numpad2 => KeyCode::Numpad2, - winit::event::VirtualKeyCode::Numpad3 => KeyCode::Numpad3, - winit::event::VirtualKeyCode::Numpad4 => KeyCode::Numpad4, - winit::event::VirtualKeyCode::Numpad5 => KeyCode::Numpad5, - winit::event::VirtualKeyCode::Numpad6 => KeyCode::Numpad6, - winit::event::VirtualKeyCode::Numpad7 => KeyCode::Numpad7, - winit::event::VirtualKeyCode::Numpad8 => KeyCode::Numpad8, - winit::event::VirtualKeyCode::Numpad9 => KeyCode::Numpad9, - winit::event::VirtualKeyCode::AbntC1 => KeyCode::AbntC1, - winit::event::VirtualKeyCode::AbntC2 => KeyCode::AbntC2, - winit::event::VirtualKeyCode::NumpadAdd => KeyCode::NumpadAdd, - winit::event::VirtualKeyCode::Plus => KeyCode::Plus, - winit::event::VirtualKeyCode::Apostrophe => KeyCode::Apostrophe, - winit::event::VirtualKeyCode::Apps => KeyCode::Apps, - winit::event::VirtualKeyCode::At => KeyCode::At, - winit::event::VirtualKeyCode::Ax => KeyCode::Ax, - winit::event::VirtualKeyCode::Backslash => KeyCode::Backslash, - winit::event::VirtualKeyCode::Calculator => KeyCode::Calculator, - winit::event::VirtualKeyCode::Capital => KeyCode::Capital, - winit::event::VirtualKeyCode::Colon => KeyCode::Colon, - winit::event::VirtualKeyCode::Comma => KeyCode::Comma, - winit::event::VirtualKeyCode::Convert => KeyCode::Convert, - winit::event::VirtualKeyCode::NumpadDecimal => KeyCode::NumpadDecimal, - winit::event::VirtualKeyCode::NumpadDivide => KeyCode::NumpadDivide, - winit::event::VirtualKeyCode::Equals => KeyCode::Equals, - winit::event::VirtualKeyCode::Grave => KeyCode::Grave, - winit::event::VirtualKeyCode::Kana => KeyCode::Kana, - winit::event::VirtualKeyCode::Kanji => KeyCode::Kanji, - winit::event::VirtualKeyCode::LAlt => KeyCode::LAlt, - winit::event::VirtualKeyCode::LBracket => KeyCode::LBracket, - winit::event::VirtualKeyCode::LControl => KeyCode::LControl, - winit::event::VirtualKeyCode::LShift => KeyCode::LShift, - winit::event::VirtualKeyCode::LWin => KeyCode::LWin, - winit::event::VirtualKeyCode::Mail => KeyCode::Mail, - winit::event::VirtualKeyCode::MediaSelect => KeyCode::MediaSelect, - winit::event::VirtualKeyCode::MediaStop => KeyCode::MediaStop, - winit::event::VirtualKeyCode::Minus => KeyCode::Minus, - winit::event::VirtualKeyCode::NumpadMultiply => KeyCode::NumpadMultiply, - winit::event::VirtualKeyCode::Mute => KeyCode::Mute, - winit::event::VirtualKeyCode::MyComputer => KeyCode::MyComputer, - winit::event::VirtualKeyCode::NavigateForward => { - KeyCode::NavigateForward - } - winit::event::VirtualKeyCode::NavigateBackward => { - KeyCode::NavigateBackward - } - winit::event::VirtualKeyCode::NextTrack => KeyCode::NextTrack, - winit::event::VirtualKeyCode::NoConvert => KeyCode::NoConvert, - winit::event::VirtualKeyCode::NumpadComma => KeyCode::NumpadComma, - winit::event::VirtualKeyCode::NumpadEnter => KeyCode::NumpadEnter, - winit::event::VirtualKeyCode::NumpadEquals => KeyCode::NumpadEquals, - winit::event::VirtualKeyCode::OEM102 => KeyCode::OEM102, - winit::event::VirtualKeyCode::Period => KeyCode::Period, - winit::event::VirtualKeyCode::PlayPause => KeyCode::PlayPause, - winit::event::VirtualKeyCode::Power => KeyCode::Power, - winit::event::VirtualKeyCode::PrevTrack => KeyCode::PrevTrack, - winit::event::VirtualKeyCode::RAlt => KeyCode::RAlt, - winit::event::VirtualKeyCode::RBracket => KeyCode::RBracket, - winit::event::VirtualKeyCode::RControl => KeyCode::RControl, - winit::event::VirtualKeyCode::RShift => KeyCode::RShift, - winit::event::VirtualKeyCode::RWin => KeyCode::RWin, - winit::event::VirtualKeyCode::Semicolon => KeyCode::Semicolon, - winit::event::VirtualKeyCode::Slash => KeyCode::Slash, - winit::event::VirtualKeyCode::Sleep => KeyCode::Sleep, - winit::event::VirtualKeyCode::Stop => KeyCode::Stop, - winit::event::VirtualKeyCode::NumpadSubtract => KeyCode::NumpadSubtract, - winit::event::VirtualKeyCode::Sysrq => KeyCode::Sysrq, - winit::event::VirtualKeyCode::Tab => KeyCode::Tab, - winit::event::VirtualKeyCode::Underline => KeyCode::Underline, - winit::event::VirtualKeyCode::Unlabeled => KeyCode::Unlabeled, - winit::event::VirtualKeyCode::VolumeDown => KeyCode::VolumeDown, - winit::event::VirtualKeyCode::VolumeUp => KeyCode::VolumeUp, - winit::event::VirtualKeyCode::Wake => KeyCode::Wake, - winit::event::VirtualKeyCode::WebBack => KeyCode::WebBack, - winit::event::VirtualKeyCode::WebFavorites => KeyCode::WebFavorites, - winit::event::VirtualKeyCode::WebForward => KeyCode::WebForward, - winit::event::VirtualKeyCode::WebHome => KeyCode::WebHome, - winit::event::VirtualKeyCode::WebRefresh => KeyCode::WebRefresh, - winit::event::VirtualKeyCode::WebSearch => KeyCode::WebSearch, - winit::event::VirtualKeyCode::WebStop => KeyCode::WebStop, - winit::event::VirtualKeyCode::Yen => KeyCode::Yen, - winit::event::VirtualKeyCode::Copy => KeyCode::Copy, - winit::event::VirtualKeyCode::Paste => KeyCode::Paste, - winit::event::VirtualKeyCode::Cut => KeyCode::Cut, - winit::event::VirtualKeyCode::Asterisk => KeyCode::Asterisk, + use winit::keyboard::NamedKey; + + match key { + winit::keyboard::Key::Character(c) => match c.as_str() { + "1" => KeyCode::Key1, + "2" => KeyCode::Key2, + "3" => KeyCode::Key3, + "4" => KeyCode::Key4, + "5" => KeyCode::Key5, + "6" => KeyCode::Key6, + "7" => KeyCode::Key7, + "8" => KeyCode::Key8, + "9" => KeyCode::Key9, + "0" => KeyCode::Key0, + "A" => KeyCode::A, + "B" => KeyCode::B, + "C" => KeyCode::C, + "D" => KeyCode::D, + "E" => KeyCode::E, + "F" => KeyCode::F, + "G" => KeyCode::G, + "H" => KeyCode::H, + "I" => KeyCode::I, + "J" => KeyCode::J, + "K" => KeyCode::K, + "L" => KeyCode::L, + "M" => KeyCode::M, + "N" => KeyCode::N, + "O" => KeyCode::O, + "P" => KeyCode::P, + "Q" => KeyCode::Q, + "R" => KeyCode::R, + "S" => KeyCode::S, + "T" => KeyCode::T, + "U" => KeyCode::U, + "V" => KeyCode::V, + "W" => KeyCode::W, + "X" => KeyCode::X, + "Y" => KeyCode::Y, + "Z" => KeyCode::Z, + _ => KeyCode::Unlabeled, + }, + winit::keyboard::Key::Named(named_key) => match named_key { + NamedKey::Escape => KeyCode::Escape, + NamedKey::F1 => KeyCode::F1, + NamedKey::F2 => KeyCode::F2, + NamedKey::F3 => KeyCode::F3, + NamedKey::F4 => KeyCode::F4, + NamedKey::F5 => KeyCode::F5, + NamedKey::F6 => KeyCode::F6, + NamedKey::F7 => KeyCode::F7, + NamedKey::F8 => KeyCode::F8, + NamedKey::F9 => KeyCode::F9, + NamedKey::F10 => KeyCode::F10, + NamedKey::F11 => KeyCode::F11, + NamedKey::F12 => KeyCode::F12, + NamedKey::F13 => KeyCode::F13, + NamedKey::F14 => KeyCode::F14, + NamedKey::F15 => KeyCode::F15, + NamedKey::F16 => KeyCode::F16, + NamedKey::F17 => KeyCode::F17, + NamedKey::F18 => KeyCode::F18, + NamedKey::F19 => KeyCode::F19, + NamedKey::F20 => KeyCode::F20, + NamedKey::F21 => KeyCode::F21, + NamedKey::F22 => KeyCode::F22, + NamedKey::F23 => KeyCode::F23, + NamedKey::F24 => KeyCode::F24, + NamedKey::PrintScreen => KeyCode::Snapshot, + NamedKey::ScrollLock => KeyCode::Scroll, + NamedKey::Pause => KeyCode::Pause, + NamedKey::Insert => KeyCode::Insert, + NamedKey::Home => KeyCode::Home, + NamedKey::Delete => KeyCode::Delete, + NamedKey::End => KeyCode::End, + NamedKey::PageDown => KeyCode::PageDown, + NamedKey::PageUp => KeyCode::PageUp, + NamedKey::ArrowLeft => KeyCode::Left, + NamedKey::ArrowUp => KeyCode::Up, + NamedKey::ArrowRight => KeyCode::Right, + NamedKey::ArrowDown => KeyCode::Down, + NamedKey::Backspace => KeyCode::Backspace, + NamedKey::Enter => KeyCode::Enter, + NamedKey::Space => KeyCode::Space, + NamedKey::Compose => KeyCode::Compose, + NamedKey::NumLock => KeyCode::Numlock, + NamedKey::AppSwitch => KeyCode::Apps, + NamedKey::Convert => KeyCode::Convert, + NamedKey::LaunchMail => KeyCode::Mail, + NamedKey::MediaApps => KeyCode::MediaSelect, + NamedKey::MediaStop => KeyCode::MediaStop, + NamedKey::AudioVolumeMute => KeyCode::Mute, + NamedKey::MediaStepForward => KeyCode::NavigateForward, + NamedKey::MediaStepBackward => KeyCode::NavigateBackward, + NamedKey::MediaSkipForward => KeyCode::NextTrack, + NamedKey::NonConvert => KeyCode::NoConvert, + NamedKey::MediaPlayPause => KeyCode::PlayPause, + NamedKey::Power => KeyCode::Power, + NamedKey::MediaSkipBackward => KeyCode::PrevTrack, + NamedKey::PowerOff => KeyCode::Sleep, + NamedKey::Tab => KeyCode::Tab, + NamedKey::AudioVolumeDown => KeyCode::VolumeDown, + NamedKey::AudioVolumeUp => KeyCode::VolumeUp, + NamedKey::WakeUp => KeyCode::Wake, + NamedKey::BrowserBack => KeyCode::WebBack, + NamedKey::BrowserFavorites => KeyCode::WebFavorites, + NamedKey::BrowserForward => KeyCode::WebForward, + NamedKey::BrowserHome => KeyCode::WebHome, + NamedKey::BrowserRefresh => KeyCode::WebRefresh, + NamedKey::BrowserSearch => KeyCode::WebSearch, + NamedKey::BrowserStop => KeyCode::WebStop, + NamedKey::Copy => KeyCode::Copy, + NamedKey::Paste => KeyCode::Paste, + NamedKey::Cut => KeyCode::Cut, + _ => KeyCode::Unlabeled, + }, + _ => KeyCode::Unlabeled, } } @@ -655,13 +595,3 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } - -// As defined in: http://www.unicode.org/faq/private_use.html -pub(crate) fn is_private_use_character(c: char) -> bool { - matches!( - c, - '\u{E000}'..='\u{F8FF}' - | '\u{F0000}'..='\u{FFFFD}' - | '\u{100000}'..='\u{10FFFD}' - ) -} diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 84651d40..16b41e7d 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -118,7 +118,10 @@ where let mut debug = Debug::new(); debug.startup_started(); - let event_loop = EventLoopBuilder::with_user_event().build(); + let event_loop = EventLoopBuilder::with_user_event() + .build() + .expect("Create event loop"); + let proxy = event_loop.create_proxy(); let runtime = { @@ -210,78 +213,64 @@ where let mut context = task::Context::from_waker(task::noop_waker_ref()); - platform::run(event_loop, move |event, window_target, control_flow| { - use winit::event_loop::ControlFlow; - - if let ControlFlow::ExitWithCode(_) = control_flow { + let _ = event_loop.run(move |event, event_loop| { + if event_loop.exiting() { return; } - let event = match event { - winit::event::Event::WindowEvent { - event: - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - }, - window_id, - } => Some(winit::event::Event::WindowEvent { - event: winit::event::WindowEvent::Resized(*new_inner_size), - window_id, - }), - _ => event.to_static(), - }; + event_sender + .start_send(Event::EventLoopAwakened(event)) + .expect("Send event"); - if let Some(event) = event { - event_sender - .start_send(Event::EventLoopAwakened(event)) - .expect("Send event"); + loop { + let poll = instance.as_mut().poll(&mut context); - loop { - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => match control_receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - *control_flow = flow; - } - Control::CreateWindow { - id, - settings, - title, - monitor, - } => { - let exit_on_close_request = - settings.exit_on_close_request; - - let window = conversion::window_settings( - settings, &title, monitor, None, - ) - .build(window_target) - .expect("Failed to build window"); - - event_sender - .start_send(Event::WindowCreated { - id, - window, - exit_on_close_request, - }) - .expect("Send event"); - } - }, - _ => { - break; + match poll { + task::Poll::Pending => match control_receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + event_loop.set_control_flow(flow); + } + Control::CreateWindow { + id, + settings, + title, + monitor, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let window = conversion::window_settings( + settings, &title, monitor, None, + ) + .build(event_loop) + .expect("Failed to build window"); + + event_sender + .start_send(Event::WindowCreated { + id, + window, + exit_on_close_request, + }) + .expect("Send event"); + } + Control::Exit => { + event_loop.exit(); } }, - task::Poll::Ready(_) => { - *control_flow = ControlFlow::Exit; + _ => { break; } - }; - } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; } - }) + }); + + Ok(()) } enum Event<Message: 'static> { @@ -290,11 +279,12 @@ enum Event<Message: 'static> { window: winit::window::Window, exit_on_close_request: bool, }, - EventLoopAwakened(winit::event::Event<'static, Message>), + EventLoopAwakened(winit::event::Event<Message>), } enum Control { ChangeFlow(winit::event_loop::ControlFlow), + Exit, CreateWindow { id: window::Id, settings: window::Settings, @@ -427,184 +417,6 @@ async fn run_instance<A, E, C>( | event::StartCause::ResumeTimeReached { .. } ); } - event::Event::MainEventsCleared => { - debug.event_processing_started(); - let mut uis_stale = false; - - for (id, window) in window_manager.iter_mut() { - let mut window_events = vec![]; - - events.retain(|(window_id, event)| { - if *window_id == Some(id) || window_id.is_none() - { - window_events.push(event.clone()); - false - } else { - true - } - }); - - if !redraw_pending - && window_events.is_empty() - && messages.is_empty() - { - continue; - } - - let (ui_state, statuses) = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .update( - &window_events, - window.state.cursor(), - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - if !uis_stale { - uis_stale = matches!( - ui_state, - user_interface::State::Outdated - ); - } - - for (event, status) in window_events - .into_iter() - .zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } - } - - debug.event_processing_finished(); - - // TODO mw application update returns which window IDs to update - if !messages.is_empty() || uis_stale { - let mut cached_interfaces: HashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); - - // Update application - update( - &mut application, - &mut compositor, - &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, - &mut debug, - &mut messages, - &mut window_manager, - &mut cached_interfaces, - ); - - // we must synchronize all window states with application state after an - // application update since we don't know what changed - for (id, window) in window_manager.iter_mut() { - window.state.synchronize( - &application, - id, - &window.raw, - ); - } - - // rebuild UIs with the synchronized states - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &application, - &mut debug, - &mut window_manager, - cached_interfaces, - )); - } - - debug.draw_started(); - - for (id, window) in window_manager.iter_mut() { - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = core::Event::Window( - id, - window::Event::RedrawRequested(Instant::now()), - ); - - let cursor = window.state.cursor(); - - let ui = user_interfaces - .get_mut(&id) - .expect("Get user interface"); - - let (ui_state, _) = ui.update( - &[redraw_event.clone()], - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - let new_mouse_interaction = { - let state = &window.state; - - ui.draw( - &mut window.renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - cursor, - ) - }; - - if new_mouse_interaction != window.mouse_interaction - { - window.raw.set_cursor_icon( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = - new_mouse_interaction; - } - - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - window.raw.request_redraw(); - - runtime.broadcast( - redraw_event.clone(), - core::event::Status::Ignored, - ); - - let _ = control_sender.start_send( - Control::ChangeFlow(match ui_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - ControlFlow::Poll - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }), - ); - } - - redraw_pending = false; - - debug.draw_finished(); - } event::Event::PlatformSpecific( event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), @@ -624,7 +436,11 @@ async fn run_instance<A, E, C>( event::Event::UserEvent(message) => { messages.push(message); } - event::Event::RedrawRequested(id) => { + event::Event::WindowEvent { + window_id: id, + event: event::WindowEvent::RedrawRequested, + .. + } => { let Some((id, window)) = window_manager.get_mut_alias(id) else { @@ -775,6 +591,163 @@ async fn run_instance<A, E, C>( } } } + + debug.event_processing_started(); + let mut uis_stale = false; + + for (id, window) in window_manager.iter_mut() { + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == Some(id) || window_id.is_none() { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if !redraw_pending + && window_events.is_empty() + && messages.is_empty() + { + continue; + } + + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + if !uis_stale { + uis_stale = matches!(ui_state, user_interface::State::Outdated); + } + + for (event, status) in + window_events.into_iter().zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + } + + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let mut cached_interfaces: HashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut messages, + &mut window_manager, + &mut cached_interfaces, + ); + + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (id, window) in window_manager.iter_mut() { + window.state.synchronize(&application, id, &window.raw); + } + + // rebuild UIs with the synchronized states + user_interfaces = ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut window_manager, + cached_interfaces, + )); + } + + debug.draw_started(); + + for (id, window) in window_manager.iter_mut() { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces.get_mut(&id).expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + let new_mouse_interaction = { + let state = &window.state; + + ui.draw( + &mut window.renderer, + state.theme(), + &renderer::Style { + text_color: state.text_color(), + }, + cursor, + ) + }; + + if new_mouse_interaction != window.mouse_interaction { + window.raw.set_cursor_icon(conversion::mouse_interaction( + new_mouse_interaction, + )); + + window.mouse_interaction = new_mouse_interaction; + } + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + + runtime + .broadcast(redraw_event.clone(), core::event::Status::Ignored); + + let _ = control_sender.start_send(Control::ChangeFlow( + match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => ControlFlow::Poll, + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }, + )); + } + + redraw_pending = false; + + debug.draw_finished(); } let _ = ManuallyDrop::into_inner(user_interfaces); @@ -901,16 +874,12 @@ fn run_command<A, C, E>( .expect("Send control action"); } window::Action::Close(id) => { - use winit::event_loop::ControlFlow; - let _ = window_manager.remove(id); let _ = ui_caches.remove(&id); if window_manager.is_empty() { control_sender - .start_send(Control::ChangeFlow( - ControlFlow::ExitWithCode(0), - )) + .start_send(Control::Exit) .expect("Send control action"); } } @@ -921,10 +890,12 @@ fn run_command<A, C, E>( } window::Action::Resize(id, size) => { if let Some(window) = window_manager.get_mut(id) { - window.raw.set_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); } } window::Action::FetchSize(id, callback) => { @@ -1153,60 +1124,20 @@ where /// Returns true if the provided event should cause an [`Application`] to /// exit. pub fn user_force_quit( - event: &winit::event::WindowEvent<'_>, - _modifiers: winit::event::ModifiersState, + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, ) -> bool { match event { #[cfg(target_os = "macos")] winit::event::WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::Q), + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), state: winit::event::ElementState::Pressed, .. }, .. - } if _modifiers.logo() => true, + } if c == "q" && _modifiers.super_key() => true, _ => false, } } - -#[cfg(not(target_arch = "wasm32"))] -mod platform { - pub fn run<T, F>( - mut event_loop: winit::event_loop::EventLoop<T>, - event_handler: F, - ) -> Result<(), super::Error> - where - F: 'static - + FnMut( - winit::event::Event<'_, T>, - &winit::event_loop::EventLoopWindowTarget<T>, - &mut winit::event_loop::ControlFlow, - ), - { - use winit::platform::run_return::EventLoopExtRunReturn; - - let _ = event_loop.run_return(event_handler); - - Ok(()) - } -} - -#[cfg(target_arch = "wasm32")] -mod platform { - pub fn run<T, F>( - event_loop: winit::event_loop::EventLoop<T>, - event_handler: F, - ) -> ! - where - F: 'static - + FnMut( - winit::event::Event<'_, T>, - &winit::event_loop::EventLoopWindowTarget<T>, - &mut winit::event_loop::ControlFlow, - ), - { - event_loop.run(event_handler) - } -} diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 03da5ad7..235771f4 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -21,7 +21,7 @@ where viewport: Viewport, viewport_version: u64, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::event::ModifiersState, + modifiers: winit::keyboard::ModifiersState, theme: <A::Renderer as core::Renderer>::Theme, appearance: application::Appearance, } @@ -72,7 +72,7 @@ where viewport, viewport_version: 0, cursor_position: None, - modifiers: winit::event::ModifiersState::default(), + modifiers: winit::keyboard::ModifiersState::default(), theme, appearance, } @@ -119,7 +119,7 @@ where } /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> winit::event::ModifiersState { + pub fn modifiers(&self) -> winit::keyboard::ModifiersState { self.modifiers } @@ -142,7 +142,7 @@ where pub fn update( &mut self, window: &Window, - event: &WindowEvent<'_>, + event: &WindowEvent, _debug: &mut crate::runtime::Debug, ) { match event { @@ -158,10 +158,9 @@ where } WindowEvent::ScaleFactorChanged { scale_factor: new_scale_factor, - new_inner_size, + .. } => { - let size = - Size::new(new_inner_size.width, new_inner_size.height); + let size = self.viewport.physical_size(); self.viewport = Viewport::with_physical_size( size, @@ -180,13 +179,16 @@ where self.cursor_position = None; } WindowEvent::ModifiersChanged(new_modifiers) => { - self.modifiers = *new_modifiers; + self.modifiers = new_modifiers.state(); } #[cfg(feature = "debug")] WindowEvent::KeyboardInput { - input: - winit::event::KeyboardInput { - virtual_keycode: Some(winit::event::VirtualKeyCode::F12), + event: + winit::event::KeyEvent { + logical_key: + winit::keyboard::Key::Named( + winit::keyboard::NamedKey::F12, + ), state: winit::event::ElementState::Pressed, .. }, -- cgit From 36073de24eecffc4644da9ab367aef15d162df86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 15 Dec 2023 13:27:58 +0100 Subject: Fix `key_code` conversion for character keys --- winit/src/conversion.rs | 52 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index ecc34320..2e382c39 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -465,32 +465,32 @@ pub fn key_code(key: &winit::keyboard::Key) -> keyboard::KeyCode { "8" => KeyCode::Key8, "9" => KeyCode::Key9, "0" => KeyCode::Key0, - "A" => KeyCode::A, - "B" => KeyCode::B, - "C" => KeyCode::C, - "D" => KeyCode::D, - "E" => KeyCode::E, - "F" => KeyCode::F, - "G" => KeyCode::G, - "H" => KeyCode::H, - "I" => KeyCode::I, - "J" => KeyCode::J, - "K" => KeyCode::K, - "L" => KeyCode::L, - "M" => KeyCode::M, - "N" => KeyCode::N, - "O" => KeyCode::O, - "P" => KeyCode::P, - "Q" => KeyCode::Q, - "R" => KeyCode::R, - "S" => KeyCode::S, - "T" => KeyCode::T, - "U" => KeyCode::U, - "V" => KeyCode::V, - "W" => KeyCode::W, - "X" => KeyCode::X, - "Y" => KeyCode::Y, - "Z" => KeyCode::Z, + "a" => KeyCode::A, + "b" => KeyCode::B, + "c" => KeyCode::C, + "d" => KeyCode::D, + "e" => KeyCode::E, + "f" => KeyCode::F, + "g" => KeyCode::G, + "h" => KeyCode::H, + "i" => KeyCode::I, + "j" => KeyCode::J, + "k" => KeyCode::K, + "l" => KeyCode::L, + "m" => KeyCode::M, + "n" => KeyCode::N, + "o" => KeyCode::O, + "p" => KeyCode::P, + "q" => KeyCode::Q, + "r" => KeyCode::R, + "s" => KeyCode::S, + "t" => KeyCode::T, + "u" => KeyCode::U, + "v" => KeyCode::V, + "w" => KeyCode::W, + "x" => KeyCode::X, + "y" => KeyCode::Y, + "z" => KeyCode::Z, _ => KeyCode::Unlabeled, }, winit::keyboard::Key::Named(named_key) => match named_key { -- cgit From 2f11102ecc30e6d4a96c4802d68f87c002a9a3f2 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 15 Dec 2023 13:46:26 +0100 Subject: Enable `rwh_06` feature for `winit` dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a78d0f8f..7e70e1e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,4 +160,4 @@ web-time = "0.2" wgpu = "0.18" winapi = "0.3" window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "3bcdb9abcd7459978ec689523bc21943d38da0f9", default-features = false, features = ["rwh_05", "x11", "wayland"] } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "3bcdb9abcd7459978ec689523bc21943d38da0f9", features = ["rwh_05"] } -- cgit From d7dd0338616d13d94689d153f6c0dedfba1ad4ba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 15 Dec 2023 13:49:53 +0100 Subject: Ignore `raw-window-handle` outdated artifact --- .github/workflows/audit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 80bbcacd..57169796 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -27,4 +27,4 @@ jobs: - name: Delete `web-sys` dependency from `integration` example run: sed -i '$d' examples/integration/Cargo.toml - name: Find outdated dependencies - run: cargo outdated --workspace --exit-code 1 + run: cargo outdated --workspace --exit-code 1 --ignore raw-window-handle -- cgit From 1481f0c0a44a2f01c06740143a1e5e47612f77f8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 15 Dec 2023 14:03:38 +0100 Subject: Use latest `raw-window-handle` in `iced_core` --- core/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index c95477c4..4baf80a9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,7 +22,8 @@ palette.workspace = true palette.optional = true [target.'cfg(windows)'.dependencies] -raw-window-handle.workspace = true +# TODO: Use `workspace` dependency once `wgpu` upgrades `raw-window-handle` +raw-window-handle = "0.6" [dev-dependencies] approx = "0.5" -- cgit From 5961030c05294b2218baf3d956eff39d94485daf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 15 Dec 2023 14:10:33 +0100 Subject: Remove `webgl` feature in `pokedex` example --- examples/pokedex/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml index 4a55f943..bf7e1e35 100644 --- a/examples/pokedex/Cargo.toml +++ b/examples/pokedex/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["image", "debug", "tokio", "webgl"] +iced.features = ["image", "debug", "tokio"] serde_json = "1.0" -- cgit From 48cebbb22cfd701984017f1f3336735bc70272d3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Dec 2023 12:37:04 +0100 Subject: Fix redraw request handling in new event loop logic --- winit/src/application.rs | 61 +++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index ed6ba9eb..aea828bc 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -338,6 +338,38 @@ async fn run_instance<A, E, C>( event: event::WindowEvent::RedrawRequested { .. }, .. } => { + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = Event::Window( + window::Id::MAIN, + window::Event::RedrawRequested(Instant::now()), + ); + + let (interface_state, _) = user_interface.update( + &[redraw_event.clone()], + state.cursor(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + + let _ = control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => ControlFlow::Poll, + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }); + + runtime.broadcast(redraw_event, core::event::Status::Ignored); + debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -514,36 +546,7 @@ async fn run_instance<A, E, C>( } } - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = Event::Window( - window::Id::MAIN, - window::Event::RedrawRequested(Instant::now()), - ); - - let (interface_state, _) = user_interface.update( - &[redraw_event.clone()], - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - window.request_redraw(); - runtime.broadcast(redraw_event, core::event::Status::Ignored); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => ControlFlow::Poll, - window::RedrawRequest::At(at) => ControlFlow::WaitUntil(at), - }, - _ => ControlFlow::Wait, - }); redraw_pending = false; } -- cgit From af917a08d8c60f1684439989f63f856d445d0383 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Dec 2023 12:44:08 +0100 Subject: Fix request redraw event handling for multi-window apps --- winit/src/application.rs | 2 +- winit/src/multi_window.rs | 144 +++++++++++++++++++++++----------------------- 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index aea828bc..34bf7f55 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -417,6 +417,7 @@ async fn run_instance<A, E, C>( }, state.cursor(), ); + debug.draw_finished(); if new_mouse_interaction != mouse_interaction { window.set_cursor_icon(conversion::mouse_interaction( @@ -425,7 +426,6 @@ async fn run_instance<A, E, C>( mouse_interaction = new_mouse_interaction; } - debug.draw_finished(); compositor.configure_surface( &mut surface, diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 16b41e7d..0ba51387 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -447,6 +447,72 @@ async fn run_instance<A, E, C>( continue; }; + // TODO: Avoid redrawing all the time by forcing widgets to + // request redraws on state changes + // + // Then, we can use the `interface_state` here to decide if a redraw + // is needed right away, or simply wait until a specific time. + let redraw_event = core::Event::Window( + id, + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + debug.draw_started(); + let new_mouse_interaction = ui.draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + cursor, + ); + debug.draw_finished(); + + if new_mouse_interaction != window.mouse_interaction { + window.raw.set_cursor_icon( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = new_mouse_interaction; + } + + runtime.broadcast( + redraw_event.clone(), + core::event::Status::Ignored, + ); + + let _ = control_sender.start_send(Control::ChangeFlow( + match ui_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }, + )); + let physical_size = window.state.physical_size(); if physical_size.width == 0 || physical_size.height == 0 @@ -454,14 +520,12 @@ async fn run_instance<A, E, C>( continue; } - debug.render_started(); if window.viewport_version != window.state.viewport_version() { let logical_size = window.state.logical_size(); debug.layout_started(); - let ui = user_interfaces .remove(&id) .expect("Remove user interface"); @@ -470,7 +534,6 @@ async fn run_instance<A, E, C>( id, ui.relayout(logical_size, &mut window.renderer), ); - debug.layout_finished(); debug.draw_started(); @@ -485,6 +548,7 @@ async fn run_instance<A, E, C>( }, window.state.cursor(), ); + debug.draw_finished(); if new_mouse_interaction != window.mouse_interaction { @@ -497,7 +561,6 @@ async fn run_instance<A, E, C>( window.mouse_interaction = new_mouse_interaction; } - debug.draw_finished(); compositor.configure_surface( &mut window.surface, @@ -509,6 +572,7 @@ async fn run_instance<A, E, C>( window.state.viewport_version(); } + debug.render_started(); match compositor.present( &mut window.renderer, &mut window.surface, @@ -529,9 +593,11 @@ async fn run_instance<A, E, C>( } _ => { debug.render_finished(); + log::error!( - "Error {error:?} when presenting surface." - ); + "Error {error:?} when \ + presenting surface." + ); // Try rendering all windows again next frame. for (_id, window) in @@ -677,77 +743,13 @@ async fn run_instance<A, E, C>( )); } - debug.draw_started(); - - for (id, window) in window_manager.iter_mut() { - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = core::Event::Window( - id, - window::Event::RedrawRequested(Instant::now()), - ); - - let cursor = window.state.cursor(); - - let ui = user_interfaces.get_mut(&id).expect("Get user interface"); - - let (ui_state, _) = ui.update( - &[redraw_event.clone()], - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - let new_mouse_interaction = { - let state = &window.state; - - ui.draw( - &mut window.renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - cursor, - ) - }; - - if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - - window.mouse_interaction = new_mouse_interaction; - } - + for (_id, window) in window_manager.iter_mut() { // TODO once widgets can request to be redrawn, we can avoid always requesting a // redraw window.raw.request_redraw(); - - runtime - .broadcast(redraw_event.clone(), core::event::Status::Ignored); - - let _ = control_sender.start_send(Control::ChangeFlow( - match ui_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => ControlFlow::Poll, - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }, - )); } redraw_pending = false; - - debug.draw_finished(); } let _ = ManuallyDrop::into_inner(user_interfaces); -- cgit From 58494bd0331b01194fd704319828849d4ed4d270 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Dec 2023 12:51:32 +0100 Subject: Pin `nightly` toolchain to a specific day in `document` workflow --- .github/workflows/document.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 62e28ca3..35bf10f4 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: hecrj/setup-rust-action@v1 with: - rust-version: nightly + rust-version: nightly-2023-12-11 - uses: actions/checkout@v2 - name: Generate documentation run: | -- cgit From e772e5a9e90b5d8ae12a9891cb7b848d81e63239 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 19 Dec 2023 14:54:27 +0100 Subject: Avoid duplicated `UserInterface::draw` calls in `RedrawRequested` --- winit/src/application.rs | 95 ++++++++++++++++++++---------------------------- 1 file changed, 39 insertions(+), 56 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 34bf7f55..75be08f1 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -312,13 +312,13 @@ async fn run_instance<A, E, C>( while let Some(event) = event_receiver.next().await { match event { - event::Event::NewEvents(start_cause) => { - redraw_pending = matches!( - start_cause, - event::StartCause::Init - | event::StartCause::Poll - | event::StartCause::ResumeTimeReached { .. } - ); + event::Event::NewEvents( + event::StartCause::Init + | event::StartCause::Poll + | event::StartCause::ResumeTimeReached { .. }, + ) if !redraw_pending => { + window.request_redraw(); + redraw_pending = true; } event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( event::MacOS::ReceivedUrl(url), @@ -338,6 +338,33 @@ async fn run_instance<A, E, C>( event: event::WindowEvent::RedrawRequested { .. }, .. } => { + let physical_size = state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 { + continue; + } + + let current_viewport_version = state.viewport_version(); + + if viewport_version != current_viewport_version { + let logical_size = state.logical_size(); + + debug.layout_started(); + user_interface = ManuallyDrop::new( + ManuallyDrop::into_inner(user_interface) + .relayout(logical_size, &mut renderer), + ); + debug.layout_finished(); + + compositor.configure_surface( + &mut surface, + physical_size.width, + physical_size.height, + ); + + viewport_version = current_viewport_version; + } + // TODO: Avoid redrawing all the time by forcing widgets to // request redraws on state changes // @@ -379,6 +406,7 @@ async fn run_instance<A, E, C>( }, state.cursor(), ); + redraw_pending = false; debug.draw_finished(); if new_mouse_interaction != mouse_interaction { @@ -389,53 +417,7 @@ async fn run_instance<A, E, C>( mouse_interaction = new_mouse_interaction; } - let physical_size = state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 { - continue; - } - debug.render_started(); - let current_viewport_version = state.viewport_version(); - - if viewport_version != current_viewport_version { - let logical_size = state.logical_size(); - - debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - debug.layout_finished(); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor(), - ); - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor_icon(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); - - viewport_version = current_viewport_version; - } - match compositor.present( &mut renderer, &mut surface, @@ -546,9 +528,10 @@ async fn run_instance<A, E, C>( } } - window.request_redraw(); - - redraw_pending = false; + if !redraw_pending { + window.request_redraw(); + redraw_pending = true; + } } // Manually drop the user interface -- cgit From 50a7852cb857cd110077ffce492bafe9ebe8786c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 20 Dec 2023 08:56:57 +0100 Subject: Stop polling in event loop on `RedrawRequest::NextFrame` --- examples/loading_spinners/src/circular.rs | 6 +----- examples/loading_spinners/src/linear.rs | 6 +----- winit/src/application.rs | 9 ++++++--- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index dca8046a..7996f970 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -275,8 +275,6 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - const FRAME_RATE: u64 = 60; - let state = tree.state.downcast_mut::<State>(); if let Event::Window(_, window::Event::RedrawRequested(now)) = event { @@ -287,9 +285,7 @@ where ); state.cache.clear(); - shell.request_redraw(RedrawRequest::At( - now + Duration::from_millis(1000 / FRAME_RATE), - )); + shell.request_redraw(RedrawRequest::NextFrame); } event::Status::Ignored diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index db10bfba..becfd2c2 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -196,16 +196,12 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - const FRAME_RATE: u64 = 60; - let state = tree.state.downcast_mut::<State>(); if let Event::Window(_, window::Event::RedrawRequested(now)) = event { *state = state.timed_transition(self.cycle_duration, now); - shell.request_redraw(RedrawRequest::At( - now + Duration::from_millis(1000 / FRAME_RATE), - )); + shell.request_redraw(RedrawRequest::NextFrame); } event::Status::Ignored diff --git a/winit/src/application.rs b/winit/src/application.rs index 75be08f1..5ff76060 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -314,7 +314,6 @@ async fn run_instance<A, E, C>( match event { event::Event::NewEvents( event::StartCause::Init - | event::StartCause::Poll | event::StartCause::ResumeTimeReached { .. }, ) if !redraw_pending => { window.request_redraw(); @@ -387,7 +386,11 @@ async fn run_instance<A, E, C>( user_interface::State::Updated { redraw_request: Some(redraw_request), } => match redraw_request { - window::RedrawRequest::NextFrame => ControlFlow::Poll, + window::RedrawRequest::NextFrame => { + window.request_redraw(); + + ControlFlow::Wait + } window::RedrawRequest::At(at) => { ControlFlow::WaitUntil(at) } @@ -469,7 +472,7 @@ async fn run_instance<A, E, C>( _ => {} } - if !redraw_pending && events.is_empty() && messages.is_empty() { + if events.is_empty() && messages.is_empty() { continue; } -- cgit From 031784e274b0a65dc67004e503b89d29fe0e36ea Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 20 Dec 2023 10:00:27 +0100 Subject: Batch events for processing in `iced_winit` event loop --- winit/src/application.rs | 123 ++++++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 5ff76060..7f5a3620 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -469,71 +469,74 @@ async fn run_instance<A, E, C>( events.push(event); } } - _ => {} - } + event::Event::AboutToWait => { + if events.is_empty() && messages.is_empty() { + continue; + } - if events.is_empty() && messages.is_empty() { - continue; - } + debug.event_processing_started(); - debug.event_processing_started(); + let (interface_state, statuses) = user_interface.update( + &events, + state.cursor(), + &mut renderer, + &mut clipboard, + &mut messages, + ); - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); + debug.event_processing_finished(); - debug.event_processing_finished(); + for (event, status) in + events.drain(..).zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } - for (event, status) in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event, status); - } + if !messages.is_empty() + || matches!( + interface_state, + user_interface::State::Outdated + ) + { + let mut cache = + ManuallyDrop::into_inner(user_interface).into_cache(); - if !messages.is_empty() - || matches!(interface_state, user_interface::State::Outdated) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - update( - &mut application, - &mut compositor, - &mut surface, - &mut cache, - &state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &mut messages, - &window, - ); - - // Update window - state.synchronize(&application, &window); - - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } - } + // Update application + update( + &mut application, + &mut compositor, + &mut surface, + &mut cache, + &mut state, + &mut renderer, + &mut runtime, + &mut clipboard, + &mut should_exit, + &mut proxy, + &mut debug, + &mut messages, + &window, + ); + + user_interface = ManuallyDrop::new(build_user_interface( + &application, + cache, + &mut renderer, + state.logical_size(), + &mut debug, + )); + + if should_exit { + break; + } + } - if !redraw_pending { - window.request_redraw(); - redraw_pending = true; + if !redraw_pending { + window.request_redraw(); + redraw_pending = true; + } + } + _ => {} } } @@ -595,7 +598,7 @@ pub fn update<A: Application, C, E: Executor>( compositor: &mut C, surface: &mut C::Surface, cache: &mut user_interface::Cache, - state: &State<A>, + state: &mut State<A>, renderer: &mut A::Renderer, runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, clipboard: &mut Clipboard, @@ -632,6 +635,8 @@ pub fn update<A: Application, C, E: Executor>( ); } + state.synchronize(application, window); + let subscription = application.subscription(); runtime.track(subscription.into_recipes()); } -- cgit From 9bbf7822e9eae4c7d0b41c2eea14e261119b1d23 Mon Sep 17 00:00:00 2001 From: Giuliano Bellini s294739 <s294739@studenti.polito.it> Date: Sat, 23 Dec 2023 00:17:10 +0100 Subject: added text::Shaping to Tooltip --- widget/src/tooltip.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 9e102c56..b888980a 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -64,6 +64,12 @@ where self } + /// Sets the [`text::Shaping`] strategy of the [`Tooltip`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.tooltip = self.tooltip.shaping(shaping); + self + } + /// Sets the font of the [`Tooltip`]. /// /// [`Font`]: Renderer::Font -- cgit From 2776d4634802d9bd7ca92e4ee7d86296bd966496 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 4 Jan 2024 05:12:38 +0100 Subject: Update `winit` fork to `0.29.8` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7e70e1e8..45d69288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,4 +160,4 @@ web-time = "0.2" wgpu = "0.18" winapi = "0.3" window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "3bcdb9abcd7459978ec689523bc21943d38da0f9", features = ["rwh_05"] } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "25b5dc1758723699015c37b0a64f16ceb9c546ea", features = ["rwh_05"] } -- cgit From 0655a20ad119e2e9790afcc45039fd4ac0e7d432 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 16 Mar 2023 20:23:25 +0100 Subject: Make `Shrink` have priority over `Fill` in layout --- core/src/layout.rs | 12 ++--- core/src/layout/flex.rs | 29 +++++++---- core/src/layout/limits.rs | 83 +++++++++++++------------------ core/src/layout/node.rs | 29 ++++++++++- core/src/padding.rs | 6 +++ core/src/size.rs | 24 +++++---- core/src/widget/text.rs | 2 +- examples/game_of_life/src/main.rs | 4 +- examples/geometry/src/main.rs | 2 +- examples/integration/src/controls.rs | 39 ++++++--------- examples/loading_spinners/src/circular.rs | 2 +- examples/loading_spinners/src/linear.rs | 2 +- examples/modal/src/main.rs | 13 ++--- examples/pane_grid/src/main.rs | 1 - examples/pick_list/src/main.rs | 1 - examples/scrollable/src/main.rs | 29 ++++------- examples/sierpinski_triangle/src/main.rs | 2 - examples/styling/src/main.rs | 9 ++-- examples/svg/src/main.rs | 1 - examples/toast/src/main.rs | 15 ++---- examples/tour/src/main.rs | 6 +-- examples/websocket/src/main.rs | 2 - widget/src/button.rs | 13 +++-- widget/src/canvas.rs | 3 +- widget/src/column.rs | 9 ++-- widget/src/container.rs | 33 ++++++------ widget/src/image.rs | 2 +- widget/src/image/viewer.rs | 9 ++-- widget/src/keyed/column.rs | 2 + widget/src/overlay/menu.rs | 12 ++--- widget/src/pane_grid.rs | 9 ++-- widget/src/pane_grid/content.rs | 9 ++-- widget/src/pane_grid/title_bar.rs | 18 +++---- widget/src/pick_list.rs | 7 ++- widget/src/progress_bar.rs | 10 ++-- widget/src/row.rs | 6 +-- widget/src/rule.rs | 4 +- widget/src/scrollable.rs | 2 +- widget/src/shader.rs | 2 +- widget/src/slider.rs | 3 +- widget/src/space.rs | 4 +- widget/src/svg.rs | 5 +- widget/src/text_editor.rs | 2 +- widget/src/text_input.rs | 57 ++++++++++----------- widget/src/tooltip.rs | 2 +- widget/src/vertical_slider.rs | 3 +- 46 files changed, 265 insertions(+), 274 deletions(-) diff --git a/core/src/layout.rs b/core/src/layout.rs index caf315b6..277473fe 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -71,12 +71,12 @@ pub fn next_to_each_other( left: impl FnOnce(&Limits) -> Node, right: impl FnOnce(&Limits) -> Node, ) -> Node { - let mut left_node = left(limits); + let left_node = left(limits); let left_size = left_node.size(); let right_limits = limits.shrink(Size::new(left_size.width + spacing, 0.0)); - let mut right_node = right(&right_limits); + let right_node = right(&right_limits); let right_size = right_node.size(); let (left_y, right_y) = if left_size.height > right_size.height { @@ -85,14 +85,14 @@ pub fn next_to_each_other( ((right_size.height - left_size.height) / 2.0, 0.0) }; - left_node.move_to(Point::new(0.0, left_y)); - right_node.move_to(Point::new(left_size.width + spacing, right_y)); - Node::with_children( Size::new( left_size.width + spacing + right_size.width, left_size.height.max(right_size.height), ), - vec![left_node, right_node], + vec![ + left_node.move_to(Point::new(0.0, left_y)), + right_node.move_to(Point::new(left_size.width + spacing, right_y)), + ], ) } diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index c02b63d8..738dc81d 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -20,7 +20,7 @@ use crate::Element; use crate::layout::{Limits, Node}; use crate::widget; -use crate::{Alignment, Padding, Point, Size}; +use crate::{Alignment, Length, Padding, Point, Size}; /// The main axis of a flex layout. #[derive(Debug)] @@ -63,6 +63,8 @@ pub fn resolve<Message, Renderer>( axis: Axis, renderer: &Renderer, limits: &Limits, + width: Length, + height: Length, padding: Padding, spacing: f32, align_items: Alignment, @@ -72,12 +74,12 @@ pub fn resolve<Message, Renderer>( where Renderer: crate::Renderer, { - let limits = limits.pad(padding); + let limits = limits.width(width).height(height).shrink(padding); let total_spacing = spacing * items.len().saturating_sub(1) as f32; let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); + let mut cross = 0.0f32; let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); @@ -109,7 +111,16 @@ where } } - let remaining = available.max(0.0); + let remaining = match axis { + Axis::Horizontal => match width { + Length::Shrink => 0.0, + _ => available.max(0.0), + }, + Axis::Vertical => match height { + Length::Shrink => 0.0, + _ => available.max(0.0), + }, + }; for (i, (child, tree)) in items.iter().zip(trees).enumerate() { let fill_factor = match axis { @@ -154,18 +165,18 @@ where let (x, y) = axis.pack(main, pad.1); - node.move_to(Point::new(x, y)); + node.move_to_mut(Point::new(x, y)); match axis { Axis::Horizontal => { - node.align( + node.align_mut( Alignment::Start, align_items, Size::new(0.0, cross), ); } Axis::Vertical => { - node.align( + node.align_mut( align_items, Alignment::Start, Size::new(cross, 0.0), @@ -179,7 +190,7 @@ where } let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(Size::new(width, height)); + let size = limits.resolve(Size::new(width, height), width, height); - Node::with_children(size.pad(padding), nodes) + Node::with_children(size.expand(padding), nodes) } diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index 39a3d98b..eef4c4c9 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -1,12 +1,11 @@ #![allow(clippy::manual_clamp)] -use crate::{Length, Padding, Size}; +use crate::{Length, Size}; /// A set of size constraints for layouting. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Limits { min: Size, max: Size, - fill: Size, } impl Limits { @@ -14,16 +13,11 @@ impl Limits { pub const NONE: Limits = Limits { min: Size::ZERO, max: Size::INFINITY, - fill: Size::INFINITY, }; /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. pub const fn new(min: Size, max: Size) -> Limits { - Limits { - min, - max, - fill: Size::INFINITY, - } + Limits { min, max } } /// Returns the minimum [`Size`] of the [`Limits`]. @@ -36,26 +30,15 @@ impl Limits { self.max } - /// Returns the fill [`Size`] of the [`Limits`]. - pub fn fill(&self) -> Size { - self.fill - } - /// Applies a width constraint to the current [`Limits`]. pub fn width(mut self, width: impl Into<Length>) -> Limits { match width.into() { - Length::Shrink => { - self.fill.width = self.min.width; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.width = self.fill.width.min(self.max.width); - } + Length::Shrink | Length::Fill | Length::FillPortion(_) => {} Length::Fixed(amount) => { let new_width = amount.min(self.max.width).max(self.min.width); self.min.width = new_width; self.max.width = new_width; - self.fill.width = new_width; } } @@ -65,19 +48,13 @@ impl Limits { /// Applies a height constraint to the current [`Limits`]. pub fn height(mut self, height: impl Into<Length>) -> Limits { match height.into() { - Length::Shrink => { - self.fill.height = self.min.height; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.height = self.fill.height.min(self.max.height); - } + Length::Shrink | Length::Fill | Length::FillPortion(_) => {} Length::Fixed(amount) => { let new_height = amount.min(self.max.height).max(self.min.height); self.min.height = new_height; self.max.height = new_height; - self.fill.height = new_height; } } @@ -112,13 +89,10 @@ impl Limits { self } - /// Shrinks the current [`Limits`] to account for the given padding. - pub fn pad(&self, padding: Padding) -> Limits { - self.shrink(Size::new(padding.horizontal(), padding.vertical())) - } - /// Shrinks the current [`Limits`] by the given [`Size`]. - pub fn shrink(&self, size: Size) -> Limits { + pub fn shrink(&self, size: impl Into<Size>) -> Limits { + let size = size.into(); + let min = Size::new( (self.min().width - size.width).max(0.0), (self.min().height - size.height).max(0.0), @@ -129,12 +103,7 @@ impl Limits { (self.max().height - size.height).max(0.0), ); - let fill = Size::new( - (self.fill.width - size.width).max(0.0), - (self.fill.height - size.height).max(0.0), - ); - - Limits { min, max, fill } + Limits { min, max } } /// Removes the minimum width constraint for the current [`Limits`]. @@ -142,22 +111,38 @@ impl Limits { Limits { min: Size::ZERO, max: self.max, - fill: self.fill, } } /// Computes the resulting [`Size`] that fits the [`Limits`] given the /// intrinsic size of some content. - pub fn resolve(&self, intrinsic_size: Size) -> Size { - Size::new( - intrinsic_size - .width - .min(self.max.width) - .max(self.fill.width), - intrinsic_size + pub fn resolve( + &self, + intrinsic_size: Size, + width: impl Into<Length>, + height: impl Into<Length>, + ) -> Size { + let width = match width.into() { + Length::Fill | Length::FillPortion(_) => self.max.width, + Length::Fixed(amount) => { + amount.min(self.max.width).max(self.min.width) + } + Length::Shrink => { + intrinsic_size.width.min(self.max.width).max(self.min.width) + } + }; + + let height = match height.into() { + Length::Fill | Length::FillPortion(_) => self.max.height, + Length::Fixed(amount) => { + amount.min(self.max.height).max(self.min.height) + } + Length::Shrink => intrinsic_size .height .min(self.max.height) - .max(self.fill.height), - ) + .max(self.min.height), + }; + + Size::new(width, height) } } diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 2b44a7d5..00087431 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -1,4 +1,4 @@ -use crate::{Alignment, Point, Rectangle, Size, Vector}; +use crate::{Alignment, Padding, Point, Rectangle, Size, Vector}; /// The bounds of an element and its children. #[derive(Debug, Clone, Default)] @@ -26,6 +26,14 @@ impl Node { } } + /// Creates a new [`Node`] that wraps a single child with some [`Padding`]. + pub fn container(child: Self, padding: Padding) -> Self { + Self::with_children( + child.bounds.size().expand(padding), + vec![child.move_to(Point::new(padding.left, padding.top))], + ) + } + /// Returns the [`Size`] of the [`Node`]. pub fn size(&self) -> Size { Size::new(self.bounds.width, self.bounds.height) @@ -43,6 +51,17 @@ impl Node { /// Aligns the [`Node`] in the given space. pub fn align( + mut self, + horizontal_alignment: Alignment, + vertical_alignment: Alignment, + space: Size, + ) -> Self { + self.align_mut(horizontal_alignment, vertical_alignment, space); + self + } + + /// Mutable reference version of [`align`]. + pub fn align_mut( &mut self, horizontal_alignment: Alignment, vertical_alignment: Alignment, @@ -70,7 +89,13 @@ impl Node { } /// Moves the [`Node`] to the given position. - pub fn move_to(&mut self, position: Point) { + pub fn move_to(mut self, position: Point) -> Self { + self.move_to_mut(position); + self + } + + /// Mutable reference version of [`move_to`]. + pub fn move_to_mut(&mut self, position: Point) { self.bounds.x = position.x; self.bounds.y = position.y; } diff --git a/core/src/padding.rs b/core/src/padding.rs index 0b1bba13..a63f6e29 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -154,3 +154,9 @@ impl From<[f32; 4]> for Padding { } } } + +impl From<Padding> for Size { + fn from(padding: Padding) -> Self { + Self::new(padding.horizontal(), padding.vertical()) + } +} diff --git a/core/src/size.rs b/core/src/size.rs index 7ef2f602..90e50d13 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,4 +1,4 @@ -use crate::{Padding, Vector}; +use crate::Vector; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -26,15 +26,7 @@ impl Size { /// A [`Size`] with infinite width and height. pub const INFINITY: Size = Size::new(f32::INFINITY, f32::INFINITY); - /// Increments the [`Size`] to account for the given padding. - pub fn pad(&self, padding: Padding) -> Self { - Size { - width: self.width + padding.horizontal(), - height: self.height + padding.vertical(), - } - } - - /// Returns the minimum of each component of this size and another + /// Returns the minimum of each component of this size and another. pub fn min(self, other: Self) -> Self { Size { width: self.width.min(other.width), @@ -42,13 +34,23 @@ impl Size { } } - /// Returns the maximum of each component of this size and another + /// Returns the maximum of each component of this size and another. pub fn max(self, other: Self) -> Self { Size { width: self.width.max(other.width), height: self.height.max(other.height), } } + + /// Expands this [`Size`] by the given amount. + pub fn expand(self, other: impl Into<Size>) -> Self { + let other = other.into(); + + Size { + width: self.width + other.width, + height: self.height + other.height, + } + } } impl From<[f32; 2]> for Size { diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index e020b030..e47e4178 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -224,7 +224,7 @@ where shaping, }); - let size = limits.resolve(paragraph.min_bounds()); + let size = limits.resolve(paragraph.min_bounds(), width, height); layout::Node::new(size) } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 96840143..56f7afd5 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -146,7 +146,8 @@ impl Application for GameOfLife { .view() .map(move |message| Message::Grid(message, version)), controls, - ]; + ] + .height(Length::Fill); container(content) .width(Length::Fill) @@ -178,7 +179,6 @@ fn view_controls<'a>( slider(1.0..=1000.0, speed as f32, Message::SpeedChanged), text(format!("x{speed}")).size(16), ] - .width(Length::Fill) .align_items(Alignment::Center) .spacing(10); diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 8ab3b493..50227f1c 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -30,7 +30,7 @@ mod rainbow { _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.width(Length::Fill).resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, Length::Fill, Length::Shrink); layout::Node::new(Size::new(size.width, size.width)) } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 4714c397..89a595c1 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -81,32 +81,25 @@ impl Program for Controls { ); Row::new() - .width(Length::Fill) .height(Length::Fill) .align_items(Alignment::End) .push( - Column::new() - .width(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new() - .padding(10) - .spacing(10) - .push( - Text::new("Background color") - .style(Color::WHITE), - ) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ) - .push( - text_input("Placeholder", text) - .on_input(Message::TextChanged), - ), - ), + Column::new().align_items(Alignment::End).push( + Column::new() + .padding(10) + .spacing(10) + .push(Text::new("Background color").style(Color::WHITE)) + .push(sliders) + .push( + Text::new(format!("{background_color:?}")) + .size(14) + .style(Color::WHITE), + ) + .push( + text_input("Placeholder", text) + .on_input(Message::TextChanged), + ), + ), ) .into() } diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index dca8046a..a92a5dd1 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -259,7 +259,7 @@ where limits: &layout::Limits, ) -> layout::Node { let limits = limits.width(self.size).height(self.size); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, self.size, self.size); layout::Node::new(size) } diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index db10bfba..da4f1ea1 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -180,7 +180,7 @@ where limits: &layout::Limits, ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, self.width, self.height); layout::Node::new(size) } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index acb14372..85ccf8b4 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -420,17 +420,14 @@ mod modal { .width(Length::Fill) .height(Length::Fill); - let mut child = self + let child = self .content .as_widget() - .layout(self.tree, renderer, &limits); + .layout(self.tree, renderer, &limits) + .align(Alignment::Center, Alignment::Center, limits.max()); - child.align(Alignment::Center, Alignment::Center, limits.max()); - - let mut node = layout::Node::with_children(self.size, vec![child]); - node.move_to(position); - - node + layout::Node::with_children(self.size, vec![child]) + .move_to(position) } fn on_event( diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index aa3149bb..96bb8e4e 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -297,7 +297,6 @@ fn view_content<'a>( text(format!("{}x{}", size.width, size.height)).size(24), controls, ] - .width(Length::Fill) .spacing(10) .align_items(Alignment::Center); diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index 21200621..bfd642f5 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -48,7 +48,6 @@ impl Sandbox for Example { pick_list, vertical_space(600), ] - .width(Length::Fill) .align_items(Alignment::Center) .spacing(10); diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index d82ea841..1042e7a4 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -147,35 +147,30 @@ impl Application for ScrollableDemo { text("Scroller width:"), scroller_width_slider, ] - .spacing(10) - .width(Length::Fill); + .spacing(10); - let scroll_orientation_controls = column(vec![ - text("Scrollbar direction:").into(), + let scroll_orientation_controls = column![ + text("Scrollbar direction:"), radio( "Vertical", Direction::Vertical, Some(self.scrollable_direction), Message::SwitchDirection, - ) - .into(), + ), radio( "Horizontal", Direction::Horizontal, Some(self.scrollable_direction), Message::SwitchDirection, - ) - .into(), + ), radio( "Both!", Direction::Multi, Some(self.scrollable_direction), Message::SwitchDirection, - ) - .into(), - ]) - .spacing(10) - .width(Length::Fill); + ), + ] + .spacing(10); let scroll_alignment_controls = column(vec![ text("Scrollable alignment:").into(), @@ -194,16 +189,14 @@ impl Application for ScrollableDemo { ) .into(), ]) - .spacing(10) - .width(Length::Fill); + .spacing(10); let scroll_controls = row![ scroll_slider_controls, scroll_orientation_controls, scroll_alignment_controls ] - .spacing(20) - .width(Length::Fill); + .spacing(20); let scroll_to_end_button = || { button("Scroll to end") @@ -229,7 +222,6 @@ impl Application for ScrollableDemo { text("End!"), scroll_to_beginning_button(), ] - .width(Length::Fill) .align_items(Alignment::Center) .padding([40, 0, 40, 0]) .spacing(40), @@ -341,7 +333,6 @@ impl Application for ScrollableDemo { let content: Element<Message> = column![scroll_controls, scrollable_content, progress_bars] - .width(Length::Fill) .height(Length::Fill) .align_items(Alignment::Center) .spacing(10) diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index ef935c33..01a114bb 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -79,12 +79,10 @@ impl Application for SierpinskiEmulator { row![ text(format!("Iteration: {:?}", self.graph.iteration)), slider(0..=10000, self.graph.iteration, Message::IterationSet) - .width(Length::Fill) ] .padding(10) .spacing(20), ] - .width(Length::Fill) .align_items(iced::Alignment::Center) .into() } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 51538ec2..f14f6a8f 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -104,10 +104,11 @@ impl Sandbox for Styling { let progress_bar = progress_bar(0.0..=100.0, self.slider_value); - let scrollable = scrollable( - column!["Scroll me!", vertical_space(800), "You did it!"] - .width(Length::Fill), - ) + let scrollable = scrollable(column![ + "Scroll me!", + vertical_space(800), + "You did it!" + ]) .width(Length::Fill) .height(100); diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index 4dc92416..3bf4960f 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -63,7 +63,6 @@ impl Sandbox for Tiger { container(apply_color_filter).width(Length::Fill).center_x() ] .spacing(20) - .width(Length::Fill) .height(Length::Fill), ) .width(Length::Fill) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 31b6f191..711d8223 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -106,9 +106,7 @@ impl Application for App { fn view<'a>(&'a self) -> Element<'a, Message> { let subtitle = |title, content: Element<'a, Message>| { - column![text(title).size(14), content] - .width(Length::Fill) - .spacing(5) + column![text(title).size(14), content].spacing(5) }; let mut add_toast = button("Add Toast"); @@ -153,14 +151,11 @@ impl Application for App { Message::Timeout ) .step(1.0) - .width(Length::Fill) ] .spacing(5) .into() ), - column![add_toast] - .width(Length::Fill) - .align_items(Alignment::End) + column![add_toast].align_items(Alignment::End) ] .spacing(10) .max_width(200), @@ -513,14 +508,14 @@ mod toast { position: Point, _translation: Vector, ) -> layout::Node { - let limits = layout::Limits::new(Size::ZERO, bounds) - .width(Length::Fill) - .height(Length::Fill); + let limits = layout::Limits::new(Size::ZERO, bounds); layout::flex::resolve( layout::flex::Axis::Vertical, renderer, &limits, + Length::Fill, + Length::Fill, 10.into(), 10.0, Alignment::End, diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 7003d8ae..b9ee1e61 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -692,11 +692,7 @@ fn ferris<'a>( } fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { - iced::widget::button( - text(label).horizontal_alignment(alignment::Horizontal::Center), - ) - .padding(12) - .width(100) + iced::widget::button(text(label)).padding([12, 24]) } fn color_slider<'a>( diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 920189f5..5fdf6657 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -116,7 +116,6 @@ impl Application for WebSocket { .map(Element::from) .collect(), ) - .width(Length::Fill) .spacing(10), ) .id(MESSAGE_LOG.clone()) @@ -149,7 +148,6 @@ impl Application for WebSocket { }; column![message_log, new_message_input] - .width(Length::Fill) .height(Length::Fill) .padding(20) .spacing(10) diff --git a/widget/src/button.rs b/widget/src/button.rs index 384a3156..ba68caa5 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -433,13 +433,18 @@ pub fn layout( ) -> layout::Node { let limits = limits.width(width).height(height); - let mut content = layout_content(&limits.pad(padding)); + let content = layout_content(&limits.shrink(padding)); let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()).pad(padding); - content.move_to(Point::new(padding.left, padding.top)); + let size = limits + .shrink(padding) + .resolve(content.size(), width, height) + .expand(padding); - layout::Node::with_children(size, vec![content]) + layout::Node::with_children( + size, + vec![content.move_to(Point::new(padding.left, padding.top))], + ) } /// Returns the [`mouse::Interaction`] of a [`Button`]. diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 390f4d92..9e33c113 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -133,8 +133,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, self.width, self.height); layout::Node::new(size) } diff --git a/widget/src/column.rs b/widget/src/column.rs index abb522be..526509bb 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -35,7 +35,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { Column { spacing: 0.0, padding: Padding::ZERO, - width: Length::Shrink, + width: Length::Fill, height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, @@ -126,15 +126,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .width(self.width) - .height(self.height); + let limits = limits.max_width(self.max_width); layout::flex::resolve( layout::flex::Axis::Vertical, renderer, &limits, + self.width, + self.height, self.padding, self.spacing, self.align_items, diff --git a/widget/src/container.rs b/widget/src/container.rs index 5dd7705b..b41a6023 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -312,24 +312,27 @@ pub fn layout( layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { let limits = limits - .loose() - .max_width(max_width) - .max_height(max_height) .width(width) - .height(height); + .height(height) + .max_width(max_width) + .max_height(max_height); - let mut content = layout_content(&limits.pad(padding).loose()); + let content = layout_content(&limits.shrink(padding).loose()); let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()); - - content.move_to(Point::new(padding.left, padding.top)); - content.align( - Alignment::from(horizontal_alignment), - Alignment::from(vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(padding), vec![content]) + let size = limits + .shrink(padding) + .resolve(content.size(), width, height); + + layout::Node::with_children( + size.expand(padding), + vec![content + .move_to(Point::new(padding.left, padding.top)) + .align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + )], + ) } /// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. diff --git a/widget/src/image.rs b/widget/src/image.rs index 67699102..b5f1e907 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -99,7 +99,7 @@ where }; // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.width(width).height(height).resolve(image_size); + let raw_size = limits.resolve(image_size, width, height); // The uncropped size of the image when fit to the bounds above let full_size = content_fit.fit(image_size, raw_size); diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 68015ba8..23c4fe86 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -113,10 +113,11 @@ where ) -> layout::Node { let Size { width, height } = renderer.dimensions(&self.handle); - let mut size = limits - .width(self.width) - .height(self.height) - .resolve(Size::new(width as f32, height as f32)); + let mut size = limits.resolve( + Size::new(width as f32, height as f32), + self.width, + self.height, + ); let expansion_size = if height > width { self.width diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 0ef82407..1b53b43a 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -196,6 +196,8 @@ where layout::flex::Axis::Vertical, renderer, &limits, + self.width, + self.height, self.padding, self.spacing, self.align_items, diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index e45b44ae..ef39a952 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -254,15 +254,14 @@ where ) .width(self.width); - let mut node = self.container.layout(self.state, renderer, &limits); + let node = self.container.layout(self.state, renderer, &limits); + let size = node.size(); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) } else { - position - Vector::new(0.0, node.size().height) - }); - - node + position - Vector::new(0.0, size.height) + }) } fn on_event( @@ -359,7 +358,6 @@ where ) -> layout::Node { use std::f32; - let limits = limits.width(Length::Fill).height(Length::Shrink); let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); @@ -372,7 +370,7 @@ where * self.options.len() as f32, ); - limits.resolve(intrinsic) + limits.resolve(intrinsic, Length::Fill, Length::Shrink) }; layout::Node::new(size) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 7057fe59..3d799fd3 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -490,8 +490,7 @@ pub fn layout<Renderer, T>( &layout::Limits, ) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, width, height); let regions = node.pane_regions(spacing, size); let children = contents @@ -500,16 +499,14 @@ pub fn layout<Renderer, T>( let region = regions.get(&pane)?; let size = Size::new(region.width, region.height); - let mut node = layout_content( + let node = layout_content( content, tree, renderer, &layout::Limits::new(size, size), ); - node.move_to(Point::new(region.x, region.y)); - - Some(node) + Some(node.move_to(Point::new(region.x, region.y))) }) .collect(); diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 826ea663..ee00f186 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -165,7 +165,7 @@ where let title_bar_size = title_bar_layout.size(); - let mut body_layout = self.body.as_widget().layout( + let body_layout = self.body.as_widget().layout( &mut tree.children[0], renderer, &layout::Limits::new( @@ -177,11 +177,12 @@ where ), ); - body_layout.move_to(Point::new(0.0, title_bar_size.height)); - layout::Node::with_children( max_size, - vec![title_bar_layout, body_layout], + vec![ + title_bar_layout, + body_layout.move_to(Point::new(0.0, title_bar_size.height)), + ], ) } else { self.body.as_widget().layout( diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index f4dbb6b1..eb21b743 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -217,7 +217,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.pad(self.padding); + let limits = limits.shrink(self.padding); let max_size = limits.max(); let title_layout = self.content.as_widget().layout( @@ -228,8 +228,8 @@ where let title_size = title_layout.size(); - let mut node = if let Some(controls) = &self.controls { - let mut controls_layout = controls.as_widget().layout( + let node = if let Some(controls) = &self.controls { + let controls_layout = controls.as_widget().layout( &mut tree.children[1], renderer, &layout::Limits::new(Size::ZERO, max_size), @@ -240,11 +240,13 @@ where let height = title_size.height.max(controls_size.height); - controls_layout.move_to(Point::new(space_before_controls, 0.0)); - layout::Node::with_children( Size::new(max_size.width, height), - vec![title_layout, controls_layout], + vec![ + title_layout, + controls_layout + .move_to(Point::new(space_before_controls, 0.0)), + ], ) } else { layout::Node::with_children( @@ -253,9 +255,7 @@ where ) }; - node.move_to(Point::new(self.padding.left, self.padding.top)); - - layout::Node::with_children(node.size().pad(self.padding), vec![node]) + layout::Node::container(node, self.padding) } pub(crate) fn operate( diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 022ca8d9..13110725 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -393,7 +393,7 @@ where { use std::f32; - let limits = limits.width(width).height(Length::Shrink).pad(padding); + let limits = limits.width(width).height(Length::Shrink); let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = text_size.unwrap_or_else(|| renderer.default_size()); @@ -451,7 +451,10 @@ where f32::from(text_line_height.to_absolute(text_size)), ); - limits.resolve(intrinsic).pad(padding) + limits + .shrink(padding) + .resolve(intrinsic, width, Length::Shrink) + .expand(padding) }; layout::Node::new(size) diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 07de72d5..b84ab2dd 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -99,11 +99,11 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .width(self.width) - .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))); - - let size = limits.resolve(Size::ZERO); + let size = limits.resolve( + Size::ZERO, + self.width, + self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), + ); layout::Node::new(size) } diff --git a/widget/src/row.rs b/widget/src/row.rs index d52b8c43..650c2c7d 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -34,7 +34,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { Row { spacing: 0.0, padding: Padding::ZERO, - width: Length::Shrink, + width: Length::Fill, height: Length::Shrink, align_items: Alignment::Start, children, @@ -118,12 +118,12 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - layout::flex::resolve( layout::flex::Axis::Horizontal, renderer, &limits, + self.width, + self.height, self.padding, self.spacing, self.align_items, diff --git a/widget/src/rule.rs b/widget/src/rule.rs index b5c5fa55..ecaedf60 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -76,9 +76,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::Node::new(limits.resolve(Size::ZERO)) + layout::Node::new(limits.resolve(Size::ZERO, self.width, self.height)) } fn draw( diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 49aed2f0..525463c4 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -489,7 +489,7 @@ pub fn layout<Renderer>( ); let content = layout_content(renderer, &child_limits); - let size = limits.resolve(content.size()); + let size = limits.resolve(content.size(), width, height); layout::Node::with_children(size, vec![content]) } diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 8e334693..5b18ec7d 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -85,7 +85,7 @@ where limits: &layout::Limits, ) -> layout::Node { let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, self.width, self.height); layout::Node::new(size) } diff --git a/widget/src/slider.rs b/widget/src/slider.rs index ac0982c8..2b600d9d 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -173,8 +173,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, self.width, self.height); layout::Node::new(size) } diff --git a/widget/src/space.rs b/widget/src/space.rs index e5a8f169..afa9a7c8 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -59,9 +59,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::Node::new(limits.resolve(Size::ZERO)) + layout::Node::new(limits.resolve(Size::ZERO, self.width, self.height)) } fn draw( diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 2d01d1ab..8367ad18 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -115,10 +115,7 @@ where let image_size = Size::new(width as f32, height as f32); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits - .width(self.width) - .height(self.height) - .resolve(image_size); + let raw_size = limits.resolve(image_size, self.width, self.height); // The uncropped size of the image when fit to the bounds above let full_size = self.content_fit.fit(image_size, raw_size); diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index a2a186f0..214bce17 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -350,7 +350,7 @@ where } internal.editor.update( - limits.pad(self.padding).max(), + limits.shrink(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 65d3e1eb..03eb2fd0 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -506,14 +506,11 @@ where { let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let limits = limits - .width(width) - .pad(padding) - .height(line_height.to_absolute(text_size)); + let height = line_height.to_absolute(text_size); - let text_bounds = limits.resolve(Size::ZERO); + let limits = limits.width(width).shrink(padding).height(height); + let text_bounds = limits.resolve(Size::ZERO, width, height); let placeholder_text = Text { font, @@ -552,41 +549,41 @@ where let icon_width = state.icon.min_width(); - let mut text_node = layout::Node::new( - text_bounds - Size::new(icon_width + icon.spacing, 0.0), - ); - - let mut icon_node = - layout::Node::new(Size::new(icon_width, text_bounds.height)); - - match icon.side { - Side::Left => { - text_node.move_to(Point::new( + let (text_position, icon_position) = match icon.side { + Side::Left => ( + Point::new( padding.left + icon_width + icon.spacing, padding.top, - )); - - icon_node.move_to(Point::new(padding.left, padding.top)); - } - Side::Right => { - text_node.move_to(Point::new(padding.left, padding.top)); - - icon_node.move_to(Point::new( + ), + Point::new(padding.left, padding.top), + ), + Side::Right => ( + Point::new(padding.left, padding.top), + Point::new( padding.left + text_bounds.width - icon_width, padding.top, - )); - } + ), + ), }; + let text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ) + .move_to(text_position); + + let icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)) + .move_to(icon_position); + layout::Node::with_children( - text_bounds.pad(padding), + text_bounds.expand(padding), vec![text_node, icon_node], ) } else { - let mut text = layout::Node::new(text_bounds); - text.move_to(Point::new(padding.left, padding.top)); + let text = layout::Node::new(text_bounds) + .move_to(Point::new(padding.left, padding.top)); - layout::Node::with_children(text_bounds.pad(padding), vec![text]) + layout::Node::with_children(text_bounds.expand(padding), vec![text]) } } diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index b888980a..adef13e4 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -353,7 +353,7 @@ where .then(|| viewport.size()) .unwrap_or(Size::INFINITY), ) - .pad(Padding::new(self.padding)), + .shrink(Padding::new(self.padding)), ); let text_bounds = text_layout.bounds(); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 01d3359c..e489104c 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -170,8 +170,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); + let size = limits.resolve(Size::ZERO, self.width, self.height); layout::Node::new(size) } -- cgit From ed3b3930180f1971da25fdcc66a4130da32400ba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 16 Mar 2023 20:37:24 +0100 Subject: Fix needless borrow in `row::layout` --- widget/src/row.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/row.rs b/widget/src/row.rs index 650c2c7d..c4a1db56 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -121,7 +121,7 @@ where layout::flex::resolve( layout::flex::Axis::Horizontal, renderer, - &limits, + limits, self.width, self.height, self.padding, -- cgit From 89418c1244d14ac6020b31f3f1e19d15b4c0a272 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Thu, 23 Mar 2023 16:07:23 +0100 Subject: Determine cross-axis max length based on contents if `Shrink` --- core/src/layout/flex.rs | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 738dc81d..823fb9e6 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -47,7 +47,7 @@ impl Axis { } } - fn pack(&self, main: f32, cross: f32) -> (f32, f32) { + fn pack<T>(&self, main: T, cross: T) -> (T, T) { match self { Axis::Horizontal => (main, cross), Axis::Vertical => (cross, main), @@ -78,7 +78,7 @@ where let total_spacing = spacing * items.len().saturating_sub(1) as f32; let max_cross = axis.cross(limits.max()); - let mut fill_sum = 0; + let mut fill_main_sum = 0; let mut cross = 0.0f32; let mut available = axis.main(limits.max()) - total_spacing; @@ -86,13 +86,12 @@ where nodes.resize(items.len(), Node::default()); for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); + let (fill_main_factor, fill_cross_factor) = axis.pack( + child.as_widget().width().fill_factor(), + child.as_widget().height().fill_factor(), + ); - if fill_factor == 0 { + if fill_main_factor == 0 && fill_cross_factor == 0 { let (max_width, max_height) = axis.pack(available, max_cross); let child_limits = @@ -107,7 +106,7 @@ where nodes[i] = layout; } else { - fill_sum += fill_factor; + fill_main_sum += fill_main_factor; } } @@ -122,15 +121,27 @@ where }, }; + let max_cross = match axis { + Axis::Horizontal => match height { + Length::Shrink => cross, + _ => max_cross, + }, + Axis::Vertical => match width { + Length::Shrink => cross, + _ => max_cross, + }, + }; + for (i, (child, tree)) in items.iter().zip(trees).enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); + let (fill_main_factor, fill_cross_factor) = axis.pack( + child.as_widget().width().fill_factor(), + child.as_widget().height().fill_factor(), + ); + + if fill_main_factor != 0 || fill_cross_factor != 0 { + let max_main = + remaining * fill_main_factor as f32 / fill_main_sum as f32; - if fill_factor != 0 { - let max_main = remaining * fill_factor as f32 / fill_sum as f32; let min_main = if max_main.is_infinite() { 0.0 } else { @@ -140,7 +151,8 @@ where let (min_width, min_height) = axis.pack(min_main, axis.cross(limits.min())); - let (max_width, max_height) = axis.pack(max_main, max_cross); + let (max_width, max_height) = axis + .pack(max_main, max_cross * fill_cross_factor.max(1) as f32); let child_limits = Limits::new( Size::new(min_width, min_height), -- cgit From aa3c956516a23af86dfb9d96b769e5f26addbe60 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Fri, 24 Mar 2023 03:02:26 +0100 Subject: Fix available space provided to children with non-fill main axis but fill cross axis --- core/src/layout/flex.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 823fb9e6..5ae98b8c 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -139,10 +139,13 @@ where ); if fill_main_factor != 0 || fill_cross_factor != 0 { - let max_main = - remaining * fill_main_factor as f32 / fill_main_sum as f32; + let max_main = if fill_main_factor == 0 { + available.max(0.0) + } else { + remaining * fill_main_factor as f32 / fill_main_sum as f32 + }; - let min_main = if max_main.is_infinite() { + let min_main = if fill_main_factor == 0 || max_main.is_infinite() { 0.0 } else { max_main -- cgit From fd8f980b88df260ce49d46ed6c514f6e382c6494 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Mon, 27 Mar 2023 14:40:03 +0200 Subject: Use `max_cross` if all elements are fluid in `layout::flex` --- core/src/layout/flex.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 5ae98b8c..9a4b2cbf 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -123,11 +123,11 @@ where let max_cross = match axis { Axis::Horizontal => match height { - Length::Shrink => cross, + Length::Shrink if cross > 0.0 => cross, _ => max_cross, }, Axis::Vertical => match width { - Length::Shrink => cross, + Length::Shrink if cross > 0.0 => cross, _ => max_cross, }, }; @@ -154,8 +154,7 @@ where let (min_width, min_height) = axis.pack(min_main, axis.cross(limits.min())); - let (max_width, max_height) = axis - .pack(max_main, max_cross * fill_cross_factor.max(1) as f32); + let (max_width, max_height) = axis.pack(max_main, max_cross); let child_limits = Limits::new( Size::new(min_width, min_height), -- cgit From b37f8f3e85962d19f18b72044efca95709aa8ee2 Mon Sep 17 00:00:00 2001 From: Imbris <imbrisf@gmail.com> Date: Thu, 4 Jan 2024 21:36:45 -0500 Subject: Remove backend module in renderer crate that has been unused since https://github.com/iced-rs/iced/pull/1932 --- renderer/src/backend.rs | 100 ------------------------------------------------ 1 file changed, 100 deletions(-) delete mode 100644 renderer/src/backend.rs diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs deleted file mode 100644 index 3f229b52..00000000 --- a/renderer/src/backend.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::core::text; -use crate::core::{Font, Point, Size}; -use crate::graphics::backend; - -use std::borrow::Cow; - -#[allow(clippy::large_enum_variant)] -pub enum Backend { - TinySkia(iced_tiny_skia::Backend), - #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::Backend), -} - -macro_rules! delegate { - ($backend:expr, $name:ident, $body:expr) => { - match $backend { - Self::TinySkia($name) => $body, - #[cfg(feature = "wgpu")] - Self::Wgpu($name) => $body, - } - }; -} - -impl backend::Text for Backend { - const ICON_FONT: Font = Font::with_name("Iced-Icons"); - const CHECKMARK_ICON: char = '\u{f00c}'; - const ARROW_DOWN_ICON: char = '\u{e800}'; - - fn default_font(&self) -> Font { - delegate!(self, backend, backend.default_font()) - } - - fn default_size(&self) -> f32 { - delegate!(self, backend, backend.default_size()) - } - - fn measure( - &self, - contents: &str, - size: f32, - line_height: text::LineHeight, - font: Font, - bounds: Size, - shaping: text::Shaping, - ) -> Size { - delegate!( - self, - backend, - backend.measure(contents, size, line_height, font, bounds, shaping) - ) - } - - fn hit_test( - &self, - contents: &str, - size: f32, - line_height: text::LineHeight, - font: Font, - bounds: Size, - shaping: text::Shaping, - position: Point, - nearest_only: bool, - ) -> Option<text::Hit> { - delegate!( - self, - backend, - backend.hit_test( - contents, - size, - line_height, - font, - bounds, - shaping, - position, - nearest_only, - ) - ) - } - - fn load_font(&mut self, font: Cow<'static, [u8]>) { - delegate!(self, backend, backend.load_font(font)); - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> { - delegate!(self, backend, backend.dimensions(handle)) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &crate::core::svg::Handle, - ) -> Size<u32> { - delegate!(self, backend, backend.viewport_dimensions(handle)) - } -} -- cgit From 6c9dfbf01ec865f2ccf3b33cc8902d4e7141cd4f Mon Sep 17 00:00:00 2001 From: William Shere <7796394+william-shere@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:50:38 +0000 Subject: Fix doc to include missing feature tags Helper functions behind `lazy` feature were missing the tag in the documentation. --- widget/src/lazy/helpers.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 8ca9cb86..5dc60d52 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -6,6 +6,7 @@ use std::hash::Hash; /// Creates a new [`Lazy`] widget with the given data `Dependency` and a /// closure that can turn this data into a widget tree. +#[cfg(feature = "lazy")] pub fn lazy<'a, Message, Renderer, Dependency, View>( dependency: Dependency, view: impl Fn(&Dependency) -> View + 'a, @@ -19,6 +20,7 @@ where /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. +#[cfg(feature = "lazy")] pub fn component<'a, C, Message, Renderer>( component: C, ) -> Element<'a, Message, Renderer> @@ -37,6 +39,7 @@ where /// The `view` closure will be provided with the current [`Size`] of /// the [`Responsive`] widget and, therefore, can be used to build the /// contents of the widget in a responsive way. +#[cfg(feature = "lazy")] pub fn responsive<'a, Message, Renderer>( f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, ) -> Responsive<'a, Message, Renderer> -- cgit From b083eda663b8939e1c3e86b5ce2cb5fa8fc80ccb Mon Sep 17 00:00:00 2001 From: Héctor Ramón <hector0193@gmail.com> Date: Tue, 9 Jan 2024 02:03:13 +0100 Subject: Fix `Svg` styling --- widget/src/svg.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/svg.rs b/widget/src/svg.rs index f9b16e1a..05c9265b 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -168,9 +168,9 @@ where }; let appearance = if is_mouse_over { - theme.appearance(&self.style) - } else { theme.hovered(&self.style) + } else { + theme.appearance(&self.style) }; renderer.draw( -- cgit From 2aa2b1712dfdc93762ebe0958614154920068731 Mon Sep 17 00:00:00 2001 From: Calastrophe <zediant@gmail.com> Date: Tue, 9 Jan 2024 02:37:45 -0600 Subject: Implemented fetch_maximized and fetch_minimized --- runtime/src/window.rs | 22 ++++++++++++++++++++++ runtime/src/window/action.rs | 23 +++++++++++++++++++++++ winit/src/application.rs | 10 ++++++++++ winit/src/multi_window.rs | 14 ++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/runtime/src/window.rs b/runtime/src/window.rs index f9d943f6..2136d64d 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -65,11 +65,33 @@ pub fn fetch_size<Message>( Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) } +/// Fetches if the window is maximized. +pub fn fetch_maximized<Message>( + id: Id, + f: impl FnOnce(bool) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchMaximized( + id, + Box::new(f), + ))) +} + /// Maximizes the window. pub fn maximize<Message>(id: Id, maximized: bool) -> Command<Message> { Command::single(command::Action::Window(Action::Maximize(id, maximized))) } +/// Fetches if the window is minimized. +pub fn fetch_minimized<Message>( + id: Id, + f: impl FnOnce(Option<bool>) -> Message + 'static, +) -> Command<Message> { + Command::single(command::Action::Window(Action::FetchMinimized( + id, + Box::new(f), + ))) +} + /// Minimizes the window. pub fn minimize<Message>(id: Id, minimized: bool) -> Command<Message> { Command::single(command::Action::Window(Action::Minimize(id, minimized))) diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index 2d98b607..8b532569 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -21,8 +21,19 @@ pub enum Action<T> { Resize(Id, Size), /// Fetch the current logical dimensions of the window. FetchSize(Id, Box<dyn FnOnce(Size) -> T + 'static>), + /// Fetch if the current window is maximized or not. + /// + /// ## Platform-specific + /// - **iOS / Android / Web:** Unsupported. + FetchMaximized(Id, Box<dyn FnOnce(bool) -> T + 'static>), /// Set the window to maximized or back Maximize(Id, bool), + /// Fetch if the current window is minimized or not. + /// + /// ## Platform-specific + /// - **Wayland:** Always `None`. + /// - **iOS / Android / Web:** Unsupported. + FetchMinimized(Id, Box<dyn FnOnce(Option<bool>) -> T + 'static>), /// Set the window to minimized or back Minimize(Id, bool), /// Move the window to the given logical coordinates. @@ -106,7 +117,13 @@ impl<T> Action<T> { Self::FetchSize(id, o) => { Action::FetchSize(id, Box::new(move |s| f(o(s)))) } + Self::FetchMaximized(id, o) => { + Action::FetchMaximized(id, Box::new(move |s| f(o(s)))) + } Self::Maximize(id, maximized) => Action::Maximize(id, maximized), + Self::FetchMinimized(id, o) => { + Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) + } Self::Minimize(id, minimized) => Action::Minimize(id, minimized), Self::Move(id, position) => Action::Move(id, position), Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), @@ -144,9 +161,15 @@ impl<T> fmt::Debug for Action<T> { write!(f, "Action::Resize({id:?}, {size:?})") } Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), + Self::FetchMaximized(id, _) => { + write!(f, "Action::FetchMaximized({id:?})") + } Self::Maximize(id, maximized) => { write!(f, "Action::Maximize({id:?}, {maximized})") } + Self::FetchMinimized(id, _) => { + write!(f, "Action::FetchMinimized({id:?})") + } Self::Minimize(id, minimized) => { write!(f, "Action::Minimize({id:?}, {minimized}") } diff --git a/winit/src/application.rs b/winit/src/application.rs index d9700075..35a35872 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -742,9 +742,19 @@ pub fn run_command<A, C, E>( ))) .expect("Send message to event loop"); } + window::Action::FetchMaximized(_id, callback) => { + proxy + .send_event(callback(window.is_maximized())) + .expect("Send message to event loop"); + } window::Action::Maximize(_id, maximized) => { window.set_maximized(maximized); } + window::Action::FetchMinimized(_id, callback) => { + proxy + .send_event(callback(window.is_minimized())) + .expect("Send message to event loop"); + } window::Action::Minimize(_id, minimized) => { window.set_minimized(minimized); } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 84651d40..1550b94b 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -942,11 +942,25 @@ fn run_command<A, C, E>( .expect("Send message to event loop"); } } + window::Action::FetchMaximized(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(callback(window.raw.is_maximized())) + .expect("Send message to event loop"); + } + } window::Action::Maximize(id, maximized) => { if let Some(window) = window_manager.get_mut(id) { window.raw.set_maximized(maximized); } } + window::Action::FetchMinimized(id, callback) => { + if let Some(window) = window_manager.get_mut(id) { + proxy + .send_event(callback(window.raw.is_minimized())) + .expect("Send message to event loop"); + } + } window::Action::Minimize(id, minimized) => { if let Some(window) = window_manager.get_mut(id) { window.raw.set_minimized(minimized); -- cgit From 082985ade8a108aa3ec1fe573411120b82da0cad Mon Sep 17 00:00:00 2001 From: Calastrophe <zediant@gmail.com> Date: Tue, 9 Jan 2024 02:39:23 -0600 Subject: Small documentation typo fixed --- core/src/window/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/window/event.rs b/core/src/window/event.rs index b9ee7aca..a14d127f 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -58,7 +58,7 @@ pub enum Event { /// for each file separately. FileHovered(PathBuf), - /// A file has beend dropped into the window. + /// A file has been dropped into the window. /// /// When the user drops multiple files at once, this event will be emitted /// for each file separately. -- cgit From 0322e820eb40d36a7425246278b7bcb22b7010aa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Mon, 27 Mar 2023 15:43:52 +0200 Subject: Create `layout` example --- examples/layout/Cargo.toml | 9 ++++ examples/layout/src/main.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++ src/time.rs | 1 + widget/src/column.rs | 2 +- widget/src/row.rs | 2 +- 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 examples/layout/Cargo.toml create mode 100644 examples/layout/src/main.rs diff --git a/examples/layout/Cargo.toml b/examples/layout/Cargo.toml new file mode 100644 index 00000000..c2c3f49b --- /dev/null +++ b/examples/layout/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "layout" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs new file mode 100644 index 00000000..1b0c0c94 --- /dev/null +++ b/examples/layout/src/main.rs @@ -0,0 +1,123 @@ +use iced::executor; +use iced::widget::{column, container, row, text, vertical_rule}; +use iced::{ + Application, Command, Element, Length, Settings, Subscription, Theme, +}; + +pub fn main() -> iced::Result { + Layout::run(Settings::default()) +} + +#[derive(Debug)] +struct Layout { + previous: Vec<Example>, + current: Example, + next: Vec<Example>, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + Next, + Previous, +} + +impl Application for Layout { + type Message = Message; + type Theme = Theme; + type Executor = executor::Default; + type Flags = (); + + fn new(_flags: Self::Flags) -> (Self, Command<Message>) { + ( + Self { + previous: vec![], + current: Example::Centered, + next: vec![Example::NestedQuotes], + }, + Command::none(), + ) + } + + fn title(&self) -> String { + String::from("Counter - Iced") + } + + fn update(&mut self, message: Self::Message) -> Command<Message> { + match message { + Message::Next => { + if !self.next.is_empty() { + let previous = std::mem::replace( + &mut self.current, + self.next.remove(0), + ); + + self.previous.push(previous); + } + } + Message::Previous => { + if let Some(previous) = self.previous.pop() { + let next = std::mem::replace(&mut self.current, previous); + + self.next.insert(0, next); + } + } + } + + Command::none() + } + + fn subscription(&self) -> Subscription<Message> { + use iced::event::{self, Event}; + use iced::keyboard; + + event::listen_with(|event, status| match event { + Event::Keyboard(keyboard::Event::KeyReleased { + key_code, .. + }) if status == event::Status::Ignored => match key_code { + keyboard::KeyCode::Left => Some(Message::Previous), + keyboard::KeyCode::Right => Some(Message::Next), + _ => None, + }, + _ => None, + }) + } + + fn view(&self) -> Element<Message> { + self.current.view() + } +} + +#[derive(Debug)] +enum Example { + Centered, + NestedQuotes, +} + +impl Example { + fn view(&self) -> Element<Message> { + match self { + Self::Centered => container(text("I am centered!").size(50)) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into(), + Self::NestedQuotes => container((1..5).fold( + column![text("Original text")].padding(10), + |quotes, i| { + column![ + row![vertical_rule(2), quotes], + text(format!("Reply {i}")) + ] + .spacing(10) + .padding(10) + }, + )) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into(), + } + } +} diff --git a/src/time.rs b/src/time.rs index 37d454ed..f10f7a5e 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,4 +1,5 @@ //! Listen and react to time. pub use iced_core::time::{Duration, Instant}; +#[allow(unused_imports)] pub use iced_futures::backend::default::time::*; diff --git a/widget/src/column.rs b/widget/src/column.rs index 526509bb..80327458 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -35,7 +35,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { Column { spacing: 0.0, padding: Padding::ZERO, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, diff --git a/widget/src/row.rs b/widget/src/row.rs index c4a1db56..50fc4de0 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -34,7 +34,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { Row { spacing: 0.0, padding: Padding::ZERO, - width: Length::Fill, + width: Length::Shrink, height: Length::Shrink, align_items: Alignment::Start, children, -- cgit From 22226394f7b1a0e0205b9bb5b3ef9b85a3b406f5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 5 Jan 2024 17:24:43 +0100 Subject: Introduce `Widget::size_hint` and fix further layout inconsistencies --- core/src/layout/flex.rs | 72 +++++++++++++++++++++++++--------- core/src/length.rs | 6 +++ core/src/widget.rs | 10 ++++- examples/download_progress/src/main.rs | 19 +++++---- examples/events/src/main.rs | 3 +- examples/layout/src/main.rs | 2 +- examples/lazy/src/main.rs | 46 ++++++++-------------- examples/loading_spinners/src/main.rs | 11 +++--- examples/scrollable/src/main.rs | 23 ++++------- examples/tour/src/main.rs | 1 - examples/websocket/src/main.rs | 11 ++---- widget/src/column.rs | 39 ++++++++++++------ widget/src/container.rs | 15 ++++++- widget/src/helpers.rs | 22 +++++++---- widget/src/lazy.rs | 7 ++++ widget/src/lazy/component.rs | 7 ++++ widget/src/row.rs | 41 +++++++++++++------ 17 files changed, 211 insertions(+), 124 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 9a4b2cbf..67cc7f2a 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -91,8 +91,46 @@ where child.as_widget().height().fill_factor(), ); - if fill_main_factor == 0 && fill_cross_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); + if fill_main_factor == 0 { + if fill_cross_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = + child.as_widget().layout(tree, renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + cross = cross.max(axis.cross(size)); + + nodes[i] = layout; + } + } else { + fill_main_sum += fill_main_factor; + } + } + + let intrinsic_cross = match axis { + Axis::Horizontal => match height { + Length::Shrink => cross, + _ => max_cross, + }, + Axis::Vertical => match width { + Length::Shrink => cross, + _ => max_cross, + }, + }; + + for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { + let (fill_main_factor, fill_cross_factor) = axis.pack( + child.as_widget().width().fill_factor(), + child.as_widget().height().fill_factor(), + ); + + if fill_main_factor == 0 && fill_cross_factor != 0 { + let (max_width, max_height) = axis.pack(available, intrinsic_cross); let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); @@ -102,11 +140,8 @@ where let size = layout.size(); available -= axis.main(size); - cross = cross.max(axis.cross(size)); nodes[i] = layout; - } else { - fill_main_sum += fill_main_factor; } } @@ -121,24 +156,13 @@ where }, }; - let max_cross = match axis { - Axis::Horizontal => match height { - Length::Shrink if cross > 0.0 => cross, - _ => max_cross, - }, - Axis::Vertical => match width { - Length::Shrink if cross > 0.0 => cross, - _ => max_cross, - }, - }; - for (i, (child, tree)) in items.iter().zip(trees).enumerate() { let (fill_main_factor, fill_cross_factor) = axis.pack( child.as_widget().width().fill_factor(), child.as_widget().height().fill_factor(), ); - if fill_main_factor != 0 || fill_cross_factor != 0 { + if fill_main_factor != 0 { let max_main = if fill_main_factor == 0 { available.max(0.0) } else { @@ -151,6 +175,12 @@ where max_main }; + let max_cross = if fill_cross_factor == 0 { + max_cross + } else { + intrinsic_cross + }; + let (min_width, min_height) = axis.pack(min_main, axis.cross(limits.min())); @@ -203,8 +233,12 @@ where main += axis.main(size); } - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(Size::new(width, height), width, height); + let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross); + let size = limits.resolve( + Size::new(intrinsic_width, intrinsic_height), + width, + height, + ); Node::with_children(size.expand(padding), nodes) } diff --git a/core/src/length.rs b/core/src/length.rs index 3adb996e..6dc15049 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -36,6 +36,12 @@ impl Length { Length::Fixed(_) => 0, } } + + /// Returns `true` iff the [`Length`] is either [`Length::Fill`] or + // [`Length::FillPortion`]. + pub fn is_fill(&self) -> bool { + self.fill_factor() != 0 + } } impl From<Pixels> for Length { diff --git a/core/src/widget.rs b/core/src/widget.rs index 294d5984..890b3773 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -15,7 +15,7 @@ use crate::layout::{self, Layout}; use crate::mouse; use crate::overlay; use crate::renderer; -use crate::{Clipboard, Length, Rectangle, Shell}; +use crate::{Clipboard, Length, Rectangle, Shell, Size}; /// A component that displays information and allows interaction. /// @@ -49,6 +49,14 @@ where /// Returns the height of the [`Widget`]. fn height(&self) -> Length; + /// Returns a [`Size`] hint for laying out the [`Widget`]. + /// + /// This hint may be used by some widget containers to adjust their sizing strategy + /// during construction. + fn size_hint(&self) -> Size<Length> { + Size::new(self.width(), self.height()) + } + /// Returns the [`layout::Node`] of the [`Widget`]. /// /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs index a2fcb275..675e9e26 100644 --- a/examples/download_progress/src/main.rs +++ b/examples/download_progress/src/main.rs @@ -73,16 +73,15 @@ impl Application for Example { } fn view(&self) -> Element<Message> { - let downloads = Column::with_children( - self.downloads.iter().map(Download::view).collect(), - ) - .push( - button("Add another download") - .on_press(Message::Add) - .padding(10), - ) - .spacing(20) - .align_items(Alignment::End); + let downloads = + Column::with_children(self.downloads.iter().map(Download::view)) + .push( + button("Add another download") + .on_press(Message::Add) + .padding(10), + ) + .spacing(20) + .align_items(Alignment::End); container(downloads) .width(Length::Fill) diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 334b012d..fc51ac4a 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -82,8 +82,7 @@ impl Application for Events { self.last .iter() .map(|event| text(format!("{event:?}")).size(40)) - .map(Element::from) - .collect(), + .map(Element::from), ); let toggle = checkbox( diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 1b0c0c94..eeaa76b6 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -106,7 +106,7 @@ impl Example { column![text("Original text")].padding(10), |quotes, i| { column![ - row![vertical_rule(2), quotes], + row![vertical_rule(2), quotes].height(Length::Shrink), text(format!("Reply {i}")) ] .spacing(10) diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 01560598..04df0744 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -178,35 +178,23 @@ impl Sandbox for App { } }); - column( - items - .into_iter() - .map(|item| { - let button = button("Delete") - .on_press(Message::DeleteItem(item.clone())) - .style(theme::Button::Destructive); - - row![ - text(&item.name) - .style(theme::Text::Color(item.color.into())), - horizontal_space(Length::Fill), - pick_list( - Color::ALL, - Some(item.color), - move |color| { - Message::ItemColorChanged( - item.clone(), - color, - ) - } - ), - button - ] - .spacing(20) - .into() - }) - .collect(), - ) + column(items.into_iter().map(|item| { + let button = button("Delete") + .on_press(Message::DeleteItem(item.clone())) + .style(theme::Button::Destructive); + + row![ + text(&item.name) + .style(theme::Text::Color(item.color.into())), + horizontal_space(Length::Fill), + pick_list(Color::ALL, Some(item.color), move |color| { + Message::ItemColorChanged(item.clone(), color) + }), + button + ] + .spacing(20) + .into() + })) .spacing(10) }); diff --git a/examples/loading_spinners/src/main.rs b/examples/loading_spinners/src/main.rs index a78e9590..93a4605e 100644 --- a/examples/loading_spinners/src/main.rs +++ b/examples/loading_spinners/src/main.rs @@ -96,15 +96,14 @@ impl Application for LoadingSpinners { container( column.push( - row(vec![ - text("Cycle duration:").into(), + row![ + text("Cycle duration:"), slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| { Message::CycleDurationChanged(x / 100.0) }) - .width(200.0) - .into(), - text(format!("{:.2}s", self.cycle_duration)).into(), - ]) + .width(200.0), + text(format!("{:.2}s", self.cycle_duration)), + ] .align_items(iced::Alignment::Center) .spacing(20.0), ), diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 1042e7a4..249bc2a5 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -172,23 +172,21 @@ impl Application for ScrollableDemo { ] .spacing(10); - let scroll_alignment_controls = column(vec![ - text("Scrollable alignment:").into(), + let scroll_alignment_controls = column![ + text("Scrollable alignment:"), radio( "Start", scrollable::Alignment::Start, Some(self.alignment), Message::AlignmentChanged, - ) - .into(), + ), radio( "End", scrollable::Alignment::End, Some(self.alignment), Message::AlignmentChanged, ) - .into(), - ]) + ] .spacing(10); let scroll_controls = row![ @@ -226,6 +224,7 @@ impl Application for ScrollableDemo { .padding([40, 0, 40, 0]) .spacing(40), ) + .width(Length::Fill) .height(Length::Fill) .direction(scrollable::Direction::Vertical( Properties::new() @@ -251,6 +250,7 @@ impl Application for ScrollableDemo { .padding([0, 40, 0, 40]) .spacing(40), ) + .width(Length::Fill) .height(Length::Fill) .direction(scrollable::Direction::Horizontal( Properties::new() @@ -293,6 +293,7 @@ impl Application for ScrollableDemo { .padding([0, 40, 0, 40]) .spacing(40), ) + .width(Length::Fill) .height(Length::Fill) .direction({ let properties = Properties::new() @@ -333,19 +334,11 @@ impl Application for ScrollableDemo { let content: Element<Message> = column![scroll_controls, scrollable_content, progress_bars] - .height(Length::Fill) .align_items(Alignment::Center) .spacing(10) .into(); - Element::from( - container(content) - .width(Length::Fill) - .height(Length::Fill) - .padding(40) - .center_x() - .center_y(), - ) + Element::from(container(content).padding(40).center_x().center_y()) } fn theme(&self) -> Self::Theme { diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index b9ee1e61..8633bc0a 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -509,7 +509,6 @@ impl<'a> Step { ) }) .map(Element::from) - .collect() ) .spacing(10) ] diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 5fdf6657..59488e69 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -3,7 +3,7 @@ mod echo; use iced::alignment::{self, Alignment}; use iced::executor; use iced::widget::{ - button, column, container, row, scrollable, text, text_input, Column, + button, column, container, row, scrollable, text, text_input, }; use iced::{ Application, Color, Command, Element, Length, Settings, Subscription, Theme, @@ -108,13 +108,8 @@ impl Application for WebSocket { .into() } else { scrollable( - Column::with_children( - self.messages - .iter() - .cloned() - .map(text) - .map(Element::from) - .collect(), + column( + self.messages.iter().cloned().map(text).map(Element::from), ) .spacing(10), ) diff --git a/widget/src/column.rs b/widget/src/column.rs index 80327458..52cf35ce 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -22,16 +22,12 @@ pub struct Column<'a, Message, Renderer = crate::Renderer> { children: Vec<Element<'a, Message, Renderer>>, } -impl<'a, Message, Renderer> Column<'a, Message, Renderer> { +impl<'a, Message, Renderer> Column<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ /// Creates an empty [`Column`]. pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: Vec<Element<'a, Message, Renderer>>, - ) -> Self { Column { spacing: 0.0, padding: Padding::ZERO, @@ -39,10 +35,17 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, - children, + children: Vec::new(), } } + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl Iterator<Item = Element<'a, Message, Renderer>>, + ) -> Self { + children.fold(Self::new(), |column, element| column.push(element)) + } + /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -88,12 +91,26 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> { mut self, child: impl Into<Element<'a, Message, Renderer>>, ) -> Self { - self.children.push(child.into()); + let child = child.into(); + let size = child.as_widget().size_hint(); + + if size.width.is_fill() { + self.width = Length::Fill; + } + + if size.height.is_fill() { + self.height = Length::Fill; + } + + self.children.push(child); self } } -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { +impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ fn default() -> Self { Self::new() } diff --git a/widget/src/container.rs b/widget/src/container.rs index b41a6023..fbc68db7 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -46,11 +46,22 @@ where where T: Into<Element<'a, Message, Renderer>>, { + let content = content.into(); + let size = content.as_widget().size_hint(); + Container { id: None, padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, + width: if size.width.is_fill() { + Length::Fill + } else { + Length::Shrink + }, + height: if size.height.is_fill() { + Length::Fill + } else { + Length::Shrink + }, max_width: f32::INFINITY, max_height: f32::INFINITY, horizontal_alignment: alignment::Horizontal::Left, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 115198fb..6eaf3392 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -34,7 +34,7 @@ macro_rules! column { $crate::Column::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + $crate::Column::with_children([$($crate::core::Element::from($x)),+].into_iter()) ); } @@ -47,7 +47,7 @@ macro_rules! row { $crate::Row::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Row::with_children(vec![$($crate::core::Element::from($x)),+]) + $crate::Row::with_children([$($crate::core::Element::from($x)),+].into_iter()) ); } @@ -65,9 +65,12 @@ where } /// Creates a new [`Column`] with the given children. -pub fn column<Message, Renderer>( - children: Vec<Element<'_, Message, Renderer>>, -) -> Column<'_, Message, Renderer> { +pub fn column<'a, Message, Renderer>( + children: impl Iterator<Item = Element<'a, Message, Renderer>>, +) -> Column<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ Column::with_children(children) } @@ -84,9 +87,12 @@ where /// Creates a new [`Row`] with the given children. /// /// [`Row`]: crate::Row -pub fn row<Message, Renderer>( - children: Vec<Element<'_, Message, Renderer>>, -) -> Row<'_, Message, Renderer> { +pub fn row<'a, Message, Renderer>( + children: impl Iterator<Item = Element<'a, Message, Renderer>>, +) -> Row<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ Row::with_children(children) } diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 167a055d..4f6513db 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -150,6 +150,13 @@ where self.with_element(|element| element.as_widget().height()) } + fn size_hint(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } + fn layout( &self, tree: &mut Tree, diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index ad0c3823..0aff7485 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -252,6 +252,13 @@ where self.with_element(|element| element.as_widget().height()) } + fn size_hint(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } + fn layout( &self, tree: &mut Tree, diff --git a/widget/src/row.rs b/widget/src/row.rs index 50fc4de0..ef371ddb 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -21,26 +21,31 @@ pub struct Row<'a, Message, Renderer = crate::Renderer> { children: Vec<Element<'a, Message, Renderer>>, } -impl<'a, Message, Renderer> Row<'a, Message, Renderer> { +impl<'a, Message, Renderer> Row<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ /// Creates an empty [`Row`]. pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Row`] with the given elements. - pub fn with_children( - children: Vec<Element<'a, Message, Renderer>>, - ) -> Self { Row { spacing: 0.0, padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, align_items: Alignment::Start, - children, + children: Vec::new(), } } + /// Creates a [`Row`] with the given elements. + pub fn with_children( + children: impl Iterator<Item = Element<'a, Message, Renderer>>, + ) -> Self { + children + .into_iter() + .fold(Self::new(), |column, element| column.push(element)) + } + /// Sets the horizontal spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -80,12 +85,26 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> { mut self, child: impl Into<Element<'a, Message, Renderer>>, ) -> Self { - self.children.push(child.into()); + let child = child.into(); + let size = child.as_widget().size_hint(); + + if size.width.is_fill() { + self.width = Length::Fill; + } + + if size.height.is_fill() { + self.height = Length::Fill; + } + + self.children.push(child); self } } -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { +impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> +where + Renderer: crate::core::Renderer, +{ fn default() -> Self { Self::new() } -- cgit From d278bfd21d0399009e652560afb9a4d185e92637 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 5 Jan 2024 17:46:33 +0100 Subject: Replace `width` and `height` with `Widget::size` --- core/src/element.rs | 18 +++++------------- core/src/layout/flex.rs | 27 +++++++++++++++------------ core/src/widget.rs | 9 +++------ core/src/widget/text.rs | 15 ++++++++------- examples/custom_quad/src/main.rs | 11 +++++------ examples/custom_widget/src/main.rs | 11 +++++------ examples/geometry/src/main.rs | 11 +++++------ examples/loading_spinners/src/circular.rs | 11 +++++------ examples/loading_spinners/src/linear.rs | 11 +++++------ examples/modal/src/main.rs | 8 ++------ examples/toast/src/main.rs | 8 ++------ widget/src/button.rs | 13 ++++++------- widget/src/canvas.rs | 16 ++++++++-------- widget/src/checkbox.rs | 11 +++++------ widget/src/column.rs | 13 ++++++------- widget/src/combo_box.rs | 12 +++++------- widget/src/container.rs | 11 +++++------ widget/src/image.rs | 11 +++++------ widget/src/image/viewer.rs | 11 +++++------ widget/src/keyed/column.rs | 13 ++++++------- widget/src/lazy.rs | 8 ++------ widget/src/lazy/component.rs | 8 ++------ widget/src/lazy/responsive.rs | 11 +++++------ widget/src/mouse_area.rs | 10 +++------- widget/src/overlay/menu.rs | 11 +++++------ widget/src/pane_grid.rs | 11 +++++------ widget/src/pick_list.rs | 11 +++++------ widget/src/progress_bar.rs | 11 +++++------ widget/src/qr_code.rs | 11 +++++------ widget/src/radio.rs | 11 +++++------ widget/src/row.rs | 13 ++++++------- widget/src/rule.rs | 11 +++++------ widget/src/scrollable.rs | 11 +++++------ widget/src/shader.rs | 11 +++++------ widget/src/slider.rs | 11 +++++------ widget/src/space.rs | 11 +++++------ widget/src/svg.rs | 11 +++++------ widget/src/text_editor.rs | 13 ++++++------- widget/src/text_input.rs | 11 +++++------ widget/src/toggler.rs | 11 +++++------ widget/src/tooltip.rs | 8 ++------ widget/src/vertical_slider.rs | 11 +++++------ 42 files changed, 212 insertions(+), 275 deletions(-) diff --git a/core/src/element.rs b/core/src/element.rs index dea111af..8b510218 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -6,7 +6,7 @@ use crate::renderer; use crate::widget; use crate::widget::tree::{self, Tree}; use crate::{ - Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget, + Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector, Widget, }; use std::any::Any; @@ -296,12 +296,8 @@ where self.widget.diff(tree); } - fn width(&self) -> Length { - self.widget.width() - } - - fn height(&self) -> Length { - self.widget.height() + fn size(&self) -> Size<Length> { + self.widget.size() } fn layout( @@ -466,12 +462,8 @@ impl<'a, Message, Renderer> Widget<Message, Renderer> where Renderer: crate::Renderer, { - fn width(&self) -> Length { - self.element.widget.width() - } - - fn height(&self) -> Length { - self.element.widget.height() + fn size(&self) -> Size<Length> { + self.element.widget.size() } fn tag(&self) -> tree::Tag { diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 67cc7f2a..036b31fd 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -86,10 +86,11 @@ where nodes.resize(items.len(), Node::default()); for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let (fill_main_factor, fill_cross_factor) = axis.pack( - child.as_widget().width().fill_factor(), - child.as_widget().height().fill_factor(), - ); + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; if fill_main_factor == 0 { if fill_cross_factor == 0 { @@ -124,10 +125,11 @@ where }; for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { - let (fill_main_factor, fill_cross_factor) = axis.pack( - child.as_widget().width().fill_factor(), - child.as_widget().height().fill_factor(), - ); + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; if fill_main_factor == 0 && fill_cross_factor != 0 { let (max_width, max_height) = axis.pack(available, intrinsic_cross); @@ -157,10 +159,11 @@ where }; for (i, (child, tree)) in items.iter().zip(trees).enumerate() { - let (fill_main_factor, fill_cross_factor) = axis.pack( - child.as_widget().width().fill_factor(), - child.as_widget().height().fill_factor(), - ); + let (fill_main_factor, fill_cross_factor) = { + let size = child.as_widget().size(); + + axis.pack(size.width.fill_factor(), size.height.fill_factor()) + }; if fill_main_factor != 0 { let max_main = if fill_main_factor == 0 { diff --git a/core/src/widget.rs b/core/src/widget.rs index 890b3773..7f5632ae 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -43,18 +43,15 @@ pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, { - /// Returns the width of the [`Widget`]. - fn width(&self) -> Length; - - /// Returns the height of the [`Widget`]. - fn height(&self) -> Length; + /// Returns the [`Size`] of the [`Widget`] in lengths. + fn size(&self) -> Size<Length>; /// Returns a [`Size`] hint for laying out the [`Widget`]. /// /// This hint may be used by some widget containers to adjust their sizing strategy /// during construction. fn size_hint(&self) -> Size<Length> { - Size::new(self.width(), self.height()) + self.size() } /// Returns the [`layout::Node`] of the [`Widget`]. diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index e47e4178..fe3b77d3 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -5,7 +5,9 @@ use crate::mouse; use crate::renderer; use crate::text::{self, Paragraph}; use crate::widget::tree::{self, Tree}; -use crate::{Color, Element, Layout, Length, Pixels, Point, Rectangle, Widget}; +use crate::{ + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, +}; use std::borrow::Cow; @@ -134,12 +136,11 @@ where tree::State::new(State(Renderer::Paragraph::default())) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 13b08250..cc9ad528 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -26,12 +26,11 @@ mod quad { where Renderer: renderer::Renderer, { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 32a14cbe..7ffb4cd0 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -33,12 +33,11 @@ mod circle { where Renderer: renderer::Renderer, { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 50227f1c..d6a4c702 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -16,12 +16,11 @@ mod rainbow { } impl<Message> Widget<Message, Renderer> for Rainbow { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index a92a5dd1..e80617d0 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -244,12 +244,11 @@ where tree::State::new(State::default()) } - fn width(&self) -> Length { - Length::Fixed(self.size) - } - - fn height(&self) -> Length { - Length::Fixed(self.size) + fn size(&self) -> Size<Length> { + Size { + width: Length::Fixed(self.size), + height: Length::Fixed(self.size), + } } fn layout( diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index da4f1ea1..d205d3f1 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -165,12 +165,11 @@ where tree::State::new(State::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 85ccf8b4..631efe6e 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -281,12 +281,8 @@ mod modal { tree.diff_children(&[&self.base, &self.modal]); } - fn width(&self) -> Length { - self.base.as_widget().width() - } - - fn height(&self) -> Length { - self.base.as_widget().height() + fn size(&self) -> Size<Length> { + self.base.as_widget().size() } fn layout( diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 711d8223..300343b9 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -313,12 +313,8 @@ mod toast { } impl<'a, Message> Widget<Message, Renderer> for Manager<'a, Message> { - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() + fn size(&self) -> Size<Length> { + self.content.as_widget().size() } fn layout( diff --git a/widget/src/button.rs b/widget/src/button.rs index ba68caa5..1ce4f662 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -11,7 +11,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, - Rectangle, Shell, Vector, Widget, + Rectangle, Shell, Size, Vector, Widget, }; pub use iced_style::button::{Appearance, StyleSheet}; @@ -149,12 +149,11 @@ where tree.diff_children(std::slice::from_ref(&self.content)); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 9e33c113..2bf09eec 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -14,8 +14,9 @@ use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; -use crate::core::{Clipboard, Element, Shell, Widget}; -use crate::core::{Length, Rectangle, Size, Vector}; +use crate::core::{ + Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, +}; use crate::graphics::geometry; use std::marker::PhantomData; @@ -119,12 +120,11 @@ where tree::State::new(P::State::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index a0d9559b..0353b3ad 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -174,12 +174,11 @@ where tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/column.rs b/widget/src/column.rs index 52cf35ce..9867d97e 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -7,7 +7,7 @@ use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Widget, + Shell, Size, Widget, }; /// A container that distributes its contents vertically. @@ -129,12 +129,11 @@ where tree.diff_children(&self.children); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 31ec27fc..1b2fa947 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -8,7 +8,9 @@ use crate::core::renderer; use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; -use crate::core::{Clipboard, Element, Length, Padding, Rectangle, Shell}; +use crate::core::{ + Clipboard, Element, Length, Padding, Rectangle, Shell, Size, +}; use crate::overlay::menu; use crate::text::LineHeight; use crate::{container, scrollable, text_input, TextInput}; @@ -297,12 +299,8 @@ where + scrollable::StyleSheet + menu::StyleSheet, { - fn width(&self) -> Length { - Widget::<TextInputEvent, Renderer>::width(&self.text_input) - } - - fn height(&self) -> Length { - Widget::<TextInputEvent, Renderer>::height(&self.text_input) + fn size(&self) -> Size<Length> { + Widget::<TextInputEvent, Renderer>::size(&self.text_input) } fn layout( diff --git a/widget/src/container.rs b/widget/src/container.rs index fbc68db7..93d8daba 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -163,12 +163,11 @@ where self.content.as_widget().diff(tree); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/image.rs b/widget/src/image.rs index b5f1e907..6750c1b3 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -164,12 +164,11 @@ where Renderer: image::Renderer<Handle = Handle>, Handle: Clone + Hash, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 23c4fe86..dc910f1f 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -97,12 +97,11 @@ where tree::State::new(State::new()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 1b53b43a..32320300 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -8,7 +8,7 @@ use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Widget, + Shell, Size, Widget, }; /// A container that distributes its contents vertically. @@ -173,12 +173,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 4f6513db..e9edbb4c 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -142,12 +142,8 @@ where } } - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) + fn size(&self) -> Size<Length> { + self.with_element(|element| element.as_widget().size()) } fn size_hint(&self) -> Size<Length> { diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 0aff7485..3684e0c9 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -244,12 +244,8 @@ where self.rebuild_element_if_necessary(); } - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) + fn size(&self) -> Size<Length> { + self.with_element(|element| element.as_widget().size()) } fn size_hint(&self) -> Size<Length> { diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index 86d37b6c..1df0866f 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -135,12 +135,11 @@ where }) } - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill + fn size(&self) -> Size<Length> { + Size { + width: Length::Fill, + height: Length::Fill, + } } fn layout( diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index 3a5b01a3..87cac3a7 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -8,7 +8,7 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::{tree, Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Widget, + Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget, }; /// Emit messages on mouse events. @@ -110,12 +110,8 @@ where tree.diff_children(std::slice::from_ref(&self.content)); } - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() + fn size(&self) -> Size<Length> { + self.content.as_widget().size() } fn layout( diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index ef39a952..b9e06de8 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -342,12 +342,11 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 3d799fd3..36c785b7 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -265,12 +265,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 13110725..d83b0624 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -164,12 +164,11 @@ where tree::State::new(State::<Renderer::Paragraph>::new()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index b84ab2dd..a05923a2 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -85,12 +85,11 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)) + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), + } } fn layout( diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index 1dc4da7f..a229eb59 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -50,12 +50,11 @@ impl<'a> QRCode<'a> { } impl<'a, Message, Theme> Widget<Message, Renderer<Theme>> for QRCode<'a> { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/radio.rs b/widget/src/radio.rs index ae2365dd..f91b20b1 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -201,12 +201,11 @@ where tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/row.rs b/widget/src/row.rs index ef371ddb..bcbe9267 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -7,7 +7,7 @@ use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, - Widget, + Size, Widget, }; /// A container that distributes its contents horizontally. @@ -123,12 +123,11 @@ where tree.diff_children(&self.children); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/rule.rs b/widget/src/rule.rs index ecaedf60..4ab16c40 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -62,12 +62,11 @@ where Renderer: crate::core::Renderer, Renderer::Theme: StyleSheet, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 525463c4..5197afde 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -220,12 +220,11 @@ where tree.diff_children(std::slice::from_ref(&self.content)); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 5b18ec7d..82432c6c 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -70,12 +70,11 @@ where tree::State::new(P::State::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 2b600d9d..27588852 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -159,12 +159,11 @@ where tree::State::new(State::new()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/space.rs b/widget/src/space.rs index afa9a7c8..9fd4dcb9 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -45,12 +45,11 @@ impl<Message, Renderer> Widget<Message, Renderer> for Space where Renderer: core::Renderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 8367ad18..75ab238a 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -96,12 +96,11 @@ where Renderer: svg::Renderer, Renderer::Theme: iced_style::svg::StyleSheet, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 214bce17..9118d124 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -9,7 +9,7 @@ use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, + Clipboard, Color, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, }; @@ -316,12 +316,11 @@ where }) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } } fn layout( diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 03eb2fd0..7e91105c 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -283,12 +283,11 @@ where } } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index d8723080..941159ea 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -168,12 +168,11 @@ where tree::State::new(widget::text::State::<Renderer::Paragraph>::default()) } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: Length::Shrink, + } } fn layout( diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index adef13e4..d09a9255 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -131,12 +131,8 @@ where widget::tree::Tag::of::<State>() } - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.content.as_widget().height() + fn size(&self) -> Size<Length> { + self.content.as_widget().size() } fn layout( diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index e489104c..35bc2fe2 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -156,12 +156,11 @@ where tree::State::new(State::new()) } - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size<Length> { + Size { + width: Length::Shrink, + height: self.height, + } } fn layout( -- cgit From 4bdd8a62791cfa4864d3d4cf1d5b19c6f227d537 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 5 Jan 2024 17:54:10 +0100 Subject: Fix `cross` axis calculation in `flex` layout --- core/src/layout/flex.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 036b31fd..2a12d57f 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -142,6 +142,7 @@ where let size = layout.size(); available -= axis.main(size); + cross = cross.max(axis.cross(layout.size())); nodes[i] = layout; } -- cgit From d24e50c1a61eee7bca887224ad583eca60e14d32 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 02:12:29 +0100 Subject: Reduce `padding` of `scrollable` example --- examples/scrollable/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 249bc2a5..4b57a5a4 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -338,7 +338,7 @@ impl Application for ScrollableDemo { .spacing(10) .into(); - Element::from(container(content).padding(40).center_x().center_y()) + container(content).padding(20).center_x().center_y().into() } fn theme(&self) -> Self::Theme { -- cgit From d62bb8193c1c43f565fcc5c52293d564c91e215d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 06:35:33 +0100 Subject: Introduce useful helpers in `layout` module --- core/src/layout.rs | 94 ++++++++++++++++++++++++++++++- core/src/layout/flex.rs | 2 +- core/src/layout/limits.rs | 7 ++- core/src/layout/node.rs | 10 +++- core/src/point.rs | 20 ++++--- core/src/widget/text.rs | 43 +++++++------- examples/geometry/src/main.rs | 4 +- examples/loading_spinners/src/circular.rs | 5 +- examples/loading_spinners/src/linear.rs | 5 +- widget/src/button.rs | 19 +------ widget/src/canvas.rs | 4 +- widget/src/container.rs | 28 ++++----- widget/src/image.rs | 2 +- widget/src/image/viewer.rs | 2 +- widget/src/overlay/menu.rs | 2 +- widget/src/pane_grid.rs | 2 +- widget/src/pick_list.rs | 5 +- widget/src/progress_bar.rs | 8 +-- widget/src/rule.rs | 2 +- widget/src/scrollable.rs | 39 ++++++------- widget/src/shader.rs | 5 +- widget/src/slider.rs | 4 +- widget/src/space.rs | 2 +- widget/src/svg.rs | 2 +- widget/src/text_input.rs | 4 +- widget/src/vertical_slider.rs | 4 +- 26 files changed, 193 insertions(+), 131 deletions(-) diff --git a/core/src/layout.rs b/core/src/layout.rs index 277473fe..95720aba 100644 --- a/core/src/layout.rs +++ b/core/src/layout.rs @@ -7,7 +7,7 @@ pub mod flex; pub use limits::Limits; pub use node::Node; -use crate::{Point, Rectangle, Size, Vector}; +use crate::{Length, Padding, Point, Rectangle, Size, Vector}; /// The bounds of a [`Node`] and its children, using absolute coordinates. #[derive(Debug, Clone, Copy)] @@ -96,3 +96,95 @@ pub fn next_to_each_other( ], ) } + +/// Computes the resulting [`Node`] that fits the [`Limits`] given +/// some width and height requirements and no intrinsic size. +pub fn atomic( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, +) -> Node { + let width = width.into(); + let height = height.into(); + + Node::new(limits.resolve(width, height, Size::ZERO)) +} + +/// Computes the resulting [`Node`] that fits the [`Limits`] given +/// some width and height requirements and a closure that produces +/// the intrinsic [`Size`] inside the given [`Limits`]. +pub fn sized( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + f: impl FnOnce(&Limits) -> Size, +) -> Node { + let width = width.into(); + let height = height.into(); + + let limits = limits.width(width).height(height); + let intrinsic_size = f(&limits); + + Node::new(limits.resolve(width, height, intrinsic_size)) +} + +/// Computes the resulting [`Node`] that fits the [`Limits`] given +/// some width and height requirements and a closure that produces +/// the content [`Node`] inside the given [`Limits`]. +pub fn contained( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + f: impl FnOnce(&Limits) -> Node, +) -> Node { + let width = width.into(); + let height = height.into(); + + let limits = limits.width(width).height(height); + let content = f(&limits); + + Node::with_children( + limits.resolve(width, height, content.size()), + vec![content], + ) +} + +/// Computes the [`Node`] that fits the [`Limits`] given some width, height, and +/// [`Padding`] requirements and a closure that produces the content [`Node`] +/// inside the given [`Limits`]. +pub fn padded( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + padding: impl Into<Padding>, + layout: impl FnOnce(&Limits) -> Node, +) -> Node { + positioned(limits, width, height, padding, layout, |content, _| content) +} + +/// Computes a [`padded`] [`Node`] with a positioning step. +pub fn positioned( + limits: &Limits, + width: impl Into<Length>, + height: impl Into<Length>, + padding: impl Into<Padding>, + layout: impl FnOnce(&Limits) -> Node, + position: impl FnOnce(Node, Size) -> Node, +) -> Node { + let width = width.into(); + let height = height.into(); + let padding = padding.into(); + + let limits = limits.width(width).height(height); + let content = layout(&limits.shrink(padding)); + let padding = padding.fit(content.size(), limits.max()); + + let size = limits + .shrink(padding) + .resolve(width, height, content.size()); + + Node::with_children( + size.expand(padding), + vec![position(content.move_to((padding.left, padding.top)), size)], + ) +} diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 2a12d57f..cf3e1340 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -239,9 +239,9 @@ where let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross); let size = limits.resolve( - Size::new(intrinsic_width, intrinsic_height), width, height, + Size::new(intrinsic_width, intrinsic_height), ); Node::with_children(size.expand(padding), nodes) diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs index eef4c4c9..7fbc7b9d 100644 --- a/core/src/layout/limits.rs +++ b/core/src/layout/limits.rs @@ -114,13 +114,14 @@ impl Limits { } } - /// Computes the resulting [`Size`] that fits the [`Limits`] given the - /// intrinsic size of some content. + /// Computes the resulting [`Size`] that fits the [`Limits`] given + /// some width and height requirements and the intrinsic size of + /// some content. pub fn resolve( &self, - intrinsic_size: Size, width: impl Into<Length>, height: impl Into<Length>, + intrinsic_size: Size, ) -> Size { let width = match width.into() { Length::Fill | Length::FillPortion(_) => self.max.width, diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 00087431..40c71436 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -89,19 +89,23 @@ impl Node { } /// Moves the [`Node`] to the given position. - pub fn move_to(mut self, position: Point) -> Self { + pub fn move_to(mut self, position: impl Into<Point>) -> Self { self.move_to_mut(position); self } /// Mutable reference version of [`move_to`]. - pub fn move_to_mut(&mut self, position: Point) { + pub fn move_to_mut(&mut self, position: impl Into<Point>) { + let position = position.into(); + self.bounds.x = position.x; self.bounds.y = position.y; } /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: Vector) -> Self { + pub fn translate(self, translation: impl Into<Vector>) -> Self { + let translation = translation.into(); + Self { bounds: self.bounds + translation, ..self diff --git a/core/src/point.rs b/core/src/point.rs index ef42852f..cea57518 100644 --- a/core/src/point.rs +++ b/core/src/point.rs @@ -36,20 +36,26 @@ impl<T: Num> Point<T> { } } -impl From<[f32; 2]> for Point { - fn from([x, y]: [f32; 2]) -> Self { +impl<T> From<[T; 2]> for Point<T> +where + T: Num, +{ + fn from([x, y]: [T; 2]) -> Self { Point { x, y } } } -impl From<[u16; 2]> for Point<u16> { - fn from([x, y]: [u16; 2]) -> Self { - Point::new(x, y) +impl<T> From<(T, T)> for Point<T> +where + T: Num, +{ + fn from((x, y): (T, T)) -> Self { + Self { x, y } } } -impl From<Point> for [f32; 2] { - fn from(point: Point) -> [f32; 2] { +impl<T> From<Point<T>> for [T; 2] { + fn from(point: Point<T>) -> [T; 2] { [point.x, point.y] } } diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index fe3b77d3..4cabc7ce 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -206,28 +206,27 @@ pub fn layout<Renderer>( where Renderer: text::Renderer, { - let limits = limits.width(width).height(height); - let bounds = limits.max(); - - let size = size.unwrap_or_else(|| renderer.default_size()); - let font = font.unwrap_or_else(|| renderer.default_font()); - - let State(ref mut paragraph) = state; - - paragraph.update(text::Text { - content, - bounds, - size, - line_height, - font, - horizontal_alignment, - vertical_alignment, - shaping, - }); - - let size = limits.resolve(paragraph.min_bounds(), width, height); - - layout::Node::new(size) + layout::sized(limits, width, height, |limits| { + let bounds = limits.max(); + + let size = size.unwrap_or_else(|| renderer.default_size()); + let font = font.unwrap_or_else(|| renderer.default_font()); + + let State(ref mut paragraph) = state; + + paragraph.update(text::Text { + content, + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping, + }); + + paragraph.min_bounds() + }) } /// Draws text using the same logic as the [`Text`] widget. diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index d6a4c702..5cf9963d 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -29,9 +29,9 @@ mod rainbow { _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.resolve(Size::ZERO, Length::Fill, Length::Shrink); + let width = limits.max().width; - layout::Node::new(Size::new(size.width, size.width)) + layout::Node::new(Size::new(width, width)) } fn draw( diff --git a/examples/loading_spinners/src/circular.rs b/examples/loading_spinners/src/circular.rs index e80617d0..1b163585 100644 --- a/examples/loading_spinners/src/circular.rs +++ b/examples/loading_spinners/src/circular.rs @@ -257,10 +257,7 @@ where _renderer: &iced::Renderer<Theme>, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.size).height(self.size); - let size = limits.resolve(Size::ZERO, self.size, self.size); - - layout::Node::new(size) + layout::atomic(limits, self.size, self.size) } fn on_event( diff --git a/examples/loading_spinners/src/linear.rs b/examples/loading_spinners/src/linear.rs index d205d3f1..d245575c 100644 --- a/examples/loading_spinners/src/linear.rs +++ b/examples/loading_spinners/src/linear.rs @@ -178,10 +178,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO, self.width, self.height); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/widget/src/button.rs b/widget/src/button.rs index 1ce4f662..86abee77 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -10,8 +10,8 @@ use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Point, - Rectangle, Shell, Size, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, + Shell, Size, Vector, Widget, }; pub use iced_style::button::{Appearance, StyleSheet}; @@ -430,20 +430,7 @@ pub fn layout( padding: Padding, layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height); - - let content = layout_content(&limits.shrink(padding)); - let padding = padding.fit(content.size(), limits.max()); - - let size = limits - .shrink(padding) - .resolve(content.size(), width, height) - .expand(padding); - - layout::Node::with_children( - size, - vec![content.move_to(Point::new(padding.left, padding.top))], - ) + layout::padded(limits, width, height, padding, layout_content) } /// Returns the [`mouse::Interaction`] of a [`Button`]. diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index 2bf09eec..4e42a671 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -133,9 +133,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.resolve(Size::ZERO, self.width, self.height); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/widget/src/container.rs b/widget/src/container.rs index 93d8daba..c98de41c 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -321,27 +321,19 @@ pub fn layout( vertical_alignment: alignment::Vertical, layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits - .width(width) - .height(height) - .max_width(max_width) - .max_height(max_height); - - let content = layout_content(&limits.shrink(padding).loose()); - let padding = padding.fit(content.size(), limits.max()); - let size = limits - .shrink(padding) - .resolve(content.size(), width, height); - - layout::Node::with_children( - size.expand(padding), - vec![content - .move_to(Point::new(padding.left, padding.top)) - .align( + layout::positioned( + &limits.max_width(max_width).max_height(max_height), + width, + height, + padding, + |limits| layout_content(&limits.loose()), + |content, size| { + content.align( Alignment::from(horizontal_alignment), Alignment::from(vertical_alignment), size, - )], + ) + }, ) } diff --git a/widget/src/image.rs b/widget/src/image.rs index 6750c1b3..e906ac13 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -99,7 +99,7 @@ where }; // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(image_size, width, height); + let raw_size = limits.resolve(width, height, image_size); // The uncropped size of the image when fit to the bounds above let full_size = content_fit.fit(image_size, raw_size); diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index dc910f1f..98080577 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -113,9 +113,9 @@ where let Size { width, height } = renderer.dimensions(&self.handle); let mut size = limits.resolve( - Size::new(width as f32, height as f32), self.width, self.height, + Size::new(width as f32, height as f32), ); let expansion_size = if height > width { diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b9e06de8..f83eebea 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -369,7 +369,7 @@ where * self.options.len() as f32, ); - limits.resolve(intrinsic, Length::Fill, Length::Shrink) + limits.resolve(Length::Fill, Length::Shrink, intrinsic) }; layout::Node::new(size) diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 36c785b7..cf1f0455 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -489,7 +489,7 @@ pub fn layout<Renderer, T>( &layout::Limits, ) -> layout::Node, ) -> layout::Node { - let size = limits.resolve(Size::ZERO, width, height); + let size = limits.resolve(width, height, Size::ZERO); let regions = node.pane_regions(spacing, size); let children = contents diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index d83b0624..2576a1e8 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -392,7 +392,6 @@ where { use std::f32; - let limits = limits.width(width).height(Length::Shrink); let font = font.unwrap_or_else(|| renderer.default_font()); let text_size = text_size.unwrap_or_else(|| renderer.default_size()); @@ -451,8 +450,10 @@ where ); limits + .width(width) + .height(Length::Shrink) .shrink(padding) - .resolve(intrinsic, width, Length::Shrink) + .resolve(width, Length::Shrink, intrinsic) .expand(padding) }; diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index a05923a2..15f1277b 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -98,13 +98,11 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.resolve( - Size::ZERO, + layout::atomic( + limits, self.width, self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), - ); - - layout::Node::new(size) + ) } fn draw( diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 4ab16c40..cded9cb1 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -75,7 +75,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::Node::new(limits.resolve(Size::ZERO, self.width, self.height)) + layout::atomic(limits, self.width, self.height) } fn draw( diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5197afde..70db490a 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -469,28 +469,25 @@ pub fn layout<Renderer>( direction: &Direction, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.width(width).height(height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); - - let content = layout_content(renderer, &child_limits); - let size = limits.resolve(content.size(), width, height); + layout::contained(limits, width, height, |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); - layout::Node::with_children(size, vec![content]) + layout_content(renderer, &child_limits) + }) } /// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] diff --git a/widget/src/shader.rs b/widget/src/shader.rs index 82432c6c..16b68c55 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -83,10 +83,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO, self.width, self.height); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 27588852..1bc94661 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -172,9 +172,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.resolve(Size::ZERO, self.width, self.height); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( diff --git a/widget/src/space.rs b/widget/src/space.rs index 9fd4dcb9..eef990d1 100644 --- a/widget/src/space.rs +++ b/widget/src/space.rs @@ -58,7 +58,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::Node::new(limits.resolve(Size::ZERO, self.width, self.height)) + layout::atomic(limits, self.width, self.height) } fn draw( diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 75ab238a..830abb0f 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -114,7 +114,7 @@ where let image_size = Size::new(width as f32, height as f32); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(image_size, self.width, self.height); + let raw_size = limits.resolve(self.width, self.height, image_size); // The uncropped size of the image when fit to the bounds above let full_size = self.content_fit.fit(image_size, raw_size); diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 7e91105c..d8540658 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -508,8 +508,8 @@ where let padding = padding.fit(Size::ZERO, limits.max()); let height = line_height.to_absolute(text_size); - let limits = limits.width(width).shrink(padding).height(height); - let text_bounds = limits.resolve(Size::ZERO, width, height); + let limits = limits.width(width).shrink(padding); + let text_bounds = limits.resolve(width, height, Size::ZERO); let placeholder_text = Text { font, diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 35bc2fe2..a3029d76 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -169,9 +169,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let size = limits.resolve(Size::ZERO, self.width, self.height); - - layout::Node::new(size) + layout::atomic(limits, self.width, self.height) } fn on_event( -- cgit From e710e7694907fe320e0a849e880c51952e6e748f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 06:44:15 +0100 Subject: Fix `size_hint` for `keyed_column` --- examples/todos/src/main.rs | 9 +-------- widget/src/helpers.rs | 1 + widget/src/keyed/column.rs | 45 +++++++++++++++++++++++++-------------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 4dac032c..aad47c20 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -254,13 +254,7 @@ impl Application for Todos { .spacing(20) .max_width(800); - scrollable( - container(content) - .width(Length::Fill) - .padding(40) - .center_x(), - ) - .into() + scrollable(container(content).padding(40).center_x()).into() } } } @@ -472,7 +466,6 @@ fn empty_message(message: &str) -> Element<'_, Message> { .horizontal_alignment(alignment::Horizontal::Center) .style(Color::from([0.7, 0.7, 0.7])), ) - .width(Length::Fill) .height(200) .center_y() .into() diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 6eaf3392..75528a0c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -80,6 +80,7 @@ pub fn keyed_column<'a, Key, Message, Renderer>( ) -> keyed::Column<'a, Key, Message, Renderer> where Key: Copy + PartialEq, + Renderer: core::Renderer, { keyed::Column::with_children(children) } diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index 32320300..7f05a81e 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -30,26 +30,10 @@ where impl<'a, Key, Message, Renderer> Column<'a, Key, Message, Renderer> where Key: Copy + PartialEq, + Renderer: crate::core::Renderer, { /// Creates an empty [`Column`]. pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>, - ) -> Self { - let (keys, children) = children.into_iter().fold( - (Vec::new(), Vec::new()), - |(mut keys, mut children), (key, child)| { - keys.push(key); - children.push(child); - - (keys, children) - }, - ); - Column { spacing: 0.0, padding: Padding::ZERO, @@ -57,11 +41,20 @@ where height: Length::Shrink, max_width: f32::INFINITY, align_items: Alignment::Start, - keys, - children, + keys: Vec::new(), + children: Vec::new(), } } + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator<Item = (Key, Element<'a, Message, Renderer>)>, + ) -> Self { + children + .into_iter() + .fold(Self::new(), |column, (key, child)| column.push(key, child)) + } + /// Sets the vertical spacing _between_ elements. /// /// Custom margins per element do not exist in iced. You should use this @@ -108,8 +101,19 @@ where key: Key, child: impl Into<Element<'a, Message, Renderer>>, ) -> Self { + let child = child.into(); + let size = child.as_widget().size_hint(); + + if size.width.is_fill() { + self.width = Length::Fill; + } + + if size.height.is_fill() { + self.height = Length::Fill; + } + self.keys.push(key); - self.children.push(child.into()); + self.children.push(child); self } } @@ -117,6 +121,7 @@ where impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> where Key: Copy + PartialEq, + Renderer: crate::core::Renderer, { fn default() -> Self { Self::new() -- cgit From 67277fbf93f4c180eff67bdc4c9dcf84a54d3425 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 06:46:49 +0100 Subject: Make `column` and `row` take an `IntoIterator` --- widget/src/column.rs | 4 ++-- widget/src/helpers.rs | 8 ++++---- widget/src/row.rs | 6 ++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/widget/src/column.rs b/widget/src/column.rs index 9867d97e..d6eea84b 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -41,9 +41,9 @@ where /// Creates a [`Column`] with the given elements. pub fn with_children( - children: impl Iterator<Item = Element<'a, Message, Renderer>>, + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, ) -> Self { - children.fold(Self::new(), |column, element| column.push(element)) + children.into_iter().fold(Self::new(), Self::push) } /// Sets the vertical spacing _between_ elements. diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 75528a0c..4b988ae3 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -34,7 +34,7 @@ macro_rules! column { $crate::Column::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Column::with_children([$($crate::core::Element::from($x)),+].into_iter()) + $crate::Column::with_children([$($crate::core::Element::from($x)),+]) ); } @@ -47,7 +47,7 @@ macro_rules! row { $crate::Row::new() ); ($($x:expr),+ $(,)?) => ( - $crate::Row::with_children([$($crate::core::Element::from($x)),+].into_iter()) + $crate::Row::with_children([$($crate::core::Element::from($x)),+]) ); } @@ -66,7 +66,7 @@ where /// Creates a new [`Column`] with the given children. pub fn column<'a, Message, Renderer>( - children: impl Iterator<Item = Element<'a, Message, Renderer>>, + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, ) -> Column<'a, Message, Renderer> where Renderer: core::Renderer, @@ -89,7 +89,7 @@ where /// /// [`Row`]: crate::Row pub fn row<'a, Message, Renderer>( - children: impl Iterator<Item = Element<'a, Message, Renderer>>, + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, ) -> Row<'a, Message, Renderer> where Renderer: core::Renderer, diff --git a/widget/src/row.rs b/widget/src/row.rs index bcbe9267..90fd2926 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -39,11 +39,9 @@ where /// Creates a [`Row`] with the given elements. pub fn with_children( - children: impl Iterator<Item = Element<'a, Message, Renderer>>, + children: impl IntoIterator<Item = Element<'a, Message, Renderer>>, ) -> Self { - children - .into_iter() - .fold(Self::new(), |column, element| column.push(element)) + children.into_iter().fold(Self::new(), Self::push) } /// Sets the horizontal spacing _between_ elements. -- cgit From ecf571dfeb033f3768fccfb06bc9380e59281df3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 06:47:52 +0100 Subject: Fix unnecessary `into` call in `Container::new` --- widget/src/container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index c98de41c..ecc5c651 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -67,7 +67,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, style: Default::default(), - content: content.into(), + content, } } -- cgit From 025064c9e028ea65cc0c6ff236d42e9861efdda9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 07:01:11 +0100 Subject: Fix broken doc links in `layout::Node` API --- core/src/layout/node.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 40c71436..5743a9bd 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -60,7 +60,7 @@ impl Node { self } - /// Mutable reference version of [`align`]. + /// Mutable reference version of [`Self::align`]. pub fn align_mut( &mut self, horizontal_alignment: Alignment, @@ -94,7 +94,7 @@ impl Node { self } - /// Mutable reference version of [`move_to`]. + /// Mutable reference version of [`Self::move_to`]. pub fn move_to_mut(&mut self, position: impl Into<Point>) { let position = position.into(); -- cgit From 88f8c343fa7d69203ab98bb7abc85fe002014422 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 9 Jan 2024 07:15:57 +0100 Subject: Fix `cross` calculation in `layout::flex` --- core/src/layout/flex.rs | 27 +++++++++++++-------------- examples/pick_list/src/main.rs | 10 +++------- widget/src/pick_list.rs | 1 - 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index cf3e1340..47cd7112 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -79,7 +79,17 @@ where let max_cross = axis.cross(limits.max()); let mut fill_main_sum = 0; - let mut cross = 0.0f32; + let mut cross = match axis { + Axis::Horizontal => match height { + Length::Shrink => 0.0, + _ => max_cross, + }, + Axis::Vertical => match width { + Length::Shrink => 0.0, + _ => max_cross, + }, + }; + let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec<Node> = Vec::with_capacity(items.len()); @@ -113,17 +123,6 @@ where } } - let intrinsic_cross = match axis { - Axis::Horizontal => match height { - Length::Shrink => cross, - _ => max_cross, - }, - Axis::Vertical => match width { - Length::Shrink => cross, - _ => max_cross, - }, - }; - for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() { let (fill_main_factor, fill_cross_factor) = { let size = child.as_widget().size(); @@ -132,7 +131,7 @@ where }; if fill_main_factor == 0 && fill_cross_factor != 0 { - let (max_width, max_height) = axis.pack(available, intrinsic_cross); + let (max_width, max_height) = axis.pack(available, cross); let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); @@ -182,7 +181,7 @@ where let max_cross = if fill_cross_factor == 0 { max_cross } else { - intrinsic_cross + cross }; let (min_width, min_height) = diff --git a/examples/pick_list/src/main.rs b/examples/pick_list/src/main.rs index bfd642f5..e4d96dc8 100644 --- a/examples/pick_list/src/main.rs +++ b/examples/pick_list/src/main.rs @@ -1,4 +1,4 @@ -use iced::widget::{column, container, pick_list, scrollable, vertical_space}; +use iced::widget::{column, pick_list, scrollable, vertical_space}; use iced::{Alignment, Element, Length, Sandbox, Settings}; pub fn main() -> iced::Result { @@ -48,15 +48,11 @@ impl Sandbox for Example { pick_list, vertical_space(600), ] + .width(Length::Fill) .align_items(Alignment::Center) .spacing(10); - container(scrollable(content)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + scrollable(content).into() } } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 2576a1e8..9f6a371a 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -451,7 +451,6 @@ where limits .width(width) - .height(Length::Shrink) .shrink(padding) .resolve(width, Length::Shrink, intrinsic) .expand(padding) -- cgit From a79b2adf5c3e345667341451a4aaaa14fc9bfe80 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 02:16:29 +0100 Subject: Use first-class functions in `layout` example --- examples/layout/src/main.rs | 143 +++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 60 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index eeaa76b6..d4d81617 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,4 +1,5 @@ use iced::executor; +use iced::keyboard; use iced::widget::{column, container, row, text, vertical_rule}; use iced::{ Application, Command, Element, Length, Settings, Subscription, Theme, @@ -10,9 +11,7 @@ pub fn main() -> iced::Result { #[derive(Debug)] struct Layout { - previous: Vec<Example>, - current: Example, - next: Vec<Example>, + example: Example, } #[derive(Debug, Clone, Copy)] @@ -30,36 +29,23 @@ impl Application for Layout { fn new(_flags: Self::Flags) -> (Self, Command<Message>) { ( Self { - previous: vec![], - current: Example::Centered, - next: vec![Example::NestedQuotes], + example: Example::default(), }, Command::none(), ) } fn title(&self) -> String { - String::from("Counter - Iced") + format!("{} - Layout - Iced", self.example.title) } fn update(&mut self, message: Self::Message) -> Command<Message> { match message { Message::Next => { - if !self.next.is_empty() { - let previous = std::mem::replace( - &mut self.current, - self.next.remove(0), - ); - - self.previous.push(previous); - } + self.example = self.example.next(); } Message::Previous => { - if let Some(previous) = self.previous.pop() { - let next = std::mem::replace(&mut self.current, previous); - - self.next.insert(0, next); - } + self.example = self.example.previous(); } } @@ -67,57 +53,94 @@ impl Application for Layout { } fn subscription(&self) -> Subscription<Message> { - use iced::event::{self, Event}; - use iced::keyboard; - - event::listen_with(|event, status| match event { - Event::Keyboard(keyboard::Event::KeyReleased { - key_code, .. - }) if status == event::Status::Ignored => match key_code { - keyboard::KeyCode::Left => Some(Message::Previous), - keyboard::KeyCode::Right => Some(Message::Next), - _ => None, - }, + keyboard::on_key_release(|key_code, _modifiers| match key_code { + keyboard::KeyCode::Left => Some(Message::Previous), + keyboard::KeyCode::Right => Some(Message::Next), _ => None, }) } fn view(&self) -> Element<Message> { - self.current.view() + self.example.view() } } -#[derive(Debug)] -enum Example { - Centered, - NestedQuotes, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Example { + title: &'static str, + view: fn() -> Element<'static, Message>, } impl Example { + const LIST: &'static [Self] = &[ + Self { + title: "Centered", + view: centered, + }, + Self { + title: "Nested Quotes", + view: nested_quotes, + }, + ]; + + fn previous(self) -> Self { + let Some(index) = + Self::LIST.iter().position(|&example| example == self) + else { + return self; + }; + + Self::LIST + .get(index.saturating_sub(1)) + .copied() + .unwrap_or(self) + } + + fn next(self) -> Self { + let Some(index) = + Self::LIST.iter().position(|&example| example == self) + else { + return self; + }; + + Self::LIST.get(index + 1).copied().unwrap_or(self) + } + fn view(&self) -> Element<Message> { - match self { - Self::Centered => container(text("I am centered!").size(50)) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into(), - Self::NestedQuotes => container((1..5).fold( - column![text("Original text")].padding(10), - |quotes, i| { - column![ - row![vertical_rule(2), quotes].height(Length::Shrink), - text(format!("Reply {i}")) - ] - .spacing(10) - .padding(10) - }, - )) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into(), - } + (self.view)() + } +} + +impl Default for Example { + fn default() -> Self { + Self::LIST[0] } } + +fn centered<'a>() -> Element<'a, Message> { + container(text("I am centered!").size(50)) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() +} + +fn nested_quotes<'a>() -> Element<'a, Message> { + container((1..5).fold( + column![text("Original text")].padding(10), + |quotes, i| { + column![ + row![vertical_rule(2), quotes].height(Length::Shrink), + text(format!("Reply {i}")) + ] + .spacing(10) + .padding(10) + }, + )) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() +} -- cgit From 81ecc4a67f7982c6300a4d5e8ec4e8aac8cbd881 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 02:58:40 +0100 Subject: Add basic controls to `layout` example --- examples/layout/src/main.rs | 67 +++++++++++++++++++++++++++++++++++---------- style/src/container.rs | 26 +++++++++++++++++- style/src/theme.rs | 6 ++++ 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index d4d81617..6d02434d 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,8 +1,11 @@ use iced::executor; use iced::keyboard; -use iced::widget::{column, container, row, text, vertical_rule}; +use iced::widget::{ + button, column, container, horizontal_space, row, text, vertical_rule, +}; use iced::{ - Application, Command, Element, Length, Settings, Subscription, Theme, + color, Application, Color, Command, Element, Length, Settings, + Subscription, Theme, }; pub fn main() -> iced::Result { @@ -61,7 +64,29 @@ impl Application for Layout { } fn view(&self) -> Element<Message> { - self.example.view() + let example = container(self.example.view()).style( + container::Appearance::default().with_border(Color::BLACK, 2.0), + ); + + let controls = row([ + (!self.example.is_first()).then_some( + button("← Previous") + .padding([5, 10]) + .on_press(Message::Previous) + .into(), + ), + Some(horizontal_space(Length::Fill).into()), + (!self.example.is_last()).then_some( + button("Next →") + .padding([5, 10]) + .on_press(Message::Next) + .into(), + ), + ] + .into_iter() + .filter_map(std::convert::identity)); + + column![example, controls].spacing(10).padding(20).into() } } @@ -83,6 +108,14 @@ impl Example { }, ]; + fn is_first(self) -> bool { + Self::LIST.first() == Some(&self) + } + + fn is_last(self) -> bool { + Self::LIST.last() == Some(&self) + } + fn previous(self) -> Self { let Some(index) = Self::LIST.iter().position(|&example| example == self) @@ -127,20 +160,26 @@ fn centered<'a>() -> Element<'a, Message> { } fn nested_quotes<'a>() -> Element<'a, Message> { - container((1..5).fold( - column![text("Original text")].padding(10), - |quotes, i| { + let quotes = + (1..5).fold(column![text("Original text")].padding(10), |quotes, i| { column![ - row![vertical_rule(2), quotes].height(Length::Shrink), + container( + row![vertical_rule(2), quotes].height(Length::Shrink) + ) + .style( + container::Appearance::default() + .with_background(color!(0x000000, 0.05)) + ), text(format!("Reply {i}")) ] .spacing(10) .padding(10) - }, - )) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + }); + + container(quotes) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() } diff --git a/style/src/container.rs b/style/src/container.rs index ec543ae4..490a9dab 100644 --- a/style/src/container.rs +++ b/style/src/container.rs @@ -1,5 +1,5 @@ //! Change the appearance of a container. -use iced_core::{Background, BorderRadius, Color}; +use crate::core::{Background, BorderRadius, Color, Pixels}; /// The appearance of a container. #[derive(Debug, Clone, Copy)] @@ -16,6 +16,30 @@ pub struct Appearance { pub border_color: Color, } +impl Appearance { + /// Derives a new [`Appearance`] with a border of the given [`Color`] and + /// `width`. + pub fn with_border( + self, + color: impl Into<Color>, + width: impl Into<Pixels>, + ) -> Self { + Self { + border_color: color.into(), + border_width: width.into().0, + ..self + } + } + + /// Derives a new [`Appearance`] with the given [`Background`]. + pub fn with_background(self, background: impl Into<Background>) -> Self { + Self { + background: Some(background.into()), + ..self + } + } +} + impl std::default::Default for Appearance { fn default() -> Self { Self { diff --git a/style/src/theme.rs b/style/src/theme.rs index 47010728..eafb0b47 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -383,6 +383,12 @@ pub enum Container { Custom(Box<dyn container::StyleSheet<Style = Theme>>), } +impl From<container::Appearance> for Container { + fn from(appearance: container::Appearance) -> Self { + Self::Custom(Box::new(move |_: &_| appearance)) + } +} + impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container { fn from(f: T) -> Self { Self::Custom(Box::new(f)) -- cgit From 5dbded61dea19f77eb370e08e72acfa20ffd1a86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 03:07:10 +0100 Subject: Use `flatten` instead of `filter_map` in `layout` example --- examples/layout/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 6d02434d..448d2995 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -84,7 +84,7 @@ impl Application for Layout { ), ] .into_iter() - .filter_map(std::convert::identity)); + .flatten()); column![example, controls].spacing(10).padding(20).into() } -- cgit From d76705df29f1960124bd06277683448e18f788b0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 03:56:39 +0100 Subject: Add `explain` toggle to `layout` example --- examples/layout/src/main.rs | 64 +++++++++++++++++++++++++++++++++++++-------- style/src/theme/palette.rs | 3 +++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 448d2995..e23b2218 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,11 +1,12 @@ use iced::executor; use iced::keyboard; use iced::widget::{ - button, column, container, horizontal_space, row, text, vertical_rule, + button, checkbox, column, container, horizontal_space, row, text, + vertical_rule, }; use iced::{ - color, Application, Color, Command, Element, Length, Settings, - Subscription, Theme, + color, Alignment, Application, Color, Command, Element, Font, Length, + Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -15,12 +16,14 @@ pub fn main() -> iced::Result { #[derive(Debug)] struct Layout { example: Example, + explain: bool, } #[derive(Debug, Clone, Copy)] enum Message { Next, Previous, + ExplainToggled(bool), } impl Application for Layout { @@ -33,6 +36,7 @@ impl Application for Layout { ( Self { example: Example::default(), + explain: false, }, Command::none(), ) @@ -50,6 +54,9 @@ impl Application for Layout { Message::Previous => { self.example = self.example.previous(); } + Message::ExplainToggled(explain) => { + self.explain = explain; + } } Command::none() @@ -64,9 +71,24 @@ impl Application for Layout { } fn view(&self) -> Element<Message> { - let example = container(self.example.view()).style( - container::Appearance::default().with_border(Color::BLACK, 2.0), - ); + let header = row![ + text(self.example.title).size(20).font(Font::MONOSPACE), + horizontal_space(Length::Fill), + checkbox("Explain", self.explain, Message::ExplainToggled), + ] + .align_items(Alignment::Center); + + let example = container(if self.explain { + self.example.view().explain(color!(0x0000ff)) + } else { + self.example.view() + }) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Appearance::default() + .with_border(palette.background.strong.color, 4.0) + }); let controls = row([ (!self.example.is_first()).then_some( @@ -86,7 +108,14 @@ impl Application for Layout { .into_iter() .flatten()); - column![example, controls].spacing(10).padding(20).into() + column![header, example, controls] + .spacing(10) + .padding(20) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark } } @@ -166,10 +195,23 @@ fn nested_quotes<'a>() -> Element<'a, Message> { container( row![vertical_rule(2), quotes].height(Length::Shrink) ) - .style( - container::Appearance::default() - .with_background(color!(0x000000, 0.05)) - ), + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Appearance::default().with_background( + if palette.is_dark { + Color { + a: 0.01, + ..Color::WHITE + } + } else { + Color { + a: 0.08, + ..Color::BLACK + } + }, + ) + }), text(format!("Reply {i}")) ] .spacing(10) diff --git a/style/src/theme/palette.rs b/style/src/theme/palette.rs index aaeb799d..76977a29 100644 --- a/style/src/theme/palette.rs +++ b/style/src/theme/palette.rs @@ -82,6 +82,8 @@ pub struct Extended { pub success: Success, /// The set of danger colors. pub danger: Danger, + /// Whether the palette is dark or not. + pub is_dark: bool, } /// The built-in light variant of an [`Extended`] palette. @@ -113,6 +115,7 @@ impl Extended { palette.background, palette.text, ), + is_dark: is_dark(palette.background), } } } -- cgit From 3850a46db6e13f2948f5731f4ceec42764391f5d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 08:15:05 +0100 Subject: Add `Theme` selector to `layout` example --- examples/layout/src/main.rs | 20 ++++++++++++++++---- examples/styling/src/main.rs | 17 ++++++++++------- style/src/theme.rs | 36 ++++++++++++++++++++++++++++++------ widget/src/helpers.rs | 2 +- widget/src/pick_list.rs | 6 +++--- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index e23b2218..c1ff3951 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,8 +1,8 @@ use iced::executor; use iced::keyboard; use iced::widget::{ - button, checkbox, column, container, horizontal_space, row, text, - vertical_rule, + button, checkbox, column, container, horizontal_space, pick_list, row, + text, vertical_rule, }; use iced::{ color, Alignment, Application, Color, Command, Element, Font, Length, @@ -17,13 +17,15 @@ pub fn main() -> iced::Result { struct Layout { example: Example, explain: bool, + theme: Theme, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] enum Message { Next, Previous, ExplainToggled(bool), + ThemeSelected(Theme), } impl Application for Layout { @@ -37,6 +39,7 @@ impl Application for Layout { Self { example: Example::default(), explain: false, + theme: Theme::Light, }, Command::none(), ) @@ -57,6 +60,9 @@ impl Application for Layout { Message::ExplainToggled(explain) => { self.explain = explain; } + Message::ThemeSelected(theme) => { + self.theme = theme; + } } Command::none() @@ -75,7 +81,13 @@ impl Application for Layout { text(self.example.title).size(20).font(Font::MONOSPACE), horizontal_space(Length::Fill), checkbox("Explain", self.explain, Message::ExplainToggled), + pick_list( + Theme::ALL, + Some(self.theme.clone()), + Message::ThemeSelected + ), ] + .spacing(20) .align_items(Alignment::Center); let example = container(if self.explain { @@ -115,7 +127,7 @@ impl Application for Layout { } fn theme(&self) -> Theme { - Theme::Dark + self.theme.clone() } } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index f14f6a8f..10f3c79d 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -53,13 +53,16 @@ impl Sandbox for Styling { self.theme = match theme { ThemeType::Light => Theme::Light, ThemeType::Dark => Theme::Dark, - ThemeType::Custom => Theme::custom(theme::Palette { - background: Color::from_rgb(1.0, 0.9, 1.0), - text: Color::BLACK, - primary: Color::from_rgb(0.5, 0.5, 0.0), - success: Color::from_rgb(0.0, 1.0, 0.0), - danger: Color::from_rgb(1.0, 0.0, 0.0), - }), + ThemeType::Custom => Theme::custom( + String::from("Custom"), + theme::Palette { + background: Color::from_rgb(1.0, 0.9, 1.0), + text: Color::BLACK, + primary: Color::from_rgb(0.5, 0.5, 0.0), + success: Color::from_rgb(0.0, 1.0, 0.0), + danger: Color::from_rgb(1.0, 0.0, 0.0), + }, + ), } } Message::InputChanged(value) => self.input_value = value, diff --git a/style/src/theme.rs b/style/src/theme.rs index eafb0b47..deccf455 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -23,6 +23,7 @@ use crate::toggler; use iced_core::{Background, Color, Vector}; +use std::fmt; use std::rc::Rc; /// A built-in theme. @@ -38,18 +39,22 @@ pub enum Theme { } impl Theme { + /// A list with all the defined themes. + pub const ALL: &'static [Self] = &[Self::Light, Self::Dark]; + /// Creates a new custom [`Theme`] from the given [`Palette`]. - pub fn custom(palette: Palette) -> Self { - Self::custom_with_fn(palette, palette::Extended::generate) + pub fn custom(name: String, palette: Palette) -> Self { + Self::custom_with_fn(name, palette, palette::Extended::generate) } /// Creates a new custom [`Theme`] from the given [`Palette`], with /// a custom generator of a [`palette::Extended`]. pub fn custom_with_fn( + name: String, palette: Palette, generate: impl FnOnce(Palette) -> palette::Extended, ) -> Self { - Self::Custom(Box::new(Custom::with_fn(palette, generate))) + Self::Custom(Box::new(Custom::with_fn(name, palette, generate))) } /// Returns the [`Palette`] of the [`Theme`]. @@ -71,32 +76,51 @@ impl Theme { } } +impl fmt::Display for Theme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Light => write!(f, "Light"), + Self::Dark => write!(f, "Dark"), + Self::Custom(custom) => custom.fmt(f), + } + } +} + /// A [`Theme`] with a customized [`Palette`]. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Custom { + name: String, palette: Palette, extended: palette::Extended, } impl Custom { /// Creates a [`Custom`] theme from the given [`Palette`]. - pub fn new(palette: Palette) -> Self { - Self::with_fn(palette, palette::Extended::generate) + pub fn new(name: String, palette: Palette) -> Self { + Self::with_fn(name, palette, palette::Extended::generate) } /// Creates a [`Custom`] theme from the given [`Palette`] with /// a custom generator of a [`palette::Extended`]. pub fn with_fn( + name: String, palette: Palette, generate: impl FnOnce(Palette) -> palette::Extended, ) -> Self { Self { + name, palette, extended: generate(palette), } } } +impl fmt::Display for Custom { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + /// The style of an application. #[derive(Default)] pub enum Application { diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 4b988ae3..498dd76c 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -271,7 +271,7 @@ pub fn pick_list<'a, Message, Renderer, T>( on_selected: impl Fn(T) -> Message + 'a, ) -> PickList<'a, T, Message, Renderer> where - T: ToString + Eq + 'static, + T: ToString + PartialEq + 'static, [T]: ToOwned<Owned = Vec<T>>, Renderer: core::text::Renderer, Renderer::Theme: pick_list::StyleSheet diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 9f6a371a..2e3aab6f 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -45,7 +45,7 @@ where impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> where - T: ToString + Eq, + T: ToString + PartialEq, [T]: ToOwned<Owned = Vec<T>>, Renderer: text::Renderer, Renderer::Theme: StyleSheet @@ -145,7 +145,7 @@ where impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> for PickList<'a, T, Message, Renderer> where - T: Clone + ToString + Eq + 'static, + T: Clone + ToString + PartialEq + 'static, [T]: ToOwned<Owned = Vec<T>>, Message: 'a, Renderer: text::Renderer + 'a, @@ -281,7 +281,7 @@ where impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>> for Element<'a, Message, Renderer> where - T: Clone + ToString + Eq + 'static, + T: Clone + ToString + PartialEq + 'static, [T]: ToOwned<Owned = Vec<T>>, Message: 'a, Renderer: text::Renderer + 'a, -- cgit From a6cbc365037d740ee9bb8d21fffe361cd198477e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 09:01:01 +0100 Subject: Showcase more layouts in `layout` example --- examples/layout/Cargo.toml | 2 +- examples/layout/src/main.rs | 144 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 132 insertions(+), 14 deletions(-) diff --git a/examples/layout/Cargo.toml b/examples/layout/Cargo.toml index c2c3f49b..855f98d0 100644 --- a/examples/layout/Cargo.toml +++ b/examples/layout/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["canvas"] } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index c1ff3951..3e69e1a8 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,12 +1,14 @@ use iced::executor; use iced::keyboard; +use iced::mouse; +use iced::theme; use iced::widget::{ - button, checkbox, column, container, horizontal_space, pick_list, row, - text, vertical_rule, + button, canvas, checkbox, column, container, horizontal_space, pick_list, + row, scrollable, text, vertical_rule, vertical_space, }; use iced::{ color, Alignment, Application, Color, Command, Element, Font, Length, - Settings, Subscription, Theme, + Point, Rectangle, Renderer, Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -100,7 +102,12 @@ impl Application for Layout { container::Appearance::default() .with_border(palette.background.strong.color, 4.0) - }); + }) + .padding(4) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(); let controls = row([ (!self.example.is_first()).then_some( @@ -143,6 +150,22 @@ impl Example { title: "Centered", view: centered, }, + Self { + title: "Column", + view: column_, + }, + Self { + title: "Row", + view: row_, + }, + Self { + title: "Space", + view: space, + }, + Self { + title: "Application", + view: application, + }, Self { title: "Nested Quotes", view: nested_quotes, @@ -200,9 +223,79 @@ fn centered<'a>() -> Element<'a, Message> { .into() } +fn column_<'a>() -> Element<'a, Message> { + column![ + "A column can be used to", + "lay out widgets vertically.", + square(50), + square(50), + square(50), + "The amount of space between", + "elements can be configured!", + ] + .spacing(40) + .into() +} + +fn row_<'a>() -> Element<'a, Message> { + row![ + "A row works like a column...", + square(50), + square(50), + square(50), + "but lays out widgets horizontally!", + ] + .spacing(40) + .into() +} + +fn space<'a>() -> Element<'a, Message> { + row!["Left!", horizontal_space(Length::Fill), "Right!"].into() +} + +fn application<'a>() -> Element<'a, Message> { + let header = container( + row![ + square(40), + horizontal_space(Length::Fill), + "Header!", + horizontal_space(Length::Fill), + square(40), + ] + .padding(10) + .align_items(Alignment::Center), + ) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Appearance::default() + .with_border(palette.background.strong.color, 1) + }); + + let sidebar = container( + column!["Sidebar!", square(50), square(50)] + .spacing(40) + .padding(10) + .width(200) + .align_items(Alignment::Center), + ) + .style(theme::Container::Box) + .height(Length::Fill) + .center_y(); + + let content = container( + scrollable(column!["Content!", vertical_space(2000), "The end"]) + .width(Length::Fill) + .height(Length::Fill), + ) + .padding(10); + + column![header, row![sidebar, content]].into() +} + fn nested_quotes<'a>() -> Element<'a, Message> { - let quotes = - (1..5).fold(column![text("Original text")].padding(10), |quotes, i| { + (1..5) + .fold(column![text("Original text")].padding(10), |quotes, i| { column![ container( row![vertical_rule(2), quotes].height(Length::Shrink) @@ -228,12 +321,37 @@ fn nested_quotes<'a>() -> Element<'a, Message> { ] .spacing(10) .padding(10) - }); - - container(quotes) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() + }) .into() } + +fn square<'a>(size: impl Into<Length> + Copy) -> Element<'a, Message> { + struct Square; + + impl canvas::Program<Message> for Square { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec<canvas::Geometry> { + let mut frame = canvas::Frame::new(renderer, bounds.size()); + + let palette = theme.extended_palette(); + + frame.fill_rectangle( + Point::ORIGIN, + bounds.size(), + palette.background.strong.color, + ); + + vec![frame.into_geometry()] + } + } + + canvas(Square).width(size).height(size).into() +} -- cgit From 226271148e77a4f8966ce84b0c948c268176d92b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 10 Jan 2024 10:08:11 +0100 Subject: Use multiple squares instead of `vertical_space` in `layout` example --- examples/layout/src/main.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 3e69e1a8..60dabe54 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -4,7 +4,7 @@ use iced::mouse; use iced::theme; use iced::widget::{ button, canvas, checkbox, column, container, horizontal_space, pick_list, - row, scrollable, text, vertical_rule, vertical_space, + row, scrollable, text, vertical_rule, }; use iced::{ color, Alignment, Application, Color, Command, Element, Font, Length, @@ -284,9 +284,19 @@ fn application<'a>() -> Element<'a, Message> { .center_y(); let content = container( - scrollable(column!["Content!", vertical_space(2000), "The end"]) - .width(Length::Fill) - .height(Length::Fill), + scrollable( + column![ + "Content!", + square(400), + square(200), + square(400), + "The end" + ] + .spacing(40) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .height(Length::Fill), ) .padding(10); -- cgit From fa53d9adbb0efbbe806a749476f83c04f756be75 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 11 Jan 2024 06:11:36 +0100 Subject: Loosen cross axis constraint for main axis fills in `flex` layout --- core/src/layout/flex.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index 47cd7112..3358ef3d 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -166,13 +166,10 @@ where }; if fill_main_factor != 0 { - let max_main = if fill_main_factor == 0 { - available.max(0.0) - } else { - remaining * fill_main_factor as f32 / fill_main_sum as f32 - }; + let max_main = + remaining * fill_main_factor as f32 / fill_main_sum as f32; - let min_main = if fill_main_factor == 0 || max_main.is_infinite() { + let min_main = if max_main.is_infinite() { 0.0 } else { max_main @@ -184,9 +181,7 @@ where cross }; - let (min_width, min_height) = - axis.pack(min_main, axis.cross(limits.min())); - + let (min_width, min_height) = axis.pack(min_main, 0.0); let (max_width, max_height) = axis.pack(max_main, max_cross); let child_limits = Limits::new( -- cgit From 03c901d49b7cce901cfd76100f08dcff31420af8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 11 Jan 2024 06:12:19 +0100 Subject: Make `Button` sizing strategy adaptive --- core/src/length.rs | 12 ++++++++++++ widget/src/button.rs | 9 ++++++--- widget/src/container.rs | 12 ++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/core/src/length.rs b/core/src/length.rs index 6dc15049..4c139895 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -42,6 +42,18 @@ impl Length { pub fn is_fill(&self) -> bool { self.fill_factor() != 0 } + + /// Returns the "fluid" variant of the [`Length`]. + /// + /// Specifically: + /// - [`Length::Shrink`] if [`Length::Shrink`] or [`Length::Fixed`]. + /// - [`Length::Fill`] otherwise. + pub fn fluid(&self) -> Length { + match self { + Length::Fill | Length::FillPortion(_) => Length::Fill, + Length::Shrink | Length::Fixed(_) => Length::Shrink, + } + } } impl From<Pixels> for Length { diff --git a/widget/src/button.rs b/widget/src/button.rs index 86abee77..0ebb8dcc 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -71,11 +71,14 @@ where { /// Creates a new [`Button`] with the given content. pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + let content = content.into(); + let size = content.as_widget().size_hint(); + Button { - content: content.into(), + content, on_press: None, - width: Length::Shrink, - height: Length::Shrink, + width: size.width.fluid(), + height: size.height.fluid(), padding: Padding::new(5.0), style: <Renderer::Theme as StyleSheet>::Style::default(), } diff --git a/widget/src/container.rs b/widget/src/container.rs index ecc5c651..cffb0458 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -52,16 +52,8 @@ where Container { id: None, padding: Padding::ZERO, - width: if size.width.is_fill() { - Length::Fill - } else { - Length::Shrink - }, - height: if size.height.is_fill() { - Length::Fill - } else { - Length::Shrink - }, + width: size.width.fluid(), + height: size.height.fluid(), max_width: f32::INFINITY, max_height: f32::INFINITY, horizontal_alignment: alignment::Horizontal::Left, -- cgit From 11474bdc3e1a43e6c167d7b98f22d87933dbd2b6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 11 Jan 2024 06:12:37 +0100 Subject: Fix `websocket` example --- examples/websocket/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 59488e69..38a6db1e 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -125,7 +125,7 @@ impl Application for WebSocket { let mut button = button( text("Send") - .height(Length::Fill) + .height(40) .vertical_alignment(alignment::Vertical::Center), ) .padding([0, 20]); -- cgit From 9c50a7ed7ee439b658f406da9018c9249ffa0b81 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 11 Jan 2024 08:29:44 +0100 Subject: Fix `grapheme_position` when ligatures are present --- graphics/src/text/paragraph.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 4a08a8f4..56cd8868 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -187,38 +187,43 @@ impl core::text::Paragraph for Paragraph { } fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { + use unicode_segmentation::UnicodeSegmentation; + let run = self.internal().buffer.layout_runs().nth(line)?; // index represents a grapheme, not a glyph // Let's find the first glyph for the given grapheme cluster let mut last_start = None; + let mut last_grapheme_count = 0; let mut graphemes_seen = 0; let glyph = run .glyphs .iter() .find(|glyph| { - if graphemes_seen == index { - return true; - } - if Some(glyph.start) != last_start { + last_grapheme_count = run.text[glyph.start..glyph.end] + .graphemes(false) + .count(); last_start = Some(glyph.start); - graphemes_seen += 1; + graphemes_seen += last_grapheme_count; } - false + graphemes_seen >= index }) .or_else(|| run.glyphs.last())?; - let advance_last = if index == run.glyphs.len() { - glyph.w - } else { + let advance = if index == 0 { 0.0 + } else { + glyph.w + * (1.0 + - graphemes_seen.saturating_sub(index) as f32 + / last_grapheme_count as f32) }; Some(Point::new( - glyph.x + glyph.x_offset * glyph.font_size + advance_last, + glyph.x + glyph.x_offset * glyph.font_size + advance, glyph.y - glyph.y_offset * glyph.font_size, )) } -- cgit From 3d88ceb482855549271fe73165eeea4871222048 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 11 Jan 2024 08:32:30 +0100 Subject: Avoid division by zero in `grapheme_position` --- graphics/src/text/paragraph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 56cd8868..5d027542 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -219,7 +219,7 @@ impl core::text::Paragraph for Paragraph { glyph.w * (1.0 - graphemes_seen.saturating_sub(index) as f32 - / last_grapheme_count as f32) + / last_grapheme_count.max(1) as f32) }; Some(Point::new( -- cgit From 3c6bb0a076c4433abe2a381856250c9d9693404e Mon Sep 17 00:00:00 2001 From: Tomáš Zemanovič <tzemanovic@gmail.com> Date: Thu, 11 Jan 2024 14:45:40 +0000 Subject: wgpu: require `Send` on stored pipelines --- wgpu/src/primitive/pipeline.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs index 302e38f6..c8e45458 100644 --- a/wgpu/src/primitive/pipeline.rs +++ b/wgpu/src/primitive/pipeline.rs @@ -82,7 +82,7 @@ impl<Theme> Renderer for crate::Renderer<Theme> { /// Stores custom, user-provided pipelines. #[derive(Default, Debug)] pub struct Storage { - pipelines: HashMap<TypeId, Box<dyn Any>>, + pipelines: HashMap<TypeId, Box<dyn Any + Send>>, } impl Storage { @@ -92,7 +92,7 @@ impl Storage { } /// Inserts the pipeline `T` in to [`Storage`]. - pub fn store<T: 'static>(&mut self, pipeline: T) { + pub fn store<T: 'static + Send>(&mut self, pipeline: T) { let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline)); } -- cgit From 5315e04a265190e943f42710f0b949e8af7dd37d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Fri, 12 Jan 2024 13:34:14 +0100 Subject: Fix clipping of `TextInput` selection --- widget/src/text_input.rs | 54 +++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 65d3e1eb..c4c74a67 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1194,31 +1194,39 @@ pub fn draw<Renderer>( (None, 0.0) }; - if let Some((cursor, color)) = cursor { - renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { - renderer.fill_quad(cursor, color); - }); + let draw = |renderer: &mut Renderer, viewport| { + if let Some((cursor, color)) = cursor { + renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { + renderer.fill_quad(cursor, color); + }); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); + } + + renderer.fill_paragraph( + if text.is_empty() { + &state.placeholder + } else { + &state.value + }, + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), + if text.is_empty() { + theme.placeholder_color(style) + } else if is_disabled { + theme.disabled_color(style) + } else { + theme.value_color(style) + }, + viewport, + ); + }; + + if cursor.is_some() { + renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport)); } else { - renderer.with_translation(Vector::ZERO, |_| {}); + draw(renderer, text_bounds); } - - renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, - Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), - if text.is_empty() { - theme.placeholder_color(style) - } else if is_disabled { - theme.disabled_color(style) - } else { - theme.value_color(style) - }, - text_bounds, - ); } /// Computes the current [`mouse::Interaction`] of the [`TextInput`]. -- cgit From d948ca6f0979bc8120dcf3ce7ba78eac54755ce3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 15 Jan 2024 23:45:24 +0100 Subject: Update `glyphon` to `0.4` `Color` is now always in the sRGB color space. --- Cargo.toml | 2 +- graphics/src/text.rs | 10 ++-------- tiny_skia/src/text.rs | 14 +------------- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0afbcd51..bdb6022a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,7 @@ bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = "0.10" futures = "0.3" glam = "0.24" -glyphon = { git = "https://github.com/grovesNL/glyphon.git", rev = "2caa9fc5e5923c1d827d177c3619cab7e9885b85" } +glyphon = "0.4" guillotiere = "0.6" half = "2.2" image = "0.24" diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 8fd037fe..7c4b5e31 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -9,7 +9,6 @@ pub use paragraph::Paragraph; pub use cosmic_text; -use crate::color; use crate::core::font::{self, Font}; use crate::core::text::Shaping; use crate::core::{Color, Point, Rectangle, Size}; @@ -173,12 +172,7 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { - let [r, g, b, a] = color::pack(color).components(); + let [r, g, b, a] = color.into_rgba8(); - cosmic_text::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) + cosmic_text::Color::rgba(r, g, b, a) } diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index a5a0a1b6..9413e311 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -1,7 +1,6 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; use crate::core::{Color, Font, Pixels, Point, Rectangle, Size}; -use crate::graphics::color; use crate::graphics::text::cache::{self, Cache}; use crate::graphics::text::editor; use crate::graphics::text::font_system; @@ -244,18 +243,7 @@ fn draw( fn from_color(color: cosmic_text::Color) -> Color { let [r, g, b, a] = color.as_rgba(); - if color::GAMMA_CORRECTION { - // `cosmic_text::Color` is linear RGB in this case, so we - // need to convert back to sRGB - Color::from_linear_rgba( - r as f32 / 255.0, - g as f32 / 255.0, - b as f32 / 255.0, - a as f32 / 255.0, - ) - } else { - Color::from_rgba8(r, g, b, a as f32 / 255.0) - } + Color::from_rgba8(r, g, b, a as f32 / 255.0) } #[derive(Debug, Clone, Default)] -- cgit From 73e7cf16e315cd179bf416e9051a562f7a8b648a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Mon, 15 Jan 2024 23:51:46 +0100 Subject: Update `rfd` to `0.13` --- examples/editor/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index a3f6ea3b..dc885728 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -12,4 +12,4 @@ iced.features = ["highlighter", "tokio", "debug"] tokio.workspace = true tokio.features = ["fs"] -rfd = "0.12" +rfd = "0.13" -- cgit From 17135cbd56316f31167eb62e026839450506573f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 16 Jan 2024 12:01:33 +0100 Subject: Update `winit` fork to `0.29.10` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 45d69288..9732579c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,4 +160,4 @@ web-time = "0.2" wgpu = "0.18" winapi = "0.3" window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "25b5dc1758723699015c37b0a64f16ceb9c546ea", features = ["rwh_05"] } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5", features = ["rwh_05"] } -- cgit From 64d1ce5532f55d152fa5819532a138da2dca1a39 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 16 Jan 2024 13:28:00 +0100 Subject: Refactor `KeyCode` into `Key` and `Location` --- Cargo.toml | 1 + core/Cargo.toml | 5 +- core/src/keyboard.rs | 7 +- core/src/keyboard/event.rs | 20 +- core/src/keyboard/key.rs | 744 +++++++++++++++++++++++++++++++++++++++ core/src/keyboard/key_code.rs | 203 ----------- core/src/keyboard/location.rs | 12 + examples/editor/src/main.rs | 4 +- examples/integration/src/main.rs | 2 +- examples/layout/src/main.rs | 10 +- examples/modal/src/main.rs | 5 +- examples/pane_grid/src/main.rs | 31 +- examples/screenshot/src/main.rs | 33 +- examples/stopwatch/src/main.rs | 12 +- examples/toast/src/main.rs | 5 +- examples/todos/src/main.rs | 16 +- futures/src/keyboard.rs | 17 +- src/lib.rs | 3 +- widget/src/combo_box.rs | 14 +- widget/src/text_editor.rs | 67 ++-- widget/src/text_input.rs | 54 +-- winit/src/application.rs | 2 +- winit/src/conversion.rs | 469 +++++++++++++++++------- winit/src/multi_window.rs | 2 +- 24 files changed, 1277 insertions(+), 461 deletions(-) create mode 100644 core/src/keyboard/key.rs delete mode 100644 core/src/keyboard/key_code.rs create mode 100644 core/src/keyboard/location.rs diff --git a/Cargo.toml b/Cargo.toml index d9daa3fd..ac72f212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ raw-window-handle = "0.5" resvg = "0.36" rustc-hash = "1.0" smol = "1.0" +smol_str = "0.2" softbuffer = "0.2" syntect = "5.1" sysinfo = "0.28" diff --git a/core/Cargo.toml b/core/Cargo.toml index 4baf80a9..be92a572 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -13,10 +13,11 @@ keywords.workspace = true [dependencies] bitflags.workspace = true log.workspace = true -thiserror.workspace = true -xxhash-rust.workspace = true num-traits.workspace = true +smol_str.workspace = true +thiserror.workspace = true web-time.workspace = true +xxhash-rust.workspace = true palette.workspace = true palette.optional = true diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index 4c6ca08d..b810ccb0 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,8 +1,11 @@ //! Listen to keyboard events. +pub mod key; + mod event; -mod key_code; +mod location; mod modifiers; pub use event::Event; -pub use key_code::KeyCode; +pub use key::Key; +pub use location::Location; pub use modifiers::Modifiers; diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 884fc502..b1792415 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,4 +1,4 @@ -use super::{KeyCode, Modifiers}; +use crate::keyboard::{Key, Location, Modifiers}; /// A keyboard event. /// @@ -10,10 +10,13 @@ use super::{KeyCode, Modifiers}; pub enum Event { /// A keyboard key was pressed. KeyPressed { - /// The key identifier - key_code: KeyCode, + /// The key pressed. + key: Key, - /// The state of the modifier keys + /// The location of the key. + location: Location, + + /// The state of the modifier keys. modifiers: Modifiers, /// The text produced by the key press, if any. @@ -22,10 +25,13 @@ pub enum Event { /// A keyboard key was released. KeyReleased { - /// The key identifier - key_code: KeyCode, + /// The key released. + key: Key, + + /// The location of the key. + location: Location, - /// The state of the modifier keys + /// The state of the modifier keys. modifiers: Modifiers, }, diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs new file mode 100644 index 00000000..ef48dae4 --- /dev/null +++ b/core/src/keyboard/key.rs @@ -0,0 +1,744 @@ +//! Identify keyboard keys. +use smol_str::SmolStr; + +/// A key on the keyboard. +/// +/// This is mostly the `Key` type found in [`winit`]. +/// +/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Key<C = SmolStr> { + /// A key with an established name. + Named(Named), + + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(C), + + /// An unidentified key. + Unidentified, +} + +impl Key { + /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on + /// `Key`. All other variants remain unchanged. + pub fn as_ref(&self) -> Key<&str> { + match self { + Self::Named(named) => Key::Named(*named), + Self::Character(c) => Key::Character(c.as_ref()), + Self::Unidentified => Key::Unidentified, + } + } +} + +/// A named key. +/// +/// This is mostly the `NamedKey` type found in [`winit`]. +/// +/// [`winit`]: https://docs.rs/winit/0.29.10/winit/keyboard/enum.Key.html +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[allow(missing_docs)] +pub enum Named { + /// The `Alt` (Alternative) key. + /// + /// This key enables the alternate modifier function for interpreting concurrent or subsequent + /// keyboard input. This key value is also used for the Apple <kbd>Option</kbd> key. + Alt, + /// The Alternate Graphics (<kbd>AltGr</kbd> or <kbd>AltGraph</kbd>) key. + /// + /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the + /// level 2 modifier). + AltGraph, + /// The `Caps Lock` (Capital) key. + /// + /// Toggle capital character lock function for interpreting subsequent keyboard input event. + CapsLock, + /// The `Control` or `Ctrl` key. + /// + /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard + /// input. + Control, + /// The Function switch `Fn` key. Activating this key simultaneously with another key changes + /// that key’s value to an alternate character or function. This key is often handled directly + /// in the keyboard hardware and does not usually generate key events. + Fn, + /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the + /// keyboard to changes some keys' values to an alternate character or function. This key is + /// often handled directly in the keyboard hardware and does not usually generate key events. + FnLock, + /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting + /// subsequent keyboard input. + NumLock, + /// Toggle between scrolling and cursor movement modes. + ScrollLock, + /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard + /// input. + Shift, + /// The Symbol modifier key (used on some virtual keyboards). + Symbol, + SymbolLock, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard + /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` key. + /// + /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key. + Super, + /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This key + /// value is also used for the `Return` (Macintosh numpad) key. This key value is also used for + /// the Android `KEYCODE_DPAD_CENTER`. + Enter, + /// The Horizontal Tabulation `Tab` key. + Tab, + /// Used in text to insert a space between words. Usually located below the character keys. + Space, + /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`) + ArrowDown, + /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`) + ArrowLeft, + /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`) + ArrowRight, + /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`) + ArrowUp, + /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`). + End, + /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`). + /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`]. + /// + /// [`GoHome`]: Self::GoHome + Home, + /// Scroll down or display next page of content. + PageDown, + /// Scroll up or display previous page of content. + PageUp, + /// Used to remove the character to the left of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards. + Backspace, + /// Remove the currently selected input. + Clear, + /// Copy the current selection. (`APPCOMMAND_COPY`) + Copy, + /// The Cursor Select key. + CrSel, + /// Cut the current selection. (`APPCOMMAND_CUT`) + Cut, + /// Used to delete the character to the right of the cursor. This key value is also used for the + /// key labeled `Delete` on MacOS keyboards when `Fn` is active. + Delete, + /// The Erase to End of Field key. This key deletes all characters from the current cursor + /// position to the end of the current field. + EraseEof, + /// The Extend Selection (Exsel) key. + ExSel, + /// Toggle between text modes for insertion or overtyping. + /// (`KEYCODE_INSERT`) + Insert, + /// The Paste key. (`APPCOMMAND_PASTE`) + Paste, + /// Redo the last action. (`APPCOMMAND_REDO`) + Redo, + /// Undo the last action. (`APPCOMMAND_UNDO`) + Undo, + /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion. + Accept, + /// Redo or repeat an action. + Again, + /// The Attention (Attn) key. + Attn, + Cancel, + /// Show the application’s context menu. + /// This key is commonly found between the right `Super` key and the right `Control` key. + ContextMenu, + /// The `Esc` key. This key was originally used to initiate an escape sequence, but is + /// now more generally used to exit or "escape" the current context, such as closing a dialog + /// or exiting full screen mode. + Escape, + Execute, + /// Open the Find dialog. (`APPCOMMAND_FIND`) + Find, + /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`, + /// `KEYCODE_HELP`) + Help, + /// Pause the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"` + /// instead. + Pause, + /// Play or resume the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"` + /// instead. + Play, + /// The properties (Props) key. + Props, + Select, + /// The ZoomIn key. (`KEYCODE_ZOOM_IN`) + ZoomIn, + /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`) + ZoomOut, + /// The Brightness Down key. Typically controls the display brightness. + /// (`KEYCODE_BRIGHTNESS_DOWN`) + BrightnessDown, + /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`) + BrightnessUp, + /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`) + Eject, + LogOff, + /// Toggle power state. (`KEYCODE_POWER`) + /// Note: Note: Some devices might not expose this key to the operating environment. + Power, + /// The `PowerOff` key. Sometime called `PowerDown`. + PowerOff, + /// Initiate print-screen function. + PrintScreen, + /// The Hibernate key. This key saves the current state of the computer to disk so that it can + /// be restored. The computer will then shutdown. + Hibernate, + /// The Standby key. This key turns off the display and places the computer into a low-power + /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key. + /// (`KEYCODE_SLEEP`) + Standby, + /// The WakeUp key. (`KEYCODE_WAKEUP`) + WakeUp, + /// Initate the multi-candidate mode. + AllCandidates, + Alphanumeric, + /// Initiate the Code Input mode to allow characters to be entered by + /// their code points. + CodeInput, + /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a + /// manner similar to a dead key, triggering a mode where subsequent key presses are combined to + /// produce a different character. + Compose, + /// Convert the current input method sequence. + Convert, + /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs. + FinalMode, + /// Switch to the first character group. (ISO/IEC 9995) + GroupFirst, + /// Switch to the last character group. (ISO/IEC 9995) + GroupLast, + /// Switch to the next character group. (ISO/IEC 9995) + GroupNext, + /// Switch to the previous character group. (ISO/IEC 9995) + GroupPrevious, + /// Toggle between or cycle through input modes of IMEs. + ModeChange, + NextCandidate, + /// Accept current input method sequence without + /// conversion in IMEs. + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + /// Toggle between Hangul and English modes. + HangulMode, + HanjaMode, + JunjaMode, + /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME. + /// (`KEYCODE_EISU`) + Eisu, + /// The (Half-Width) Characters key. + Hankaku, + /// The Hiragana (Japanese Kana characters) key. + Hiragana, + /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`) + HiraganaKatakana, + /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from + /// romaji mode). + KanaMode, + /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key is + /// typically used to switch to a hiragana keyboard for the purpose of converting input into + /// kanji. (`KEYCODE_KANA`) + KanjiMode, + /// The Katakana (Japanese Kana characters) key. + Katakana, + /// The Roman characters function key. + Romaji, + /// The Zenkaku (Full-Width) Characters key. + Zenkaku, + /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`) + ZenkakuHankaku, + /// General purpose virtual function key, as index 1. + Soft1, + /// General purpose virtual function key, as index 2. + Soft2, + /// General purpose virtual function key, as index 3. + Soft3, + /// General purpose virtual function key, as index 4. + Soft4, + /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`, + /// `KEYCODE_CHANNEL_DOWN`) + ChannelDown, + /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`, + /// `KEYCODE_CHANNEL_UP`) + ChannelUp, + /// Close the current document or message (Note: This doesn’t close the application). + /// (`APPCOMMAND_CLOSE`) + Close, + /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`) + MailForward, + /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`) + MailReply, + /// Send the current message. (`APPCOMMAND_SEND_MAIL`) + MailSend, + /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`) + MediaClose, + /// Initiate or continue forward playback at faster than normal speed, or increase speed if + /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`) + MediaFastForward, + /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`) + /// + /// Note: Media controller devices should use this value rather than `"Pause"` for their pause + /// keys. + MediaPause, + /// Initiate or continue media playback at normal speed, if not currently playing at normal + /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`) + MediaPlay, + /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`, + /// `KEYCODE_MEDIA_PLAY_PAUSE`) + MediaPlayPause, + /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`, + /// `KEYCODE_MEDIA_RECORD`) + MediaRecord, + /// Initiate or continue reverse playback at faster than normal speed, or increase speed if + /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`) + MediaRewind, + /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped. + /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`) + MediaStop, + /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`) + MediaTrackNext, + /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`, + /// `KEYCODE_MEDIA_PREVIOUS`) + MediaTrackPrevious, + /// Open a new document or message. (`APPCOMMAND_NEW`) + New, + /// Open an existing document or message. (`APPCOMMAND_OPEN`) + Open, + /// Print the current document or message. (`APPCOMMAND_PRINT`) + Print, + /// Save the current document or message. (`APPCOMMAND_SAVE`) + Save, + /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`) + SpellCheck, + /// The `11` key found on media numpads that + /// have buttons from `1` ... `12`. + Key11, + /// The `12` key found on media numpads that + /// have buttons from `1` ... `12`. + Key12, + /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`) + AudioBalanceLeft, + /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`) + AudioBalanceRight, + /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`, + /// `VK_BASS_BOOST_DOWN`) + AudioBassBoostDown, + /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`) + AudioBassBoostToggle, + /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`, + /// `VK_BASS_BOOST_UP`) + AudioBassBoostUp, + /// Adjust audio fader towards front. (`VK_FADER_FRONT`) + AudioFaderFront, + /// Adjust audio fader towards rear. (`VK_FADER_REAR`) + AudioFaderRear, + /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`) + AudioSurroundModeNext, + /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`) + AudioTrebleDown, + /// Increase treble. (`APPCOMMAND_TREBLE_UP`) + AudioTrebleUp, + /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`) + AudioVolumeDown, + /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`) + AudioVolumeUp, + /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`, + /// `KEYCODE_VOLUME_MUTE`) + AudioVolumeMute, + /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`) + MicrophoneToggle, + /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`) + MicrophoneVolumeDown, + /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`) + MicrophoneVolumeUp, + /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`) + MicrophoneVolumeMute, + /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`) + SpeechCorrectionList, + /// Toggle between dictation mode and command/control mode. + /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`) + SpeechInputToggle, + /// The first generic "LaunchApplication" key. This is commonly associated with launching "My + /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`) + LaunchApplication1, + /// The second generic "LaunchApplication" key. This is commonly associated with launching + /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`, + /// `KEYCODE_CALCULATOR`) + LaunchApplication2, + /// The "Calendar" key. (`KEYCODE_CALENDAR`) + LaunchCalendar, + /// The "Contacts" key. (`KEYCODE_CONTACTS`) + LaunchContacts, + /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`) + LaunchMail, + /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`) + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`) + BrowserBack, + /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`) + BrowserFavorites, + /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`) + BrowserForward, + /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`) + BrowserHome, + /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`) + BrowserRefresh, + /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`) + BrowserSearch, + /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`) + BrowserStop, + /// The Application switch key, which provides a list of recent apps to switch between. + /// (`KEYCODE_APP_SWITCH`) + AppSwitch, + /// The Call key. (`KEYCODE_CALL`) + Call, + /// The Camera key. (`KEYCODE_CAMERA`) + Camera, + /// The Camera focus key. (`KEYCODE_FOCUS`) + CameraFocus, + /// The End Call key. (`KEYCODE_ENDCALL`) + EndCall, + /// The Back key. (`KEYCODE_BACK`) + GoBack, + /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`) + GoHome, + /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`) + HeadsetHook, + LastNumberRedial, + /// The Notification key. (`KEYCODE_NOTIFICATION`) + Notification, + /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`) + MannerMode, + VoiceDial, + /// Switch to viewing TV. (`KEYCODE_TV`) + TV, + /// TV 3D Mode. (`KEYCODE_3D_MODE`) + TV3DMode, + /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`) + TVAntennaCable, + /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`) + TVAudioDescription, + /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`) + TVAudioDescriptionMixDown, + /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`) + TVAudioDescriptionMixUp, + /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`) + TVContentsMenu, + /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`) + TVDataService, + /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`) + TVInput, + /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`) + TVInputComponent1, + /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`) + TVInputComponent2, + /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`) + TVInputComposite1, + /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`) + TVInputComposite2, + /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`) + TVInputHDMI1, + /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`) + TVInputHDMI2, + /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`) + TVInputHDMI3, + /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`) + TVInputHDMI4, + /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`) + TVInputVGA1, + /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`) + TVMediaContext, + /// Toggle network. (`KEYCODE_TV_NETWORK`) + TVNetwork, + /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`) + TVNumberEntry, + /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`) + TVPower, + /// Radio. (`KEYCODE_TV_RADIO_SERVICE`) + TVRadioService, + /// Satellite. (`KEYCODE_TV_SATELLITE`) + TVSatellite, + /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`) + TVSatelliteBS, + /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`) + TVSatelliteCS, + /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`) + TVSatelliteToggle, + /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`) + TVTerrestrialAnalog, + /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`) + TVTerrestrialDigital, + /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`) + TVTimer, + /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`) + AVRInput, + /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`) + AVRPower, + /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`, + /// `KEYCODE_PROG_RED`) + ColorF0Red, + /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`, + /// `KEYCODE_PROG_GREEN`) + ColorF1Green, + /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`, + /// `KEYCODE_PROG_YELLOW`) + ColorF2Yellow, + /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`, + /// `KEYCODE_PROG_BLUE`) + ColorF3Blue, + /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`) + ColorF4Grey, + /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`) + ColorF5Brown, + /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`) + ClosedCaptionToggle, + /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`) + Dimmer, + /// Swap video sources. (`VK_DISPLAY_SWAP`) + DisplaySwap, + /// Select Digital Video Rrecorder. (`KEYCODE_DVR`) + DVR, + /// Exit the current application. (`VK_EXIT`) + Exit, + /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`) + FavoriteClear0, + /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`) + FavoriteClear1, + /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`) + FavoriteClear2, + /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`) + FavoriteClear3, + /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`) + FavoriteRecall0, + /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`) + FavoriteRecall1, + /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`) + FavoriteRecall2, + /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`) + FavoriteRecall3, + /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`) + FavoriteStore0, + /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`) + FavoriteStore1, + /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`) + FavoriteStore2, + /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`) + FavoriteStore3, + /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`) + Guide, + /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`) + GuideNextDay, + /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`) + GuidePreviousDay, + /// Toggle display of information about currently selected context or media. (`VK_INFO`, + /// `KEYCODE_INFO`) + Info, + /// Toggle instant replay. (`VK_INSTANT_REPLAY`) + InstantReplay, + /// Launch linked content, if available and appropriate. (`VK_LINK`) + Link, + /// List the current program. (`VK_LIST`) + ListProgram, + /// Toggle display listing of currently available live content or programs. (`VK_LIVE`) + LiveContent, + /// Lock or unlock current content or program. (`VK_LOCK`) + Lock, + /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`) + /// + /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key, + /// which is encoded as `"ContextMenu"`. + MediaApps, + /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`) + MediaAudioTrack, + /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`) + MediaLast, + /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`) + MediaSkipBackward, + /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`) + MediaSkipForward, + /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`) + MediaStepBackward, + /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`) + MediaStepForward, + /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`) + MediaTopMenu, + /// Navigate in. (`KEYCODE_NAVIGATE_IN`) + NavigateIn, + /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`) + NavigateNext, + /// Navigate out. (`KEYCODE_NAVIGATE_OUT`) + NavigateOut, + /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`) + NavigatePrevious, + /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`) + NextFavoriteChannel, + /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`) + NextUserProfile, + /// Access on-demand content or programs. (`VK_ON_DEMAND`) + OnDemand, + /// Pairing key to pair devices. (`KEYCODE_PAIRING`) + Pairing, + /// Move picture-in-picture window down. (`VK_PINP_DOWN`) + PinPDown, + /// Move picture-in-picture window. (`VK_PINP_MOVE`) + PinPMove, + /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`) + PinPToggle, + /// Move picture-in-picture window up. (`VK_PINP_UP`) + PinPUp, + /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`) + PlaySpeedDown, + /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`) + PlaySpeedReset, + /// Increase media playback speed. (`VK_PLAY_SPEED_UP`) + PlaySpeedUp, + /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`) + RandomToggle, + /// Not a physical key, but this key code is sent when the remote control battery is low. + /// (`VK_RC_LOW_BATTERY`) + RcLowBattery, + /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`) + RecordSpeedNext, + /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output). + /// (`VK_RF_BYPASS`) + RfBypass, + /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`) + ScanChannelsToggle, + /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`) + ScreenModeNext, + /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`) + Settings, + /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`) + SplitScreenToggle, + /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`) + STBInput, + /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`) + STBPower, + /// Toggle display of subtitles, if available. (`VK_SUBTITLE`) + Subtitle, + /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`). + Teletext, + /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`) + VideoModeNext, + /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`) + Wink, + /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`, + /// `KEYCODE_TV_ZOOM_MODE`) + ZoomToggle, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} diff --git a/core/src/keyboard/key_code.rs b/core/src/keyboard/key_code.rs deleted file mode 100644 index 74ead170..00000000 --- a/core/src/keyboard/key_code.rs +++ /dev/null @@ -1,203 +0,0 @@ -/// The symbolic name of a keyboard key. -/// -/// This is mostly the `KeyCode` type found in [`winit`]. -/// -/// [`winit`]: https://docs.rs/winit/0.20.0-alpha3/winit/ -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -#[repr(u32)] -#[allow(missing_docs)] -pub enum KeyCode { - /// The '1' key over the letters. - Key1, - /// The '2' key over the letters. - Key2, - /// The '3' key over the letters. - Key3, - /// The '4' key over the letters. - Key4, - /// The '5' key over the letters. - Key5, - /// The '6' key over the letters. - Key6, - /// The '7' key over the letters. - Key7, - /// The '8' key over the letters. - Key8, - /// The '9' key over the letters. - Key9, - /// The '0' key over the 'O' and 'P' keys. - Key0, - - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - - /// The Escape key, next to F1. - Escape, - - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - - /// Print Screen/SysRq. - Snapshot, - /// Scroll Lock. - Scroll, - /// Pause/Break key, next to Scroll lock. - Pause, - - /// `Insert`, next to Backspace. - Insert, - Home, - Delete, - End, - PageDown, - PageUp, - - Left, - Up, - Right, - Down, - - /// The Backspace key, right over Enter. - Backspace, - /// The Enter key. - Enter, - /// The space bar. - Space, - - /// The "Compose" key on Linux. - Compose, - - Caret, - - Numlock, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - NumpadAdd, - NumpadDivide, - NumpadDecimal, - NumpadComma, - NumpadEnter, - NumpadEquals, - NumpadMultiply, - NumpadSubtract, - - AbntC1, - AbntC2, - Apostrophe, - Apps, - Asterisk, - At, - Ax, - Backslash, - Calculator, - Capital, - Colon, - Comma, - Convert, - Equals, - Grave, - Kana, - Kanji, - LAlt, - LBracket, - LControl, - LShift, - LWin, - Mail, - MediaSelect, - MediaStop, - Minus, - Mute, - MyComputer, - NavigateForward, // also called "Next" - NavigateBackward, // also called "Prior" - NextTrack, - NoConvert, - OEM102, - Period, - PlayPause, - Plus, - Power, - PrevTrack, - RAlt, - RBracket, - RControl, - RShift, - RWin, - Semicolon, - Slash, - Sleep, - Stop, - Sysrq, - Tab, - Underline, - Unlabeled, - VolumeDown, - VolumeUp, - Wake, - WebBack, - WebFavorites, - WebForward, - WebHome, - WebRefresh, - WebSearch, - WebStop, - Yen, - Copy, - Paste, - Cut, -} diff --git a/core/src/keyboard/location.rs b/core/src/keyboard/location.rs new file mode 100644 index 00000000..feff0820 --- /dev/null +++ b/core/src/keyboard/location.rs @@ -0,0 +1,12 @@ +/// The location of a key on the keyboard. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Location { + /// The standard group of keys on the keyboard. + Standard, + /// The left side of the keyboard. + Left, + /// The right side of the keyboard. + Right, + /// The numpad of the keyboard. + Numpad, +} diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 03d1e283..bf2aaaa3 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -134,8 +134,8 @@ impl Application for Editor { } fn subscription(&self) -> Subscription<Message> { - keyboard::on_key_press(|key_code, modifiers| match key_code { - keyboard::KeyCode::S if modifiers.command() => { + keyboard::on_key_press(|key, modifiers| match key.as_ref() { + keyboard::Key::Character("s") if modifiers.command() => { Some(Message::SaveFile) } _ => None, diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index fab81553..b0939d68 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -278,7 +278,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { // Map window event to iced event if let Some(event) = iced_winit::conversion::window_event( window::Id::MAIN, - &event, + event, window.scale_factor(), modifiers, ) { diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 60dabe54..6cf0e570 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -71,9 +71,13 @@ impl Application for Layout { } fn subscription(&self) -> Subscription<Message> { - keyboard::on_key_release(|key_code, _modifiers| match key_code { - keyboard::KeyCode::Left => Some(Message::Previous), - keyboard::KeyCode::Right => Some(Message::Next), + use keyboard::key; + + keyboard::on_key_release(|key, _modifiers| match key { + keyboard::Key::Named(key::Named::ArrowLeft) => { + Some(Message::Previous) + } + keyboard::Key::Named(key::Named::ArrowRight) => Some(Message::Next), _ => None, }) } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index d1cc7bb0..963c839e 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -1,6 +1,7 @@ use iced::event::{self, Event}; use iced::executor; use iced::keyboard; +use iced::keyboard::key; use iced::theme; use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, @@ -85,7 +86,7 @@ impl Application for App { } Message::Event(event) => match event { Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Tab, + key: keyboard::Key::Named(key::Named::Tab), modifiers, .. }) => { @@ -96,7 +97,7 @@ impl Application for App { } } Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Escape, + key: keyboard::Key::Named(key::Named::Escape), .. }) => { self.hide_modal(); diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 96bb8e4e..d5e5bcbe 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -220,23 +220,26 @@ const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb( 0x47 as f32 / 255.0, ); -fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> { - use keyboard::KeyCode; +fn handle_hotkey(key: keyboard::Key) -> Option<Message> { + use keyboard::key::{self, Key}; use pane_grid::{Axis, Direction}; - let direction = match key_code { - KeyCode::Up => Some(Direction::Up), - KeyCode::Down => Some(Direction::Down), - KeyCode::Left => Some(Direction::Left), - KeyCode::Right => Some(Direction::Right), - _ => None, - }; + match key.as_ref() { + Key::Character("v") => Some(Message::SplitFocused(Axis::Vertical)), + Key::Character("h") => Some(Message::SplitFocused(Axis::Horizontal)), + Key::Character("w") => Some(Message::CloseFocused), + Key::Named(key) => { + let direction = match key { + key::Named::ArrowUp => Some(Direction::Up), + key::Named::ArrowDown => Some(Direction::Down), + key::Named::ArrowLeft => Some(Direction::Left), + key::Named::ArrowRight => Some(Direction::Right), + _ => None, + }; - match key_code { - KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)), - KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)), - KeyCode::W => Some(Message::CloseFocused), - _ => direction.map(Message::FocusAdjacent), + direction.map(Message::FocusAdjacent) + } + _ => None, } } diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 20d34be6..6955551e 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,11 +1,13 @@ -use iced::keyboard::KeyCode; -use iced::theme::{Button, Container}; +use iced::alignment; +use iced::executor; +use iced::keyboard; +use iced::theme; use iced::widget::{button, column, container, image, row, text, text_input}; +use iced::window; use iced::window::screenshot::{self, Screenshot}; -use iced::{alignment, window}; use iced::{ - event, executor, keyboard, Alignment, Application, Command, ContentFit, - Element, Event, Length, Rectangle, Renderer, Subscription, Theme, + Alignment, Application, Command, ContentFit, Element, Length, Rectangle, + Renderer, Subscription, Theme, }; use ::image as img; @@ -147,7 +149,7 @@ impl Application for Example { let image = container(image) .padding(10) - .style(Container::Box) + .style(theme::Container::Box) .width(Length::FillPortion(2)) .height(Length::Fill) .center_x() @@ -202,9 +204,10 @@ impl Application for Example { self.screenshot.is_some().then(|| Message::Png), ) } else { - button(centered_text("Saving...")).style(Button::Secondary) + button(centered_text("Saving...")) + .style(theme::Button::Secondary) } - .style(Button::Secondary) + .style(theme::Button::Secondary) .padding([10, 20, 10, 20]) .width(Length::Fill) ] @@ -213,7 +216,7 @@ impl Application for Example { crop_controls, button(centered_text("Crop")) .on_press(Message::Crop) - .style(Button::Destructive) + .style(theme::Button::Destructive) .padding([10, 20, 10, 20]) .width(Length::Fill), ] @@ -256,16 +259,10 @@ impl Application for Example { } fn subscription(&self) -> Subscription<Self::Message> { - event::listen_with(|event, status| { - if let event::Status::Captured = status { - return None; - } + use keyboard::key; - if let Event::Keyboard(keyboard::Event::KeyPressed { - key_code: KeyCode::F5, - .. - }) = event - { + keyboard::on_key_press(|key, _modifiers| { + if let keyboard::Key::Named(key::Named::F5) = key { Some(Message::Screenshot) } else { None diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 0b0f0607..8a0674c1 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -86,12 +86,16 @@ impl Application for Stopwatch { }; fn handle_hotkey( - key_code: keyboard::KeyCode, + key: keyboard::Key, _modifiers: keyboard::Modifiers, ) -> Option<Message> { - match key_code { - keyboard::KeyCode::Space => Some(Message::Toggle), - keyboard::KeyCode::R => Some(Message::Reset), + use keyboard::key; + + match key.as_ref() { + keyboard::Key::Named(key::Named::Space) => { + Some(Message::Toggle) + } + keyboard::Key::Character("r") => Some(Message::Reset), _ => None, } } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 609f9087..2e837fa3 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -1,6 +1,7 @@ use iced::event::{self, Event}; use iced::executor; use iced::keyboard; +use iced::keyboard::key; use iced::widget::{ self, button, column, container, pick_list, row, slider, text, text_input, }; @@ -93,12 +94,12 @@ impl Application for App { Command::none() } Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Tab, + key: keyboard::Key::Named(key::Named::Tab), modifiers, .. })) if modifiers.shift() => widget::focus_previous(), Message::Event(Event::Keyboard(keyboard::Event::KeyPressed { - key_code: keyboard::KeyCode::Tab, + key: keyboard::Key::Named(key::Named::Tab), .. })) => widget::focus_next(), Message::Event(_) => Command::none(), diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index aad47c20..3d79f087 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -260,15 +260,21 @@ impl Application for Todos { } fn subscription(&self) -> Subscription<Message> { - keyboard::on_key_press(|key_code, modifiers| { - match (key_code, modifiers) { - (keyboard::KeyCode::Tab, _) => Some(Message::TabPressed { + use keyboard::key; + + keyboard::on_key_press(|key, modifiers| { + let keyboard::Key::Named(key) = key else { + return None; + }; + + match (key, modifiers) { + (key::Named::Tab, _) => Some(Message::TabPressed { shift: modifiers.shift(), }), - (keyboard::KeyCode::Up, keyboard::Modifiers::SHIFT) => { + (key::Named::ArrowUp, keyboard::Modifiers::SHIFT) => { Some(Message::ToggleFullscreen(window::Mode::Fullscreen)) } - (keyboard::KeyCode::Down, keyboard::Modifiers::SHIFT) => { + (key::Named::ArrowDown, keyboard::Modifiers::SHIFT) => { Some(Message::ToggleFullscreen(window::Mode::Windowed)) } _ => None, diff --git a/futures/src/keyboard.rs b/futures/src/keyboard.rs index 855eecd4..8e7da38f 100644 --- a/futures/src/keyboard.rs +++ b/futures/src/keyboard.rs @@ -1,6 +1,6 @@ //! Listen to keyboard events. use crate::core; -use crate::core::keyboard::{Event, KeyCode, Modifiers}; +use crate::core::keyboard::{Event, Key, Modifiers}; use crate::subscription::{self, Subscription}; use crate::MaybeSend; @@ -10,7 +10,7 @@ use crate::MaybeSend; /// If the function returns `None`, the key press will be simply /// ignored. pub fn on_key_press<Message>( - f: fn(KeyCode, Modifiers) -> Option<Message>, + f: fn(Key, Modifiers) -> Option<Message>, ) -> Subscription<Message> where Message: MaybeSend + 'static, @@ -22,12 +22,10 @@ where match (event, status) { ( core::Event::Keyboard(Event::KeyPressed { - key_code, - modifiers, - .. + key, modifiers, .. }), core::event::Status::Ignored, - ) => f(key_code, modifiers), + ) => f(key, modifiers), _ => None, } }) @@ -39,7 +37,7 @@ where /// If the function returns `None`, the key release will be simply /// ignored. pub fn on_key_release<Message>( - f: fn(KeyCode, Modifiers) -> Option<Message>, + f: fn(Key, Modifiers) -> Option<Message>, ) -> Subscription<Message> where Message: MaybeSend + 'static, @@ -51,11 +49,12 @@ where match (event, status) { ( core::Event::Keyboard(Event::KeyReleased { - key_code, + key, modifiers, + .. }), core::event::Status::Ignored, - ) => f(key_code, modifiers), + ) => f(key, modifiers), _ => None, } }) diff --git a/src/lib.rs b/src/lib.rs index 002d2a79..446590ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,7 +230,8 @@ pub mod event { pub mod keyboard { //! Listen and react to keyboard events. - pub use crate::core::keyboard::{Event, KeyCode, Modifiers}; + pub use crate::core::keyboard::key; + pub use crate::core::keyboard::{Event, Key, Location, Modifiers}; pub use iced_futures::keyboard::{on_key_press, on_key_release}; } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 1b2fa947..73beeac3 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -1,6 +1,7 @@ //! Display a dropdown list of searchable and selectable options. use crate::core::event::{self, Event}; use crate::core::keyboard; +use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -436,14 +437,14 @@ where } if let Event::Keyboard(keyboard::Event::KeyPressed { - key_code, + key: keyboard::Key::Named(named_key), modifiers, .. }) = event { let shift_modifer = modifiers.shift(); - match (key_code, shift_modifer) { - (keyboard::KeyCode::Enter, _) => { + match (named_key, shift_modifer) { + (key::Named::Enter, _) => { if let Some(index) = &menu.hovered_option { if let Some(option) = state.filtered_options.options.get(*index) @@ -455,8 +456,7 @@ where event_status = event::Status::Captured; } - (keyboard::KeyCode::Up, _) - | (keyboard::KeyCode::Tab, true) => { + (key::Named::ArrowUp, _) | (key::Named::Tab, true) => { if let Some(index) = &mut menu.hovered_option { if *index == 0 { *index = state @@ -492,8 +492,8 @@ where event_status = event::Status::Captured; } - (keyboard::KeyCode::Down, _) - | (keyboard::KeyCode::Tab, false) + (key::Named::ArrowDown, _) + | (key::Named::Tab, false) if !modifiers.shift() => { if let Some(index) = &mut menu.hovered_option { diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index b95a45e4..09a0cac0 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,6 +1,7 @@ //! Display a multi-line text input for text editing. use crate::core::event::{self, Event}; use crate::core::keyboard; +use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; @@ -646,34 +647,48 @@ impl Update { }, Event::Keyboard(event) => match event { keyboard::Event::KeyPressed { - key_code, + key, modifiers, text, + .. } if state.is_focused => { - if let Some(motion) = motion(key_code) { - let motion = - if platform::is_jump_modifier_pressed(modifiers) { + if let keyboard::Key::Named(named_key) = key.as_ref() { + if let Some(motion) = motion(named_key) { + let motion = if platform::is_jump_modifier_pressed( + modifiers, + ) { motion.widen() } else { motion }; - return action(if modifiers.shift() { - Action::Select(motion) - } else { - Action::Move(motion) - }); + return action(if modifiers.shift() { + Action::Select(motion) + } else { + Action::Move(motion) + }); + } } - match key_code { - keyboard::KeyCode::Enter => edit(Edit::Enter), - keyboard::KeyCode::Backspace => edit(Edit::Backspace), - keyboard::KeyCode::Delete => edit(Edit::Delete), - keyboard::KeyCode::Escape => Some(Self::Unfocus), - keyboard::KeyCode::C if modifiers.command() => { + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { + edit(Edit::Enter) + } + keyboard::Key::Named(key::Named::Backspace) => { + edit(Edit::Backspace) + } + keyboard::Key::Named(key::Named::Delete) => { + edit(Edit::Delete) + } + keyboard::Key::Named(key::Named::Escape) => { + Some(Self::Unfocus) + } + keyboard::Key::Character("c") + if modifiers.command() => + { Some(Self::Copy) } - keyboard::KeyCode::V + keyboard::Key::Character("v") if modifiers.command() && !modifiers.alt() => { Some(Self::Paste) @@ -694,16 +709,16 @@ impl Update { } } -fn motion(key_code: keyboard::KeyCode) -> Option<Motion> { - match key_code { - keyboard::KeyCode::Left => Some(Motion::Left), - keyboard::KeyCode::Right => Some(Motion::Right), - keyboard::KeyCode::Up => Some(Motion::Up), - keyboard::KeyCode::Down => Some(Motion::Down), - keyboard::KeyCode::Home => Some(Motion::Home), - keyboard::KeyCode::End => Some(Motion::End), - keyboard::KeyCode::PageUp => Some(Motion::PageUp), - keyboard::KeyCode::PageDown => Some(Motion::PageDown), +fn motion(key: key::Named) -> Option<Motion> { + match key { + key::Named::ArrowLeft => Some(Motion::Left), + key::Named::ArrowRight => Some(Motion::Right), + key::Named::ArrowUp => Some(Motion::Up), + key::Named::ArrowDown => Some(Motion::Down), + key::Named::Home => Some(Motion::Home), + key::Named::End => Some(Motion::End), + key::Named::PageUp => Some(Motion::PageUp), + key::Named::PageDown => Some(Motion::PageDown), _ => None, } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 8d28e8ee..c3dce8be 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -14,6 +14,7 @@ use editor::Editor; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; +use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; @@ -748,9 +749,7 @@ where return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::KeyPressed { - key_code, text, .. - }) => { + Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -761,14 +760,13 @@ where let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); - match key_code { - keyboard::KeyCode::Enter - | keyboard::KeyCode::NumpadEnter => { + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { if let Some(on_submit) = on_submit.clone() { shell.publish(on_submit); } } - keyboard::KeyCode::Backspace => { + keyboard::Key::Named(key::Named::Backspace) => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { @@ -788,7 +786,7 @@ where update_cache(state, value); } - keyboard::KeyCode::Delete => { + keyboard::Key::Named(key::Named::Delete) => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { @@ -810,7 +808,7 @@ where update_cache(state, value); } - keyboard::KeyCode::Left => { + keyboard::Key::Named(key::Named::ArrowLeft) => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -825,7 +823,7 @@ where state.cursor.move_left(value); } } - keyboard::KeyCode::Right => { + keyboard::Key::Named(key::Named::ArrowRight) => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -840,7 +838,7 @@ where state.cursor.move_right(value); } } - keyboard::KeyCode::Home => { + keyboard::Key::Named(key::Named::Home) => { if modifiers.shift() { state .cursor @@ -849,7 +847,7 @@ where state.cursor.move_to(0); } } - keyboard::KeyCode::End => { + keyboard::Key::Named(key::Named::End) => { if modifiers.shift() { state.cursor.select_range( state.cursor.start(value), @@ -859,7 +857,7 @@ where state.cursor.move_to(value.len()); } } - keyboard::KeyCode::C + keyboard::Key::Character("c") if state.keyboard_modifiers.command() => { if let Some((start, end)) = @@ -869,7 +867,7 @@ where .write(value.select(start, end).to_string()); } } - keyboard::KeyCode::X + keyboard::Key::Character("x") if state.keyboard_modifiers.command() => { if let Some((start, end)) = @@ -887,7 +885,7 @@ where update_cache(state, value); } - keyboard::KeyCode::V => { + keyboard::Key::Character("v") => { if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() { @@ -924,12 +922,12 @@ where state.is_pasting = None; } } - keyboard::KeyCode::A + keyboard::Key::Character("a") if state.keyboard_modifiers.command() => { state.cursor.select_all(value); } - keyboard::KeyCode::Escape => { + keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; state.is_dragging = false; state.is_pasting = None; @@ -937,9 +935,11 @@ where state.keyboard_modifiers = keyboard::Modifiers::default(); } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { return event::Status::Ignored; } _ => { @@ -971,17 +971,19 @@ where return event::Status::Captured; } } - Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state(); if state.is_focused.is_some() { - match key_code { - keyboard::KeyCode::V => { + match key.as_ref() { + keyboard::Key::Character("v") => { state.is_pasting = None; } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { return event::Status::Ignored; } _ => {} diff --git a/winit/src/application.rs b/winit/src/application.rs index 46d1cddc..bf48538d 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -462,7 +462,7 @@ async fn run_instance<A, E, C>( if let Some(event) = conversion::window_event( window::Id::MAIN, - &window_event, + window_event, state.scale_factor(), state.modifiers(), ) { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 2e382c39..387289e8 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -128,7 +128,7 @@ pub fn window_settings( /// Converts a winit window event into an iced event. pub fn window_event( id: window::Id, - event: &winit::event::WindowEvent, + event: winit::event::WindowEvent, scale_factor: f64, modifiers: winit::keyboard::ModifiersState, ) -> Option<Event> { @@ -163,7 +163,7 @@ pub fn window_event( Some(Event::Mouse(mouse::Event::CursorLeft)) } WindowEvent::MouseInput { button, state, .. } => { - let button = mouse_button(*button); + let button = mouse_button(button); Some(Event::Mouse(match state { winit::event::ElementState::Pressed => { @@ -178,8 +178,8 @@ pub fn window_event( winit::event::MouseScrollDelta::LineDelta(delta_x, delta_y) => { Some(Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Lines { - x: *delta_x, - y: *delta_y, + x: delta_x, + y: delta_y, }, })) } @@ -198,18 +198,33 @@ pub fn window_event( logical_key, state, text, + location, .. }, .. } => Some(Event::Keyboard({ - let key_code = key_code(logical_key); + let key = key(logical_key); let modifiers = self::modifiers(modifiers); + let location = match location { + winit::keyboard::KeyLocation::Standard => { + keyboard::Location::Standard + } + winit::keyboard::KeyLocation::Left => keyboard::Location::Left, + winit::keyboard::KeyLocation::Right => { + keyboard::Location::Right + } + winit::keyboard::KeyLocation::Numpad => { + keyboard::Location::Numpad + } + }; + match state { winit::event::ElementState::Pressed => { keyboard::Event::KeyPressed { - key_code, + key, modifiers, + location, text: text .as_ref() .map(winit::keyboard::SmolStr::to_string), @@ -217,8 +232,9 @@ pub fn window_event( } winit::event::ElementState::Released => { keyboard::Event::KeyReleased { - key_code, + key, modifiers, + location, } } } @@ -230,7 +246,7 @@ pub fn window_event( } WindowEvent::Focused(focused) => Some(Event::Window( id, - if *focused { + if focused { window::Event::Focused } else { window::Event::Unfocused @@ -246,7 +262,7 @@ pub fn window_event( Some(Event::Window(id, window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { - Some(Event::Touch(touch_event(*touch, scale_factor))) + Some(Event::Touch(touch_event(touch, scale_factor))) } WindowEvent::Moved(position) => { let winit::dpi::LogicalPosition { x, y } = @@ -449,125 +465,328 @@ pub fn touch_event( /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.10 -pub fn key_code(key: &winit::keyboard::Key) -> keyboard::KeyCode { - use keyboard::KeyCode; +pub fn key(key: winit::keyboard::Key) -> keyboard::Key { + use keyboard::key::Named; use winit::keyboard::NamedKey; match key { - winit::keyboard::Key::Character(c) => match c.as_str() { - "1" => KeyCode::Key1, - "2" => KeyCode::Key2, - "3" => KeyCode::Key3, - "4" => KeyCode::Key4, - "5" => KeyCode::Key5, - "6" => KeyCode::Key6, - "7" => KeyCode::Key7, - "8" => KeyCode::Key8, - "9" => KeyCode::Key9, - "0" => KeyCode::Key0, - "a" => KeyCode::A, - "b" => KeyCode::B, - "c" => KeyCode::C, - "d" => KeyCode::D, - "e" => KeyCode::E, - "f" => KeyCode::F, - "g" => KeyCode::G, - "h" => KeyCode::H, - "i" => KeyCode::I, - "j" => KeyCode::J, - "k" => KeyCode::K, - "l" => KeyCode::L, - "m" => KeyCode::M, - "n" => KeyCode::N, - "o" => KeyCode::O, - "p" => KeyCode::P, - "q" => KeyCode::Q, - "r" => KeyCode::R, - "s" => KeyCode::S, - "t" => KeyCode::T, - "u" => KeyCode::U, - "v" => KeyCode::V, - "w" => KeyCode::W, - "x" => KeyCode::X, - "y" => KeyCode::Y, - "z" => KeyCode::Z, - _ => KeyCode::Unlabeled, - }, - winit::keyboard::Key::Named(named_key) => match named_key { - NamedKey::Escape => KeyCode::Escape, - NamedKey::F1 => KeyCode::F1, - NamedKey::F2 => KeyCode::F2, - NamedKey::F3 => KeyCode::F3, - NamedKey::F4 => KeyCode::F4, - NamedKey::F5 => KeyCode::F5, - NamedKey::F6 => KeyCode::F6, - NamedKey::F7 => KeyCode::F7, - NamedKey::F8 => KeyCode::F8, - NamedKey::F9 => KeyCode::F9, - NamedKey::F10 => KeyCode::F10, - NamedKey::F11 => KeyCode::F11, - NamedKey::F12 => KeyCode::F12, - NamedKey::F13 => KeyCode::F13, - NamedKey::F14 => KeyCode::F14, - NamedKey::F15 => KeyCode::F15, - NamedKey::F16 => KeyCode::F16, - NamedKey::F17 => KeyCode::F17, - NamedKey::F18 => KeyCode::F18, - NamedKey::F19 => KeyCode::F19, - NamedKey::F20 => KeyCode::F20, - NamedKey::F21 => KeyCode::F21, - NamedKey::F22 => KeyCode::F22, - NamedKey::F23 => KeyCode::F23, - NamedKey::F24 => KeyCode::F24, - NamedKey::PrintScreen => KeyCode::Snapshot, - NamedKey::ScrollLock => KeyCode::Scroll, - NamedKey::Pause => KeyCode::Pause, - NamedKey::Insert => KeyCode::Insert, - NamedKey::Home => KeyCode::Home, - NamedKey::Delete => KeyCode::Delete, - NamedKey::End => KeyCode::End, - NamedKey::PageDown => KeyCode::PageDown, - NamedKey::PageUp => KeyCode::PageUp, - NamedKey::ArrowLeft => KeyCode::Left, - NamedKey::ArrowUp => KeyCode::Up, - NamedKey::ArrowRight => KeyCode::Right, - NamedKey::ArrowDown => KeyCode::Down, - NamedKey::Backspace => KeyCode::Backspace, - NamedKey::Enter => KeyCode::Enter, - NamedKey::Space => KeyCode::Space, - NamedKey::Compose => KeyCode::Compose, - NamedKey::NumLock => KeyCode::Numlock, - NamedKey::AppSwitch => KeyCode::Apps, - NamedKey::Convert => KeyCode::Convert, - NamedKey::LaunchMail => KeyCode::Mail, - NamedKey::MediaApps => KeyCode::MediaSelect, - NamedKey::MediaStop => KeyCode::MediaStop, - NamedKey::AudioVolumeMute => KeyCode::Mute, - NamedKey::MediaStepForward => KeyCode::NavigateForward, - NamedKey::MediaStepBackward => KeyCode::NavigateBackward, - NamedKey::MediaSkipForward => KeyCode::NextTrack, - NamedKey::NonConvert => KeyCode::NoConvert, - NamedKey::MediaPlayPause => KeyCode::PlayPause, - NamedKey::Power => KeyCode::Power, - NamedKey::MediaSkipBackward => KeyCode::PrevTrack, - NamedKey::PowerOff => KeyCode::Sleep, - NamedKey::Tab => KeyCode::Tab, - NamedKey::AudioVolumeDown => KeyCode::VolumeDown, - NamedKey::AudioVolumeUp => KeyCode::VolumeUp, - NamedKey::WakeUp => KeyCode::Wake, - NamedKey::BrowserBack => KeyCode::WebBack, - NamedKey::BrowserFavorites => KeyCode::WebFavorites, - NamedKey::BrowserForward => KeyCode::WebForward, - NamedKey::BrowserHome => KeyCode::WebHome, - NamedKey::BrowserRefresh => KeyCode::WebRefresh, - NamedKey::BrowserSearch => KeyCode::WebSearch, - NamedKey::BrowserStop => KeyCode::WebStop, - NamedKey::Copy => KeyCode::Copy, - NamedKey::Paste => KeyCode::Paste, - NamedKey::Cut => KeyCode::Cut, - _ => KeyCode::Unlabeled, - }, - _ => KeyCode::Unlabeled, + winit::keyboard::Key::Character(c) => keyboard::Key::Character(c), + winit::keyboard::Key::Named(named_key) => { + keyboard::Key::Named(match named_key { + NamedKey::Alt => Named::Alt, + NamedKey::AltGraph => Named::AltGraph, + NamedKey::CapsLock => Named::CapsLock, + NamedKey::Control => Named::Control, + NamedKey::Fn => Named::Fn, + NamedKey::FnLock => Named::FnLock, + NamedKey::NumLock => Named::NumLock, + NamedKey::ScrollLock => Named::ScrollLock, + NamedKey::Shift => Named::Shift, + NamedKey::Symbol => Named::Symbol, + NamedKey::SymbolLock => Named::SymbolLock, + NamedKey::Meta => Named::Meta, + NamedKey::Hyper => Named::Hyper, + NamedKey::Super => Named::Super, + NamedKey::Enter => Named::Enter, + NamedKey::Tab => Named::Tab, + NamedKey::Space => Named::Space, + NamedKey::ArrowDown => Named::ArrowDown, + NamedKey::ArrowLeft => Named::ArrowLeft, + NamedKey::ArrowRight => Named::ArrowRight, + NamedKey::ArrowUp => Named::ArrowUp, + NamedKey::End => Named::End, + NamedKey::Home => Named::Home, + NamedKey::PageDown => Named::PageDown, + NamedKey::PageUp => Named::PageUp, + NamedKey::Backspace => Named::Backspace, + NamedKey::Clear => Named::Clear, + NamedKey::Copy => Named::Copy, + NamedKey::CrSel => Named::CrSel, + NamedKey::Cut => Named::Cut, + NamedKey::Delete => Named::Delete, + NamedKey::EraseEof => Named::EraseEof, + NamedKey::ExSel => Named::ExSel, + NamedKey::Insert => Named::Insert, + NamedKey::Paste => Named::Paste, + NamedKey::Redo => Named::Redo, + NamedKey::Undo => Named::Undo, + NamedKey::Accept => Named::Accept, + NamedKey::Again => Named::Again, + NamedKey::Attn => Named::Attn, + NamedKey::Cancel => Named::Cancel, + NamedKey::ContextMenu => Named::ContextMenu, + NamedKey::Escape => Named::Escape, + NamedKey::Execute => Named::Execute, + NamedKey::Find => Named::Find, + NamedKey::Help => Named::Help, + NamedKey::Pause => Named::Pause, + NamedKey::Play => Named::Play, + NamedKey::Props => Named::Props, + NamedKey::Select => Named::Select, + NamedKey::ZoomIn => Named::ZoomIn, + NamedKey::ZoomOut => Named::ZoomOut, + NamedKey::BrightnessDown => Named::BrightnessDown, + NamedKey::BrightnessUp => Named::BrightnessUp, + NamedKey::Eject => Named::Eject, + NamedKey::LogOff => Named::LogOff, + NamedKey::Power => Named::Power, + NamedKey::PowerOff => Named::PowerOff, + NamedKey::PrintScreen => Named::PrintScreen, + NamedKey::Hibernate => Named::Hibernate, + NamedKey::Standby => Named::Standby, + NamedKey::WakeUp => Named::WakeUp, + NamedKey::AllCandidates => Named::AllCandidates, + NamedKey::Alphanumeric => Named::Alphanumeric, + NamedKey::CodeInput => Named::CodeInput, + NamedKey::Compose => Named::Compose, + NamedKey::Convert => Named::Convert, + NamedKey::FinalMode => Named::FinalMode, + NamedKey::GroupFirst => Named::GroupFirst, + NamedKey::GroupLast => Named::GroupLast, + NamedKey::GroupNext => Named::GroupNext, + NamedKey::GroupPrevious => Named::GroupPrevious, + NamedKey::ModeChange => Named::ModeChange, + NamedKey::NextCandidate => Named::NextCandidate, + NamedKey::NonConvert => Named::NonConvert, + NamedKey::PreviousCandidate => Named::PreviousCandidate, + NamedKey::Process => Named::Process, + NamedKey::SingleCandidate => Named::SingleCandidate, + NamedKey::HangulMode => Named::HangulMode, + NamedKey::HanjaMode => Named::HanjaMode, + NamedKey::JunjaMode => Named::JunjaMode, + NamedKey::Eisu => Named::Eisu, + NamedKey::Hankaku => Named::Hankaku, + NamedKey::Hiragana => Named::Hiragana, + NamedKey::HiraganaKatakana => Named::HiraganaKatakana, + NamedKey::KanaMode => Named::KanaMode, + NamedKey::KanjiMode => Named::KanjiMode, + NamedKey::Katakana => Named::Katakana, + NamedKey::Romaji => Named::Romaji, + NamedKey::Zenkaku => Named::Zenkaku, + NamedKey::ZenkakuHankaku => Named::ZenkakuHankaku, + NamedKey::Soft1 => Named::Soft1, + NamedKey::Soft2 => Named::Soft2, + NamedKey::Soft3 => Named::Soft3, + NamedKey::Soft4 => Named::Soft4, + NamedKey::ChannelDown => Named::ChannelDown, + NamedKey::ChannelUp => Named::ChannelUp, + NamedKey::Close => Named::Close, + NamedKey::MailForward => Named::MailForward, + NamedKey::MailReply => Named::MailReply, + NamedKey::MailSend => Named::MailSend, + NamedKey::MediaClose => Named::MediaClose, + NamedKey::MediaFastForward => Named::MediaFastForward, + NamedKey::MediaPause => Named::MediaPause, + NamedKey::MediaPlay => Named::MediaPlay, + NamedKey::MediaPlayPause => Named::MediaPlayPause, + NamedKey::MediaRecord => Named::MediaRecord, + NamedKey::MediaRewind => Named::MediaRewind, + NamedKey::MediaStop => Named::MediaStop, + NamedKey::MediaTrackNext => Named::MediaTrackNext, + NamedKey::MediaTrackPrevious => Named::MediaTrackPrevious, + NamedKey::New => Named::New, + NamedKey::Open => Named::Open, + NamedKey::Print => Named::Print, + NamedKey::Save => Named::Save, + NamedKey::SpellCheck => Named::SpellCheck, + NamedKey::Key11 => Named::Key11, + NamedKey::Key12 => Named::Key12, + NamedKey::AudioBalanceLeft => Named::AudioBalanceLeft, + NamedKey::AudioBalanceRight => Named::AudioBalanceRight, + NamedKey::AudioBassBoostDown => Named::AudioBassBoostDown, + NamedKey::AudioBassBoostToggle => Named::AudioBassBoostToggle, + NamedKey::AudioBassBoostUp => Named::AudioBassBoostUp, + NamedKey::AudioFaderFront => Named::AudioFaderFront, + NamedKey::AudioFaderRear => Named::AudioFaderRear, + NamedKey::AudioSurroundModeNext => Named::AudioSurroundModeNext, + NamedKey::AudioTrebleDown => Named::AudioTrebleDown, + NamedKey::AudioTrebleUp => Named::AudioTrebleUp, + NamedKey::AudioVolumeDown => Named::AudioVolumeDown, + NamedKey::AudioVolumeUp => Named::AudioVolumeUp, + NamedKey::AudioVolumeMute => Named::AudioVolumeMute, + NamedKey::MicrophoneToggle => Named::MicrophoneToggle, + NamedKey::MicrophoneVolumeDown => Named::MicrophoneVolumeDown, + NamedKey::MicrophoneVolumeUp => Named::MicrophoneVolumeUp, + NamedKey::MicrophoneVolumeMute => Named::MicrophoneVolumeMute, + NamedKey::SpeechCorrectionList => Named::SpeechCorrectionList, + NamedKey::SpeechInputToggle => Named::SpeechInputToggle, + NamedKey::LaunchApplication1 => Named::LaunchApplication1, + NamedKey::LaunchApplication2 => Named::LaunchApplication2, + NamedKey::LaunchCalendar => Named::LaunchCalendar, + NamedKey::LaunchContacts => Named::LaunchContacts, + NamedKey::LaunchMail => Named::LaunchMail, + NamedKey::LaunchMediaPlayer => Named::LaunchMediaPlayer, + NamedKey::LaunchMusicPlayer => Named::LaunchMusicPlayer, + NamedKey::LaunchPhone => Named::LaunchPhone, + NamedKey::LaunchScreenSaver => Named::LaunchScreenSaver, + NamedKey::LaunchSpreadsheet => Named::LaunchSpreadsheet, + NamedKey::LaunchWebBrowser => Named::LaunchWebBrowser, + NamedKey::LaunchWebCam => Named::LaunchWebCam, + NamedKey::LaunchWordProcessor => Named::LaunchWordProcessor, + NamedKey::BrowserBack => Named::BrowserBack, + NamedKey::BrowserFavorites => Named::BrowserFavorites, + NamedKey::BrowserForward => Named::BrowserForward, + NamedKey::BrowserHome => Named::BrowserHome, + NamedKey::BrowserRefresh => Named::BrowserRefresh, + NamedKey::BrowserSearch => Named::BrowserSearch, + NamedKey::BrowserStop => Named::BrowserStop, + NamedKey::AppSwitch => Named::AppSwitch, + NamedKey::Call => Named::Call, + NamedKey::Camera => Named::Camera, + NamedKey::CameraFocus => Named::CameraFocus, + NamedKey::EndCall => Named::EndCall, + NamedKey::GoBack => Named::GoBack, + NamedKey::GoHome => Named::GoHome, + NamedKey::HeadsetHook => Named::HeadsetHook, + NamedKey::LastNumberRedial => Named::LastNumberRedial, + NamedKey::Notification => Named::Notification, + NamedKey::MannerMode => Named::MannerMode, + NamedKey::VoiceDial => Named::VoiceDial, + NamedKey::TV => Named::TV, + NamedKey::TV3DMode => Named::TV3DMode, + NamedKey::TVAntennaCable => Named::TVAntennaCable, + NamedKey::TVAudioDescription => Named::TVAudioDescription, + NamedKey::TVAudioDescriptionMixDown => { + Named::TVAudioDescriptionMixDown + } + NamedKey::TVAudioDescriptionMixUp => { + Named::TVAudioDescriptionMixUp + } + NamedKey::TVContentsMenu => Named::TVContentsMenu, + NamedKey::TVDataService => Named::TVDataService, + NamedKey::TVInput => Named::TVInput, + NamedKey::TVInputComponent1 => Named::TVInputComponent1, + NamedKey::TVInputComponent2 => Named::TVInputComponent2, + NamedKey::TVInputComposite1 => Named::TVInputComposite1, + NamedKey::TVInputComposite2 => Named::TVInputComposite2, + NamedKey::TVInputHDMI1 => Named::TVInputHDMI1, + NamedKey::TVInputHDMI2 => Named::TVInputHDMI2, + NamedKey::TVInputHDMI3 => Named::TVInputHDMI3, + NamedKey::TVInputHDMI4 => Named::TVInputHDMI4, + NamedKey::TVInputVGA1 => Named::TVInputVGA1, + NamedKey::TVMediaContext => Named::TVMediaContext, + NamedKey::TVNetwork => Named::TVNetwork, + NamedKey::TVNumberEntry => Named::TVNumberEntry, + NamedKey::TVPower => Named::TVPower, + NamedKey::TVRadioService => Named::TVRadioService, + NamedKey::TVSatellite => Named::TVSatellite, + NamedKey::TVSatelliteBS => Named::TVSatelliteBS, + NamedKey::TVSatelliteCS => Named::TVSatelliteCS, + NamedKey::TVSatelliteToggle => Named::TVSatelliteToggle, + NamedKey::TVTerrestrialAnalog => Named::TVTerrestrialAnalog, + NamedKey::TVTerrestrialDigital => Named::TVTerrestrialDigital, + NamedKey::TVTimer => Named::TVTimer, + NamedKey::AVRInput => Named::AVRInput, + NamedKey::AVRPower => Named::AVRPower, + NamedKey::ColorF0Red => Named::ColorF0Red, + NamedKey::ColorF1Green => Named::ColorF1Green, + NamedKey::ColorF2Yellow => Named::ColorF2Yellow, + NamedKey::ColorF3Blue => Named::ColorF3Blue, + NamedKey::ColorF4Grey => Named::ColorF4Grey, + NamedKey::ColorF5Brown => Named::ColorF5Brown, + NamedKey::ClosedCaptionToggle => Named::ClosedCaptionToggle, + NamedKey::Dimmer => Named::Dimmer, + NamedKey::DisplaySwap => Named::DisplaySwap, + NamedKey::DVR => Named::DVR, + NamedKey::Exit => Named::Exit, + NamedKey::FavoriteClear0 => Named::FavoriteClear0, + NamedKey::FavoriteClear1 => Named::FavoriteClear1, + NamedKey::FavoriteClear2 => Named::FavoriteClear2, + NamedKey::FavoriteClear3 => Named::FavoriteClear3, + NamedKey::FavoriteRecall0 => Named::FavoriteRecall0, + NamedKey::FavoriteRecall1 => Named::FavoriteRecall1, + NamedKey::FavoriteRecall2 => Named::FavoriteRecall2, + NamedKey::FavoriteRecall3 => Named::FavoriteRecall3, + NamedKey::FavoriteStore0 => Named::FavoriteStore0, + NamedKey::FavoriteStore1 => Named::FavoriteStore1, + NamedKey::FavoriteStore2 => Named::FavoriteStore2, + NamedKey::FavoriteStore3 => Named::FavoriteStore3, + NamedKey::Guide => Named::Guide, + NamedKey::GuideNextDay => Named::GuideNextDay, + NamedKey::GuidePreviousDay => Named::GuidePreviousDay, + NamedKey::Info => Named::Info, + NamedKey::InstantReplay => Named::InstantReplay, + NamedKey::Link => Named::Link, + NamedKey::ListProgram => Named::ListProgram, + NamedKey::LiveContent => Named::LiveContent, + NamedKey::Lock => Named::Lock, + NamedKey::MediaApps => Named::MediaApps, + NamedKey::MediaAudioTrack => Named::MediaAudioTrack, + NamedKey::MediaLast => Named::MediaLast, + NamedKey::MediaSkipBackward => Named::MediaSkipBackward, + NamedKey::MediaSkipForward => Named::MediaSkipForward, + NamedKey::MediaStepBackward => Named::MediaStepBackward, + NamedKey::MediaStepForward => Named::MediaStepForward, + NamedKey::MediaTopMenu => Named::MediaTopMenu, + NamedKey::NavigateIn => Named::NavigateIn, + NamedKey::NavigateNext => Named::NavigateNext, + NamedKey::NavigateOut => Named::NavigateOut, + NamedKey::NavigatePrevious => Named::NavigatePrevious, + NamedKey::NextFavoriteChannel => Named::NextFavoriteChannel, + NamedKey::NextUserProfile => Named::NextUserProfile, + NamedKey::OnDemand => Named::OnDemand, + NamedKey::Pairing => Named::Pairing, + NamedKey::PinPDown => Named::PinPDown, + NamedKey::PinPMove => Named::PinPMove, + NamedKey::PinPToggle => Named::PinPToggle, + NamedKey::PinPUp => Named::PinPUp, + NamedKey::PlaySpeedDown => Named::PlaySpeedDown, + NamedKey::PlaySpeedReset => Named::PlaySpeedReset, + NamedKey::PlaySpeedUp => Named::PlaySpeedUp, + NamedKey::RandomToggle => Named::RandomToggle, + NamedKey::RcLowBattery => Named::RcLowBattery, + NamedKey::RecordSpeedNext => Named::RecordSpeedNext, + NamedKey::RfBypass => Named::RfBypass, + NamedKey::ScanChannelsToggle => Named::ScanChannelsToggle, + NamedKey::ScreenModeNext => Named::ScreenModeNext, + NamedKey::Settings => Named::Settings, + NamedKey::SplitScreenToggle => Named::SplitScreenToggle, + NamedKey::STBInput => Named::STBInput, + NamedKey::STBPower => Named::STBPower, + NamedKey::Subtitle => Named::Subtitle, + NamedKey::Teletext => Named::Teletext, + NamedKey::VideoModeNext => Named::VideoModeNext, + NamedKey::Wink => Named::Wink, + NamedKey::ZoomToggle => Named::ZoomToggle, + NamedKey::F1 => Named::F1, + NamedKey::F2 => Named::F2, + NamedKey::F3 => Named::F3, + NamedKey::F4 => Named::F4, + NamedKey::F5 => Named::F5, + NamedKey::F6 => Named::F6, + NamedKey::F7 => Named::F7, + NamedKey::F8 => Named::F8, + NamedKey::F9 => Named::F9, + NamedKey::F10 => Named::F10, + NamedKey::F11 => Named::F11, + NamedKey::F12 => Named::F12, + NamedKey::F13 => Named::F13, + NamedKey::F14 => Named::F14, + NamedKey::F15 => Named::F15, + NamedKey::F16 => Named::F16, + NamedKey::F17 => Named::F17, + NamedKey::F18 => Named::F18, + NamedKey::F19 => Named::F19, + NamedKey::F20 => Named::F20, + NamedKey::F21 => Named::F21, + NamedKey::F22 => Named::F22, + NamedKey::F23 => Named::F23, + NamedKey::F24 => Named::F24, + NamedKey::F25 => Named::F25, + NamedKey::F26 => Named::F26, + NamedKey::F27 => Named::F27, + NamedKey::F28 => Named::F28, + NamedKey::F29 => Named::F29, + NamedKey::F30 => Named::F30, + NamedKey::F31 => Named::F31, + NamedKey::F32 => Named::F32, + NamedKey::F33 => Named::F33, + NamedKey::F34 => Named::F34, + NamedKey::F35 => Named::F35, + _ => return keyboard::Key::Unidentified, + }) + } + _ => keyboard::Key::Unidentified, } } diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 93f0cde3..dc659c1a 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -645,7 +645,7 @@ async fn run_instance<A, E, C>( if let Some(event) = conversion::window_event( id, - &window_event, + window_event, window.state.scale_factor(), window.state.modifiers(), ) { -- cgit From 03f5a351c37dbe1b0a286583e620d1cf074f1b91 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Tue, 16 Jan 2024 13:31:02 +0100 Subject: Use `SmolStr` for `text` field in `KeyPressed` event --- core/src/keyboard/event.rs | 3 ++- core/src/keyboard/key.rs | 2 +- core/src/lib.rs | 2 ++ winit/src/conversion.rs | 4 +--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index b1792415..1eb42334 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -1,4 +1,5 @@ use crate::keyboard::{Key, Location, Modifiers}; +use crate::SmolStr; /// A keyboard event. /// @@ -20,7 +21,7 @@ pub enum Event { modifiers: Modifiers, /// The text produced by the key press, if any. - text: Option<String>, + text: Option<SmolStr>, }, /// A keyboard key was released. diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index ef48dae4..dbde5196 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -1,5 +1,5 @@ //! Identify keyboard keys. -use smol_str::SmolStr; +use crate::SmolStr; /// A key on the keyboard. /// diff --git a/core/src/lib.rs b/core/src/lib.rs index 54ea5839..864df6e6 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -75,3 +75,5 @@ pub use size::Size; pub use text::Text; pub use vector::Vector; pub use widget::Widget; + +pub use smol_str::SmolStr; diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 387289e8..90a5d27f 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -225,9 +225,7 @@ pub fn window_event( key, modifiers, location, - text: text - .as_ref() - .map(winit::keyboard::SmolStr::to_string), + text, } } winit::event::ElementState::Released => { -- cgit From ff268c8c4268d930fc337636302175d44e201448 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott <idscott@system76.com> Date: Tue, 9 Jan 2024 12:25:53 -0800 Subject: Update to `softbuffer` 0.3, tracking up to `age` sets of primitives --- Cargo.toml | 2 +- renderer/src/compositor.rs | 1 + tiny_skia/src/window/compositor.rs | 121 +++++++++++++++++++++++-------------- 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac72f212..1f2da434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,7 +145,7 @@ resvg = "0.36" rustc-hash = "1.0" smol = "1.0" smol_str = "0.2" -softbuffer = "0.2" +softbuffer = "0.3.4" syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 9d0ff9ab..f9afdea4 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -238,6 +238,7 @@ impl Candidate { default_font: settings.default_font, default_text_size: settings.default_text_size, }, + _compatible_window, ); Ok(Compositor::TinySkia(compositor)) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 87ded746..d99b85d4 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -5,18 +5,21 @@ use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::collections::VecDeque; use std::marker::PhantomData; +use std::num::NonZeroU32; pub struct Compositor<Theme> { + context: Option<softbuffer::Context>, settings: Settings, _theme: PhantomData<Theme>, } pub struct Surface { - window: softbuffer::GraphicsContext, - buffer: Vec<u32>, + window: softbuffer::Surface, clip_mask: tiny_skia::Mask, - primitives: Option<Vec<Primitive>>, + // Primitives of existing buffers, by decreasing age + primitives: VecDeque<Vec<Primitive>>, background_color: Color, } @@ -27,9 +30,9 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( settings: Self::Settings, - _compatible_window: Option<&W>, + compatible_window: Option<&W>, ) -> Result<Self, Error> { - Ok(new(settings)) + Ok(new(settings, compatible_window)) } fn create_renderer(&self) -> Self::Renderer { @@ -47,16 +50,21 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { height: u32, ) -> Surface { #[allow(unsafe_code)] - let window = - unsafe { softbuffer::GraphicsContext::new(window, window) } - .expect("Create softbuffer for window"); + let window = if let Some(context) = self.context.as_ref() { + unsafe { softbuffer::Surface::new(context, window) } + .expect("Create softbuffer surface for window") + } else { + let context = unsafe { softbuffer::Context::new(window) } + .expect("Create softbuffer context for window"); + unsafe { softbuffer::Surface::new(&context, window) } + .expect("Create softbuffer surface for window") + }; Surface { window, - buffer: vec![0; width as usize * height as usize], clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), - primitives: None, + primitives: VecDeque::new(), background_color: Color::BLACK, } } @@ -67,10 +75,9 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { width: u32, height: u32, ) { - surface.buffer.resize((width * height) as usize, 0); surface.clip_mask = tiny_skia::Mask::new(width, height).expect("Create clip mask"); - surface.primitives = None; + surface.primitives.clear(); } fn fetch_information(&self) -> Information { @@ -121,8 +128,15 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } } -pub fn new<Theme>(settings: Settings) -> Compositor<Theme> { +pub fn new<W: HasRawWindowHandle + HasRawDisplayHandle, Theme>( + settings: Settings, + compatible_window: Option<&W>, +) -> Compositor<Theme> { + #[allow(unsafe_code)] + let context = compatible_window + .and_then(|w| unsafe { softbuffer::Context::new(w) }.ok()); Compositor { + context, settings, _theme: PhantomData, } @@ -139,48 +153,67 @@ pub fn present<T: AsRef<str>>( let physical_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; - let mut pixels = tiny_skia::PixmapMut::from_bytes( - bytemuck::cast_slice_mut(&mut surface.buffer), - physical_size.width, - physical_size.height, - ) - .expect("Create pixel map"); + surface + .window + .resize( + NonZeroU32::new(physical_size.width).unwrap(), + NonZeroU32::new(physical_size.height).unwrap(), + ) + .unwrap(); + + // TODO Add variants to `SurfaceError`? + let mut buffer = surface + .window + .buffer_mut() + .map_err(|_| compositor::SurfaceError::Lost)?; + + let age = buffer.age(); - let damage = surface - .primitives - .as_deref() + // Forget primatives for back buffers older than `age` + // Or if this is a new buffer, keep at most two. + let max = if age == 0 { 2 } else { age }; + while surface.primitives.len() as u8 > max { + let _ = surface.primitives.pop_front(); + } + + let last_primitives = if surface.primitives.len() as u8 == age { + surface.primitives.pop_front() + } else { + None + }; + + let damage = last_primitives .and_then(|last_primitives| { (surface.background_color == background_color) - .then(|| damage::list(last_primitives, primitives)) + .then(|| damage::list(&last_primitives, primitives)) }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); - if damage.is_empty() { - return Ok(()); - } - - surface.primitives = Some(primitives.to_vec()); + surface.primitives.push_back(primitives.to_vec()); surface.background_color = background_color; - let damage = damage::group(damage, scale_factor, physical_size); + if !damage.is_empty() { + let damage = damage::group(damage, scale_factor, physical_size); - backend.draw( - &mut pixels, - &mut surface.clip_mask, - primitives, - viewport, - &damage, - background_color, - overlay, - ); + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); - surface.window.set_buffer( - &surface.buffer, - physical_size.width as u16, - physical_size.height as u16, - ); + backend.draw( + &mut pixels, + &mut surface.clip_mask, + primitives, + viewport, + &damage, + background_color, + overlay, + ); + } - Ok(()) + buffer.present().map_err(|_| compositor::SurfaceError::Lost) } pub fn screenshot<T: AsRef<str>>( -- cgit From 7289b6091b61b0aa448a756cfe32211c78a4cce0 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott <idscott@system76.com> Date: Tue, 9 Jan 2024 07:19:15 -0800 Subject: WIP raw-window-handle 0.6 --- Cargo.toml | 18 ++++++---- graphics/src/compositor.rs | 12 +++---- renderer/src/compositor.rs | 51 ++++++++++++++------------ src/application.rs | 7 +++- tiny_skia/src/window/compositor.rs | 56 +++++++++++++++-------------- wgpu/src/window/compositor.rs | 73 +++++++++++++++++++++++--------------- winit/src/application.rs | 24 +++++++------ winit/src/clipboard.rs | 3 +- 8 files changed, 142 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1f2da434..421c7c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,8 @@ thiserror.workspace = true image.workspace = true image.optional = true +winit.workspace = true + [profile.release-opt] inherits = "release" codegen-units = 1 @@ -126,7 +128,10 @@ bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = "0.10" futures = "0.3" glam = "0.24" -glyphon = "0.4" +# glyphon = "0.4" +# TODO update for wgpu 0.19 +# https://github.com/grovesNL/glyphon/pull/80 +glyphon = { git = "https://github.com/EggShark/glyphon" } guillotiere = "0.6" half = "2.2" image = "0.24" @@ -140,12 +145,12 @@ once_cell = "1.0" ouroboros = "0.17" palette = "0.7" qrcode = { version = "0.12", default-features = false } -raw-window-handle = "0.5" +raw-window-handle = "0.6" resvg = "0.36" rustc-hash = "1.0" smol = "1.0" smol_str = "0.2" -softbuffer = "0.3.4" +softbuffer = "0.4.1" syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" @@ -158,7 +163,8 @@ wasm-bindgen-futures = "0.4" wasm-timer = "0.2" web-sys = "0.3" web-time = "0.2" -wgpu = "0.18" +wgpu = "0.19" winapi = "0.3" -window_clipboard = "0.3" -winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5", features = ["rwh_05"] } +# window_clipboard = "0.3" +window_clipboard = { git = "https://github.com/ids1024/window_clipboard", branch = "raw-window-handle-0.6" } +winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5", features = ["rwh_06"] } diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index b8b575b4..6a4c7909 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -4,11 +4,11 @@ use crate::{Error, Viewport}; use iced_core::Color; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use thiserror::Error; /// A graphics compositor that can draw to windows. -pub trait Compositor: Sized { +pub trait Compositor<W: HasWindowHandle + HasDisplayHandle>: Sized { /// The settings of the backend. type Settings: Default; @@ -19,9 +19,9 @@ pub trait Compositor: Sized { type Surface; /// Creates a new [`Compositor`]. - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new( settings: Self::Settings, - compatible_window: Option<&W>, + compatible_window: Option<W>, ) -> Result<Self, Error>; /// Creates a [`Self::Renderer`] for the [`Compositor`]. @@ -30,9 +30,9 @@ pub trait Compositor: Sized { /// Crates a new [`Surface`] for the given window. /// /// [`Surface`]: Self::Surface - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface( &mut self, - window: &W, + window: W, width: u32, height: u32, ) -> Self::Surface; diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index f9afdea4..17157c66 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -3,29 +3,36 @@ use crate::graphics::compositor::{Information, SurfaceError}; use crate::graphics::{Error, Viewport}; use crate::{Renderer, Settings}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::env; -pub enum Compositor<Theme> { - TinySkia(iced_tiny_skia::window::Compositor<Theme>), +pub enum Compositor<W: HasWindowHandle + HasDisplayHandle, Theme> { + TinySkia(iced_tiny_skia::window::Compositor<W, Theme>), #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Compositor<Theme>), + Wgpu(iced_wgpu::window::Compositor<W, Theme>), } -pub enum Surface { - TinySkia(iced_tiny_skia::window::Surface), +pub enum Surface<W: HasWindowHandle + HasDisplayHandle> { + TinySkia(iced_tiny_skia::window::Surface<W>), #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Surface), + Wgpu(iced_wgpu::window::Surface<'static>), } -impl<Theme> crate::graphics::Compositor for Compositor<Theme> { +// XXX Clone bound +// XXX Send/Sync? +// 'static? +impl< + W: Clone + Send + Sync + HasWindowHandle + HasDisplayHandle + 'static, + Theme, + > crate::graphics::Compositor<W> for Compositor<W, Theme> +{ type Settings = Settings; type Renderer = Renderer<Theme>; - type Surface = Surface; + type Surface = Surface<W>; - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new( settings: Self::Settings, - compatible_window: Option<&W>, + compatible_window: Option<W>, ) -> Result<Self, Error> { let candidates = Candidate::list_from_env().unwrap_or(Candidate::default_list()); @@ -33,7 +40,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { let mut error = Error::GraphicsAdapterNotFound; for candidate in candidates { - match candidate.build(settings, compatible_window) { + match candidate.build(settings, compatible_window.clone()) { Ok(compositor) => return Ok(compositor), Err(new_error) => { error = new_error; @@ -56,12 +63,12 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } } - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface( &mut self, - window: &W, + window: W, width: u32, height: u32, - ) -> Surface { + ) -> Surface<W> { match self { Self::TinySkia(compositor) => Surface::TinySkia( compositor.create_surface(window, width, height), @@ -75,7 +82,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn configure_surface( &mut self, - surface: &mut Surface, + surface: &mut Surface<W>, width: u32, height: u32, ) { @@ -114,7 +121,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ( Self::TinySkia(_compositor), crate::Renderer::TinySkia(renderer), - Surface::TinySkia(surface), + Surface::TinySkia(ref mut surface), ) => renderer.with_primitives(|backend, primitives| { iced_tiny_skia::window::compositor::present( backend, @@ -129,7 +136,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ( Self::Wgpu(compositor), crate::Renderer::Wgpu(renderer), - Surface::Wgpu(surface), + Surface::Wgpu(ref mut surface), ) => renderer.with_primitives(|backend, primitives| { iced_wgpu::window::compositor::present( compositor, @@ -161,7 +168,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ( Self::TinySkia(_compositor), Renderer::TinySkia(renderer), - Surface::TinySkia(surface), + Surface::TinySkia(ref mut surface), ) => renderer.with_primitives(|backend, primitives| { iced_tiny_skia::window::compositor::screenshot( surface, @@ -226,11 +233,11 @@ impl Candidate { ) } - fn build<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( + fn build<Theme, W: HasWindowHandle + HasDisplayHandle + Send + Sync>( self, settings: Settings, - _compatible_window: Option<&W>, - ) -> Result<Compositor<Theme>, Error> { + _compatible_window: Option<W>, + ) -> Result<Compositor<W, Theme>, Error> { match self { Self::TinySkia => { let compositor = iced_tiny_skia::window::compositor::new( diff --git a/src/application.rs b/src/application.rs index 9518b8c5..d7be6719 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,6 +1,8 @@ //! Build interactive cross-platform applications. use crate::{Command, Element, Executor, Settings, Subscription}; +use std::sync::Arc; + pub use crate::style::application::{Appearance, StyleSheet}; /// An interactive cross-platform application. @@ -208,7 +210,10 @@ pub trait Application: Sized { Ok(crate::shell::application::run::< Instance<Self>, Self::Executor, - crate::renderer::Compositor<Self::Theme>, + crate::renderer::Compositor< + Arc<winit::window::Window>, + Self::Theme, + >, >(settings.into(), renderer_settings)?) } } diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index d99b85d4..788d7297 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -4,33 +4,36 @@ use crate::graphics::damage; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::collections::VecDeque; use std::marker::PhantomData; use std::num::NonZeroU32; -pub struct Compositor<Theme> { - context: Option<softbuffer::Context>, +pub struct Compositor<W: HasDisplayHandle + HasWindowHandle, Theme> { + context: Option<softbuffer::Context<W>>, settings: Settings, _theme: PhantomData<Theme>, } -pub struct Surface { - window: softbuffer::Surface, +pub struct Surface<W: HasDisplayHandle + HasWindowHandle> { + window: softbuffer::Surface<W, W>, clip_mask: tiny_skia::Mask, // Primitives of existing buffers, by decreasing age primitives: VecDeque<Vec<Primitive>>, background_color: Color, } -impl<Theme> crate::graphics::Compositor for Compositor<Theme> { +// XXX avoid clone bound? +impl<W: HasDisplayHandle + HasWindowHandle + Clone, Theme> + crate::graphics::Compositor<W> for Compositor<W, Theme> +{ type Settings = Settings; type Renderer = Renderer<Theme>; - type Surface = Surface; + type Surface = Surface<W>; - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new( settings: Self::Settings, - compatible_window: Option<&W>, + compatible_window: Option<W>, ) -> Result<Self, Error> { Ok(new(settings, compatible_window)) } @@ -43,20 +46,19 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ) } - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface( &mut self, - window: &W, + window: W, width: u32, height: u32, - ) -> Surface { - #[allow(unsafe_code)] + ) -> Surface<W> { let window = if let Some(context) = self.context.as_ref() { - unsafe { softbuffer::Surface::new(context, window) } + softbuffer::Surface::new(context, window) .expect("Create softbuffer surface for window") } else { - let context = unsafe { softbuffer::Context::new(window) } + let context = softbuffer::Context::new(window.clone()) .expect("Create softbuffer context for window"); - unsafe { softbuffer::Surface::new(&context, window) } + softbuffer::Surface::new(&context, window) .expect("Create softbuffer surface for window") }; @@ -71,7 +73,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn configure_surface( &mut self, - surface: &mut Surface, + surface: &mut Surface<W>, width: u32, height: u32, ) { @@ -90,7 +92,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, + surface: &mut Surface<W>, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -128,13 +130,13 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { } } -pub fn new<W: HasRawWindowHandle + HasRawDisplayHandle, Theme>( +pub fn new<W: HasWindowHandle + HasDisplayHandle, Theme>( settings: Settings, - compatible_window: Option<&W>, -) -> Compositor<Theme> { + compatible_window: Option<W>, +) -> Compositor<W, Theme> { #[allow(unsafe_code)] - let context = compatible_window - .and_then(|w| unsafe { softbuffer::Context::new(w) }.ok()); + let context = + compatible_window.and_then(|w| softbuffer::Context::new(w).ok()); Compositor { context, settings, @@ -142,9 +144,9 @@ pub fn new<W: HasRawWindowHandle + HasRawDisplayHandle, Theme>( } } -pub fn present<T: AsRef<str>>( +pub fn present<W: HasDisplayHandle + HasWindowHandle, T: AsRef<str>>( backend: &mut Backend, - surface: &mut Surface, + surface: &mut Surface<W>, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -216,8 +218,8 @@ pub fn present<T: AsRef<str>>( buffer.present().map_err(|_| compositor::SurfaceError::Lost) } -pub fn screenshot<T: AsRef<str>>( - surface: &mut Surface, +pub fn screenshot<W: HasDisplayHandle + HasWindowHandle, T: AsRef<str>>( + surface: &mut Surface<W>, backend: &mut Backend, primitives: &[Primitive], viewport: &Viewport, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 090e0e9f..e2dc4901 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,13 +6,13 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::marker::PhantomData; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] -pub struct Compositor<Theme> { +pub struct Compositor<W, Theme> { settings: Settings, instance: wgpu::Instance, adapter: wgpu::Adapter, @@ -20,15 +20,18 @@ pub struct Compositor<Theme> { queue: wgpu::Queue, format: wgpu::TextureFormat, theme: PhantomData<Theme>, + w: PhantomData<W>, } -impl<Theme> Compositor<Theme> { +impl<W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, Theme> + Compositor<W, Theme> +{ /// Requests a new [`Compositor`] with the given [`Settings`]. /// /// Returns `None` if no compatible graphics adapter could be found. - pub async fn request<W: HasRawWindowHandle + HasRawDisplayHandle>( + pub async fn request( settings: Settings, - compatible_window: Option<&W>, + compatible_window: Option<W>, ) -> Option<Self> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.internal_backend, @@ -41,6 +44,7 @@ impl<Theme> Compositor<Theme> { if log::max_level() >= log::LevelFilter::Info { let available_adapters: Vec<_> = instance .enumerate_adapters(settings.internal_backend) + .iter() .map(|adapter| adapter.get_info()) .collect(); log::info!("Available adapters: {available_adapters:#?}"); @@ -48,7 +52,7 @@ impl<Theme> Compositor<Theme> { #[allow(unsafe_code)] let compatible_surface = compatible_window - .and_then(|window| unsafe { instance.create_surface(window).ok() }); + .and_then(|window| instance.create_surface(window).ok()); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { @@ -100,14 +104,14 @@ impl<Theme> Compositor<Theme> { let (device, queue) = loop { - let limits = limits.next()?; + let required_limits = limits.next()?; let device = adapter.request_device( &wgpu::DeviceDescriptor { label: Some( "iced_wgpu::window::compositor device descriptor", ), - features: wgpu::Features::empty(), - limits, + required_features: wgpu::Features::empty(), + required_limits, }, None, ).await.ok(); @@ -125,6 +129,7 @@ impl<Theme> Compositor<Theme> { queue, format, theme: PhantomData, + w: PhantomData, }) } @@ -136,10 +141,13 @@ impl<Theme> Compositor<Theme> { /// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and /// window. -pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( +pub fn new< + Theme, + W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, +>( settings: Settings, - compatible_window: Option<&W>, -) -> Result<Compositor<Theme>, Error> { + compatible_window: Option<W>, +) -> Result<Compositor<W, Theme>, Error> { let compositor = futures::executor::block_on(Compositor::request( settings, compatible_window, @@ -150,10 +158,10 @@ pub fn new<Theme, W: HasRawWindowHandle + HasRawDisplayHandle>( } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. -pub fn present<Theme, T: AsRef<str>>( - compositor: &mut Compositor<Theme>, +pub fn present<W, Theme, T: AsRef<str>>( + compositor: &mut Compositor<W, Theme>, backend: &mut Backend, - surface: &mut wgpu::Surface, + surface: &mut wgpu::Surface<'static>, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -204,14 +212,19 @@ pub fn present<Theme, T: AsRef<str>>( } } -impl<Theme> graphics::Compositor for Compositor<Theme> { +impl< + W: HasDisplayHandle + HasWindowHandle + wgpu::WasmNotSendSync + 'static, + Theme, + > graphics::Compositor<W> for Compositor<W, Theme> +{ type Settings = Settings; type Renderer = Renderer<Theme>; - type Surface = wgpu::Surface; + // XXX generic instead of 'static + type Surface = wgpu::Surface<'static>; - fn new<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn new( settings: Self::Settings, - compatible_window: Option<&W>, + compatible_window: Option<W>, ) -> Result<Self, Error> { new(settings, compatible_window) } @@ -224,14 +237,15 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { ) } - fn create_surface<W: HasRawWindowHandle + HasRawDisplayHandle>( + fn create_surface( &mut self, - window: &W, + window: W, width: u32, height: u32, - ) -> wgpu::Surface { - #[allow(unsafe_code)] - let mut surface = unsafe { self.instance.create_surface(window) } + ) -> wgpu::Surface<'static> { + let mut surface = self + .instance + .create_surface(window) .expect("Create surface"); self.configure_surface(&mut surface, width, height); @@ -241,7 +255,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn configure_surface( &mut self, - surface: &mut Self::Surface, + surface: &mut wgpu::Surface<'static>, width: u32, height: u32, ) { @@ -255,6 +269,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { height, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], + desired_maximum_frame_latency: 2, }, ); } @@ -271,7 +286,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, + surface: &mut wgpu::Surface<'static>, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -292,7 +307,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn screenshot<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - _surface: &mut Self::Surface, + _surface: &mut wgpu::Surface<'static>, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -313,8 +328,8 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { /// Renders the current surface to an offscreen buffer. /// /// Returns RGBA bytes of the texture data. -pub fn screenshot<Theme, T: AsRef<str>>( - compositor: &Compositor<Theme>, +pub fn screenshot<W, Theme, T: AsRef<str>>( + compositor: &Compositor<W, Theme>, backend: &mut Backend, primitives: &[Primitive], viewport: &Viewport, diff --git a/winit/src/application.rs b/winit/src/application.rs index bf48538d..d639a36b 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -24,6 +24,7 @@ use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; use std::mem::ManuallyDrop; +use std::sync::Arc; /// An interactive, native cross-platform application. /// @@ -105,7 +106,7 @@ pub fn run<A, E, C>( where A: Application + 'static, E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, + C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use futures::task; @@ -149,9 +150,12 @@ where log::debug!("Window builder: {builder:#?}"); - let window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; + // XXX Arc? + let window = Arc::new( + builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?, + ); #[cfg(target_arch = "wasm32")] { @@ -183,7 +187,7 @@ where }; } - let compositor = C::new(compositor_settings, Some(&window))?; + let compositor = C::new(compositor_settings, Some(window.clone()))?; let mut renderer = compositor.create_renderer(); for font in settings.fonts { @@ -248,13 +252,13 @@ async fn run_instance<A, E, C>( >, mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, init_command: Command<A::Message>, - window: winit::window::Window, + window: Arc<winit::window::Window>, should_be_visible: bool, exit_on_close_request: bool, ) where A: Application + 'static, E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, + C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use futures::stream::StreamExt; @@ -268,7 +272,7 @@ async fn run_instance<A, E, C>( let mut clipboard = Clipboard::connect(&window); let mut cache = user_interface::Cache::default(); let mut surface = compositor.create_surface( - &window, + window.clone(), physical_size.width, physical_size.height, ); @@ -608,7 +612,7 @@ pub fn update<A: Application, C, E: Executor>( messages: &mut Vec<A::Message>, window: &winit::window::Window, ) where - C: Compositor<Renderer = A::Renderer> + 'static, + C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { @@ -659,7 +663,7 @@ pub fn run_command<A, C, E>( ) where A: Application, E: Executor, - C: Compositor<Renderer = A::Renderer> + 'static, + C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use crate::runtime::command; diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index f7a32868..8f5c5e63 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -15,7 +15,8 @@ enum State { impl Clipboard { /// Creates a new [`Clipboard`] for the given window. pub fn connect(window: &winit::window::Window) -> Clipboard { - let state = window_clipboard::Clipboard::connect(window) + #[allow(unsafe_code)] + let state = unsafe { window_clipboard::Clipboard::connect(window) } .ok() .map(State::Connected) .unwrap_or(State::Unavailable); -- cgit From 985acb2b1532b3e56161bd35201c4a2e21a86b85 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 08:05:19 +0100 Subject: Fine-tune event loop of `multi-window` applications --- winit/src/multi_window.rs | 219 ++++++++++++++++++++++++++-------------------- 1 file changed, 123 insertions(+), 96 deletions(-) diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index dc659c1a..84c81bea 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -229,7 +229,21 @@ where task::Poll::Pending => match control_receiver.try_next() { Ok(Some(control)) => match control { Control::ChangeFlow(flow) => { - event_loop.set_control_flow(flow); + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if new < current => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } } Control::CreateWindow { id, @@ -362,7 +376,6 @@ async fn run_instance<A, E, C>( runtime.track(application.subscription().into_recipes()); let mut messages = Vec::new(); - let mut redraw_pending = false; debug.startup_finished(); @@ -409,13 +422,15 @@ async fn run_instance<A, E, C>( } Event::EventLoopAwakened(event) => { match event { - event::Event::NewEvents(start_cause) => { - redraw_pending = matches!( - start_cause, - event::StartCause::Init - | event::StartCause::Poll - | event::StartCause::ResumeTimeReached { .. } - ); + event::Event::NewEvents( + event::StartCause::Init + | event::StartCause::ResumeTimeReached { .. }, + ) => { + for (_id, window) in window_manager.iter_mut() { + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + } } event::Event::PlatformSpecific( event::PlatformSpecific::MacOS( @@ -503,7 +518,9 @@ async fn run_instance<A, E, C>( redraw_request: Some(redraw_request), } => match redraw_request { window::RedrawRequest::NextFrame => { - ControlFlow::Poll + window.raw.request_redraw(); + + ControlFlow::Wait } window::RedrawRequest::At(at) => { ControlFlow::WaitUntil(at) @@ -653,103 +670,113 @@ async fn run_instance<A, E, C>( } } } - _ => {} - } - } - } + event::Event::AboutToWait => { + if events.is_empty() && messages.is_empty() { + continue; + } - debug.event_processing_started(); - let mut uis_stale = false; + debug.event_processing_started(); + let mut uis_stale = false; - for (id, window) in window_manager.iter_mut() { - let mut window_events = vec![]; + for (id, window) in window_manager.iter_mut() { + let mut window_events = vec![]; - events.retain(|(window_id, event)| { - if *window_id == Some(id) || window_id.is_none() { - window_events.push(event.clone()); - false - } else { - true - } - }); + events.retain(|(window_id, event)| { + if *window_id == Some(id) || window_id.is_none() + { + window_events.push(event.clone()); + false + } else { + true + } + }); - if !redraw_pending - && window_events.is_empty() - && messages.is_empty() - { - continue; - } + if window_events.is_empty() && messages.is_empty() { + continue; + } - let (ui_state, statuses) = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .update( - &window_events, - window.state.cursor(), - &mut window.renderer, - &mut clipboard, - &mut messages, - ); + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); - if !uis_stale { - uis_stale = matches!(ui_state, user_interface::State::Outdated); - } + window.raw.request_redraw(); - for (event, status) in - window_events.into_iter().zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } - } + if !uis_stale { + uis_stale = matches!( + ui_state, + user_interface::State::Outdated + ); + } - debug.event_processing_finished(); - - // TODO mw application update returns which window IDs to update - if !messages.is_empty() || uis_stale { - let mut cached_interfaces: HashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); - - // Update application - update( - &mut application, - &mut compositor, - &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, - &mut debug, - &mut messages, - &mut window_manager, - &mut cached_interfaces, - ); - - // we must synchronize all window states with application state after an - // application update since we don't know what changed - for (id, window) in window_manager.iter_mut() { - window.state.synchronize(&application, id, &window.raw); - } + for (event, status) in window_events + .into_iter() + .zip(statuses.into_iter()) + { + runtime.broadcast(event, status); + } + } - // rebuild UIs with the synchronized states - user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut debug, - &mut window_manager, - cached_interfaces, - )); - } + debug.event_processing_finished(); + + // TODO mw application update returns which window IDs to update + if !messages.is_empty() || uis_stale { + let mut cached_interfaces: HashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + // Update application + update( + &mut application, + &mut compositor, + &mut runtime, + &mut clipboard, + &mut control_sender, + &mut proxy, + &mut debug, + &mut messages, + &mut window_manager, + &mut cached_interfaces, + ); - for (_id, window) in window_manager.iter_mut() { - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - window.raw.request_redraw(); - } + // we must synchronize all window states with application state after an + // application update since we don't know what changed + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &application, + id, + &window.raw, + ); + + // TODO once widgets can request to be redrawn, we can avoid always requesting a + // redraw + window.raw.request_redraw(); + } - redraw_pending = false; + // rebuild UIs with the synchronized states + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &application, + &mut debug, + &mut window_manager, + cached_interfaces, + )); + } + } + _ => {} + } + } + } } let _ = ManuallyDrop::into_inner(user_interfaces); -- cgit From 468f7432dd96839a86a7bac751351fcf43b7ae63 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector0193@gmail.com> Date: Tue, 20 Dec 2022 10:42:53 +0100 Subject: Add `vectorial_text` example --- examples/vectorial_text/Cargo.toml | 9 ++ examples/vectorial_text/src/main.rs | 175 ++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 examples/vectorial_text/Cargo.toml create mode 100644 examples/vectorial_text/src/main.rs diff --git a/examples/vectorial_text/Cargo.toml b/examples/vectorial_text/Cargo.toml new file mode 100644 index 00000000..76c1af7c --- /dev/null +++ b/examples/vectorial_text/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "vectorial_text" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../..", features = ["canvas", "debug"] } diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs new file mode 100644 index 00000000..54ca7c5e --- /dev/null +++ b/examples/vectorial_text/src/main.rs @@ -0,0 +1,175 @@ +use iced::alignment::{self, Alignment}; +use iced::mouse; +use iced::widget::{ + canvas, checkbox, column, horizontal_space, row, slider, text, +}; +use iced::{ + Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Theme, + Vector, +}; + +pub fn main() -> iced::Result { + VectorialText::run(Settings { + antialiasing: true, + ..Settings::default() + }) +} + +struct VectorialText { + state: State, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + SizeChanged(f32), + AngleChanged(f32), + ScaleChanged(f32), + ToggleJapanese(bool), +} + +impl Sandbox for VectorialText { + type Message = Message; + + fn new() -> Self { + Self { + state: State::new(), + } + } + + fn title(&self) -> String { + String::from("Vectorial Text - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::SizeChanged(size) => { + self.state.size = size; + } + Message::AngleChanged(angle) => { + self.state.angle = angle; + } + Message::ScaleChanged(scale) => { + self.state.scale = scale; + } + Message::ToggleJapanese(use_japanese) => { + self.state.use_japanese = use_japanese; + } + } + + self.state.cache.clear(); + } + + fn view(&self) -> Element<Message> { + let slider_with_label = |label, range, value, message: fn(f32) -> _| { + column![ + row![ + text(label), + horizontal_space(Length::Fill), + text(format!("{:.2}", value)) + ], + slider(range, value, message).step(0.01) + ] + .spacing(2) + }; + + column![ + canvas(&self.state).width(Length::Fill).height(Length::Fill), + column![ + checkbox( + "Use Japanese", + self.state.use_japanese, + Message::ToggleJapanese + ), + row![ + slider_with_label( + "Size", + 2.0..=80.0, + self.state.size, + Message::SizeChanged, + ), + slider_with_label( + "Angle", + 0.0..=360.0, + self.state.angle, + Message::AngleChanged, + ), + slider_with_label( + "Scale", + 1.0..=20.0, + self.state.scale, + Message::ScaleChanged, + ), + ] + .spacing(20), + ] + .align_items(Alignment::Center) + .spacing(10) + ] + .spacing(10) + .padding(20) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } +} + +struct State { + size: f32, + angle: f32, + scale: f32, + use_japanese: bool, + cache: canvas::Cache, +} + +impl State { + pub fn new() -> Self { + Self { + size: 40.0, + angle: 0.0, + scale: 1.0, + use_japanese: false, + cache: canvas::Cache::new(), + } + } +} + +impl<Message> canvas::Program<Message> for State { + type State = (); + + fn draw( + &self, + _state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec<canvas::Geometry> { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { + let palette = theme.palette(); + let center = bounds.center(); + + frame.translate(Vector::new(center.x, center.y)); + frame.scale(self.scale); + frame.rotate(self.angle * std::f32::consts::PI / 180.0); + + frame.fill_text(canvas::Text { + position: Point::new(0.0, 0.0), + color: palette.text, + size: self.size.into(), + content: String::from(if self.use_japanese { + "ベクトルテキスト🎉" + } else { + "Vectorial Text! 🎉" + }), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + ..canvas::Text::default() + }) + }); + + vec![geometry] + } +} -- cgit From 66bea7bb6d4575c1d36d28a10e08dc60a0ea20b0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 13:22:02 +0100 Subject: Apply scaling during `Frame::fill_text` in `iced_wgpu` --- wgpu/src/geometry.rs | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index e0bff67e..36092da0 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,4 +1,5 @@ //! Build and draw geometry. +use crate::core::text::LineHeight; use crate::core::{Point, Rectangle, Size, Vector}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; @@ -318,14 +319,41 @@ impl Frame { pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let position = if self.transforms.current.is_identity { - text.position + let (position, size, line_height) = if self + .transforms + .current + .is_identity + { + (text.position, text.size, text.line_height) } else { - let transformed = self.transforms.current.raw.transform_point( + let position = self.transforms.current.raw.transform_point( lyon::math::Point::new(text.position.x, text.position.y), ); - Point::new(transformed.x, transformed.y) + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(0.0, text.size.0), + ); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + let new_height = self + .transforms + .current + .raw + .transform_vector(lyon::math::Vector::new(0.0, size.0)) + .y; + + LineHeight::Absolute(new_height.into()) + } + LineHeight::Relative(factor) => LineHeight::Relative(factor), + }; + + ( + Point::new(position.x, position.y), + size.y.into(), + line_height, + ) }; let bounds = Rectangle { @@ -340,8 +368,8 @@ impl Frame { content: text.content, bounds, color: text.color, - size: text.size, - line_height: text.line_height, + size, + line_height, font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, -- cgit From 5aa741a177e6220640ea884827f93f152cbd07d1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 13:27:39 +0100 Subject: Apply scaling during `Frame::fill_text` in `iced_tiny_skia` --- tiny_skia/src/geometry.rs | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 5f28b737..4cc04c6e 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,4 +1,5 @@ -use crate::core::{Point, Rectangle, Size, Vector}; +use crate::core::text::LineHeight; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{Path, Style, Text}; @@ -96,17 +97,32 @@ impl Frame { pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let position = if self.transform.is_identity() { - text.position + let (position, size, line_height) = if self.transform.is_identity() { + (text.position, text.size, text.line_height) } else { - let mut transformed = [tiny_skia::Point { + let mut position = [tiny_skia::Point { x: text.position.x, y: text.position.y, }]; - self.transform.map_points(&mut transformed); + self.transform.map_points(&mut position); - Point::new(transformed[0].x, transformed[0].y) + let (_, scale_y) = self.transform.get_scale(); + + let size = text.size.0 * scale_y; + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => LineHeight::Relative(factor), + }; + + ( + Point::new(position[0].x, position[0].y), + size.into(), + line_height, + ) }; let bounds = Rectangle { @@ -121,8 +137,8 @@ impl Frame { content: text.content, bounds, color: text.color, - size: text.size, - line_height: text.line_height, + size, + line_height, font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, -- cgit From fda96a9eda261b9fbe499eae1c6eedcfa252c5ea Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 13:44:30 +0100 Subject: Simplify `Transform` API in `iced_wgpu::geometry` --- wgpu/src/geometry.rs | 136 +++++++++++++++++++++++---------------------------- 1 file changed, 62 insertions(+), 74 deletions(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 36092da0..04718441 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,6 +1,6 @@ //! Build and draw geometry. use crate::core::text::LineHeight; -use crate::core::{Point, Rectangle, Size, Vector}; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -116,19 +116,26 @@ struct Transforms { } #[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} +struct Transform(lyon::math::Transform); impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { + fn is_identity(&self) -> bool { + self.0 == lyon::math::Transform::identity() + } + + fn scale(&self) -> (f32, f32) { + (self.0.m12, self.0.m22) + } + + fn transform_point(&self, point: Point) -> Point { let transformed = self - .raw + .0 .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; + + Point { + x: transformed.x, + y: transformed.y, + } } fn transform_style(&self, style: Style) -> Style { @@ -143,8 +150,8 @@ impl Transform { fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { match &mut gradient { Gradient::Linear(linear) => { - self.transform_point(&mut linear.start); - self.transform_point(&mut linear.end); + linear.start = self.transform_point(linear.start); + linear.end = self.transform_point(linear.end); } } @@ -164,10 +171,7 @@ impl Frame { primitives: Vec::new(), transforms: Transforms { previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, + current: Transform(lyon::math::Transform::identity()), }, fill_tessellator: tessellation::FillTessellator::new(), stroke_tessellator: tessellation::StrokeTessellator::new(), @@ -210,14 +214,14 @@ impl Frame { let options = tessellation::FillOptions::default() .with_fill_rule(into_fill_rule(rule)); - if self.transforms.current.is_identity { + if self.transforms.current.is_identity() { self.fill_tessellator.tessellate_path( path.raw(), &options, buffer.as_mut(), ) } else { - let path = path.transform(&self.transforms.current.raw); + let path = path.transform(&self.transforms.current.0); self.fill_tessellator.tessellate_path( path.raw(), @@ -242,13 +246,14 @@ impl Frame { .buffers .get_fill(&self.transforms.current.transform_style(style)); - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); let size = - self.transforms.current.raw.transform_vector( + self.transforms.current.0.transform_vector( lyon::math::Vector::new(size.width, size.height), ); @@ -285,14 +290,14 @@ impl Frame { Cow::Owned(dashed(path, stroke.line_dash)) }; - if self.transforms.current.is_identity { + if self.transforms.current.is_identity() { self.stroke_tessellator.tessellate_path( path.raw(), &options, buffer.as_mut(), ) } else { - let path = path.transform(&self.transforms.current.raw); + let path = path.transform(&self.transforms.current.0); self.stroke_tessellator.tessellate_path( path.raw(), @@ -319,42 +324,28 @@ impl Frame { pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let (position, size, line_height) = if self - .transforms - .current - .is_identity - { - (text.position, text.size, text.line_height) - } else { - let position = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); + let (position, size, line_height) = + if self.transforms.current.is_identity() { + (text.position, text.size, text.line_height) + } else { + let (_, scale_y) = self.transforms.current.scale(); - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(0.0, text.size.0), - ); - - let line_height = match text.line_height { - LineHeight::Absolute(size) => { - let new_height = self - .transforms - .current - .raw - .transform_vector(lyon::math::Vector::new(0.0, size.0)) - .y; - - LineHeight::Absolute(new_height.into()) - } - LineHeight::Relative(factor) => LineHeight::Relative(factor), - }; + let position = + self.transforms.current.transform_point(text.position); - ( - Point::new(position.x, position.y), - size.y.into(), - line_height, - ) - }; + let size = Pixels(text.size.0 * scale_y); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + (position, size, line_height) + }; let bounds = Rectangle { x: position.x, @@ -451,26 +442,24 @@ impl Frame { /// Applies a translation to the current transform of the [`Frame`]. #[inline] pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; + self.transforms.current.0 = + self.transforms + .current + .0 + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); } /// Applies a rotation in radians to the current transform of the [`Frame`]. #[inline] pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self + self.transforms.current.0 = self .transforms .current - .raw + .0 .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; } /// Applies a uniform scaling to the current transform of the [`Frame`]. @@ -486,9 +475,8 @@ impl Frame { pub fn scale_nonuniform(&mut self, scale: impl Into<Vector>) { let scale = scale.into(); - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale.x, scale.y); - self.transforms.current.is_identity = false; + self.transforms.current.0 = + self.transforms.current.0.pre_scale(scale.x, scale.y); } /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. -- cgit From d09f36e054b00cad206431654392fc68ba2b345b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 13:45:29 +0100 Subject: Fix missing semi-colon lint in `vectorial_text` example --- examples/vectorial_text/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/vectorial_text/src/main.rs b/examples/vectorial_text/src/main.rs index 54ca7c5e..d366b907 100644 --- a/examples/vectorial_text/src/main.rs +++ b/examples/vectorial_text/src/main.rs @@ -167,7 +167,7 @@ impl<Message> canvas::Program<Message> for State { vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, ..canvas::Text::default() - }) + }); }); vec![geometry] -- cgit From dd032d9a7a73dc28c12802e1e702d0aebe92e261 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 14:25:39 +0100 Subject: Implement vectorial text support for `iced_wgpu` --- wgpu/src/geometry.rs | 225 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 188 insertions(+), 37 deletions(-) diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index 04718441..a1583a07 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,6 +1,7 @@ //! Build and draw geometry. +use crate::core::alignment; use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Rectangle, Size, Vector}; +use crate::core::{Color, Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -8,6 +9,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; +use crate::graphics::text::{self, cosmic_text}; use crate::primitive::{self, Primitive}; use lyon::geom::euclid; @@ -123,8 +125,13 @@ impl Transform { self.0 == lyon::math::Transform::identity() } + fn is_scale_translation(&self) -> bool { + self.0.m12.abs() < 2.0 * f32::EPSILON + && self.0.m21.abs() < 2.0 * f32::EPSILON + } + fn scale(&self) -> (f32, f32) { - (self.0.m12, self.0.m22) + (self.0.m11, self.0.m22) } fn transform_point(&self, point: Point) -> Point { @@ -324,49 +331,193 @@ impl Frame { pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let (position, size, line_height) = - if self.transforms.current.is_identity() { - (text.position, text.size, text.line_height) - } else { - let (_, scale_y) = self.transforms.current.scale(); + let (scale_x, scale_y) = self.transforms.current.scale(); + + if self.transforms.current.is_scale_translation() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = + if self.transforms.current.is_identity() { + (text.position, text.size, text.line_height) + } else { + let position = + self.transforms.current.transform_point(text.position); + + let size = Pixels(text.size.0 * scale_y); + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + (position, size, line_height) + }; - let position = - self.transforms.current.transform_point(text.position); + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }; - let size = Pixels(text.size.0 * scale_y); + // TODO: Honor layering! + self.primitives.push(Primitive::Text { + content: text.content, + bounds, + color: text.color, + size, + line_height, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITY), + }); + } else { + let mut font_system = + text::font_system().write().expect("Write font system"); - let line_height = match text.line_height { - LineHeight::Absolute(size) => { - LineHeight::Absolute(Pixels(size.0 * scale_y)) - } - LineHeight::Relative(factor) => { - LineHeight::Relative(factor) + let mut buffer = cosmic_text::BufferLine::new( + &text.content, + cosmic_text::AttrsList::new(text::to_attributes(text.font)), + text::to_shaping(text.shaping), + ); + + let layout = buffer.layout( + font_system.raw(), + text.size.0, + f32::MAX, + cosmic_text::Wrap::None, + ); + + let translation_x = match text.horizontal_alignment { + alignment::Horizontal::Left => text.position.x, + alignment::Horizontal::Center + | alignment::Horizontal::Right => { + let mut line_width = 0.0f32; + + for line in layout.iter() { + line_width = line_width.max(line.w); } - }; - (position, size, line_height) + if text.horizontal_alignment + == alignment::Horizontal::Center + { + text.position.x - line_width / 2.0 + } else { + text.position.x - line_width + } + } }; - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; + let translation_y = { + let line_height = text.line_height.to_absolute(text.size); - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds, - color: text.color, - size, - line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }); + match text.vertical_alignment { + alignment::Vertical::Top => text.position.y, + alignment::Vertical::Center => { + text.position.y - line_height.0 / 2.0 + } + alignment::Vertical::Bottom => { + text.position.y - line_height.0 + } + } + }; + + let mut swash_cache = cosmic_text::SwashCache::new(); + + for run in layout.iter() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y + glyph.y_offset + text.size.0; + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = swash_cache.get_outline_commands( + font_system.raw(), + physical_glyph.cache_key, + ) { + let glyph = Path::new(|path| { + use cosmic_text::Command; + + for command in commands { + match command { + Command::MoveTo(p) => { + path.move_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::LineTo(p) => { + path.line_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::CurveTo( + control_a, + control_b, + to, + ) => { + path.bezier_curve_to( + Point::new( + control_a.x, + -control_a.y, + ) + offset, + Point::new( + control_b.x, + -control_b.y, + ) + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::QuadTo(control, to) => { + path.quadratic_curve_to( + Point::new(control.x, -control.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::Close => { + path.close(); + } + } + } + }); + + self.fill(&glyph, text.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = text.color.into_rgba8(); + + swash_cache.with_pixels( + font_system.raw(), + physical_glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + self.fill( + &Path::rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + ), + Color::from_rgba8( + color.r(), + color.g(), + color.b(), + color.a() as f32 / 255.0, + ), + ); + }, + ) + } + } + } + } } /// Stores the current transform of the [`Frame`] and executes the given -- cgit From 4cb53a6e225f9e533126eb03d3cc34be3fd09f1d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 14:48:33 +0100 Subject: Implement vectorial text support for `iced_tiny_skia` --- graphics/src/geometry/text.rs | 135 ++++++++++++++++++++++++++++++++++++++- tiny_skia/src/geometry.rs | 99 ++++++++++++++++------------- wgpu/src/geometry.rs | 142 +----------------------------------------- 3 files changed, 191 insertions(+), 185 deletions(-) diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 0bf7ec97..d314e85e 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,6 +1,8 @@ use crate::core::alignment; use crate::core::text::{LineHeight, Shaping}; -use crate::core::{Color, Font, Pixels, Point}; +use crate::core::{Color, Font, Pixels, Point, Size, Vector}; +use crate::geometry::Path; +use crate::text; /// A bunch of text that can be drawn to a canvas #[derive(Debug, Clone)] @@ -32,6 +34,137 @@ pub struct Text { pub shaping: Shaping, } +impl Text { + /// Computes the [`Path`]s of the [`Text`] and draws them using + /// the given closure. + pub fn draw_with(&self, mut f: impl FnMut(Path, Color)) { + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::BufferLine::new( + &self.content, + cosmic_text::AttrsList::new(text::to_attributes(self.font)), + text::to_shaping(self.shaping), + ); + + let layout = buffer.layout( + font_system.raw(), + self.size.0, + f32::MAX, + cosmic_text::Wrap::None, + ); + + let translation_x = match self.horizontal_alignment { + alignment::Horizontal::Left => self.position.x, + alignment::Horizontal::Center | alignment::Horizontal::Right => { + let mut line_width = 0.0f32; + + for line in layout.iter() { + line_width = line_width.max(line.w); + } + + if self.horizontal_alignment == alignment::Horizontal::Center { + self.position.x - line_width / 2.0 + } else { + self.position.x - line_width + } + } + }; + + let translation_y = { + let line_height = self.line_height.to_absolute(self.size); + + match self.vertical_alignment { + alignment::Vertical::Top => self.position.y, + alignment::Vertical::Center => { + self.position.y - line_height.0 / 2.0 + } + alignment::Vertical::Bottom => self.position.y - line_height.0, + } + }; + + let mut swash_cache = cosmic_text::SwashCache::new(); + + for run in layout.iter() { + for glyph in run.glyphs.iter() { + let physical_glyph = glyph.physical((0.0, 0.0), 1.0); + + let start_x = translation_x + glyph.x + glyph.x_offset; + let start_y = translation_y + glyph.y_offset + self.size.0; + let offset = Vector::new(start_x, start_y); + + if let Some(commands) = swash_cache.get_outline_commands( + font_system.raw(), + physical_glyph.cache_key, + ) { + let glyph = Path::new(|path| { + use cosmic_text::Command; + + for command in commands { + match command { + Command::MoveTo(p) => { + path.move_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::LineTo(p) => { + path.line_to( + Point::new(p.x, -p.y) + offset, + ); + } + Command::CurveTo(control_a, control_b, to) => { + path.bezier_curve_to( + Point::new(control_a.x, -control_a.y) + + offset, + Point::new(control_b.x, -control_b.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::QuadTo(control, to) => { + path.quadratic_curve_to( + Point::new(control.x, -control.y) + + offset, + Point::new(to.x, -to.y) + offset, + ); + } + Command::Close => { + path.close(); + } + } + } + }); + + f(glyph, self.color); + } else { + // TODO: Raster image support for `Canvas` + let [r, g, b, a] = self.color.into_rgba8(); + + swash_cache.with_pixels( + font_system.raw(), + physical_glyph.cache_key, + cosmic_text::Color::rgba(r, g, b, a), + |x, y, color| { + f( + Path::rectangle( + Point::new(x as f32, y as f32) + offset, + Size::new(1.0, 1.0), + ), + Color::from_rgba8( + color.r(), + color.g(), + color.b(), + color.a() as f32 / 255.0, + ), + ); + }, + ); + } + } + } + } +} + impl Default for Text { fn default() -> Text { Text { diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 4cc04c6e..b00f4676 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -97,54 +97,65 @@ impl Frame { pub fn fill_text(&mut self, text: impl Into<Text>) { let text = text.into(); - let (position, size, line_height) = if self.transform.is_identity() { - (text.position, text.size, text.line_height) - } else { - let mut position = [tiny_skia::Point { - x: text.position.x, - y: text.position.y, - }]; - - self.transform.map_points(&mut position); - - let (_, scale_y) = self.transform.get_scale(); - - let size = text.size.0 * scale_y; + let (scale_x, scale_y) = self.transform.get_scale(); + + if self.transform.is_scale_translate() + && scale_x == scale_y + && scale_x > 0.0 + && scale_y > 0.0 + { + let (position, size, line_height) = if self.transform.is_identity() + { + (text.position, text.size, text.line_height) + } else { + let mut position = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut position); + + let size = text.size.0 * scale_y; + + let line_height = match text.line_height { + LineHeight::Absolute(size) => { + LineHeight::Absolute(Pixels(size.0 * scale_y)) + } + LineHeight::Relative(factor) => { + LineHeight::Relative(factor) + } + }; + + ( + Point::new(position[0].x, position[0].y), + size.into(), + line_height, + ) + }; - let line_height = match text.line_height { - LineHeight::Absolute(size) => { - LineHeight::Absolute(Pixels(size.0 * scale_y)) - } - LineHeight::Relative(factor) => LineHeight::Relative(factor), + let bounds = Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, }; - ( - Point::new(position[0].x, position[0].y), - size.into(), + // TODO: Honor layering! + self.primitives.push(Primitive::Text { + content: text.content, + bounds, + color: text.color, + size, line_height, - ) - }; - - let bounds = Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds, - color: text.color, - size, - line_height, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - shaping: text.shaping, - clip_bounds: Rectangle::with_size(Size::INFINITY), - }); + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + clip_bounds: Rectangle::with_size(Size::INFINITY), + }); + } else { + text.draw_with(|path, color| self.fill(&path, color)); + } } pub fn push_transform(&mut self) { diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index a1583a07..4d7f443e 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,7 +1,6 @@ //! Build and draw geometry. -use crate::core::alignment; use crate::core::text::LineHeight; -use crate::core::{Color, Pixels, Point, Rectangle, Size, Vector}; +use crate::core::{Pixels, Point, Rectangle, Size, Vector}; use crate::graphics::color; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::{ @@ -9,7 +8,6 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::text::{self, cosmic_text}; use crate::primitive::{self, Primitive}; use lyon::geom::euclid; @@ -380,143 +378,7 @@ impl Frame { clip_bounds: Rectangle::with_size(Size::INFINITY), }); } else { - let mut font_system = - text::font_system().write().expect("Write font system"); - - let mut buffer = cosmic_text::BufferLine::new( - &text.content, - cosmic_text::AttrsList::new(text::to_attributes(text.font)), - text::to_shaping(text.shaping), - ); - - let layout = buffer.layout( - font_system.raw(), - text.size.0, - f32::MAX, - cosmic_text::Wrap::None, - ); - - let translation_x = match text.horizontal_alignment { - alignment::Horizontal::Left => text.position.x, - alignment::Horizontal::Center - | alignment::Horizontal::Right => { - let mut line_width = 0.0f32; - - for line in layout.iter() { - line_width = line_width.max(line.w); - } - - if text.horizontal_alignment - == alignment::Horizontal::Center - { - text.position.x - line_width / 2.0 - } else { - text.position.x - line_width - } - } - }; - - let translation_y = { - let line_height = text.line_height.to_absolute(text.size); - - match text.vertical_alignment { - alignment::Vertical::Top => text.position.y, - alignment::Vertical::Center => { - text.position.y - line_height.0 / 2.0 - } - alignment::Vertical::Bottom => { - text.position.y - line_height.0 - } - } - }; - - let mut swash_cache = cosmic_text::SwashCache::new(); - - for run in layout.iter() { - for glyph in run.glyphs.iter() { - let physical_glyph = glyph.physical((0.0, 0.0), 1.0); - - let start_x = translation_x + glyph.x + glyph.x_offset; - let start_y = translation_y + glyph.y_offset + text.size.0; - let offset = Vector::new(start_x, start_y); - - if let Some(commands) = swash_cache.get_outline_commands( - font_system.raw(), - physical_glyph.cache_key, - ) { - let glyph = Path::new(|path| { - use cosmic_text::Command; - - for command in commands { - match command { - Command::MoveTo(p) => { - path.move_to( - Point::new(p.x, -p.y) + offset, - ); - } - Command::LineTo(p) => { - path.line_to( - Point::new(p.x, -p.y) + offset, - ); - } - Command::CurveTo( - control_a, - control_b, - to, - ) => { - path.bezier_curve_to( - Point::new( - control_a.x, - -control_a.y, - ) + offset, - Point::new( - control_b.x, - -control_b.y, - ) + offset, - Point::new(to.x, -to.y) + offset, - ); - } - Command::QuadTo(control, to) => { - path.quadratic_curve_to( - Point::new(control.x, -control.y) - + offset, - Point::new(to.x, -to.y) + offset, - ); - } - Command::Close => { - path.close(); - } - } - } - }); - - self.fill(&glyph, text.color); - } else { - // TODO: Raster image support for `Canvas` - let [r, g, b, a] = text.color.into_rgba8(); - - swash_cache.with_pixels( - font_system.raw(), - physical_glyph.cache_key, - cosmic_text::Color::rgba(r, g, b, a), - |x, y, color| { - self.fill( - &Path::rectangle( - Point::new(x as f32, y as f32) + offset, - Size::new(1.0, 1.0), - ), - Color::from_rgba8( - color.r(), - color.g(), - color.b(), - color.a() as f32 / 255.0, - ), - ); - }, - ) - } - } - } + text.draw_with(|path, color| self.fill(&path, color)); } } -- cgit From acee3b030baf4df24a871e56789772c677b66bcf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 15:31:29 +0100 Subject: Fix paths with negative coordinates in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 18 ++++++++++-------- tiny_skia/src/geometry.rs | 17 +++++++++++------ tiny_skia/src/primitive.rs | 4 ---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 706db40e..d1393b4d 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -543,7 +543,6 @@ impl Backend { path, paint, rule, - transform, }) => { let bounds = path.bounds(); @@ -566,9 +565,11 @@ impl Backend { path, paint, *rule, - transform - .post_translate(translation.x, translation.y) - .post_scale(scale_factor, scale_factor), + tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor), clip_mask, ); } @@ -576,7 +577,6 @@ impl Backend { path, paint, stroke, - transform, }) => { let bounds = path.bounds(); @@ -599,9 +599,11 @@ impl Backend { path, paint, stroke, - transform - .post_translate(translation.x, translation.y) - .post_scale(scale_factor, scale_factor), + tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor), clip_mask, ); } diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index b00f4676..501638e0 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -40,9 +40,12 @@ impl Frame { } pub fn fill(&mut self, path: &Path, fill: impl Into<Fill>) { - let Some(path) = convert_path(path) else { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { return; }; + let fill = fill.into(); self.primitives @@ -50,7 +53,6 @@ impl Frame { path, paint: into_paint(fill.style), rule: into_fill_rule(fill.rule), - transform: self.transform, })); } @@ -60,9 +62,12 @@ impl Frame { size: Size, fill: impl Into<Fill>, ) { - let Some(path) = convert_path(&Path::rectangle(top_left, size)) else { + let Some(path) = convert_path(&Path::rectangle(top_left, size)) + .and_then(|path| path.transform(self.transform)) + else { return; }; + let fill = fill.into(); self.primitives @@ -73,12 +78,13 @@ impl Frame { ..into_paint(fill.style) }, rule: into_fill_rule(fill.rule), - transform: self.transform, })); } pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) { - let Some(path) = convert_path(path) else { + let Some(path) = + convert_path(path).and_then(|path| path.transform(self.transform)) + else { return; }; @@ -90,7 +96,6 @@ impl Frame { path, paint: into_paint(stroke.style), stroke: skia_stroke, - transform: self.transform, })); } diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs index 0ed24969..7718d542 100644 --- a/tiny_skia/src/primitive.rs +++ b/tiny_skia/src/primitive.rs @@ -13,8 +13,6 @@ pub enum Custom { paint: tiny_skia::Paint<'static>, /// The fill rule to follow. rule: tiny_skia::FillRule, - /// The transform to apply to the path. - transform: tiny_skia::Transform, }, /// A path stroked with some paint. Stroke { @@ -24,8 +22,6 @@ pub enum Custom { paint: tiny_skia::Paint<'static>, /// The stroke settings. stroke: tiny_skia::Stroke, - /// The transform to apply to the path. - transform: tiny_skia::Transform, }, } -- cgit From 5d4c55c07a80d93e6009e94c2a861ad549d30aab Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Wed, 17 Jan 2024 15:53:08 +0100 Subject: Fix `paint` not being transformed in `iced_tiny_skia` --- tiny_skia/src/geometry.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 501638e0..74a08d38 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -48,10 +48,13 @@ impl Frame { let fill = fill.into(); + let mut paint = into_paint(fill.style); + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Fill { path, - paint: into_paint(fill.style), + paint, rule: into_fill_rule(fill.rule), })); } @@ -70,13 +73,16 @@ impl Frame { let fill = fill.into(); + let mut paint = tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }; + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Fill { path, - paint: tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }, + paint, rule: into_fill_rule(fill.rule), })); } @@ -91,10 +97,13 @@ impl Frame { let stroke = stroke.into(); let skia_stroke = into_stroke(&stroke); + let mut paint = into_paint(stroke.style); + paint.shader.transform(self.transform); + self.primitives .push(Primitive::Custom(primitive::Custom::Stroke { path, - paint: into_paint(stroke.style), + paint, stroke: skia_stroke, })); } -- cgit From 8bf238697226e827dc983f9d89afbd0e252c5254 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 09:55:27 +0100 Subject: Remove `Compositor` window generic And update `glyphon` and `window_clipboard` --- Cargo.toml | 10 ++--- examples/custom_shader/src/scene/pipeline.rs | 2 + examples/integration/src/main.rs | 12 ++++-- futures/src/lib.rs | 4 +- futures/src/maybe.rs | 35 +++++++++++++++++ futures/src/maybe_send.rs | 21 ----------- graphics/Cargo.toml | 1 + graphics/src/compositor.rs | 23 ++++++++++-- graphics/src/lib.rs | 1 + renderer/Cargo.toml | 1 - renderer/src/compositor.rs | 36 +++++++----------- src/application.rs | 7 +--- tiny_skia/Cargo.toml | 1 - tiny_skia/src/window/compositor.rs | 56 ++++++++++++++-------------- wgpu/Cargo.toml | 1 - wgpu/src/window/compositor.rs | 40 +++++++------------- winit/src/application.rs | 8 ++-- winit/src/multi_window.rs | 14 ++++--- winit/src/multi_window/window_manager.rs | 7 ++-- 19 files changed, 146 insertions(+), 134 deletions(-) create mode 100644 futures/src/maybe.rs delete mode 100644 futures/src/maybe_send.rs diff --git a/Cargo.toml b/Cargo.toml index 421c7c76..131a4ef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,10 +128,7 @@ bytemuck = { version = "1.0", features = ["derive"] } cosmic-text = "0.10" futures = "0.3" glam = "0.24" -# glyphon = "0.4" -# TODO update for wgpu 0.19 -# https://github.com/grovesNL/glyphon/pull/80 -glyphon = { git = "https://github.com/EggShark/glyphon" } +glyphon = "0.5" guillotiere = "0.6" half = "2.2" image = "0.24" @@ -165,6 +162,5 @@ web-sys = "0.3" web-time = "0.2" wgpu = "0.19" winapi = "0.3" -# window_clipboard = "0.3" -window_clipboard = { git = "https://github.com/ids1024/window_clipboard", branch = "raw-window-handle-0.6" } -winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5", features = ["rwh_06"] } +window_clipboard = "0.4" +winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" } diff --git a/examples/custom_shader/src/scene/pipeline.rs b/examples/custom_shader/src/scene/pipeline.rs index 124b421f..50b70a98 100644 --- a/examples/custom_shader/src/scene/pipeline.rs +++ b/examples/custom_shader/src/scene/pipeline.rs @@ -97,6 +97,7 @@ impl Pipeline { usage: wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }, + wgpu::util::TextureDataOrder::LayerMajor, &normal_map_data, ); @@ -122,6 +123,7 @@ impl Pipeline { usage: wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }, + wgpu::util::TextureDataOrder::LayerMajor, &skybox_data, ); diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index b0939d68..ed61459f 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -24,6 +24,8 @@ use winit::{ keyboard::ModifiersState, }; +use std::sync::Arc; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] @@ -59,6 +61,8 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { #[cfg(not(target_arch = "wasm32"))] let window = winit::window::Window::new(&event_loop)?; + let window = Arc::new(window); + let physical_size = window.inner_size(); let mut viewport = Viewport::with_physical_size( Size::new(physical_size.width, physical_size.height), @@ -81,7 +85,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { backends: backend, ..Default::default() }); - let surface = unsafe { instance.create_surface(&window) }?; + let surface = instance.create_surface(window.clone())?; let (format, (device, queue)) = futures::futures::executor::block_on(async { @@ -115,9 +119,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { .request_device( &wgpu::DeviceDescriptor { label: None, - features: adapter_features + required_features: adapter_features & wgpu::Features::default(), - limits: needed_limits, + required_limits: needed_limits, }, None, ) @@ -136,6 +140,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { present_mode: wgpu::PresentMode::AutoVsync, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], + desired_maximum_frame_latency: 2, }, ); @@ -188,6 +193,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { present_mode: wgpu::PresentMode::AutoVsync, alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![], + desired_maximum_frame_latency: 2, }, ); diff --git a/futures/src/lib.rs b/futures/src/lib.rs index d54ba18a..b0acb76f 100644 --- a/futures/src/lib.rs +++ b/futures/src/lib.rs @@ -15,7 +15,7 @@ pub use futures; pub use iced_core as core; -mod maybe_send; +mod maybe; mod runtime; pub mod backend; @@ -25,7 +25,7 @@ pub mod keyboard; pub mod subscription; pub use executor::Executor; -pub use maybe_send::MaybeSend; +pub use maybe::{MaybeSend, MaybeSync}; pub use platform::*; pub use runtime::Runtime; pub use subscription::Subscription; diff --git a/futures/src/maybe.rs b/futures/src/maybe.rs new file mode 100644 index 00000000..1a0bd1d1 --- /dev/null +++ b/futures/src/maybe.rs @@ -0,0 +1,35 @@ +#[cfg(not(target_arch = "wasm32"))] +mod platform { + /// An extension trait that enforces `Send` only on native platforms. + /// + /// Useful to write cross-platform async code! + pub trait MaybeSend: Send {} + + impl<T> MaybeSend for T where T: Send {} + + /// An extension trait that enforces `Sync` only on native platforms. + /// + /// Useful to write cross-platform async code! + pub trait MaybeSync: Sync {} + + impl<T> MaybeSync for T where T: Sync {} +} + +#[cfg(target_arch = "wasm32")] +mod platform { + /// An extension trait that enforces `Send` only on native platforms. + /// + /// Useful to write cross-platform async code! + pub trait MaybeSend {} + + impl<T> MaybeSend for T {} + + /// An extension trait that enforces `Send` only on native platforms. + /// + /// Useful to write cross-platform async code! + pub trait MaybeSync {} + + impl<T> MaybeSync for T {} +} + +pub use platform::{MaybeSend, MaybeSync}; diff --git a/futures/src/maybe_send.rs b/futures/src/maybe_send.rs deleted file mode 100644 index a6670f0e..00000000 --- a/futures/src/maybe_send.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -mod platform { - /// An extension trait that enforces `Send` only on native platforms. - /// - /// Useful to write cross-platform async code! - pub trait MaybeSend: Send {} - - impl<T> MaybeSend for T where T: Send {} -} - -#[cfg(target_arch = "wasm32")] -mod platform { - /// An extension trait that enforces `Send` only on native platforms. - /// - /// Useful to write cross-platform async code! - pub trait MaybeSend {} - - impl<T> MaybeSend for T {} -} - -pub use platform::MaybeSend; diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 6741d7cf..4f323f9e 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -21,6 +21,7 @@ web-colors = [] [dependencies] iced_core.workspace = true +iced_futures.workspace = true bitflags.workspace = true bytemuck.workspace = true diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index 6a4c7909..e6b9030b 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -2,13 +2,14 @@ //! surfaces. use crate::{Error, Viewport}; -use iced_core::Color; +use crate::core::Color; +use crate::futures::{MaybeSend, MaybeSync}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use thiserror::Error; /// A graphics compositor that can draw to windows. -pub trait Compositor<W: HasWindowHandle + HasDisplayHandle>: Sized { +pub trait Compositor: Sized { /// The settings of the backend. type Settings: Default; @@ -19,7 +20,7 @@ pub trait Compositor<W: HasWindowHandle + HasDisplayHandle>: Sized { type Surface; /// Creates a new [`Compositor`]. - fn new( + fn new<W: Window + Clone>( settings: Self::Settings, compatible_window: Option<W>, ) -> Result<Self, Error>; @@ -30,7 +31,7 @@ pub trait Compositor<W: HasWindowHandle + HasDisplayHandle>: Sized { /// Crates a new [`Surface`] for the given window. /// /// [`Surface`]: Self::Surface - fn create_surface( + fn create_surface<W: Window + Clone>( &mut self, window: W, width: u32, @@ -77,6 +78,20 @@ pub trait Compositor<W: HasWindowHandle + HasDisplayHandle>: Sized { ) -> Vec<u8>; } +/// A window that can be used in a [`Compositor`]. +/// +/// This is just a convenient super trait of the `raw-window-handle` +/// traits. +pub trait Window: + HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + +impl<T> Window for T where + T: HasWindowHandle + HasDisplayHandle + MaybeSend + MaybeSync + 'static +{ +} + /// Result of an unsuccessful call to [`Compositor::present`]. #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum SurfaceError { diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 7a213909..76de56bf 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -50,3 +50,4 @@ pub use transformation::Transformation; pub use viewport::Viewport; pub use iced_core as core; +pub use iced_futures as futures; diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 56e17209..a159978c 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -27,5 +27,4 @@ iced_wgpu.workspace = true iced_wgpu.optional = true log.workspace = true -raw-window-handle.workspace = true thiserror.workspace = true diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 17157c66..a7c63444 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -1,36 +1,28 @@ use crate::core::Color; -use crate::graphics::compositor::{Information, SurfaceError}; +use crate::graphics::compositor::{Information, SurfaceError, Window}; use crate::graphics::{Error, Viewport}; use crate::{Renderer, Settings}; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::env; -pub enum Compositor<W: HasWindowHandle + HasDisplayHandle, Theme> { - TinySkia(iced_tiny_skia::window::Compositor<W, Theme>), +pub enum Compositor<Theme> { + TinySkia(iced_tiny_skia::window::Compositor<Theme>), #[cfg(feature = "wgpu")] - Wgpu(iced_wgpu::window::Compositor<W, Theme>), + Wgpu(iced_wgpu::window::Compositor<Theme>), } -pub enum Surface<W: HasWindowHandle + HasDisplayHandle> { - TinySkia(iced_tiny_skia::window::Surface<W>), +pub enum Surface { + TinySkia(iced_tiny_skia::window::Surface), #[cfg(feature = "wgpu")] Wgpu(iced_wgpu::window::Surface<'static>), } -// XXX Clone bound -// XXX Send/Sync? -// 'static? -impl< - W: Clone + Send + Sync + HasWindowHandle + HasDisplayHandle + 'static, - Theme, - > crate::graphics::Compositor<W> for Compositor<W, Theme> -{ +impl<Theme> crate::graphics::Compositor for Compositor<Theme> { type Settings = Settings; type Renderer = Renderer<Theme>; - type Surface = Surface<W>; + type Surface = Surface; - fn new( + fn new<W: Window + Clone>( settings: Self::Settings, compatible_window: Option<W>, ) -> Result<Self, Error> { @@ -63,12 +55,12 @@ impl< } } - fn create_surface( + fn create_surface<W: Window + Clone>( &mut self, window: W, width: u32, height: u32, - ) -> Surface<W> { + ) -> Surface { match self { Self::TinySkia(compositor) => Surface::TinySkia( compositor.create_surface(window, width, height), @@ -82,7 +74,7 @@ impl< fn configure_surface( &mut self, - surface: &mut Surface<W>, + surface: &mut Surface, width: u32, height: u32, ) { @@ -233,11 +225,11 @@ impl Candidate { ) } - fn build<Theme, W: HasWindowHandle + HasDisplayHandle + Send + Sync>( + fn build<Theme, W: Window>( self, settings: Settings, _compatible_window: Option<W>, - ) -> Result<Compositor<W, Theme>, Error> { + ) -> Result<Compositor<Theme>, Error> { match self { Self::TinySkia => { let compositor = iced_tiny_skia::window::compositor::new( diff --git a/src/application.rs b/src/application.rs index d7be6719..9518b8c5 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,8 +1,6 @@ //! Build interactive cross-platform applications. use crate::{Command, Element, Executor, Settings, Subscription}; -use std::sync::Arc; - pub use crate::style::application::{Appearance, StyleSheet}; /// An interactive cross-platform application. @@ -210,10 +208,7 @@ pub trait Application: Sized { Ok(crate::shell::application::run::< Instance<Self>, Self::Executor, - crate::renderer::Compositor< - Arc<winit::window::Window>, - Self::Theme, - >, + crate::renderer::Compositor<Self::Theme>, >(settings.into(), renderer_settings)?) } } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index df4c6143..68b2a03a 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -22,7 +22,6 @@ bytemuck.workspace = true cosmic-text.workspace = true kurbo.workspace = true log.workspace = true -raw-window-handle.workspace = true rustc-hash.workspace = true softbuffer.workspace = true tiny-skia.workspace = true diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 788d7297..b5e9bcd8 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -4,34 +4,33 @@ use crate::graphics::damage; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::collections::VecDeque; use std::marker::PhantomData; use std::num::NonZeroU32; -pub struct Compositor<W: HasDisplayHandle + HasWindowHandle, Theme> { - context: Option<softbuffer::Context<W>>, +pub struct Compositor<Theme> { + context: Option<softbuffer::Context<Box<dyn compositor::Window>>>, settings: Settings, _theme: PhantomData<Theme>, } -pub struct Surface<W: HasDisplayHandle + HasWindowHandle> { - window: softbuffer::Surface<W, W>, +pub struct Surface { + window: softbuffer::Surface< + Box<dyn compositor::Window>, + Box<dyn compositor::Window>, + >, clip_mask: tiny_skia::Mask, // Primitives of existing buffers, by decreasing age primitives: VecDeque<Vec<Primitive>>, background_color: Color, } -// XXX avoid clone bound? -impl<W: HasDisplayHandle + HasWindowHandle + Clone, Theme> - crate::graphics::Compositor<W> for Compositor<W, Theme> -{ +impl<Theme> crate::graphics::Compositor for Compositor<Theme> { type Settings = Settings; type Renderer = Renderer<Theme>; - type Surface = Surface<W>; + type Surface = Surface; - fn new( + fn new<W: compositor::Window>( settings: Self::Settings, compatible_window: Option<W>, ) -> Result<Self, Error> { @@ -46,19 +45,21 @@ impl<W: HasDisplayHandle + HasWindowHandle + Clone, Theme> ) } - fn create_surface( + fn create_surface<W: compositor::Window + Clone>( &mut self, window: W, width: u32, height: u32, - ) -> Surface<W> { + ) -> Surface { let window = if let Some(context) = self.context.as_ref() { - softbuffer::Surface::new(context, window) + softbuffer::Surface::new(context, Box::new(window.clone()) as _) .expect("Create softbuffer surface for window") } else { - let context = softbuffer::Context::new(window.clone()) - .expect("Create softbuffer context for window"); - softbuffer::Surface::new(&context, window) + let context = + softbuffer::Context::new(Box::new(window.clone()) as _) + .expect("Create softbuffer context for window"); + + softbuffer::Surface::new(&context, Box::new(window.clone()) as _) .expect("Create softbuffer surface for window") }; @@ -73,7 +74,7 @@ impl<W: HasDisplayHandle + HasWindowHandle + Clone, Theme> fn configure_surface( &mut self, - surface: &mut Surface<W>, + surface: &mut Surface, width: u32, height: u32, ) { @@ -92,7 +93,7 @@ impl<W: HasDisplayHandle + HasWindowHandle + Clone, Theme> fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Surface<W>, + surface: &mut Surface, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -130,13 +131,14 @@ impl<W: HasDisplayHandle + HasWindowHandle + Clone, Theme> } } -pub fn new<W: HasWindowHandle + HasDisplayHandle, Theme>( +pub fn new<W: compositor::Window, Theme>( settings: Settings, compatible_window: Option<W>, -) -> Compositor<W, Theme> { +) -> Compositor<Theme> { #[allow(unsafe_code)] - let context = - compatible_window.and_then(|w| softbuffer::Context::new(w).ok()); + let context = compatible_window + .and_then(|w| softbuffer::Context::new(Box::new(w) as _).ok()); + Compositor { context, settings, @@ -144,9 +146,9 @@ pub fn new<W: HasWindowHandle + HasDisplayHandle, Theme>( } } -pub fn present<W: HasDisplayHandle + HasWindowHandle, T: AsRef<str>>( +pub fn present<T: AsRef<str>>( backend: &mut Backend, - surface: &mut Surface<W>, + surface: &mut Surface, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -218,8 +220,8 @@ pub fn present<W: HasDisplayHandle + HasWindowHandle, T: AsRef<str>>( buffer.present().map_err(|_| compositor::SurfaceError::Lost) } -pub fn screenshot<W: HasDisplayHandle + HasWindowHandle, T: AsRef<str>>( - surface: &mut Surface<W>, +pub fn screenshot<T: AsRef<str>>( + surface: &mut Surface, backend: &mut Backend, primitives: &[Primitive], viewport: &Viewport, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index a460c127..1d3b57a7 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -32,7 +32,6 @@ glyphon.workspace = true guillotiere.workspace = true log.workspace = true once_cell.workspace = true -raw-window-handle.workspace = true wgpu.workspace = true lyon.workspace = true diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index e2dc4901..0c063d0b 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -6,13 +6,11 @@ use crate::graphics::compositor; use crate::graphics::{Error, Viewport}; use crate::{Backend, Primitive, Renderer, Settings}; -use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; - use std::marker::PhantomData; /// A window graphics backend for iced powered by `wgpu`. #[allow(missing_debug_implementations)] -pub struct Compositor<W, Theme> { +pub struct Compositor<Theme> { settings: Settings, instance: wgpu::Instance, adapter: wgpu::Adapter, @@ -20,16 +18,13 @@ pub struct Compositor<W, Theme> { queue: wgpu::Queue, format: wgpu::TextureFormat, theme: PhantomData<Theme>, - w: PhantomData<W>, } -impl<W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, Theme> - Compositor<W, Theme> -{ +impl<Theme> Compositor<Theme> { /// Requests a new [`Compositor`] with the given [`Settings`]. /// /// Returns `None` if no compatible graphics adapter could be found. - pub async fn request( + pub async fn request<W: compositor::Window>( settings: Settings, compatible_window: Option<W>, ) -> Option<Self> { @@ -45,7 +40,7 @@ impl<W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, Theme> let available_adapters: Vec<_> = instance .enumerate_adapters(settings.internal_backend) .iter() - .map(|adapter| adapter.get_info()) + .map(wgpu::Adapter::get_info) .collect(); log::info!("Available adapters: {available_adapters:#?}"); } @@ -129,7 +124,6 @@ impl<W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, Theme> queue, format, theme: PhantomData, - w: PhantomData, }) } @@ -141,13 +135,10 @@ impl<W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, Theme> /// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and /// window. -pub fn new< - Theme, - W: HasWindowHandle + HasDisplayHandle + wgpu::WasmNotSendSync, ->( +pub fn new<W: compositor::Window, Theme>( settings: Settings, compatible_window: Option<W>, -) -> Result<Compositor<W, Theme>, Error> { +) -> Result<Compositor<Theme>, Error> { let compositor = futures::executor::block_on(Compositor::request( settings, compatible_window, @@ -158,8 +149,8 @@ pub fn new< } /// Presents the given primitives with the given [`Compositor`] and [`Backend`]. -pub fn present<W, Theme, T: AsRef<str>>( - compositor: &mut Compositor<W, Theme>, +pub fn present<Theme, T: AsRef<str>>( + compositor: &mut Compositor<Theme>, backend: &mut Backend, surface: &mut wgpu::Surface<'static>, primitives: &[Primitive], @@ -212,17 +203,12 @@ pub fn present<W, Theme, T: AsRef<str>>( } } -impl< - W: HasDisplayHandle + HasWindowHandle + wgpu::WasmNotSendSync + 'static, - Theme, - > graphics::Compositor<W> for Compositor<W, Theme> -{ +impl<Theme> graphics::Compositor for Compositor<Theme> { type Settings = Settings; type Renderer = Renderer<Theme>; - // XXX generic instead of 'static type Surface = wgpu::Surface<'static>; - fn new( + fn new<W: compositor::Window>( settings: Self::Settings, compatible_window: Option<W>, ) -> Result<Self, Error> { @@ -237,7 +223,7 @@ impl< ) } - fn create_surface( + fn create_surface<W: compositor::Window>( &mut self, window: W, width: u32, @@ -328,8 +314,8 @@ impl< /// Renders the current surface to an offscreen buffer. /// /// Returns RGBA bytes of the texture data. -pub fn screenshot<W, Theme, T: AsRef<str>>( - compositor: &Compositor<W, Theme>, +pub fn screenshot<Theme, T: AsRef<str>>( + compositor: &Compositor<Theme>, backend: &mut Backend, primitives: &[Primitive], viewport: &Viewport, diff --git a/winit/src/application.rs b/winit/src/application.rs index d639a36b..c5e11167 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -106,7 +106,7 @@ pub fn run<A, E, C>( where A: Application + 'static, E: Executor + 'static, - C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use futures::task; @@ -258,7 +258,7 @@ async fn run_instance<A, E, C>( ) where A: Application + 'static, E: Executor + 'static, - C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use futures::stream::StreamExt; @@ -612,7 +612,7 @@ pub fn update<A: Application, C, E: Executor>( messages: &mut Vec<A::Message>, window: &winit::window::Window, ) where - C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { for message in messages.drain(..) { @@ -663,7 +663,7 @@ pub fn run_command<A, C, E>( ) where A: Application, E: Executor, - C: Compositor<Arc<winit::window::Window>, Renderer = A::Renderer> + 'static, + C: Compositor<Renderer = A::Renderer> + 'static, <A::Renderer as core::Renderer>::Theme: StyleSheet, { use crate::runtime::command; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 84c81bea..21196460 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -24,6 +24,7 @@ use crate::{Clipboard, Error, Proxy, Settings}; use std::collections::HashMap; use std::mem::ManuallyDrop; +use std::sync::Arc; use std::time::Instant; /// An interactive, native, cross-platform, multi-windowed application. @@ -150,9 +151,11 @@ where log::info!("Window builder: {:#?}", builder); - let main_window = builder - .build(&event_loop) - .map_err(Error::WindowCreationFailed)?; + let main_window = Arc::new( + builder + .build(&event_loop) + .map_err(Error::WindowCreationFailed)?, + ); #[cfg(target_arch = "wasm32")] { @@ -184,7 +187,8 @@ where }; } - let mut compositor = C::new(compositor_settings, Some(&main_window))?; + let mut compositor = + C::new(compositor_settings, Some(main_window.clone()))?; let mut window_manager = WindowManager::new(); let _ = window_manager.insert( @@ -388,7 +392,7 @@ async fn run_instance<A, E, C>( } => { let window = window_manager.insert( id, - window, + Arc::new(window), &application, &mut compositor, exit_on_close_request, diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index d54156e7..9e15f9ea 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -6,6 +6,7 @@ use crate::multi_window::{Application, State}; use crate::style::application::StyleSheet; use std::collections::BTreeMap; +use std::sync::Arc; use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] @@ -34,7 +35,7 @@ where pub fn insert( &mut self, id: Id, - window: winit::window::Window, + window: Arc<winit::window::Window>, application: &A, compositor: &mut C, exit_on_close_request: bool, @@ -43,7 +44,7 @@ where let viewport_version = state.viewport_version(); let physical_size = state.physical_size(); let surface = compositor.create_surface( - &window, + window.clone(), physical_size.width, physical_size.height, ); @@ -122,7 +123,7 @@ where C: Compositor<Renderer = A::Renderer>, <A::Renderer as crate::core::Renderer>::Theme: StyleSheet, { - pub raw: winit::window::Window, + pub raw: Arc<winit::window::Window>, pub state: State<A>, pub viewport_version: u64, pub exit_on_close_request: bool, -- cgit From b9dc106a56a9f91673f3a64b05e8413150adf5e0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 09:58:54 +0100 Subject: Remove `winit` dependency from `iced` root crate --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 131a4ef5..665dc5a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,8 +70,6 @@ thiserror.workspace = true image.workspace = true image.optional = true -winit.workspace = true - [profile.release-opt] inherits = "release" codegen-units = 1 -- cgit From 4c90ed6a1b50331d889d79aacbf653fc6d98950f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 09:59:36 +0100 Subject: Remove patch version from `softbuffer` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 665dc5a1..c9dee6b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,7 +145,7 @@ resvg = "0.36" rustc-hash = "1.0" smol = "1.0" smol_str = "0.2" -softbuffer = "0.4.1" +softbuffer = "0.4" syntect = "5.1" sysinfo = "0.28" thiserror = "1.0" -- cgit From 1701ec815d3f25ea8097e806081e7a3ac9ba4d82 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:02:50 +0100 Subject: Remove redundant `ref mut` in `iced_renderer::compositor` --- renderer/src/compositor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index a7c63444..0b56f101 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -113,7 +113,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ( Self::TinySkia(_compositor), crate::Renderer::TinySkia(renderer), - Surface::TinySkia(ref mut surface), + Surface::TinySkia(surface), ) => renderer.with_primitives(|backend, primitives| { iced_tiny_skia::window::compositor::present( backend, @@ -128,7 +128,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ( Self::Wgpu(compositor), crate::Renderer::Wgpu(renderer), - Surface::Wgpu(ref mut surface), + Surface::Wgpu(surface), ) => renderer.with_primitives(|backend, primitives| { iced_wgpu::window::compositor::present( compositor, @@ -160,7 +160,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ( Self::TinySkia(_compositor), Renderer::TinySkia(renderer), - Surface::TinySkia(ref mut surface), + Surface::TinySkia(surface), ) => renderer.with_primitives(|backend, primitives| { iced_tiny_skia::window::compositor::screenshot( surface, -- cgit From 5fc49edc55a0e64c4c46ca55eddafe9d4e8232e1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:06:30 +0100 Subject: Make `compatible_window` mandatory in `Compositor` --- graphics/src/compositor.rs | 2 +- renderer/src/compositor.rs | 4 ++-- tiny_skia/src/window/compositor.rs | 26 ++++++++++---------------- wgpu/src/window/compositor.rs | 6 +++--- winit/src/application.rs | 2 +- winit/src/multi_window.rs | 3 +-- 6 files changed, 18 insertions(+), 25 deletions(-) diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index e6b9030b..0188f4d8 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -22,7 +22,7 @@ pub trait Compositor: Sized { /// Creates a new [`Compositor`]. fn new<W: Window + Clone>( settings: Self::Settings, - compatible_window: Option<W>, + compatible_window: W, ) -> Result<Self, Error>; /// Creates a [`Self::Renderer`] for the [`Compositor`]. diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs index 0b56f101..f10ed048 100644 --- a/renderer/src/compositor.rs +++ b/renderer/src/compositor.rs @@ -24,7 +24,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn new<W: Window + Clone>( settings: Self::Settings, - compatible_window: Option<W>, + compatible_window: W, ) -> Result<Self, Error> { let candidates = Candidate::list_from_env().unwrap_or(Candidate::default_list()); @@ -228,7 +228,7 @@ impl Candidate { fn build<Theme, W: Window>( self, settings: Settings, - _compatible_window: Option<W>, + _compatible_window: W, ) -> Result<Compositor<Theme>, Error> { match self { Self::TinySkia => { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index b5e9bcd8..86400aa0 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -9,7 +9,7 @@ use std::marker::PhantomData; use std::num::NonZeroU32; pub struct Compositor<Theme> { - context: Option<softbuffer::Context<Box<dyn compositor::Window>>>, + context: softbuffer::Context<Box<dyn compositor::Window>>, settings: Settings, _theme: PhantomData<Theme>, } @@ -32,7 +32,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn new<W: compositor::Window>( settings: Self::Settings, - compatible_window: Option<W>, + compatible_window: W, ) -> Result<Self, Error> { Ok(new(settings, compatible_window)) } @@ -51,17 +51,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { width: u32, height: u32, ) -> Surface { - let window = if let Some(context) = self.context.as_ref() { - softbuffer::Surface::new(context, Box::new(window.clone()) as _) - .expect("Create softbuffer surface for window") - } else { - let context = - softbuffer::Context::new(Box::new(window.clone()) as _) - .expect("Create softbuffer context for window"); - - softbuffer::Surface::new(&context, Box::new(window.clone()) as _) - .expect("Create softbuffer surface for window") - }; + let window = softbuffer::Surface::new( + &self.context, + Box::new(window.clone()) as _, + ) + .expect("Create softbuffer surface for window"); Surface { window, @@ -133,11 +127,11 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { pub fn new<W: compositor::Window, Theme>( settings: Settings, - compatible_window: Option<W>, + compatible_window: W, ) -> Compositor<Theme> { #[allow(unsafe_code)] - let context = compatible_window - .and_then(|w| softbuffer::Context::new(Box::new(w) as _).ok()); + let context = softbuffer::Context::new(Box::new(compatible_window) as _) + .expect("Create softbuffer context"); Compositor { context, diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 0c063d0b..105d83a8 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -137,11 +137,11 @@ impl<Theme> Compositor<Theme> { /// window. pub fn new<W: compositor::Window, Theme>( settings: Settings, - compatible_window: Option<W>, + compatible_window: W, ) -> Result<Compositor<Theme>, Error> { let compositor = futures::executor::block_on(Compositor::request( settings, - compatible_window, + Some(compatible_window), )) .ok_or(Error::GraphicsAdapterNotFound)?; @@ -210,7 +210,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn new<W: compositor::Window>( settings: Self::Settings, - compatible_window: Option<W>, + compatible_window: W, ) -> Result<Self, Error> { new(settings, compatible_window) } diff --git a/winit/src/application.rs b/winit/src/application.rs index c5e11167..5fcdbbd8 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -187,7 +187,7 @@ where }; } - let compositor = C::new(compositor_settings, Some(window.clone()))?; + let compositor = C::new(compositor_settings, window.clone())?; let mut renderer = compositor.create_renderer(); for font in settings.fonts { diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 21196460..3f0ba056 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -187,8 +187,7 @@ where }; } - let mut compositor = - C::new(compositor_settings, Some(main_window.clone()))?; + let mut compositor = C::new(compositor_settings, main_window.clone())?; let mut window_manager = WindowManager::new(); let _ = window_manager.insert( -- cgit From 4b7744b9806397c9891b1fc179df8a61eaa3670d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:35:27 +0100 Subject: Support out-of-order `Buffer` ages in `iced_tiny_skia` --- tiny_skia/src/window/compositor.rs | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 86400aa0..c0aabdb6 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -20,9 +20,9 @@ pub struct Surface { Box<dyn compositor::Window>, >, clip_mask: tiny_skia::Mask, - // Primitives of existing buffers, by decreasing age - primitives: VecDeque<Vec<Primitive>>, + primitive_stack: VecDeque<Vec<Primitive>>, background_color: Color, + max_age: u8, } impl<Theme> crate::graphics::Compositor for Compositor<Theme> { @@ -61,8 +61,9 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { window, clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), - primitives: VecDeque::new(), + primitive_stack: VecDeque::new(), background_color: Color::BLACK, + max_age: 0, } } @@ -74,7 +75,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ) { surface.clip_mask = tiny_skia::Mask::new(width, height).expect("Create clip mask"); - surface.primitives.clear(); + surface.primitive_stack.clear(); } fn fetch_information(&self) -> Information { @@ -159,7 +160,6 @@ pub fn present<T: AsRef<str>>( ) .unwrap(); - // TODO Add variants to `SurfaceError`? let mut buffer = surface .window .buffer_mut() @@ -167,27 +167,25 @@ pub fn present<T: AsRef<str>>( let age = buffer.age(); - // Forget primatives for back buffers older than `age` - // Or if this is a new buffer, keep at most two. - let max = if age == 0 { 2 } else { age }; - while surface.primitives.len() as u8 > max { - let _ = surface.primitives.pop_front(); - } + let last_primitives = { + surface.max_age = surface.max_age.max(age); + surface.primitive_stack.truncate(surface.max_age as usize); - let last_primitives = if surface.primitives.len() as u8 == age { - surface.primitives.pop_front() - } else { - None + if age > 0 { + surface.primitive_stack.get(age as usize - 1) + } else { + None + } }; let damage = last_primitives .and_then(|last_primitives| { (surface.background_color == background_color) - .then(|| damage::list(&last_primitives, primitives)) + .then(|| damage::list(last_primitives, primitives)) }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); - surface.primitives.push_back(primitives.to_vec()); + surface.primitive_stack.push_front(primitives.to_vec()); surface.background_color = background_color; if !damage.is_empty() { -- cgit From b6b3e9b9f995abf5cc65814e143418b6f1ec7464 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:42:02 +0100 Subject: Avoid stacking new primitives when undamaged --- tiny_skia/src/window/compositor.rs | 40 ++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index c0aabdb6..dae57975 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -185,29 +185,31 @@ pub fn present<T: AsRef<str>>( }) .unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]); + if damage.is_empty() { + return Ok(()); + } + surface.primitive_stack.push_front(primitives.to_vec()); surface.background_color = background_color; - if !damage.is_empty() { - let damage = damage::group(damage, scale_factor, physical_size); + let damage = damage::group(damage, scale_factor, physical_size); - let mut pixels = tiny_skia::PixmapMut::from_bytes( - bytemuck::cast_slice_mut(&mut buffer), - physical_size.width, - physical_size.height, - ) - .expect("Create pixel map"); - - backend.draw( - &mut pixels, - &mut surface.clip_mask, - primitives, - viewport, - &damage, - background_color, - overlay, - ); - } + let mut pixels = tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"); + + backend.draw( + &mut pixels, + &mut surface.clip_mask, + primitives, + viewport, + &damage, + background_color, + overlay, + ); buffer.present().map_err(|_| compositor::SurfaceError::Lost) } -- cgit From 150ce65e209414847ae133a70c833addd3086e15 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:43:52 +0100 Subject: Nest `age` declaration inside `last_primitives` --- tiny_skia/src/window/compositor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index dae57975..08a49bc5 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -165,9 +165,9 @@ pub fn present<T: AsRef<str>>( .buffer_mut() .map_err(|_| compositor::SurfaceError::Lost)?; - let age = buffer.age(); - let last_primitives = { + let age = buffer.age(); + surface.max_age = surface.max_age.max(age); surface.primitive_stack.truncate(surface.max_age as usize); -- cgit From 3cf8f77d6537f9d864f4a554b2fff46676a761f6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:52:25 +0100 Subject: Resize surface in `configure_surface` in `iced_tiny_skia` --- tiny_skia/src/window/compositor.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 08a49bc5..17d21100 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -57,14 +57,18 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { ) .expect("Create softbuffer surface for window"); - Surface { + let mut surface = Surface { window, clip_mask: tiny_skia::Mask::new(width, height) .expect("Create clip mask"), primitive_stack: VecDeque::new(), background_color: Color::BLACK, max_age: 0, - } + }; + + self.configure_surface(&mut surface, width, height); + + surface } fn configure_surface( @@ -73,6 +77,14 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { width: u32, height: u32, ) { + surface + .window + .resize( + NonZeroU32::new(width).expect("Non-zero width"), + NonZeroU32::new(height).expect("Non-zero height"), + ) + .expect("Resize surface"); + surface.clip_mask = tiny_skia::Mask::new(width, height).expect("Create clip mask"); surface.primitive_stack.clear(); @@ -152,14 +164,6 @@ pub fn present<T: AsRef<str>>( let physical_size = viewport.physical_size(); let scale_factor = viewport.scale_factor() as f32; - surface - .window - .resize( - NonZeroU32::new(physical_size.width).unwrap(), - NonZeroU32::new(physical_size.height).unwrap(), - ) - .unwrap(); - let mut buffer = surface .window .buffer_mut() -- cgit From c929e6f5dd30044df4e7400ab633eaf0a53ce3dd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:56:02 +0100 Subject: Use `Self::Surface` in `Compositor` implementors --- tiny_skia/src/window/compositor.rs | 6 +++--- wgpu/src/window/compositor.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 17d21100..781ed8a5 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -50,7 +50,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { window: W, width: u32, height: u32, - ) -> Surface { + ) -> Self::Surface { let window = softbuffer::Surface::new( &self.context, Box::new(window.clone()) as _, @@ -73,7 +73,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn configure_surface( &mut self, - surface: &mut Surface, + surface: &mut Self::Surface, width: u32, height: u32, ) { @@ -100,7 +100,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> { fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - surface: &mut Surface, + surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 105d83a8..31cf3819 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -228,7 +228,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { window: W, width: u32, height: u32, - ) -> wgpu::Surface<'static> { + ) -> Self::Surface { let mut surface = self .instance .create_surface(window) @@ -241,7 +241,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn configure_surface( &mut self, - surface: &mut wgpu::Surface<'static>, + surface: &mut Self::Surface, width: u32, height: u32, ) { @@ -272,7 +272,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn present<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - surface: &mut wgpu::Surface<'static>, + surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], @@ -293,7 +293,7 @@ impl<Theme> graphics::Compositor for Compositor<Theme> { fn screenshot<T: AsRef<str>>( &mut self, renderer: &mut Self::Renderer, - _surface: &mut wgpu::Surface<'static>, + _surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, overlay: &[T], -- cgit From 74a6e58cbc3354d45ed6cd86e58c624a946d0f05 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 10:57:53 +0100 Subject: Remove comment in `iced_winit::application` --- winit/src/application.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/winit/src/application.rs b/winit/src/application.rs index 5fcdbbd8..09bf63cc 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -150,7 +150,6 @@ where log::debug!("Window builder: {builder:#?}"); - // XXX Arc? let window = Arc::new( builder .build(&event_loop) -- cgit From cba56ea76821a3204923d34c3fe634730a86f22d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 11:02:53 +0100 Subject: Fix typo `Send -> Sync` --- futures/src/maybe.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/futures/src/maybe.rs b/futures/src/maybe.rs index 1a0bd1d1..c564a739 100644 --- a/futures/src/maybe.rs +++ b/futures/src/maybe.rs @@ -24,7 +24,7 @@ mod platform { impl<T> MaybeSend for T {} - /// An extension trait that enforces `Send` only on native platforms. + /// An extension trait that enforces `Sync` only on native platforms. /// /// Useful to write cross-platform async code! pub trait MaybeSync {} -- cgit From bdb8f4966ea75c0147165541771ed3f952ac8d13 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 11:21:41 +0100 Subject: Fix grammar in `iced_futures::maybe` module --- futures/src/maybe.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/futures/src/maybe.rs b/futures/src/maybe.rs index c564a739..c6a507c1 100644 --- a/futures/src/maybe.rs +++ b/futures/src/maybe.rs @@ -2,14 +2,14 @@ mod platform { /// An extension trait that enforces `Send` only on native platforms. /// - /// Useful to write cross-platform async code! + /// Useful for writing cross-platform async code! pub trait MaybeSend: Send {} impl<T> MaybeSend for T where T: Send {} /// An extension trait that enforces `Sync` only on native platforms. /// - /// Useful to write cross-platform async code! + /// Useful for writing cross-platform async code! pub trait MaybeSync: Sync {} impl<T> MaybeSync for T where T: Sync {} @@ -19,14 +19,14 @@ mod platform { mod platform { /// An extension trait that enforces `Send` only on native platforms. /// - /// Useful to write cross-platform async code! + /// Useful for writing cross-platform async code! pub trait MaybeSend {} impl<T> MaybeSend for T {} /// An extension trait that enforces `Sync` only on native platforms. /// - /// Useful to write cross-platform async code! + /// Useful for writing cross-platform async code! pub trait MaybeSync {} impl<T> MaybeSync for T {} -- cgit From 9df7bf8ec30ca76016018bc758b4323760e231b0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez <hector@hecrj.dev> Date: Thu, 18 Jan 2024 15:41:19 +0100 Subject: Use `workspace` dependency for `raw-window-handle` in `iced_core` --- core/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index be92a572..32dd3df2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,8 +23,7 @@ palette.workspace = true palette.optional = true [target.'cfg(windows)'.dependencies] -# TODO: Use `workspace` dependency once `wgpu` upgrades `raw-window-handle` -raw-window-handle = "0.6" +raw-window-handle.workspace = true [dev-dependencies] approx = "0.5" -- cgit