diff options
author | 2023-04-17 23:41:12 +0200 | |
---|---|---|
committer | 2023-04-17 23:41:12 +0200 | |
commit | 4bae457c37b499f3cfddbdac9ff37a34cbce61d5 (patch) | |
tree | 79af93b2f7fabca1687900b48b165c5c74dcd26f | |
parent | c0431aedd3bbef4161456f2fa5f29866e8f17fc5 (diff) | |
parent | 4b05f42fd6d18bf572b772dd60d6a4309ea5f343 (diff) | |
download | iced-4bae457c37b499f3cfddbdac9ff37a34cbce61d5.tar.gz iced-4bae457c37b499f3cfddbdac9ff37a34cbce61d5.tar.bz2 iced-4bae457c37b499f3cfddbdac9ff37a34cbce61d5.zip |
Merge branch 'master' into advanced-text
73 files changed, 1588 insertions, 705 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9d1fb3..077f4af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.0] - 2023-04-13 +### Added +- `MouseArea` widget. [#1594](https://github.com/iced-rs/iced/pull/1594) +- `channel` helper in `subscription`. [#1786](https://github.com/iced-rs/iced/pull/1786) +- Configurable `width` for `Scrollable`. [#1749](https://github.com/iced-rs/iced/pull/1749) +- Support for disabled `TextInput`. [#1744](https://github.com/iced-rs/iced/pull/1744) +- Platform-specific window settings. [#1730](https://github.com/iced-rs/iced/pull/1730) +- Left and right colors for sliders. [#1643](https://github.com/iced-rs/iced/pull/1643) +- Icon for `TextInput`. [#1702](https://github.com/iced-rs/iced/pull/1702) +- Mouse over scrollbar flag for `scrollable::StyleSheet`. [#1669](https://github.com/iced-rs/iced/pull/1669) +- Better example for `Radio`. [#1762](https://github.com/iced-rs/iced/pull/1762) + +### Changed +- `wgpu` has been updated to `0.15` in `iced_wgpu`. [#1789](https://github.com/iced-rs/iced/pull/1789) +- `resvg` has been updated to `0.29` in `iced_graphics`. [#1733](https://github.com/iced-rs/iced/pull/1733) +- `subscription::run` now takes a function pointer. [#1723](https://github.com/iced-rs/iced/pull/1723) + +### Fixed +- Redundant `on_scroll` messages for `Scrollable`. [#1788](https://github.com/iced-rs/iced/pull/1788) +- Outdated items in `ROADMAP.md` [#1782](https://github.com/iced-rs/iced/pull/1782) +- Colons in shader labels causing compilation issues in `iced_wgpu`. [#1779](https://github.com/iced-rs/iced/pull/1779) +- Re-expose winit features for window servers in Linux. [#1777](https://github.com/iced-rs/iced/pull/1777) +- Replacement of application node in Wasm. [#1765](https://github.com/iced-rs/iced/pull/1765) +- `clippy` lints for Rust 1.68. [#1755](https://github.com/iced-rs/iced/pull/1755) +- Unnecessary `Component` rebuilds. [#1754](https://github.com/iced-rs/iced/pull/1754) +- Incorrect package name in checkbox example docs. [#1750](https://github.com/iced-rs/iced/pull/1750) +- Fullscreen only working on primary monitor. [#1742](https://github.com/iced-rs/iced/pull/1742) +- `Padding::fit` on irregular values for an axis. [#1734](https://github.com/iced-rs/iced/pull/1734) +- `Debug` implementation of `Font` displaying its bytes. [#1731](https://github.com/iced-rs/iced/pull/1731) +- Sliders bleeding over their rail. [#1721](https://github.com/iced-rs/iced/pull/1721) + +### Removed +- `Fill` variant for `Alignment`. [#1735](https://github.com/iced-rs/iced/pull/1735) + +Many thanks to... + +- @ahoneybun +- @bq-wrongway +- @bungoboingo +- @casperstorm +- @Davidster +- @ElhamAryanpur +- @FinnPerry +- @GyulyVGC +- @JungleTryne +- @lupd +- @mmstick +- @nicksenger +- @Night-Hunter-NF +- @tarkah +- @traxys +- @Xaeroxe + ## [0.8.0] - 2023-02-18 ### Added - Generic pixel units. [#1711](https://github.com/iced-rs/iced/pull/1711) @@ -414,7 +467,8 @@ Many thanks to... ### Added - First release! :tada: -[Unreleased]: https://github.com/iced-rs/iced/compare/0.8.0...HEAD +[Unreleased]: https://github.com/iced-rs/iced/compare/0.9.0...HEAD +[0.9.0]: https://github.com/iced-rs/iced/compare/0.8.0...0.9.0 [0.8.0]: https://github.com/iced-rs/iced/compare/0.7.0...0.8.0 [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 @@ -1,6 +1,6 @@ [package] name = "iced" -version = "0.8.0" +version = "0.9.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A cross-platform GUI library inspired by Elm" @@ -61,11 +61,11 @@ members = [ ] [dependencies] -iced_core = { version = "0.8", path = "core" } +iced_core = { version = "0.9", path = "core" } iced_futures = { version = "0.6", path = "futures" } iced_renderer = { version = "0.1", path = "renderer" } iced_widget = { version = "0.1", path = "widget" } -iced_winit = { version = "0.8", path = "winit", features = ["application"] } +iced_winit = { version = "0.9", path = "winit", features = ["application"] } thiserror = "1" [dependencies.image_rs] @@ -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.8" +iced = "0.9" ``` 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.8", default-features = false, features = ["glow"] } +iced = { version = "0.9", default-features = false, features = ["glow"] } ``` __NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0, @@ -19,6 +19,7 @@ Once a step is completed, it is collapsed and added to this list: * [x] Custom styling ([#146]) * [x] Canvas for 2D graphics ([#193]) * [x] Basic overlay support ([#444]) + * [x] Animations [#31] [#24]: https://github.com/iced-rs/iced/issues/24 [#25]: https://github.com/iced-rs/iced/issues/25 @@ -29,6 +30,7 @@ Once a step is completed, it is collapsed and added to this list: [#146]: https://github.com/iced-rs/iced/pull/146 [#193]: https://github.com/iced-rs/iced/pull/193 [#444]: https://github.com/iced-rs/iced/pull/444 +[#31]: https://github.com/iced-rs/iced/issues/31 ### Multi-window support ([#27]) Open and control multiple windows at runtime. @@ -39,16 +41,7 @@ This approach should also allow us to perform custom optimizations for this part [#27]: https://github.com/iced-rs/iced/issues/27 -### Animations ([#31]) -Allow widgets to request a redraw at a specific time. - -This is a necessary feature to render loading spinners, a blinking text cursor, GIF images, etc. - -[`winit`] allows flexible control of its event loop. We may be able to use [`ControlFlow::WaitUntil`](https://docs.rs/winit/0.20.0-alpha3/winit/event_loop/enum.ControlFlow.html#variant.WaitUntil) for this purpose. - -[#31]: https://github.com/iced-rs/iced/issues/31 - -### Canvas widget for 3D graphics ([#32]) +### Canvas widget for 3D graphics (~~[#32]~~ [#343]) A widget to draw freely in 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc. As a first approach, we could expose the underlying renderer directly here, and couple this widget with it ([`wgpu`] for now). Once [`wgpu`] gets WebGL or WebGPU support, this widget will be able to run on the web too. The renderer primitive could be a simple texture that the widget draws to. @@ -56,6 +49,7 @@ As a first approach, we could expose the underlying renderer directly here, and In the long run, we could expose a renderer-agnostic abstraction to perform the drawing. [#32]: https://github.com/iced-rs/iced/issues/32 +[#343] https://github.com/iced-rs/iced/issues/343 ### Text shaping and font fallback ([#33]) [`wgpu_glyph`] uses [`glyph_brush`], which in turn uses [`rusttype`]. While the current implementation is able to layout text quite nicely, it does not perform any [text shaping]. diff --git a/core/Cargo.toml b/core/Cargo.toml index dac31828..92d9773f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_core" -version = "0.8.1" +version = "0.9.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "The essential concepts of Iced" diff --git a/core/README.md b/core/README.md index 64d92e78..519e0608 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.8" +iced_core = "0.9" ``` __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 5bdcee6a..89dfb828 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,7 +7,7 @@ //!  //! //! [Iced]: https://github.com/iced-rs/iced -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/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/mouse/interaction.rs b/core/src/mouse/interaction.rs index 57da93fe..072033fd 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -12,4 +12,5 @@ pub enum Interaction { Grabbing, ResizingHorizontally, ResizingVertically, + NotAllowed, } diff --git a/core/src/widget.rs b/core/src/widget.rs index 70e2c2d9..769f8659 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -33,12 +33,12 @@ use crate::{Clipboard, 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.8/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry +/// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry /// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/wgpu pub trait Widget<Message, Renderer> where Renderer: crate::Renderer, diff --git a/core/src/window.rs b/core/src/window.rs index d829a4b4..81bd7e3d 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -1,10 +1,13 @@ //! Build window-based GUI applications. +pub mod icon; + mod event; mod mode; mod redraw_request; mod user_attention; pub use event::Event; +pub use icon::Icon; pub use mode::Mode; pub use redraw_request::RedrawRequest; pub use user_attention::UserAttention; diff --git a/core/src/window/icon.rs b/core/src/window/icon.rs new file mode 100644 index 00000000..31868ecf --- /dev/null +++ b/core/src/window/icon.rs @@ -0,0 +1,80 @@ +//! Change the icon of a window. +use crate::Size; + +use std::mem; + +/// Builds an [`Icon`] from its RGBA pixels in the sRGB color space. +pub fn from_rgba( + rgba: Vec<u8>, + width: u32, + height: u32, +) -> Result<Icon, Error> { + const PIXEL_SIZE: usize = mem::size_of::<u8>() * 4; + + if rgba.len() % PIXEL_SIZE != 0 { + return Err(Error::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + + let pixel_count = rgba.len() / PIXEL_SIZE; + + if pixel_count != (width * height) as usize { + return Err(Error::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + pixel_count, + }); + } + + Ok(Icon { + rgba, + size: Size::new(width, height), + }) +} + +/// An window icon normally used for the titlebar or taskbar. +#[derive(Debug, Clone)] +pub struct Icon { + rgba: Vec<u8>, + size: Size<u32>, +} + +impl Icon { + /// Returns the raw data of the [`Icon`]. + pub fn into_raw(self) -> (Vec<u8>, Size<u32>) { + (self.rgba, self.size) + } +} + +#[derive(Debug, thiserror::Error)] +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +pub enum Error { + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + #[error( + "The provided RGBA data (with length {byte_count}) isn't divisible \ + by 4. Therefore, it cannot be safely interpreted as 32bpp RGBA pixels" + )] + ByteCountNotDivisibleBy4 { + /// The length of the provided RGBA data. + byte_count: usize, + }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + #[error( + "The number of RGBA pixels ({pixel_count}) does not match the \ + provided dimensions ({width}x{height})." + )] + DimensionsVsPixelCount { + /// The provided width. + width: u32, + /// The provided height. + height: u32, + /// The product of `width` and `height`. + width_x_height: usize, + /// The amount of pixels of the provided RGBA data. + pixel_count: usize, + }, +} diff --git a/docs/release_summary.py b/docs/release_summary.py new file mode 100644 index 00000000..cd4593b5 --- /dev/null +++ b/docs/release_summary.py @@ -0,0 +1,52 @@ +import re +import sys +import requests +from typing import List, Tuple + +if len(sys.argv) < 3: + print("Usage: python release_summary.py <personal_access_token> <previous_release_branch>") + exit(1) + +TOKEN = sys.argv[1] +HEADERS = {"Authorization": f"Bearer {TOKEN}"} +PR_COMMIT_REGEX = re.compile(r"(?i)Merge pull request #(\d+).*") + +def get_merged_prs_since_release(repo: str, previous_release_branch: str) -> List[Tuple[str, int, str, str]]: + prs = [] + compare_url = f"https://api.github.com/repos/{repo}/compare/{previous_release_branch}...master" + compare_response = requests.get(compare_url, headers=HEADERS) + + if compare_response.status_code == 200: + compare_data = compare_response.json() + for commit in compare_data["commits"]: + match = PR_COMMIT_REGEX.search(commit["commit"]["message"]) + if match: + pr_number = int(match.group(1)) + pr_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" + pr_response = requests.get(pr_url, headers=HEADERS) + if pr_response.status_code == 200: + pr_data = pr_response.json() + prs.append((pr_data["title"], pr_number, pr_data["html_url"], pr_data["user"]["login"])) + else: + print(f"Error fetching PR {pr_number}: {pr_response.status_code}") + else: + print(f"Error comparing branches: {compare_response.status_code}") + + return prs + +def print_pr_list(prs: List[Tuple[str, int, str, str]]): + for pr in prs: + print(f"- {pr[0]}. [#{pr[1]}]({pr[2]})") + +def print_authors(prs: List[Tuple[str, int, str, str]]): + authors = set(pr[3] for pr in prs) + print("\nAuthors:") + for author in sorted(authors, key=str.casefold): + print(f"- @{author}") + +if __name__ == "__main__": + repo = "iced-rs/iced" + previous_release_branch = sys.argv[2] + merged_prs = get_merged_prs_since_release(repo, previous_release_branch) + print_pr_list(merged_prs) + print_authors(merged_prs) diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index 09e5e4a2..010321a9 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -134,8 +134,8 @@ mod numeric_input { .map(u32::to_string) .as_deref() .unwrap_or(""), - Event::InputChanged, ) + .on_input(Event::InputChanged) .padding(10), button("+", Event::IncrementPressed), ] diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs index 5ff951b3..3b11cb76 100644 --- a/examples/download_progress/src/download.rs +++ b/examples/download_progress/src/download.rs @@ -18,10 +18,7 @@ pub struct Download<I> { url: String, } -async fn download<I: Copy>( - id: I, - state: State, -) -> (Option<(I, Progress)>, State) { +async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) { match state { State::Ready(url) => { let response = reqwest::get(&url).await; @@ -30,7 +27,7 @@ async fn download<I: Copy>( Ok(response) => { if let Some(total) = response.content_length() { ( - Some((id, Progress::Started)), + (id, Progress::Started), State::Downloading { response, total, @@ -38,10 +35,10 @@ async fn download<I: Copy>( }, ) } else { - (Some((id, Progress::Errored)), State::Finished) + ((id, Progress::Errored), State::Finished) } } - Err(_) => (Some((id, Progress::Errored)), State::Finished), + Err(_) => ((id, Progress::Errored), State::Finished), } } State::Downloading { @@ -55,7 +52,7 @@ async fn download<I: Copy>( let percentage = (downloaded as f32 / total as f32) * 100.0; ( - Some((id, Progress::Advanced(percentage))), + (id, Progress::Advanced(percentage)), State::Downloading { response, total, @@ -63,8 +60,8 @@ async fn download<I: Copy>( }, ) } - Ok(None) => (Some((id, Progress::Finished)), State::Finished), - Err(_) => (Some((id, Progress::Errored)), State::Finished), + Ok(None) => ((id, Progress::Finished), State::Finished), + Err(_) => ((id, Progress::Errored), State::Finished), }, State::Finished => { // We do not let the stream die, as it would start a diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index f6863cd3..2ab5d75f 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -10,7 +10,7 @@ iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu" } iced_widget = { path = "../../widget" } iced_renderer = { path = "../../renderer", features = ["wgpu", "tiny-skia"] } -env_logger = "0.8" +env_logger = "0.10" [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.7" diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 5849f730..14e53ede 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -102,11 +102,10 @@ impl Program for Controls { .size(14) .style(Color::WHITE), ) - .push(text_input( - "Placeholder", - text, - Message::TextChanged, - )), + .push( + text_input("Placeholder", text) + .on_input(Message::TextChanged), + ), ), ) .into() diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 949a726a..98d2bc59 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -30,6 +30,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { #[cfg(target_arch = "wasm32")] let canvas_element = { console_log::init_with_level(log::Level::Debug)?; + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); web_sys::window() @@ -49,7 +50,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { .build(&event_loop)?; #[cfg(not(target_arch = "wasm32"))] - let window = winit::window::Window::new(&event_loop).unwrap(); + let window = winit::window::Window::new(&event_loop)?; let physical_size = window.inner_size(); let mut viewport = Viewport::with_physical_size( @@ -61,7 +62,6 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { let mut clipboard = Clipboard::connect(&window); // Initialize wgpu - #[cfg(target_arch = "wasm32")] let default_backend = wgpu::Backends::GL; #[cfg(not(target_arch = "wasm32"))] @@ -95,12 +95,16 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { #[cfg(not(target_arch = "wasm32"))] let needed_limits = wgpu::Limits::default(); + let capabilities = surface.get_capabilities(&adapter); + ( - surface - .get_capabilities(&adapter) + capabilities .formats - .first() + .iter() + .filter(|format| format.describe().srgb) .copied() + .next() + .or_else(|| capabilities.formats.first().copied()) .expect("Get preferred format"), adapter .request_device( diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index e1cdaefe..c6baa6a1 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -213,12 +213,9 @@ impl Sandbox for App { column![ scrollable(options).height(Length::Fill), row![ - text_input( - "Add a new option", - &self.input, - Message::InputChanged, - ) - .on_submit(Message::AddItem(self.input.clone())), + text_input("Add a new option", &self.input) + .on_input(Message::InputChanged) + .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 214ec97e..f48afb69 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -133,18 +133,16 @@ impl Application for App { column![ column![ text("Email").size(12), - text_input( - "abc@123.com", - &self.email, - Message::Email - ) - .on_submit(Message::Submit) - .padding(5), + text_input("abc@123.com", &self.email,) + .on_input(Message::Email) + .on_submit(Message::Submit) + .padding(5), ] .spacing(5), column![ text("Password").size(12), - text_input("", &self.password, Message::Password) + text_input("", &self.password) + .on_input(Message::Password) .on_submit(Message::Submit) .password() .padding(5), diff --git a/examples/qr_code/src/main.rs b/examples/qr_code/src/main.rs index d8041745..867ebfa4 100644 --- a/examples/qr_code/src/main.rs +++ b/examples/qr_code/src/main.rs @@ -49,13 +49,11 @@ impl Sandbox for QRGenerator { .size(70) .style(Color::from([0.5, 0.5, 0.5])); - let input = text_input( - "Type the data of your QR code here...", - &self.data, - Message::DataChanged, - ) - .size(30) - .padding(15); + let input = + text_input("Type the data of your QR code here...", &self.data) + .on_input(Message::DataChanged) + .size(30) + .padding(15); let mut content = column![title, input] .width(700) diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index a3ade54f..2e99b1ac 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -338,22 +338,36 @@ impl scrollable::StyleSheet for ScrollbarCustomStyle { style.active(&theme::Scrollable::Default) } - fn hovered(&self, style: &Self::Style) -> Scrollbar { - style.hovered(&theme::Scrollable::Default) + fn hovered( + &self, + style: &Self::Style, + is_mouse_over_scrollbar: bool, + ) -> Scrollbar { + style.hovered(&theme::Scrollable::Default, is_mouse_over_scrollbar) } - 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), + fn hovered_horizontal( + &self, + style: &Self::Style, + is_mouse_over_scrollbar: bool, + ) -> Scrollbar { + if is_mouse_over_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(), + }, + } + } else { + self.active(style) } } } diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs index 448c9792..e2015bac 100644 --- a/examples/styling/src/main.rs +++ b/examples/styling/src/main.rs @@ -90,13 +90,10 @@ impl Sandbox for Styling { }, ); - let text_input = text_input( - "Type something...", - &self.input_value, - Message::InputChanged, - ) - .padding(10) - .size(20); + let text_input = text_input("Type something...", &self.input_value) + .on_input(Message::InputChanged) + .padding(10) + .size(20); let button = button("Submit") .padding(10) diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index 78fb9de1..9d859258 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -119,13 +119,15 @@ impl Application for App { column![ subtitle( "Title", - text_input("", &self.editing.title, Message::Title) + text_input("", &self.editing.title) + .on_input(Message::Title) .on_submit(Message::Add) .into() ), subtitle( "Message", - text_input("", &self.editing.body, Message::Body) + text_input("", &self.editing.body) + .on_input(Message::Body) .on_submit(Message::Add) .into() ), diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index ed3684d3..8bc7be09 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -210,15 +210,12 @@ impl Application for Todos { .style(Color::from([0.5, 0.5, 0.5])) .horizontal_alignment(alignment::Horizontal::Center); - let input = text_input( - "What needs to be done?", - input_value, - Message::InputChanged, - ) - .id(INPUT_ID.clone()) - .padding(15) - .size(30) - .on_submit(Message::CreateTask); + let input = text_input("What needs to be done?", input_value) + .id(INPUT_ID.clone()) + .on_input(Message::InputChanged) + .on_submit(Message::CreateTask) + .padding(15) + .size(30); let controls = view_controls(tasks, *filter); let filtered_tasks = @@ -381,14 +378,12 @@ impl Task { .into() } TaskState::Editing => { - let text_input = text_input( - "Describe your task...", - &self.description, - TaskMessage::DescriptionEdited, - ) - .id(Self::text_input_id(i)) - .on_submit(TaskMessage::FinishEdition) - .padding(10); + let text_input = + text_input("Describe your task...", &self.description) + .id(Self::text_input_id(i)) + .on_input(TaskMessage::DescriptionEdited) + .on_submit(TaskMessage::FinishEdition) + .padding(10); row![ text_input, diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index de063d00..9c38ad0e 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -5,7 +5,7 @@ use iced::widget::{ scrollable, slider, text, text_input, toggler, vertical_space, }; use iced::widget::{Button, Column, Container, Slider}; -use iced::{Color, Element, Length, Renderer, Sandbox, Settings}; +use iced::{Color, Element, Font, Length, Renderer, Sandbox, Settings}; pub fn main() -> iced::Result { env_logger::init(); @@ -127,6 +127,7 @@ impl Steps { Step::TextInput { value: String::new(), is_secure: false, + is_showing_icon: false, }, Step::Debugger, Step::End, @@ -171,14 +172,32 @@ impl Steps { enum Step { Welcome, - Slider { value: u8 }, - RowsAndColumns { layout: Layout, spacing: u16 }, - Text { size: u16, color: Color }, - Radio { selection: Option<Language> }, - Toggler { can_continue: bool }, - Image { width: u16 }, + Slider { + value: u8, + }, + RowsAndColumns { + layout: Layout, + spacing: u16, + }, + Text { + size: u16, + color: Color, + }, + Radio { + selection: Option<Language>, + }, + Toggler { + can_continue: bool, + }, + Image { + width: u16, + }, Scrollable, - TextInput { value: String, is_secure: bool }, + TextInput { + value: String, + is_secure: bool, + is_showing_icon: bool, + }, Debugger, End, } @@ -194,6 +213,7 @@ pub enum StepMessage { ImageWidthChanged(u16), InputChanged(String), ToggleSecureInput(bool), + ToggleTextInputIcon(bool), DebugToggled(bool), TogglerChanged(bool), } @@ -256,6 +276,14 @@ impl<'a> Step { *can_continue = value; } } + StepMessage::ToggleTextInputIcon(toggle) => { + if let Step::TextInput { + is_showing_icon, .. + } = self + { + *is_showing_icon = toggle + } + } }; } @@ -303,9 +331,11 @@ impl<'a> Step { Self::rows_and_columns(*layout, *spacing) } Step::Scrollable => Self::scrollable(), - Step::TextInput { value, is_secure } => { - Self::text_input(value, *is_secure) - } + Step::TextInput { + value, + is_secure, + is_showing_icon, + } => Self::text_input(value, *is_secure, *is_showing_icon), Step::Debugger => Self::debugger(debug), Step::End => Self::end(), } @@ -530,14 +560,25 @@ impl<'a> Step { ) } - fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> { - let text_input = text_input( - "Type something to continue...", - value, - StepMessage::InputChanged, - ) - .padding(10) - .size(30); + fn text_input( + value: &str, + is_secure: bool, + is_showing_icon: bool, + ) -> Column<'a, StepMessage> { + let mut text_input = text_input("Type something to continue...", value) + .on_input(StepMessage::InputChanged) + .padding(10) + .size(30); + + if is_showing_icon { + text_input = text_input.icon(text_input::Icon { + font: Font::default(), + code_point: '🚀', + size: Some(28.0), + spacing: 10.0, + side: text_input::Side::Right, + }); + } Self::container("Text input") .push("Use a text input to ask for different kinds of information.") @@ -551,6 +592,11 @@ impl<'a> Step { is_secure, StepMessage::ToggleSecureInput, )) + .push(checkbox( + "Show icon", + is_showing_icon, + StepMessage::ToggleTextInputIcon, + )) .push( "A text input produces a message every time it changes. It is \ very easy to keep track of its contents:", diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs index 122c20db..281ed4bd 100644 --- a/examples/websocket/src/echo.rs +++ b/examples/websocket/src/echo.rs @@ -13,63 +13,67 @@ use std::fmt; pub fn connect() -> Subscription<Event> { struct Connect; - subscription::unfold( + subscription::channel( std::any::TypeId::of::<Connect>(), - State::Disconnected, - |state| async move { - match state { - State::Disconnected => { - const ECHO_SERVER: &str = "ws://localhost:3030"; - - match async_tungstenite::tokio::connect_async(ECHO_SERVER) + 100, + |mut output| async move { + let mut state = State::Disconnected; + + loop { + match &mut state { + State::Disconnected => { + const ECHO_SERVER: &str = "ws://127.0.0.1:3030"; + + match async_tungstenite::tokio::connect_async( + ECHO_SERVER, + ) .await - { - Ok((websocket, _)) => { - let (sender, receiver) = mpsc::channel(100); - - ( - Some(Event::Connected(Connection(sender))), - State::Connected(websocket, receiver), - ) - } - Err(_) => { - tokio::time::sleep( - tokio::time::Duration::from_secs(1), - ) - .await; + { + Ok((websocket, _)) => { + let (sender, receiver) = mpsc::channel(100); + + let _ = output + .send(Event::Connected(Connection(sender))) + .await; - (Some(Event::Disconnected), State::Disconnected) + state = State::Connected(websocket, receiver); + } + Err(_) => { + tokio::time::sleep( + tokio::time::Duration::from_secs(1), + ) + .await; + + let _ = output.send(Event::Disconnected).await; + } } } - } - State::Connected(mut websocket, mut input) => { - let mut fused_websocket = websocket.by_ref().fuse(); - - futures::select! { - received = fused_websocket.select_next_some() => { - match received { - Ok(tungstenite::Message::Text(message)) => { - ( - Some(Event::MessageReceived(Message::User(message))), - State::Connected(websocket, input) - ) - } - Ok(_) => { - (None, State::Connected(websocket, input)) - } - Err(_) => { - (Some(Event::Disconnected), State::Disconnected) + State::Connected(websocket, input) => { + let mut fused_websocket = websocket.by_ref().fuse(); + + futures::select! { + received = fused_websocket.select_next_some() => { + match received { + Ok(tungstenite::Message::Text(message)) => { + let _ = output.send(Event::MessageReceived(Message::User(message))).await; + } + Err(_) => { + let _ = output.send(Event::Disconnected).await; + + state = State::Disconnected; + } + Ok(_) => continue, } } - } - message = input.select_next_some() => { - let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; + message = input.select_next_some() => { + let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; + + if result.is_err() { + let _ = output.send(Event::Disconnected).await; - if result.is_ok() { - (None, State::Connected(websocket, input)) - } else { - (Some(Event::Disconnected), State::Disconnected) + state = State::Disconnected; + } } } } diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index e617b8ce..920189f5 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -125,12 +125,9 @@ impl Application for WebSocket { }; let new_message_input = { - let mut input = text_input( - "Type a message...", - &self.new_message, - Message::NewMessageChanged, - ) - .padding(10); + let mut input = text_input("Type a message...", &self.new_message) + .on_input(Message::NewMessageChanged) + .padding(10); let mut button = button( text("Send") diff --git a/futures/Cargo.toml b/futures/Cargo.toml index 411e7c2a..f636a304 100644 --- a/futures/Cargo.toml +++ b/futures/Cargo.toml @@ -17,7 +17,7 @@ thread-pool = ["futures/thread-pool"] log = "0.4" [dependencies.iced_core] -version = "0.8" +version = "0.9" path = "../core" [dependencies.futures] diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 876f29c2..801c2694 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -9,6 +9,8 @@ use crate::core::Hasher; use crate::futures::{Future, Stream}; use crate::{BoxStream, MaybeSend}; +use futures::channel::mpsc; +use futures::never::Never; use std::hash::Hash; /// A stream of runtime events. @@ -126,9 +128,9 @@ impl<Message> std::fmt::Debug for Subscription<Message> { /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how /// to listen to time. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.8/examples/download_progress -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.8/examples/stopwatch +/// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.9/examples/download_progress +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.9/examples/stopwatch pub trait Recipe { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. @@ -317,6 +319,27 @@ where /// [`Stream`] that will call the provided closure to produce every `Message`. /// /// The `id` will be used to uniquely identify the [`Subscription`]. +pub fn unfold<I, T, Fut, Message>( + id: I, + initial: T, + mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, +) -> Subscription<Message> +where + I: Hash + 'static, + T: MaybeSend + 'static, + Fut: Future<Output = (Message, T)> + MaybeSend + 'static, + Message: 'static + MaybeSend, +{ + use futures::future::FutureExt; + + run_with_id( + id, + futures::stream::unfold(initial, move |state| f(state).map(Some)), + ) +} + +/// Creates a [`Subscription`] that publishes the events sent from a [`Future`] +/// to an [`mpsc::Sender`] with the given bounds. /// /// # Creating an asynchronous worker with bidirectional communication /// You can leverage this helper to create a [`Subscription`] that spawns @@ -328,9 +351,8 @@ where /// /// ``` /// use iced_futures::subscription::{self, Subscription}; -/// use iced_futures::futures; -/// -/// use futures::channel::mpsc; +/// use iced_futures::futures::channel::mpsc; +/// use iced_futures::futures::sink::SinkExt; /// /// pub enum Event { /// Ready(mpsc::Sender<Input>), @@ -351,27 +373,35 @@ where /// fn some_worker() -> Subscription<Event> { /// struct SomeWorker; /// -/// subscription::unfold(std::any::TypeId::of::<SomeWorker>(), State::Starting, |state| async move { -/// match state { -/// State::Starting => { -/// // Create channel -/// let (sender, receiver) = mpsc::channel(100); +/// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move { +/// let mut state = State::Starting; /// -/// (Some(Event::Ready(sender)), State::Ready(receiver)) -/// } -/// State::Ready(mut receiver) => { -/// use futures::StreamExt; +/// loop { +/// match &mut state { +/// State::Starting => { +/// // Create channel +/// let (sender, receiver) = mpsc::channel(100); +/// +/// // Send the sender back to the application +/// output.send(Event::Ready(sender)).await; +/// +/// // We are ready to receive messages +/// state = State::Ready(receiver); +/// } +/// State::Ready(receiver) => { +/// use iced_futures::futures::StreamExt; /// -/// // Read next input sent from `Application` -/// let input = receiver.select_next_some().await; +/// // Read next input sent from `Application` +/// let input = receiver.select_next_some().await; /// -/// match input { -/// Input::DoSomeWork => { -/// // Do some async work... +/// match input { +/// Input::DoSomeWork => { +/// // Do some async work... /// -/// // Finally, we can optionally return a message to tell the -/// // `Application` the work is done -/// (Some(Event::WorkFinished), State::Ready(receiver)) +/// // Finally, we can optionally produce a message to tell the +/// // `Application` the work is done +/// output.send(Event::WorkFinished).await; +/// } /// } /// } /// } @@ -383,26 +413,29 @@ 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.8/examples/websocket -pub fn unfold<I, T, Fut, Message>( +/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.9/examples/websocket +pub fn channel<I, Fut, Message>( id: I, - initial: T, - mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, + size: usize, + f: impl Fn(mpsc::Sender<Message>) -> Fut + MaybeSend + Sync + 'static, ) -> Subscription<Message> where I: Hash + 'static, - T: MaybeSend + 'static, - Fut: Future<Output = (Option<Message>, T)> + MaybeSend + 'static, + Fut: Future<Output = Never> + MaybeSend + 'static, Message: 'static + MaybeSend, { - use futures::future::{self, FutureExt}; - use futures::stream::StreamExt; + use futures::stream::{self, StreamExt}; - run_with_id( + Subscription::from_recipe(Runner { id, - futures::stream::unfold(initial, move |state| f(state).map(Some)) - .filter_map(future::ready), - ) + spawn: move |_| { + let (sender, receiver) = mpsc::channel(size); + + let runner = stream::once(f(sender)).map(|_| unreachable!()); + + stream::select(receiver, runner) + }, + }) } struct Runner<I, F, S, Message> diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index a08c6707..f1ce6b3a 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_graphics" -version = "0.7.0" +version = "0.8.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced" @@ -27,7 +27,7 @@ version = "1.4" features = ["derive"] [dependencies.iced_core] -version = "0.8" +version = "0.9" path = "../core" [dependencies.tiny-skia] diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 629c11ba..17657888 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -16,11 +16,11 @@ raw-window-handle = "0.5" thiserror = "1" [dependencies.iced_graphics] -version = "0.7" +version = "0.8" path = "../graphics" [dependencies.iced_wgpu] -version = "0.9" +version = "0.10" path = "../wgpu" optional = true diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 2d3e8db3..a65f07f2 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_runtime" -version = "0.9.1" +version = "0.1.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A renderer-agnostic library for native GUIs" @@ -14,7 +14,7 @@ debug = [] thiserror = "1" [dependencies.iced_core] -version = "0.8" +version = "0.9" path = "../core" [dependencies.iced_futures] diff --git a/runtime/README.md b/runtime/README.md index 497fd145..1b0fa857 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -12,13 +12,6 @@ [`druid`]: https://github.com/xi-editor/druid [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle -## Installation -Add `iced_runtime` as a dependency in your `Cargo.toml`: - -```toml -iced_runtime = "0.9" -``` - __Iced moves fast and the `master` branch can contain breaking changes!__ If you want to learn about a specific release, check out [the release list]. diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index aa45e57a..8a277e47 100644 --- a/runtime/src/lib.rs +++ b/runtime/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.8/core -//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.8/winit +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.9/core +//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.9/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/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 2c76fd8a..c29de7db 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -19,8 +19,8 @@ use crate::core::{Element, Layout, Shell}; /// 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.8/examples/integration_opengl -/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_wgpu +/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration_opengl +/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration_wgpu #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Renderer> { root: Element<'a, Message, Renderer>, diff --git a/runtime/src/window.rs b/runtime/src/window.rs index 236064f7..833a1125 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -5,7 +5,7 @@ pub use action::Action; use crate::command::{self, Command}; use crate::core::time::Instant; -use crate::core::window::{Event, Mode, UserAttention}; +use crate::core::window::{Event, Icon, Mode, UserAttention}; use crate::futures::subscription::{self, Subscription}; /// Subscribes to the frames of the window of the running application. @@ -110,3 +110,8 @@ pub fn fetch_id<Message>( ) -> Command<Message> { Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) } + +/// Changes the [`Icon`] of the window. +pub fn change_icon<Message>(icon: Icon) -> Command<Message> { + Command::single(command::Action::Window(Action::ChangeIcon(icon))) +} diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs index c1dbd84f..83b71c75 100644 --- a/runtime/src/window/action.rs +++ b/runtime/src/window/action.rs @@ -1,4 +1,4 @@ -use crate::core::window::{Mode, UserAttention}; +use crate::core::window::{Icon, Mode, UserAttention}; use crate::futures::MaybeSend; use std::fmt; @@ -78,6 +78,21 @@ pub enum Action<T> { ChangeAlwaysOnTop(bool), /// Fetch an identifier unique to the window. FetchId(Box<dyn FnOnce(u64) -> T + 'static>), + /// Changes the window [`Icon`]. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland / macOS:** Unsupported. + /// + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That + /// said, it's usually in the same ballpark as on Windows. + ChangeIcon(Icon), } impl<T> Action<T> { @@ -108,6 +123,7 @@ impl<T> Action<T> { Action::ChangeAlwaysOnTop(on_top) } Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), + Self::ChangeIcon(icon) => Action::ChangeIcon(icon), } } } @@ -142,6 +158,9 @@ impl<T> fmt::Debug for Action<T> { write!(f, "Action::AlwaysOnTop({on_top})") } Self::FetchId(_) => write!(f, "Action::FetchId"), + Self::ChangeIcon(_icon) => { + write!(f, "Action::ChangeIcon(icon)") + } } } } diff --git a/src/application.rs b/src/application.rs index c9ddf840..abf58fa3 100644 --- a/src/application.rs +++ b/src/application.rs @@ -39,15 +39,15 @@ pub use crate::style::application::{Appearance, StyleSheet}; /// to listen to time. /// - [`todos`], a todos tracker inspired by [TodoMVC]. /// -/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.8/examples -/// [`clock`]: https://github.com/iced-rs/iced/tree/0.8/examples/clock -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.8/examples/download_progress -/// [`events`]: https://github.com/iced-rs/iced/tree/0.8/examples/events -/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.8/examples/game_of_life -/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.8/examples/pokedex -/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.8/examples/solar_system -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.8/examples/stopwatch -/// [`todos`]: https://github.com/iced-rs/iced/tree/0.8/examples/todos +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.9/examples +/// [`clock`]: https://github.com/iced-rs/iced/tree/0.9/examples/clock +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.9/examples/download_progress +/// [`events`]: https://github.com/iced-rs/iced/tree/0.9/examples/events +/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.9/examples/game_of_life +/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.9/examples/pokedex +/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.9/examples/solar_system +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.9/examples/stopwatch +/// [`todos`]: https://github.com/iced-rs/iced/tree/0.9/examples/todos /// [`Sandbox`]: crate::Sandbox /// [`Canvas`]: crate::widget::Canvas /// [PokéAPI]: https://pokeapi.co/ @@ -24,13 +24,13 @@ //! [scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui //! [Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee //! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md -//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.8/native +//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.9/native //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.8/wgpu -//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.8/winit +//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.9/wgpu +//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.9/winit //! [`dodrio`]: https://github.com/fitzgen/dodrio //! [web runtime]: https://github.com/iced-rs/iced_web -//! [examples]: https://github.com/iced-rs/iced/tree/0.8/examples +//! [examples]: https://github.com/iced-rs/iced/tree/0.9/examples //! [repository]: https://github.com/iced-rs/iced //! //! # Overview @@ -235,7 +235,7 @@ pub mod mouse { pub mod subscription { //! Listen to external events in your application. pub use iced_futures::subscription::{ - events, events_with, run, run_with_id, unfold, Subscription, + channel, events, events_with, run, run_with_id, unfold, Subscription, }; } diff --git a/src/sandbox.rs b/src/sandbox.rs index e8ed0f81..cca327b6 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -34,19 +34,19 @@ use crate::{Application, Command, Element, Error, Settings, Subscription}; /// - [`tour`], a simple UI tour that can run both on native platforms and the /// web! /// -/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.8/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool -/// [`counter`]: https://github.com/iced-rs/iced/tree/0.8/examples/counter -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry -/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid -/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.8/examples/progress_bar -/// [`styling`]: https://github.com/iced-rs/iced/tree/0.8/examples/styling -/// [`svg`]: https://github.com/iced-rs/iced/tree/0.8/examples/svg -/// [`tour`]: https://github.com/iced-rs/iced/tree/0.8/examples/tour +/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.9/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool +/// [`counter`]: https://github.com/iced-rs/iced/tree/0.9/examples/counter +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry +/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid +/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.9/examples/progress_bar +/// [`styling`]: https://github.com/iced-rs/iced/tree/0.9/examples/styling +/// [`svg`]: https://github.com/iced-rs/iced/tree/0.9/examples/svg +/// [`tour`]: https://github.com/iced-rs/iced/tree/0.9/examples/tour /// [`Canvas widget`]: crate::widget::Canvas /// [the overview]: index.html#overview -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/wgpu /// [`Svg` widget]: crate::widget::Svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// diff --git a/src/window/icon.rs b/src/window/icon.rs index 659d2b64..b67b2ea3 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -1,175 +1,66 @@ //! Attach an icon to the window of your application. -use std::fmt; +pub use crate::core::window::icon::*; + +use crate::core::window::icon; + use std::io; -#[cfg(feature = "image_rs")] +#[cfg(feature = "image")] use std::path::Path; -/// The icon of a window. -#[derive(Clone)] -pub struct Icon(iced_winit::winit::window::Icon); +/// Creates an icon from an image file. +/// +/// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> { + let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); -impl fmt::Debug for Icon { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Icon").field(&format_args!("_")).finish() - } + Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) } -impl Icon { - /// Creates an icon from 32bpp RGBA data. - pub fn from_rgba( - rgba: Vec<u8>, - width: u32, - height: u32, - ) -> Result<Self, Error> { - let raw = - iced_winit::winit::window::Icon::from_rgba(rgba, width, height)?; - - Ok(Icon(raw)) - } - - /// Creates an icon from an image file. - /// - /// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. - #[cfg(feature = "image_rs")] - pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Self, Error> { - let icon = image_rs::io::Reader::open(icon_path)?.decode()?.to_rgba8(); - - Self::from_rgba(icon.to_vec(), icon.width(), icon.height()) - } - - /// Creates an icon from the content of an image file. - /// - /// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. \ - /// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. - #[cfg(feature = "image_rs")] - pub fn from_file_data( - data: &[u8], - explicit_format: Option<image_rs::ImageFormat>, - ) -> Result<Self, Error> { - let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data)); - let icon_with_format = match explicit_format { - Some(format) => { - icon.set_format(format); - icon - } - None => icon.with_guessed_format()?, - }; +/// Creates an icon from the content of an image file. +/// +/// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro. +/// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime. +#[cfg(feature = "image")] +#[cfg_attr(docsrs, doc(cfg(feature = "image")))] +pub fn from_file_data( + data: &[u8], + explicit_format: Option<image_rs::ImageFormat>, +) -> Result<Icon, Error> { + let mut icon = image_rs::io::Reader::new(std::io::Cursor::new(data)); + let icon_with_format = match explicit_format { + Some(format) => { + icon.set_format(format); + icon + } + None => icon.with_guessed_format()?, + }; - let pixels = icon_with_format.decode()?.to_rgba8(); + let pixels = icon_with_format.decode()?.to_rgba8(); - Self::from_rgba(pixels.to_vec(), pixels.width(), pixels.height()) - } + Ok(icon::from_rgba( + pixels.to_vec(), + pixels.width(), + pixels.height(), + )?) } -/// An error produced when using `Icon::from_rgba` with invalid arguments. -#[derive(Debug)] +/// An error produced when creating an [`Icon`]. +#[derive(Debug, thiserror::Error)] pub enum Error { - /// The provided RGBA data isn't divisble by 4. - /// - /// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels. - InvalidData { - /// The length of the provided RGBA data. - byte_count: usize, - }, - - /// The number of RGBA pixels does not match the provided dimensions. - DimensionsMismatch { - /// The provided width. - width: u32, - /// The provided height. - height: u32, - /// The amount of pixels of the provided RGBA data. - pixel_count: usize, - }, + /// The [`Icon`] is not valid. + #[error("The icon is invalid: {0}")] + InvalidError(#[from] icon::Error), /// The underlying OS failed to create the icon. - OsError(io::Error), - - /// The `image` crate reported an error - #[cfg(feature = "image_rs")] - ImageError(image_rs::error::ImageError), -} - -impl From<std::io::Error> for Error { - fn from(os_error: std::io::Error) -> Self { - Error::OsError(os_error) - } -} - -impl From<iced_winit::winit::window::BadIcon> for Error { - fn from(error: iced_winit::winit::window::BadIcon) -> Self { - use iced_winit::winit::window::BadIcon; - - match error { - BadIcon::ByteCountNotDivisibleBy4 { byte_count } => { - Error::InvalidData { byte_count } - } - BadIcon::DimensionsVsPixelCount { - width, - height, - pixel_count, - .. - } => Error::DimensionsMismatch { - width, - height, - pixel_count, - }, - BadIcon::OsError(os_error) => Error::OsError(os_error), - } - } -} - -impl From<Icon> for iced_winit::winit::window::Icon { - fn from(icon: Icon) -> Self { - icon.0 - } -} - -#[cfg(feature = "image_rs")] -impl From<image_rs::error::ImageError> for Error { - fn from(image_error: image_rs::error::ImageError) -> Self { - Self::ImageError(image_error) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::InvalidData { byte_count } => { - write!( - f, - "The provided RGBA data (with length {byte_count:?}) isn't divisble by \ - 4. Therefore, it cannot be safely interpreted as 32bpp RGBA \ - pixels." - ) - } - Error::DimensionsMismatch { - width, - height, - pixel_count, - } => { - write!( - f, - "The number of RGBA pixels ({pixel_count:?}) does not match the provided \ - dimensions ({width:?}x{height:?})." - ) - } - Error::OsError(e) => write!( - f, - "The underlying OS failed to create the window \ - icon: {e:?}" - ), - #[cfg(feature = "image_rs")] - Error::ImageError(e) => { - write!(f, "Unable to create icon from a file: {e:?}") - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(self) - } + #[error("The underlying OS failted to create the window icon: {0}")] + OsError(#[from] io::Error), + + /// The `image` crate reported an error. + #[cfg(feature = "image")] + #[cfg_attr(docsrs, doc(cfg(feature = "image")))] + #[error("Unable to create icon from a file: {0}")] + ImageError(#[from] image_rs::error::ImageError), } diff --git a/style/Cargo.toml b/style/Cargo.toml index 3b54f1ec..0bb354e0 100644 --- a/style/Cargo.toml +++ b/style/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_style" -version = "0.7.0" +version = "0.8.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "The default set of styles of Iced" @@ -11,7 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [dependencies.iced_core] -version = "0.8" +version = "0.9" path = "../core" features = ["palette"] diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs index 64ed8462..b528c444 100644 --- a/style/src/scrollable.rs +++ b/style/src/scrollable.rs @@ -37,12 +37,16 @@ pub trait StyleSheet { /// Produces the style of an active scrollbar. fn active(&self, style: &Self::Style) -> Scrollbar; - /// Produces the style of a hovered scrollbar. - fn hovered(&self, style: &Self::Style) -> Scrollbar; + /// Produces the style of a scrollbar when the scrollable is being hovered. + fn hovered( + &self, + style: &Self::Style, + is_mouse_over_scrollbar: bool, + ) -> Scrollbar; /// Produces the style of a scrollbar that is being dragged. fn dragging(&self, style: &Self::Style) -> Scrollbar { - self.hovered(style) + self.hovered(style, true) } /// Produces the style of an active horizontal scrollbar. @@ -50,13 +54,17 @@ pub trait StyleSheet { self.active(style) } - /// Produces the style of a hovered horizontal scrollbar. - fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar { - self.hovered(style) + /// Produces the style of a horizontal scrollbar when the scrollable is being hovered. + fn hovered_horizontal( + &self, + style: &Self::Style, + is_mouse_over_scrollbar: bool, + ) -> Scrollbar { + self.hovered(style, is_mouse_over_scrollbar) } /// Produces the style of a horizontal scrollbar that is being dragged. fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar { - self.hovered_horizontal(style) + self.hovered_horizontal(style, true) } } diff --git a/style/src/slider.rs b/style/src/slider.rs index 4b52fad3..884d3871 100644 --- a/style/src/slider.rs +++ b/style/src/slider.rs @@ -5,11 +5,20 @@ use iced_core::Color; #[derive(Debug, Clone, Copy)] pub struct Appearance { /// The colors of the rail of the slider. - pub rail_colors: (Color, Color), + pub rail: Rail, /// The appearance of the [`Handle`] of the slider. pub handle: Handle, } +/// The appearance of a slider rail +#[derive(Debug, Clone, Copy)] +pub struct Rail { + /// The colors of the rail of the slider. + pub colors: (Color, Color), + /// The width of the stroke of a slider rail. + pub width: f32, +} + /// The appearance of the handle of a slider. #[derive(Debug, Clone, Copy)] pub struct Handle { diff --git a/style/src/text_input.rs b/style/src/text_input.rs index d97016dc..2616ad5a 100644 --- a/style/src/text_input.rs +++ b/style/src/text_input.rs @@ -12,6 +12,8 @@ pub struct Appearance { pub border_width: f32, /// The border [`Color`] of the text input. pub border_color: Color, + /// The icon [`Color`] of the text input. + pub icon_color: Color, } /// A set of rules that dictate the style of a text input. @@ -31,6 +33,9 @@ pub trait StyleSheet { /// Produces the [`Color`] of the value of a text input. fn value_color(&self, style: &Self::Style) -> Color; + /// Produces the [`Color`] of the value of a disabled text input. + fn disabled_color(&self, style: &Self::Style) -> Color; + /// Produces the [`Color`] of the selection of a text input. fn selection_color(&self, style: &Self::Style) -> Color; @@ -38,4 +43,7 @@ pub trait StyleSheet { fn hovered(&self, style: &Self::Style) -> Appearance { self.focused(style) } + + /// Produces the style of a disabled text input. + fn disabled(&self, style: &Self::Style) -> Appearance; } diff --git a/style/src/theme.rs b/style/src/theme.rs index b507d096..d9893bcf 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -416,10 +416,13 @@ impl slider::StyleSheet for Theme { }; slider::Appearance { - rail_colors: ( - palette.primary.base.color, - Color::TRANSPARENT, - ), + rail: slider::Rail { + colors: ( + palette.primary.base.color, + palette.primary.base.color, + ), + width: 2.0, + }, handle: slider::Handle { color: palette.background.base.color, border_color: palette.primary.base.color, @@ -906,31 +909,41 @@ impl scrollable::StyleSheet for Theme { } } - fn hovered(&self, style: &Self::Style) -> scrollable::Scrollbar { + fn hovered( + &self, + style: &Self::Style, + is_mouse_over_scrollbar: bool, + ) -> scrollable::Scrollbar { match style { Scrollable::Default => { - let palette = self.extended_palette(); + if is_mouse_over_scrollbar { + let palette = self.extended_palette(); - scrollable::Scrollbar { - background: palette.background.weak.color.into(), - border_radius: 2.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: palette.primary.strong.color, + scrollable::Scrollbar { + background: palette.background.weak.color.into(), border_radius: 2.0, border_width: 0.0, border_color: Color::TRANSPARENT, - }, + scroller: scrollable::Scroller { + color: palette.primary.strong.color, + border_radius: 2.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } else { + self.active(style) } } - Scrollable::Custom(custom) => custom.hovered(self), + Scrollable::Custom(custom) => { + custom.hovered(self, is_mouse_over_scrollbar) + } } } fn dragging(&self, style: &Self::Style) -> scrollable::Scrollbar { match style { - Scrollable::Default => self.hovered(style), + Scrollable::Default => self.hovered(style, true), Scrollable::Custom(custom) => custom.dragging(self), } } @@ -942,10 +955,16 @@ impl scrollable::StyleSheet for Theme { } } - fn hovered_horizontal(&self, style: &Self::Style) -> scrollable::Scrollbar { + fn hovered_horizontal( + &self, + style: &Self::Style, + is_mouse_over_scrollbar: bool, + ) -> scrollable::Scrollbar { match style { - Scrollable::Default => self.hovered(style), - Scrollable::Custom(custom) => custom.hovered_horizontal(self), + Scrollable::Default => self.hovered(style, is_mouse_over_scrollbar), + Scrollable::Custom(custom) => { + custom.hovered_horizontal(self, is_mouse_over_scrollbar) + } } } @@ -954,7 +973,7 @@ impl scrollable::StyleSheet for Theme { style: &Self::Style, ) -> scrollable::Scrollbar { match style { - Scrollable::Default => self.hovered_horizontal(style), + Scrollable::Default => self.hovered_horizontal(style, true), Scrollable::Custom(custom) => custom.dragging_horizontal(self), } } @@ -1012,6 +1031,7 @@ impl text_input::StyleSheet for Theme { border_radius: 2.0, border_width: 1.0, border_color: palette.background.strong.color, + icon_color: palette.background.weak.text, } } @@ -1027,6 +1047,7 @@ impl text_input::StyleSheet for Theme { border_radius: 2.0, border_width: 1.0, border_color: palette.background.base.text, + icon_color: palette.background.weak.text, } } @@ -1042,6 +1063,7 @@ impl text_input::StyleSheet for Theme { border_radius: 2.0, border_width: 1.0, border_color: palette.primary.strong.color, + icon_color: palette.background.weak.text, } } @@ -1074,4 +1096,28 @@ impl text_input::StyleSheet for Theme { palette.primary.weak.color } + + fn disabled(&self, style: &Self::Style) -> text_input::Appearance { + if let TextInput::Custom(custom) = style { + return custom.disabled(self); + } + + let palette = self.extended_palette(); + + text_input::Appearance { + background: palette.background.weak.color.into(), + border_radius: 2.0, + border_width: 1.0, + border_color: palette.background.strong.color, + icon_color: palette.background.strong.color, + } + } + + fn disabled_color(&self, style: &Self::Style) -> Color { + if let TextInput::Custom(custom) = style { + return custom.disabled_color(self); + } + + self.placeholder_color(style) + } } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 7a5f5b17..f629dab9 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -19,7 +19,7 @@ kurbo = "0.9" log = "0.4" [dependencies.iced_graphics] -version = "0.7" +version = "0.8" path = "../graphics" features = ["tiny-skia"] diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 36c891bb..4559040e 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_wgpu" -version = "0.9.0" +version = "0.10.0" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A wgpu renderer for Iced" @@ -38,7 +38,7 @@ version = "1.9" features = ["derive"] [dependencies.iced_graphics] -version = "0.7" +version = "0.8" path = "../graphics" [dependencies.glyphon] diff --git a/wgpu/README.md b/wgpu/README.md index 3e6af103..f8c88374 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -30,7 +30,7 @@ Currently, `iced_wgpu` supports the following primitives: Add `iced_wgpu` as a dependency in your `Cargo.toml`: ```toml -iced_wgpu = "0.9" +iced_wgpu = "0.10" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 4163e338..263bcfa2 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -205,7 +205,7 @@ impl Pipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("iced_wgpu::image::shader"), + label: Some("iced_wgpu image shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("shader/image.wgsl"), )), diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 473f3621..0b169140 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -16,7 +16,7 @@ //! - Meshes of triangles, useful to draw geometry freely. //! //! [Iced]: https://github.com/iced-rs/iced -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs //! [WebGPU API]: https://gpuweb.github.io/gpuweb/ //! [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index b55216d7..8fa7359e 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -48,7 +48,7 @@ impl Pipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("iced_wgpu::quad::shader"), + label: Some("iced_wgpu quad shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("shader/quad.wgsl"), )), diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 9fa521d7..0df8dd02 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -577,7 +577,7 @@ mod solid { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some( - "iced_wgpu::triangle::solid create shader module", + "iced_wgpu triangle solid create shader module", ), source: wgpu::ShaderSource::Wgsl( std::borrow::Cow::Borrowed(include_str!( diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 14522c89..4afbdb32 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -75,7 +75,7 @@ impl Blit { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("iced_wgpu::triangle::blit_shader"), + label: Some("iced_wgpu triangle blit_shader"), source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed( include_str!("../shader/blit.wgsl"), )), diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 025cd43a..54ab83ea 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -66,7 +66,19 @@ impl<Theme> Compositor<Theme> { log::info!("Selected: {:#?}", adapter.get_info()); let format = compatible_surface.as_ref().and_then(|surface| { - surface.get_capabilities(&adapter).formats.first().copied() + let capabilities = surface.get_capabilities(&adapter); + + capabilities + .formats + .iter() + .filter(|format| format.describe().srgb) + .copied() + .next() + .or_else(|| { + log::warn!("No sRGB format found!"); + + capabilities.formats.first().copied() + }) })?; log::info!("Selected format: {:?}", format); diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 4c23f3e8..40e4db37 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -16,7 +16,7 @@ num-traits = "0.2" thiserror = "1" [dependencies.iced_runtime] -version = "0.9" +version = "0.1" path = "../runtime" [dependencies.iced_renderer] @@ -24,7 +24,7 @@ version = "0.1" path = "../renderer" [dependencies.iced_style] -version = "0.7" +version = "0.8" path = "../style" [dependencies.ouroboros] diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index d1f886c6..6505cfdd 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -15,17 +15,6 @@ use crate::{Row, Text}; pub use iced_style::checkbox::{Appearance, StyleSheet}; -/// The icon in a [`Checkbox`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Icon<Font> { - /// Font that will be used to display the `code_point`, - pub font: Font, - /// The unicode code point that will be used as the icon. - pub code_point: char, - /// Font size of the content. - pub size: Option<f32>, -} - /// A box that can be checked. /// /// # Example @@ -321,3 +310,14 @@ where Element::new(checkbox) } } + +/// The icon in a [`Checkbox`]. +#[derive(Debug, Clone, PartialEq)] +pub struct Icon<Font> { + /// Font that will be used to display the `code_point`, + pub font: Font, + /// The unicode code point that will be used as the icon. + pub code_point: char, + /// Font size of the content. + pub size: Option<f32>, +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index a43e7248..336ac4ee 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -17,7 +17,7 @@ use crate::text::{self, Text}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; -use crate::{Column, Row, Space, VerticalSlider}; +use crate::{Column, MouseArea, Row, Space, VerticalSlider}; use std::borrow::Cow; use std::ops::RangeInclusive; @@ -163,7 +163,7 @@ where Renderer::Theme: radio::StyleSheet, V: Copy + Eq, { - Radio::new(value, label, selected, on_click) + Radio::new(label, value, selected, on_click) } /// Creates a new [`Toggler`]. @@ -187,14 +187,13 @@ where pub fn text_input<'a, Message, Renderer>( placeholder: &str, value: &str, - on_change: impl Fn(String) -> Message + 'a, ) -> TextInput<'a, Message, Renderer> where Message: Clone, Renderer: core::text::Renderer, Renderer::Theme: text_input::StyleSheet, { - TextInput::new(placeholder, value, on_change) + TextInput::new(placeholder, value) } /// Creates a new [`Slider`]. @@ -360,3 +359,13 @@ where { Command::widget(operation::focusable::focus_next()) } + +/// A container intercepting mouse events. +pub fn mouse_area<'a, Message, Renderer>( + widget: impl Into<Element<'a, Message, Renderer>>, +) -> MouseArea<'a, Message, Renderer> +where + Renderer: core::Renderer, +{ + MouseArea::new(widget) +} diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 0b8070af..49ae68af 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -13,6 +13,7 @@ use crate::core::{ use ouroboros::self_referencing; use std::cell::RefCell; use std::marker::PhantomData; +use std::rc::Rc; /// A reusable, custom widget that uses The Elm Architecture. /// @@ -58,6 +59,8 @@ pub trait Component<Message, Renderer> { } } +struct Tag<T>(T); + /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. pub fn view<'a, C, Message, Renderer>( @@ -79,11 +82,13 @@ where } .build(), )), + tree: RefCell::new(Rc::new(RefCell::new(None))), }) } struct Instance<'a, Message, Renderer, Event, S> { state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, + tree: RefCell<Rc<RefCell<Option<Tree>>>>, } #[self_referencing] @@ -100,40 +105,91 @@ struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> where - S: Default, + S: Default + 'static, + Renderer: renderer::Renderer, { - fn rebuild_element(&self, state: &S) { - let heads = self.state.borrow_mut().take().unwrap().into_heads(); + fn diff_self(&self) { + self.with_element(|element| { + self.tree + .borrow_mut() + .borrow_mut() + .as_mut() + .unwrap() + .diff_children(std::slice::from_ref(&element)); + }); + } - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| Some(component.view(state)), - } - .build(), - ); + fn rebuild_element_if_necessary(&self) { + let inner = self.state.borrow_mut().take().unwrap(); + if inner.borrow_element().is_none() { + let heads = inner.into_heads(); + + *self.state.borrow_mut() = Some( + StateBuilder { + component: heads.component, + message: PhantomData, + state: PhantomData, + element_builder: |component| { + Some( + component.view( + self.tree + .borrow() + .borrow() + .as_ref() + .unwrap() + .state + .downcast_ref::<S>(), + ), + ) + }, + } + .build(), + ); + self.diff_self(); + } else { + *self.state.borrow_mut() = Some(inner); + } } fn rebuild_element_with_operation( &self, - state: &mut S, operation: &mut dyn widget::Operation<Message>, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); - heads.component.operate(state, operation); + heads.component.operate( + self.tree + .borrow_mut() + .borrow_mut() + .as_mut() + .unwrap() + .state + .downcast_mut(), + operation, + ); *self.state.borrow_mut() = Some( StateBuilder { component: heads.component, message: PhantomData, state: PhantomData, - element_builder: |component| Some(component.view(state)), + element_builder: |component| { + Some( + component.view( + self.tree + .borrow() + .borrow() + .as_ref() + .unwrap() + .state + .downcast_ref(), + ), + ) + }, } .build(), ); + self.diff_self(); } fn with_element<T>( @@ -147,6 +203,7 @@ where &self, f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, ) -> T { + self.rebuild_element_if_necessary(); self.state .borrow_mut() .as_mut() @@ -162,24 +219,27 @@ where Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { - struct Tag<T>(T); tree::Tag::of::<Tag<S>>() } fn state(&self) -> tree::State { - tree::State::new(S::default()) + let state = Rc::new(RefCell::new(Some(Tree { + tag: tree::Tag::of::<Tag<S>>(), + state: tree::State::new(S::default()), + children: vec![Tree::empty()], + }))); + *self.tree.borrow_mut() = state.clone(); + tree::State::new(state) } fn children(&self) -> Vec<Tree> { - self.rebuild_element(&S::default()); - self.with_element(|element| vec![Tree::new(element)]) + vec![] } fn diff(&self, tree: &mut Tree) { - self.rebuild_element(tree.state.downcast_ref()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }) + let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); + *self.tree.borrow_mut() = tree.clone(); + self.rebuild_element_if_necessary(); } fn width(&self) -> Length { @@ -213,9 +273,10 @@ where let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); + let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); let event_status = self.with_element_mut(|element| { element.as_widget_mut().on_event( - &mut tree.children[0], + &mut t.borrow_mut().as_mut().unwrap().children[0], event, layout, cursor_position, @@ -235,9 +296,10 @@ where let mut heads = self.state.take().unwrap().into_heads(); for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(tree.state.downcast_mut::<S>(), message) + heads.component.update( + t.borrow_mut().as_mut().unwrap().state.downcast_mut(), + message, + ) }) { shell.publish(message); } @@ -247,17 +309,11 @@ where component: heads.component, message: PhantomData, state: PhantomData, - element_builder: |state| { - Some(state.view(tree.state.downcast_ref::<S>())) - }, + element_builder: |_| None, } .build(), )); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }); - shell.invalidate_layout(); } @@ -271,10 +327,7 @@ where renderer: &Renderer, operation: &mut dyn widget::Operation<Message>, ) { - self.rebuild_element_with_operation( - tree.state.downcast_mut(), - operation, - ); + self.rebuild_element_with_operation(operation); struct MapOperation<'a, B> { operation: &'a mut dyn widget::Operation<B>, @@ -308,13 +361,28 @@ where ) { self.operation.text_input(state, id); } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn custom( + &mut self, + state: &mut dyn std::any::Any, + id: Option<&widget::Id>, + ) { + self.operation.custom(state, id); + } } + let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)); - element.as_widget().operate( - &mut tree.children[0], + &mut tree.borrow_mut().as_mut().unwrap().children[0], layout, renderer, &mut MapOperation { operation }, @@ -332,9 +400,10 @@ where cursor_position: Point, viewport: &Rectangle, ) { + let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); self.with_element(|element| { element.as_widget().draw( - &tree.children[0], + &tree.borrow().as_ref().unwrap().children[0], renderer, theme, style, @@ -353,9 +422,10 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { + let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>(); self.with_element(|element| { element.as_widget().mouse_interaction( - &tree.children[0], + &tree.borrow().as_ref().unwrap().children[0], layout, cursor_position, viewport, @@ -370,25 +440,34 @@ where layout: Layout<'_>, renderer: &Renderer, ) -> Option<overlay::Element<'b, Message, Renderer>> { - let overlay = OverlayBuilder { - instance: self, - tree, - types: PhantomData, - overlay_builder: |instance, tree| { - instance.state.get_mut().as_mut().unwrap().with_element_mut( - move |element| { - element.as_mut().unwrap().as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - ) - }, - ) - }, - } - .build(); + self.rebuild_element_if_necessary(); + let tree = tree + .state + .downcast_mut::<Rc<RefCell<Option<Tree>>>>() + .borrow_mut() + .take() + .unwrap(); + let overlay = Overlay(Some( + InnerBuilder { + instance: self, + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance.state.get_mut().as_mut().unwrap().with_element_mut( + move |element| { + element.as_mut().unwrap().as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + ) + }, + ) + }, + } + .build(), + )); - let has_overlay = overlay.with_overlay(|overlay| { + let has_overlay = overlay.0.as_ref().unwrap().with_overlay(|overlay| { overlay.as_ref().map(overlay::Element::position) }); @@ -403,10 +482,24 @@ where } } +struct Overlay<'a, 'b, Message, Renderer, Event, S>( + Option<Inner<'a, 'b, Message, Renderer, Event, S>>, +); + +impl<'a, 'b, Message, Renderer, Event, S> Drop + for Overlay<'a, 'b, Message, Renderer, Event, S> +{ + fn drop(&mut self) { + if let Some(heads) = self.0.take().map(|inner| inner.into_heads()) { + *heads.instance.tree.borrow_mut().borrow_mut() = Some(heads.tree); + } + } +} + #[self_referencing] -struct Overlay<'a, 'b, Message, Renderer, Event, S> { +struct Inner<'a, 'b, Message, Renderer, Event, S> { instance: &'a mut Instance<'b, Message, Renderer, Event, S>, - tree: &'a mut Tree, + tree: Tree, types: PhantomData<(Message, Event, S)>, #[borrows(mut instance, mut tree)] @@ -428,6 +521,9 @@ impl<'a, 'b, Message, Renderer, Event, S> self.overlay .as_ref() .unwrap() + .0 + .as_ref() + .unwrap() .borrow_overlay() .as_ref() .map(f) @@ -440,6 +536,9 @@ impl<'a, 'b, Message, Renderer, Event, S> self.overlay .as_mut() .unwrap() + .0 + .as_mut() + .unwrap() .with_overlay_mut(|overlay| overlay.as_mut().map(f)) } } @@ -523,42 +622,37 @@ where local_shell.revalidate_layout(|| shell.invalidate_layout()); if !local_messages.is_empty() { - let overlay = self.overlay.take().unwrap().into_heads(); - let mut heads = overlay.instance.state.take().unwrap().into_heads(); + let mut inner = + self.overlay.take().unwrap().0.take().unwrap().into_heads(); + let mut heads = inner.instance.state.take().unwrap().into_heads(); for message in local_messages.into_iter().filter_map(|message| { heads .component - .update(overlay.tree.state.downcast_mut::<S>(), message) + .update(inner.tree.state.downcast_mut(), message) }) { shell.publish(message); } - *overlay.instance.state.borrow_mut() = Some( + *inner.instance.state.borrow_mut() = Some( StateBuilder { component: heads.component, message: PhantomData, state: PhantomData, - element_builder: |state| { - Some(state.view(overlay.tree.state.downcast_ref::<S>())) - }, + element_builder: |_| None, } .build(), ); - overlay.instance.with_element(|element| { - overlay.tree.diff_children(std::slice::from_ref(&element)) - }); - - self.overlay = Some( - OverlayBuilder { - instance: overlay.instance, - tree: overlay.tree, + self.overlay = Some(Overlay(Some( + InnerBuilder { + instance: inner.instance, + tree: inner.tree, types: PhantomData, overlay_builder: |_, _| None, } .build(), - ); + ))); shell.invalidate_layout(); } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index a3e7c8bc..904f62ad 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -21,6 +21,7 @@ pub use iced_runtime::core; pub use iced_style as style; mod column; +mod mouse_area; mod row; pub mod button; @@ -63,6 +64,8 @@ pub use column::Column; #[doc(no_inline)] pub use container::Container; #[doc(no_inline)] +pub use mouse_area::MouseArea; +#[doc(no_inline)] pub use pane_grid::PaneGrid; #[doc(no_inline)] pub use pick_list::PickList; diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs new file mode 100644 index 00000000..0232c494 --- /dev/null +++ b/widget/src/mouse_area.rs @@ -0,0 +1,311 @@ +//! A container for capturing mouse events. + +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::touch; +use crate::core::widget::{tree, Operation, Tree}; +use crate::core::{ + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Widget, +}; + +/// Emit messages on mouse events. +#[allow(missing_debug_implementations)] +pub struct MouseArea<'a, Message, Renderer> { + content: Element<'a, Message, Renderer>, + on_press: Option<Message>, + on_release: Option<Message>, + on_right_press: Option<Message>, + on_right_release: Option<Message>, + on_middle_press: Option<Message>, + on_middle_release: Option<Message>, +} + +impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { + /// The message to emit on a left button press. + #[must_use] + pub fn on_press(mut self, message: Message) -> Self { + self.on_press = Some(message); + self + } + + /// The message to emit on a left button release. + #[must_use] + pub fn on_release(mut self, message: Message) -> Self { + self.on_release = Some(message); + self + } + + /// The message to emit on a right button press. + #[must_use] + pub fn on_right_press(mut self, message: Message) -> Self { + self.on_right_press = Some(message); + self + } + + /// The message to emit on a right button release. + #[must_use] + pub fn on_right_release(mut self, message: Message) -> Self { + self.on_right_release = Some(message); + self + } + + /// The message to emit on a middle button press. + #[must_use] + pub fn on_middle_press(mut self, message: Message) -> Self { + self.on_middle_press = Some(message); + self + } + + /// The message to emit on a middle button release. + #[must_use] + pub fn on_middle_release(mut self, message: Message) -> Self { + self.on_middle_release = Some(message); + self + } +} + +/// Local state of the [`MouseArea`]. +#[derive(Default)] +struct State { + // TODO: Support on_mouse_enter and on_mouse_exit +} + +impl<'a, Message, Renderer> MouseArea<'a, Message, Renderer> { + /// Creates a [`MouseArea`] with the given content. + pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { + MouseArea { + content: content.into(), + on_press: None, + on_release: None, + on_right_press: None, + on_right_release: None, + on_middle_press: None, + on_middle_release: None, + } + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for MouseArea<'a, Message, Renderer> +where + Renderer: renderer::Renderer, + Message: Clone, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn width(&self) -> Length { + self.content.as_widget().width() + } + + fn height(&self) -> Length { + self.content.as_widget().height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget().layout(renderer, limits) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<Message>, + ) { + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + + 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 { + if let event::Status::Captured = self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) { + return event::Status::Captured; + } + + update(self, &event, layout, cursor_position, shell) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option<overlay::Element<'b, Message, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + ) + } +} + +impl<'a, Message, Renderer> From<MouseArea<'a, Message, Renderer>> + for Element<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + renderer::Renderer, +{ + fn from( + area: MouseArea<'a, Message, Renderer>, + ) -> Element<'a, Message, Renderer> { + Element::new(area) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`] +/// accordingly. +fn update<Message: Clone, Renderer>( + widget: &mut MouseArea<'_, Message, Renderer>, + event: &Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, +) -> event::Status { + if !layout.bounds().contains(cursor_position) { + return event::Status::Ignored; + } + + if let Some(message) = widget.on_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_release.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_right_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = + event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_right_release.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Right, + )) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_middle_press.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Middle, + )) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + if let Some(message) = widget.on_middle_release.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Middle, + )) = event + { + shell.publish(message.clone()); + + return event::Status::Captured; + } + } + + event::Status::Ignored +} diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 257c0144..67145e8e 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/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.8/examples/pane_grid +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid mod axis; mod configuration; mod content; diff --git a/widget/src/radio.rs b/widget/src/radio.rs index c2b6b017..c3229aed 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -22,10 +22,13 @@ pub use iced_style::radio::{Appearance, StyleSheet}; /// # type Radio<Message> = /// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; /// # +/// # use iced_widget::column; /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// pub enum Choice { /// A, /// B, +/// C, +/// All, /// } /// /// #[derive(Debug, Clone, Copy)] @@ -35,12 +38,36 @@ pub use iced_style::radio::{Appearance, StyleSheet}; /// /// let selected_choice = Some(Choice::A); /// -/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected); +/// let a = Radio::new( +/// "A", +/// Choice::A, +/// selected_choice, +/// Message::RadioSelected, +/// ); /// -/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected); -/// ``` +/// let b = Radio::new( +/// "B", +/// Choice::B, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let c = Radio::new( +/// "C", +/// Choice::C, +/// selected_choice, +/// Message::RadioSelected, +/// ); +/// +/// let all = Radio::new( +/// "All of the above", +/// Choice::All, +/// selected_choice, +/// Message::RadioSelected +/// ); /// -///  +/// let content = column![a, b, c, all]; +/// ``` #[allow(missing_debug_implementations)] pub struct Radio<Message, Renderer = crate::Renderer> where @@ -79,8 +106,8 @@ where /// * a function that will be called when the [`Radio`] is selected. It /// receives the value of the radio and must produce a `Message`. pub fn new<F, V>( - value: V, label: impl Into<String>, + value: V, selected: Option<V>, f: F, ) -> Self diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 5a7481f7..161ae664 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -27,6 +27,7 @@ where Renderer::Theme: StyleSheet, { id: Option<Id>, + width: Length, height: Length, vertical: Properties, horizontal: Option<Properties>, @@ -44,6 +45,7 @@ where pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { Scrollable { id: None, + width: Length::Shrink, height: Length::Shrink, vertical: Properties::default(), horizontal: None, @@ -59,6 +61,12 @@ where self } + /// Sets the width of the [`Scrollable`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + /// Sets the height of the [`Scrollable`]. pub fn height(mut self, height: impl Into<Length>) -> Self { self.height = height.into(); @@ -167,7 +175,7 @@ where } fn width(&self) -> Length { - self.content.as_widget().width() + self.width } fn height(&self) -> Length { @@ -182,7 +190,7 @@ where layout( renderer, limits, - Widget::<Message, Renderer>::width(self), + self.width, self.height, self.horizontal.is_some(), |renderer, limits| { @@ -391,15 +399,7 @@ pub fn layout<Renderer>( horizontal_enabled: bool, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, ) -> layout::Node { - let limits = limits - .max_height(f32::INFINITY) - .max_width(if horizontal_enabled { - f32::INFINITY - } else { - limits.max().width - }) - .width(width) - .height(height); + let limits = limits.width(width).height(height); let child_limits = layout::Limits::new( Size::new(limits.min().width, 0.0), @@ -851,8 +851,8 @@ pub fn draw<Renderer>( 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 if mouse_over_scrollable { + theme.hovered(style, mouse_over_y_scrollbar) } else { theme.active(style) }; @@ -864,8 +864,8 @@ pub fn draw<Renderer>( 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 if mouse_over_scrollable { + theme.hovered_horizontal(style, mouse_over_x_scrollbar) } else { theme.active_horizontal(style) }; @@ -889,7 +889,7 @@ pub fn draw<Renderer>( } fn notify_on_scroll<Message>( - state: &State, + state: &mut State, on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>, bounds: Rectangle, content_bounds: Rectangle, @@ -910,7 +910,23 @@ fn notify_on_scroll<Message>( .absolute(bounds.height, content_bounds.height) / (content_bounds.height - bounds.height); - shell.publish(on_scroll(RelativeOffset { x, y })) + let new_offset = RelativeOffset { x, y }; + + // Don't publish redundant offsets to shell + if let Some(prev_offset) = state.last_notified { + let unchanged = |a: f32, b: f32| { + (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) + }; + + if unchanged(prev_offset.x, new_offset.x) + && unchanged(prev_offset.y, new_offset.y) + { + return; + } + } + + shell.publish(on_scroll(new_offset)); + state.last_notified = Some(new_offset); } } @@ -923,6 +939,7 @@ pub struct State { offset_x: Offset, x_scroller_grabbed_at: Option<f32>, keyboard_modifiers: keyboard::Modifiers, + last_notified: Option<RelativeOffset>, } impl Default for State { @@ -934,6 +951,7 @@ impl Default for State { offset_x: Offset::Absolute(0.0), x_scroller_grabbed_at: None, keyboard_modifiers: keyboard::Modifiers::default(), + last_notified: None, } } } diff --git a/widget/src/slider.rs b/widget/src/slider.rs index e1153d2d..5a884e21 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -8,13 +8,15 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Widget, + Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, + Size, Widget, }; use std::ops::RangeInclusive; -pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; +pub use iced_style::slider::{ + Appearance, Handle, HandleShape, Rail, StyleSheet, +}; /// An horizontal bar and a handle that selects a single value from a range of /// values. @@ -366,38 +368,6 @@ pub fn draw<T, R>( style_sheet.active(style) }; - let rail_y = bounds.y + (bounds.height / 2.0).round(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y - 1.0, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail_colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y + 1.0, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(style.rail_colors.1), - ); - let (handle_width, handle_height, handle_border_radius) = match style .handle .shape @@ -416,17 +386,49 @@ pub fn draw<T, R>( (start.into() as f32, end.into() as f32) }; - let handle_offset = if range_start >= range_end { + let offset = if range_start >= range_end { 0.0 } else { - (bounds.width - handle_width) * (value - range_start) + (bounds.width - handle_width / 2.0) * (value - range_start) / (range_end - range_start) }; + let rail_y = bounds.y + bounds.height / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y - style.rail.width / 2.0, + width: offset + handle_width / 2.0, + height: style.rail.width, + }, + border_radius: Default::default(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + offset + handle_width / 2.0, + y: rail_y - style.rail.width / 2.0, + width: bounds.width - offset, + height: style.rail.width, + }, + border_radius: Default::default(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.1, + ); + renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + handle_offset.round(), + x: bounds.x + offset, y: rail_y - handle_height / 2.0, width: handle_width, height: handle_height, diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index d066109a..0faa51c0 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -49,8 +49,8 @@ pub use iced_style::text_input::{Appearance, StyleSheet}; /// let input = TextInput::new( /// "This is the placeholder...", /// value, -/// Message::TextInputChanged, /// ) +/// .on_input(Message::TextInputChanged) /// .padding(10); /// ``` ///  @@ -68,9 +68,10 @@ where width: Length, padding: Padding, size: Option<f32>, - on_change: Box<dyn Fn(String) -> Message + 'a>, + on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, + icon: Option<Icon<Renderer::Font>>, style: <Renderer::Theme as StyleSheet>::Style, } @@ -84,12 +85,8 @@ where /// /// It expects: /// - a placeholder, - /// - the current value, and - /// - a function that produces a message when the [`TextInput`] changes. - pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self - where - F: 'a + Fn(String) -> Message, - { + /// - the current value + pub fn new(placeholder: &str, value: &str) -> Self { TextInput { id: None, placeholder: String::from(placeholder), @@ -99,9 +96,10 @@ where width: Length::Fill, padding: Padding::new(5.0), size: None, - on_change: Box::new(on_change), + on_input: None, on_paste: None, on_submit: None, + icon: None, style: Default::default(), } } @@ -118,6 +116,25 @@ where self } + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`]. + /// + /// If this method is not called, the [`TextInput`] will be disabled. + pub fn on_input<F>(mut self, callback: F) -> Self + where + F: 'a + Fn(String) -> Message, + { + self.on_input = Some(Box::new(callback)); + self + } + + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed. + pub fn on_submit(mut self, message: Message) -> Self { + self.on_submit = Some(message); + self + } + /// Sets the message that should be produced when some text is pasted into /// the [`TextInput`]. pub fn on_paste( @@ -135,6 +152,13 @@ where self.font = Some(font); self } + + /// Sets the [`Icon`] of the [`TextInput`]. + pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self { + self.icon = Some(icon); + self + } + /// Sets the width of the [`TextInput`]. pub fn width(mut self, width: impl Into<Length>) -> Self { self.width = width.into(); @@ -153,13 +177,6 @@ where self } - /// Sets the message that should be produced when the [`TextInput`] is - /// focused and the enter key is pressed. - pub fn on_submit(mut self, message: Message) -> Self { - self.on_submit = Some(message); - self - } - /// Sets the style of the [`TextInput`]. pub fn style( mut self, @@ -192,7 +209,9 @@ where &self.placeholder, self.size, self.font, + self.on_input.is_none(), self.is_secure, + self.icon.as_ref(), &self.style, ) } @@ -213,6 +232,18 @@ where tree::State::new(State::new()) } + fn diff(&self, tree: &mut Tree) { + let state = tree.state.downcast_mut::<State>(); + + // Unfocus text input if it becomes disabled + if self.on_input.is_none() { + state.last_click = None; + state.is_focused = None; + state.is_pasting = None; + state.is_dragging = false; + } + } + fn width(&self) -> Length { self.width } @@ -226,7 +257,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout(renderer, limits, self.width, self.padding, self.size) + layout( + renderer, + limits, + self.width, + self.padding, + self.size, + self.icon.as_ref(), + ) } fn operate( @@ -263,7 +301,7 @@ where self.size, self.font, self.is_secure, - self.on_change.as_ref(), + self.on_input.as_deref(), self.on_paste.as_deref(), &self.on_submit, || tree.state.downcast_mut::<State>(), @@ -290,7 +328,9 @@ where &self.placeholder, self.size, self.font, + self.on_input.is_none(), self.is_secure, + self.icon.as_ref(), &self.style, ) } @@ -303,7 +343,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) + mouse_interaction(layout, cursor_position, self.on_input.is_none()) } } @@ -321,6 +361,30 @@ where } } +/// The content of the [`Icon`]. +#[derive(Debug, Clone)] +pub struct Icon<Font> { + /// The font that will be used to display the `code_point`. + pub font: Font, + /// The unicode code point that will be used as the icon. + pub code_point: char, + /// The font size of the content. + pub size: Option<f32>, + /// The spacing between the [`Icon`] and the text in a [`TextInput`]. + pub spacing: f32, + /// The side of a [`TextInput`] where to display the [`Icon`]. + pub side: Side, +} + +/// The side of a [`TextInput`]. +#[derive(Debug, Clone)] +pub enum Side { + /// The left side of a [`TextInput`]. + Left, + /// The right side of a [`TextInput`]. + Right, +} + /// The identifier of a [`TextInput`]. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Id(widget::Id); @@ -383,6 +447,7 @@ pub fn layout<Renderer>( width: Length, padding: Padding, size: Option<f32>, + icon: Option<&Icon<Renderer::Font>>, ) -> layout::Node where Renderer: text::Renderer, @@ -391,10 +456,51 @@ where let padding = padding.fit(Size::ZERO, limits.max()); let limits = limits.width(width).pad(padding).height(text_size * 1.2); - let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new(padding.left, padding.top)); + let text_bounds = limits.resolve(Size::ZERO); + + if let Some(icon) = icon { + let icon_width = renderer.measure_width( + &icon.code_point.to_string(), + icon.size.unwrap_or_else(|| renderer.default_size()), + icon.font, + ); + + let mut text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ); + + let mut icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)); + + match icon.side { + Side::Left => { + text_node.move_to(Point::new( + padding.left + icon_width + icon.spacing, + padding.top, + )); + + icon_node.move_to(Point::new(padding.left, padding.top)); + } + Side::Right => { + text_node.move_to(Point::new(padding.left, padding.top)); + + icon_node.move_to(Point::new( + padding.left + text_bounds.width - icon_width, + padding.top, + )); + } + }; + + layout::Node::with_children( + text_bounds.pad(padding), + vec![text_node, icon_node], + ) + } else { + let mut text = layout::Node::new(text_bounds); + text.move_to(Point::new(padding.left, padding.top)); - layout::Node::with_children(text.size().pad(padding), vec![text]) + layout::Node::with_children(text_bounds.pad(padding), vec![text]) + } } /// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] @@ -410,7 +516,7 @@ pub fn update<'a, Message, Renderer>( size: Option<f32>, font: Option<Renderer::Font>, is_secure: bool, - on_change: &dyn Fn(String) -> Message, + on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, on_submit: &Option<Message>, state: impl FnOnce() -> &'a mut State, @@ -423,7 +529,8 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - let is_clicked = layout.bounds().contains(cursor_position); + let is_clicked = + layout.bounds().contains(cursor_position) && on_input.is_some(); state.is_focused = if is_clicked { state.is_focused.or_else(|| { @@ -553,6 +660,8 @@ where let state = state(); if let Some(focus) = &mut state.is_focused { + let Some(on_input) = on_input else { return event::Status::Ignored }; + if state.is_pasting.is_none() && !state.keyboard_modifiers.command() && !c.is_control() @@ -561,7 +670,7 @@ where editor.insert(c); - let message = (on_change)(editor.contents()); + let message = (on_input)(editor.contents()); shell.publish(message); focus.updated_at = Instant::now(); @@ -574,6 +683,8 @@ where let state = state(); if let Some(focus) = &mut state.is_focused { + let Some(on_input) = on_input else { return event::Status::Ignored }; + let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -599,7 +710,7 @@ where let mut editor = Editor::new(value, &mut state.cursor); editor.backspace(); - let message = (on_change)(editor.contents()); + let message = (on_input)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Delete => { @@ -619,7 +730,7 @@ where let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (on_change)(editor.contents()); + let message = (on_input)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Left => { @@ -694,7 +805,7 @@ where let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (on_change)(editor.contents()); + let message = (on_input)(editor.contents()); shell.publish(message); } keyboard::KeyCode::V => { @@ -721,7 +832,7 @@ where let message = if let Some(paste) = &on_paste { (paste)(editor.contents()) } else { - (on_change)(editor.contents()) + (on_input)(editor.contents()) }; shell.publish(message); @@ -815,7 +926,9 @@ pub fn draw<Renderer>( placeholder: &str, size: Option<f32>, font: Option<Renderer::Font>, + is_disabled: bool, is_secure: bool, + icon: Option<&Icon<Renderer::Font>>, style: &<Renderer::Theme as StyleSheet>::Style, ) where Renderer: text::Renderer, @@ -825,11 +938,15 @@ pub fn draw<Renderer>( let value = secure_value.as_ref().unwrap_or(value); let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); let is_mouse_over = bounds.contains(cursor_position); - let appearance = if state.is_focused() { + let appearance = if is_disabled { + theme.disabled(style) + } else if state.is_focused() { theme.focused(style) } else if is_mouse_over { theme.hovered(style) @@ -847,6 +964,20 @@ pub fn draw<Renderer>( appearance.background, ); + if let Some(icon) = icon { + let icon_layout = children_layout.next().unwrap(); + + renderer.fill_text(Text { + content: &icon.code_point.to_string(), + size: icon.size.unwrap_or_else(|| renderer.default_size()), + font: icon.font, + color: appearance.icon_color, + bounds: icon_layout.bounds(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + }); + } + let text = value.to_string(); let font = font.unwrap_or_else(|| renderer.default_font()); let size = size.unwrap_or_else(|| renderer.default_size()); @@ -959,6 +1090,8 @@ pub fn draw<Renderer>( content: if text.is_empty() { placeholder } else { &text }, color: if text.is_empty() { theme.placeholder_color(style) + } else if is_disabled { + theme.disabled_color(style) } else { theme.value_color(style) }, @@ -987,9 +1120,14 @@ pub fn draw<Renderer>( pub fn mouse_interaction( layout: Layout<'_>, cursor_position: Point, + is_disabled: bool, ) -> mouse::Interaction { if layout.bounds().contains(cursor_position) { - mouse::Interaction::Text + if is_disabled { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } } else { mouse::Interaction::default() } diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 62dc997f..a7551aef 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -13,8 +13,8 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Length, Pixels, Point, Rectangle, - Shell, Size, Widget, + Clipboard, Color, Element, Length, Pixels, Point, Rectangle, Shell, Size, + Widget, }; /// An vertical bar and a handle that selects a single value from a range of @@ -366,38 +366,6 @@ pub fn draw<T, R>( style_sheet.active(style) }; - let rail_x = bounds.x + (bounds.width / 2.0).round(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x - 1.0, - y: bounds.y, - width: 2.0, - height: bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail_colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: rail_x + 1.0, - y: bounds.y, - width: 2.0, - height: bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(style.rail_colors.1), - ); - let (handle_width, handle_height, handle_border_radius) = match style .handle .shape @@ -416,18 +384,50 @@ pub fn draw<T, R>( (start.into() as f32, end.into() as f32) }; - let handle_offset = if range_start >= range_end { + let offset = if range_start >= range_end { 0.0 } else { - (bounds.height - handle_width) * (value - range_end) + (bounds.height - handle_width / 2.0) * (value - range_end) / (range_start - range_end) }; + let rail_x = bounds.x + bounds.width / 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y, + width: style.rail.width, + height: offset + handle_width / 2.0, + }, + border_radius: Default::default(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.1, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: rail_x - style.rail.width / 2.0, + y: bounds.y + offset + handle_width / 2.0, + width: style.rail.width, + height: bounds.height - offset, + }, + border_radius: Default::default(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail.colors.0, + ); + renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: rail_x - (handle_height / 2.0), - y: bounds.y + handle_offset.round(), + x: rail_x - handle_height / 2.0, + y: bounds.y + offset, width: handle_height, height: handle_width, }, diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 1bf63804..58e13b3e 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iced_winit" -version = "0.8.0" +version = "0.9.1" authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] edition = "2021" description = "A winit runtime for Iced" @@ -11,11 +11,16 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] +default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] trace = ["tracing", "tracing-core", "tracing-subscriber"] chrome-trace = ["trace", "tracing-chrome"] debug = ["iced_runtime/debug"] system = ["sysinfo"] application = [] +x11 = ["winit/x11"] +wayland = ["winit/wayland"] +wayland-dlopen = ["winit/wayland-dlopen"] +wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] [dependencies] window_clipboard = "0.2" @@ -26,17 +31,18 @@ thiserror = "1.0" version = "0.27" git = "https://github.com/iced-rs/winit.git" rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c" +default-features = false [dependencies.iced_runtime] -version = "0.9" +version = "0.1" path = "../runtime" [dependencies.iced_graphics] -version = "0.7" +version = "0.8" path = "../graphics" [dependencies.iced_style] -version = "0.7" +version = "0.8" path = "../style" [dependencies.tracing] diff --git a/winit/README.md b/winit/README.md index 83810473..91307970 100644 --- a/winit/README.md +++ b/winit/README.md @@ -20,7 +20,7 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t Add `iced_winit` as a dependency in your `Cargo.toml`: ```toml -iced_winit = "0.8" +iced_winit = "0.9" ``` __Iced moves fast and the `master` branch can contain breaking changes!__ If diff --git a/winit/src/application.rs b/winit/src/application.rs index 48f94235..3d7c6e5d 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -179,13 +179,17 @@ where .unwrap_or(None) }); - let _ = match target { - Some(node) => node - .replace_child(&canvas, &node) - .expect(&format!("Could not replace #{}", node.id())), - None => body - .append_child(&canvas) - .expect("Append canvas to HTML body"), + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1(&canvas) + .expect(&format!("Could not replace #{}", node.id())); + } + None => { + let _ = body + .append_child(&canvas) + .expect("Append canvas to HTML body"); + } }; } @@ -762,6 +766,9 @@ pub fn run_command<A, E>( mode, )); } + window::Action::ChangeIcon(icon) => { + window.set_window_icon(conversion::icon(icon)) + } window::Action::FetchMode(tag) => { let mode = if window.is_visible().unwrap_or(true) { conversion::mode(window.fullscreen()) diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index 0759ad9d..a9262184 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -1,7 +1,7 @@ //! Convert [`winit`] types into [`iced_native`] types, and viceversa. //! //! [`winit`]: https://github.com/rust-windowing/winit -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -219,7 +219,7 @@ pub fn mode(mode: Option<winit::window::Fullscreen>) -> window::Mode { /// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native pub fn mouse_interaction( interaction: mouse::Interaction, ) -> winit::window::CursorIcon { @@ -237,13 +237,14 @@ pub fn mouse_interaction( winit::window::CursorIcon::EwResize } Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, + Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed, } } /// Converts a `MouseButton` from [`winit`] to an [`iced_native`] mouse button. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { match mouse_button { winit::event::MouseButton::Left => mouse::Button::Left, @@ -259,7 +260,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button { /// modifiers state. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native pub fn modifiers( modifiers: winit::event::ModifiersState, ) -> keyboard::Modifiers { @@ -286,7 +287,7 @@ pub fn cursor_position( /// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native pub fn touch_event( touch: winit::event::Touch, scale_factor: f64, @@ -317,7 +318,7 @@ pub fn touch_event( /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// /// [`winit`]: https://github.com/rust-windowing/winit -/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +/// [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native pub fn key_code( virtual_keycode: winit::event::VirtualKeyCode, ) -> keyboard::KeyCode { @@ -510,6 +511,15 @@ pub fn user_attention( } } +/// Converts some [`Icon`] into it's `winit` counterpart. +/// +/// Returns `None` if there is an error during the conversion. +pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { + let (pixels, size) = icon.into_raw(); + + winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() +} + // As defined in: http://www.unicode.org/faq/private_use.html pub(crate) fn is_private_use_character(c: char) -> bool { matches!( diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 5cde510a..9f6bcebb 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -11,7 +11,7 @@ //! Additionally, a [`conversion`] module is available for users that decide to //! implement a custom event loop. //! -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native +//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.9/native //! [`winit`]: https://github.com/rust-windowing/winit //! [`conversion`]: crate::conversion #![doc( diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 5c64727b..be0ab329 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -22,6 +22,7 @@ mod platform; pub use platform::PlatformSpecific; use crate::conversion; +use crate::core::window::Icon; use crate::Position; use winit::monitor::MonitorHandle; @@ -84,7 +85,7 @@ pub struct Window { pub always_on_top: bool, /// The window icon, which is also usually used in the taskbar - pub icon: Option<winit::window::Icon>, + pub icon: Option<Icon>, /// Platform specific settings. pub platform_specific: platform::PlatformSpecific, @@ -126,8 +127,9 @@ impl Window { .with_resizable(self.resizable) .with_decorations(self.decorations) .with_transparent(self.transparent) - .with_window_icon(self.icon) - .with_always_on_top(self.always_on_top); + .with_window_icon(self.icon.and_then(conversion::icon)) + .with_always_on_top(self.always_on_top) + .with_visible(self.visible); if let Some(position) = conversion::position( primary_monitor.as_ref(), |