Merge branch 'master' into feat/text-macro

This commit is contained in:
Héctor Ramón Jiménez 2024-05-23 13:29:45 +02:00
commit d8ba6b0673
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
232 changed files with 12638 additions and 8527 deletions

View file

@ -1,53 +1,2 @@
[alias]
lint = """
clippy --workspace --no-deps -- \
-D warnings \
-A clippy::type_complexity \
-D clippy::semicolon_if_nothing_returned \
-D clippy::trivially-copy-pass-by-ref \
-D clippy::default_trait_access \
-D clippy::match-wildcard-for-single-variants \
-D clippy::redundant-closure-for-method-calls \
-D clippy::filter_map_next \
-D clippy::manual_let_else \
-D clippy::unused_async \
-D clippy::from_over_into \
-D clippy::needless_borrow \
-D clippy::new_without_default \
-D clippy::useless_conversion
"""
nitpick = """
clippy --workspace --no-deps -- \
-D warnings \
-D clippy::pedantic \
-A clippy::type_complexity \
-A clippy::must_use_candidate \
-A clippy::return_self_not_must_use \
-A clippy::needless_pass_by_value \
-A clippy::cast_precision_loss \
-A clippy::cast_sign_loss \
-A clippy::cast_possible_truncation \
-A clippy::match_same_arms \
-A clippy::missing-errors-doc \
-A clippy::missing-panics-doc \
-A clippy::cast_lossless \
-A clippy::doc_markdown \
-A clippy::items_after_statements \
-A clippy::too_many_lines \
-A clippy::module_name_repetitions \
-A clippy::if_not_else \
-A clippy::redundant_else \
-A clippy::used_underscore_binding \
-A clippy::cast_possible_wrap \
-A clippy::unnecessary_wraps \
-A clippy::struct-excessive-bools \
-A clippy::float-cmp \
-A clippy::single_match_else \
-A clippy::unreadable_literal \
-A clippy::explicit_deref_methods \
-A clippy::map_unwrap_or \
-A clippy::unnested_or_patterns \
-A clippy::similar_names \
-A clippy::unused_self
"""
lint = "clippy --workspace --benches --all-features --no-deps -- -D warnings"

View file

@ -1,14 +1,6 @@
name: Check
on: [push, pull_request]
jobs:
widget:
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v2
- uses: actions/checkout@master
- name: Check standalone `iced_widget` crate
run: cargo check --package iced_widget --features image,svg,canvas
wasm:
runs-on: ubuntu-latest
env:
@ -25,5 +17,11 @@ jobs:
run: cargo build --package tour --target wasm32-unknown-unknown
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
- name: Check compilation of `integration` example
run: cargo build --package integration --target wasm32-unknown-unknown
widget:
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v2
- uses: actions/checkout@master
- name: Check standalone `iced_widget` crate
run: cargo check --package iced_widget --features image,svg,canvas

View file

@ -27,6 +27,8 @@ jobs:
-p iced
- name: Write CNAME file
run: echo 'docs.iced.rs' > ./target/doc/CNAME
- name: Copy redirect file as index.html
run: cp docs/redirect.html target/doc/index.html
- name: Publish documentation
if: github.ref == 'refs/heads/master'
uses: peaceiris/actions-gh-pages@v3

View file

@ -2,11 +2,16 @@ name: Lint
on: [push, pull_request]
jobs:
all:
runs-on: macOS-latest
runs-on: ubuntu-latest
steps:
- uses: hecrj/setup-rust-action@v2
with:
components: clippy
- uses: actions/checkout@master
- name: Install dependencies
run: |
export DEBIAN_FRONTED=noninteractive
sudo apt-get -qq update
sudo apt-get install -y libxkbcommon-dev libgtk-3-dev
- name: Check lints
run: cargo lint

View file

@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
@ -18,9 +21,11 @@ all-features = true
maintenance = { status = "actively-developed" }
[features]
default = ["wgpu", "fira-sans"]
default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"]
# Enable the `wgpu` GPU-accelerated renderer backend
wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enable the `tiny-skia` software renderer backend
tiny-skia = ["iced_renderer/tiny-skia"]
# Enables the `Image` widget
image = ["iced_widget/image", "dep:image"]
# Enables the `Svg` widget
@ -50,11 +55,14 @@ highlighter = ["iced_highlighter"]
# Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"]
# Enables the advanced module
advanced = []
advanced = ["iced_core/advanced", "iced_widget/advanced"]
# Enables embedding Fira Sans as the default font on Wasm builds
fira-sans = ["iced_renderer/fira-sans"]
# Enables auto-detecting light/dark mode for the built-in theme
auto-detect-theme = ["iced_core/auto-detect-theme"]
[dependencies]
iced_core.workspace = true
iced_futures.workspace = true
iced_renderer.workspace = true
iced_widget.workspace = true
@ -69,6 +77,15 @@ thiserror.workspace = true
image.workspace = true
image.optional = true
[dev-dependencies]
criterion = "0.5"
iced_wgpu.workspace = true
[[bench]]
name = "wgpu"
harness = false
required-features = ["canvas"]
[profile.release-opt]
inherits = "release"
codegen-units = 1
@ -120,10 +137,12 @@ iced_winit = { version = "0.13.0-dev", path = "winit" }
async-std = "1.0"
bitflags = "2.0"
bytemuck = { version = "1.0", features = ["derive"] }
bytes = "1.6"
cosmic-text = "0.10"
dark-light = "1.0"
futures = "0.3"
glam = "0.25"
glyphon = "0.5"
glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" }
guillotiere = "0.6"
half = "2.2"
image = "0.24"
@ -149,13 +168,37 @@ thiserror = "1.0"
tiny-skia = "0.11"
tokio = "1.0"
tracing = "0.1"
xxhash-rust = { version = "0.8", features = ["xxh3"] }
unicode-segmentation = "1.0"
wasm-bindgen-futures = "0.4"
wasm-timer = "0.2"
web-sys = "=0.3.67"
web-time = "0.2"
web-time = "1.1"
wgpu = "0.19"
winapi = "0.3"
window_clipboard = "0.4.1"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "b91e39ece2c0d378c3b80da7f3ab50e17bb798a5" }
winit = { git = "https://github.com/iced-rs/winit.git", rev = "8affa522bc6dcc497d332a28c03491d22a22f5a7" }
[workspace.lints.rust]
rust_2018_idioms = "forbid"
missing_debug_implementations = "deny"
missing_docs = "deny"
unsafe_code = "deny"
unused_results = "deny"
[workspace.lints.clippy]
type-complexity = "allow"
semicolon_if_nothing_returned = "deny"
trivially-copy-pass-by-ref = "deny"
default_trait_access = "deny"
match-wildcard-for-single-variants = "deny"
redundant-closure-for-method-calls = "deny"
filter_map_next = "deny"
manual_let_else = "deny"
unused_async = "deny"
from_over_into = "deny"
needless_borrow = "deny"
new_without_default = "deny"
useless_conversion = "deny"
[workspace.lints.rustdoc]
broken_intra_doc_links = "forbid"

View file

@ -20,7 +20,7 @@ pkgs.mkShell rec {
freetype
freetype.dev
libGL
pkgconfig
pkg-config
xorg.libX11
xorg.libXcursor
xorg.libXi

9
benches/ipsum.txt Normal file
View file

@ -0,0 +1,9 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at elit mollis, dictum nunc non, tempus metus. Sed iaculis ac mauris eu lobortis. Integer elementum venenatis eros, id placerat odio feugiat vel. Maecenas consequat convallis tincidunt. Nunc eu lorem justo. Praesent quis ornare sapien. Aliquam interdum tortor ut rhoncus faucibus. Suspendisse molestie scelerisque nulla, eget sodales lacus sodales vel. Nunc placerat id arcu sodales venenatis. Praesent ullamcorper viverra nibh eget efficitur. Aliquam molestie felis vehicula, finibus sapien eget, accumsan purus. Praesent vestibulum eleifend consectetur. Sed tincidunt lectus a libero efficitur, non scelerisque lectus tincidunt.
Cras ullamcorper tincidunt tellus non tempor. Integer pulvinar turpis quam, nec pharetra purus egestas non. Vivamus sed ipsum consequat, dignissim ante et, suscipit nibh. Quisque et mauris eu erat rutrum cursus. Pellentesque ut neque eu neque eleifend auctor ac hendrerit dolor. Morbi eget egestas ex. Integer hendrerit ipsum in enim bibendum, at vehicula ipsum dapibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce tempus consectetur tortor, vel fermentum sem pulvinar eget. Maecenas rutrum fringilla eros a pellentesque. Cras quis magna consectetur, tristique massa vel, aliquet nunc. Aliquam erat volutpat. Suspendisse porttitor risus id auctor fermentum. Vivamus efficitur tellus sed tortor cursus tincidunt. Sed auctor varius arcu, non congue tellus vehicula finibus.
Fusce a tincidunt urna. Nunc at quam ac enim tempor vehicula imperdiet in sapien. Donec lobortis tristique felis vel semper. Quisque vulputate felis eu enim vestibulum malesuada. Fusce a lobortis mauris, iaculis eleifend ligula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus sodales vel elit dignissim mattis.
Aliquam placerat vulputate dignissim. Proin pellentesque vitae arcu ut feugiat. Nunc mi felis, ornare at gravida sed, vestibulum sed urna. Duis fermentum maximus viverra. Donec imperdiet pellentesque sollicitudin. Cras non sem quis metus bibendum molestie. Duis imperdiet nec lectus eu rutrum. Mauris congue enim purus, in iaculis arcu dapibus ut. Nullam id erat tincidunt, iaculis dolor non, lobortis magna. Proin convallis scelerisque maximus. Morbi at lorem fringilla libero blandit fringilla. Ut aliquet tellus non sem dictum viverra. Aenean venenatis purus eget lacus placerat, non mollis mauris pellentesque.
Etiam elit diam, aliquet quis suscipit non, condimentum viverra odio. Praesent mi enim, suscipit id mi in, rhoncus ultricies lorem. Nulla facilisi. Integer convallis sagittis euismod. Vestibulum porttitor sodales turpis ac accumsan. Nullam molestie turpis vel lacus tincidunt, sed finibus erat pharetra. Nullam vestibulum turpis id sollicitudin accumsan. Praesent eget posuere lacus. Donec vehicula, nisl nec suscipit porta, felis lorem gravida orci, a hendrerit tellus nibh sit amet elit.

228
benches/wgpu.rs Normal file
View file

@ -0,0 +1,228 @@
#![allow(missing_docs)]
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
use iced::alignment;
use iced::mouse;
use iced::widget::{canvas, scrollable, stack, text};
use iced::{
Color, Element, Font, Length, Pixels, Point, Rectangle, Size, Theme,
};
use iced_wgpu::Renderer;
criterion_main!(benches);
criterion_group!(benches, wgpu_benchmark);
#[allow(unused_results)]
pub fn wgpu_benchmark(c: &mut Criterion) {
c.bench_function("wgpu — canvas (light)", |b| {
benchmark(b, |_| scene(10));
});
c.bench_function("wgpu — canvas (heavy)", |b| {
benchmark(b, |_| scene(1_000));
});
c.bench_function("wgpu - layered text (light)", |b| {
benchmark(b, |_| layered_text(10));
});
c.bench_function("wgpu - layered text (heavy)", |b| {
benchmark(b, |_| layered_text(1_000));
});
c.bench_function("wgpu - dynamic text (light)", |b| {
benchmark(b, |i| dynamic_text(1_000, i));
});
c.bench_function("wgpu - dynamic text (heavy)", |b| {
benchmark(b, |i| dynamic_text(100_000, i));
});
}
fn benchmark<'a>(
bencher: &mut Bencher<'_>,
view: impl Fn(usize) -> Element<'a, (), Theme, Renderer>,
) {
use iced_futures::futures::executor;
use iced_wgpu::graphics;
use iced_wgpu::graphics::Antialiasing;
use iced_wgpu::wgpu;
use iced_winit::core;
use iced_winit::runtime;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = executor::block_on(instance.request_adapter(
&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
},
))
.expect("request adapter");
let (device, queue) = executor::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
))
.expect("request device");
let format = wgpu::TextureFormat::Bgra8UnormSrgb;
let mut engine = iced_wgpu::Engine::new(
&adapter,
&device,
&queue,
format,
Some(Antialiasing::MSAAx4),
);
let mut renderer =
Renderer::new(&device, &engine, Font::DEFAULT, Pixels::from(16));
let viewport =
graphics::Viewport::with_physical_size(Size::new(3840, 2160), 2.0);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 3840,
height: 2160,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let texture_view =
texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut i = 0;
let mut cache = Some(runtime::user_interface::Cache::default());
bencher.iter(|| {
let mut user_interface = runtime::UserInterface::build(
view(i),
viewport.logical_size(),
cache.take().unwrap(),
&mut renderer,
);
let _ = user_interface.draw(
&mut renderer,
&Theme::Dark,
&core::renderer::Style {
text_color: Color::WHITE,
},
mouse::Cursor::Unavailable,
);
cache = Some(user_interface.into_cache());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: None,
});
renderer.present::<&str>(
&mut engine,
&device,
&queue,
&mut encoder,
Some(Color::BLACK),
format,
&texture_view,
&viewport,
&[],
);
let submission = engine.submit(&queue, encoder);
let _ = device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission));
i += 1;
});
}
fn scene<'a, Message: 'a>(n: usize) -> Element<'a, Message, Theme, Renderer> {
struct Scene {
n: usize,
}
impl<Message, Theme> canvas::Program<Message, Theme, Renderer> for Scene {
type State = canvas::Cache<Renderer>;
fn draw(
&self,
cache: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry<Renderer>> {
vec![cache.draw(renderer, bounds.size(), |frame| {
for i in 0..self.n {
frame.fill_rectangle(
Point::new(0.0, i as f32),
Size::new(10.0, 10.0),
Color::WHITE,
);
}
for i in 0..self.n {
frame.fill_text(canvas::Text {
content: i.to_string(),
position: Point::new(0.0, i as f32),
color: Color::BLACK,
size: Pixels::from(16),
line_height: text::LineHeight::default(),
font: Font::DEFAULT,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Basic,
});
}
})]
}
}
canvas(Scene { n })
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn layered_text<'a, Message: 'a>(
n: usize,
) -> Element<'a, Message, Theme, Renderer> {
stack((0..n).map(|i| text(format!("I am paragraph {i}!")).into()))
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn dynamic_text<'a, Message: 'a>(
n: usize,
i: usize,
) -> Element<'a, Message, Theme, Renderer> {
const LOREM_IPSUM: &str = include_str!("ipsum.txt");
scrollable(
text(format!(
"{}... Iteration {i}",
std::iter::repeat(LOREM_IPSUM.chars())
.flatten()
.take(n)
.collect::<String>(),
))
.size(10),
)
.into()
}

View file

@ -10,17 +10,28 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[lints]
workspace = true
[features]
auto-detect-theme = ["dep:dark-light"]
advanced = []
[dependencies]
bitflags.workspace = true
bytes.workspace = true
glam.workspace = true
log.workspace = true
num-traits.workspace = true
once_cell.workspace = true
palette.workspace = true
rustc-hash.workspace = true
smol_str.workspace = true
thiserror.workspace = true
web-time.workspace = true
xxhash-rust.workspace = true
dark-light.workspace = true
dark-light.optional = true
[target.'cfg(windows)'.dependencies]
raw-window-handle.workspace = true

