diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 96dcc3b64c..e93a01ae86 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -58,12 +58,12 @@ body: attributes: label: Version description: | - We only offer support for the `0.4` release on crates.io and the `master` branch on this repository. Which version are you using? Please make sure you are using the latest patch available (e.g. run `cargo update`). + We only offer support for the latest release on crates.io and the `master` branch on this repository. Which version are you using? Please make sure you are using the latest patch available (e.g. run `cargo update`). - If you are using an older release, please upgrade to `0.4` before filing an issue. + If you are using an older release, please upgrade to the latest one before filing an issue. options: - master - - 0.4 + - 0.7 validations: required: true - type: dropdown diff --git a/.gitignore b/.gitignore index 56faba09c3..0c46184fd1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ pkg/ Cargo.lock .cargo/ dist/ +traces/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 907fced4da..d3e7641b7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.0] - 2023-01-14 +### Added +- Widget-driven animations. [#1647](https://github.com/iced-rs/iced/pull/1647) +- Multidirectional scrolling support for `Scrollable`. [#1550](https://github.com/iced-rs/iced/pull/1550) +- `VerticalSlider` widget. [#1596](https://github.com/iced-rs/iced/pull/1596) +- `Shift+Click` text selection support in `TextInput`. [#1622](https://github.com/iced-rs/iced/pull/1622) +- Profiling support with the `chrome-trace` feature. [#1565](https://github.com/iced-rs/iced/pull/1565) +- Customization of the handle of a `PickList`. [#1562](https://github.com/iced-rs/iced/pull/1562) +- `window` action to request user attention. [#1584](https://github.com/iced-rs/iced/pull/1584) +- `window` action to gain focus. [#1585](https://github.com/iced-rs/iced/pull/1585) +- `window` action to toggle decorations. [#1588](https://github.com/iced-rs/iced/pull/1588) +- `Copy` implementation for `gradient::Location`. [#1636](https://github.com/iced-rs/iced/pull/1636) + +### Changed +- Replaced `Application::should_exit` with a `window::close` action. [#1606](https://github.com/iced-rs/iced/pull/1606) +- Made `focusable::Count` fields public. [#1635](https://github.com/iced-rs/iced/pull/1635) +- Added `Dependency` argument to the closure of `Lazy`. [#1646](https://github.com/iced-rs/iced/pull/1646) +- Switched arguments order of `Toggler::new` for consistency. [#1616](https://github.com/iced-rs/iced/pull/1616) +- Switched arguments order of `Checkbox::new` for consistency. [#1633](https://github.com/iced-rs/iced/pull/1633) + +### Fixed +- Compilation error in `iced_glow` when the `image` feature is enabled but `svg` isn't. [#1593](https://github.com/iced-rs/iced/pull/1593) +- Widget operations for `Responsive` widget. [#1615](https://github.com/iced-rs/iced/pull/1615) +- Overlay placement for `Responsive`. [#1638](https://github.com/iced-rs/iced/pull/1638) +- `overlay` implementation for `Lazy`. [#1644](https://github.com/iced-rs/iced/pull/1644) +- Minor typo in documentation. [#1624](https://github.com/iced-rs/iced/pull/1624) +- Links in documentation. [#1634](https://github.com/iced-rs/iced/pull/1634) +- Missing comment in documentation. [#1648](https://github.com/iced-rs/iced/pull/1648) + +Many thanks to... + +- @13r0ck +- @Araxeus +- @ben-wallis +- @bungoboingo +- @casperstorm +- @nicksenger +- @Night-Hunter-NF +- @rpitasky +- @rs017991 +- @tarkah +- @wiktor-k + ## [0.6.0] - 2022-12-07 ### Added - Support for non-uniform border radius for `Primitive::Quad`. [#1506](https://github.com/iced-rs/iced/pull/1506) @@ -321,7 +364,9 @@ Many thanks to... ### Added - First release! :tada: -[Unreleased]: https://github.com/iced-rs/iced/compare/0.5.0...HEAD +[Unreleased]: https://github.com/iced-rs/iced/compare/0.7.0...HEAD +[0.7.0]: https://github.com/iced-rs/iced/compare/0.6.0...0.7.0 +[0.6.0]: https://github.com/iced-rs/iced/compare/0.5.0...0.6.0 [0.5.0]: https://github.com/iced-rs/iced/compare/0.4.2...0.5.0 [0.4.2]: https://github.com/iced-rs/iced/compare/0.4.1...0.4.2 [0.4.1]: https://github.com/iced-rs/iced/compare/0.4.0...0.4.1 diff --git a/Cargo.toml b/Cargo.toml index 9c45b2f588..185cd39d2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced" -version = "0.6.0" +version = "0.7.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A cross-platform GUI library inspired by Elm" @@ -39,6 +39,13 @@ smol = ["iced_futures/smol"] palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] +# Enables chrome traces +chrome-trace = [ + "iced_winit/chrome-trace", + "iced_glutin?/trace", + "iced_wgpu?/tracing", + "iced_glow?/tracing", +] [badges] maintenance = { status = "actively-developed" } @@ -59,13 +66,13 @@ members = [ ] [dependencies] -iced_core = { version = "0.6", path = "core" } +iced_core = { version = "0.7", path = "core" } iced_futures = { version = "0.5", path = "futures" } -iced_native = { version = "0.7", path = "native" } -iced_graphics = { version = "0.5", path = "graphics" } -iced_winit = { version = "0.6", path = "winit", features = ["application"] } -iced_glutin = { version = "0.5", path = "glutin", optional = true } -iced_glow = { version = "0.5", path = "glow", optional = true } +iced_native = { version = "0.8", path = "native" } +iced_graphics = { version = "0.6", path = "graphics" } +iced_winit = { version = "0.7", path = "winit", features = ["application"] } +iced_glutin = { version = "0.6", path = "glutin", optional = true } +iced_glow = { version = "0.6", path = "glow", optional = true } thiserror = "1.0" [dependencies.image_rs] @@ -74,10 +81,10 @@ package = "image" optional = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_wgpu = { version = "0.7", path = "wgpu", optional = true } +iced_wgpu = { version = "0.8", path = "wgpu", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -iced_wgpu = { version = "0.7", path = "wgpu", features = ["webgl"], optional = true } +iced_wgpu = { version = "0.8", path = "wgpu", features = ["webgl"], optional = true } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index bc7f5440ea..b3790478ef 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Crates.io](https://img.shields.io/crates/v/iced.svg)](https://crates.io/crates/iced) [![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/workflow/status/iced-rs/iced/Test?event=push&label=test)](https://github.com/iced-rs/iced/actions) +[![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) [![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. @@ -68,7 +68,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap], Add `iced` as a dependency in your `Cargo.toml`: ```toml -iced = "0.6" +iced = "0.7" ``` If your project is using a Rust edition older than 2021, then you will need to @@ -215,7 +215,7 @@ cargo run --features iced/glow --package game_of_life and then use it in your project with ```toml -iced = { version = "0.6", default-features = false, features = ["glow"] } +iced = { version = "0.7", default-features = false, features = ["glow"] } ``` __NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0, diff --git a/core/Cargo.toml b/core/Cargo.toml index c401f30a38..eebd2fe3e1 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.6.2" +version = "0.7.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "The essential concepts of Iced" @@ -15,4 +15,4 @@ version = "0.6" optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-timer = { version = "0.2" } +instant = "0.1" diff --git a/core/README.md b/core/README.md index bbb7983c9f..cecbc1e423 100644 --- a/core/README.md +++ b/core/README.md @@ -18,7 +18,7 @@ This crate is meant to be a starting point for an Iced runtime. Add `iced_core` as a dependency in your `Cargo.toml`: ```toml -iced_core = "0.4" +iced_core = "0.7" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/core/src/lib.rs b/core/src/lib.rs index f95d61f642..3aa5defeba 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,7 +7,7 @@ //! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true) //! //! [Iced]: https://github.com/iced-rs/iced -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.6/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.7/native //! [`iced_web`]: https://github.com/iced-rs/iced_web #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 4e0820510e..4fe9151957 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -116,8 +116,8 @@ impl std::ops::Mul for Rectangle { fn mul(self, scale: f32) -> Self { Self { - x: self.x as f32 * scale, - y: self.y as f32 * scale, + x: self.x * scale, + y: self.y * scale, width: self.width * scale, height: self.height * scale, } diff --git a/core/src/size.rs b/core/src/size.rs index 31f3171b66..a2c7292631 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,5 +1,4 @@ use crate::{Padding, Vector}; -use std::f32; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/core/src/time.rs b/core/src/time.rs index f496d1a4ba..9355ae6d84 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -1,9 +1,13 @@ //! Keep track of time, both in native and web platforms! #[cfg(target_arch = "wasm32")] -pub use wasm_timer::Instant; +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; diff --git a/examples/README.md b/examples/README.md index bb15dc2ed2..74cf145b57 100644 --- a/examples/README.md +++ b/examples/README.md @@ -99,7 +99,7 @@ A bunch of simpler examples exist: - [`pick_list`](pick_list), a dropdown list of selectable options. - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI]. - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. -- [`scrollable`](scrollable), a showcase of the various scrollbar width options. +- [`scrollable`](scrollable), a showcase of various scrollable content configurations. - [`sierpinski_triangle`](sierpinski_triangle), a [sierpiński triangle](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) Emulator, use `Canvas` and `Slider`. - [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms. - [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time. diff --git a/examples/cached/Cargo.toml b/examples/cached/Cargo.toml deleted file mode 100644 index 2c7edde268..0000000000 --- a/examples/cached/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "cached" -version = "0.1.0" -authors = ["Nick Senger "] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../..", features = ["debug"] } -iced_lazy = { path = "../../lazy" } diff --git a/examples/cached/src/main.rs b/examples/cached/src/main.rs deleted file mode 100644 index 8845b87407..0000000000 --- a/examples/cached/src/main.rs +++ /dev/null @@ -1,139 +0,0 @@ -use iced::theme; -use iced::widget::{ - button, column, horizontal_space, row, scrollable, text, text_input, -}; -use iced::{Element, Length, Sandbox, Settings}; -use iced_lazy::lazy; - -use std::collections::HashSet; - -pub fn main() -> iced::Result { - App::run(Settings::default()) -} - -struct App { - options: HashSet, - input: String, - order: Order, -} - -impl Default for App { - fn default() -> Self { - Self { - options: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] - .into_iter() - .map(ToString::to_string) - .collect(), - input: Default::default(), - order: Order::Ascending, - } - } -} - -#[derive(Debug, Clone)] -enum Message { - InputChanged(String), - ToggleOrder, - DeleteOption(String), - AddOption(String), -} - -impl Sandbox for App { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Cached - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::InputChanged(input) => { - self.input = input; - } - Message::ToggleOrder => { - self.order = match self.order { - Order::Ascending => Order::Descending, - Order::Descending => Order::Ascending, - } - } - Message::AddOption(option) => { - self.options.insert(option); - self.input.clear(); - } - Message::DeleteOption(option) => { - self.options.remove(&option); - } - } - } - - fn view(&self) -> Element { - let options = lazy((&self.order, self.options.len()), || { - let mut options: Vec<_> = self.options.iter().collect(); - - options.sort_by(|a, b| match self.order { - Order::Ascending => a.to_lowercase().cmp(&b.to_lowercase()), - Order::Descending => b.to_lowercase().cmp(&a.to_lowercase()), - }); - - column( - options - .into_iter() - .map(|option| { - row![ - text(option), - horizontal_space(Length::Fill), - button("Delete") - .on_press(Message::DeleteOption( - option.to_string(), - ),) - .style(theme::Button::Destructive) - ] - .into() - }) - .collect(), - ) - .spacing(10) - }); - - column![ - scrollable(options).height(Length::Fill), - row![ - text_input( - "Add a new option", - &self.input, - Message::InputChanged, - ) - .on_submit(Message::AddOption(self.input.clone())), - button(text(format!("Toggle Order ({})", self.order))) - .on_press(Message::ToggleOrder) - ] - .spacing(10) - ] - .spacing(20) - .padding(20) - .into() - } -} - -#[derive(Debug, Hash)] -enum Order { - Ascending, - Descending, -} - -impl std::fmt::Display for Order { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Ascending => "Ascending", - Self::Descending => "Descending", - } - ) - } -} diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml index 8ad04a36a4..8c56e4711f 100644 --- a/examples/events/Cargo.toml +++ b/examples/events/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } +iced = { path = "../..", features = ["debug"] } iced_native = { path = "../../native" } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 234e14239d..4ae8d6fb5d 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,11 +1,12 @@ use iced::alignment; use iced::executor; use iced::widget::{button, checkbox, container, text, Column}; +use iced::window; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, Theme, }; -use iced_native::{window, Event}; +use iced_native::Event; pub fn main() -> iced::Result { Events::run(Settings { @@ -18,7 +19,6 @@ pub fn main() -> iced::Result { struct Events { last: Vec, enabled: bool, - should_exit: bool, } #[derive(Debug, Clone)] @@ -50,31 +50,29 @@ impl Application for Events { if self.last.len() > 5 { let _ = self.last.remove(0); } + + Command::none() } Message::EventOccurred(event) => { if let Event::Window(window::Event::CloseRequested) = event { - self.should_exit = true; + window::close() + } else { + Command::none() } } Message::Toggled(enabled) => { self.enabled = enabled; - } - Message::Exit => { - self.should_exit = true; - } - }; - Command::none() + Command::none() + } + Message::Exit => window::close(), + } } fn subscription(&self) -> Subscription { iced_native::subscription::events().map(Message::EventOccurred) } - fn should_exit(&self) -> bool { - self.should_exit - } - fn view(&self) -> Element { let events = Column::with_children( self.last diff --git a/examples/exit/src/main.rs b/examples/exit/src/main.rs index 5d518d2fd0..6152f62706 100644 --- a/examples/exit/src/main.rs +++ b/examples/exit/src/main.rs @@ -1,5 +1,7 @@ +use iced::executor; use iced::widget::{button, column, container}; -use iced::{Alignment, Element, Length, Sandbox, Settings}; +use iced::window; +use iced::{Alignment, Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { Exit::run(Settings::default()) @@ -8,7 +10,6 @@ pub fn main() -> iced::Result { #[derive(Default)] struct Exit { show_confirm: bool, - exit: bool, } #[derive(Debug, Clone, Copy)] @@ -17,28 +18,27 @@ enum Message { Exit, } -impl Sandbox for Exit { +impl Application for Exit { + type Executor = executor::Default; type Message = Message; + type Theme = Theme; + type Flags = (); - fn new() -> Self { - Self::default() + fn new(_flags: ()) -> (Self, Command) { + (Self::default(), Command::none()) } fn title(&self) -> String { String::from("Exit - Iced") } - fn should_exit(&self) -> bool { - self.exit - } - - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { match message { - Message::Confirm => { - self.exit = true; - } + Message::Confirm => window::close(), Message::Exit => { self.show_confirm = true; + + Command::none() } } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 2a8b3721f7..b0f1c96dba 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -492,8 +492,10 @@ mod grid { let old_scaling = self.scaling; let scaling = (self.scaling * (1.0 + y / 30.0)) - .max(Self::MIN_SCALING) - .min(Self::MAX_SCALING); + .clamp( + Self::MIN_SCALING, + Self::MAX_SCALING, + ); let translation = if let Some(cursor_to_center) = diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 8845b87407..6512106f23 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,18 +1,21 @@ use iced::theme; use iced::widget::{ - button, column, horizontal_space, row, scrollable, text, text_input, + button, column, horizontal_space, pick_list, row, scrollable, text, + text_input, }; use iced::{Element, Length, Sandbox, Settings}; use iced_lazy::lazy; use std::collections::HashSet; +use std::hash::Hash; pub fn main() -> iced::Result { App::run(Settings::default()) } struct App { - options: HashSet, + version: u8, + items: HashSet, input: String, order: Order, } @@ -20,9 +23,10 @@ struct App { impl Default for App { fn default() -> Self { Self { - options: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] + version: 0, + items: ["Foo", "Bar", "Baz", "Qux", "Corge", "Waldo", "Fred"] .into_iter() - .map(ToString::to_string) + .map(From::from) .collect(), input: Default::default(), order: Order::Ascending, @@ -30,12 +34,92 @@ impl Default for App { } } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +enum Color { + #[default] + Black, + Red, + Orange, + Yellow, + Green, + Blue, + Purple, +} + +impl Color { + const ALL: &[Color] = &[ + Color::Black, + Color::Red, + Color::Orange, + Color::Yellow, + Color::Green, + Color::Blue, + Color::Purple, + ]; +} + +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Black => "Black", + Self::Red => "Red", + Self::Orange => "Orange", + Self::Yellow => "Yellow", + Self::Green => "Green", + Self::Blue => "Blue", + Self::Purple => "Purple", + }) + } +} + +impl From for iced::Color { + fn from(value: Color) -> Self { + match value { + Color::Black => iced::Color::from_rgb8(0, 0, 0), + Color::Red => iced::Color::from_rgb8(220, 50, 47), + Color::Orange => iced::Color::from_rgb8(203, 75, 22), + Color::Yellow => iced::Color::from_rgb8(181, 137, 0), + Color::Green => iced::Color::from_rgb8(133, 153, 0), + Color::Blue => iced::Color::from_rgb8(38, 139, 210), + Color::Purple => iced::Color::from_rgb8(108, 113, 196), + } + } +} + +#[derive(Clone, Debug, Eq)] +struct Item { + name: String, + color: Color, +} + +impl Hash for Item { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl PartialEq for Item { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl From<&str> for Item { + fn from(s: &str) -> Self { + Self { + name: s.to_owned(), + color: Default::default(), + } + } +} + #[derive(Debug, Clone)] enum Message { InputChanged(String), ToggleOrder, - DeleteOption(String), - AddOption(String), + DeleteItem(Item), + AddItem(String), + ItemColorChanged(Item, Color), } impl Sandbox for App { @@ -46,7 +130,7 @@ impl Sandbox for App { } fn title(&self) -> String { - String::from("Cached - Iced") + String::from("Lazy - Iced") } fn update(&mut self, message: Message) { @@ -55,43 +139,71 @@ impl Sandbox for App { self.input = input; } Message::ToggleOrder => { + self.version = self.version.wrapping_add(1); self.order = match self.order { Order::Ascending => Order::Descending, Order::Descending => Order::Ascending, } } - Message::AddOption(option) => { - self.options.insert(option); + Message::AddItem(name) => { + self.version = self.version.wrapping_add(1); + self.items.insert(name.as_str().into()); self.input.clear(); } - Message::DeleteOption(option) => { - self.options.remove(&option); + Message::DeleteItem(item) => { + self.version = self.version.wrapping_add(1); + self.items.remove(&item); + } + Message::ItemColorChanged(item, color) => { + self.version = self.version.wrapping_add(1); + if self.items.remove(&item) { + self.items.insert(Item { + name: item.name, + color, + }); + } } } } fn view(&self) -> Element { - let options = lazy((&self.order, self.options.len()), || { - let mut options: Vec<_> = self.options.iter().collect(); + let options = lazy(self.version, |_| { + let mut items: Vec<_> = self.items.iter().cloned().collect(); - options.sort_by(|a, b| match self.order { - Order::Ascending => a.to_lowercase().cmp(&b.to_lowercase()), - Order::Descending => b.to_lowercase().cmp(&a.to_lowercase()), + items.sort_by(|a, b| match self.order { + Order::Ascending => { + a.name.to_lowercase().cmp(&b.name.to_lowercase()) + } + Order::Descending => { + b.name.to_lowercase().cmp(&a.name.to_lowercase()) + } }); column( - options + items .into_iter() - .map(|option| { + .map(|item| { + let button = button("Delete") + .on_press(Message::DeleteItem(item.clone())) + .style(theme::Button::Destructive); + row![ - text(option), + text(&item.name) + .style(theme::Text::Color(item.color.into())), horizontal_space(Length::Fill), - button("Delete") - .on_press(Message::DeleteOption( - option.to_string(), - ),) - .style(theme::Button::Destructive) + pick_list( + Color::ALL, + Some(item.color), + move |color| { + Message::ItemColorChanged( + item.clone(), + color, + ) + } + ), + button ] + .spacing(20) .into() }) .collect(), @@ -107,7 +219,7 @@ impl Sandbox for App { &self.input, Message::InputChanged, ) - .on_submit(Message::AddOption(self.input.clone())), + .on_submit(Message::AddItem(self.input.clone())), button(text(format!("Toggle Order ({})", self.order))) .on_press(Message::ToggleOrder) ] diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 2f20795c0a..5afafd0df2 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -325,11 +325,13 @@ mod modal { &self, state: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { self.base.as_widget().operate( &mut state.children[0], layout, + renderer, operation, ); } @@ -436,11 +438,13 @@ mod modal { fn operate( &mut self, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { self.content.as_widget().operate( self.tree, layout.children().next().unwrap(), + renderer, operation, ); } diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml index 610c13b4b1..e6411e26ec 100644 --- a/examples/scrollable/Cargo.toml +++ b/examples/scrollable/Cargo.toml @@ -7,3 +7,4 @@ publish = false [dependencies] iced = { path = "../..", features = ["debug"] } +once_cell = "1.16.0" diff --git a/examples/scrollable/screenshot.png b/examples/scrollable/screenshot.png index e91fd565c5..ee044447b6 100644 Binary files a/examples/scrollable/screenshot.png and b/examples/scrollable/screenshot.png differ diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index 6eba34e26c..128d98b220 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -1,44 +1,58 @@ -use iced::executor; +use iced::widget::scrollable::{Properties, Scrollbar, Scroller}; use iced::widget::{ - button, column, container, horizontal_rule, progress_bar, radio, - scrollable, text, vertical_space, Row, + button, column, container, horizontal_space, progress_bar, radio, row, + scrollable, slider, text, vertical_space, }; +use iced::{executor, theme, Alignment, Color}; use iced::{Application, Command, Element, Length, Settings, Theme}; +use once_cell::sync::Lazy; + +static SCROLLABLE_ID: Lazy = Lazy::new(scrollable::Id::unique); pub fn main() -> iced::Result { ScrollableDemo::run(Settings::default()) } struct ScrollableDemo { - theme: Theme, - variants: Vec, + scrollable_direction: Direction, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + current_scroll_offset: scrollable::RelativeOffset, } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -enum ThemeType { - Light, - Dark, +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +enum Direction { + Vertical, + Horizontal, + Multi, } #[derive(Debug, Clone)] enum Message { - ThemeChanged(ThemeType), - ScrollToTop(usize), - ScrollToBottom(usize), - Scrolled(usize, f32), + SwitchDirection(Direction), + ScrollbarWidthChanged(u16), + ScrollbarMarginChanged(u16), + ScrollerWidthChanged(u16), + ScrollToBeginning, + ScrollToEnd, + Scrolled(scrollable::RelativeOffset), } impl Application for ScrollableDemo { + type Executor = executor::Default; type Message = Message; type Theme = Theme; - type Executor = executor::Default; type Flags = (); fn new(_flags: Self::Flags) -> (Self, Command) { ( ScrollableDemo { - theme: Default::default(), - variants: Variant::all(), + scrollable_direction: Direction::Vertical, + scrollbar_width: 10, + scrollbar_margin: 0, + scroller_width: 10, + current_scroll_offset: scrollable::RelativeOffset::START, }, Command::none(), ) @@ -50,36 +64,48 @@ impl Application for ScrollableDemo { fn update(&mut self, message: Message) -> Command { match message { - Message::ThemeChanged(theme) => { - self.theme = match theme { - ThemeType::Light => Theme::Light, - ThemeType::Dark => Theme::Dark, - }; + Message::SwitchDirection(direction) => { + self.current_scroll_offset = scrollable::RelativeOffset::START; + self.scrollable_direction = direction; + + scrollable::snap_to( + SCROLLABLE_ID.clone(), + self.current_scroll_offset, + ) + } + Message::ScrollbarWidthChanged(width) => { + self.scrollbar_width = width; + + Command::none() + } + Message::ScrollbarMarginChanged(margin) => { + self.scrollbar_margin = margin; Command::none() } - Message::ScrollToTop(i) => { - if let Some(variant) = self.variants.get_mut(i) { - variant.latest_offset = 0.0; - - scrollable::snap_to(Variant::id(i), 0.0) - } else { - Command::none() - } + Message::ScrollerWidthChanged(width) => { + self.scroller_width = width; + + Command::none() } - Message::ScrollToBottom(i) => { - if let Some(variant) = self.variants.get_mut(i) { - variant.latest_offset = 1.0; - - scrollable::snap_to(Variant::id(i), 1.0) - } else { - Command::none() - } + Message::ScrollToBeginning => { + self.current_scroll_offset = scrollable::RelativeOffset::START; + + scrollable::snap_to( + SCROLLABLE_ID.clone(), + self.current_scroll_offset, + ) } - Message::Scrolled(i, offset) => { - if let Some(variant) = self.variants.get_mut(i) { - variant.latest_offset = offset; - } + Message::ScrollToEnd => { + self.current_scroll_offset = scrollable::RelativeOffset::END; + + scrollable::snap_to( + SCROLLABLE_ID.clone(), + self.current_scroll_offset, + ) + } + Message::Scrolled(offset) => { + self.current_scroll_offset = offset; Command::none() } @@ -87,172 +113,262 @@ impl Application for ScrollableDemo { } fn view(&self) -> Element { - let ScrollableDemo { variants, .. } = self; - - let choose_theme = [ThemeType::Light, ThemeType::Dark].iter().fold( - column!["Choose a theme:"].spacing(10), - |column, option| { - column.push(radio( - format!("{:?}", option), - *option, - Some(*option), - Message::ThemeChanged, - )) - }, + let scrollbar_width_slider = slider( + 0..=15, + self.scrollbar_width, + Message::ScrollbarWidthChanged, + ); + let scrollbar_margin_slider = slider( + 0..=15, + self.scrollbar_margin, + Message::ScrollbarMarginChanged, ); + let scroller_width_slider = + slider(0..=15, self.scroller_width, Message::ScrollerWidthChanged); - let scrollable_row = Row::with_children( - variants - .iter() - .enumerate() - .map(|(i, variant)| { - let mut contents = column![ - variant.title, - button("Scroll to bottom",) - .width(Length::Fill) - .padding(10) - .on_press(Message::ScrollToBottom(i)), - ] - .padding(10) - .spacing(10) - .width(Length::Fill); - - if let Some(scrollbar_width) = variant.scrollbar_width { - contents = contents.push(text(format!( - "scrollbar_width: {:?}", - scrollbar_width - ))); - } - - if let Some(scrollbar_margin) = variant.scrollbar_margin { - contents = contents.push(text(format!( - "scrollbar_margin: {:?}", - scrollbar_margin - ))); - } - - if let Some(scroller_width) = variant.scroller_width { - contents = contents.push(text(format!( - "scroller_width: {:?}", - scroller_width - ))); - } - - contents = contents - .push(vertical_space(Length::Units(100))) - .push( - "Some content that should wrap within the \ - scrollable. Let's output a lot of short words, so \ - that we'll make sure to see how wrapping works \ - with these scrollbars.", - ) - .push(vertical_space(Length::Units(1200))) - .push("Middle") - .push(vertical_space(Length::Units(1200))) - .push("The End.") - .push( - button("Scroll to top") - .width(Length::Fill) - .padding(10) - .on_press(Message::ScrollToTop(i)), - ); - - let mut scrollable = scrollable(contents) - .id(Variant::id(i)) - .height(Length::Fill) - .on_scroll(move |offset| Message::Scrolled(i, offset)); - - if let Some(scrollbar_width) = variant.scrollbar_width { - scrollable = - scrollable.scrollbar_width(scrollbar_width); - } - - if let Some(scrollbar_margin) = variant.scrollbar_margin { - scrollable = - scrollable.scrollbar_margin(scrollbar_margin); - } - - if let Some(scroller_width) = variant.scroller_width { - scrollable = scrollable.scroller_width(scroller_width); - } + let scroll_slider_controls = column![ + text("Scrollbar width:"), + scrollbar_width_slider, + text("Scrollbar margin:"), + scrollbar_margin_slider, + text("Scroller width:"), + scroller_width_slider, + ] + .spacing(10) + .width(Length::Fill); + + let scroll_orientation_controls = column(vec![ + text("Scrollbar direction:").into(), + 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); + let scroll_controls = + row![scroll_slider_controls, scroll_orientation_controls] + .spacing(20) + .width(Length::Fill); + + let scroll_to_end_button = || { + button("Scroll to end") + .padding(10) + .on_press(Message::ScrollToEnd) + }; + + let scroll_to_beginning_button = || { + button("Scroll to beginning") + .padding(10) + .on_press(Message::ScrollToBeginning) + }; + + let scrollable_content: Element = + Element::from(match self.scrollable_direction { + Direction::Vertical => scrollable( column![ - scrollable, - progress_bar(0.0..=1.0, variant.latest_offset,) + scroll_to_end_button(), + text("Beginning!"), + vertical_space(Length::Units(1200)), + text("Middle!"), + vertical_space(Length::Units(1200)), + text("End!"), + scroll_to_beginning_button(), ] .width(Length::Fill) - .height(Length::Fill) - .spacing(10) + .align_items(Alignment::Center) + .padding([40, 0, 40, 0]) + .spacing(40), + ) + .height(Length::Fill) + .vertical_scroll( + Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width), + ) + .id(SCROLLABLE_ID.clone()) + .on_scroll(Message::Scrolled), + Direction::Horizontal => scrollable( + row![ + scroll_to_end_button(), + text("Beginning!"), + horizontal_space(Length::Units(1200)), + text("Middle!"), + horizontal_space(Length::Units(1200)), + text("End!"), + scroll_to_beginning_button(), + ] + .height(Length::Units(450)) + .align_items(Alignment::Center) + .padding([0, 40, 0, 40]) + .spacing(40), + ) + .height(Length::Fill) + .horizontal_scroll( + Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width), + ) + .style(theme::Scrollable::custom(ScrollbarCustomStyle)) + .id(SCROLLABLE_ID.clone()) + .on_scroll(Message::Scrolled), + Direction::Multi => scrollable( + //horizontal content + row![ + column![ + text("Let's do some scrolling!"), + vertical_space(Length::Units(2400)) + ], + scroll_to_end_button(), + text("Horizontal - Beginning!"), + horizontal_space(Length::Units(1200)), + //vertical content + column![ + text("Horizontal - Middle!"), + scroll_to_end_button(), + text("Vertical - Beginning!"), + vertical_space(Length::Units(1200)), + text("Vertical - Middle!"), + vertical_space(Length::Units(1200)), + text("Vertical - End!"), + scroll_to_beginning_button(), + vertical_space(Length::Units(40)), + ] + .align_items(Alignment::Fill) + .spacing(40), + horizontal_space(Length::Units(1200)), + text("Horizontal - End!"), + scroll_to_beginning_button(), + ] + .align_items(Alignment::Center) + .padding([0, 40, 0, 40]) + .spacing(40), + ) + .height(Length::Fill) + .vertical_scroll( + Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width), + ) + .horizontal_scroll( + Properties::new() + .width(self.scrollbar_width) + .margin(self.scrollbar_margin) + .scroller_width(self.scroller_width), + ) + .style(theme::Scrollable::Custom(Box::new( + ScrollbarCustomStyle, + ))) + .id(SCROLLABLE_ID.clone()) + .on_scroll(Message::Scrolled), + }); + + let progress_bars: Element = match self.scrollable_direction { + Direction::Vertical => { + progress_bar(0.0..=1.0, self.current_scroll_offset.y).into() + } + Direction::Horizontal => { + progress_bar(0.0..=1.0, self.current_scroll_offset.x) + .style(theme::ProgressBar::Custom(Box::new( + ProgressBarCustomStyle, + ))) .into() - }) - .collect(), - ) - .spacing(20) - .width(Length::Fill) - .height(Length::Fill); + } + Direction::Multi => column![ + progress_bar(0.0..=1.0, self.current_scroll_offset.y), + progress_bar(0.0..=1.0, self.current_scroll_offset.x).style( + theme::ProgressBar::Custom(Box::new( + ProgressBarCustomStyle, + )) + ) + ] + .spacing(10) + .into(), + }; - let content = - column![choose_theme, horizontal_rule(20), scrollable_row] - .spacing(20) - .padding(20); - - container(content) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() + let content: Element = + column![scroll_controls, scrollable_content, progress_bars] + .width(Length::Fill) + .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(), + ) } - fn theme(&self) -> Theme { - self.theme.clone() + fn theme(&self) -> Self::Theme { + Theme::Dark } } -/// A version of a scrollable -struct Variant { - title: &'static str, - scrollbar_width: Option, - scrollbar_margin: Option, - scroller_width: Option, - latest_offset: f32, -} +struct ScrollbarCustomStyle; -impl Variant { - pub fn all() -> Vec { - vec![ - Self { - title: "Default Scrollbar", - scrollbar_width: None, - scrollbar_margin: None, - scroller_width: None, - latest_offset: 0.0, - }, - Self { - title: "Slimmed & Margin", - scrollbar_width: Some(4), - scrollbar_margin: Some(3), - scroller_width: Some(4), - latest_offset: 0.0, - }, - Self { - title: "Wide Scroller", - scrollbar_width: Some(4), - scrollbar_margin: None, - scroller_width: Some(10), - latest_offset: 0.0, - }, - Self { - title: "Narrow Scroller", - scrollbar_width: Some(10), - scrollbar_margin: None, - scroller_width: Some(4), - latest_offset: 0.0, +impl scrollable::StyleSheet for ScrollbarCustomStyle { + type Style = Theme; + + fn active(&self, style: &Self::Style) -> Scrollbar { + style.active(&theme::Scrollable::Default) + } + + fn hovered(&self, style: &Self::Style) -> Scrollbar { + style.hovered(&theme::Scrollable::Default) + } + + fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar { + Scrollbar { + background: style.active(&theme::Scrollable::default()).background, + border_radius: 0.0, + border_width: 0.0, + border_color: Default::default(), + scroller: Scroller { + color: Color::from_rgb8(250, 85, 134), + border_radius: 0.0, + border_width: 0.0, + border_color: Default::default(), }, - ] + } } +} + +struct ProgressBarCustomStyle; - pub fn id(i: usize) -> scrollable::Id { - scrollable::Id::new(format!("scrollable-{}", i)) +impl progress_bar::StyleSheet for ProgressBarCustomStyle { + type Style = Theme; + + fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { + progress_bar::Appearance { + background: style.extended_palette().background.strong.color.into(), + bar: Color::from_rgb8(250, 85, 134).into(), + border_radius: 0.0, + } } } diff --git a/examples/slider/Cargo.toml b/examples/slider/Cargo.toml new file mode 100644 index 0000000000..112d7cff5e --- /dev/null +++ b/examples/slider/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "slider" +version = "0.1.0" +authors = ["Casper Rogild Storm"] +edition = "2021" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/slider/README.md b/examples/slider/README.md new file mode 100644 index 0000000000..829d828506 --- /dev/null +++ b/examples/slider/README.md @@ -0,0 +1,14 @@ +## Slider + +A `Slider` is a bar and a handle that selects a single value from a range of values. +There exists both `Slider` and `VerticalSlider` depending on which orientation you need. + +
+ +
+ +You can run it with `cargo run`: + +``` +cargo run --package slider +``` diff --git a/examples/slider/sliders.gif b/examples/slider/sliders.gif new file mode 100644 index 0000000000..f906d05ab1 Binary files /dev/null and b/examples/slider/sliders.gif differ diff --git a/examples/slider/src/main.rs b/examples/slider/src/main.rs new file mode 100644 index 0000000000..6286d62581 --- /dev/null +++ b/examples/slider/src/main.rs @@ -0,0 +1,63 @@ +use iced::widget::{column, container, slider, text, vertical_slider}; +use iced::{Element, Length, Sandbox, Settings}; + +pub fn main() -> iced::Result { + Slider::run(Settings::default()) +} + +#[derive(Debug, Clone)] +pub enum Message { + SliderChanged(u8), +} + +pub struct Slider { + slider_value: u8, +} + +impl Sandbox for Slider { + type Message = Message; + + fn new() -> Slider { + Slider { slider_value: 50 } + } + + fn title(&self) -> String { + String::from("Slider - Iced") + } + + fn update(&mut self, message: Message) { + match message { + Message::SliderChanged(value) => { + self.slider_value = value; + } + } + } + + fn view(&self) -> Element { + let value = self.slider_value; + + let h_slider = + container(slider(0..=100, value, Message::SliderChanged)) + .width(Length::Units(250)); + + let v_slider = + container(vertical_slider(0..=100, value, Message::SliderChanged)) + .height(Length::Units(200)); + + let text = text(format!("{value}")); + + container( + column![ + container(v_slider).width(Length::Fill).center_x(), + container(h_slider).width(Length::Fill).center_x(), + container(text).width(Length::Fill).center_x(), + ] + .spacing(25), + ) + .height(Length::Fill) + .width(Length::Fill) + .center_x() + .center_y() + .into() + } +} diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9e303576f9..9a4ee7549a 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -9,7 +9,6 @@ use iced::application; use iced::executor; use iced::theme::{self, Theme}; -use iced::time; use iced::widget::canvas; use iced::widget::canvas::gradient::{self, Gradient}; use iced::widget::canvas::stroke::{self, Stroke}; @@ -90,7 +89,7 @@ impl Application for SolarSystem { } fn subscription(&self) -> Subscription { - time::every(time::Duration::from_millis(10)).map(Message::Tick) + window::frames().map(Message::Tick) } } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index ff2929da84..ccd9c81582 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -81,7 +81,10 @@ impl Application for WebSocket { echo::Event::MessageReceived(message) => { self.messages.push(message); - scrollable::snap_to(MESSAGE_LOG.clone(), 1.0) + scrollable::snap_to( + MESSAGE_LOG.clone(), + scrollable::RelativeOffset::END, + ) } }, Message::Server => Command::none(), diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 1d4b68a632..e96fa704ea 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -125,9 +125,9 @@ impl std::fmt::Debug for Subscription { /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.6/examples -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.6/examples/download_progress -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.6/examples/stopwatch +/// [examples]: https://github.com/iced-rs/iced/tree/0.7/examples +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.7/examples/download_progress +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.7/examples/stopwatch pub trait Recipe { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. diff --git a/glow/Cargo.toml b/glow/Cargo.toml index f586d24d96..cf5dfb8a87 100644 --- a/glow/Cargo.toml +++ b/glow/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glow" -version = "0.5.1" +version = "0.6.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A glow renderer for iced" @@ -34,14 +34,18 @@ bytemuck = "1.4" log = "0.4" [dependencies.iced_native] -version = "0.7" +version = "0.8" path = "../native" [dependencies.iced_graphics] -version = "0.5" +version = "0.6" path = "../graphics" features = ["font-fallback", "font-icons", "opengl"] +[dependencies.tracing] +version = "0.1.6" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/glow/README.md b/glow/README.md index 00f38f644d..38449c640a 100644 --- a/glow/README.md +++ b/glow/README.md @@ -28,7 +28,7 @@ Currently, `iced_glow` supports the following primitives: Add `iced_glow` as a dependency in your `Cargo.toml`: ```toml -iced_glow = "0.2" +iced_glow = "0.6" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/glow/src/image.rs b/glow/src/image.rs index 521a01e7bc..d3a25b5ba7 100644 --- a/glow/src/image.rs +++ b/glow/src/image.rs @@ -21,6 +21,9 @@ use glow::HasContext; use std::cell::RefCell; +#[cfg(feature = "tracing")] +use tracing::info_span; + #[derive(Debug)] pub(crate) struct Pipeline { program: ::Program, @@ -148,6 +151,9 @@ impl Pipeline { images: &[layer::Image], layer_bounds: Rectangle, ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Glow::Image", "DRAW").entered(); + unsafe { gl.use_program(Some(self.program)); gl.bind_vertex_array(Some(self.vertex_array)); diff --git a/glow/src/lib.rs b/glow/src/lib.rs index 710ac36d8d..a12c45b83e 100644 --- a/glow/src/lib.rs +++ b/glow/src/lib.rs @@ -3,7 +3,7 @@ //! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) //! //! [`glow`]: https://github.com/grovesNL/glow -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.6/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.7/native #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] diff --git a/glow/src/quad.rs b/glow/src/quad.rs index d9f1c6ae70..67d9a0982b 100644 --- a/glow/src/quad.rs +++ b/glow/src/quad.rs @@ -7,6 +7,9 @@ use glow::HasContext; use iced_graphics::layer; use iced_native::Rectangle; +#[cfg(feature = "tracing")] +use tracing::info_span; + #[derive(Debug)] pub enum Pipeline { Core(core::Pipeline), @@ -42,6 +45,9 @@ impl Pipeline { scale: f32, bounds: Rectangle, ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Glow::Quad", "DRAW").enter(); + match self { Pipeline::Core(pipeline) => { pipeline.draw( diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs index d0205e0818..42c8845526 100644 --- a/glow/src/triangle.rs +++ b/glow/src/triangle.rs @@ -9,6 +9,9 @@ use iced_graphics::triangle::{ColoredVertex2D, Vertex2D}; use glow::HasContext; use std::marker::PhantomData; +#[cfg(feature = "tracing")] +use tracing::info_span; + const DEFAULT_VERTICES: usize = 1_000; const DEFAULT_INDICES: usize = 1_000; @@ -58,6 +61,9 @@ impl Pipeline { transformation: Transformation, scale_factor: f32, ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Glow::Triangle", "DRAW").enter(); + unsafe { gl.enable(glow::MULTISAMPLE); gl.enable(glow::SCISSOR_TEST); diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml index 687b71e89a..a1edad1739 100644 --- a/glutin/Cargo.toml +++ b/glutin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_glutin" -version = "0.5.0" +version = "0.6.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A glutin runtime for Iced" @@ -11,25 +11,30 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +trace = ["iced_winit/trace"] debug = ["iced_winit/debug"] system = ["iced_winit/system"] -[dependencies.log] -version = "0.4" +[dependencies] +log = "0.4" [dependencies.glutin] version = "0.29" [dependencies.iced_native] -version = "0.7" +version = "0.8" path = "../native" [dependencies.iced_winit] -version = "0.6" +version = "0.7" path = "../winit" features = ["application"] [dependencies.iced_graphics] -version = "0.5" +version = "0.6" path = "../graphics" features = ["opengl"] + +[dependencies.tracing] +version = "0.1.6" +optional = true diff --git a/glutin/README.md b/glutin/README.md index 263cc0af7d..1d87387454 100644 --- a/glutin/README.md +++ b/glutin/README.md @@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t Add `iced_glutin` as a dependency in your `Cargo.toml`: ```toml -iced_glutin = "0.2" +iced_glutin = "0.6" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/glutin/src/application.rs b/glutin/src/application.rs index b897afb021..c1e0159d32 100644 --- a/glutin/src/application.rs +++ b/glutin/src/application.rs @@ -11,12 +11,16 @@ use iced_winit::conversion; use iced_winit::futures; use iced_winit::futures::channel::mpsc; use iced_winit::renderer; +use iced_winit::time::Instant; use iced_winit::user_interface; -use iced_winit::{Clipboard, Command, Debug, Proxy, Settings}; +use iced_winit::{Clipboard, Command, Debug, Event, Proxy, Settings}; use glutin::window::Window; use std::mem::ManuallyDrop; +#[cfg(feature = "tracing")] +use tracing::{info_span, instrument::Instrument}; + /// Runs an [`Application`] with an executor, compositor, and the provided /// settings. pub fn run( @@ -35,9 +39,15 @@ where use glutin::platform::run_return::EventLoopExtRunReturn; use glutin::ContextBuilder; + #[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(); @@ -122,20 +132,30 @@ where })? }; - 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 event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, mut control_receiver) = mpsc::unbounded(); + + let mut instance = Box::pin({ + let run_instance = run_instance::( + application, + compositor, + renderer, + runtime, + proxy, + debug, + event_receiver, + control_sender, + 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()); @@ -162,14 +182,20 @@ 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; + } + } } }); @@ -183,7 +209,10 @@ async fn run_instance( mut runtime: Runtime, A::Message>, mut proxy: glutin::event_loop::EventLoopProxy, mut debug: Debug, - mut receiver: mpsc::UnboundedReceiver>, + 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, @@ -194,12 +223,14 @@ async fn run_instance( ::Theme: StyleSheet, { use glutin::event; + use glutin::event_loop::ControlFlow; use iced_winit::futures::stream::StreamExt; let mut clipboard = Clipboard::connect(context.window()); let mut cache = user_interface::Cache::default(); let mut state = application::State::new(&application, context.window()); let mut viewport_version = state.viewport_version(); + let mut should_exit = false; application::run_command( &application, @@ -209,6 +240,7 @@ async fn run_instance( init_command, &mut runtime, &mut clipboard, + &mut should_exit, &mut proxy, &mut debug, context.window(), @@ -228,13 +260,22 @@ 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(); - while let Some(event) = receiver.next().await { + 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 => { - if events.is_empty() && messages.is_empty() { + if !redraw_pending && events.is_empty() && messages.is_empty() { continue; } @@ -271,6 +312,7 @@ async fn run_instance( &mut renderer, &mut runtime, &mut clipboard, + &mut should_exit, &mut proxy, &mut debug, &mut messages, @@ -281,8 +323,6 @@ async fn run_instance( // Update window state.synchronize(&application, context.window()); - let should_exit = application.should_exit(); - user_interface = ManuallyDrop::new(application::build_user_interface( &application, @@ -297,6 +337,23 @@ async fn run_instance( } } + // 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( + crate::window::Event::RedrawRequested(Instant::now()), + ); + + let (interface_state, _) = user_interface.update( + &[redraw_event.clone()], + state.cursor_position(), + &mut renderer, + &mut clipboard, + &mut messages, + ); + debug.draw_started(); let new_mouse_interaction = user_interface.draw( &mut renderer, @@ -317,11 +374,32 @@ async fn run_instance( } context.window().request_redraw(); + runtime + .broadcast((redraw_event, crate::event::Status::Ignored)); + + let _ = control_sender.start_send(match interface_state { + user_interface::State::Updated { + redraw_request: Some(redraw_request), + } => match redraw_request { + crate::window::RedrawRequest::NextFrame => { + ControlFlow::Poll + } + crate::window::RedrawRequest::At(at) => { + ControlFlow::WaitUntil(at) + } + }, + _ => ControlFlow::Wait, + }); + + redraw_pending = false; } event::Event::UserEvent(message) => { messages.push(message); } event::Event::RedrawRequested(_) => { + #[cfg(feature = "tracing")] + let _ = info_span!("Application", "FRAME").entered(); + debug.render_started(); #[allow(unsafe_code)] diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 823a05f436..664bb19fb6 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_graphics" -version = "0.5.0" +version = "0.6.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced" @@ -44,11 +44,11 @@ version = "1.4" features = ["derive"] [dependencies.iced_native] -version = "0.7" +version = "0.8" path = "../native" [dependencies.iced_style] -version = "0.5" +version = "0.6" path = "../style" [dependencies.lyon] diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs index 83f25238b2..61e919d6bf 100644 --- a/graphics/src/gradient.rs +++ b/graphics/src/gradient.rs @@ -64,7 +64,7 @@ impl From<(Point, Point)> for Position { } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] /// The location of a relatively-positioned gradient. pub enum Location { /// Top left. @@ -86,7 +86,7 @@ pub enum Location { } impl Location { - fn to_absolute(&self, top_left: Point, size: Size) -> Point { + fn to_absolute(self, top_left: Point, size: Size) -> Point { match self { Location::TopLeft => top_left, Location::Top => { diff --git a/graphics/src/image/storage.rs b/graphics/src/image/storage.rs index 2098c7b2ba..1b5b5c3556 100644 --- a/graphics/src/image/storage.rs +++ b/graphics/src/image/storage.rs @@ -20,7 +20,7 @@ pub trait Storage { state: &mut Self::State<'_>, ) -> Option; - /// Romve a [`Self::Entry`] from the [`Storage`]. + /// Remove a [`Self::Entry`] from the [`Storage`]. fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); } diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml index 1b26e5c9c2..657da5caac 100644 --- a/lazy/Cargo.toml +++ b/lazy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_lazy" -version = "0.3.0" +version = "0.4.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "Lazy widgets for Iced" @@ -14,5 +14,5 @@ categories = ["gui"] ouroboros = "0.13" [dependencies.iced_native] -version = "0.7" +version = "0.8" path = "../native" diff --git a/lazy/src/component.rs b/lazy/src/component.rs index 3d7b8758bc..d8f21f8acf 100644 --- a/lazy/src/component.rs +++ b/lazy/src/component.rs @@ -46,6 +46,16 @@ pub trait Component { /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) /// on user interaction. fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>; + + /// Update the [`Component`] state based on the provided [`Operation`](widget::Operation) + /// + /// By default, it does nothing. + fn operate( + &self, + _state: &mut Self::State, + _operation: &mut dyn widget::Operation, + ) { + } } /// Turns an implementor of [`Component`] into an [`Element`] that can be @@ -106,6 +116,26 @@ where ); } + fn rebuild_element_with_operation( + &self, + state: &mut S, + operation: &mut dyn widget::Operation, + ) { + let heads = self.state.borrow_mut().take().unwrap().into_heads(); + + heads.component.operate(state, operation); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| Some(component.view(state)), + } + .build(), + ); + } + fn with_element( &self, f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, @@ -234,8 +264,14 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { + self.rebuild_element_with_operation( + tree.state.downcast_mut(), + operation, + ); + struct MapOperation<'a, B> { operation: &'a mut dyn widget::Operation, } @@ -274,6 +310,7 @@ where element.as_widget().operate( &mut tree.children[0], layout, + renderer, &mut MapOperation { operation }, ); }); diff --git a/lazy/src/lazy.rs b/lazy/src/lazy.rs index 2611dd1092..933def96eb 100644 --- a/lazy/src/lazy.rs +++ b/lazy/src/lazy.rs @@ -9,16 +9,17 @@ use iced_native::Element; use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size}; use ouroboros::self_referencing; -use std::cell::{Ref, RefCell, RefMut}; +use std::cell::RefCell; use std::hash::{Hash, Hasher as H}; -use std::marker::PhantomData; use std::rc::Rc; #[allow(missing_debug_implementations)] pub struct Lazy<'a, Message, Renderer, Dependency, View> { dependency: Dependency, - view: Box View + 'a>, - element: RefCell>>>>, + view: Box View + 'a>, + element: RefCell< + Option>>>>, + >, } impl<'a, Message, Renderer, Dependency, View> @@ -27,7 +28,10 @@ where Dependency: Hash + 'a, View: Into>, { - pub fn new(dependency: Dependency, view: impl Fn() -> View + 'a) -> Self { + pub fn new( + dependency: Dependency, + view: impl Fn(&Dependency) -> View + 'a, + ) -> Self { Self { dependency, view: Box::new(view), @@ -37,21 +41,35 @@ where fn with_element( &self, - f: impl FnOnce(Ref>) -> T, + f: impl FnOnce(&Element) -> T, ) -> T { - f(self.element.borrow().as_ref().unwrap().borrow()) + f(self + .element + .borrow() + .as_ref() + .unwrap() + .borrow() + .as_ref() + .unwrap()) } fn with_element_mut( &self, - f: impl FnOnce(RefMut>) -> T, + f: impl FnOnce(&mut Element) -> T, ) -> T { - f(self.element.borrow().as_ref().unwrap().borrow_mut()) + f(self + .element + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .as_mut() + .unwrap()) } } struct Internal { - element: Rc>>, + element: Rc>>>, hash: u64, } @@ -73,7 +91,8 @@ where self.dependency.hash(&mut hasher); let hash = hasher.finish(); - let element = Rc::new(RefCell::new((self.view)().into())); + let element = + Rc::new(RefCell::new(Some((self.view)(&self.dependency).into()))); (*self.element.borrow_mut()) = Some(element.clone()); @@ -81,9 +100,7 @@ where } fn children(&self) -> Vec { - vec![Tree::new( - self.element.borrow().as_ref().unwrap().borrow().as_widget(), - )] + self.with_element(|element| vec![Tree::new(element.as_widget())]) } fn diff(&self, tree: &mut Tree) { @@ -96,13 +113,13 @@ where if current.hash != new_hash { current.hash = new_hash; - let element = (self.view)().into(); - current.element = Rc::new(RefCell::new(element)); + let element = (self.view)(&self.dependency).into(); + current.element = Rc::new(RefCell::new(Some(element))); (*self.element.borrow_mut()) = Some(current.element.clone()); - tree.diff_children(std::slice::from_ref( - &self.element.borrow().as_ref().unwrap().borrow().as_widget(), - )); + self.with_element(|element| { + tree.diff_children(std::slice::from_ref(&element.as_widget())) + }); } else { (*self.element.borrow_mut()) = Some(current.element.clone()); } @@ -130,12 +147,14 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { self.with_element(|element| { element.as_widget().operate( &mut tree.children[0], layout, + renderer, operation, ); }); @@ -151,7 +170,7 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - self.with_element_mut(|mut element| { + self.with_element_mut(|element| { element.as_widget_mut().on_event( &mut tree.children[0], event, @@ -212,23 +231,27 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option> { - let overlay = OverlayBuilder { - cached: self, - tree: &mut tree.children[0], - types: PhantomData, - overlay_builder: |cached, tree| { - Rc::get_mut(cached.element.get_mut().as_mut().unwrap()) + let overlay = Overlay(Some( + InnerBuilder { + cell: self.element.borrow().as_ref().unwrap().clone(), + element: self + .element + .borrow() + .as_ref() .unwrap() - .get_mut() - .as_widget_mut() - .overlay(tree, layout, renderer) - }, - } - .build(); - - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); + .borrow_mut() + .take() + .unwrap(), + tree: &mut tree.children[0], + overlay_builder: |element, tree| { + element.as_widget_mut().overlay(tree, layout, renderer) + }, + } + .build(), + )); + + let has_overlay = overlay + .with_overlay_maybe(|overlay| overlay::Element::position(overlay)); has_overlay .map(|position| overlay::Element::new(position, Box::new(overlay))) @@ -236,37 +259,50 @@ where } #[self_referencing] -struct Overlay<'a, 'b, Message, Renderer, Dependency, View> { - cached: &'a mut Lazy<'b, Message, Renderer, Dependency, View>, +struct Inner<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a, +{ + cell: Rc>>>, + element: Element<'static, Message, Renderer>, tree: &'a mut Tree, - types: PhantomData<(Message, Dependency, View)>, - #[borrows(mut cached, mut tree)] + #[borrows(mut element, mut tree)] #[covariant] overlay: Option>, } -impl<'a, 'b, Message, Renderer, Dependency, View> - Overlay<'a, 'b, Message, Renderer, Dependency, View> -{ +struct Overlay<'a, Message, Renderer>(Option>); + +impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> { + fn drop(&mut self) { + let heads = self.0.take().unwrap().into_heads(); + (*heads.cell.borrow_mut()) = Some(heads.element); + } +} + +impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> { fn with_overlay_maybe( &self, f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, ) -> Option { - self.borrow_overlay().as_ref().map(f) + self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f) } fn with_overlay_mut_maybe( &mut self, f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, ) -> Option { - self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) + self.0 + .as_mut() + .unwrap() + .with_overlay_mut(|overlay| overlay.as_mut().map(f)) } } -impl<'a, 'b, Message, Renderer, Dependency, View> - overlay::Overlay - for Overlay<'a, 'b, Message, Renderer, Dependency, View> +impl<'a, Message, Renderer> overlay::Overlay + for Overlay<'a, Message, Renderer> where Renderer: iced_native::Renderer, { diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs index f49fe4b6e6..41a2877361 100644 --- a/lazy/src/lib.rs +++ b/lazy/src/lib.rs @@ -33,7 +33,7 @@ use std::hash::Hash; pub fn lazy<'a, Message, Renderer, Dependency, View>( dependency: Dependency, - view: impl Fn() -> View + 'a, + view: impl Fn(&Dependency) -> View + 'a, ) -> Lazy<'a, Message, Renderer, Dependency, View> where Dependency: Hash + 'a, diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs index 5e1b5dff1c..52badda24a 100644 --- a/lazy/src/responsive.rs +++ b/lazy/src/responsive.rs @@ -3,8 +3,8 @@ use iced_native::layout::{self, Layout}; use iced_native::mouse; use iced_native::overlay; use iced_native::renderer; -use iced_native::widget::horizontal_space; use iced_native::widget::tree::{self, Tree}; +use iced_native::widget::{self, horizontal_space}; use iced_native::{ Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, }; @@ -142,6 +142,29 @@ where layout::Node::new(limits.max()) } + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let state = tree.state.downcast_mut::(); + let mut content = self.content.borrow_mut(); + + content.resolve( + &mut state.tree.borrow_mut(), + renderer, + layout, + &self.view, + |tree, renderer, layout, element| { + element + .as_widget() + .operate(tree, layout, renderer, operation); + }, + ); + } + fn on_event( &mut self, tree: &mut Tree, @@ -257,12 +280,14 @@ where ); let Content { - element, layout, .. + element, + layout: content_layout, + .. } = content.deref_mut(); let content_layout = Layout::with_offset( layout.bounds().position() - Point::ORIGIN, - layout, + content_layout, ); element diff --git a/native/Cargo.toml b/native/Cargo.toml index bbf9295122..79e4dac4b3 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_native" -version = "0.7.0" +version = "0.8.0" authors = ["Héctor Ramón Jiménez "] edition = "2021" description = "A renderer-agnostic library for native GUIs" @@ -16,7 +16,7 @@ unicode-segmentation = "1.6" num-traits = "0.2" [dependencies.iced_core] -version = "0.6" +version = "0.7" path = "../core" [dependencies.iced_futures] @@ -25,5 +25,5 @@ path = "../futures" features = ["thread-pool"] [dependencies.iced_style] -version = "0.5.1" +version = "0.6.0" path = "../style" diff --git a/native/README.md b/native/README.md index c1e160c7cc..9e1f65fbba 100644 --- a/native/README.md +++ b/native/README.md @@ -28,7 +28,7 @@ To achieve this, it introduces a bunch of reusable interfaces: Add `iced_native` as a dependency in your `Cargo.toml`: ```toml -iced_native = "0.4" +iced_native = "0.8" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/native/src/element.rs b/native/src/element.rs index 2f1adeff59..2409b1c976 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -290,6 +290,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { struct MapOperation<'a, B> { @@ -334,8 +335,12 @@ where } } - self.widget - .operate(tree, layout, &mut MapOperation { operation }); + self.widget.operate( + tree, + layout, + renderer, + &mut MapOperation { operation }, + ); } fn on_event( @@ -473,9 +478,12 @@ where &self, state: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - self.element.widget.operate(state, layout, operation) + self.element + .widget + .operate(state, layout, renderer, operation) } fn on_event( diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs index 6d5f65630d..4cbb970d39 100644 --- a/native/src/layout/limits.rs +++ b/native/src/layout/limits.rs @@ -1,3 +1,4 @@ +#![allow(clippy::manual_clamp)] use crate::{Length, Padding, Size}; /// A set of size constraints for layouting. diff --git a/native/src/lib.rs b/native/src/lib.rs index ce7c010d4d..124423a667 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -23,8 +23,8 @@ //! - Build a new renderer, see the [renderer] module. //! - Build a custom widget, start at the [`Widget`] trait. //! -//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.6/core -//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.6/winit +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.7/core +//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.7/winit //! [`druid`]: https://github.com/xi-editor/druid //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [renderer]: crate::renderer diff --git a/native/src/overlay.rs b/native/src/overlay.rs index 0b05b058e1..22f8b6ec80 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -46,6 +46,7 @@ where fn operate( &mut self, _layout: Layout<'_>, + _renderer: &Renderer, _operation: &mut dyn widget::Operation, ) { } diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs index 4f5ef32ac7..498e9ae361 100644 --- a/native/src/overlay/element.rs +++ b/native/src/overlay/element.rs @@ -108,9 +108,10 @@ where pub fn operate( &mut self, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - self.overlay.operate(layout, operation); + self.overlay.operate(layout, renderer, operation); } } @@ -144,6 +145,7 @@ where fn operate( &mut self, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { struct MapOperation<'a, B> { @@ -189,7 +191,7 @@ where } self.content - .operate(layout, &mut MapOperation { operation }); + .operate(layout, renderer, &mut MapOperation { operation }); } fn on_event( diff --git a/native/src/renderer.rs b/native/src/renderer.rs index 5e776be6cd..d5329acd8a 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -36,11 +36,11 @@ pub trait Renderer: Sized { f: impl FnOnce(&mut Self), ); - /// Clears all of the recorded primitives in the [`Renderer`]. - fn clear(&mut self); - /// Fills a [`Quad`] with the provided [`Background`]. fn fill_quad(&mut self, quad: Quad, background: impl Into); + + /// Clears all of the recorded primitives in the [`Renderer`]. + fn clear(&mut self); } /// A polygon with four sides. diff --git a/native/src/shell.rs b/native/src/shell.rs index b96d23e5aa..f1ddb48e61 100644 --- a/native/src/shell.rs +++ b/native/src/shell.rs @@ -1,3 +1,5 @@ +use crate::window; + /// A connection to the state of a shell. /// /// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application, @@ -7,6 +9,7 @@ #[derive(Debug)] pub struct Shell<'a, Message> { messages: &'a mut Vec, + redraw_request: Option, is_layout_invalid: bool, are_widgets_invalid: bool, } @@ -16,31 +19,40 @@ impl<'a, Message> Shell<'a, Message> { pub fn new(messages: &'a mut Vec) -> Self { Self { messages, + redraw_request: None, is_layout_invalid: false, are_widgets_invalid: false, } } - /// Triggers the given function if the layout is invalid, cleaning it in the - /// process. - pub fn revalidate_layout(&mut self, f: impl FnOnce()) { - if self.is_layout_invalid { - self.is_layout_invalid = false; + /// Publish the given `Message` for an application to process it. + pub fn publish(&mut self, message: Message) { + self.messages.push(message); + } - f() + /// Requests a new frame to be drawn at the given [`Instant`]. + pub fn request_redraw(&mut self, request: window::RedrawRequest) { + match self.redraw_request { + None => { + self.redraw_request = Some(request); + } + Some(current) if request < current => { + self.redraw_request = Some(request); + } + _ => {} } } + /// Returns the requested [`Instant`] a redraw should happen, if any. + pub fn redraw_request(&self) -> Option { + self.redraw_request + } + /// Returns whether the current layout is invalid or not. pub fn is_layout_invalid(&self) -> bool { self.is_layout_invalid } - /// Publish the given `Message` for an application to process it. - pub fn publish(&mut self, message: Message) { - self.messages.push(message); - } - /// Invalidates the current application layout. /// /// The shell will relayout the application widgets. @@ -48,6 +60,22 @@ impl<'a, Message> Shell<'a, Message> { self.is_layout_invalid = true; } + /// Triggers the given function if the layout is invalid, cleaning it in the + /// process. + pub fn revalidate_layout(&mut self, f: impl FnOnce()) { + if self.is_layout_invalid { + self.is_layout_invalid = false; + + f() + } + } + + /// Returns whether the widgets of the current application have been + /// invalidated. + pub fn are_widgets_invalid(&self) -> bool { + self.are_widgets_invalid + } + /// Invalidates the current application widgets. /// /// The shell will rebuild and relayout the widget tree. @@ -62,16 +90,14 @@ impl<'a, Message> Shell<'a, Message> { pub fn merge(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) { self.messages.extend(other.messages.drain(..).map(f)); + if let Some(at) = other.redraw_request { + self.request_redraw(at); + } + self.is_layout_invalid = self.is_layout_invalid || other.is_layout_invalid; self.are_widgets_invalid = self.are_widgets_invalid || other.are_widgets_invalid; } - - /// Returns whether the widgets of the current application have been - /// invalidated. - pub fn are_widgets_invalid(&self) -> bool { - self.are_widgets_invalid - } } diff --git a/native/src/subscription.rs b/native/src/subscription.rs index c60b12819a..8c92efad47 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,5 +1,6 @@ //! Listen to external events in your application. use crate::event::{self, Event}; +use crate::window; use crate::Hasher; use iced_futures::futures::{self, Future, Stream}; @@ -33,7 +34,7 @@ pub type Tracker = pub use iced_futures::subscription::Recipe; -/// Returns a [`Subscription`] to all the runtime events. +/// Returns a [`Subscription`] to all the ignored runtime events. /// /// This subscription will notify your application of any [`Event`] that was /// not captured by any widget. @@ -58,8 +59,36 @@ pub fn events_with( where Message: 'static + MaybeSend, { + #[derive(Hash)] + struct EventsWith; + + Subscription::from_recipe(Runner { + id: (EventsWith, f), + spawn: move |events| { + use futures::future; + use futures::stream::StreamExt; + + events.filter_map(move |(event, status)| { + future::ready(match event { + Event::Window(window::Event::RedrawRequested(_)) => None, + _ => f(event, status), + }) + }) + }, + }) +} + +pub(crate) fn raw_events( + f: fn(Event, event::Status) -> Option, +) -> Subscription +where + Message: 'static + MaybeSend, +{ + #[derive(Hash)] + struct RawEvents; + Subscription::from_recipe(Runner { - id: f, + id: (RawEvents, f), spawn: move |events| { use futures::future; use futures::stream::StreamExt; @@ -155,7 +184,7 @@ where /// Check out the [`websocket`] example, which showcases this pattern to maintain a WebSocket /// connection open. /// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.6/examples/websocket +/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.7/examples/websocket pub fn unfold( id: I, initial: T, diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 376ce568f8..29cc3472e4 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -5,6 +5,7 @@ use crate::layout; use crate::mouse; use crate::renderer; use crate::widget; +use crate::window; use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -18,8 +19,8 @@ use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; /// The [`integration_opengl`] & [`integration_wgpu`] examples use a /// [`UserInterface`] to integrate Iced in an existing graphical application. /// -/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.6/examples/integration_opengl -/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.6/examples/integration_wgpu +/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.7/examples/integration_opengl +/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.7/examples/integration_wgpu #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, @@ -188,7 +189,9 @@ where ) -> (State, Vec) { use std::mem::ManuallyDrop; - let mut state = State::Updated; + let mut outdated = false; + let mut redraw_request = None; + let mut manual_overlay = ManuallyDrop::new(self.root.as_widget_mut().overlay( &mut self.state, @@ -217,6 +220,16 @@ where event_statuses.push(event_status); + match (redraw_request, shell.redraw_request()) { + (None, Some(at)) => { + redraw_request = Some(at); + } + (Some(current), Some(new)) if new < current => { + redraw_request = Some(new); + } + _ => {} + } + if shell.is_layout_invalid() { let _ = ManuallyDrop::into_inner(manual_overlay); @@ -244,7 +257,7 @@ where } if shell.are_widgets_invalid() { - state = State::Outdated; + outdated = true; } } @@ -289,6 +302,16 @@ where self.overlay = None; } + match (redraw_request, shell.redraw_request()) { + (None, Some(at)) => { + redraw_request = Some(at); + } + (Some(current), Some(new)) if new < current => { + redraw_request = Some(new); + } + _ => {} + } + shell.revalidate_layout(|| { self.base = renderer.layout( &self.root, @@ -299,14 +322,21 @@ where }); if shell.are_widgets_invalid() { - state = State::Outdated; + outdated = true; } event_status.merge(overlay_status) }) .collect(); - (state, event_statuses) + ( + if outdated { + State::Outdated + } else { + State::Updated { redraw_request } + }, + event_statuses, + ) } /// Draws the [`UserInterface`] with the provided [`Renderer`]. @@ -493,6 +523,7 @@ where self.root.as_widget().operate( &mut self.state, Layout::new(&self.base), + renderer, operation, ); @@ -507,6 +538,7 @@ where overlay.operate( Layout::new(self.overlay.as_ref().unwrap()), + renderer, operation, ); } @@ -557,5 +589,8 @@ pub enum State { /// The [`UserInterface`] is up-to-date and can be reused without /// rebuilding. - Updated, + Updated { + /// The [`Instant`] when a redraw should be performed. + redraw_request: Option, + }, } diff --git a/native/src/widget.rs b/native/src/widget.rs index a4b46ed431..fb759ec8a8 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -33,6 +33,7 @@ pub mod text_input; pub mod toggler; pub mod tooltip; pub mod tree; +pub mod vertical_slider; mod action; mod id; @@ -79,6 +80,8 @@ pub use toggler::Toggler; pub use tooltip::Tooltip; #[doc(no_inline)] pub use tree::Tree; +#[doc(no_inline)] +pub use vertical_slider::VerticalSlider; pub use action::Action; pub use id::Id; @@ -107,12 +110,12 @@ use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell}; /// - [`geometry`], a custom widget showcasing how to draw geometry with the /// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.6/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.6/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.6/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.6/examples/geometry +/// [examples]: https://github.com/iced-rs/iced/tree/0.7/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.7/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.7/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.7/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.6/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.7/wgpu pub trait Widget where Renderer: crate::Renderer, @@ -172,6 +175,7 @@ where &self, _state: &mut Tree, _layout: Layout<'_>, + _renderer: &Renderer, _operation: &mut dyn Operation, ) { } diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index bbd9451ce3..b4276317f6 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -169,12 +169,14 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn Operation, ) { operation.container(None, &mut |operation| { self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), + renderer, operation, ); }); diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index bec5c44807..b46433c256 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -27,7 +27,7 @@ pub use iced_style::checkbox::{Appearance, StyleSheet}; /// /// let is_checked = true; /// -/// Checkbox::new(is_checked, "Toggle me!", Message::CheckboxToggled); +/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled); /// ``` /// /// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) @@ -67,7 +67,7 @@ where /// * a function that will be called when the [`Checkbox`] is toggled. It /// will receive the new state of the [`Checkbox`] and must produce a /// `Message`. - pub fn new(is_checked: bool, label: impl Into, f: F) -> Self + pub fn new(label: impl Into, is_checked: bool, f: F) -> Self where F: 'a + Fn(bool) -> Message, { diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 8030778bff..5ad4d85892 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -10,8 +10,6 @@ use crate::{ Shell, Widget, }; -use std::u32; - /// A container that distributes its contents vertically. #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Renderer> { @@ -147,6 +145,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn Operation, ) { operation.container(None, &mut |operation| { @@ -155,7 +154,9 @@ where .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), layout)| { - child.as_widget().operate(state, layout, operation); + child + .as_widget() + .operate(state, layout, renderer, operation); }) }); } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 16d0cb61bd..cdf1c85925 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -169,12 +169,14 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn Operation, ) { operation.container(None, &mut |operation| { self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), + renderer, operation, ); }); diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs index 0bde288fca..dfd949f6a9 100644 --- a/native/src/widget/helpers.rs +++ b/native/src/widget/helpers.rs @@ -129,7 +129,7 @@ where Renderer: crate::text::Renderer, Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet, { - widget::Checkbox::new(is_checked, label, f) + widget::Checkbox::new(label, is_checked, f) } /// Creates a new [`Radio`]. @@ -162,7 +162,7 @@ where Renderer: crate::text::Renderer, Renderer::Theme: widget::toggler::StyleSheet, { - widget::Toggler::new(is_checked, label, f) + widget::Toggler::new(label, is_checked, f) } /// Creates a new [`TextInput`]. @@ -198,6 +198,23 @@ where widget::Slider::new(range, value, on_change) } +/// Creates a new [`VerticalSlider`]. +/// +/// [`VerticalSlider`]: widget::VerticalSlider +pub fn vertical_slider<'a, T, Message, Renderer>( + range: std::ops::RangeInclusive, + value: T, + on_change: impl Fn(T) -> Message + 'a, +) -> widget::VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + From + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: widget::slider::StyleSheet, +{ + widget::VerticalSlider::new(range, value, on_change) +} + /// Creates a new [`PickList`]. /// /// [`PickList`]: widget::PickList diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 9c83287ea1..fdbd321688 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -170,8 +170,7 @@ where } else { state.scale / (1.0 + self.scale_step) }) - .max(self.min_scale) - .min(self.max_scale); + .clamp(self.min_scale, self.max_scale); let image_size = image_size( renderer, @@ -251,16 +250,14 @@ where let x = if bounds.width < image_size.width { (state.starting_offset.x - delta.x) - .min(hidden_width) - .max(-hidden_width) + .clamp(-hidden_width, hidden_width) } else { 0.0 }; let y = if bounds.height < image_size.height { (state.starting_offset.y - delta.y) - .min(hidden_height) - .max(-hidden_height) + .clamp(-hidden_height, hidden_height) } else { 0.0 }; @@ -374,8 +371,8 @@ impl State { (image_size.height - bounds.height / 2.0).max(0.0).round(); Vector::new( - self.current_offset.x.min(hidden_width).max(-hidden_width), - self.current_offset.y.min(hidden_height).max(-hidden_height), + self.current_offset.x.clamp(-hidden_width, hidden_width), + self.current_offset.y.clamp(-hidden_height, hidden_height), ) } diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs index 0067006bbd..312e48943d 100644 --- a/native/src/widget/operation/focusable.rs +++ b/native/src/widget/operation/focusable.rs @@ -18,10 +18,10 @@ pub trait Focusable { #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Count { /// The index of the current focused widget, if any. - focused: Option, + pub focused: Option, /// The total amount of focusable widgets. - total: usize, + pub total: usize, } /// Produces an [`Operation`] that focuses the widget with the given [`Id`]. diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs index 2210137d53..3b20631f30 100644 --- a/native/src/widget/operation/scrollable.rs +++ b/native/src/widget/operation/scrollable.rs @@ -3,25 +3,19 @@ use crate::widget::{Id, Operation}; /// The internal state of a widget that can be scrolled. pub trait Scrollable { - /// Snaps the scroll of the widget to the given `percentage`. - fn snap_to(&mut self, percentage: f32); + /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. + fn snap_to(&mut self, offset: RelativeOffset); } /// Produces an [`Operation`] that snaps the widget with the given [`Id`] to /// the provided `percentage`. -pub fn snap_to(target: Id, percentage: f32) -> impl Operation { +pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { struct SnapTo { target: Id, - percentage: f32, + offset: RelativeOffset, } impl Operation for SnapTo { - fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { - if Some(&self.target) == id { - state.snap_to(self.percentage); - } - } - fn container( &mut self, _id: Option<&Id>, @@ -29,7 +23,32 @@ pub fn snap_to(target: Id, percentage: f32) -> impl Operation { ) { operate_on_children(self) } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.offset); + } + } } - SnapTo { target, percentage } + SnapTo { target, offset } +} + +/// The amount of offset in each direction of a [`Scrollable`]. +/// +/// A value of `0.0` means start, while `1.0` means end. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct RelativeOffset { + /// The amount of horizontal offset + pub x: f32, + /// The amount of vertical offset + pub y: f32, +} + +impl RelativeOffset { + /// A relative offset that points to the top-left of a [`Scrollable`]. + pub const START: Self = Self { x: 0.0, y: 0.0 }; + + /// A relative offset that points to the bottom-right of a [`Scrollable`]. + pub const END: Self = Self { x: 1.0, y: 1.0 }; } diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 5de95c651f..8dbd182569 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -6,7 +6,7 @@ //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.6/examples/pane_grid +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.7/examples/pane_grid mod axis; mod configuration; mod content; @@ -294,6 +294,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { operation.container(None, &mut |operation| { @@ -302,7 +303,7 @@ where .zip(&mut tree.children) .zip(layout.children()) .for_each(|(((_pane, content), state), layout)| { - content.operate(state, layout, operation); + content.operate(state, layout, renderer, operation); }) }); } @@ -630,13 +631,13 @@ pub fn update<'a, Message, T: Draggable>( let position = cursor_position.y - bounds.y - rectangle.y; - (position / rectangle.height).max(0.1).min(0.9) + (position / rectangle.height).clamp(0.1, 0.9) } Axis::Vertical => { let position = cursor_position.x - bounds.x - rectangle.x; - (position / rectangle.width).max(0.1).min(0.9) + (position / rectangle.width).clamp(0.1, 0.9) } }; diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 5f269d1f60..c9b0df078f 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -187,6 +187,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { let body_layout = if let Some(title_bar) = &self.title_bar { @@ -195,6 +196,7 @@ where title_bar.operate( &mut tree.children[1], children.next().unwrap(), + renderer, operation, ); @@ -206,6 +208,7 @@ where self.body.as_widget().operate( &mut tree.children[0], body_layout, + renderer, operation, ); } diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs index 28e4670f4f..ea0969aa97 100644 --- a/native/src/widget/pane_grid/title_bar.rs +++ b/native/src/widget/pane_grid/title_bar.rs @@ -261,6 +261,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn widget::Operation, ) { let mut children = layout.children(); @@ -282,6 +283,7 @@ where controls.as_widget().operate( &mut tree.children[1], controls_layout, + renderer, operation, ) }; @@ -290,6 +292,7 @@ where self.content.as_widget().operate( &mut tree.children[0], title_layout, + renderer, operation, ) } diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 52cb1ad189..c285331455 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -20,6 +20,60 @@ use std::borrow::Cow; pub use iced_style::pick_list::{Appearance, StyleSheet}; +/// The handle to the right side of the [`PickList`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Handle +where + Renderer: text::Renderer, +{ + /// Displays an arrow icon (▼). + /// + /// This is the default. + Arrow { + /// Font size of the content. + size: Option, + }, + /// A custom handle. + Custom { + /// Font that will be used to display the `text`, + font: Renderer::Font, + /// Text that will be shown. + text: String, + /// Font size of the content. + size: Option, + }, + /// No handle will be shown. + None, +} + +impl Default for Handle +where + Renderer: text::Renderer, +{ + fn default() -> Self { + Self::Arrow { size: None } + } +} + +impl Handle +where + Renderer: text::Renderer, +{ + fn content(&self) -> Option<(Renderer::Font, String, Option)> { + match self { + Self::Arrow { size } => Some(( + Renderer::ICON_FONT, + Renderer::ARROW_DOWN_ICON.to_string(), + *size, + )), + Self::Custom { font, text, size } => { + Some((font.clone(), text.clone(), *size)) + } + Self::None => None, + } + } +} + /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] pub struct PickList<'a, T, Message, Renderer> @@ -36,6 +90,7 @@ where padding: Padding, text_size: Option, font: Renderer::Font, + handle: Handle, style: ::Style, } @@ -67,9 +122,10 @@ where placeholder: None, selected, width: Length::Shrink, - text_size: None, padding: Self::DEFAULT_PADDING, + text_size: None, font: Default::default(), + handle: Default::default(), style: Default::default(), } } @@ -104,6 +160,12 @@ where self } + /// Sets the [`Handle`] of the [`PickList`]. + pub fn handle(mut self, handle: Handle) -> Self { + self.handle = handle; + self + } + /// Sets the style of the [`PickList`]. pub fn style( mut self, @@ -214,6 +276,7 @@ where &self.font, self.placeholder.as_deref(), self.selected.as_ref(), + &self.handle, &self.style, ) } @@ -515,6 +578,7 @@ pub fn draw( font: &Renderer::Font, placeholder: Option<&str>, selected: Option<&T>, + handle: &Handle, style: &::Style, ) where Renderer: text::Renderer, @@ -541,19 +605,24 @@ pub fn draw( style.background, ); - renderer.fill_text(Text { - content: &Renderer::ARROW_DOWN_ICON.to_string(), - font: Renderer::ICON_FONT, - size: bounds.height * style.icon_size, - bounds: Rectangle { - x: bounds.x + bounds.width - f32::from(padding.horizontal()), - y: bounds.center_y(), - ..bounds - }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); + if let Some((font, text, size)) = handle.content() { + let size = f32::from(size.unwrap_or_else(|| renderer.default_size())); + + renderer.fill_text(Text { + content: &text, + size, + font, + color: style.handle_color, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding.horizontal()), + y: bounds.center_y() - size / 2.0, + height: size, + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Top, + }); + } let label = selected.map(ToString::to_string); diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index f059026f44..7d5d5be555 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -47,7 +47,7 @@ where /// * the current value of the [`ProgressBar`] pub fn new(range: RangeInclusive, value: f32) -> Self { ProgressBar { - value: value.max(*range.start()).min(*range.end()), + value: value.clamp(*range.start(), *range.end()), range, width: Length::Fill, height: None, diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index c689ac13f2..108e98e4a5 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -134,6 +134,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn Operation, ) { operation.container(None, &mut |operation| { @@ -142,7 +143,9 @@ where .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), layout)| { - child.as_widget().operate(state, layout, operation); + child + .as_widget() + .operate(state, layout, renderer, operation); }) }); } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index a5e0e0e30c..822860364d 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,5 +1,6 @@ //! Navigate an endless amount of content with a scrollbar. use crate::event::{self, Event}; +use crate::keyboard; use crate::layout; use crate::mouse; use crate::overlay; @@ -13,9 +14,8 @@ use crate::{ Rectangle, Shell, Size, Vector, Widget, }; -use std::{f32, u32}; - pub use iced_style::scrollable::StyleSheet; +pub use operation::scrollable::RelativeOffset; pub mod style { //! The styles of a [`Scrollable`]. @@ -34,11 +34,10 @@ where { id: Option, height: Length, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + vertical: Properties, + horizontal: Option, content: Element<'a, Message, Renderer>, - on_scroll: Option Message + 'a>>, + on_scroll: Option Message + 'a>>, style: ::Style, } @@ -52,9 +51,8 @@ where Scrollable { id: None, height: Length::Shrink, - scrollbar_width: 10, - scrollbar_margin: 0, - scroller_width: 10, + vertical: Properties::default(), + horizontal: None, content: content.into(), on_scroll: None, style: Default::default(), @@ -73,32 +71,26 @@ where self } - /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum value of 1. - pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { - self.scrollbar_width = scrollbar_width.max(1); + /// Configures the vertical scrollbar of the [`Scrollable`] . + pub fn vertical_scroll(mut self, properties: Properties) -> Self { + self.vertical = properties; self } - /// Sets the scrollbar margin of the [`Scrollable`] . - pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { - self.scrollbar_margin = scrollbar_margin; - self - } - - /// Sets the scroller width of the [`Scrollable`] . - /// - /// It silently enforces a minimum value of 1. - pub fn scroller_width(mut self, scroller_width: u16) -> Self { - self.scroller_width = scroller_width.max(1); + /// Configures the horizontal scrollbar of the [`Scrollable`] . + pub fn horizontal_scroll(mut self, properties: Properties) -> Self { + self.horizontal = Some(properties); self } /// Sets a function to call when the [`Scrollable`] is scrolled. /// - /// The function takes the new relative offset of the [`Scrollable`] - /// (e.g. `0` means top, while `1` means bottom). - pub fn on_scroll(mut self, f: impl Fn(f32) -> Message + 'a) -> Self { + /// The function takes the new relative x & y offset of the [`Scrollable`] + /// (e.g. `0` means beginning, while `1` means end). + pub fn on_scroll( + mut self, + f: impl Fn(RelativeOffset) -> Message + 'a, + ) -> Self { self.on_scroll = Some(Box::new(f)); self } @@ -113,6 +105,51 @@ where } } +/// Properties of a scrollbar within a [`Scrollable`]. +#[derive(Debug)] +pub struct Properties { + width: u16, + margin: u16, + scroller_width: u16, +} + +impl Default for Properties { + fn default() -> Self { + Self { + width: 10, + margin: 0, + scroller_width: 10, + } + } +} + +impl Properties { + /// Creates new [`Properties`] for use in a [`Scrollable`]. + pub fn new() -> Self { + Self::default() + } + + /// Sets the scrollbar width of the [`Scrollable`] . + /// Silently enforces a minimum width of 1. + pub fn width(mut self, width: u16) -> Self { + self.width = width.max(1); + self + } + + /// Sets the scrollbar margin of the [`Scrollable`] . + pub fn margin(mut self, margin: u16) -> Self { + self.margin = margin; + self + } + + /// Sets the scroller width of the [`Scrollable`] . + /// Silently enforces a minimum width of 1. + pub fn scroller_width(mut self, scroller_width: u16) -> Self { + self.scroller_width = scroller_width.max(1); + self + } +} + impl<'a, Message, Renderer> Widget for Scrollable<'a, Message, Renderer> where @@ -153,7 +190,7 @@ where limits, Widget::::width(self), self.height, - u32::MAX, + self.horizontal.is_some(), |renderer, limits| { self.content.as_widget().layout(renderer, limits) }, @@ -164,6 +201,7 @@ where &self, tree: &mut Tree, layout: Layout<'_>, + renderer: &Renderer, operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::(); @@ -174,6 +212,7 @@ where self.content.as_widget().operate( &mut tree.children[0], layout.children().next().unwrap(), + renderer, operation, ); }); @@ -196,9 +235,8 @@ where cursor_position, clipboard, shell, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, + &self.vertical, + self.horizontal.as_ref(), &self.on_scroll, |event, layout, cursor_position, clipboard, shell| { self.content.as_widget_mut().on_event( @@ -230,9 +268,8 @@ where theme, layout, cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, + &self.vertical, + self.horizontal.as_ref(), &self.style, |renderer, layout, cursor_position, viewport| { self.content.as_widget().draw( @@ -260,9 +297,8 @@ where tree.state.downcast_ref::(), layout, cursor_position, - self.scrollbar_width, - self.scrollbar_margin, - self.scroller_width, + &self.vertical, + self.horizontal.as_ref(), |layout, cursor_position, viewport| { self.content.as_widget().mouse_interaction( &tree.children[0], @@ -297,7 +333,7 @@ where .downcast_ref::() .offset(bounds, content_bounds); - overlay.translate(Vector::new(0.0, -(offset as f32))) + overlay.translate(Vector::new(-offset.x, -offset.y)) }) } } @@ -341,9 +377,12 @@ impl From for widget::Id { } /// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage`. -pub fn snap_to(id: Id, percentage: f32) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, percentage)) +/// to the provided `percentage` along the x & y axis. +pub fn snap_to( + id: Id, + offset: RelativeOffset, +) -> Command { + Command::widget(operation::scrollable::snap_to(id.0, offset)) } /// Computes the layout of a [`Scrollable`]. @@ -352,14 +391,29 @@ pub fn layout( limits: &layout::Limits, width: Length, height: Length, - max_height: u32, + horizontal_enabled: bool, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits.max_height(max_height).width(width).height(height); + let limits = limits + .max_height(u32::MAX) + .max_width(if horizontal_enabled { + u32::MAX + } else { + limits.max().width as u32 + }) + .width(width) + .height(height); let child_limits = layout::Limits::new( Size::new(limits.min().width, 0.0), - Size::new(limits.max().width, f32::INFINITY), + Size::new( + if horizontal_enabled { + f32::INFINITY + } else { + limits.max().width + }, + f32::MAX, + ), ); let content = layout_content(renderer, &child_limits); @@ -377,10 +431,9 @@ pub fn update( cursor_position: Point, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - on_scroll: &Option Message + '_>>, + vertical: &Properties, + horizontal: Option<&Properties>, + on_scroll: &Option Message + '_>>, update_content: impl FnOnce( Event, Layout<'_>, @@ -390,36 +443,28 @@ pub fn update( ) -> event::Status, ) -> event::Status { let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); + let mouse_over_scrollable = bounds.contains(cursor_position); let content = layout.children().next().unwrap(); let content_bounds = content.bounds(); - let scrollbar = scrollbar( - state, - scrollbar_width, - scrollbar_margin, - scroller_width, - bounds, - content_bounds, - ); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); let event_status = { - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y + state.offset(bounds, content_bounds) as f32, - ) + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + cursor_position + state.offset(bounds, content_bounds) } else { // TODO: Make `cursor_position` an `Option` so we can encode // cursor availability. // This will probably happen naturally once we add multi-window // support. - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; update_content( @@ -435,18 +480,31 @@ pub fn update( return event::Status::Captured; } - if is_mouse_over { + if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event + { + state.keyboard_modifiers = modifiers; + + return event::Status::Ignored; + } + + if mouse_over_scrollable { match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) - state.scroll(y * 60.0, bounds, content_bounds); + let delta = match delta { + mouse::ScrollDelta::Lines { x, y } => { + // TODO: Configurable speed/friction (?) + let movement = if state.keyboard_modifiers.shift() { + Vector::new(y, x) + } else { + Vector::new(x, y) + }; + + movement * 60.0 } - mouse::ScrollDelta::Pixels { y, .. } => { - state.scroll(y, bounds, content_bounds); - } - } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; + + state.scroll(delta, bounds, content_bounds); notify_on_scroll( state, @@ -458,21 +516,27 @@ pub fn update( return event::Status::Captured; } - Event::Touch(event) => { + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => + { match event { touch::Event::FingerPressed { .. } => { - state.scroll_box_touched_at = Some(cursor_position); + state.scroll_area_touched_at = Some(cursor_position); } touch::Event::FingerMoved { .. } => { if let Some(scroll_box_touched_at) = - state.scroll_box_touched_at + state.scroll_area_touched_at { - let delta = - cursor_position.y - scroll_box_touched_at.y; + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); state.scroll(delta, bounds, content_bounds); - state.scroll_box_touched_at = Some(cursor_position); + state.scroll_area_touched_at = + Some(cursor_position); notify_on_scroll( state, @@ -485,7 +549,7 @@ pub fn update( } touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. } => { - state.scroll_box_touched_at = None; + state.scroll_area_touched_at = None; } } @@ -495,22 +559,20 @@ pub fn update( } } - if state.is_scroller_grabbed() { + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { match event { Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) | Event::Touch(touch::Event::FingerLost { .. }) => { - state.scroller_grabbed_at = None; + state.y_scroller_grabbed_at = None; return event::Status::Captured; } Event::Mouse(mouse::Event::CursorMoved { .. }) | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, state.scroller_grabbed_at) - { - state.scroll_to( - scrollbar.scroll_percentage( + if let Some(scrollbar) = scrollbars.y { + state.scroll_y_to( + scrollbar.scroll_percentage_y( scroller_grabbed_at, cursor_position, ), @@ -531,35 +593,100 @@ pub fn update( } _ => {} } - } else if is_mouse_over_scrollbar { + } else if mouse_over_y_scrollbar { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } + if let (Some(scroller_grabbed_at), Some(scrollbar)) = + (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) + { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.x_scroller_grabbed_at = None; + + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + } + + return event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let (Some(scroller_grabbed_at), Some(scrollbar)) = + (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) + { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; } } _ => {} @@ -574,9 +701,8 @@ pub fn mouse_interaction( state: &State, layout: Layout<'_>, cursor_position: Point, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + vertical: &Properties, + horizontal: Option<&Properties>, content_interaction: impl FnOnce( Layout<'_>, Point, @@ -584,39 +710,38 @@ pub fn mouse_interaction( ) -> mouse::Interaction, ) -> mouse::Interaction { let bounds = layout.bounds(); + let mouse_over_scrollable = bounds.contains(cursor_position); + let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let scrollbar = scrollbar( - state, - scrollbar_width, - scrollbar_margin, - scroller_width, - bounds, - content_bounds, - ); - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); + + if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) + || state.scrollers_grabbed() + { mouse::Interaction::Idle } else { let offset = state.offset(bounds, content_bounds); - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) + let cursor_position = if mouse_over_scrollable + && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) + { + cursor_position + offset } else { - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; content_interaction( content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + y: bounds.y + offset.y, + x: bounds.x + offset.x, ..bounds }, ) @@ -630,9 +755,8 @@ pub fn draw( theme: &Renderer::Theme, layout: Layout<'_>, cursor_position: Point, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, + vertical: &Properties, + horizontal: Option<&Properties>, style: &::Style, draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), ) where @@ -642,39 +766,37 @@ pub fn draw( let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); let content_bounds = content_layout.bounds(); - let offset = state.offset(bounds, content_bounds); - let scrollbar = scrollbar( - state, - scrollbar_width, - scrollbar_margin, - scroller_width, - bounds, - content_bounds, - ); - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); + let scrollbars = + Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) + let mouse_over_scrollable = bounds.contains(cursor_position); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor_position); + + let offset = state.offset(bounds, content_bounds); + + let cursor_position = if mouse_over_scrollable + && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) + { + cursor_position + offset } else { - Point::new(cursor_position.x, -1.0) + Point::new(-1.0, -1.0) }; - if let Some(scrollbar) = scrollbar { + // Draw inner content + if scrollbars.active() { renderer.with_layer(bounds, |renderer| { renderer.with_translation( - Vector::new(0.0, -(offset as f32)), + Vector::new(-offset.x, -offset.y), |renderer| { draw_content( renderer, content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + y: bounds.y + offset.y, + x: bounds.x + offset.x, ..bounds }, ); @@ -682,25 +804,15 @@ pub fn draw( ); }); - let style = if state.is_scroller_grabbed() { - theme.dragging(style) - } else if is_mouse_over_scrollbar { - theme.hovered(style) - } else { - theme.active(style) - }; - - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0.0; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - if is_scrollbar_visible { + let draw_scrollbar = + |renderer: &mut Renderer, + style: style::Scrollbar, + scrollbar: &Scrollbar| { + //track + if style.background.is_some() + || (style.border_color != Color::TRANSPARENT + && style.border_width > 0.0) + { renderer.fill_quad( renderer::Quad { bounds: scrollbar.bounds, @@ -714,8 +826,10 @@ pub fn draw( ); } - if (is_mouse_over || state.is_scroller_grabbed()) - && is_scrollbar_visible + //thumb + if style.scroller.color != Color::TRANSPARENT + || (style.scroller.border_color != Color::TRANSPARENT + && style.scroller.border_width > 0.0) { renderer.fill_quad( renderer::Quad { @@ -727,6 +841,40 @@ pub fn draw( style.scroller.color, ); } + }; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + //draw y scrollbar + if let Some(scrollbar) = scrollbars.y { + let style = if state.y_scroller_grabbed_at.is_some() { + theme.dragging(style) + } else if mouse_over_y_scrollbar { + theme.hovered(style) + } else { + theme.active(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } + + //draw x scrollbar + if let Some(scrollbar) = scrollbars.x { + let style = if state.x_scroller_grabbed_at.is_some() { + theme.dragging_horizontal(style) + } else if mouse_over_x_scrollbar { + theme.hovered_horizontal(style) + } else { + theme.active_horizontal(style) + }; + + draw_scrollbar(renderer, style, &scrollbar); + } }, ); } else { @@ -735,110 +883,70 @@ pub fn draw( content_layout, cursor_position, &Rectangle { - y: bounds.y + offset as f32, + x: bounds.x + offset.x, + y: bounds.y + offset.y, ..bounds }, ); } } -fn scrollbar( - state: &State, - scrollbar_width: u16, - scrollbar_margin: u16, - scroller_width: u16, - bounds: Rectangle, - content_bounds: Rectangle, -) -> Option { - let offset = state.offset(bounds, content_bounds); - - if content_bounds.height > bounds.height { - let outer_width = - scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; - - let outer_bounds = Rectangle { - x: bounds.x + bounds.width - outer_width as f32, - y: bounds.y, - width: outer_width as f32, - height: bounds.height, - }; - - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + scrollbar_width / 2), - y: bounds.y, - width: scrollbar_width as f32, - height: bounds.height, - }; - - let ratio = bounds.height / content_bounds.height; - let scroller_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + scroller_width / 2), - y: scrollbar_bounds.y + y_offset, - width: scroller_width as f32, - height: scroller_height, - }; - - Some(Scrollbar { - outer_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - } -} - fn notify_on_scroll( state: &State, - on_scroll: &Option Message + '_>>, + on_scroll: &Option Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, shell: &mut Shell<'_, Message>, ) { - if content_bounds.height <= bounds.height { - return; - } - if let Some(on_scroll) = on_scroll { - shell.publish(on_scroll( - state.offset.absolute(bounds, content_bounds) - / (content_bounds.height - bounds.height), - )); + if content_bounds.width <= bounds.width + && content_bounds.height <= bounds.height + { + return; + } + + let x = state.offset_x.absolute(bounds.width, content_bounds.width) + / (content_bounds.width - bounds.width); + + let y = state + .offset_y + .absolute(bounds.height, content_bounds.height) + / (content_bounds.height - bounds.height); + + shell.publish(on_scroll(RelativeOffset { x, y })) } } /// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] pub struct State { - scroller_grabbed_at: Option, - scroll_box_touched_at: Option, - offset: Offset, + scroll_area_touched_at: Option, + offset_y: Offset, + y_scroller_grabbed_at: Option, + offset_x: Offset, + x_scroller_grabbed_at: Option, + keyboard_modifiers: keyboard::Modifiers, } impl Default for State { fn default() -> Self { Self { - scroller_grabbed_at: None, - scroll_box_touched_at: None, - offset: Offset::Absolute(0.0), + scroll_area_touched_at: None, + offset_y: Offset::Absolute(0.0), + y_scroller_grabbed_at: None, + offset_x: Offset::Absolute(0.0), + x_scroller_grabbed_at: None, + keyboard_modifiers: keyboard::Modifiers::default(), } } } impl operation::Scrollable for State { - fn snap_to(&mut self, percentage: f32) { - State::snap_to(self, percentage); + fn snap_to(&mut self, offset: RelativeOffset) { + State::snap_to(self, offset); } } -/// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] enum Offset { Absolute(f32), @@ -846,23 +954,20 @@ enum Offset { } impl Offset { - fn absolute(self, bounds: Rectangle, content_bounds: Rectangle) -> f32 { + fn absolute(self, window: f32, content: f32) -> f32 { match self { - Self::Absolute(absolute) => { - let hidden_content = - (content_bounds.height - bounds.height).max(0.0); - - absolute.min(hidden_content) + Offset::Absolute(absolute) => { + absolute.min((content - window).max(0.0)) } - Self::Relative(percentage) => { - ((content_bounds.height - bounds.height) * percentage).max(0.0) + Offset::Relative(percentage) => { + ((content - window) * percentage).max(0.0) } } } } impl State { - /// Creates a new [`State`] with the scrollbar located at the top. + /// Creates a new [`State`] with the scrollbar(s) at the beginning. pub fn new() -> Self { State::default() } @@ -871,108 +976,341 @@ impl State { /// the [`Scrollable`] and its contents. pub fn scroll( &mut self, - delta_y: f32, + delta: Vector, bounds: Rectangle, content_bounds: Rectangle, ) { - if bounds.height >= content_bounds.height { - return; + if bounds.height < content_bounds.height { + self.offset_y = Offset::Absolute( + (self.offset_y.absolute(bounds.height, content_bounds.height) + - delta.y) + .clamp(0.0, content_bounds.height - bounds.height), + ) } - self.offset = Offset::Absolute( - (self.offset.absolute(bounds, content_bounds) - delta_y) - .max(0.0) - .min((content_bounds.height - bounds.height) as f32), - ); + if bounds.width < content_bounds.width { + self.offset_x = Offset::Absolute( + (self.offset_x.absolute(bounds.width, content_bounds.width) + - delta.x) + .clamp(0.0, content_bounds.width - bounds.width), + ); + } } - /// Scrolls the [`Scrollable`] to a relative amount. + /// Scrolls the [`Scrollable`] to a relative amount along the y axis. /// - /// `0` represents scrollbar at the top, while `1` represents scrollbar at - /// the bottom. - pub fn scroll_to( + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_y_to( &mut self, percentage: f32, bounds: Rectangle, content_bounds: Rectangle, ) { - self.snap_to(percentage); + self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)); self.unsnap(bounds, content_bounds); } - /// Snaps the scroll position to a relative amount. + /// Scrolls the [`Scrollable`] to a relative amount along the x axis. /// - /// `0` represents scrollbar at the top, while `1` represents scrollbar at - /// the bottom. - pub fn snap_to(&mut self, percentage: f32) { - self.offset = Offset::Relative(percentage.max(0.0).min(1.0)); + /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at + /// the end. + pub fn scroll_x_to( + &mut self, + percentage: f32, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)); + self.unsnap(bounds, content_bounds); + } + + /// Snaps the scroll position to a [`RelativeOffset`]. + pub fn snap_to(&mut self, offset: RelativeOffset) { + self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0)); + self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0)); } /// Unsnaps the current scroll position, if snapped, given the bounds of the /// [`Scrollable`] and its contents. pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { - self.offset = - Offset::Absolute(self.offset.absolute(bounds, content_bounds)); + self.offset_x = Offset::Absolute( + self.offset_x.absolute(bounds.width, content_bounds.width), + ); + self.offset_y = Offset::Absolute( + self.offset_y.absolute(bounds.height, content_bounds.height), + ); } - /// Returns the current scrolling offset of the [`State`], given the bounds - /// of the [`Scrollable`] and its contents. - pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 { - self.offset.absolute(bounds, content_bounds) as u32 + /// Returns the scrolling offset of the [`State`], given the bounds of the + /// [`Scrollable`] and its contents. + pub fn offset( + &self, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Vector { + Vector::new( + self.offset_x.absolute(bounds.width, content_bounds.width), + self.offset_y.absolute(bounds.height, content_bounds.height), + ) } - /// Returns whether the scroller is currently grabbed or not. - pub fn is_scroller_grabbed(&self) -> bool { - self.scroller_grabbed_at.is_some() + /// Returns whether any scroller is currently grabbed or not. + pub fn scrollers_grabbed(&self) -> bool { + self.x_scroller_grabbed_at.is_some() + || self.y_scroller_grabbed_at.is_some() } +} + +#[derive(Debug)] +/// State of both [`Scrollbar`]s. +struct Scrollbars { + y: Option, + x: Option, +} - /// Returns whether the scroll box is currently touched or not. - pub fn is_scroll_box_touched(&self) -> bool { - self.scroll_box_touched_at.is_some() +impl Scrollbars { + /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. + fn new( + state: &State, + vertical: &Properties, + horizontal: Option<&Properties>, + bounds: Rectangle, + content_bounds: Rectangle, + ) -> Self { + let offset = state.offset(bounds, content_bounds); + + let show_scrollbar_x = horizontal.and_then(|h| { + if content_bounds.width > bounds.width { + Some(h) + } else { + None + } + }); + + let y_scrollbar = if content_bounds.height > bounds.height { + let Properties { + width, + margin, + scroller_width, + } = *vertical; + + // Adjust the height of the vertical scrollbar if the horizontal scrollbar + // is present + let x_scrollbar_height = show_scrollbar_x.map_or(0.0, |h| { + (h.width.max(h.scroller_width) + h.margin) as f32 + }); + + let total_scrollbar_width = width.max(scroller_width) + 2 * margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width - total_scrollbar_width as f32, + y: bounds.y, + width: total_scrollbar_width as f32, + height: (bounds.height - x_scrollbar_height).max(0.0), + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(total_scrollbar_width / 2 + width / 2), + y: bounds.y, + width: width as f32, + height: (bounds.height - x_scrollbar_height).max(0.0), + }; + + let ratio = bounds.height / content_bounds.height; + // min height for easier grabbing with super tall content + let scroller_height = (bounds.height * ratio).max(2.0); + let scroller_offset = offset.y * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(total_scrollbar_width / 2 + scroller_width / 2), + y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height) + .max(0.0), + width: scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + }; + + let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { + let Properties { + width, + margin, + scroller_width, + } = *horizontal; + + // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar + // is present + let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| { + (vertical.width.max(vertical.scroller_width) + vertical.margin) + as f32 + }); + + let total_scrollbar_height = width.max(scroller_width) + 2 * margin; + + // Total bounds of the scrollbar + margin + scroller width + let total_scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height - total_scrollbar_height as f32, + width: (bounds.width - scrollbar_y_width).max(0.0), + height: total_scrollbar_height as f32, + }; + + // Bounds of just the scrollbar + let scrollbar_bounds = Rectangle { + x: bounds.x, + y: bounds.y + bounds.height + - f32::from(total_scrollbar_height / 2 + width / 2), + width: (bounds.width - scrollbar_y_width).max(0.0), + height: width as f32, + }; + + let ratio = bounds.width / content_bounds.width; + // min width for easier grabbing with extra wide content + let scroller_length = (bounds.width * ratio).max(2.0); + let scroller_offset = offset.x * ratio; + + let scroller_bounds = Rectangle { + x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) + .max(0.0), + y: bounds.y + bounds.height + - f32::from( + total_scrollbar_height / 2 + scroller_width / 2, + ), + width: scroller_length, + height: scroller_width as f32, + }; + + Some(Scrollbar { + total_bounds: total_scrollbar_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + }; + + Self { + y: y_scrollbar, + x: x_scrollbar, + } + } + + fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { + ( + self.y + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false), + self.x + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false), + ) + } + + fn grab_y_scroller(&self, cursor_position: Point) -> Option { + self.y.and_then(|scrollbar| { + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scrollbar.scroller.bounds.contains(cursor_position) { + (cursor_position.y - scrollbar.scroller.bounds.y) + / scrollbar.scroller.bounds.height + } else { + 0.5 + }) + } else { + None + } + }) + } + + fn grab_x_scroller(&self, cursor_position: Point) -> Option { + self.x.and_then(|scrollbar| { + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scrollbar.scroller.bounds.contains(cursor_position) { + (cursor_position.x - scrollbar.scroller.bounds.x) + / scrollbar.scroller.bounds.width + } else { + 0.5 + }) + } else { + None + } + }) + } + + fn active(&self) -> bool { + self.y.is_some() || self.x.is_some() } } /// The scrollbar of a [`Scrollable`]. -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] struct Scrollbar { - /// The outer bounds of the scrollable, including the [`Scrollbar`] and - /// [`Scroller`]. - outer_bounds: Rectangle, + /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, + /// and the scrollbar margin. + total_bounds: Rectangle, - /// The bounds of the [`Scrollbar`]. + /// The bounds of just the [`Scrollbar`]. bounds: Rectangle, - /// The bounds of the [`Scroller`]. + /// The state of this scrollbar's [`Scroller`]. scroller: Scroller, } impl Scrollbar { + /// Returns whether the mouse is over the scrollbar or not. fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.outer_bounds.contains(cursor_position) + self.total_bounds.contains(cursor_position) } - fn grab_scroller(&self, cursor_position: Point) -> Option { - if self.outer_bounds.contains(cursor_position) { - Some(if self.scroller.bounds.contains(cursor_position) { - (cursor_position.y - self.scroller.bounds.y) - / self.scroller.bounds.height - } else { - 0.5 - }) + /// Returns the y-axis scrolled percentage from the cursor position. + fn scroll_percentage_y( + &self, + grabbed_at: f32, + cursor_position: Point, + ) -> f32 { + if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + // cursor position is unavailable! Set to either end or beginning of scrollbar depending + // on where the thumb currently is in the track + (self.scroller.bounds.y / self.total_bounds.height).round() } else { - None + (cursor_position.y + - self.bounds.y + - self.scroller.bounds.height * grabbed_at) + / (self.bounds.height - self.scroller.bounds.height) } } - fn scroll_percentage( + /// Returns the x-axis scrolled percentage from the cursor position. + fn scroll_percentage_x( &self, grabbed_at: f32, cursor_position: Point, ) -> f32 { - (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height) + if cursor_position.x < 0.0 && cursor_position.y < 0.0 { + (self.scroller.bounds.x / self.total_bounds.width).round() + } else { + (cursor_position.x + - self.bounds.x + - self.scroller.bounds.width * grabbed_at) + / (self.bounds.width - self.scroller.bounds.width) + } } } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 9391d1dd8f..8755b85d81 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -18,10 +18,12 @@ use crate::layout; use crate::mouse::{self, click}; use crate::renderer; use crate::text::{self, Text}; +use crate::time::{Duration, Instant}; use crate::touch; use crate::widget; use crate::widget::operation::{self, Operation}; use crate::widget::tree::{self, Tree}; +use crate::window; use crate::{ Clipboard, Color, Command, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, Widget, @@ -228,6 +230,7 @@ where &self, tree: &mut Tree, _layout: Layout<'_>, + _renderer: &Renderer, operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::(); @@ -424,7 +427,18 @@ where let state = state(); let is_clicked = layout.bounds().contains(cursor_position); - state.is_focused = is_clicked; + state.is_focused = if is_clicked { + state.is_focused.or_else(|| { + let now = Instant::now(); + + Some(Focus { + updated_at: now, + now, + }) + }) + } else { + None + }; if is_clicked { let text_layout = layout.children().next().unwrap(); @@ -453,9 +467,17 @@ where ) } else { None - }; + } + .unwrap_or(0); - state.cursor.move_to(position.unwrap_or(0)); + if state.keyboard_modifiers.shift() { + state.cursor.select_range( + state.cursor.start(value), + position, + ); + } else { + state.cursor.move_to(position); + } state.is_dragging = true; } click::Kind::Double => { @@ -532,26 +554,30 @@ where Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { let state = state(); - if state.is_focused - && state.is_pasting.is_none() - && !state.keyboard_modifiers.command() - && !c.is_control() - { - let mut editor = Editor::new(value, &mut state.cursor); + if let Some(focus) = &mut state.is_focused { + 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); + editor.insert(c); - let message = (on_change)(editor.contents()); - shell.publish(message); + let message = (on_change)(editor.contents()); + shell.publish(message); - return event::Status::Captured; + focus.updated_at = Instant::now(); + + return event::Status::Captured; + } } } Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { let state = state(); - if state.is_focused { + if let Some(focus) = &mut state.is_focused { let modifiers = state.keyboard_modifiers; + focus.updated_at = Instant::now(); match key_code { keyboard::KeyCode::Enter @@ -712,7 +738,7 @@ where state.cursor.select_all(value); } keyboard::KeyCode::Escape => { - state.is_focused = false; + state.is_focused = None; state.is_dragging = false; state.is_pasting = None; @@ -733,7 +759,7 @@ where Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { let state = state(); - if state.is_focused { + if state.is_focused.is_some() { match key_code { keyboard::KeyCode::V => { state.is_pasting = None; @@ -756,6 +782,21 @@ where state.keyboard_modifiers = modifiers; } + Event::Window(window::Event::RedrawRequested(now)) => { + let state = state(); + + if let Some(focus) = &mut state.is_focused { + focus.now = now; + + let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis(millis_until_redraw as u64), + )); + } + } _ => {} } @@ -811,7 +852,7 @@ pub fn draw( let text = value.to_string(); let size = size.unwrap_or_else(|| renderer.default_size()); - let (cursor, offset) = if state.is_focused() { + let (cursor, offset) = if let Some(focus) = &state.is_focused { match state.cursor.state(value) { cursor::State::Index(position) => { let (text_value_width, offset) = @@ -824,7 +865,13 @@ pub fn draw( font.clone(), ); - ( + let is_cursor_visible = ((focus.now - focus.updated_at) + .as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; + + let cursor = if is_cursor_visible { Some(( renderer::Quad { bounds: Rectangle { @@ -838,9 +885,12 @@ pub fn draw( border_color: Color::TRANSPARENT, }, theme.value_color(style), - )), - offset, - ) + )) + } else { + None + }; + + (cursor, offset) } cursor::State::Selection { start, end } => { let left = start.min(end); @@ -949,7 +999,7 @@ pub fn mouse_interaction( /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State { - is_focused: bool, + is_focused: Option, is_dragging: bool, is_pasting: Option, last_click: Option, @@ -958,6 +1008,12 @@ pub struct State { // TODO: Add stateful horizontal scrolling offset } +#[derive(Debug, Clone, Copy)] +struct Focus { + updated_at: Instant, + now: Instant, +} + impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. pub fn new() -> Self { @@ -967,7 +1023,7 @@ impl State { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { - is_focused: true, + is_focused: None, is_dragging: false, is_pasting: None, last_click: None, @@ -978,7 +1034,7 @@ impl State { /// Returns whether the [`TextInput`] is currently focused or not. pub fn is_focused(&self) -> bool { - self.is_focused + self.is_focused.is_some() } /// Returns the [`Cursor`] of the [`TextInput`]. @@ -988,13 +1044,19 @@ impl State { /// Focuses the [`TextInput`]. pub fn focus(&mut self) { - self.is_focused = true; + let now = Instant::now(); + + self.is_focused = Some(Focus { + updated_at: now, + now, + }); + self.move_cursor_to_end(); } /// Unfocuses the [`TextInput`]. pub fn unfocus(&mut self) { - self.is_focused = false; + self.is_focused = None; } /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. @@ -1147,3 +1209,5 @@ where ) .map(text::Hit::cursor) } + +const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 37cafd92f6..f0a944a3b6 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -24,9 +24,9 @@ pub use iced_style::toggler::{Appearance, StyleSheet}; /// TogglerToggled(bool), /// } /// -/// let is_active = true; +/// let is_toggled = true; /// -/// Toggler::new(is_active, String::from("Toggle me!"), |b| Message::TogglerToggled(b)); +/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); /// ``` #[allow(missing_debug_implementations)] pub struct Toggler<'a, Message, Renderer> @@ -34,7 +34,7 @@ where Renderer: text::Renderer, Renderer::Theme: StyleSheet, { - is_active: bool, + is_toggled: bool, on_toggle: Box Message + 'a>, label: Option, width: Length, @@ -63,15 +63,15 @@ where /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. pub fn new( - is_active: bool, label: impl Into>, + is_toggled: bool, f: F, ) -> Self where F: 'a + Fn(bool) -> Message, { Toggler { - is_active, + is_toggled, on_toggle: Box::new(f), label: label.into(), width: Length::Fill, @@ -193,7 +193,7 @@ where let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { - shell.publish((self.on_toggle)(!self.is_active)); + shell.publish((self.on_toggle)(!self.is_toggled)); event::Status::Captured } else { @@ -260,13 +260,13 @@ where let is_mouse_over = bounds.contains(cursor_position); let style = if is_mouse_over { - theme.hovered(&self.style, self.is_active) + theme.hovered(&self.style, self.is_toggled) } else { - theme.active(&self.style, self.is_active) + theme.active(&self.style, self.is_toggled) }; - let border_radius = bounds.height as f32 / BORDER_RADIUS_RATIO; - let space = SPACE_RATIO * bounds.height as f32; + let border_radius = bounds.height / BORDER_RADIUS_RATIO; + let space = SPACE_RATIO * bounds.height; let toggler_background_bounds = Rectangle { x: bounds.x + space, @@ -289,7 +289,7 @@ where let toggler_foreground_bounds = Rectangle { x: bounds.x - + if self.is_active { + + if self.is_toggled { bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) } else { 2.0 * space diff --git a/native/src/widget/vertical_slider.rs b/native/src/widget/vertical_slider.rs new file mode 100644 index 0000000000..28e8405ccc --- /dev/null +++ b/native/src/widget/vertical_slider.rs @@ -0,0 +1,470 @@ +//! Display an interactive selector of a single value from a range of values. +//! +//! A [`VerticalSlider`] has some local [`State`]. +use std::ops::RangeInclusive; + +pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; + +use crate::event::{self, Event}; +use crate::widget::tree::{self, Tree}; +use crate::{ + layout, mouse, renderer, touch, Background, Clipboard, Color, Element, + Layout, Length, Point, Rectangle, Shell, Size, Widget, +}; + +/// An vertical bar and a handle that selects a single value from a range of +/// values. +/// +/// A [`VerticalSlider`] will try to fill the vertical space of its container. +/// +/// The [`VerticalSlider`] range of numeric values is generic and its step size defaults +/// to 1 unit. +/// +/// # Example +/// ``` +/// # use iced_native::widget::vertical_slider; +/// # use iced_native::renderer::Null; +/// # +/// # type VerticalSlider<'a, T, Message> = vertical_slider::VerticalSlider<'a, T, Message, Null>; +/// # +/// #[derive(Clone)] +/// pub enum Message { +/// SliderChanged(f32), +/// } +/// +/// let value = 50.0; +/// +/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); +/// ``` +#[allow(missing_debug_implementations)] +pub struct VerticalSlider<'a, T, Message, Renderer> +where + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + range: RangeInclusive, + step: T, + value: T, + on_change: Box Message + 'a>, + on_release: Option, + width: u16, + height: Length, + style: ::Style, +} + +impl<'a, T, Message, Renderer> VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + From + std::cmp::PartialOrd, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + /// The default width of a [`VerticalSlider`]. + pub const DEFAULT_WIDTH: u16 = 22; + + /// Creates a new [`VerticalSlider`]. + /// + /// It expects: + /// * an inclusive range of possible values + /// * the current value of the [`VerticalSlider`] + /// * a function that will be called when the [`VerticalSlider`] is dragged. + /// It receives the new value of the [`VerticalSlider`] and must produce a + /// `Message`. + pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self + where + F: 'a + Fn(T) -> Message, + { + let value = if value >= *range.start() { + value + } else { + *range.start() + }; + + let value = if value <= *range.end() { + value + } else { + *range.end() + }; + + VerticalSlider { + value, + range, + step: T::from(1), + on_change: Box::new(on_change), + on_release: None, + width: Self::DEFAULT_WIDTH, + height: Length::Fill, + style: Default::default(), + } + } + + /// Sets the release message of the [`VerticalSlider`]. + /// This is called when the mouse is released from the slider. + /// + /// Typically, the user's interaction with the slider is finished when this message is produced. + /// This is useful if you need to spawn a long-running task from the slider's result, where + /// the default on_change message could create too many events. + pub fn on_release(mut self, on_release: Message) -> Self { + self.on_release = Some(on_release); + self + } + + /// Sets the width of the [`VerticalSlider`]. + pub fn width(mut self, width: u16) -> Self { + self.width = width; + self + } + + /// Sets the height of the [`VerticalSlider`]. + pub fn height(mut self, height: Length) -> Self { + self.height = height; + self + } + + /// Sets the style of the [`VerticalSlider`]. + pub fn style( + mut self, + style: impl Into<::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the step size of the [`VerticalSlider`]. + pub fn step(mut self, step: T) -> Self { + self.step = step; + self + } +} + +impl<'a, T, Message, Renderer> Widget + for VerticalSlider<'a, T, Message, Renderer> +where + T: Copy + Into + num_traits::FromPrimitive, + Message: Clone, + Renderer: crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = + limits.width(Length::Units(self.width)).height(self.height); + + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + tree.state.downcast_mut::(), + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + layout, + cursor_position, + tree.state.downcast_ref::(), + self.value, + &self.range, + theme, + &self.style, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse_interaction( + layout, + cursor_position, + tree.state.downcast_ref::(), + ) + } +} + +impl<'a, T, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + T: 'a + Copy + Into + num_traits::FromPrimitive, + Message: 'a + Clone, + Renderer: 'a + crate::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from( + slider: VerticalSlider<'a, T, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(slider) + } +} + +/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`] +/// accordingly. +pub fn update( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option, +) -> event::Status +where + T: Copy + Into + num_traits::FromPrimitive, + Message: Clone, +{ + let is_dragging = state.is_dragging; + + let mut change = || { + let bounds = layout.bounds(); + let new_value = if cursor_position.y >= bounds.y + bounds.height { + *range.start() + } else if cursor_position.y <= bounds.y { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + state.is_dragging = true; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`VerticalSlider`]. +pub fn draw( + renderer: &mut R, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive, + style_sheet: &dyn StyleSheet