Merge branch 'master' into advanced-text

This commit is contained in:
Héctor Ramón Jiménez 2023-04-17 23:41:12 +02:00
commit 4bae457c37
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
73 changed files with 1586 additions and 703 deletions

View file

@ -6,6 +6,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.8.0] - 2023-02-18
### Added ### Added
- Generic pixel units. [#1711](https://github.com/iced-rs/iced/pull/1711) - Generic pixel units. [#1711](https://github.com/iced-rs/iced/pull/1711)
@ -414,7 +467,8 @@ Many thanks to...
### Added ### Added
- First release! :tada: - 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.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.7.0]: https://github.com/iced-rs/iced/compare/0.6.0...0.7.0
[0.6.0]: https://github.com/iced-rs/iced/compare/0.5.0...0.6.0 [0.6.0]: https://github.com/iced-rs/iced/compare/0.5.0...0.6.0

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced" name = "iced"
version = "0.8.0" version = "0.9.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "A cross-platform GUI library inspired by Elm" description = "A cross-platform GUI library inspired by Elm"
@ -61,11 +61,11 @@ members = [
] ]
[dependencies] [dependencies]
iced_core = { version = "0.8", path = "core" } iced_core = { version = "0.9", path = "core" }
iced_futures = { version = "0.6", path = "futures" } iced_futures = { version = "0.6", path = "futures" }
iced_renderer = { version = "0.1", path = "renderer" } iced_renderer = { version = "0.1", path = "renderer" }
iced_widget = { version = "0.1", path = "widget" } 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" thiserror = "1"
[dependencies.image_rs] [dependencies.image_rs]

View file

@ -68,7 +68,7 @@ __Iced is currently experimental software.__ [Take a look at the roadmap],
Add `iced` as a dependency in your `Cargo.toml`: Add `iced` as a dependency in your `Cargo.toml`:
```toml ```toml
iced = "0.8" iced = "0.9"
``` ```
If your project is using a Rust edition older than 2021, then you will need to 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 and then use it in your project with
```toml ```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, __NOTE:__ Chances are you have hardware that supports at least OpenGL 2.1 or OpenGL ES 2.0,

View file