View file

@ -1,12 +1,17 @@
use crate::{Point, Rectangle, Vector};
use std::f32::consts::{FRAC_PI_2, PI};
use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign};
use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign};
/// Degrees
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Degrees(pub f32);
impl Degrees {
/// The range of degrees of a circle.
pub const RANGE: RangeInclusive<Self> = Self(0.0)..=Self(360.0);
}
impl PartialEq<f32> for Degrees {
fn eq(&self, other: &f32) -> bool {
self.0.eq(other)
@ -19,6 +24,52 @@ impl PartialOrd<f32> for Degrees {
}
}
impl From<f32> for Degrees {
fn from(degrees: f32) -> Self {
Self(degrees)
}
}
impl From<u8> for Degrees {
fn from(degrees: u8) -> Self {
Self(f32::from(degrees))
}
}
impl From<Degrees> for f32 {
fn from(degrees: Degrees) -> Self {
degrees.0
}
}
impl From<Degrees> for f64 {
fn from(degrees: Degrees) -> Self {
Self::from(degrees.0)
}
}
impl Mul<f32> for Degrees {
type Output = Degrees;
fn mul(self, rhs: f32) -> Self::Output {
Self(self.0 * rhs)
}
}
impl num_traits::FromPrimitive for Degrees {
fn from_i64(n: i64) -> Option<Self> {
Some(Self(n as f32))
}
fn from_u64(n: u64) -> Option<Self> {
Some(Self(n as f32))
}
fn from_f64(n: f64) -> Option<Self> {
Some(Self(n as f32))
}
}
/// Radians
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Radians(pub f32);
@ -65,6 +116,12 @@ impl From<u8> for Radians {
}
}
impl From<Radians> for f32 {
fn from(radians: Radians) -> Self {
radians.0
}
}
impl From<Radians> for f64 {
fn from(radians: Radians) -> Self {
Self::from(radians.0)
@ -107,6 +164,14 @@ impl Add for Radians {
}
}
impl Add<Degrees> for Radians {
type Output = Self;
fn add(self, rhs: Degrees) -> Self::Output {
Self(self.0 + rhs.0.to_radians())
}
}
impl AddAssign for Radians {
fn add_assign(&mut self, rhs: Radians) {
self.0 = self.0 + rhs.0;
@ -153,6 +218,14 @@ impl Div for Radians {
}
}
impl Rem for Radians {
type Output = Self;
fn rem(self, rhs: Self) -> Self::Output {
Self(self.0 % rhs.0)
}
}
impl PartialEq<f32> for Radians {
fn eq(&self, other: &f32) -> bool {
self.0.eq(other)

View file

@ -1,6 +1,8 @@
//! Control the fit of some content (like an image) within a space.
use crate::Size;
use std::fmt;
/// The strategy used to fit the contents of a widget to its bounding box.
///
/// Each variant of this enum is a strategy that can be applied for resolving
@ -11,7 +13,7 @@ use crate::Size;
/// in CSS, see [Mozilla's docs][1], or run the `tour` example
///
/// [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Default)]
pub enum ContentFit {
/// Scale as big as it can be without needing to crop or hide parts.
///
@ -23,6 +25,7 @@ pub enum ContentFit {
/// This is a great fit for when you need to display an image without losing
/// any part of it, particularly when the image itself is the focus of the
/// screen.
#[default]
Contain,
/// Scale the image to cover all of the bounding box, cropping if needed.
@ -117,3 +120,15 @@ impl ContentFit {
}
}
}
impl fmt::Display for ContentFit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ContentFit::Contain => "Contain",
ContentFit::Cover => "Cover",
ContentFit::Fill => "Fill",
ContentFit::None => "None",
ContentFit::ScaleDown => "Scale Down",
})
}
}

View file

@ -95,7 +95,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
///
/// ```no_run
/// # mod iced {
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>;
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
/// #
/// # pub mod widget {
/// # pub fn row<'a, Message>(iter: impl IntoIterator<Item = super::Element<'a, Message>>) -> super::Element<'a, Message> {
@ -109,7 +109,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
/// # pub enum Message {}
/// # pub struct Counter;
/// #
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, iced_core::renderer::Null>;
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
/// #
/// # impl Counter {
/// # pub fn view(&self) -> Element<Message> {

View file