@ -19,6 +19,7 @@ Once a step is completed, it is collapsed and added to this list:
* [x] Custom styling ([#146]) * [x] Custom styling ([#146])
* [x] Canvas for 2D graphics ([#193]) * [x] Canvas for 2D graphics ([#193])
* [x] Basic overlay support ([#444]) * [x] Basic overlay support ([#444])
* [x] Animations [#31]
[#24]: https://github.com/iced-rs/iced/issues/24 [#24]: https://github.com/iced-rs/iced/issues/24
[#25]: https://github.com/iced-rs/iced/issues/25 [#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 [#146]: https://github.com/iced-rs/iced/pull/146
[#193]: https://github.com/iced-rs/iced/pull/193 [#193]: https://github.com/iced-rs/iced/pull/193
[#444]: https://github.com/iced-rs/iced/pull/444 [#444]: https://github.com/iced-rs/iced/pull/444
[#31]: https://github.com/iced-rs/iced/issues/31
### Multi-window support ([#27]) ### Multi-window support ([#27])
Open and control multiple windows at runtime. 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 [#27]: https://github.com/iced-rs/iced/issues/27
### Animations ([#31]) ### Canvas widget for 3D graphics (~~[#32]~~ [#343])
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])
A widget to draw freely in 3D. It could be used to draw charts, implement a Paint clone, a CAD application, etc. 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. 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. In the long run, we could expose a renderer-agnostic abstraction to perform the drawing.
[#32]: https://github.com/iced-rs/iced/issues/32 [#32]: https://github.com/iced-rs/iced/issues/32
[#343] https://github.com/iced-rs/iced/issues/343
### Text shaping and font fallback ([#33]) ### 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]. [`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].

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced_core" name = "iced_core"
version = "0.8.1" version = "0.9.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "The essential concepts of Iced" description = "The essential concepts of Iced"

View file

@ -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`: Add `iced_core` as a dependency in your `Cargo.toml`:
```toml ```toml
iced_core = "0.8" iced_core = "0.9"
``` ```
__Iced moves fast and the `master` branch can contain breaking changes!__ If __Iced moves fast and the `master` branch can contain breaking changes!__ If

View file

@ -7,7 +7,7 @@
//! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true) //! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
//! //!
//! [Iced]: https://github.com/iced-rs/iced //! [Iced]: 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 //! [`iced_web`]: https://github.com/iced-rs/iced_web
#![doc( #![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"

View file

@ -12,4 +12,5 @@ pub enum Interaction {
Grabbing, Grabbing,
ResizingHorizontally, ResizingHorizontally,
ResizingVertically, ResizingVertically,
NotAllowed,
} }

View file

@ -33,12 +33,12 @@ use crate::{Clipboard, Length, Point, Rectangle, Shell};
/// - [`geometry`], a custom widget showcasing how to draw geometry with the /// - [`geometry`], a custom widget showcasing how to draw geometry with the
/// `Mesh2D` primitive in [`iced_wgpu`]. /// `Mesh2D` primitive in [`iced_wgpu`].
/// ///
/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples /// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples
/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool /// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool
/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget /// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget
/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry /// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry
/// [`lyon`]: https://github.com/nical/lyon /// [`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> pub trait Widget<Message, Renderer>
where where
Renderer: crate::Renderer, Renderer: crate::Renderer,

View file

@ -1,10 +1,13 @@
//! Build window-based GUI applications. //! Build window-based GUI applications.
pub mod icon;
mod event; mod event;
mod mode; mod mode;
mod redraw_request; mod redraw_request;
mod user_attention; mod user_attention;
pub use event::Event; pub use event::Event;
pub use icon::Icon;
pub use mode::Mode; pub use mode::Mode;
pub use redraw_request::RedrawRequest; pub use redraw_request::RedrawRequest;
pub use user_attention::UserAttention; pub use user_attention::UserAttention;

80
core/src/window/icon.rs Normal file
View file

@ -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,
},
}

52
docs/release_summary.py Normal file
View file

@ -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)

View file

@ -134,8 +134,8 @@ mod numeric_input {
.map(u32::to_string) .map(u32::to_string)
.as_deref() .as_deref()
.unwrap_or(""), .unwrap_or(""),
Event::InputChanged,
) )
.on_input(Event::InputChanged)
.padding(10), .padding(10),
button("+", Event::IncrementPressed), button("+", Event::IncrementPressed),
] ]

View file

@ -18,10 +18,7 @@ pub struct Download<I> {
url: String, url: String,
} }
async fn download<I: Copy>( async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
id: I,
state: State,
) -> (Option<(I, Progress)>, State) {
match state { match state {
State::Ready(url) => { State::Ready(url) => {
let response = reqwest::get(&url).await; let response = reqwest::get(&url).await;
@ -30,7 +27,7 @@ async fn download<I: Copy>(
Ok(response) => { Ok(response) => {
if let Some(total) = response.content_length() { if let Some(total) = response.content_length() {
( (
Some((id, Progress::Started)), (id, Progress::Started),
State::Downloading { State::Downloading {
response, response,
total, total,
@ -38,10 +35,10 @@ async fn download<I: Copy>(
}, },
) )
} else { } 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 { State::Downloading {
@ -55,7 +52,7 @@ async fn download<I: Copy>(
let percentage = (downloaded as f32 / total as f32) * 100.0; let percentage = (downloaded as f32 / total as f32) * 100.0;
( (
Some((id, Progress::Advanced(percentage))), (id, Progress::Advanced(percentage)),
State::Downloading { State::Downloading {
response, response,
total, total,
@ -63,8 +60,8 @@ async fn download<I: Copy>(
}, },
) )
} }
Ok(None) => (Some((id, Progress::Finished)), State::Finished), Ok(None) => ((id, Progress::Finished), State::Finished),
Err(_) => (Some((id, Progress::Errored)), State::Finished), Err(_) => ((id, Progress::Errored), State::Finished),
}, },
State::Finished => { State::Finished => {
// We do not let the stream die, as it would start a // We do not let the stream die, as it would start a

View file

@ -10,7 +10,7 @@ iced_winit = { path = "../../winit" }
iced_wgpu = { path = "../../wgpu" } iced_wgpu = { path = "../../wgpu" }
iced_widget = { path = "../../widget" } iced_widget = { path = "../../widget" }
iced_renderer = { path = "../../renderer", features = ["wgpu", "tiny-skia"] } iced_renderer = { path = "../../renderer", features = ["wgpu", "tiny-skia"] }
env_logger = "0.8" env_logger = "0.10"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"

View file

@ -102,11 +102,10 @@ impl Program for Controls {
.size(14) .size(14)
.style(Color::WHITE), .style(Color::WHITE),
) )
.push(text_input( .push(
"Placeholder", text_input("Placeholder", text)
text, .on_input(Message::TextChanged),
Message::TextChanged, ),
)),
), ),
) )
.into() .into()

View file

@ -30,6 +30,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let canvas_element = { let canvas_element = {
console_log::init_with_level(log::Level::Debug)?; console_log::init_with_level(log::Level::Debug)?;
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); std::panic::set_hook(Box::new(console_error_panic_hook::hook));
web_sys::window() web_sys::window()
@ -49,7 +50,7 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
.build(&event_loop)?; .build(&event_loop)?;
#[cfg(not(target_arch = "wasm32"))] #[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 physical_size = window.inner_size();
let mut viewport = Viewport::with_physical_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); let mut clipboard = Clipboard::connect(&window);
// Initialize wgpu // Initialize wgpu
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let default_backend = wgpu::Backends::GL; let default_backend = wgpu::Backends::GL;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -95,12 +95,16 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let needed_limits = wgpu::Limits::default(); let needed_limits = wgpu::Limits::default();
let capabilities = surface.get_capabilities(&adapter);
( (
surface capabilities
.get_capabilities(&adapter)
.formats .formats
.first() .iter()
.filter(|format| format.describe().srgb)
.copied() .copied()
.next()
.or_else(|| capabilities.formats.first().copied())
.expect("Get preferred format"), .expect("Get preferred format"),
adapter adapter
.request_device( .request_device(

View file

@ -213,12 +213,9 @@ impl Sandbox for App {
column![ column![
scrollable(options).height(Length::Fill), scrollable(options).height(Length::Fill),
row![ row![
text_input( text_input("Add a new option", &self.input)
"Add a new option", .on_input(Message::InputChanged)
&self.input, .on_submit(Message::AddItem(self.input.clone())),
Message::InputChanged,
)
.on_submit(Message::AddItem(self.input.clone())),
button(text(format!("Toggle Order ({})", self.order))) button(text(format!("Toggle Order ({})", self.order)))
.on_press(Message::ToggleOrder) .on_press(Message::ToggleOrder)
] ]

View file

@ -133,18 +133,16 @@ impl Application for App {
column![ column![
column![ column![
text("Email").size(12), text("Email").size(12),
text_input( text_input("abc@123.com", &self.email,)
"abc@123.com", .on_input(Message::Email)
&self.email, .on_submit(Message::Submit)
Message::Email .padding(5),
)
.on_submit(Message::Submit)
.padding(5),
] ]
.spacing(5), .spacing(5),
column![ column![
text("Password").size(12), text("Password").size(12),
text_input("", &self.password, Message::Password) text_input("", &self.password)
.on_input(Message::Password)
.on_submit(Message::Submit) .on_submit(Message::Submit)
.password() .password()
.padding(5), .padding(5),

View file

@ -49,13 +49,11 @@ impl Sandbox for QRGenerator {
.size(70) .size(70)
.style(Color::from([0.5, 0.5, 0.5])); .style(Color::from([0.5, 0.5, 0.5]));
let input = text_input( let input =
"Type the data of your QR code here...", text_input("Type the data of your QR code here...", &self.data)
&self.data, .on_input(Message::DataChanged)
Message::DataChanged, .size(30)
) .padding(15);
.size(30)
.padding(15);
let mut content = column![title, input] let mut content = column![title, input]
.width(700) .width(700)

View file

@ -338,22 +338,36 @@ impl scrollable::StyleSheet for ScrollbarCustomStyle {
style.active(&theme::Scrollable::Default) style.active(&theme::Scrollable::Default)
} }
fn hovered(&self, style: &Self::Style) -> Scrollbar { fn hovered(
style.hovered(&theme::Scrollable::Default) &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 { fn hovered_horizontal(
Scrollbar { &self,
background: style.active(&theme::Scrollable::default()).background, style: &Self::Style,
border_radius: 0.0, is_mouse_over_scrollbar: bool,
border_width: 0.0, ) -> Scrollbar {
border_color: Default::default(), if is_mouse_over_scrollbar {
scroller: Scroller { Scrollbar {
color: Color::from_rgb8(250, 85, 134), background: style
.active(&theme::Scrollable::default())
.background,
border_radius: 0.0, border_radius: 0.0,
border_width: 0.0, border_width: 0.0,
border_color: Default::default(), 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)
} }
} }
} }

View file

@ -90,13 +90,10 @@ impl Sandbox for Styling {
}, },
); );
let text_input = text_input( let text_input = text_input("Type something...", &self.input_value)
"Type something...", .on_input(Message::InputChanged)
&self.input_value, .padding(10)
Message::InputChanged, .size(20);
)
.padding(10)
.size(20);
let button = button("Submit") let button = button("Submit")
.padding(10) .padding(10)

View file

@ -119,13 +119,15 @@ impl Application for App {
column![ column![
subtitle( subtitle(
"Title", "Title",
text_input("", &self.editing.title, Message::Title) text_input("", &self.editing.title)
.on_input(Message::Title)
.on_submit(Message::Add) .on_submit(Message::Add)
.into() .into()
), ),
subtitle( subtitle(
"Message", "Message",
text_input("", &self.editing.body, Message::Body) text_input("", &self.editing.body)
.on_input(Message::Body)
.on_submit(Message::Add) .on_submit(Message::Add)
.into() .into()
), ),

View file

@ -210,15 +210,12 @@ impl Application for Todos {
.style(Color::from([0.5, 0.5, 0.5])) .style(Color::from([0.5, 0.5, 0.5]))
.horizontal_alignment(alignment::Horizontal::Center); .horizontal_alignment(alignment::Horizontal::Center);
let input = text_input( let input = text_input("What needs to be done?", input_value)
"What needs to be done?", .id(INPUT_ID.clone())
input_value, .on_input(Message::InputChanged)
Message::InputChanged, .on_submit(Message::CreateTask)
) .padding(15)
.id(INPUT_ID.clone()) .size(30);
.padding(15)
.size(30)
.on_submit(Message::CreateTask);
let controls = view_controls(tasks, *filter); let controls = view_controls(tasks, *filter);
let filtered_tasks = let filtered_tasks =
@ -381,14 +378,12 @@ impl Task {
.into() .into()
} }
TaskState::Editing => { TaskState::Editing => {
let text_input = text_input( let text_input =
"Describe your task...", text_input("Describe your task...", &self.description)
&self.description, .id(Self::text_input_id(i))
TaskMessage::DescriptionEdited, .on_input(TaskMessage::DescriptionEdited)
) .on_submit(TaskMessage::FinishEdition)
.id(Self::text_input_id(i)) .padding(10);
.on_submit(TaskMessage::FinishEdition)
.padding(10);
row![ row![
text_input, text_input,

View file

@ -5,7 +5,7 @@ use iced::widget::{
scrollable, slider, text, text_input, toggler, vertical_space, scrollable, slider, text, text_input, toggler, vertical_space,
}; };
use iced::widget::{Button, Column, Container, Slider}; 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 { pub fn main() -> iced::Result {
env_logger::init(); env_logger::init();
@ -127,6 +127,7 @@ impl Steps {
Step::TextInput { Step::TextInput {
value: String::new(), value: String::new(),
is_secure: false, is_secure: false,
is_showing_icon: false,
}, },
Step::Debugger, Step::Debugger,
Step::End, Step::End,
@ -171,14 +172,32 @@ impl Steps {
enum Step { enum Step {
Welcome, Welcome,
Slider { value: u8 }, Slider {
RowsAndColumns { layout: Layout, spacing: u16 }, value: u8,
Text { size: u16, color: Color }, },
Radio { selection: Option<Language> }, RowsAndColumns {
Toggler { can_continue: bool }, layout: Layout,
Image { width: u16 }, spacing: u16,
},
Text {
size: u16,
color: Color,
},
Radio {
selection: Option<Language>,
},
Toggler {
can_continue: bool,
},
Image {
width: u16,
},
Scrollable, Scrollable,
TextInput { value: String, is_secure: bool }, TextInput {
value: String,
is_secure: bool,
is_showing_icon: bool,
},
Debugger, Debugger,
End, End,
} }
@ -194,6 +213,7 @@ pub enum StepMessage {
ImageWidthChanged(u16), ImageWidthChanged(u16),
InputChanged(String), InputChanged(String),
ToggleSecureInput(bool), ToggleSecureInput(bool),
ToggleTextInputIcon(bool),
DebugToggled(bool), DebugToggled(bool),
TogglerChanged(bool), TogglerChanged(bool),
} }
@ -256,6 +276,14 @@ impl<'a> Step {
*can_continue = value; *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) Self::rows_and_columns(*layout, *spacing)
} }
Step::Scrollable => Self::scrollable(), Step::Scrollable => Self::scrollable(),
Step::TextInput { value, is_secure } => { Step::TextInput {
Self::text_input(value, *is_secure) value,
} is_secure,
is_showing_icon,
} => Self::text_input(value, *is_secure, *is_showing_icon),
Step::Debugger => Self::debugger(debug), Step::Debugger => Self::debugger(debug),
Step::End => Self::end(), Step::End => Self::end(),
} }
@ -530,14 +560,25 @@ impl<'a> Step {
) )
} }
fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> { fn text_input(
let text_input = text_input( value: &str,
"Type something to continue...", is_secure: bool,
value, is_showing_icon: bool,
StepMessage::InputChanged, ) -> Column<'a, StepMessage> {
) let mut text_input = text_input("Type something to continue...", value)
.padding(10) .on_input(StepMessage::InputChanged)
.size(30); .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") Self::container("Text input")
.push("Use a text input to ask for different kinds of information.") .push("Use a text input to ask for different kinds of information.")
@ -551,6 +592,11 @@ impl<'a> Step {
is_secure, is_secure,
StepMessage::ToggleSecureInput, StepMessage::ToggleSecureInput,
)) ))
.push(checkbox(
"Show icon",
is_showing_icon,
StepMessage::ToggleTextInputIcon,
))
.push( .push(
"A text input produces a message every time it changes. It is \ "A text input produces a message every time it changes. It is \
very easy to keep track of its contents:", very easy to keep track of its contents:",

View file

@ -13,63 +13,67 @@ use std::fmt;
pub fn connect() -> Subscription<Event> { pub fn connect() -> Subscription<Event> {
struct Connect; struct Connect;
subscription::unfold( subscription::channel(
std::any::TypeId::of::<Connect>(), std::any::TypeId::of::<Connect>(),
State::Disconnected, 100,
|state| async move { |mut output| async move {
match state { let mut state = State::Disconnected;
State::Disconnected => {
const ECHO_SERVER: &str = "ws://localhost:3030";
match async_tungstenite::tokio::connect_async(ECHO_SERVER) 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 .await
{ {
Ok((websocket, _)) => { Ok((websocket, _)) => {
let (sender, receiver) = mpsc::channel(100); let (sender, receiver) = mpsc::channel(100);
( let _ = output
Some(Event::Connected(Connection(sender))), .send(Event::Connected(Connection(sender)))
State::Connected(websocket, receiver), .await;
)
}
Err(_) => {
tokio::time::sleep(
tokio::time::Duration::from_secs(1),
)
.await;
(Some(Event::Disconnected), State::Disconnected) state = State::Connected(websocket, receiver);
} }
} Err(_) => {
} tokio::time::sleep(
State::Connected(mut websocket, mut input) => { tokio::time::Duration::from_secs(1),
let mut fused_websocket = websocket.by_ref().fuse(); )
.await;
futures::select! { let _ = output.send(Event::Disconnected).await;
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();
message = input.select_next_some() => { futures::select! {
let result = websocket.send(tungstenite::Message::Text(message.to_string())).await; 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;
if result.is_ok() { state = State::Disconnected;
(None, State::Connected(websocket, input)) }
} else { Ok(_) => continue,
(Some(Event::Disconnected), State::Disconnected) }
}
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;
state = State::Disconnected;
}
} }
} }
} }

View file

@ -125,12 +125,9 @@ impl Application for WebSocket {
}; };
let new_message_input = { let new_message_input = {
let mut input = text_input( let mut input = text_input("Type a message...", &self.new_message)
"Type a message...", .on_input(Message::NewMessageChanged)
&self.new_message, .padding(10);
Message::NewMessageChanged,
)
.padding(10);
let mut button = button( let mut button = button(
text("Send") text("Send")

View file

@ -17,7 +17,7 @@ thread-pool = ["futures/thread-pool"]
log = "0.4" log = "0.4"
[dependencies.iced_core] [dependencies.iced_core]
version = "0.8" version = "0.9"
path = "../core" path = "../core"
[dependencies.futures] [dependencies.futures]

View file

@ -9,6 +9,8 @@ use crate::core::Hasher;
use crate::futures::{Future, Stream}; use crate::futures::{Future, Stream};
use crate::{BoxStream, MaybeSend}; use crate::{BoxStream, MaybeSend};
use futures::channel::mpsc;
use futures::never::Never;
use std::hash::Hash; use std::hash::Hash;
/// A stream of runtime events. /// 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 /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how
/// to listen to time. /// to listen to time.
/// ///
/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples /// [examples]: https://github.com/iced-rs/iced/tree/0.9/examples
/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.8/examples/download_progress /// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.9/examples/download_progress
/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.8/examples/stopwatch /// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.9/examples/stopwatch
pub trait Recipe { pub trait Recipe {
/// The events that will be produced by a [`Subscription`] with this /// The events that will be produced by a [`Subscription`] with this
/// [`Recipe`]. /// [`Recipe`].
@ -317,6 +319,27 @@ where
/// [`Stream`] that will call the provided closure to produce every `Message`. /// [`Stream`] that will call the provided closure to produce every `Message`.
/// ///
/// The `id` will be used to uniquely identify the [`Subscription`]. /// 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 /// # Creating an asynchronous worker with bidirectional communication
/// You can leverage this helper to create a [`Subscription`] that spawns /// 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::subscription::{self, Subscription};
/// use iced_futures::futures; /// use iced_futures::futures::channel::mpsc;
/// /// use iced_futures::futures::sink::SinkExt;
/// use futures::channel::mpsc;
/// ///
/// pub enum Event { /// pub enum Event {
/// Ready(mpsc::Sender<Input>), /// Ready(mpsc::Sender<Input>),
@ -351,27 +373,35 @@ where
/// fn some_worker() -> Subscription<Event> { /// fn some_worker() -> Subscription<Event> {
/// struct SomeWorker; /// struct SomeWorker;
/// ///
/// subscription::unfold(std::any::TypeId::of::<SomeWorker>(), State::Starting, |state| async move { /// subscription::channel(std::any::TypeId::of::<SomeWorker>(), 100, |mut output| async move {
/// match state { /// let mut state = State::Starting;
/// State::Starting => {
/// // Create channel
/// let (sender, receiver) = mpsc::channel(100);
/// ///
/// (Some(Event::Ready(sender)), State::Ready(receiver)) /// loop {
/// } /// match &mut state {
/// State::Ready(mut receiver) => { /// State::Starting => {
/// use futures::StreamExt; /// // Create channel
/// let (sender, receiver) = mpsc::channel(100);
/// ///
/// // Read next input sent from `Application` /// // Send the sender back to the application
/// let input = receiver.select_next_some().await; /// output.send(Event::Ready(sender)).await;
/// ///
/// match input { /// // We are ready to receive messages
/// Input::DoSomeWork => { /// state = State::Ready(receiver);
/// // Do some async work... /// }
/// State::Ready(receiver) => {
/// use iced_futures::futures::StreamExt;
/// ///
/// // Finally, we can optionally return a message to tell the /// // Read next input sent from `Application`
/// // `Application` the work is done /// let input = receiver.select_next_some().await;
/// (Some(Event::WorkFinished), State::Ready(receiver)) ///
/// match input {
/// Input::DoSomeWork => {
/// // Do some async work...
///
/// // 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 /// Check out the [`websocket`] example, which showcases this pattern to maintain a WebSocket
/// connection open. /// connection open.
/// ///
/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.8/examples/websocket /// [`websocket`]: https://github.com/iced-rs/iced/tree/0.9/examples/websocket
pub fn unfold<I, T, Fut, Message>( pub fn channel<I, Fut, Message>(
id: I, id: I,
initial: T, size: usize,
mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, f: impl Fn(mpsc::Sender<Message>) -> Fut + MaybeSend + Sync + 'static,
) -> Subscription<Message> ) -> Subscription<Message>
where where
I: Hash + 'static, I: Hash + 'static,
T: MaybeSend + 'static, Fut: Future<Output = Never> + MaybeSend + 'static,
Fut: Future<Output = (Option<Message>, T)> + MaybeSend + 'static,
Message: 'static + MaybeSend, Message: 'static + MaybeSend,
{ {
use futures::future::{self, FutureExt}; use futures::stream::{self, StreamExt};
use futures::stream::StreamExt;
run_with_id( Subscription::from_recipe(Runner {
id, id,
futures::stream::unfold(initial, move |state| f(state).map(Some)) spawn: move |_| {
.filter_map(future::ready), let (sender, receiver) = mpsc::channel(size);
)
let runner = stream::once(f(sender)).map(|_| unreachable!());
stream::select(receiver, runner)
},
})
} }
struct Runner<I, F, S, Message> struct Runner<I, F, S, Message>

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced_graphics" name = "iced_graphics"
version = "0.7.0" version = "0.8.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "A bunch of backend-agnostic types that can be leveraged to build a renderer for Iced" 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"] features = ["derive"]
[dependencies.iced_core] [dependencies.iced_core]
version = "0.8" version = "0.9"
path = "../core" path = "../core"
[dependencies.tiny-skia] [dependencies.tiny-skia]

View file

@ -16,11 +16,11 @@ raw-window-handle = "0.5"
thiserror = "1" thiserror = "1"
[dependencies.iced_graphics] [dependencies.iced_graphics]
version = "0.7" version = "0.8"
path = "../graphics" path = "../graphics"
[dependencies.iced_wgpu] [dependencies.iced_wgpu]
version = "0.9" version = "0.10"
path = "../wgpu" path = "../wgpu"
optional = true optional = true

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced_runtime" name = "iced_runtime"
version = "0.9.1" version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "A renderer-agnostic library for native GUIs" description = "A renderer-agnostic library for native GUIs"
@ -14,7 +14,7 @@ debug = []
thiserror = "1" thiserror = "1"
[dependencies.iced_core] [dependencies.iced_core]
version = "0.8" version = "0.9"
path = "../core" path = "../core"
[dependencies.iced_futures] [dependencies.iced_futures]

View file

@ -12,13 +12,6 @@
[`druid`]: https://github.com/xi-editor/druid [`druid`]: https://github.com/xi-editor/druid
[`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle [`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 __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]. you want to learn about a specific release, check out [the release list].

View file

@ -23,8 +23,8 @@
//! - Build a new renderer, see the [renderer] module. //! - Build a new renderer, see the [renderer] module.
//! - Build a custom widget, start at the [`Widget`] trait. //! - Build a custom widget, start at the [`Widget`] trait.
//! //!
//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.8/core //! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.9/core
//! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.8/winit //! [`iced_winit`]: https://github.com/iced-rs/iced/tree/0.9/winit
//! [`druid`]: https://github.com/xi-editor/druid //! [`druid`]: https://github.com/xi-editor/druid
//! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle //! [`raw-window-handle`]: https://github.com/rust-windowing/raw-window-handle
//! [renderer]: crate::renderer //! [renderer]: crate::renderer

View file

@ -19,8 +19,8 @@ use crate::core::{Element, Layout, Shell};
/// The [`integration_opengl`] & [`integration_wgpu`] examples use a /// The [`integration_opengl`] & [`integration_wgpu`] examples use a
/// [`UserInterface`] to integrate Iced in an existing graphical application. /// [`UserInterface`] to integrate Iced in an existing graphical application.
/// ///
/// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_opengl /// [`integration_opengl`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration_opengl
/// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/examples/integration_wgpu /// [`integration_wgpu`]: https://github.com/iced-rs/iced/tree/0.9/examples/integration_wgpu
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct UserInterface<'a, Message, Renderer> { pub struct UserInterface<'a, Message, Renderer> {
root: Element<'a, Message, Renderer>, root: Element<'a, Message, Renderer>,

View file

@ -5,7 +5,7 @@ pub use action::Action;
use crate::command::{self, Command}; use crate::command::{self, Command};
use crate::core::time::Instant; use crate::core::time::Instant;
use crate::core::window::{Event, Mode, UserAttention}; use crate::core::window::{Event, Icon, Mode, UserAttention};
use crate::futures::subscription::{self, Subscription}; use crate::futures::subscription::{self, Subscription};
/// Subscribes to the frames of the window of the running application. /// Subscribes to the frames of the window of the running application.
@ -110,3 +110,8 @@ pub fn fetch_id<Message>(
) -> Command<Message> { ) -> Command<Message> {
Command::single(command::Action::Window(Action::FetchId(Box::new(f)))) 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)))
}

View file

@ -1,4 +1,4 @@
use crate::core::window::{Mode, UserAttention}; use crate::core::window::{Icon, Mode, UserAttention};
use crate::futures::MaybeSend; use crate::futures::MaybeSend;
use std::fmt; use std::fmt;
@ -78,6 +78,21 @@ pub enum Action<T> {
ChangeAlwaysOnTop(bool), ChangeAlwaysOnTop(bool),
/// Fetch an identifier unique to the window. /// Fetch an identifier unique to the window.
FetchId(Box<dyn FnOnce(u64) -> T + 'static>), 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> { impl<T> Action<T> {
@ -108,6 +123,7 @@ impl<T> Action<T> {
Action::ChangeAlwaysOnTop(on_top) Action::ChangeAlwaysOnTop(on_top)
} }
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))), 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})") write!(f, "Action::AlwaysOnTop({on_top})")
} }
Self::FetchId(_) => write!(f, "Action::FetchId"), Self::FetchId(_) => write!(f, "Action::FetchId"),
Self::ChangeIcon(_icon) => {
write!(f, "Action::ChangeIcon(icon)")
}
} }
} }
} }

View file

@ -39,15 +39,15 @@ pub use crate::style::application::{Appearance, StyleSheet};
/// to listen to time. /// to listen to time.
/// - [`todos`], a todos tracker inspired by [TodoMVC]. /// - [`todos`], a todos tracker inspired by [TodoMVC].
/// ///
/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.8/examples /// [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.8/examples/clock /// [`clock`]: https://github.com/iced-rs/iced/tree/0.9/examples/clock
/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.8/examples/download_progress /// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.9/examples/download_progress
/// [`events`]: https://github.com/iced-rs/iced/tree/0.8/examples/events /// [`events`]: https://github.com/iced-rs/iced/tree/0.9/examples/events
/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.8/examples/game_of_life /// [`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.8/examples/pokedex /// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.9/examples/pokedex
/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.8/examples/solar_system /// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.9/examples/solar_system
/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.8/examples/stopwatch /// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.9/examples/stopwatch
/// [`todos`]: https://github.com/iced-rs/iced/tree/0.8/examples/todos /// [`todos`]: https://github.com/iced-rs/iced/tree/0.9/examples/todos
/// [`Sandbox`]: crate::Sandbox /// [`Sandbox`]: crate::Sandbox
/// [`Canvas`]: crate::widget::Canvas /// [`Canvas`]: crate::widget::Canvas
/// [PokéAPI]: https://pokeapi.co/ /// [PokéAPI]: https://pokeapi.co/

View file

@ -24,13 +24,13 @@
//! [scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui //! [scrollables]: https://gfycat.com/perkybaggybaboon-rust-gui
//! [Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee //! [Debug overlay with performance metrics]: https://gfycat.com/incredibledarlingbee
//! [Modular ecosystem]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md //! [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 //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.8/wgpu //! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.9/wgpu
//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.8/winit //! [windowing shell]: https://github.com/iced-rs/iced/tree/0.9/winit
//! [`dodrio`]: https://github.com/fitzgen/dodrio //! [`dodrio`]: https://github.com/fitzgen/dodrio
//! [web runtime]: https://github.com/iced-rs/iced_web //! [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 //! [repository]: https://github.com/iced-rs/iced
//! //!
//! # Overview //! # Overview
@ -235,7 +235,7 @@ pub mod mouse {
pub mod subscription { pub mod subscription {
//! Listen to external events in your application. //! Listen to external events in your application.
pub use iced_futures::subscription::{ pub use iced_futures::subscription::{
events, events_with, run, run_with_id, unfold, Subscription, channel, events, events_with, run, run_with_id, unfold, Subscription,
}; };
} }

View file

@ -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 /// - [`tour`], a simple UI tour that can run both on native platforms and the
/// web! /// web!
/// ///
/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.8/examples /// [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.8/examples/bezier_tool /// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.9/examples/bezier_tool
/// [`counter`]: https://github.com/iced-rs/iced/tree/0.8/examples/counter /// [`counter`]: https://github.com/iced-rs/iced/tree/0.9/examples/counter
/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget /// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.9/examples/custom_widget
/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry /// [`geometry`]: https://github.com/iced-rs/iced/tree/0.9/examples/geometry
/// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid /// [`pane_grid`]: https://github.com/iced-rs/iced/tree/0.9/examples/pane_grid
/// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.8/examples/progress_bar /// [`progress_bar`]: https://github.com/iced-rs/iced/tree/0.9/examples/progress_bar
/// [`styling`]: https://github.com/iced-rs/iced/tree/0.8/examples/styling /// [`styling`]: https://github.com/iced-rs/iced/tree/0.9/examples/styling
/// [`svg`]: https://github.com/iced-rs/iced/tree/0.8/examples/svg /// [`svg`]: https://github.com/iced-rs/iced/tree/0.9/examples/svg
/// [`tour`]: https://github.com/iced-rs/iced/tree/0.8/examples/tour /// [`tour`]: https://github.com/iced-rs/iced/tree/0.9/examples/tour
/// [`Canvas widget`]: crate::widget::Canvas /// [`Canvas widget`]: crate::widget::Canvas
/// [the overview]: index.html#overview /// [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 /// [`Svg` widget]: crate::widget::Svg
/// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg /// [Ghostscript Tiger]: https://commons.wikimedia.org/wiki/File:Ghostscript_Tiger.svg
/// ///

View file

@ -1,175 +1,66 @@
//! Attach an icon to the window of your application. //! 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; use std::io;
#[cfg(feature = "image_rs")] #[cfg(feature = "image")]
use std::path::Path; use std::path::Path;
/// The icon of a window. /// Creates an icon from an image file.
#[derive(Clone)] ///
pub struct Icon(iced_winit::winit::window::Icon); /// 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 { Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Icon").field(&format_args!("_")).finish()
}
} }
impl Icon { /// Creates an icon from the content of an image file.
/// Creates an icon from 32bpp RGBA data. ///
pub fn from_rgba( /// This content can be included in your application at compile-time, e.g. using the `include_bytes!` macro.
rgba: Vec<u8>, /// You can pass an explicit file format. Otherwise, the file format will be guessed at runtime.
width: u32, #[cfg(feature = "image")]
height: u32, #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
) -> Result<Self, Error> { pub fn from_file_data(
let raw = data: &[u8],
iced_winit::winit::window::Icon::from_rgba(rgba, width, height)?; 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()?,
};
Ok(Icon(raw)) let pixels = icon_with_format.decode()?.to_rgba8();
}
/// Creates an icon from an image file. Ok(icon::from_rgba(
/// pixels.to_vec(),
/// This will return an error in case the file is missing at run-time. You may prefer [`Self::from_file_data`] instead. pixels.width(),
#[cfg(feature = "image_rs")] pixels.height(),
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()?,
};
let pixels = icon_with_format.decode()?.to_rgba8();
Self::from_rgba(pixels.to_vec(), pixels.width(), pixels.height())
}
} }
/// An error produced when using `Icon::from_rgba` with invalid arguments. /// An error produced when creating an [`Icon`].
#[derive(Debug)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
/// The provided RGBA data isn't divisble by 4. /// The [`Icon`] is not valid.
/// #[error("The icon is invalid: {0}")]
/// Therefore, it cannot be safely interpreted as 32bpp RGBA pixels. InvalidError(#[from] icon::Error),
InvalidData {
/// The length of the provided RGBA data.
byte_count: usize,
},
/// The number of RGBA pixels does not match the provided dimensions.
DimensionsMismatch {
/// The provided width.
width: u32,
/// The provided height.
height: u32,
/// The amount of pixels of the provided RGBA data.
pixel_count: usize,
},
/// The underlying OS failed to create the icon. /// The underlying OS failed to create the icon.
OsError(io::Error), #[error("The underlying OS failted to create the window icon: {0}")]
OsError(#[from] io::Error),
/// The `image` crate reported an error /// The `image` crate reported an error.
#[cfg(feature = "image_rs")] #[cfg(feature = "image")]
ImageError(image_rs::error::ImageError), #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
} #[error("Unable to create icon from a file: {0}")]
ImageError(#[from] 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)
}
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced_style" name = "iced_style"
version = "0.7.0" version = "0.8.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "The default set of styles of Iced" description = "The default set of styles of Iced"
@ -11,7 +11,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"] categories = ["gui"]
[dependencies.iced_core] [dependencies.iced_core]
version = "0.8" version = "0.9"
path = "../core" path = "../core"
features = ["palette"] features = ["palette"]

View file

@ -37,12 +37,16 @@ pub trait StyleSheet {
/// Produces the style of an active scrollbar. /// Produces the style of an active scrollbar.
fn active(&self, style: &Self::Style) -> Scrollbar; fn active(&self, style: &Self::Style) -> Scrollbar;
/// Produces the style of a hovered scrollbar. /// Produces the style of a scrollbar when the scrollable is being hovered.
fn hovered(&self, style: &Self::Style) -> Scrollbar; fn hovered(
&self,
style: &Self::Style,
is_mouse_over_scrollbar: bool,
) -> Scrollbar;
/// Produces the style of a scrollbar that is being dragged. /// Produces the style of a scrollbar that is being dragged.
fn dragging(&self, style: &Self::Style) -> Scrollbar { fn dragging(&self, style: &Self::Style) -> Scrollbar {
self.hovered(style) self.hovered(style, true)
} }
/// Produces the style of an active horizontal scrollbar. /// Produces the style of an active horizontal scrollbar.
@ -50,13 +54,17 @@ pub trait StyleSheet {
self.active(style) self.active(style)
} }
/// Produces the style of a hovered horizontal scrollbar. /// Produces the style of a horizontal scrollbar when the scrollable is being hovered.
fn hovered_horizontal(&self, style: &Self::Style) -> Scrollbar { fn hovered_horizontal(
self.hovered(style) &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. /// Produces the style of a horizontal scrollbar that is being dragged.
fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar { fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar {
self.hovered_horizontal(style) self.hovered_horizontal(style, true)
} }
} }

View file

@ -5,11 +5,20 @@ use iced_core::Color;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Appearance { pub struct Appearance {
/// The colors of the rail of the slider. /// The colors of the rail of the slider.
pub rail_colors: (Color, Color), pub rail: Rail,
/// The appearance of the [`Handle`] of the slider. /// The appearance of the [`Handle`] of the slider.
pub handle: Handle, 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. /// The appearance of the handle of a slider.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Handle { pub struct Handle {

View file

@ -12,6 +12,8 @@ pub struct Appearance {
pub border_width: f32, pub border_width: f32,
/// The border [`Color`] of the text input. /// The border [`Color`] of the text input.
pub border_color: Color, 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. /// 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. /// Produces the [`Color`] of the value of a text input.
fn value_color(&self, style: &Self::Style) -> Color; 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. /// Produces the [`Color`] of the selection of a text input.
fn selection_color(&self, style: &Self::Style) -> Color; fn selection_color(&self, style: &Self::Style) -> Color;
@ -38,4 +43,7 @@ pub trait StyleSheet {
fn hovered(&self, style: &Self::Style) -> Appearance { fn hovered(&self, style: &Self::Style) -> Appearance {
self.focused(style) self.focused(style)
} }
/// Produces the style of a disabled text input.
fn disabled(&self, style: &Self::Style) -> Appearance;
} }

View file

@ -416,10 +416,13 @@ impl slider::StyleSheet for Theme {
}; };
slider::Appearance { slider::Appearance {
rail_colors: ( rail: slider::Rail {
palette.primary.base.color, colors: (
Color::TRANSPARENT, palette.primary.base.color,
), palette.primary.base.color,
),
width: 2.0,
},
handle: slider::Handle { handle: slider::Handle {
color: palette.background.base.color, color: palette.background.base.color,
border_color: palette.primary.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 { match style {
Scrollable::Default => { Scrollable::Default => {
let palette = self.extended_palette(); if is_mouse_over_scrollbar {
let palette = self.extended_palette();
scrollable::Scrollbar { scrollable::Scrollbar {
background: palette.background.weak.color.into(), 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_radius: 2.0,
border_width: 0.0, border_width: 0.0,
border_color: Color::TRANSPARENT, 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 { fn dragging(&self, style: &Self::Style) -> scrollable::Scrollbar {
match style { match style {
Scrollable::Default => self.hovered(style), Scrollable::Default => self.hovered(style, true),
Scrollable::Custom(custom) => custom.dragging(self), 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 { match style {
Scrollable::Default => self.hovered(style), Scrollable::Default => self.hovered(style, is_mouse_over_scrollbar),
Scrollable::Custom(custom) => custom.hovered_horizontal(self), Scrollable::Custom(custom) => {
custom.hovered_horizontal(self, is_mouse_over_scrollbar)
}
} }
} }
@ -954,7 +973,7 @@ impl scrollable::StyleSheet for Theme {
style: &Self::Style, style: &Self::Style,
) -> scrollable::Scrollbar { ) -> scrollable::Scrollbar {
match style { match style {
Scrollable::Default => self.hovered_horizontal(style), Scrollable::Default => self.hovered_horizontal(style, true),
Scrollable::Custom(custom) => custom.dragging_horizontal(self), Scrollable::Custom(custom) => custom.dragging_horizontal(self),
} }
} }
@ -1012,6 +1031,7 @@ impl text_input::StyleSheet for Theme {
border_radius: 2.0, border_radius: 2.0,
border_width: 1.0, border_width: 1.0,
border_color: palette.background.strong.color, 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_radius: 2.0,
border_width: 1.0, border_width: 1.0,
border_color: palette.background.base.text, 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_radius: 2.0,
border_width: 1.0, border_width: 1.0,
border_color: palette.primary.strong.color, 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 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)
}
} }

View file

@ -19,7 +19,7 @@ kurbo = "0.9"
log = "0.4" log = "0.4"
[dependencies.iced_graphics] [dependencies.iced_graphics]
version = "0.7" version = "0.8"
path = "../graphics" path = "../graphics"
features = ["tiny-skia"] features = ["tiny-skia"]

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced_wgpu" name = "iced_wgpu"
version = "0.9.0" version = "0.10.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "A wgpu renderer for Iced" description = "A wgpu renderer for Iced"
@ -38,7 +38,7 @@ version = "1.9"
features = ["derive"] features = ["derive"]
[dependencies.iced_graphics] [dependencies.iced_graphics]
version = "0.7" version = "0.8"
path = "../graphics" path = "../graphics"
[dependencies.glyphon] [dependencies.glyphon]

View file

@ -30,7 +30,7 @@ Currently, `iced_wgpu` supports the following primitives:
Add `iced_wgpu` as a dependency in your `Cargo.toml`: Add `iced_wgpu` as a dependency in your `Cargo.toml`:
```toml ```toml
iced_wgpu = "0.9" iced_wgpu = "0.10"
``` ```
__Iced moves fast and the `master` branch can contain breaking changes!__ If __Iced moves fast and the `master` branch can contain breaking changes!__ If

View file

@ -205,7 +205,7 @@ impl Pipeline {
let shader = let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor { 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( source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("shader/image.wgsl"), include_str!("shader/image.wgsl"),
)), )),

View file

@ -16,7 +16,7 @@
//! - Meshes of triangles, useful to draw geometry freely. //! - Meshes of triangles, useful to draw geometry freely.
//! //!
//! [Iced]: https://github.com/iced-rs/iced //! [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 //! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
//! [WebGPU API]: https://gpuweb.github.io/gpuweb/ //! [WebGPU API]: https://gpuweb.github.io/gpuweb/
//! [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph //! [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph

View file

@ -48,7 +48,7 @@ impl Pipeline {
let shader = let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor { 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( source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("shader/quad.wgsl"), include_str!("shader/quad.wgsl"),
)), )),

View file

@ -577,7 +577,7 @@ mod solid {
let shader = let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor { device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some( label: Some(
"iced_wgpu::triangle::solid create shader module", "iced_wgpu triangle solid create shader module",
), ),
source: wgpu::ShaderSource::Wgsl( source: wgpu::ShaderSource::Wgsl(
std::borrow::Cow::Borrowed(include_str!( std::borrow::Cow::Borrowed(include_str!(

View file

@ -75,7 +75,7 @@ impl Blit {
let shader = let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor { 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( source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shader/blit.wgsl"), include_str!("../shader/blit.wgsl"),
)), )),

View file

@ -66,7 +66,19 @@ impl<Theme> Compositor<Theme> {
log::info!("Selected: {:#?}", adapter.get_info()); log::info!("Selected: {:#?}", adapter.get_info());
let format = compatible_surface.as_ref().and_then(|surface| { 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); log::info!("Selected format: {:?}", format);

View file

@ -16,7 +16,7 @@ num-traits = "0.2"
thiserror = "1" thiserror = "1"
[dependencies.iced_runtime] [dependencies.iced_runtime]
version = "0.9" version = "0.1"
path = "../runtime" path = "../runtime"
[dependencies.iced_renderer] [dependencies.iced_renderer]
@ -24,7 +24,7 @@ version = "0.1"
path = "../renderer" path = "../renderer"
[dependencies.iced_style] [dependencies.iced_style]
version = "0.7" version = "0.8"
path = "../style" path = "../style"
[dependencies.ouroboros] [dependencies.ouroboros]

View file

@ -15,17 +15,6 @@ use crate::{Row, Text};
pub use iced_style::checkbox::{Appearance, StyleSheet}; 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. /// A box that can be checked.
/// ///
/// # Example /// # Example
@ -321,3 +310,14 @@ where
Element::new(checkbox) 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>,
}

View file

@ -17,7 +17,7 @@ use crate::text::{self, Text};
use crate::text_input::{self, TextInput}; use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler}; use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip}; use crate::tooltip::{self, Tooltip};
use crate::{Column, Row, Space, VerticalSlider}; use crate::{Column, MouseArea, Row, Space, VerticalSlider};
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
@ -163,7 +163,7 @@ where
Renderer::Theme: radio::StyleSheet, Renderer::Theme: radio::StyleSheet,
V: Copy + Eq, V: Copy + Eq,
{ {
Radio::new(value, label, selected, on_click) Radio::new(label, value, selected, on_click)
} }
/// Creates a new [`Toggler`]. /// Creates a new [`Toggler`].
@ -187,14 +187,13 @@ where
pub fn text_input<'a, Message, Renderer>( pub fn text_input<'a, Message, Renderer>(
placeholder: &str, placeholder: &str,
value: &str, value: &str,
on_change: impl Fn(String) -> Message + 'a,
) -> TextInput<'a, Message, Renderer> ) -> TextInput<'a, Message, Renderer>
where where
Message: Clone, Message: Clone,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
Renderer::Theme: text_input::StyleSheet, Renderer::Theme: text_input::StyleSheet,
{ {
TextInput::new(placeholder, value, on_change) TextInput::new(placeholder, value)
} }
/// Creates a new [`Slider`]. /// Creates a new [`Slider`].
@ -360,3 +359,13 @@ where
{ {
Command::widget(operation::focusable::focus_next()) 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)
}

View file

@ -13,6 +13,7 @@ use crate::core::{
use ouroboros::self_referencing; use ouroboros::self_referencing;
use std::cell::RefCell; use std::cell::RefCell;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::rc::Rc;
/// A reusable, custom widget that uses The Elm Architecture. /// 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 /// Turns an implementor of [`Component`] into an [`Element`] that can be
/// embedded in any application. /// embedded in any application.
pub fn view<'a, C, Message, Renderer>( pub fn view<'a, C, Message, Renderer>(
@ -79,11 +82,13 @@ where
} }
.build(), .build(),
)), )),
tree: RefCell::new(Rc::new(RefCell::new(None))),
}) })
} }
struct Instance<'a, Message, Renderer, Event, S> { struct Instance<'a, Message, Renderer, Event, S> {
state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>, state: RefCell<Option<State<'a, Message, Renderer, Event, S>>>,
tree: RefCell<Rc<RefCell<Option<Tree>>>>,
} }
#[self_referencing] #[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> impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S>
where where
S: Default, S: Default + 'static,
Renderer: renderer::Renderer,
{ {
fn rebuild_element(&self, state: &S) { fn diff_self(&self) {
let heads = self.state.borrow_mut().take().unwrap().into_heads(); 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( fn rebuild_element_if_necessary(&self) {
StateBuilder { let inner = self.state.borrow_mut().take().unwrap();
component: heads.component, if inner.borrow_element().is_none() {
message: PhantomData, let heads = inner.into_heads();
state: PhantomData,
element_builder: |component| Some(component.view(state)), *self.state.borrow_mut() = Some(
} StateBuilder {
.build(), 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( fn rebuild_element_with_operation(
&self, &self,
state: &mut S,
operation: &mut dyn widget::Operation<Message>, operation: &mut dyn widget::Operation<Message>,
) { ) {
let heads = self.state.borrow_mut().take().unwrap().into_heads(); 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( *self.state.borrow_mut() = Some(
StateBuilder { StateBuilder {
component: heads.component, component: heads.component,
message: PhantomData, message: PhantomData,
state: 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(), .build(),
); );
self.diff_self();
} }
fn with_element<T>( fn with_element<T>(
@ -147,6 +203,7 @@ where
&self, &self,
f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T,
) -> T { ) -> T {
self.rebuild_element_if_necessary();
self.state self.state
.borrow_mut() .borrow_mut()
.as_mut() .as_mut()
@ -162,24 +219,27 @@ where
Renderer: core::Renderer, Renderer: core::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
tree::Tag::of::<Tag<S>>() tree::Tag::of::<Tag<S>>()
} }
fn state(&self) -> tree::State { 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> { fn children(&self) -> Vec<Tree> {
self.rebuild_element(&S::default()); vec![]
self.with_element(|element| vec![Tree::new(element)])
} }
fn diff(&self, tree: &mut Tree) { fn diff(&self, tree: &mut Tree) {
self.rebuild_element(tree.state.downcast_ref()); let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| { *self.tree.borrow_mut() = tree.clone();
tree.diff_children(std::slice::from_ref(&element)) self.rebuild_element_if_necessary();
})
} }
fn width(&self) -> Length { fn width(&self) -> Length {
@ -213,9 +273,10 @@ where
let mut local_messages = Vec::new(); let mut local_messages = Vec::new();
let mut local_shell = Shell::new(&mut local_messages); 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| { let event_status = self.with_element_mut(|element| {
element.as_widget_mut().on_event( element.as_widget_mut().on_event(
&mut tree.children[0], &mut t.borrow_mut().as_mut().unwrap().children[0],
event, event,
layout, layout,
cursor_position, cursor_position,
@ -235,9 +296,10 @@ where
let mut heads = self.state.take().unwrap().into_heads(); let mut heads = self.state.take().unwrap().into_heads();
for message in local_messages.into_iter().filter_map(|message| { for message in local_messages.into_iter().filter_map(|message| {
heads heads.component.update(
.component t.borrow_mut().as_mut().unwrap().state.downcast_mut(),
.update(tree.state.downcast_mut::<S>(), message) message,
)
}) { }) {
shell.publish(message); shell.publish(message);
} }
@ -247,17 +309,11 @@ where
component: heads.component, component: heads.component,
message: PhantomData, message: PhantomData,
state: PhantomData, state: PhantomData,
element_builder: |state| { element_builder: |_| None,
Some(state.view(tree.state.downcast_ref::<S>()))
},
} }
.build(), .build(),
)); ));
self.with_element(|element| {
tree.diff_children(std::slice::from_ref(&element))
});
shell.invalidate_layout(); shell.invalidate_layout();
} }
@ -271,10 +327,7 @@ where
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>, operation: &mut dyn widget::Operation<Message>,
) { ) {
self.rebuild_element_with_operation( self.rebuild_element_with_operation(operation);
tree.state.downcast_mut(),
operation,
);
struct MapOperation<'a, B> { struct MapOperation<'a, B> {
operation: &'a mut dyn widget::Operation<B>, operation: &'a mut dyn widget::Operation<B>,
@ -308,13 +361,28 @@ where
) { ) {
self.operation.text_input(state, id); 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| { self.with_element(|element| {
tree.diff_children(std::slice::from_ref(&element));
element.as_widget().operate( element.as_widget().operate(
&mut tree.children[0], &mut tree.borrow_mut().as_mut().unwrap().children[0],
layout, layout,
renderer, renderer,
&mut MapOperation { operation }, &mut MapOperation { operation },
@ -332,9 +400,10 @@ where
cursor_position: Point, cursor_position: Point,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| { self.with_element(|element| {
element.as_widget().draw( element.as_widget().draw(
&tree.children[0], &tree.borrow().as_ref().unwrap().children[0],
renderer, renderer,
theme, theme,
style, style,
@ -353,9 +422,10 @@ where
viewport: &Rectangle, viewport: &Rectangle,
renderer: &Renderer, renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
let tree = tree.state.downcast_ref::<Rc<RefCell<Option<Tree>>>>();
self.with_element(|element| { self.with_element(|element| {
element.as_widget().mouse_interaction( element.as_widget().mouse_interaction(
&tree.children[0], &tree.borrow().as_ref().unwrap().children[0],
layout, layout,
cursor_position, cursor_position,
viewport, viewport,
@ -370,25 +440,34 @@ where
layout: Layout<'_>, layout: Layout<'_>,
renderer: &Renderer, renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Renderer>> { ) -> Option<overlay::Element<'b, Message, Renderer>> {
let overlay = OverlayBuilder { self.rebuild_element_if_necessary();
instance: self, let tree = tree
tree, .state
types: PhantomData, .downcast_mut::<Rc<RefCell<Option<Tree>>>>()
overlay_builder: |instance, tree| { .borrow_mut()
instance.state.get_mut().as_mut().unwrap().with_element_mut( .take()
move |element| { .unwrap();
element.as_mut().unwrap().as_widget_mut().overlay( let overlay = Overlay(Some(
&mut tree.children[0], InnerBuilder {
layout, instance: self,
renderer, 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(
.build(); &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) 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] #[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>, instance: &'a mut Instance<'b, Message, Renderer, Event, S>,
tree: &'a mut Tree, tree: Tree,
types: PhantomData<(Message, Event, S)>, types: PhantomData<(Message, Event, S)>,
#[borrows(mut instance, mut tree)] #[borrows(mut instance, mut tree)]
@ -426,6 +519,9 @@ impl<'a, 'b, Message, Renderer, Event, S>
f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T,
) -> Option<T> { ) -> Option<T> {
self.overlay self.overlay
.as_ref()
.unwrap()
.0
.as_ref() .as_ref()
.unwrap() .unwrap()
.borrow_overlay() .borrow_overlay()
@ -438,6 +534,9 @@ impl<'a, 'b, Message, Renderer, Event, S>
f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T,
) -> Option<T> { ) -> Option<T> {
self.overlay self.overlay
.as_mut()
.unwrap()
.0
.as_mut() .as_mut()
.unwrap() .unwrap()
.with_overlay_mut(|overlay| overlay.as_mut().map(f)) .with_overlay_mut(|overlay| overlay.as_mut().map(f))
@ -523,42 +622,37 @@ where
local_shell.revalidate_layout(|| shell.invalidate_layout()); local_shell.revalidate_layout(|| shell.invalidate_layout());
if !local_messages.is_empty() { if !local_messages.is_empty() {
let overlay = self.overlay.take().unwrap().into_heads(); let mut inner =
let mut heads = overlay.instance.state.take().unwrap().into_heads(); 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| { for message in local_messages.into_iter().filter_map(|message| {
heads heads
.component .component
.update(overlay.tree.state.downcast_mut::<S>(), message) .update(inner.tree.state.downcast_mut(), message)
}) { }) {
shell.publish(message); shell.publish(message);
} }
*overlay.instance.state.borrow_mut() = Some( *inner.instance.state.borrow_mut() = Some(
StateBuilder { StateBuilder {
component: heads.component, component: heads.component,
message: PhantomData, message: PhantomData,
state: PhantomData, state: PhantomData,
element_builder: |state| { element_builder: |_| None,
Some(state.view(overlay.tree.state.downcast_ref::<S>()))
},
} }
.build(), .build(),
); );
overlay.instance.with_element(|element| { self.overlay = Some(Overlay(Some(
overlay.tree.diff_children(std::slice::from_ref(&element)) InnerBuilder {
}); instance: inner.instance,
tree: inner.tree,
self.overlay = Some(
OverlayBuilder {
instance: overlay.instance,
tree: overlay.tree,
types: PhantomData, types: PhantomData,
overlay_builder: |_, _| None, overlay_builder: |_, _| None,
} }
.build(), .build(),
); )));
shell.invalidate_layout(); shell.invalidate_layout();
} }

View file

@ -21,6 +21,7 @@ pub use iced_runtime::core;
pub use iced_style as style; pub use iced_style as style;
mod column; mod column;
mod mouse_area;
mod row; mod row;
pub mod button; pub mod button;
@ -63,6 +64,8 @@ pub use column::Column;
#[doc(no_inline)] #[doc(no_inline)]
pub use container::Container; pub use container::Container;
#[doc(no_inline)] #[doc(no_inline)]
pub use mouse_area::MouseArea;
#[doc(no_inline)]
pub use pane_grid::PaneGrid; pub use pane_grid::PaneGrid;
#[doc(no_inline)] #[doc(no_inline)]
pub use pick_list::PickList; pub use pick_list::PickList;

311
widget/src/mouse_area.rs Normal file
View file

@ -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
}

View file

@ -6,7 +6,7 @@
//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing,
//! drag and drop, and hotkey support. //! 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 axis;
mod configuration; mod configuration;
mod content; mod content;

View file

@ -22,10 +22,13 @@ pub use iced_style::radio::{Appearance, StyleSheet};
/// # type Radio<Message> = /// # type Radio<Message> =
/// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>; /// # iced_widget::Radio<Message, iced_widget::renderer::Renderer<iced_widget::style::Theme>>;
/// # /// #
/// # use iced_widget::column;
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// pub enum Choice { /// pub enum Choice {
/// A, /// A,
/// B, /// B,
/// C,
/// All,
/// } /// }
/// ///
/// #[derive(Debug, Clone, Copy)] /// #[derive(Debug, Clone, Copy)]
@ -35,12 +38,36 @@ pub use iced_style::radio::{Appearance, StyleSheet};
/// ///
/// let selected_choice = Some(Choice::A); /// 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];
/// ``` /// ```
///
/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true)
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Radio<Message, Renderer = crate::Renderer> pub struct Radio<Message, Renderer = crate::Renderer>
where where
@ -79,8 +106,8 @@ where
/// * a function that will be called when the [`Radio`] is selected. It /// * a function that will be called when the [`Radio`] is selected. It
/// receives the value of the radio and must produce a `Message`. /// receives the value of the radio and must produce a `Message`.
pub fn new<F, V>( pub fn new<F, V>(
value: V,
label: impl Into<String>, label: impl Into<String>,
value: V,
selected: Option<V>, selected: Option<V>,
f: F, f: F,
) -> Self ) -> Self

View file

@ -27,6 +27,7 @@ where
Renderer::Theme: StyleSheet, Renderer::Theme: StyleSheet,
{ {
id: Option<Id>, id: Option<Id>,
width: Length,
height: Length, height: Length,
vertical: Properties, vertical: Properties,
horizontal: Option<Properties>, horizontal: Option<Properties>,
@ -44,6 +45,7 @@ where
pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self { pub fn new(content: impl Into<Element<'a, Message, Renderer>>) -> Self {
Scrollable { Scrollable {
id: None, id: None,
width: Length::Shrink,
height: Length::Shrink, height: Length::Shrink,
vertical: Properties::default(), vertical: Properties::default(),
horizontal: None, horizontal: None,
@ -59,6 +61,12 @@ where
self 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`]. /// Sets the height of the [`Scrollable`].
pub fn height(mut self, height: impl Into<Length>) -> Self { pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into(); self.height = height.into();
@ -167,7 +175,7 @@ where
} }
fn width(&self) -> Length { fn width(&self) -> Length {
self.content.as_widget().width() self.width
} }
fn height(&self) -> Length { fn height(&self) -> Length {
@ -182,7 +190,7 @@ where
layout( layout(
renderer, renderer,
limits, limits,
Widget::<Message, Renderer>::width(self), self.width,
self.height, self.height,
self.horizontal.is_some(), self.horizontal.is_some(),
|renderer, limits| { |renderer, limits| {
@ -391,15 +399,7 @@ pub fn layout<Renderer>(
horizontal_enabled: bool, horizontal_enabled: bool,
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
) -> layout::Node { ) -> layout::Node {
let limits = limits let limits = limits.width(width).height(height);
.max_height(f32::INFINITY)
.max_width(if horizontal_enabled {
f32::INFINITY
} else {
limits.max().width
})
.width(width)
.height(height);
let child_limits = layout::Limits::new( let child_limits = layout::Limits::new(
Size::new(limits.min().width, 0.0), Size::new(limits.min().width, 0.0),
@ -851,8 +851,8 @@ pub fn draw<Renderer>(
if let Some(scrollbar) = scrollbars.y { if let Some(scrollbar) = scrollbars.y {
let style = if state.y_scroller_grabbed_at.is_some() { let style = if state.y_scroller_grabbed_at.is_some() {
theme.dragging(style) theme.dragging(style)
} else if mouse_over_y_scrollbar { } else if mouse_over_scrollable {
theme.hovered(style) theme.hovered(style, mouse_over_y_scrollbar)
} else { } else {
theme.active(style) theme.active(style)
}; };
@ -864,8 +864,8 @@ pub fn draw<Renderer>(
if let Some(scrollbar) = scrollbars.x { if let Some(scrollbar) = scrollbars.x {
let style = if state.x_scroller_grabbed_at.is_some() { let style = if state.x_scroller_grabbed_at.is_some() {
theme.dragging_horizontal(style) theme.dragging_horizontal(style)
} else if mouse_over_x_scrollbar { } else if mouse_over_scrollable {
theme.hovered_horizontal(style) theme.hovered_horizontal(style, mouse_over_x_scrollbar)
} else { } else {
theme.active_horizontal(style) theme.active_horizontal(style)
}; };
@ -889,7 +889,7 @@ pub fn draw<Renderer>(
} }
fn notify_on_scroll<Message>( fn notify_on_scroll<Message>(
state: &State, state: &mut State,
on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>, on_scroll: &Option<Box<dyn Fn(RelativeOffset) -> Message + '_>>,
bounds: Rectangle, bounds: Rectangle,
content_bounds: Rectangle, content_bounds: Rectangle,
@ -910,7 +910,23 @@ fn notify_on_scroll<Message>(
.absolute(bounds.height, content_bounds.height) .absolute(bounds.height, content_bounds.height)
/ (content_bounds.height - 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, offset_x: Offset,
x_scroller_grabbed_at: Option<f32>, x_scroller_grabbed_at: Option<f32>,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
last_notified: Option<RelativeOffset>,
} }
impl Default for State { impl Default for State {
@ -934,6 +951,7 @@ impl Default for State {
offset_x: Offset::Absolute(0.0), offset_x: Offset::Absolute(0.0),
x_scroller_grabbed_at: None, x_scroller_grabbed_at: None,
keyboard_modifiers: keyboard::Modifiers::default(), keyboard_modifiers: keyboard::Modifiers::default(),
last_notified: None,
} }
} }
} }

View file

@ -8,13 +8,15 @@ use crate::core::renderer;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell,
Rectangle, Shell, Size, Widget, Size, Widget,
}; };
use std::ops::RangeInclusive; 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 /// An horizontal bar and a handle that selects a single value from a range of
/// values. /// values.
@ -366,38 +368,6 @@ pub fn draw<T, R>(
style_sheet.active(style) 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 let (handle_width, handle_height, handle_border_radius) = match style
.handle .handle
.shape .shape
@ -416,17 +386,49 @@ pub fn draw<T, R>(
(start.into() as f32, end.into() as f32) (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 0.0
} else { } else {
(bounds.width - handle_width) * (value - range_start) (bounds.width - handle_width / 2.0) * (value - range_start)
/ (range_end - range_start) / (range_end - range_start)
}; };
let rail_y = bounds.y + bounds.height / 2.0;
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: Rectangle {
x: bounds.x + handle_offset.round(), 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 + offset,
y: rail_y - handle_height / 2.0, y: rail_y - handle_height / 2.0,
width: handle_width, width: handle_width,
height: handle_height, height: handle_height,

View file

@ -49,8 +49,8 @@ pub use iced_style::text_input::{Appearance, StyleSheet};
/// let input = TextInput::new( /// let input = TextInput::new(
/// "This is the placeholder...", /// "This is the placeholder...",
/// value, /// value,
/// Message::TextInputChanged,
/// ) /// )
/// .on_input(Message::TextInputChanged)
/// .padding(10); /// .padding(10);
/// ``` /// ```
/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true) /// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true)
@ -68,9 +68,10 @@ where
width: Length, width: Length,
padding: Padding, padding: Padding,
size: Option<f32>, 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_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>, on_submit: Option<Message>,
icon: Option<Icon<Renderer::Font>>,
style: <Renderer::Theme as StyleSheet>::Style, style: <Renderer::Theme as StyleSheet>::Style,
} }
@ -84,12 +85,8 @@ where
/// ///
/// It expects: /// It expects:
/// - a placeholder, /// - a placeholder,
/// - the current value, and /// - the current value
/// - a function that produces a message when the [`TextInput`] changes. pub fn new(placeholder: &str, value: &str) -> Self {
pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self
where
F: 'a + Fn(String) -> Message,
{
TextInput { TextInput {
id: None, id: None,
placeholder: String::from(placeholder), placeholder: String::from(placeholder),
@ -99,9 +96,10 @@ where
width: Length::Fill, width: Length::Fill,
padding: Padding::new(5.0), padding: Padding::new(5.0),
size: None, size: None,
on_change: Box::new(on_change), on_input: None,
on_paste: None, on_paste: None,
on_submit: None, on_submit: None,
icon: None,
style: Default::default(), style: Default::default(),
} }
} }
@ -118,6 +116,25 @@ where
self 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 /// Sets the message that should be produced when some text is pasted into
/// the [`TextInput`]. /// the [`TextInput`].
pub fn on_paste( pub fn on_paste(
@ -135,6 +152,13 @@ where
self.font = Some(font); self.font = Some(font);
self 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`]. /// Sets the width of the [`TextInput`].
pub fn width(mut self, width: impl Into<Length>) -> Self { pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into(); self.width = width.into();
@ -153,13 +177,6 @@ where
self 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`]. /// Sets the style of the [`TextInput`].
pub fn style( pub fn style(
mut self, mut self,
@ -192,7 +209,9 @@ where
&self.placeholder, &self.placeholder,
self.size, self.size,
self.font, self.font,
self.on_input.is_none(),
self.is_secure, self.is_secure,
self.icon.as_ref(),
&self.style, &self.style,
) )
} }
@ -213,6 +232,18 @@ where
tree::State::new(State::new()) 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 { fn width(&self) -> Length {
self.width self.width
} }
@ -226,7 +257,14 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> 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( fn operate(
@ -263,7 +301,7 @@ where
self.size, self.size,
self.font, self.font,
self.is_secure, self.is_secure,
self.on_change.as_ref(), self.on_input.as_deref(),
self.on_paste.as_deref(), self.on_paste.as_deref(),
&self.on_submit, &self.on_submit,
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State>(),
@ -290,7 +328,9 @@ where
&self.placeholder, &self.placeholder,
self.size, self.size,
self.font, self.font,
self.on_input.is_none(),
self.is_secure, self.is_secure,
self.icon.as_ref(),
&self.style, &self.style,
) )
} }
@ -303,7 +343,7 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
_renderer: &Renderer, _renderer: &Renderer,
) -> mouse::Interaction { ) -> 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`]. /// The identifier of a [`TextInput`].
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Id(widget::Id); pub struct Id(widget::Id);
@ -383,6 +447,7 @@ pub fn layout<Renderer>(
width: Length, width: Length,
padding: Padding, padding: Padding,
size: Option<f32>, size: Option<f32>,
icon: Option<&Icon<Renderer::Font>>,
) -> layout::Node ) -> layout::Node
where where
Renderer: text::Renderer, Renderer: text::Renderer,
@ -391,10 +456,51 @@ where
let padding = padding.fit(Size::ZERO, limits.max()); let padding = padding.fit(Size::ZERO, limits.max());
let limits = limits.width(width).pad(padding).height(text_size * 1.2); let limits = limits.width(width).pad(padding).height(text_size * 1.2);
let mut text = layout::Node::new(limits.resolve(Size::ZERO)); let text_bounds = limits.resolve(Size::ZERO);
text.move_to(Point::new(padding.left, padding.top));
layout::Node::with_children(text.size().pad(padding), vec![text]) 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_bounds.pad(padding), vec![text])
}
} }
/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] /// Processes an [`Event`] and updates the [`State`] of a [`TextInput`]
@ -410,7 +516,7 @@ pub fn update<'a, Message, Renderer>(
size: Option<f32>, size: Option<f32>,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
is_secure: bool, is_secure: bool,
on_change: &dyn Fn(String) -> Message, on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>, on_submit: &Option<Message>,
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
@ -423,7 +529,8 @@ where
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state(); 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 = if is_clicked {
state.is_focused.or_else(|| { state.is_focused.or_else(|| {
@ -553,6 +660,8 @@ where
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { 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() if state.is_pasting.is_none()
&& !state.keyboard_modifiers.command() && !state.keyboard_modifiers.command()
&& !c.is_control() && !c.is_control()
@ -561,7 +670,7 @@ where
editor.insert(c); editor.insert(c);
let message = (on_change)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
@ -574,6 +683,8 @@ where
let state = state(); let state = state();
if let Some(focus) = &mut state.is_focused { if let Some(focus) = &mut state.is_focused {
let Some(on_input) = on_input else { return event::Status::Ignored };
let modifiers = state.keyboard_modifiers; let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
@ -599,7 +710,7 @@ where
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(value, &mut state.cursor);
editor.backspace(); editor.backspace();
let message = (on_change)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
} }
keyboard::KeyCode::Delete => { keyboard::KeyCode::Delete => {
@ -619,7 +730,7 @@ where
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(value, &mut state.cursor);
editor.delete(); editor.delete();
let message = (on_change)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
} }
keyboard::KeyCode::Left => { keyboard::KeyCode::Left => {
@ -694,7 +805,7 @@ where
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(value, &mut state.cursor);
editor.delete(); editor.delete();
let message = (on_change)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
} }
keyboard::KeyCode::V => { keyboard::KeyCode::V => {
@ -721,7 +832,7 @@ where
let message = if let Some(paste) = &on_paste { let message = if let Some(paste) = &on_paste {
(paste)(editor.contents()) (paste)(editor.contents())
} else { } else {
(on_change)(editor.contents()) (on_input)(editor.contents())
}; };
shell.publish(message); shell.publish(message);
@ -815,7 +926,9 @@ pub fn draw<Renderer>(
placeholder: &str, placeholder: &str,
size: Option<f32>, size: Option<f32>,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
is_disabled: bool,
is_secure: bool, is_secure: bool,
icon: Option<&Icon<Renderer::Font>>,
style: &<Renderer::Theme as StyleSheet>::Style, style: &<Renderer::Theme as StyleSheet>::Style,
) where ) where
Renderer: text::Renderer, Renderer: text::Renderer,
@ -825,11 +938,15 @@ pub fn draw<Renderer>(
let value = secure_value.as_ref().unwrap_or(value); let value = secure_value.as_ref().unwrap_or(value);
let bounds = layout.bounds(); 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 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) theme.focused(style)
} else if is_mouse_over { } else if is_mouse_over {
theme.hovered(style) theme.hovered(style)
@ -847,6 +964,20 @@ pub fn draw<Renderer>(
appearance.background, 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 text = value.to_string();
let font = font.unwrap_or_else(|| renderer.default_font()); let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size()); 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 }, content: if text.is_empty() { placeholder } else { &text },
color: if text.is_empty() { color: if text.is_empty() {
theme.placeholder_color(style) theme.placeholder_color(style)
} else if is_disabled {
theme.disabled_color(style)
} else { } else {
theme.value_color(style) theme.value_color(style)
}, },
@ -987,9 +1120,14 @@ pub fn draw<Renderer>(
pub fn mouse_interaction( pub fn mouse_interaction(
layout: Layout<'_>, layout: Layout<'_>,
cursor_position: Point, cursor_position: Point,
is_disabled: bool,
) -> mouse::Interaction { ) -> mouse::Interaction {
if layout.bounds().contains(cursor_position) { if layout.bounds().contains(cursor_position) {
mouse::Interaction::Text if is_disabled {
mouse::Interaction::NotAllowed
} else {
mouse::Interaction::Text
}
} else { } else {
mouse::Interaction::default() mouse::Interaction::default()
} }

View file

@ -13,8 +13,8 @@ use crate::core::renderer;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Background, Clipboard, Color, Element, Length, Pixels, Point, Rectangle, Clipboard, Color, Element, Length, Pixels, Point, Rectangle, Shell, Size,
Shell, Size, Widget, Widget,
}; };
/// An vertical bar and a handle that selects a single value from a range of /// 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) 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 let (handle_width, handle_height, handle_border_radius) = match style
.handle .handle
.shape .shape
@ -416,18 +384,50 @@ pub fn draw<T, R>(
(start.into() as f32, end.into() as f32) (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 0.0
} else { } else {
(bounds.height - handle_width) * (value - range_end) (bounds.height - handle_width / 2.0) * (value - range_end)
/ (range_start - range_end) / (range_start - range_end)
}; };
let rail_x = bounds.x + bounds.width / 2.0;
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { bounds: Rectangle {
x: rail_x - (handle_height / 2.0), x: rail_x - style.rail.width / 2.0,
y: bounds.y + handle_offset.round(), 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 + offset,
width: handle_height, width: handle_height,
height: handle_width, height: handle_width,
}, },

View file

@ -1,6 +1,6 @@
[package] [package]
name = "iced_winit" name = "iced_winit"
version = "0.8.0" version = "0.9.1"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021" edition = "2021"
description = "A winit runtime for Iced" description = "A winit runtime for Iced"
@ -11,11 +11,16 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"] categories = ["gui"]
[features] [features]
default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
trace = ["tracing", "tracing-core", "tracing-subscriber"] trace = ["tracing", "tracing-core", "tracing-subscriber"]
chrome-trace = ["trace", "tracing-chrome"] chrome-trace = ["trace", "tracing-chrome"]
debug = ["iced_runtime/debug"] debug = ["iced_runtime/debug"]
system = ["sysinfo"] system = ["sysinfo"]
application = [] application = []
x11 = ["winit/x11"]
wayland = ["winit/wayland"]
wayland-dlopen = ["winit/wayland-dlopen"]
wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
[dependencies] [dependencies]
window_clipboard = "0.2" window_clipboard = "0.2"
@ -26,17 +31,18 @@ thiserror = "1.0"
version = "0.27" version = "0.27"
git = "https://github.com/iced-rs/winit.git" git = "https://github.com/iced-rs/winit.git"
rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c" rev = "940457522e9fb9f5dac228b0ecfafe0138b4048c"
default-features = false
[dependencies.iced_runtime] [dependencies.iced_runtime]
version = "0.9" version = "0.1"
path = "../runtime" path = "../runtime"
[dependencies.iced_graphics] [dependencies.iced_graphics]
version = "0.7" version = "0.8"
path = "../graphics" path = "../graphics"
[dependencies.iced_style] [dependencies.iced_style]
version = "0.7" version = "0.8"
path = "../style" path = "../style"
[dependencies.tracing] [dependencies.tracing]

View file

@ -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`: Add `iced_winit` as a dependency in your `Cargo.toml`:
```toml ```toml
iced_winit = "0.8" iced_winit = "0.9"
``` ```
__Iced moves fast and the `master` branch can contain breaking changes!__ If __Iced moves fast and the `master` branch can contain breaking changes!__ If

View file

@ -179,13 +179,17 @@ where
.unwrap_or(None) .unwrap_or(None)
}); });
let _ = match target { match target {
Some(node) => node Some(node) => {
.replace_child(&canvas, &node) let _ = node
.expect(&format!("Could not replace #{}", node.id())), .replace_with_with_node_1(&canvas)
None => body .expect(&format!("Could not replace #{}", node.id()));
.append_child(&canvas) }
.expect("Append canvas to HTML body"), None => {
let _ = body
.append_child(&canvas)
.expect("Append canvas to HTML body");
}
}; };
} }
@ -762,6 +766,9 @@ pub fn run_command<A, E>(
mode, mode,
)); ));
} }
window::Action::ChangeIcon(icon) => {
window.set_window_icon(conversion::icon(icon))
}
window::Action::FetchMode(tag) => { window::Action::FetchMode(tag) => {
let mode = if window.is_visible().unwrap_or(true) { let mode = if window.is_visible().unwrap_or(true) {
conversion::mode(window.fullscreen()) conversion::mode(window.fullscreen())

View file

@ -1,7 +1,7 @@
//! Convert [`winit`] types into [`iced_native`] types, and viceversa. //! Convert [`winit`] types into [`iced_native`] types, and viceversa.
//! //!
//! [`winit`]: https://github.com/rust-windowing/winit //! [`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::keyboard;
use crate::core::mouse; use crate::core::mouse;
use crate::core::touch; 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. /// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`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( pub fn mouse_interaction(
interaction: mouse::Interaction, interaction: mouse::Interaction,
) -> winit::window::CursorIcon { ) -> winit::window::CursorIcon {
@ -237,13 +237,14 @@ pub fn mouse_interaction(
winit::window::CursorIcon::EwResize winit::window::CursorIcon::EwResize
} }
Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, Interaction::ResizingVertically => winit::window::CursorIcon::NsResize,
Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed,
} }
} }
/// Converts a `MouseButton` from [`winit`] to an [`iced_native`] mouse button. /// Converts a `MouseButton` from [`winit`] to an [`iced_native`] mouse button.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`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 { pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
match mouse_button { match mouse_button {
winit::event::MouseButton::Left => mouse::Button::Left, winit::event::MouseButton::Left => mouse::Button::Left,
@ -259,7 +260,7 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
/// modifiers state. /// modifiers state.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`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( pub fn modifiers(
modifiers: winit::event::ModifiersState, modifiers: winit::event::ModifiersState,
) -> keyboard::Modifiers { ) -> keyboard::Modifiers {
@ -286,7 +287,7 @@ pub fn cursor_position(
/// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event. /// Converts a `Touch` from [`winit`] to an [`iced_native`] touch event.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`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( pub fn touch_event(
touch: winit::event::Touch, touch: winit::event::Touch,
scale_factor: f64, scale_factor: f64,
@ -317,7 +318,7 @@ pub fn touch_event(
/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code. /// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`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( pub fn key_code(
virtual_keycode: winit::event::VirtualKeyCode, virtual_keycode: winit::event::VirtualKeyCode,
) -> keyboard::KeyCode { ) -> 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 // As defined in: http://www.unicode.org/faq/private_use.html
pub(crate) fn is_private_use_character(c: char) -> bool { pub(crate) fn is_private_use_character(c: char) -> bool {
matches!( matches!(

View file

@ -11,7 +11,7 @@
//! Additionally, a [`conversion`] module is available for users that decide to //! Additionally, a [`conversion`] module is available for users that decide to
//! implement a custom event loop. //! 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 //! [`winit`]: https://github.com/rust-windowing/winit
//! [`conversion`]: crate::conversion //! [`conversion`]: crate::conversion
#![doc( #![doc(

View file

@ -22,6 +22,7 @@ mod platform;
pub use platform::PlatformSpecific; pub use platform::PlatformSpecific;
use crate::conversion; use crate::conversion;
use crate::core::window::Icon;
use crate::Position; use crate::Position;
use winit::monitor::MonitorHandle; use winit::monitor::MonitorHandle;
@ -84,7 +85,7 @@ pub struct Window {
pub always_on_top: bool, pub always_on_top: bool,
/// The window icon, which is also usually used in the taskbar /// The window icon, which is also usually used in the taskbar
pub icon: Option<winit::window::Icon>, pub icon: Option<Icon>,
/// Platform specific settings. /// Platform specific settings.
pub platform_specific: platform::PlatformSpecific, pub platform_specific: platform::PlatformSpecific,
@ -126,8 +127,9 @@ impl Window {
.with_resizable(self.resizable) .with_resizable(self.resizable)
.with_decorations(self.decorations) .with_decorations(self.decorations)
.with_transparent(self.transparent) .with_transparent(self.transparent)
.with_window_icon(self.icon) .with_window_icon(self.icon.and_then(conversion::icon))
.with_always_on_top(self.always_on_top); .with_always_on_top(self.always_on_top)
.with_visible(self.visible);
if let Some(position) = conversion::position( if let Some(position) = conversion::position(
primary_monitor.as_ref(), primary_monitor.as_ref(),