@ -1,7 +1,7 @@
/// The hasher used to compare layouts.
#[allow(missing_debug_implementations)] // Doesn't really make sense to have debug on the hasher state anyways.
#[derive(Default)]
pub struct Hasher(xxhash_rust::xxh3::Xxh3);
pub struct Hasher(rustc_hash::FxHasher);
impl core::hash::Hasher for Hasher {
fn write(&mut self, bytes: &[u8]) {

View file

@ -1,15 +1,45 @@
//! Load and draw raster graphics.
use crate::{Hasher, Rectangle, Size};
pub use bytes::Bytes;
use std::hash::{Hash, Hasher as _};
use std::path::PathBuf;
use std::sync::Arc;
use crate::{Radians, Rectangle, Size};
use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
/// A handle of some image data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Handle {
id: u64,
data: Data,
#[derive(Clone, PartialEq, Eq)]
pub enum Handle {
/// A file handle. The image data will be read
/// from the file path.
///
/// Use [`from_path`] to create this variant.
///
/// [`from_path`]: Self::from_path
Path(Id, PathBuf),
/// A handle pointing to some encoded image bytes in-memory.
///
/// Use [`from_bytes`] to create this variant.
///
/// [`from_bytes`]: Self::from_bytes
Bytes(Id, Bytes),
/// A handle pointing to decoded image pixels in RGBA format.
///
/// Use [`from_rgba`] to create this variant.
///
/// [`from_rgba`]: Self::from_rgba
Rgba {
/// The id of this handle.
id: Id,
/// The width of the image.
width: u32,
/// The height of the image.
height: u32,
/// The pixels.
pixels: Bytes,
},
}
impl Handle {
@ -17,56 +47,48 @@ impl Handle {
///
/// Makes an educated guess about the image format by examining the data in the file.
pub fn from_path<T: Into<PathBuf>>(path: T) -> Handle {
Self::from_data(Data::Path(path.into()))
let path = path.into();
Self::Path(Id::path(&path), path)
}
/// Creates an image [`Handle`] containing the image pixels directly. This
/// function expects the input data to be provided as a `Vec<u8>` of RGBA
/// pixels.
///
/// This is useful if you have already decoded your image.
pub fn from_pixels(
width: u32,
height: u32,
pixels: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Handle {
Self::from_data(Data::Rgba {
width,
height,
pixels: Bytes::new(pixels),
})
}
/// Creates an image [`Handle`] containing the image data directly.
/// Creates an image [`Handle`] containing the encoded image data directly.
///
/// Makes an educated guess about the image format by examining the given data.
///
/// This is useful if you already have your image loaded in-memory, maybe
/// because you downloaded or generated it procedurally.
pub fn from_memory(
bytes: impl AsRef<[u8]> + Send + Sync + 'static,
) -> Handle {
Self::from_data(Data::Bytes(Bytes::new(bytes)))
pub fn from_bytes(bytes: impl Into<Bytes>) -> Handle {
Self::Bytes(Id::unique(), bytes.into())
}
fn from_data(data: Data) -> Handle {
let mut hasher = Hasher::default();
data.hash(&mut hasher);
Handle {
id: hasher.finish(),
data,
/// Creates an image [`Handle`] containing the decoded image pixels directly.
///
/// This function expects the pixel data to be provided as a collection of [`Bytes`]
/// of RGBA pixels. Therefore, the length of the pixel data should always be
/// `width * height * 4`.
///
/// This is useful if you have already decoded your image.
pub fn from_rgba(
width: u32,
height: u32,
pixels: impl Into<Bytes>,
) -> Handle {
Self::Rgba {
id: Id::unique(),
width,
height,
pixels: pixels.into(),
}
}
/// Returns the unique identifier of the [`Handle`].
pub fn id(&self) -> u64 {
self.id
}
/// Returns a reference to the image [`Data`].
pub fn data(&self) -> &Data {
&self.data
pub fn id(&self) -> Id {
match self {
Handle::Path(id, _)
| Handle::Bytes(id, _)
| Handle::Rgba { id, .. } => *id,
}
}
}
@ -79,93 +101,49 @@ where
}
}
impl Hash for Handle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// A wrapper around raw image data.
///
/// It behaves like a `&[u8]`.
#[derive(Clone)]
pub struct Bytes(Arc<dyn AsRef<[u8]> + Send + Sync + 'static>);
impl Bytes {
/// Creates new [`Bytes`] around `data`.
pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self {
Self(Arc::new(data))
}
}
impl std::fmt::Debug for Bytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.as_ref().as_ref().fmt(f)
}
}
impl std::hash::Hash for Bytes {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.as_ref().as_ref().hash(state);
}
}
impl PartialEq for Bytes {
fn eq(&self, other: &Self) -> bool {
let a = self.as_ref();
let b = other.as_ref();
core::ptr::eq(a, b) || a == b
}
}
impl Eq for Bytes {}
impl AsRef<[u8]> for Bytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref().as_ref()
}
}
impl std::ops::Deref for Bytes {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.0.as_ref().as_ref()
}
}
/// The data of a raster image.
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum Data {
/// File data
Path(PathBuf),
/// In-memory data
Bytes(Bytes),
/// Decoded image pixels in RGBA format.
Rgba {
/// The width of the image.
width: u32,
/// The height of the image.
height: u32,
/// The pixels.
pixels: Bytes,
},
}
impl std::fmt::Debug for Data {
impl std::fmt::Debug for Handle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Data::Path(path) => write!(f, "Path({path:?})"),
Data::Bytes(_) => write!(f, "Bytes(...)"),
Data::Rgba { width, height, .. } => {
Self::Path(_, path) => write!(f, "Path({path:?})"),
Self::Bytes(_, _) => write!(f, "Bytes(...)"),
Self::Rgba { width, height, .. } => {
write!(f, "Pixels({width} * {height})")
}
}
}
}
/// The unique identifier of some [`Handle`] data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id(_Id);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum _Id {
Unique(u64),
Hash(u64),
}
impl Id {
fn unique() -> Self {
use std::sync::atomic::{self, AtomicU64};
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
Self(_Id::Unique(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)))
}
fn path(path: impl AsRef<Path>) -> Self {
let hash = {
let mut hasher = FxHasher::default();
path.as_ref().hash(&mut hasher);
hasher.finish()
};
Self(_Id::Hash(hash))
}
}
/// Image filtering strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FilterMethod {
@ -183,17 +161,19 @@ pub trait Renderer: crate::Renderer {
/// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`]
///
/// [`Handle`]: Self::Handle
type Handle: Clone + Hash;
type Handle: Clone;
/// Returns the dimensions of an image for the given [`Handle`].
fn dimensions(&self, handle: &Self::Handle) -> Size<u32>;
fn measure_image(&self, handle: &Self::Handle) -> Size<u32>;
/// Draws an image with the given [`Handle`] and inside the provided
/// `bounds`.
fn draw(
fn draw_image(
&mut self,
handle: Self::Handle,
filter_method: FilterMethod,
bounds: Rectangle,
rotation: Radians,
opacity: f32,
);
}

View file

@ -54,7 +54,7 @@ impl<'a> Layout<'a> {
}
/// Returns an iterator over the [`Layout`] of the children of a [`Node`].
pub fn children(self) -> impl Iterator<Item = Layout<'a>> {
pub fn children(self) -> impl DoubleEndedIterator<Item = Layout<'a>> {
self.node.children().iter().map(move |node| {
Layout::with_offset(
Vector::new(self.position.x, self.position.y),

View file

@ -9,13 +9,6 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![deny(
missing_debug_implementations,
missing_docs,
unused_results,
rustdoc::broken_intra_doc_links
)]
pub mod alignment;
pub mod border;
pub mod clipboard;
@ -41,12 +34,12 @@ mod background;
mod color;
mod content_fit;
mod element;
mod hasher;
mod length;
mod padding;
mod pixels;
mod point;
mod rectangle;
mod rotation;
mod shadow;
mod shell;
mod size;
@ -64,7 +57,6 @@ pub use element::Element;
pub use event::Event;
pub use font::Font;
pub use gradient::Gradient;
pub use hasher::Hasher;
pub use layout::Layout;
pub use length::Length;
pub use overlay::Overlay;
@ -73,6 +65,7 @@ pub use pixels::Pixels;
pub use point::Point;
pub use rectangle::Rectangle;
pub use renderer::Renderer;
pub use rotation::Rotation;
pub use shadow::Shadow;
pub use shell::Shell;
pub use size::Size;

View file

@ -3,6 +3,7 @@
#[allow(missing_docs)]
pub enum Interaction {
#[default]
None,
Idle,
Pointer,
Grab,

View file

@ -79,7 +79,7 @@ where
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse::Interaction::Idle
mouse::Interaction::None
}
/// Returns true if the cursor is over the [`Overlay`].

View file

@ -1,6 +1,6 @@
use crate::{Point, Size, Vector};
use crate::{Point, Radians, Size, Vector};
/// A rectangle.
/// An axis-aligned rectangle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rectangle<T = f32> {
/// X coordinate of the top-left corner.
@ -16,24 +16,32 @@ pub struct Rectangle<T = f32> {
pub height: T,
}
impl Rectangle<f32> {
/// Creates a new [`Rectangle`] with its top-left corner in the given
/// [`Point`] and with the provided [`Size`].
pub fn new(top_left: Point, size: Size) -> Self {
impl<T> Rectangle<T>
where
T: Default,
{
/// Creates a new [`Rectangle`] with its top-left corner at the origin
/// and with the provided [`Size`].
pub fn with_size(size: Size<T>) -> Self {
Self {
x: top_left.x,
y: top_left.y,
x: T::default(),
y: T::default(),
width: size.width,
height: size.height,
}
}
}
/// Creates a new [`Rectangle`] with its top-left corner at the origin
/// and with the provided [`Size`].
pub fn with_size(size: Size) -> Self {
impl Rectangle<f32> {
/// A rectangle starting at [`Point::ORIGIN`] with infinite width and height.
pub const INFINITE: Self = Self::new(Point::ORIGIN, Size::INFINITY);
/// Creates a new [`Rectangle`] with its top-left corner in the given
/// [`Point`] and with the provided [`Size`].
pub const fn new(top_left: Point, size: Size) -> Self {
Self {
x: 0.0,
y: 0.0,
x: top_left.x,
y: top_left.y,
width: size.width,
height: size.height,
}
@ -139,13 +147,20 @@ impl Rectangle<f32> {
}
/// Snaps the [`Rectangle`] to __unsigned__ integer coordinates.
pub fn snap(self) -> Rectangle<u32> {
Rectangle {
pub fn snap(self) -> Option<Rectangle<u32>> {
let width = self.width as u32;
let height = self.height as u32;
if width < 1 || height < 1 {
return None;
}
Some(Rectangle {
x: self.x as u32,
y: self.y as u32,
width: self.width as u32,
height: self.height as u32,
}
width,
height,
})
}
/// Expands the [`Rectangle`] a given amount.
@ -157,6 +172,18 @@ impl Rectangle<f32> {
height: self.height + amount * 2.0,
}
}
/// Rotates the [`Rectangle`] and returns the smallest [`Rectangle`]
/// containing it.
pub fn rotate(self, rotation: Radians) -> Self {
let size = self.size().rotate(rotation);
let position = Point::new(
self.center_x() - size.width / 2.0,
self.center_y() - size.height / 2.0,
);
Self::new(position, size)
}
}
impl std::ops::Mul<f32> for Rectangle<f32> {
@ -212,3 +239,19 @@ where
}
}
}
impl<T> std::ops::Mul<Vector<T>> for Rectangle<T>
where
T: std::ops::Mul<Output = T> + Copy,
{
type Output = Rectangle<T>;
fn mul(self, scale: Vector<T>) -> Self {
Rectangle {
x: self.x * scale.x,
y: self.y * scale.y,
width: self.width * scale.x,
height: self.height * scale.y,
}
}
}

View file

@ -2,26 +2,47 @@
#[cfg(debug_assertions)]
mod null;
#[cfg(debug_assertions)]
pub use null::Null;
use crate::{
Background, Border, Color, Rectangle, Shadow, Size, Transformation, Vector,
};
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized {
pub trait Renderer {
/// Starts recording a new layer.
fn start_layer(&mut self, bounds: Rectangle);
/// Ends recording a new layer.
///
/// The new layer will clip its contents to the provided `bounds`.
fn end_layer(&mut self);
/// Draws the primitives recorded in the given closure in a new layer.
///
/// The layer will clip its contents to the provided `bounds`.
fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self));
fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {
self.start_layer(bounds);
f(self);
self.end_layer();
}
/// Starts recording with a new [`Transformation`].
fn start_transformation(&mut self, transformation: Transformation);
/// Ends recording a new layer.
///
/// The new layer will clip its contents to the provided `bounds`.
fn end_transformation(&mut self);
/// Applies a [`Transformation`] to the primitives recorded in the given closure.
fn with_transformation(
&mut self,
transformation: Transformation,
f: impl FnOnce(&mut Self),
);
) {
self.start_transformation(transformation);
f(self);
self.end_transformation();
}
/// Applies a translation to the primitives recorded in the given closure.
fn with_translation(

View file

@ -1,34 +1,21 @@
use crate::alignment;
use crate::image;
use crate::renderer::{self, Renderer};
use crate::svg;
use crate::text::{self, Text};
use crate::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
Background, Color, Font, Pixels, Point, Radians, Rectangle, Size,
Transformation,
};
use std::borrow::Cow;
impl Renderer for () {
fn start_layer(&mut self, _bounds: Rectangle) {}
/// A renderer that does nothing.
///
/// It can be useful if you are writing tests!
#[derive(Debug, Clone, Copy, Default)]
pub struct Null;
fn end_layer(&mut self) {}
impl Null {
/// Creates a new [`Null`] renderer.
pub fn new() -> Null {
Null
}
}
fn start_transformation(&mut self, _transformation: Transformation) {}
impl Renderer for Null {
fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {}
fn with_transformation(
&mut self,
_transformation: Transformation,
_f: impl FnOnce(&mut Self),
) {
}
fn end_transformation(&mut self) {}
fn clear(&mut self) {}
@ -40,7 +27,7 @@ impl Renderer for Null {
}
}
impl text::Renderer for Null {
impl text::Renderer for () {
type Font = Font;
type Paragraph = ();
type Editor = ();
@ -57,8 +44,6 @@ impl text::Renderer for Null {
Pixels(16.0)
}
fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn fill_paragraph(
&mut self,
_paragraph: &Self::Paragraph,
@ -79,7 +64,7 @@ impl text::Renderer for Null {
fn fill_text(
&mut self,
_paragraph: Text<'_, Self::Font>,
_paragraph: Text,
_position: Point,
_color: Color,
_clip_bounds: Rectangle,
@ -90,11 +75,11 @@ impl text::Renderer for Null {
impl text::Paragraph for () {
type Font = Font;
fn with_text(_text: Text<'_, Self::Font>) -> Self {}
fn with_text(_text: Text<&str>) -> Self {}
fn resize(&mut self, _new_bounds: Size) {}
fn compare(&self, _text: Text<'_, Self::Font>) -> text::Difference {
fn compare(&self, _text: Text<&str>) -> text::Difference {
text::Difference::None
}
@ -174,3 +159,37 @@ impl text::Editor for () {
) {
}
}
impl image::Renderer for () {
type Handle = ();
fn measure_image(&self, _handle: &Self::Handle) -> Size<u32> {
Size::default()
}
fn draw_image(
&mut self,
_handle: Self::Handle,
_filter_method: image::FilterMethod,
_bounds: Rectangle,
_rotation: Radians,
_opacity: f32,
) {
}
}
impl svg::Renderer for () {
fn measure_svg(&self, _handle: &svg::Handle) -> Size<u32> {
Size::default()
}
fn draw_svg(
&mut self,
_handle: svg::Handle,
_color: Option<Color>,
_bounds: Rectangle,
_rotation: Radians,
_opacity: f32,
) {
}
}

72
core/src/rotation.rs Normal file
View file

@ -0,0 +1,72 @@
//! Control the rotation of some content (like an image) within a space.
use crate::{Degrees, Radians, Size};
/// The strategy used to rotate the content.
///
/// This is used to control the behavior of the layout when the content is rotated
/// by a certain angle.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Rotation {
/// The element will float while rotating. The layout will be kept exactly as it was
/// before the rotation.
///
/// This is especially useful when used for animations, as it will avoid the
/// layout being shifted or resized when smoothly i.e. an icon.
///
/// This is the default.
Floating(Radians),
/// The element will be solid while rotating. The layout will be adjusted to fit
/// the rotated content.
///
/// This allows you to rotate an image and have the layout adjust to fit the new
/// size of the image.
Solid(Radians),
}
impl Rotation {
/// Returns the angle of the [`Rotation`] in [`Radians`].
pub fn radians(self) -> Radians {
match self {
Rotation::Floating(radians) | Rotation::Solid(radians) => radians,
}
}
/// Returns a mutable reference to the angle of the [`Rotation`] in [`Radians`].
pub fn radians_mut(&mut self) -> &mut Radians {
match self {
Rotation::Floating(radians) | Rotation::Solid(radians) => radians,
}
}
/// Returns the angle of the [`Rotation`] in [`Degrees`].
pub fn degrees(self) -> Degrees {
Degrees(self.radians().0.to_degrees())
}
/// Applies the [`Rotation`] to the given [`Size`], returning
/// the minimum [`Size`] containing the rotated one.
pub fn apply(self, size: Size) -> Size {
match self {
Self::Floating(_) => size,
Self::Solid(rotation) => size.rotate(rotation),
}
}
}
impl Default for Rotation {
fn default() -> Self {
Self::Floating(Radians(0.0))
}
}
impl From<Radians> for Rotation {
fn from(radians: Radians) -> Self {
Self::Floating(radians)
}
}
impl From<f32> for Rotation {
fn from(radians: f32) -> Self {
Self::Floating(Radians(radians))
}
}

View file

@ -1,7 +1,7 @@
use crate::Vector;
use crate::{Radians, Vector};
/// An amount of space in 2 dimensions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Size<T = f32> {
/// The width.
pub width: T,
@ -51,6 +51,19 @@ impl Size {
height: self.height + other.height,
}
}
/// Rotates the given [`Size`] and returns the minimum [`Size`]
/// containing it.
pub fn rotate(self, rotation: Radians) -> Size {
let radians = f32::from(rotation);
Size {
width: (self.width * radians.cos()).abs()
+ (self.height * radians.sin()).abs(),
height: (self.width * radians.sin()).abs()
+ (self.height * radians.cos()).abs(),
}
}
}
impl<T> From<[T; 2]> for Size<T> {
@ -99,3 +112,31 @@ where
}
}
}
impl<T> std::ops::Mul<T> for Size<T>
where
T: std::ops::Mul<Output = T> + Copy,
{
type Output = Size<T>;
fn mul(self, rhs: T) -> Self::Output {
Size {
width: self.width * rhs,
height: self.height * rhs,
}
}
}
impl<T> std::ops::Mul<Vector<T>> for Size<T>
where
T: std::ops::Mul<Output = T> + Copy,
{
type Output = Size<T>;
fn mul(self, scale: Vector<T>) -> Self::Output {
Size {
width: self.width * scale.x,
height: self.height * scale.y,
}
}
}

View file

@ -1,6 +1,7 @@
//! Load and draw vector graphics.
use crate::{Color, Hasher, Rectangle, Size};
use crate::{Color, Radians, Rectangle, Size};
use rustc_hash::FxHasher;
use std::borrow::Cow;
use std::hash::{Hash, Hasher as _};
use std::path::PathBuf;
@ -30,7 +31,7 @@ impl Handle {
}
fn from_data(data: Data) -> Handle {
let mut hasher = Hasher::default();
let mut hasher = FxHasher::default();
data.hash(&mut hasher);
Handle {
@ -91,8 +92,15 @@ impl std::fmt::Debug for Data {
/// [renderer]: crate::renderer
pub trait Renderer: crate::Renderer {
/// Returns the default dimensions of an SVG for the given [`Handle`].
fn dimensions(&self, handle: &Handle) -> Size<u32>;
fn measure_svg(&self, handle: &Handle) -> Size<u32>;
/// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`.
fn draw(&mut self, handle: Handle, color: Option<Color>, bounds: Rectangle);
fn draw_svg(
&mut self,
handle: Handle,
color: Option<Color>,
bounds: Rectangle,
rotation: Radians,
opacity: f32,
);
}

View file

@ -11,14 +11,13 @@ pub use paragraph::Paragraph;
use crate::alignment;
use crate::{Color, Pixels, Point, Rectangle, Size};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
/// A paragraph.
#[derive(Debug, Clone, Copy)]
pub struct Text<'a, Font> {
pub struct Text<Content = String, Font = crate::Font> {
/// The content of the paragraph.
pub content: &'a str,
pub content: Content,
/// The bounds of the paragraph.
pub bounds: Size,
@ -192,9 +191,6 @@ pub trait Renderer: crate::Renderer {
/// Returns the default size of [`Text`].
fn default_size(&self) -> Pixels;
/// Loads a [`Self::Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
/// Draws the given [`Paragraph`] at the given position and with the given
/// [`Color`].
fn fill_paragraph(
@ -219,7 +215,7 @@ pub trait Renderer: crate::Renderer {
/// [`Color`].
fn fill_text(
&mut self,
text: Text<'_, Self::Font>,
text: Text<String, Self::Font>,
position: Point,
color: Color,
clip_bounds: Rectangle,

View file

@ -8,14 +8,14 @@ pub trait Paragraph: Sized + Default {
type Font: Copy + PartialEq;
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_text(text: Text<'_, Self::Font>) -> Self;
fn with_text(text: Text<&str, Self::Font>) -> Self;
/// Lays out the [`Paragraph`] with some new boundaries.
fn resize(&mut self, new_bounds: Size);
/// Compares the [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
fn compare(&self, text: Text<'_, Self::Font>) -> Difference;
fn compare(&self, text: Text<&str, Self::Font>) -> Difference;
/// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal;
@ -35,7 +35,7 @@ pub trait Paragraph: Sized + Default {
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;
/// Updates the [`Paragraph`] to match the given [`Text`], if needed.
fn update(&mut self, text: Text<'_, Self::Font>) {
fn update(&mut self, text: Text<&str, Self::Font>) {
match self.compare(text) {
Difference::None => {}
Difference::Bounds => {

View file

@ -7,10 +7,9 @@ use std::fmt;
use std::sync::Arc;
/// A built-in theme.
#[derive(Debug, Clone, PartialEq, Default)]
#[derive(Debug, Clone, PartialEq)]
pub enum Theme {
/// The built-in light variant.
#[default]
Light,
/// The built-in dark variant.
Dark,
@ -161,6 +160,28 @@ impl Theme {
}
}
impl Default for Theme {
fn default() -> Self {
#[cfg(feature = "auto-detect-theme")]
{
use once_cell::sync::Lazy;
static DEFAULT: Lazy<Theme> =
Lazy::new(|| match dark_light::detect() {
dark_light::Mode::Dark => Theme::Dark,
dark_light::Mode::Light | dark_light::Mode::Default => {
Theme::Light
}
});
DEFAULT.clone()
}
#[cfg(not(feature = "auto-detect-theme"))]
Theme::Light
}
}
impl fmt::Display for Theme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View file

@ -613,10 +613,15 @@ fn mix(a: Color, b: Color, factor: f32) -> Color {
fn readable(background: Color, text: Color) -> Color {
if is_readable(background, text) {
text
} else if is_dark(background) {
Color::WHITE
} else {
Color::BLACK
let white_contrast = relative_contrast(background, Color::WHITE);
let black_contrast = relative_contrast(background, Color::BLACK);
if white_contrast >= black_contrast {
Color::WHITE
} else {
Color::BLACK
}
}
}
@ -631,6 +636,13 @@ fn is_readable(a: Color, b: Color) -> bool {
a_srgb.has_enhanced_contrast_text(b_srgb)
}
fn relative_contrast(a: Color, b: Color) -> f32 {
let a_srgb = Rgb::from(a);
let b_srgb = Rgb::from(b);
a_srgb.relative_contrast(b_srgb)
}
fn to_hsl(color: Color) -> Hsl {
Hsl::from_color(Rgb::from(color))
}

View file

@ -42,6 +42,12 @@ impl Transformation {
}
}
impl Default for Transformation {
fn default() -> Self {
Transformation::IDENTITY
}
}
impl Mul for Transformation {
type Output = Self;

View file

@ -18,6 +18,9 @@ impl<T> Vector<T> {
impl Vector {
/// The zero [`Vector`].
pub const ZERO: Self = Self::new(0.0, 0.0);
/// The unit [`Vector`].
pub const UNIT: Self = Self::new(0.0, 0.0);
}
impl<T> std::ops::Add for Vector<T>

View file

@ -137,7 +137,7 @@ where
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
mouse::Interaction::Idle
mouse::Interaction::None
}
/// Returns the overlay of the [`Widget`], if there is any.

View file

@ -18,9 +18,10 @@ pub use text::{LineHeight, Shaping};
#[allow(missing_debug_implementations)]
pub struct Text<'a, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
content: Cow<'a, str>,
fragment: Fragment<'a>,
size: Option<Pixels>,
line_height: LineHeight,
width: Length,
@ -29,20 +30,18 @@ where
vertical_alignment: alignment::Vertical,
font: Option<Renderer::Font>,
shaping: Shaping,
style: Style<'a, Theme>,
class: Theme::Class<'a>,
}
impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
/// Create a new fragment of [`Text`] with the given contents.
pub fn new(content: impl Into<Cow<'a, str>>) -> Self
where
Theme: DefaultStyle + 'a,
{
pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Text {
content: content.into(),
fragment: fragment.into_fragment(),
size: None,
line_height: LineHeight::default(),
font: None,
@ -51,7 +50,7 @@ where
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: Shaping::Basic,
style: Box::new(Theme::default_style),
class: Theme::default(),
}
}
@ -75,25 +74,6 @@ where
self
}
/// Sets the style of the [`Text`].
pub fn style(mut self, style: impl Fn(&Theme) -> Appearance + 'a) -> Self {
self.style = Box::new(style);
self
}
/// Sets the [`Color`] of the [`Text`].
pub fn color(self, color: impl Into<Color>) -> Self {
self.color_maybe(Some(color))
}
/// Sets the [`Color`] of the [`Text`], if `Some`.
pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self {
let color = color.map(Into::into);
self.style = Box::new(move |_theme| Appearance { color });
self
}
/// Sets the width of the [`Text`] boundaries.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
@ -129,6 +109,42 @@ where
self.shaping = shaping;
self
}
/// Sets the style of the [`Text`].
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
self
}
/// Sets the [`Color`] of the [`Text`].
pub fn color(self, color: impl Into<Color>) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
self.color_maybe(Some(color))
}
/// Sets the [`Color`] of the [`Text`], if `Some`.
pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
where
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
{
let color = color.map(Into::into);
self.style(move |_theme| Style { color })
}
/// Sets the style class of the [`Text`].
#[cfg(feature = "advanced")]
#[must_use]
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
self.class = class.into();
self
}
}
/// The internal state of a [`Text`] widget.
@ -138,6 +154,7 @@ pub struct State<P: Paragraph>(P);
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Text<'a, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
fn tag(&self) -> tree::Tag {
@ -167,7 +184,7 @@ where
limits,
self.width,
self.height,
&self.content,
&self.fragment,
self.line_height,
self.size,
self.font,
@ -182,15 +199,15 @@ where
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
defaults: &renderer::Style,
layout: Layout<'_>,
_cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
let appearance = (self.style)(theme);
let style = theme.style(&self.class);
draw(renderer, style, layout, state, appearance, viewport);
draw(renderer, defaults, layout, state, style, viewport);
}
}
@ -250,7 +267,7 @@ pub fn draw<Renderer>(
style: &renderer::Style,
layout: Layout<'_>,
state: &State<Renderer::Paragraph>,
appearance: Appearance,
appearance: Style,
viewport: &Rectangle,
) where
Renderer: text::Renderer,
@ -281,7 +298,7 @@ pub fn draw<Renderer>(
impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Theme: 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(
@ -293,7 +310,7 @@ where
impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
where
Theme: DefaultStyle + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer,
{
fn from(content: &'a str) -> Self {
@ -304,7 +321,7 @@ where
impl<'a, Message, Theme, Renderer> From<&'a str>
for Element<'a, Message, Theme, Renderer>
where
Theme: DefaultStyle + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(content: &'a str) -> Self {
@ -314,30 +331,116 @@ where
/// The appearance of some text.
#[derive(Debug, Clone, Copy, Default)]
pub struct Appearance {
pub struct Style {
/// The [`Color`] of the text.
///
/// The default, `None`, means using the inherited color.
pub color: Option<Color>,
}
/// The style of some [`Text`].
pub type Style<'a, Theme> = Box<dyn Fn(&Theme) -> Appearance + 'a>;
/// The theme catalog of a [`Text`].
pub trait Catalog: Sized {
/// The item class of this [`Catalog`].
type Class<'a>;
/// The default style of some [`Text`].
pub trait DefaultStyle {
/// Returns the default style of some [`Text`].
fn default_style(&self) -> Appearance;
/// The default class produced by this [`Catalog`].
fn default<'a>() -> Self::Class<'a>;
/// The [`Style`] of a class with the given status.
fn style(&self, item: &Self::Class<'_>) -> Style;
}
impl DefaultStyle for Theme {
fn default_style(&self) -> Appearance {
Appearance::default()
/// A styling function for a [`Text`].
///
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
impl Catalog for Theme {
type Class<'a> = StyleFn<'a, Self>;
fn default<'a>() -> Self::Class<'a> {
Box::new(|_theme| Style::default())
}
fn style(&self, class: &Self::Class<'_>) -> Style {
class(self)
}
}
impl DefaultStyle for Color {
fn default_style(&self) -> Appearance {
Appearance { color: Some(*self) }
/// A fragment of [`Text`].
///
/// This is just an alias to a string that may be either
/// borrowed or owned.
pub type Fragment<'a> = Cow<'a, str>;
/// A trait for converting a value to some text [`Fragment`].
pub trait IntoFragment<'a> {
/// Converts the value to some text [`Fragment`].
fn into_fragment(self) -> Fragment<'a>;
}
impl<'a> IntoFragment<'a> for Fragment<'a> {
fn into_fragment(self) -> Fragment<'a> {
self
}
}
impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Borrowed(self)
}
}
impl<'a> IntoFragment<'a> for &'a str {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Borrowed(self)
}
}
impl<'a> IntoFragment<'a> for &'a String {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Borrowed(self.as_str())
}
}
impl<'a> IntoFragment<'a> for String {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Owned(self)
}
}
macro_rules! into_fragment {
($type:ty) => {
impl<'a> IntoFragment<'a> for $type {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Owned(self.to_string())
}
}
impl<'a> IntoFragment<'a> for &$type {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Owned(self.to_string())
}
}
};
}
into_fragment!(char);
into_fragment!(bool);
into_fragment!(u8);
into_fragment!(u16);
into_fragment!(u32);
into_fragment!(u64);
into_fragment!(u128);
into_fragment!(usize);
into_fragment!(i8);
into_fragment!(i16);
into_fragment!(i32);
into_fragment!(i64);
into_fragment!(i128);
into_fragment!(isize);
into_fragment!(f32);
into_fragment!(f64);

View file

@ -1,4 +1,4 @@
use crate::Point;
use crate::{Point, Size};
/// The position of a window in a given screen.
#[derive(Debug, Clone, Copy, PartialEq)]
@ -15,6 +15,12 @@ pub enum Position {
/// at (0, 0) you would have to set the position to
/// `(PADDING_X, PADDING_Y)`.
Specific(Point),
/// Like [`Specific`], but the window is positioned with the specific coordinates returned by the function.
///
/// The function receives the window size and the monitor's resolution as input.
///
/// [`Specific`]: Self::Specific
SpecificWith(fn(Size, Size) -> Point),
}
impl Default for Position {

View file

@ -13,7 +13,7 @@ pub enum RedrawRequest {
#[cfg(test)]
mod tests {
use super::*;
use crate::time::{Duration, Instant};
use crate::time::Duration;
#[test]
fn ordering() {

View file

@ -1,11 +1,21 @@
//! Platform specific settings for WebAssembly.
/// The platform specific window settings of an application.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlatformSpecific {
/// The identifier of a DOM element that will be replaced with the
/// application.
///
/// If set to `None`, the application will be appended to the HTML body.
///
/// By default, it is set to `"iced"`.
pub target: Option<String>,
}
impl Default for PlatformSpecific {
fn default() -> Self {
Self {
target: Some(String::from("iced")),
}
}
}

13
docs/redirect.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; URL='/iced/'" />
</head>
<body>
<p>If you are not redirected automatically, follow this <a href="/iced/">link</a>.</p>
</body>
</html>

View file

@ -1,9 +1,11 @@
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
use iced::widget::{button, column, text};
use iced::{Alignment, Element, Length};
use iced::alignment;
use iced::widget::{button, container, horizontal_space, hover};
use iced::{Element, Length, Theme};
pub fn main() -> iced::Result {
iced::program("Bezier Tool - Iced", Example::update, Example::view)
.theme(|_| Theme::CatppuccinMocha)
.antialiasing(true)
.run()
}
@ -35,16 +37,22 @@ impl Example {
}
fn view(&self) -> Element<Message> {
column![
text("Bezier tool example").width(Length::Shrink).size(50),
container(hover(
self.bezier.view(&self.curves).map(Message::AddCurve),
button("Clear")
.style(button::danger)
.on_press(Message::Clear),
]
if self.curves.is_empty() {
container(horizontal_space())
} else {
container(
button("Clear")
.style(button::danger)
.on_press(Message::Clear),
)
.padding(10)
.width(Length::Fill)
.align_x(alignment::Horizontal::Right)
},
))
.padding(20)
.spacing(20)
.align_items(Alignment::Center)
.into()
}
}
@ -139,27 +147,24 @@ mod bezier {
&self,
state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Vec<Geometry> {
let content = self.state.cache.draw(
renderer,
bounds.size(),
|frame: &mut Frame| {
Curve::draw_all(self.curves, frame);
let content =
self.state.cache.draw(renderer, bounds.size(), |frame| {
Curve::draw_all(self.curves, frame, theme);
frame.stroke(
&Path::rectangle(Point::ORIGIN, frame.size()),
Stroke::default().with_width(2.0),
Stroke::default()
.with_width(2.0)
.with_color(theme.palette().text),
);
},
);
});
if let Some(pending) = state {
let pending_curve = pending.draw(renderer, bounds, cursor);
vec![content, pending_curve]
vec![content, pending.draw(renderer, theme, bounds, cursor)]
} else {
vec![content]
}
@ -187,7 +192,7 @@ mod bezier {
}
impl Curve {
fn draw_all(curves: &[Curve], frame: &mut Frame) {
fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) {
let curves = Path::new(|p| {
for curve in curves {
p.move_to(curve.from);
@ -195,7 +200,12 @@ mod bezier {
}
});
frame.stroke(&curves, Stroke::default().with_width(2.0));
frame.stroke(
&curves,
Stroke::default()
.with_width(2.0)
.with_color(theme.palette().text),
);
}
}
@ -209,6 +219,7 @@ mod bezier {
fn draw(
&self,
renderer: &Renderer,
theme: &Theme,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Geometry {
@ -218,7 +229,12 @@ mod bezier {
match *self {
Pending::One { from } => {
let line = Path::line(from, cursor_position);
frame.stroke(&line, Stroke::default().with_width(2.0));
frame.stroke(
&line,
Stroke::default()
.with_width(2.0)
.with_color(theme.palette().text),
);
}
Pending::Two { from, to } => {
let curve = Curve {
@ -227,7 +243,7 @@ mod bezier {
control: cursor_position,
};
Curve::draw_all(&[curve], &mut frame);
Curve::draw_all(&[curve], &mut frame, theme);
}
};
}

View file

@ -1,5 +1,5 @@
use iced::widget::{checkbox, column, container, row, text};
use iced::{Element, Font, Length};
use iced::widget::{center, checkbox, column, row, text};
use iced::{Element, Font};
const ICON_FONT: Font = Font::with_name("icons");
@ -68,11 +68,6 @@ impl Example {
let content =
column![default_checkbox, checkboxes, custom_checkbox].spacing(20);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
}

View file

@ -8,5 +8,5 @@ publish = false
[dependencies]
iced.workspace = true
iced.features = ["canvas", "tokio", "debug"]
time = { version = "0.3", features = ["local-offset"] }
chrono = "0.4"
tracing-subscriber = "0.3"

View file

@ -1,5 +1,6 @@
use iced::alignment;
use iced::mouse;
use iced::time;
use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::widget::{canvas, container};
use iced::{
@ -8,6 +9,8 @@ use iced::{
};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("Clock - Iced", Clock::update, Clock::view)
.subscription(Clock::subscription)
.theme(Clock::theme)
@ -16,13 +19,13 @@ pub fn main() -> iced::Result {
}
struct Clock {
now: time::OffsetDateTime,
now: chrono::DateTime<chrono::Local>,
clock: Cache,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Tick(time::OffsetDateTime),
Tick(chrono::DateTime<chrono::Local>),
}
impl Clock {
@ -52,16 +55,12 @@ impl Clock {
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(500)).map(|_| {
Message::Tick(
time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
)
})
time::every(time::Duration::from_millis(500))
.map(|_| Message::Tick(chrono::offset::Local::now()))
}
fn theme(&self) -> Theme {
Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()]
Theme::ALL[(self.now.timestamp() as usize / 10) % Theme::ALL.len()]
.clone()
}
}
@ -69,8 +68,7 @@ impl Clock {
impl Default for Clock {
fn default() -> Self {
Self {
now: time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
now: chrono::offset::Local::now(),
clock: Cache::default(),
}
}
@ -87,6 +85,8 @@ impl<Message> canvas::Program<Message> for Clock {
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
use chrono::Timelike;
let clock = self.clock.draw(renderer, bounds.size(), |frame| {
let palette = theme.extended_palette();
@ -167,7 +167,7 @@ impl<Message> canvas::Program<Message> for Clock {
}
}
fn hand_rotation(n: u8, total: u8) -> Degrees {
fn hand_rotation(n: u32, total: u32) -> Degrees {
let turns = n as f32 / total as f32;
Degrees(360.0 * turns)

View file

@ -6,9 +6,7 @@ use iced::{
Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size,
Vector,
};
use palette::{
self, convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue,
};
use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue};
use std::marker::PhantomData;
use std::ops::RangeInclusive;

View file

@ -1,5 +1,5 @@
use iced::widget::{
column, combo_box, container, scrollable, text, vertical_space,
center, column, combo_box, scrollable, text, vertical_space,
};
use iced::{Alignment, Element, Length};
@ -68,12 +68,7 @@ impl Example {
.align_items(Alignment::Center)
.spacing(10);
container(scrollable(content))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(scrollable(content)).into()
}
}

View file

@ -1,5 +1,5 @@
use iced::widget::container;
use iced::{Element, Length};
use iced::widget::center;
use iced::Element;
use numeric_input::numeric_input;
@ -27,10 +27,8 @@ impl Component {
}
fn view(&self) -> Element<Message> {
container(numeric_input(self.value, Message::NumericInputChanged))
center(numeric_input(self.value, Message::NumericInputChanged))
.padding(20)
.height(Length::Fill)
.center_y()
.into()
}
}
@ -73,10 +71,7 @@ mod numeric_input {
impl<Message, Theme> Component<Message, Theme> for NumericInput<Message>
where
Theme: text::DefaultStyle
+ button::DefaultStyle
+ text_input::DefaultStyle
+ 'static,
Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
{
type State = ();
type Event = Event;
@ -151,10 +146,7 @@ mod numeric_input {
impl<'a, Message, Theme> From<NumericInput<Message>>
for Element<'a, Message, Theme>
where
Theme: text::DefaultStyle
+ button::DefaultStyle
+ text_input::DefaultStyle
+ 'static,
Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
Message: 'a,
{
fn from(numeric_input: NumericInput<Message>) -> Self {

View file

@ -81,8 +81,8 @@ mod quad {
}
}
use iced::widget::{column, container, slider, text};
use iced::{Alignment, Color, Element, Length, Shadow, Vector};
use iced::widget::{center, column, slider, text};
use iced::{Alignment, Color, Element, Shadow, Vector};
pub fn main() -> iced::Result {
iced::run("Custom Quad - Iced", Example::update, Example::view)
@ -187,12 +187,7 @@ impl Example {
.max_width(500)
.align_items(Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
}

View file

@ -4,7 +4,7 @@ use scene::Scene;
use iced::time::Instant;
use iced::widget::shader::wgpu;
use iced::widget::{checkbox, column, container, row, shader, slider, text};
use iced::widget::{center, checkbox, column, row, shader, slider, text};
use iced::window;
use iced::{Alignment, Color, Element, Length, Subscription};
@ -123,12 +123,7 @@ impl IcedCubes {
let shader =
shader(&self.scene).width(Length::Fill).height(Length::Fill);
container(column![shader, controls].align_items(Alignment::Center))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(column![shader, controls].align_items(Alignment::Center)).into()
}
fn subscription(&self) -> Subscription<Message> {

View file

@ -9,8 +9,8 @@ use pipeline::cube::{self, Cube};
use iced::mouse;
use iced::time::Duration;
use iced::widget::shader;
use iced::{Color, Rectangle, Size};
use iced::widget::shader::{self, Viewport};
use iced::{Color, Rectangle};
use glam::Vec3;
use rand::Rng;
@ -130,25 +130,29 @@ impl Primitive {
impl shader::Primitive for Primitive {
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
_bounds: Rectangle,
target_size: Size<u32>,
_scale_factor: f32,
format: wgpu::TextureFormat,
storage: &mut shader::Storage,
_bounds: &Rectangle,
viewport: &Viewport,
) {
if !storage.has::<Pipeline>() {
storage.store(Pipeline::new(device, queue, format, target_size));
storage.store(Pipeline::new(
device,
queue,
format,
viewport.physical_size(),
));
}
let pipeline = storage.get_mut::<Pipeline>().unwrap();
//upload data to GPU
// Upload data to GPU
pipeline.update(
device,
queue,
target_size,
viewport.physical_size(),
&self.uniforms,
self.cubes.len(),
&self.cubes,
@ -157,20 +161,19 @@ impl shader::Primitive for Primitive {
fn render(
&self,
encoder: &mut wgpu::CommandEncoder,
storage: &shader::Storage,
target: &wgpu::TextureView,
_target_size: Size<u32>,
viewport: Rectangle<u32>,
encoder: &mut wgpu::CommandEncoder,
clip_bounds: &Rectangle<u32>,
) {
//at this point our pipeline should always be initialized
// At this point our pipeline should always be initialized
let pipeline = storage.get::<Pipeline>().unwrap();
//render primitive
// Render primitive
pipeline.render(
target,
encoder,
viewport,
*clip_bounds,
self.cubes.len() as u32,
self.show_depth_buffer,
);

View file

@ -82,8 +82,8 @@ mod circle {
}
use circle::circle;
use iced::widget::{column, container, slider, text};
use iced::{Alignment, Element, Length};
use iced::widget::{center, column, slider, text};
use iced::{Alignment, Element};
pub fn main() -> iced::Result {
iced::run("Custom Widget - Iced", Example::update, Example::view)
@ -122,12 +122,7 @@ impl Example {
.max_width(500)
.align_items(Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
}

View file

@ -12,12 +12,6 @@ pub fn file<I: 'static + Hash + Copy + Send + Sync, T: ToString>(
})
}
#[derive(Debug, Hash, Clone)]
pub struct Download<I> {
id: I,
url: String,
}
async fn download<I: Copy>(id: I, state: State) -> ((I, Progress), State) {
match state {
State::Ready(url) => {

View file

@ -1,7 +1,7 @@
mod download;
use iced::widget::{button, column, container, progress_bar, text, Column};
use iced::{Alignment, Element, Length, Subscription};
use iced::widget::{button, center, column, progress_bar, text, Column};
use iced::{Alignment, Element, Subscription};
pub fn main() -> iced::Result {
iced::program("Download Progress - Iced", Example::update, Example::view)
@ -67,13 +67,7 @@ impl Example {
.spacing(20)
.align_items(Alignment::End);
container(downloads)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.padding(20)
.into()
center(downloads).padding(20).into()
}
}

View file

@ -277,7 +277,7 @@ fn action<'a, Message: Clone + 'a>(
label: &'a str,
on_press: Option<Message>,
) -> Element<'a, Message> {
let action = button(container(content).width(30).center_x());
let action = button(container(content).center_x(30));
if let Some(on_press) = on_press {
tooltip(

View file

@ -1,6 +1,6 @@
use iced::alignment;
use iced::event::{self, Event};
use iced::widget::{button, checkbox, container, text, Column};
use iced::widget::{button, center, checkbox, text, Column};
use iced::window;
use iced::{Alignment, Command, Element, Length, Subscription};
@ -84,11 +84,6 @@ impl Events {
.push(toggle)
.push(exit);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
}

View file

@ -1,6 +1,6 @@
use iced::widget::{button, column, container};
use iced::widget::{button, center, column};
use iced::window;
use iced::{Alignment, Command, Element, Length};
use iced::{Alignment, Command, Element};
pub fn main() -> iced::Result {
iced::program("Exit - Iced", Exit::update, Exit::view).run()
@ -46,12 +46,6 @@ impl Exit {
.spacing(10)
.align_items(Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.padding(20)
.center_x()
.center_y()
.into()
center(content).padding(20).into()
}
}

View file

@ -0,0 +1,10 @@
[package]
name = "ferris"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["image", "tokio", "debug"]

211
examples/ferris/src/main.rs Normal file
View file

@ -0,0 +1,211 @@
use iced::time::Instant;
use iced::widget::{
center, checkbox, column, container, image, pick_list, row, slider, text,
};
use iced::window;
use iced::{
Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation,
Subscription, Theme,
};
pub fn main() -> iced::Result {
iced::program("Ferris - Iced", Image::update, Image::view)
.subscription(Image::subscription)
.theme(|_| Theme::TokyoNight)
.run()
}
struct Image {
width: f32,
opacity: f32,
rotation: Rotation,
content_fit: ContentFit,
spin: bool,
last_tick: Instant,
}
#[derive(Debug, Clone, Copy)]
enum Message {
WidthChanged(f32),
OpacityChanged(f32),
RotationStrategyChanged(RotationStrategy),
RotationChanged(Degrees),
ContentFitChanged(ContentFit),
SpinToggled(bool),
RedrawRequested(Instant),
}
impl Image {
fn update(&mut self, message: Message) {
match message {
Message::WidthChanged(width) => {
self.width = width;
}
Message::OpacityChanged(opacity) => {
self.opacity = opacity;
}
Message::RotationStrategyChanged(strategy) => {
self.rotation = match strategy {
RotationStrategy::Floating => {
Rotation::Floating(self.rotation.radians())
}
RotationStrategy::Solid => {
Rotation::Solid(self.rotation.radians())
}
};
}
Message::RotationChanged(rotation) => {
self.rotation = match self.rotation {
Rotation::Floating(_) => {
Rotation::Floating(rotation.into())
}
Rotation::Solid(_) => Rotation::Solid(rotation.into()),
};
}
Message::ContentFitChanged(content_fit) => {
self.content_fit = content_fit;
}
Message::SpinToggled(spin) => {
self.spin = spin;
self.last_tick = Instant::now();
}
Message::RedrawRequested(now) => {
const ROTATION_SPEED: Degrees = Degrees(360.0);
let delta = (now - self.last_tick).as_millis() as f32 / 1_000.0;
*self.rotation.radians_mut() = (self.rotation.radians()
+ ROTATION_SPEED * delta)
% (2.0 * Radians::PI);
self.last_tick = now;
}
}
}
fn subscription(&self) -> Subscription<Message> {
if self.spin {
window::frames().map(Message::RedrawRequested)
} else {
Subscription::none()
}
}
fn view(&self) -> Element<Message> {
let i_am_ferris = column![
"Hello!",
Element::from(
image(format!(
"{}/../tour/images/ferris.png",
env!("CARGO_MANIFEST_DIR")
))
.width(self.width)
.content_fit(self.content_fit)
.rotation(self.rotation)
.opacity(self.opacity)
)
.explain(Color::WHITE),
"I am Ferris!"
]
.spacing(20)
.align_items(Alignment::Center);
let fit = row![
pick_list(
[
ContentFit::Contain,
ContentFit::Cover,
ContentFit::Fill,
ContentFit::None,
ContentFit::ScaleDown
],
Some(self.content_fit),
Message::ContentFitChanged
)
.width(Length::Fill),
pick_list(
[RotationStrategy::Floating, RotationStrategy::Solid],
Some(match self.rotation {
Rotation::Floating(_) => RotationStrategy::Floating,
Rotation::Solid(_) => RotationStrategy::Solid,
}),
Message::RotationStrategyChanged,
)
.width(Length::Fill),
]
.spacing(10)
.align_items(Alignment::End);
let properties = row![
with_value(
slider(100.0..=500.0, self.width, Message::WidthChanged),
format!("Width: {}px", self.width)
),
with_value(
slider(0.0..=1.0, self.opacity, Message::OpacityChanged)
.step(0.01),
format!("Opacity: {:.2}", self.opacity)
),
with_value(
row![
slider(
Degrees::RANGE,
self.rotation.degrees(),
Message::RotationChanged
),
checkbox("Spin!", self.spin)
.text_size(12)
.on_toggle(Message::SpinToggled)
.size(12)
]
.spacing(10)
.align_items(Alignment::Center),
format!("Rotation: {:.0}°", f32::from(self.rotation.degrees()))
)
]
.spacing(10)
.align_items(Alignment::End);
container(column![fit, center(i_am_ferris), properties].spacing(10))
.padding(10)
.into()
}
}
impl Default for Image {
fn default() -> Self {
Self {
width: 300.0,
opacity: 1.0,
rotation: Rotation::default(),
content_fit: ContentFit::default(),
spin: false,
last_tick: Instant::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RotationStrategy {
Floating,
Solid,
}
impl std::fmt::Display for RotationStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Floating => "Floating",
Self::Solid => "Solid",
})
}
}
fn with_value<'a>(
control: impl Into<Element<'a, Message>>,
value: String,
) -> Element<'a, Message> {
column![control.into(), text(value).size(12).line_height(1.0)]
.spacing(2)
.align_items(Alignment::Center)
.into()
}

View file

@ -602,9 +602,7 @@ mod grid {
frame.into_geometry()
};
if self.scaling < 0.2 || !self.show_lines {
vec![life, overlay]
} else {
if self.scaling >= 0.2 && self.show_lines {
let grid =
self.grid_cache.draw(renderer, bounds.size(), |frame| {
frame.translate(center);
@ -641,6 +639,8 @@ mod grid {
});
vec![life, grid, overlay]
} else {
vec![life, overlay]
}
}

View file

@ -6,7 +6,10 @@ mod rainbow {
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
use iced::mouse;
use iced::{Element, Length, Rectangle, Renderer, Size, Theme, Vector};
use iced::{
Element, Length, Rectangle, Renderer, Size, Theme, Transformation,
Vector,
};
#[derive(Debug, Clone, Copy, Default)]
pub struct Rainbow;
@ -44,7 +47,9 @@ mod rainbow {
cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
use iced::advanced::graphics::mesh::{self, Mesh, SolidVertex2D};
use iced::advanced::graphics::mesh::{
self, Mesh, Renderer as _, SolidVertex2D,
};
use iced::advanced::Renderer as _;
let bounds = layout.bounds();
@ -77,7 +82,6 @@ mod rainbow {
let posn_l = [0.0, bounds.height / 2.0];
let mesh = Mesh::Solid {
size: bounds.size(),
buffers: mesh::Indexed {
vertices: vec![
SolidVertex2D {
@ -128,6 +132,8 @@ mod rainbow {
0, 8, 1, // L
],
},
transformation: Transformation::IDENTITY,
clip_bounds: Rectangle::INFINITE,
};
renderer.with_translation(
@ -170,12 +176,7 @@ fn view(_state: &()) -> Element<'_, ()> {
.spacing(20)
.max_width(500);
let scrollable =
scrollable(container(content).width(Length::Fill).center_x());
let scrollable = scrollable(container(content).center_x(Length::Fill));
container(scrollable)
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.into()
container(scrollable).center_y(Length::Fill).into()
}

View file

@ -60,7 +60,7 @@ impl Gradient {
} = *self;
let gradient_box = container(horizontal_space())
.style(move |_theme, _status| {
.style(move |_theme| {
let gradient = gradient::Linear::new(angle)
.add_stop(0.0, start)
.add_stop(1.0, end);

View file

@ -8,7 +8,9 @@ publish = false
[dependencies]
iced_winit.workspace = true
iced_wgpu.workspace = true
iced_widget.workspace = true
iced_widget.features = ["wgpu"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"

View file

@ -10,25 +10,8 @@ The __[`main`]__ file contains all the code of the example.
You can run it with `cargo run`:
```
cargo run --package integration_wgpu
cargo run --package integration
```
### How to run this example with WebGL backend
NOTE: Currently, WebGL backend is still experimental, so expect bugs.
```sh
# 0. Install prerequisites
cargo install wasm-bindgen-cli https
# 1. cd to the current folder
# 2. Compile wasm module
cargo build -p integration_wgpu --target wasm32-unknown-unknown
# 3. Invoke wasm-bindgen
wasm-bindgen ../../target/wasm32-unknown-unknown/debug/integration_wgpu.wasm --out-dir . --target web --no-typescript
# 4. run http server
http
# 5. Open 127.0.0.1:8000 in browser
```
[`main`]: src/main.rs
[`wgpu`]: https://github.com/gfx-rs/wgpu

View file

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>Iced - wgpu + WebGL integration</title>
</head>
<body>
<h1>integration_wgpu</h1>
<canvas id="iced_canvas"></canvas>
<script type="module">
import init from "./integration.js";
init('./integration_bg.wasm');
</script>
<style>
body {
width: 100%;
text-align: center;
}
</style>
</body>
</html>

View file

@ -5,7 +5,7 @@ use controls::Controls;
use scene::Scene;
use iced_wgpu::graphics::Viewport;
use iced_wgpu::{wgpu, Backend, Renderer, Settings};
use iced_wgpu::{wgpu, Engine, Renderer};
use iced_winit::conversion;
use iced_winit::core::mouse;
use iced_winit::core::renderer;
@ -18,309 +18,342 @@ use iced_winit::winit;
use iced_winit::Clipboard;
use winit::{
event::{Event, WindowEvent},
event::WindowEvent,
event_loop::{ControlFlow, EventLoop},
keyboard::ModifiersState,
};
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use web_sys::HtmlCanvasElement;
#[cfg(target_arch = "wasm32")]
use winit::platform::web::WindowBuilderExtWebSys;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_arch = "wasm32")]
let canvas_element = {
console_log::init().expect("Initialize logger");
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.get_element_by_id("iced_canvas"))
.and_then(|element| element.dyn_into::<HtmlCanvasElement>().ok())
.expect("Get canvas element")
};
#[cfg(not(target_arch = "wasm32"))]
pub fn main() -> Result<(), winit::error::EventLoopError> {
tracing_subscriber::fmt::init();
// Initialize winit
let event_loop = EventLoop::new()?;
#[cfg(target_arch = "wasm32")]
let window = winit::window::WindowBuilder::new()
.with_canvas(Some(canvas_element))
.build(&event_loop)?;
#[allow(clippy::large_enum_variant)]
enum Runner {
Loading,
Ready {
window: Arc<winit::window::Window>,
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
format: wgpu::TextureFormat,
engine: Engine,
renderer: Renderer,
scene: Scene,
state: program::State<Controls>,
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
clipboard: Clipboard,
viewport: Viewport,
modifiers: ModifiersState,
resized: bool,
debug: Debug,
},
}
#[cfg(not(target_arch = "wasm32"))]
let window = winit::window::Window::new(&event_loop)?;
impl winit::application::ApplicationHandler for Runner {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if let Self::Loading = self {
let window = Arc::new(
event_loop
.create_window(
winit::window::WindowAttributes::default(),
)
.expect("Create window"),
);
let window = Arc::new(window);
let physical_size = window.inner_size();
let viewport = Viewport::with_physical_size(
Size::new(physical_size.width, physical_size.height),
window.scale_factor(),
);
let clipboard = Clipboard::connect(&window);
let physical_size = window.inner_size();
let mut viewport = Viewport::with_physical_size(
Size::new(physical_size.width, physical_size.height),
window.scale_factor(),
);
let mut cursor_position = None;
let mut modifiers = ModifiersState::default();
let mut clipboard = Clipboard::connect(&window);
let backend =
wgpu::util::backend_bits_from_env().unwrap_or_default();
// Initialize wgpu
#[cfg(target_arch = "wasm32")]
let default_backend = wgpu::Backends::GL;
#[cfg(not(target_arch = "wasm32"))]
let default_backend = wgpu::Backends::PRIMARY;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: backend,
..Default::default()
});
let surface = instance
.create_surface(window.clone())
.expect("Create window surface");
let backend =
wgpu::util::backend_bits_from_env().unwrap_or(default_backend);
let (format, adapter, device, queue) =
futures::futures::executor::block_on(async {
let adapter =
wgpu::util::initialize_adapter_from_env_or_default(
&instance,
Some(&surface),
)
.await
.expect("Create adapter");
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: backend,
..Default::default()
});
let surface = instance.create_surface(window.clone())?;
let adapter_features = adapter.features();
let (format, adapter, device, queue) =
futures::futures::executor::block_on(async {
let adapter = wgpu::util::initialize_adapter_from_env_or_default(
&instance,
Some(&surface),
)
.await
.expect("Create adapter");
let capabilities = surface.get_capabilities(&adapter);
let adapter_features = adapter.features();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: adapter_features
& wgpu::Features::default(),
required_limits: wgpu::Limits::default(),
},
None,
)
.await
.expect("Request device");
#[cfg(target_arch = "wasm32")]
let needed_limits = wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits());
(
capabilities
.formats
.iter()
.copied()
.find(wgpu::TextureFormat::is_srgb)
.or_else(|| {
capabilities.formats.first().copied()
})
.expect("Get preferred format"),
adapter,
device,
queue,
)
});
#[cfg(not(target_arch = "wasm32"))]
let needed_limits = wgpu::Limits::default();
let capabilities = surface.get_capabilities(&adapter);
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: adapter_features
& wgpu::Features::default(),
required_limits: needed_limits,
surface.configure(
&device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: physical_size.width,
height: physical_size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
None,
)
.await
.expect("Request device");
);
(
capabilities
.formats
.iter()
.copied()
.find(wgpu::TextureFormat::is_srgb)
.or_else(|| capabilities.formats.first().copied())
.expect("Get preferred format"),
adapter,
// Initialize scene and GUI controls
let scene = Scene::new(&device, format);
let controls = Controls::new();
// Initialize iced
let mut debug = Debug::new();
let engine =
Engine::new(&adapter, &device, &queue, format, None);
let mut renderer = Renderer::new(
&device,
&engine,
Font::default(),
Pixels::from(16),
);
let state = program::State::new(
controls,
viewport.logical_size(),
&mut renderer,
&mut debug,
);
// You should change this if you want to render continuously
event_loop.set_control_flow(ControlFlow::Wait);
*self = Self::Ready {
window,
device,
queue,
surface,
format,
engine,
renderer,
scene,
state,
cursor_position: None,
modifiers: ModifiersState::default(),
clipboard,
viewport,
resized: false,
debug,
};
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
event: WindowEvent,
) {
let Self::Ready {
window,
device,
queue,
)
});
surface,
format,
engine,
renderer,
scene,
state,
viewport,
cursor_position,
modifiers,
clipboard,
resized,
debug,
} = self
else {
return;
};
surface.configure(
&device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: physical_size.width,
height: physical_size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
match event {
WindowEvent::RedrawRequested => {
if *resized {
let size = window.inner_size();
let mut resized = false;
// Initialize scene and GUI controls
let scene = Scene::new(&device, format);
let controls = Controls::new();
// Initialize iced
let mut debug = Debug::new();
let mut renderer = Renderer::new(
Backend::new(&adapter, &device, &queue, Settings::default(), format),
Font::default(),
Pixels(16.0),
);
let mut state = program::State::new(
controls,
viewport.logical_size(),
&mut renderer,
&mut debug,
);
// Run event loop
event_loop.run(move |event, window_target| {
// You should change this if you want to render continuously
window_target.set_control_flow(ControlFlow::Wait);
match event {
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
if resized {
let size = window.inner_size();
viewport = Viewport::with_physical_size(
Size::new(size.width, size.height),
window.scale_factor(),
);
surface.configure(
&device,
&wgpu::SurfaceConfiguration {
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
resized = false;
}
match surface.get_current_texture() {
Ok(frame) => {
let mut encoder = device.create_command_encoder(
&wgpu::CommandEncoderDescriptor { label: None },
*viewport = Viewport::with_physical_size(
Size::new(size.width, size.height),
window.scale_factor(),
);
let program = state.program();
let view = frame.texture.create_view(
&wgpu::TextureViewDescriptor::default(),
surface.configure(
device,
&wgpu::SurfaceConfiguration {
format: *format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
{
// We clear the frame
let mut render_pass = Scene::clear(
&view,
&mut encoder,
program.background_color(),
*resized = false;
}
match surface.get_current_texture() {
Ok(frame) => {
let mut encoder = device.create_command_encoder(
&wgpu::CommandEncoderDescriptor { label: None },
);
// Draw the scene
scene.draw(&mut render_pass);
}
let program = state.program();
// And then iced on top
renderer.with_primitives(|backend, primitive| {
backend.present(
&device,
&queue,
let view = frame.texture.create_view(
&wgpu::TextureViewDescriptor::default(),
);
{
// We clear the frame
let mut render_pass = Scene::clear(
&view,
&mut encoder,
program.background_color(),
);
// Draw the scene
scene.draw(&mut render_pass);
}
// And then iced on top
renderer.present(
engine,
device,
queue,
&mut encoder,
None,
frame.texture.format(),
&view,
primitive,
&viewport,
viewport,
&debug.overlay(),
);
});
// Then we submit the work
queue.submit(Some(encoder.finish()));
frame.present();
// Then we submit the work
engine.submit(queue, encoder);
frame.present();
// Update the mouse cursor
window.set_cursor_icon(
iced_winit::conversion::mouse_interaction(
state.mouse_interaction(),
),
);
}
Err(error) => match error {
wgpu::SurfaceError::OutOfMemory => {
panic!(
"Swapchain error: {error}. \
// Update the mouse cursor
window.set_cursor(
iced_winit::conversion::mouse_interaction(
state.mouse_interaction(),
),
);
}
Err(error) => match error {
wgpu::SurfaceError::OutOfMemory => {
panic!(
"Swapchain error: {error}. \
Rendering cannot continue."
)
}
_ => {
// Try rendering again next frame.
window.request_redraw();
}
},
}
}
WindowEvent::CursorMoved { position, .. } => {
*cursor_position = Some(position);
}
WindowEvent::ModifiersChanged(new_modifiers) => {
*modifiers = new_modifiers.state();
}
WindowEvent::Resized(_) => {
*resized = true;
}
WindowEvent::CloseRequested => {
event_loop.exit();
}
_ => {}
}
// Map window event to iced event
if let Some(event) = iced_winit::conversion::window_event(
window::Id::MAIN,
event,
window.scale_factor(),
*modifiers,
) {
state.queue_event(event);
}
// If there are events pending
if !state.is_queue_empty() {
// We update iced
let _ = state.update(
viewport.logical_size(),
cursor_position
.map(|p| {
conversion::cursor_position(
p,
viewport.scale_factor(),
)
}
_ => {
// Try rendering again next frame.
window.request_redraw();
}
})
.map(mouse::Cursor::Available)
.unwrap_or(mouse::Cursor::Unavailable),
renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
}
clipboard,
debug,
);
// and request a redraw
window.request_redraw();
}
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::CursorMoved { position, .. } => {
cursor_position = Some(position);
}
WindowEvent::ModifiersChanged(new_modifiers) => {
modifiers = new_modifiers.state();
}
WindowEvent::Resized(_) => {
resized = true;
}
WindowEvent::CloseRequested => {
window_target.exit();
}
_ => {}
}
// Map window event to iced event
if let Some(event) = iced_winit::conversion::window_event(
window::Id::MAIN,
event,
window.scale_factor(),
modifiers,
) {
state.queue_event(event);
}
}
_ => {}
}
}
// If there are events pending
if !state.is_queue_empty() {
// We update iced
let _ = state.update(
viewport.logical_size(),
cursor_position
.map(|p| {
conversion::cursor_position(p, viewport.scale_factor())
})
.map(mouse::Cursor::Available)
.unwrap_or(mouse::Cursor::Unavailable),
&mut renderer,
&Theme::Dark,
&renderer::Style {
text_color: Color::WHITE,
},
&mut clipboard,
&mut debug,
);
// and request a redraw
window.request_redraw();
}
})?;
Ok(())
let mut runner = Runner::Loading;
event_loop.run_app(&mut runner)
}

View file

@ -1,8 +1,8 @@
use iced::keyboard;
use iced::mouse;
use iced::widget::{
button, canvas, checkbox, column, container, horizontal_space, pick_list,
row, scrollable, text,
button, canvas, center, checkbox, column, container, horizontal_space,
pick_list, row, scrollable, text,
};
use iced::{
color, Alignment, Element, Font, Length, Point, Rectangle, Renderer,
@ -76,22 +76,18 @@ impl Layout {
.spacing(20)
.align_items(Alignment::Center);
let example = container(if self.explain {
let example = center(if self.explain {
self.example.view().explain(color!(0x0000ff))
} else {
self.example.view()
})
.style(|theme, _status| {
.style(|theme| {
let palette = theme.extended_palette();
container::Appearance::default()
container::Style::default()
.with_border(palette.background.strong.color, 4.0)
})
.padding(4)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y();
.padding(4);
let controls = row([
(!self.example.is_first()).then_some(
@ -195,12 +191,7 @@ impl Default for Example {
}
fn centered<'a>() -> Element<'a, Message> {
container(text("I am centered!").size(50))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(text("I am centered!").size(50)).into()
}
fn column_<'a>() -> Element<'a, Message> {
@ -245,10 +236,10 @@ fn application<'a>() -> Element<'a, Message> {
.padding(10)
.align_items(Alignment::Center),
)
.style(|theme, _status| {
.style(|theme| {
let palette = theme.extended_palette();
container::Appearance::default()
container::Style::default()
.with_border(palette.background.strong.color, 1)
});
@ -260,8 +251,7 @@ fn application<'a>() -> Element<'a, Message> {
.align_items(Alignment::Center),
)
.style(container::rounded_box)
.height(Length::Fill)
.center_y();
.center_y(Length::Fill);
let content = container(
scrollable(

View file

@ -173,7 +173,7 @@ impl App {
.style(button::danger);
row![
text(&item.name).color(item.color),
text(item.name.clone()).color(item.color),
horizontal_space(),
pick_list(Color::ALL, Some(item.color), move |color| {
Message::ItemColorChanged(item.clone(), color)

View file

@ -358,7 +358,7 @@ where
|renderer| {
use iced::advanced::graphics::geometry::Renderer as _;
renderer.draw(vec![geometry]);
renderer.draw_geometry(geometry);
},
);
}

View file

@ -1,5 +1,5 @@
use iced::widget::{column, container, row, slider, text};
use iced::{Element, Length};
use iced::widget::{center, column, row, slider, text};
use iced::Element;
use std::time::Duration;
@ -73,7 +73,7 @@ impl LoadingSpinners {
})
.spacing(20);
container(
center(
column.push(
row![
text("Cycle duration:"),
@ -87,10 +87,6 @@ impl LoadingSpinners {
.spacing(20.0),
),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}

View file

@ -1,5 +1,5 @@
use iced::widget::{button, column, container, text};
use iced::{Alignment, Element, Length};
use iced::widget::{button, center, column, text};
use iced::{Alignment, Element};
use loupe::loupe;
@ -31,7 +31,7 @@ impl Loupe {
}
fn view(&self) -> Element<Message> {
container(loupe(
center(loupe(
3.0,
column![
button("Increment").on_press(Message::Increment),
@ -41,10 +41,6 @@ impl Loupe {
.padding(20)
.align_items(Alignment::Center),
))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
@ -159,7 +155,7 @@ mod loupe {
if cursor.is_over(layout.bounds()) {
mouse::Interaction::ZoomIn
} else {
mouse::Interaction::Idle
mouse::Interaction::None
}
}
}

View file

@ -2,12 +2,11 @@ use iced::event::{self, Event};
use iced::keyboard;
use iced::keyboard::key;
use iced::widget::{
self, button, column, container, horizontal_space, pick_list, row, text,
text_input,
self, button, center, column, container, horizontal_space, mouse_area,
opaque, pick_list, row, stack, text, text_input,
};
use iced::{Alignment, Command, Element, Length, Subscription};
use iced::{Alignment, Color, Command, Element, Length, Subscription};
use modal::Modal;
use std::fmt;
pub fn main() -> iced::Result {
@ -99,13 +98,7 @@ impl App {
row![text("Top Left"), horizontal_space(), text("Top Right")]
.align_items(Alignment::Start)
.height(Length::Fill),
container(
button(text("Show Modal")).on_press(Message::ShowModal)
)
.center_x()
.center_y()
.width(Length::Fill)
.height(Length::Fill),
center(button(text("Show Modal")).on_press(Message::ShowModal)),
row![
text("Bottom Left"),
horizontal_space(),
@ -116,12 +109,10 @@ impl App {
]
.height(Length::Fill),
)
.padding(10)
.width(Length::Fill)
.height(Length::Fill);
.padding(10);
if self.show_modal {
let modal = container(
let signup = container(
column![
text("Sign Up").size(24),
column![
@ -162,9 +153,7 @@ impl App {
.padding(10)
.style(container::rounded_box);
Modal::new(content, modal)
.on_blur(Message::HideModal)
.into()
modal(content, signup, Message::HideModal)
} else {
content.into()
}
@ -203,326 +192,29 @@ impl fmt::Display for Plan {
}
}
mod modal {
use iced::advanced::layout::{self, Layout};
use iced::advanced::overlay;
use iced::advanced::renderer;
use iced::advanced::widget::{self, Widget};
use iced::advanced::{self, Clipboard, Shell};
use iced::alignment::Alignment;
use iced::event;
use iced::mouse;
use iced::{Color, Element, Event, Length, Point, Rectangle, Size, Vector};
/// A widget that centers a modal element over some base element
pub struct Modal<'a, Message, Theme, Renderer> {
base: Element<'a, Message, Theme, Renderer>,
modal: Element<'a, Message, Theme, Renderer>,
on_blur: Option<Message>,
}
impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> {
/// Returns a new [`Modal`]
pub fn new(
base: impl Into<Element<'a, Message, Theme, Renderer>>,
modal: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self {
Self {
base: base.into(),
modal: modal.into(),
on_blur: None,
}
}
/// Sets the message that will be produces when the background
/// of the [`Modal`] is pressed
pub fn on_blur(self, on_blur: Message) -> Self {
Self {
on_blur: Some(on_blur),
..self
}
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Modal<'a, Message, Theme, Renderer>
where
Renderer: advanced::Renderer,
Message: Clone,
{
fn children(&self) -> Vec<widget::Tree> {
vec![
widget::Tree::new(&self.base),
widget::Tree::new(&self.modal),
]
}
fn diff(&self, tree: &mut widget::Tree) {
tree.diff_children(&[&self.base, &self.modal]);
}
fn size(&self) -> Size<Length> {
self.base.as_widget().size()
}
fn layout(
&self,
tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.base.as_widget().layout(
&mut tree.children[0],
renderer,
limits,
)
}
fn on_event(
&mut self,
state: &mut widget::Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.base.as_widget_mut().on_event(
&mut state.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
)
}
fn draw(
&self,
state: &widget::Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.base.as_widget().draw(
&state.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
state: &'b mut widget::Tree,
layout: Layout<'_>,
_renderer: &Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
Some(overlay::Element::new(Box::new(Overlay {
position: layout.position() + translation,
content: &mut self.modal,
tree: &mut state.children[1],
size: layout.bounds().size(),
on_blur: self.on_blur.clone(),
})))
}
fn mouse_interaction(
&self,
state: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.base.as_widget().mouse_interaction(
&state.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn operate(
&self,
state: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
self.base.as_widget().operate(
&mut state.children[0],
layout,
renderer,
operation,
);
}
}
struct Overlay<'a, 'b, Message, Theme, Renderer> {
position: Point,
content: &'b mut Element<'a, Message, Theme, Renderer>,
tree: &'b mut widget::Tree,
size: Size,
on_blur: Option<Message>,
}
impl<'a, 'b, Message, Theme, Renderer>
overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, 'b, Message, Theme, Renderer>
where
Renderer: advanced::Renderer,
Message: Clone,
{
fn layout(
&mut self,
renderer: &Renderer,
_bounds: Size,
) -> layout::Node {
let limits = layout::Limits::new(Size::ZERO, self.size)
.width(Length::Fill)
.height(Length::Fill);
let child = self
.content
.as_widget()
.layout(self.tree, renderer, &limits)
.align(Alignment::Center, Alignment::Center, limits.max());
layout::Node::with_children(self.size, vec![child])
.move_to(self.position)
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let content_bounds = layout.children().next().unwrap().bounds();
if let Some(message) = self.on_blur.as_ref() {
if let Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)) = &event
{
if !cursor.is_over(content_bounds) {
shell.publish(message.clone());
return event::Status::Captured;
fn modal<'a, Message>(
base: impl Into<Element<'a, Message>>,
content: impl Into<Element<'a, Message>>,
on_blur: Message,
) -> Element<'a, Message>
where
Message: Clone + 'a,
{
stack![
base.into(),
mouse_area(center(opaque(content)).style(|_theme| {
container::Style {
background: Some(
Color {
a: 0.8,
..Color::BLACK
}
}
.into(),
),
..container::Style::default()
}
self.content.as_widget_mut().on_event(
self.tree,
event,
layout.children().next().unwrap(),
cursor,
renderer,
clipboard,
shell,
&layout.bounds(),
)
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
..renderer::Quad::default()
},
Color {
a: 0.80,
..Color::BLACK
},
);
self.content.as_widget().draw(
self.tree,
renderer,
theme,
style,
layout.children().next().unwrap(),
cursor,
&layout.bounds(),
);
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
self.content.as_widget().operate(
self.tree,
layout.children().next().unwrap(),
renderer,
operation,
);
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
self.tree,
layout.children().next().unwrap(),
cursor,
viewport,
renderer,
)
}
fn overlay<'c>(
&'c mut self,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'c, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
self.tree,
layout.children().next().unwrap(),
renderer,
Vector::ZERO,
)
}
}
impl<'a, Message, Theme, Renderer> From<Modal<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Theme: 'a,
Message: 'a + Clone,
Renderer: 'a + advanced::Renderer,
{
fn from(modal: Modal<'a, Message, Theme, Renderer>) -> Self {
Element::new(modal)
}
}
}))
.on_press(on_blur)
]
.into()
}

View file

@ -1,7 +1,9 @@
use iced::event;
use iced::executor;
use iced::multi_window::{self, Application};
use iced::widget::{button, column, container, scrollable, text, text_input};
use iced::widget::{
button, center, column, container, scrollable, text, text_input,
};
use iced::window;
use iced::{
Alignment, Command, Element, Length, Point, Settings, Subscription, Theme,
@ -128,12 +130,7 @@ impl multi_window::Application for Example {
fn view(&self, window: window::Id) -> Element<Message> {
let content = self.windows.get(&window).unwrap().view(window);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
fn theme(&self, window: window::Id) -> Self::Theme {
@ -210,6 +207,6 @@ impl Window {
.align_items(Alignment::Center),
);
container(content).width(200).center_x().into()
container(content).center_x(200).into()
}
}

View file

@ -290,10 +290,8 @@ fn view_content<'a>(
.align_items(Alignment::Center);
container(scrollable(content))
.width(Length::Fill)
.height(Length::Fill)
.center_y(Length::Fill)
.padding(5)
.center_y()
.into()
}
@ -336,39 +334,30 @@ mod style {
use iced::widget::container;
use iced::{Border, Theme};
pub fn title_bar_active(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
pub fn title_bar_active(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Appearance {
container::Style {
text_color: Some(palette.background.strong.text),
background: Some(palette.background.strong.color.into()),
..Default::default()
}
}
pub fn title_bar_focused(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
pub fn title_bar_focused(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Appearance {
container::Style {
text_color: Some(palette.primary.strong.text),
background: Some(palette.primary.strong.color.into()),
..Default::default()
}
}
pub fn pane_active(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
pub fn pane_active(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Appearance {
container::Style {
background: Some(palette.background.weak.color.into()),
border: Border {
width: 2.0,
@ -379,13 +368,10 @@ mod style {
}
}
pub fn pane_focused(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
pub fn pane_focused(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Appearance {
container::Style {
background: Some(palette.background.weak.color.into()),
border: Border {
width: 2.0,

View file

@ -1,5 +1,5 @@
use iced::futures;
use iced::widget::{self, column, container, image, row, text};
use iced::widget::{self, center, column, image, row, text};
use iced::{Alignment, Command, Element, Length};
pub fn main() -> iced::Result {
@ -83,12 +83,7 @@ impl Pokedex {
.align_items(Alignment::End),
};
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
}
@ -186,7 +181,7 @@ impl Pokemon {
{
let bytes = reqwest::get(&url).await?.bytes().await?;
Ok(image::Handle::from_memory(bytes))
Ok(image::Handle::from_bytes(bytes))
}
#[cfg(target_arch = "wasm32")]

View file

@ -1,7 +1,5 @@
use iced::widget::{
column, container, pick_list, qr_code, row, text, text_input,
};
use iced::{Alignment, Element, Length, Theme};
use iced::widget::{center, column, pick_list, qr_code, row, text, text_input};
use iced::{Alignment, Element, Theme};
pub fn main() -> iced::Result {
iced::program(
@ -72,13 +70,7 @@ impl QRGenerator {
.spacing(20)
.align_items(Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.padding(20)
.center_x()
.center_y()
.into()
center(content).padding(20).into()
}
fn theme(&self) -> Theme {

View file

@ -109,7 +109,7 @@ impl Example {
fn view(&self) -> Element<'_, Message> {
let image: Element<Message> = if let Some(screenshot) = &self.screenshot
{
image(image::Handle::from_pixels(
image(image::Handle::from_rgba(
screenshot.size.width,
screenshot.size.height,
screenshot.clone(),
@ -123,12 +123,9 @@ impl Example {
};
let image = container(image)
.center_y(Length::FillPortion(2))
.padding(10)
.style(container::rounded_box)
.width(Length::FillPortion(2))
.height(Length::Fill)
.center_x()
.center_y();
.style(container::rounded_box);
let crop_origin_controls = row![
text("X:")
@ -213,12 +210,7 @@ impl Example {
.spacing(40)
};
let side_content = container(controls)
.align_x(alignment::Horizontal::Center)
.width(Length::FillPortion(1))
.height(Length::Fill)
.center_y()
.center_x();
let side_content = container(controls).center_y(Length::Fill);
let content = row![side_content, image]
.spacing(10)
@ -226,13 +218,7 @@ impl Example {
.height(Length::Fill)
.align_items(Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.padding(10)
.center_x()
.center_y()
.into()
container(content).padding(10).into()
}
fn subscription(&self) -> Subscription<Message> {

View file

@ -327,7 +327,7 @@ impl ScrollableDemo {
.spacing(10)
.into();
container(content).padding(20).center_x().center_y().into()
container(content).padding(20).into()
}
fn theme(&self) -> Theme {
@ -341,8 +341,8 @@ impl Default for ScrollableDemo {
}
}
fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance {
progress_bar::Appearance {
fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Style {
progress_bar::Style {
background: theme.extended_palette().background.strong.color.into(),
bar: Color::from_rgb8(250, 85, 134).into(),
border: Border::default(),

View file

@ -1,6 +1,6 @@
use iced::mouse;
use iced::widget::canvas::event::{self, Event};
use iced::widget::canvas::{self, Canvas};
use iced::widget::canvas::{self, Canvas, Geometry};
use iced::widget::{column, row, slider, text};
use iced::{Color, Length, Point, Rectangle, Renderer, Size, Theme};
@ -111,7 +111,7 @@ impl canvas::Program<Message> for SierpinskiGraph {
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
) -> Vec<Geometry> {
let geom = self.cache.draw(renderer, bounds.size(), |frame| {
frame.stroke(
&canvas::Path::rectangle(Point::ORIGIN, frame.size()),

View file

@ -1,4 +1,4 @@
use iced::widget::{column, container, slider, text, vertical_slider};
use iced::widget::{center, column, container, slider, text, vertical_slider};
use iced::{Element, Length};
pub fn main() -> iced::Result {
@ -54,18 +54,14 @@ impl Slider {
let text = text(self.value);
container(
center(
column![
container(v_slider).width(Length::Fill).center_x(),
container(h_slider).width(Length::Fill).center_x(),
container(text).width(Length::Fill).center_x(),
container(v_slider).center_x(Length::Fill),
container(h_slider).center_x(Length::Fill),
container(text).center_x(Length::Fill)
]
.spacing(25),
)
.height(Length::Fill)
.width(Length::Fill)
.center_x()
.center_y()
.into()
}
}

View file

@ -10,7 +10,7 @@ use iced::mouse;
use iced::widget::canvas;
use iced::widget::canvas::gradient;
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::Path;
use iced::widget::canvas::{Geometry, Path};
use iced::window;
use iced::{
Color, Element, Length, Point, Rectangle, Renderer, Size, Subscription,
@ -130,7 +130,7 @@ impl<Message> canvas::Program<Message> for State {
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
) -> Vec<Geometry> {
use std::f32::consts::PI;
let background =

View file

@ -1,8 +1,8 @@
use iced::alignment;
use iced::keyboard;
use iced::time;
use iced::widget::{button, column, container, row, text};
use iced::{Alignment, Element, Length, Subscription, Theme};
use iced::widget::{button, center, column, row, text};
use iced::{Alignment, Element, Subscription, Theme};
use std::time::{Duration, Instant};
@ -128,12 +128,7 @@ impl Stopwatch {
.align_items(Alignment::Center)
.spacing(20);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
fn theme(&self) -> Theme {

View file

@ -1,7 +1,7 @@
use iced::widget::{
button, checkbox, column, container, horizontal_rule, pick_list,
progress_bar, row, scrollable, slider, text, text_input, toggler,
vertical_rule, vertical_space,
button, center, checkbox, column, horizontal_rule, pick_list, progress_bar,
row, scrollable, slider, text, text_input, toggler, vertical_rule,
vertical_space,
};
use iced::{Alignment, Element, Length, Theme};
@ -106,12 +106,7 @@ impl Styling {
.padding(20)
.max_width(600);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content).into()
}
fn theme(&self) -> Theme {

View file

@ -1,4 +1,4 @@
use iced::widget::{checkbox, column, container, svg};
use iced::widget::{center, checkbox, column, container, svg};
use iced::{color, Element, Length};
pub fn main() -> iced::Result {
@ -31,7 +31,7 @@ impl Tiger {
));
let svg = svg(handle).width(Length::Fill).height(Length::Fill).style(
|_theme, _status| svg::Appearance {
|_theme, _status| svg::Style {
color: if self.apply_color_filter {
Some(color!(0x0000ff))
} else {
@ -44,19 +44,12 @@ impl Tiger {
checkbox("Apply a color filter", self.apply_color_filter)
.on_toggle(Message::ToggleColorFilter);
container(
column![
svg,
container(apply_color_filter).width(Length::Fill).center_x()
]
.spacing(20)
.height(Length::Fill),
center(
column![svg, container(apply_color_filter).center_x(Length::Fill)]
.spacing(20)
.height(Length::Fill),
)
.width(Length::Fill)
.height(Length::Fill)
.padding(20)
.center_x()
.center_y()
.into()
}
}

View file

@ -1,5 +1,5 @@
use iced::widget::{button, column, container, text};
use iced::{system, Command, Element, Length};
use iced::widget::{button, center, column, text};
use iced::{system, Command, Element};
pub fn main() -> iced::Result {
iced::program("System Information - Iced", Example::update, Example::view)
@ -132,11 +132,6 @@ impl Example {
}
};
container(content)
.center_x()
.center_y()
.width(Length::Fill)
.height(Length::Fill)
.into()
center(content).into()
}
}

View file

@ -0,0 +1,13 @@
[package]
name = "the_matrix"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced.workspace = true
iced.features = ["canvas", "tokio", "debug"]
rand = "0.8"
tracing-subscriber = "0.3"

View file

@ -0,0 +1,115 @@
use iced::mouse;
use iced::time::{self, Instant};
use iced::widget::canvas;
use iced::{
Color, Element, Font, Length, Point, Rectangle, Renderer, Subscription,
Theme,
};
use std::cell::RefCell;
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
iced::program("The Matrix - Iced", TheMatrix::update, TheMatrix::view)
.subscription(TheMatrix::subscription)
.antialiasing(true)
.run()
}
#[derive(Default)]
struct TheMatrix {
tick: usize,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Tick(Instant),
}
impl TheMatrix {
fn update(&mut self, message: Message) {
match message {
Message::Tick(_now) => {
self.tick += 1;
}
}
}
fn view(&self) -> Element<Message> {
canvas(self as &Self)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn subscription(&self) -> Subscription<Message> {
time::every(std::time::Duration::from_millis(50)).map(Message::Tick)
}
}
impl<Message> canvas::Program<Message> for TheMatrix {
type State = RefCell<Vec<canvas::Cache>>;
fn draw(
&self,
state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
use rand::distributions::Distribution;
use rand::Rng;
const CELL_SIZE: f32 = 10.0;
let mut caches = state.borrow_mut();
if caches.is_empty() {
let group = canvas::Group::unique();
caches.resize_with(30, || canvas::Cache::with_group(group));
}
vec![caches[self.tick % caches.len()].draw(
renderer,
bounds.size(),
|frame| {
frame.fill_rectangle(Point::ORIGIN, frame.size(), Color::BLACK);
let mut rng = rand::thread_rng();
let rows = (frame.height() / CELL_SIZE).ceil() as usize;
let columns = (frame.width() / CELL_SIZE).ceil() as usize;
for row in 0..rows {
for column in 0..columns {
let position = Point::new(
column as f32 * CELL_SIZE,
row as f32 * CELL_SIZE,
);
let alphas = [0.05, 0.1, 0.2, 0.5];
let weights = [10, 4, 2, 1];
let distribution =
rand::distributions::WeightedIndex::new(weights)
.expect("Create distribution");
frame.fill_text(canvas::Text {
content: rng.gen_range('!'..'z').to_string(),
position,
color: Color {
a: alphas[distribution.sample(&mut rng)],
g: 1.0,
..Color::BLACK
},
size: CELL_SIZE.into(),
font: Font::MONOSPACE,
..canvas::Text::default()
});
}
}
},
)]
}
}

View file

@ -2,7 +2,7 @@ use iced::event::{self, Event};
use iced::keyboard;
use iced::keyboard::key;
use iced::widget::{
self, button, column, container, pick_list, row, slider, text, text_input,
self, button, center, column, pick_list, row, slider, text, text_input,
};
use iced::{Alignment, Command, Element, Length, Subscription};
@ -102,7 +102,7 @@ impl App {
.then_some(Message::Add),
);
let content = container(
let content = center(
column![
subtitle(
"Title",
@ -146,11 +146,7 @@ impl App {
]
.spacing(10)
.max_width(200),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y();
);
toast::Manager::new(content, &self.toasts, Message::Close)
.timeout(self.timeout_secs)
@ -651,45 +647,33 @@ mod toast {
}
}
fn styled(pair: theme::palette::Pair) -> container::Appearance {
container::Appearance {
fn styled(pair: theme::palette::Pair) -> container::Style {
container::Style {
background: Some(pair.color.into()),
text_color: pair.text.into(),
..Default::default()
}
}
fn primary(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
fn primary(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.primary.weak)
}
fn secondary(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
fn secondary(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.secondary.weak)
}
fn success(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
fn success(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.success.weak)
}
fn danger(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
fn danger(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
styled(palette.danger.weak)

View file

@ -1,8 +1,8 @@
use iced::alignment::{self, Alignment};
use iced::keyboard;
use iced::widget::{
self, button, checkbox, column, container, keyed_column, row, scrollable,
text, text_input, Text,
self, button, center, checkbox, column, container, keyed_column, row,
scrollable, text, text_input, Text,
};
use iced::window;
use iced::{Command, Element, Font, Length, Subscription};
@ -238,7 +238,10 @@ impl Todos {
.spacing(20)
.max_width(800);
scrollable(container(content).padding(40).center_x()).into()
scrollable(
container(content).center_x(Length::Fill).padding(40),
)
.into()
}
}
}
@ -435,19 +438,16 @@ impl Filter {
}
fn loading_message<'a>() -> Element<'a, Message> {
container(
center(
text("Loading...")
.horizontal_alignment(alignment::Horizontal::Center)
.size(50),
)
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.into()
}
fn empty_message(message: &str) -> Element<'_, Message> {
container(
center(
text(message)
.width(Length::Fill)
.size(25)
@ -455,7 +455,6 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.color([0.7, 0.7, 0.7]),
)
.height(200)
.center_y()
.into()
}

View file

@ -1,6 +1,6 @@
use iced::widget::tooltip::Position;
use iced::widget::{button, container, tooltip};
use iced::{Element, Length};
use iced::widget::{button, center, container, tooltip};
use iced::Element;
pub fn main() -> iced::Result {
iced::run("Tooltip - Iced", Tooltip::update, Tooltip::view)
@ -43,12 +43,7 @@ impl Tooltip {
.gap(10)
.style(container::rounded_box);
container(tooltip)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(tooltip).into()
}
}

View file

@ -76,11 +76,10 @@ impl Tour {
} else {
content
})
.width(Length::Fill)
.center_x(),
.center_x(Length::Fill),
);
container(scrollable).height(Length::Fill).center_y().into()
container(scrollable).center_y(Length::Fill).into()
}
}
@ -357,7 +356,7 @@ impl<'a> Step {
.into()
}
fn container(title: &str) -> Column<'a, StepMessage> {
fn container(title: &str) -> Column<'_, StepMessage> {
column![text(title).size(50)].spacing(20)
}
@ -589,7 +588,7 @@ impl<'a> Step {
value: &str,
is_secure: bool,
is_showing_icon: bool,
) -> Column<'a, StepMessage> {
) -> Column<'_, StepMessage> {
let mut text_input = text_input("Type something to continue...", value)
.on_input(StepMessage::InputChanged)
.padding(10)
@ -670,11 +669,10 @@ fn ferris<'a>(
.filter_method(filter_method)
.width(width),
)
.width(Length::Fill)
.center_x()
.center_x(Length::Fill)
}
fn padded_button<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> {
button(text(label)).padding([12, 24])
}

View file

@ -1,6 +1,6 @@
use iced::event::{self, Event};
use iced::widget::{container, text};
use iced::{Element, Length, Subscription};
use iced::widget::{center, text};
use iced::{Element, Subscription};
pub fn main() -> iced::Result {
iced::program("URL Handler - Iced", App::update, App::view)
@ -44,11 +44,6 @@ impl App {
None => text("No URL received yet!"),
};
container(content.size(48))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
center(content.size(48)).into()
}
}

View file

@ -2,6 +2,7 @@ pub mod server;
use iced::futures;
use iced::subscription::{self, Subscription};
use iced::widget::text;
use futures::channel::mpsc;
use futures::sink::SinkExt;
@ -136,16 +137,24 @@ impl Message {
pub fn disconnected() -> Self {
Message::Disconnected
}
pub fn as_str(&self) -> &str {
match self {
Message::Connected => "Connected successfully!",
Message::Disconnected => "Connection lost... Retrying...",
Message::User(message) => message.as_str(),
}
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Message::Connected => write!(f, "Connected successfully!"),
Message::Disconnected => {
write!(f, "Connection lost... Retrying...")
}
Message::User(message) => write!(f, "{message}"),
}
f.write_str(self.as_str())
}
}
impl<'a> text::IntoFragment<'a> for &'a Message {
fn into_fragment(self) -> text::Fragment<'a> {
text::Fragment::Borrowed(self.as_str())
}
}

View file

@ -2,7 +2,7 @@ mod echo;
use iced::alignment::{self, Alignment};
use iced::widget::{
button, column, container, row, scrollable, text, text_input,
self, button, center, column, row, scrollable, text, text_input,
};
use iced::{color, Command, Element, Length, Subscription};
use once_cell::sync::Lazy;
@ -31,7 +31,10 @@ enum Message {
impl WebSocket {
fn load() -> Command<Message> {
Command::perform(echo::server::run(), |_| Message::Server)
Command::batch([
Command::perform(echo::server::run(), |_| Message::Server),
widget::focus_next(),
])
}
fn update(&mut self, message: Message) -> Command<Message> {
@ -85,21 +88,15 @@ impl WebSocket {
fn view(&self) -> Element<Message> {
let message_log: Element<_> = if self.messages.is_empty() {
container(
center(
text("Your messages will appear here...")
.color(color!(0x888888)),
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
} else {
scrollable(
column(
self.messages.iter().cloned().map(text).map(Element::from),
)
.spacing(10),
column(self.messages.iter().map(text).map(Element::from))
.spacing(10),
)
.id(MESSAGE_LOG.clone())
.height(Length::Fill)

View file

@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
@ -22,6 +25,7 @@ iced_core.workspace = true
futures.workspace = true
log.workspace = true
rustc-hash.workspace = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-std.workspace = true

View file

@ -18,8 +18,7 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
use crate::core::Hasher;
use crate::subscription::{self, Subscription};
use crate::subscription::{self, Hasher, Subscription};
/// Returns a [`Subscription`] that produces messages at a set interval.
///

View file

@ -17,8 +17,7 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
use crate::core::Hasher;
use crate::subscription::{self, Subscription};
use crate::subscription::{self, Hasher, Subscription};
/// Returns a [`Subscription`] that produces messages at a set interval.
///

View file

@ -22,8 +22,7 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
use crate::core::Hasher;
use crate::subscription::{self, Subscription};
use crate::subscription::{self, Hasher, Subscription};
/// Returns a [`Subscription`] that produces messages at a set interval.
///
@ -56,13 +55,15 @@ pub mod time {
let start = tokio::time::Instant::now() + self.0;
let mut interval = tokio::time::interval_at(start, self.0);
interval.set_missed_tick_behavior(
tokio::time::MissedTickBehavior::Skip,
);
let stream = {
futures::stream::unfold(
tokio::time::interval_at(start, self.0),
|mut interval| async move {
Some((interval.tick().await, interval))
},
)
futures::stream::unfold(interval, |mut interval| async move {
Some((interval.tick().await, interval))
})
};
stream.map(tokio::time::Instant::into_std).boxed()

View file

@ -16,8 +16,7 @@ impl crate::Executor for Executor {
pub mod time {
//! Listen and react to time.
use crate::core::Hasher;
use crate::subscription::{self, Subscription};
use crate::subscription::{self, Hasher, Subscription};
use crate::BoxStream;
/// Returns a [`Subscription`] that produces messages at a set interval.

View file

@ -4,13 +4,6 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![deny(
missing_debug_implementations,
missing_docs,
unused_results,
rustdoc::broken_intra_doc_links
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use futures;
pub use iced_core as core;

View file

@ -4,7 +4,6 @@ mod tracker;
pub use tracker::Tracker;
use crate::core::event::{self, Event};
use crate::core::Hasher;
use crate::futures::{Future, Stream};
use crate::{BoxStream, MaybeSend};
@ -18,6 +17,9 @@ use std::hash::Hash;
/// It is the input of a [`Subscription`].
pub type EventStream = BoxStream<(Event, event::Status)>;
/// The hasher used for identifying subscriptions.
pub type Hasher = rustc_hash::FxHasher;
/// A request to listen to external events.
///
/// Besides performing async actions on demand with `Command`, most

View file

@ -1,12 +1,11 @@
use crate::core::event::{self, Event};
use crate::core::Hasher;
use crate::subscription::Recipe;
use crate::subscription::{Hasher, Recipe};
use crate::{BoxFuture, MaybeSend};
use futures::channel::mpsc;
use futures::sink::{Sink, SinkExt};
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::hash::Hasher as _;
/// A registry of subscription streams.
@ -18,7 +17,7 @@ use std::hash::Hasher as _;
/// [`Subscription`]: crate::Subscription
#[derive(Debug, Default)]
pub struct Tracker {
subscriptions: HashMap<u64, Execution>,
subscriptions: FxHashMap<u64, Execution>,
}
#[derive(Debug)]
@ -31,7 +30,7 @@ impl Tracker {
/// Creates a new empty [`Tracker`].
pub fn new() -> Self {
Self {
subscriptions: HashMap::new(),
subscriptions: FxHashMap::default(),
}
}

View file

@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[lints]
workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
all-features = true
@ -34,7 +37,6 @@ raw-window-handle.workspace = true
rustc-hash.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
xxhash-rust.workspace = true
image.workspace = true
image.optional = true

View file

@ -1,32 +0,0 @@
//! Write a graphics backend.
use crate::core::image;
use crate::core::svg;
use crate::core::Size;
use std::borrow::Cow;
/// The graphics backend of a [`Renderer`].
///
/// [`Renderer`]: crate::Renderer
pub trait Backend {
/// The custom kind of primitives this [`Backend`] supports.
type Primitive;
}
/// A graphics backend that supports text rendering.
pub trait Text {
/// Loads a font from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
}
/// A graphics backend that supports image rendering.
pub trait Image {
/// Returns the dimensions of the provided image.
fn dimensions(&self, handle: &image::Handle) -> Size<u32>;
}
/// A graphics backend that supports SVG rendering.
pub trait Svg {
/// Returns the viewport dimensions of the provided SVG.
fn viewport_dimensions(&self, handle: &svg::Handle) -> Size<u32>;
}

189
graphics/src/cache.rs Normal file
View file

@ -0,0 +1,189 @@
//! Cache computations and efficiently reuse them.
use std::cell::RefCell;
use std::fmt;
use std::sync::atomic::{self, AtomicU64};
/// A simple cache that stores generated values to avoid recomputation.
///
/// Keeps track of the last generated value after clearing.
pub struct Cache<T> {
group: Group,
state: RefCell<State<T>>,
}
impl<T> Cache<T> {
/// Creates a new empty [`Cache`].
pub fn new() -> Self {
Cache {
group: Group::singleton(),
state: RefCell::new(State::Empty { previous: None }),
}
}
/// Creates a new empty [`Cache`] with the given [`Group`].
///
/// Caches within the same group may reuse internal rendering storage.
///
/// You should generally group caches that are likely to change
/// together.
pub fn with_group(group: Group) -> Self {
assert!(
!group.is_singleton(),
"The group {group:?} cannot be shared!"
);
Cache {
group,
state: RefCell::new(State::Empty { previous: None }),
}
}
/// Returns the [`Group`] of the [`Cache`].
pub fn group(&self) -> Group {
self.group
}
/// Puts the given value in the [`Cache`].
///
/// Notice that, given this is a cache, a mutable reference is not
/// necessary to call this method. You can safely update the cache in
/// rendering code.
pub fn put(&self, value: T) {
*self.state.borrow_mut() = State::Filled { current: value };
}
/// Returns a reference cell to the internal [`State`] of the [`Cache`].
pub fn state(&self) -> &RefCell<State<T>> {
&self.state
}
/// Clears the [`Cache`].
pub fn clear(&self)
where
T: Clone,
{
use std::ops::Deref;
let previous = match self.state.borrow().deref() {
State::Empty { previous } => previous.clone(),
State::Filled { current } => Some(current.clone()),
};
*self.state.borrow_mut() = State::Empty { previous };
}
}
/// A cache group.
///
/// Caches that share the same group generally change together.
///
/// A cache group can be used to implement certain performance
/// optimizations during rendering, like batching or sharing atlases.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Group {
id: u64,
is_singleton: bool,
}
impl Group {
/// Generates a new unique cache [`Group`].
pub fn unique() -> Self {
static NEXT: AtomicU64 = AtomicU64::new(0);
Self {
id: NEXT.fetch_add(1, atomic::Ordering::Relaxed),
is_singleton: false,
}
}
/// Returns `true` if the [`Group`] can only ever have a
/// single [`Cache`] in it.
///
/// This is the default kind of [`Group`] assigned when using
/// [`Cache::new`].
///
/// Knowing that a [`Group`] will never be shared may be
/// useful for rendering backends to perform additional
/// optimizations.
pub fn is_singleton(self) -> bool {
self.is_singleton
}
fn singleton() -> Self {
Self {
is_singleton: true,
..Self::unique()
}
}
}
impl<T> fmt::Debug for Cache<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::ops::Deref;
let state = self.state.borrow();
match state.deref() {
State::Empty { previous } => {
write!(f, "Cache::Empty {{ previous: {previous:?} }}")
}
State::Filled { current } => {
write!(f, "Cache::Filled {{ current: {current:?} }}")
}
}
}
}
impl<T> Default for Cache<T> {
fn default() -> Self {
Self::new()
}
}
/// The state of a [`Cache`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State<T> {
/// The [`Cache`] is empty.
Empty {
/// The previous value of the [`Cache`].
previous: Option<T>,
},
/// The [`Cache`] is filled.
Filled {
/// The current value of the [`Cache`]
current: T,
},
}
/// A piece of data that can be cached.
pub trait Cached: Sized {
/// The type of cache produced.
type Cache: Clone;
/// Loads the [`Cache`] into a proper instance.
///
/// [`Cache`]: Self::Cache
fn load(cache: &Self::Cache) -> Self;
/// Caches this value, producing its corresponding [`Cache`].
///
/// [`Cache`]: Self::Cache
fn cache(self, group: Group, previous: Option<Self::Cache>) -> Self::Cache;
}
#[cfg(debug_assertions)]
impl Cached for () {
type Cache = ();
fn load(_cache: &Self::Cache) -> Self {}
fn cache(
self,
_group: Group,
_previous: Option<Self::Cache>,
) -> Self::Cache {
}
}

Some files were not shown because too many files have changed in this diff Show more