summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.cargo/config.toml53
-rw-r--r--.github/workflows/check.yml16
-rw-r--r--.github/workflows/lint.yml7
-rw-r--r--Cargo.toml40
-rw-r--r--DEPENDENCIES.md2
-rw-r--r--benches/wgpu.rs176
-rw-r--r--core/Cargo.toml5
-rw-r--r--core/src/hasher.rs2
-rw-r--r--core/src/image.rs5
-rw-r--r--core/src/lib.rs9
-rw-r--r--core/src/rectangle.rs47
-rw-r--r--core/src/renderer.rs16
-rw-r--r--core/src/renderer/null.rs18
-rw-r--r--core/src/size.rs14
-rw-r--r--core/src/svg.rs5
-rw-r--r--core/src/text.rs10
-rw-r--r--core/src/text/paragraph.rs6
-rw-r--r--core/src/theme/palette.rs18
-rw-r--r--core/src/transformation.rs6
-rw-r--r--core/src/widget/text.rs86
-rw-r--r--examples/custom_shader/src/scene.rs33
-rw-r--r--examples/download_progress/src/download.rs6
-rw-r--r--examples/geometry/src/main.rs8
-rw-r--r--examples/integration/src/main.rs34
-rw-r--r--examples/lazy/src/main.rs2
-rw-r--r--examples/tour/src/main.rs6
-rw-r--r--examples/websocket/src/echo.rs23
-rw-r--r--examples/websocket/src/main.rs6
-rw-r--r--futures/Cargo.toml4
-rw-r--r--futures/src/backend/native/async_std.rs3
-rw-r--r--futures/src/backend/native/smol.rs3
-rw-r--r--futures/src/backend/native/tokio.rs3
-rw-r--r--futures/src/backend/wasm/wasm_bindgen.rs3
-rw-r--r--futures/src/lib.rs7
-rw-r--r--futures/src/subscription.rs4
-rw-r--r--futures/src/subscription/tracker.rs9
-rw-r--r--graphics/Cargo.toml4
-rw-r--r--graphics/src/backend.rs36
-rw-r--r--graphics/src/cached.rs24
-rw-r--r--graphics/src/compositor.rs14
-rw-r--r--graphics/src/damage.rs240
-rw-r--r--graphics/src/geometry.rs9
-rw-r--r--graphics/src/geometry/cache.rs39
-rw-r--r--graphics/src/geometry/frame.rs16
-rw-r--r--graphics/src/image.rs167
-rw-r--r--graphics/src/layer.rs144
-rw-r--r--graphics/src/lib.rs23
-rw-r--r--graphics/src/mesh.rs94
-rw-r--r--graphics/src/primitive.rs160
-rw-r--r--graphics/src/renderer.rs269
-rw-r--r--graphics/src/settings.rs2
-rw-r--r--graphics/src/text.rs129
-rw-r--r--graphics/src/text/cache.rs16
-rw-r--r--graphics/src/text/paragraph.rs4
-rw-r--r--highlighter/Cargo.toml3
-rw-r--r--highlighter/src/lib.rs31
-rw-r--r--renderer/Cargo.toml4
-rw-r--r--renderer/src/fallback.rs315
-rw-r--r--renderer/src/lib.rs3
-rw-r--r--runtime/Cargo.toml3
-rw-r--r--runtime/src/lib.rs7
-rw-r--r--src/advanced.rs6
-rw-r--r--src/lib.rs7
-rw-r--r--tiny_skia/Cargo.toml4
-rw-r--r--tiny_skia/src/backend.rs1032
-rw-r--r--tiny_skia/src/engine.rs831
-rw-r--r--tiny_skia/src/geometry.rs148
-rw-r--r--tiny_skia/src/layer.rs333
-rw-r--r--tiny_skia/src/lib.rs387
-rw-r--r--tiny_skia/src/primitive.rs38
-rw-r--r--tiny_skia/src/raster.rs4
-rw-r--r--tiny_skia/src/settings.rs4
-rw-r--r--tiny_skia/src/text.rs31
-rw-r--r--tiny_skia/src/vector.rs11
-rw-r--r--tiny_skia/src/window/compositor.rs71
-rw-r--r--wgpu/Cargo.toml7
-rw-r--r--wgpu/src/backend.rs410
-rw-r--r--wgpu/src/buffer.rs45
-rw-r--r--wgpu/src/engine.rs84
-rw-r--r--wgpu/src/geometry.rs207
-rw-r--r--wgpu/src/image/cache.rs107
-rw-r--r--wgpu/src/image/mod.rs (renamed from wgpu/src/image.rs)430
-rw-r--r--wgpu/src/image/null.rs10
-rw-r--r--wgpu/src/image/raster.rs6
-rw-r--r--wgpu/src/image/vector.rs10
-rw-r--r--wgpu/src/layer.rs574
-rw-r--r--wgpu/src/layer/image.rs30
-rw-r--r--wgpu/src/layer/mesh.rs97
-rw-r--r--wgpu/src/layer/pipeline.rs17
-rw-r--r--wgpu/src/layer/text.rs70
-rw-r--r--wgpu/src/lib.rs549
-rw-r--r--wgpu/src/primitive.rs105
-rw-r--r--wgpu/src/primitive/pipeline.rs116
-rw-r--r--wgpu/src/quad.rs118
-rw-r--r--wgpu/src/quad/gradient.rs5
-rw-r--r--wgpu/src/quad/solid.rs5
-rw-r--r--wgpu/src/settings.rs14
-rw-r--r--wgpu/src/shader/blit.wgsl20
-rw-r--r--wgpu/src/text.rs731
-rw-r--r--wgpu/src/triangle.rs496
-rw-r--r--wgpu/src/triangle/msaa.rs130
-rw-r--r--wgpu/src/window/compositor.rs102
-rw-r--r--widget/Cargo.toml4
-rw-r--r--widget/src/canvas.rs6
-rw-r--r--widget/src/checkbox.rs2
-rw-r--r--widget/src/column.rs9
-rw-r--r--widget/src/combo_box.rs65
-rw-r--r--widget/src/helpers.rs17
-rw-r--r--widget/src/keyed/column.rs46
-rw-r--r--widget/src/lazy.rs21
-rw-r--r--widget/src/lib.rs7
-rw-r--r--widget/src/overlay/menu.rs2
-rw-r--r--widget/src/pane_grid/state.rs8
-rw-r--r--widget/src/pick_list.rs4
-rw-r--r--widget/src/row.rs9
-rw-r--r--widget/src/scrollable.rs6
-rw-r--r--widget/src/shader.rs11
-rw-r--r--widget/src/shader/program.rs4
-rw-r--r--widget/src/text_editor.rs15
-rw-r--r--widget/src/text_input.rs21
-rw-r--r--winit/Cargo.toml5
-rw-r--r--winit/src/application.rs12
-rw-r--r--winit/src/lib.rs8
-rw-r--r--winit/src/multi_window.rs23
124 files changed, 5685 insertions, 4473 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 85a46cda..49ca3252 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -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"
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 4107e618..ceac39e7 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -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:
@@ -27,3 +19,11 @@ jobs:
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
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index ccf79cb7..16ee8bf9 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -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
diff --git a/Cargo.toml b/Cargo.toml
index e2639944..2b6a0d03 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
@@ -74,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
@@ -129,7 +141,7 @@ 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 = "ceed55403ce53e120ce9d1fae17dcfe388726118" }
guillotiere = "0.6"
half = "2.2"
image = "0.24"
@@ -155,7 +167,6 @@ 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"
@@ -165,3 +176,28 @@ wgpu = "0.19"
winapi = "0.3"
window_clipboard = "0.4.1"
winit = { git = "https://github.com/iced-rs/winit.git", rev = "592bd152f6d5786fae7d918532d7db752c0d164f" }
+
+[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"
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 809371cb..5d738d85 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -20,7 +20,7 @@ pkgs.mkShell rec {
freetype
freetype.dev
libGL
- pkgconfig
+ pkg-config
xorg.libX11
xorg.libXcursor
xorg.libXi
diff --git a/benches/wgpu.rs b/benches/wgpu.rs
new file mode 100644
index 00000000..61b4eb6c
--- /dev/null
+++ b/benches/wgpu.rs
@@ -0,0 +1,176 @@
+#![allow(missing_docs)]
+use criterion::{criterion_group, criterion_main, Bencher, Criterion};
+
+use iced::alignment;
+use iced::mouse;
+use iced::widget::{canvas, 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)));
+}
+
+fn benchmark(
+ bencher: &mut Bencher<'_>,
+ widget: Element<'_, (), 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(&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 user_interface = runtime::UserInterface::build(
+ widget,
+ viewport.logical_size(),
+ runtime::user_interface::Cache::default(),
+ &mut renderer,
+ );
+
+ bencher.iter(|| {
+ let _ = user_interface.draw(
+ &mut renderer,
+ &Theme::Dark,
+ &core::renderer::Style {
+ text_color: Color::WHITE,
+ },
+ mouse::Cursor::Unavailable,
+ );
+
+ 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));
+ });
+}
+
+fn scene<'a, Message: 'a, Theme: 'a>(
+ n: usize,
+) -> Element<'a, Message, Theme, Renderer> {
+ canvas(Scene { n })
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+}
+
+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,
+ });
+ }
+ })]
+ }
+}
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 32d233ee..7bd37021 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[features]
auto-detect-theme = ["dep:dark-light"]
advanced = []
@@ -21,10 +24,10 @@ 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
diff --git a/core/src/hasher.rs b/core/src/hasher.rs
index a13d78af..13180e41 100644
--- a/core/src/hasher.rs
+++ b/core/src/hasher.rs
@@ -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]) {
diff --git a/core/src/image.rs b/core/src/image.rs
index 32b95f03..dc74e5c1 100644
--- a/core/src/image.rs
+++ b/core/src/image.rs
@@ -1,6 +1,7 @@
//! Load and draw raster graphics.
-use crate::{Hasher, Rectangle, Size};
+use crate::{Rectangle, Size};
+use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher as _};
use std::path::PathBuf;
use std::sync::Arc;
@@ -50,7 +51,7 @@ impl Handle {
}
fn from_data(data: Data) -> Handle {
- let mut hasher = Hasher::default();
+ let mut hasher = FxHasher::default();
data.hash(&mut hasher);
Handle {
diff --git a/core/src/lib.rs b/core/src/lib.rs
index d076413e..feda4fb4 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -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,7 +34,6 @@ mod background;
mod color;
mod content_fit;
mod element;
-mod hasher;
mod length;
mod padding;
mod pixels;
@@ -64,7 +56,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;
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index c1c2eeac..2ab50137 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -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.
diff --git a/core/src/renderer.rs b/core/src/renderer.rs
index 6712314e..a2785ae8 100644
--- a/core/src/renderer.rs
+++ b/core/src/renderer.rs
@@ -9,29 +9,29 @@ use crate::{
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer {
/// Starts recording a new layer.
- fn start_layer(&mut self);
+ 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, bounds: Rectangle);
+ 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)) {
- self.start_layer();
+ self.start_layer(bounds);
f(self);
- self.end_layer(bounds);
+ self.end_layer();
}
/// Starts recording with a new [`Transformation`].
- fn start_transformation(&mut self);
+ 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, transformation: Transformation);
+ fn end_transformation(&mut self);
/// Applies a [`Transformation`] to the primitives recorded in the given closure.
fn with_transformation(
@@ -39,9 +39,9 @@ pub trait Renderer {
transformation: Transformation,
f: impl FnOnce(&mut Self),
) {
- self.start_transformation();
+ self.start_transformation(transformation);
f(self);
- self.end_transformation(transformation);
+ self.end_transformation();
}
/// Applies a translation to the primitives recorded in the given closure.
diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs
index c26ce1a5..fe38ec87 100644
--- a/core/src/renderer/null.rs
+++ b/core/src/renderer/null.rs
@@ -7,16 +7,14 @@ use crate::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
-use std::borrow::Cow;
-
impl Renderer for () {
- fn start_layer(&mut self) {}
+ fn start_layer(&mut self, _bounds: Rectangle) {}
- fn end_layer(&mut self, _bounds: Rectangle) {}
+ fn end_layer(&mut self) {}
- fn start_transformation(&mut self) {}
+ fn start_transformation(&mut self, _transformation: Transformation) {}
- fn end_transformation(&mut self, _transformation: Transformation) {}
+ fn end_transformation(&mut self) {}
fn clear(&mut self) {}
@@ -45,8 +43,6 @@ impl text::Renderer for () {
Pixels(16.0)
}
- fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
-
fn fill_paragraph(
&mut self,
_paragraph: &Self::Paragraph,
@@ -67,7 +63,7 @@ impl text::Renderer for () {
fn fill_text(
&mut self,
- _paragraph: Text<'_, Self::Font>,
+ _paragraph: Text,
_position: Point,
_color: Color,
_clip_bounds: Rectangle,
@@ -78,11 +74,11 @@ impl text::Renderer for () {
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
}
diff --git a/core/src/size.rs b/core/src/size.rs
index 55db759d..c2b5671a 100644
--- a/core/src/size.rs
+++ b/core/src/size.rs
@@ -99,3 +99,17 @@ 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,
+ }
+ }
+}
diff --git a/core/src/svg.rs b/core/src/svg.rs
index ab207cca..0106e0c2 100644
--- a/core/src/svg.rs
+++ b/core/src/svg.rs
@@ -1,6 +1,7 @@
//! Load and draw vector graphics.
-use crate::{Color, Hasher, Rectangle, Size};
+use crate::{Color, 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 {
diff --git a/core/src/text.rs b/core/src/text.rs
index edef79c2..b30feae0 100644
--- a/core/src/text.rs
+++ b/core/src/text.rs
@@ -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,
diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs
index de1fb74d..8ff04015 100644
--- a/core/src/text/paragraph.rs
+++ b/core/src/text/paragraph.rs
@@ -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 => {
diff --git a/core/src/theme/palette.rs b/core/src/theme/palette.rs
index ca91c248..e0ff397a 100644
--- a/core/src/theme/palette.rs
+++ b/core/src/theme/palette.rs
@@ -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))
}
diff --git a/core/src/transformation.rs b/core/src/transformation.rs
index b2c488b0..74183147 100644
--- a/core/src/transformation.rs
+++ b/core/src/transformation.rs
@@ -42,6 +42,12 @@ impl Transformation {
}
}
+impl Default for Transformation {
+ fn default() -> Self {
+ Transformation::IDENTITY
+ }
+}
+
impl Mul for Transformation {
type Output = Self;
diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs
index 12f6956a..f1f0b345 100644
--- a/core/src/widget/text.rs
+++ b/core/src/widget/text.rs
@@ -21,7 +21,7 @@ where
Theme: Catalog,
Renderer: text::Renderer,
{
- content: Cow<'a, str>,
+ fragment: Fragment<'a>,
size: Option<Pixels>,
line_height: LineHeight,
width: Length,
@@ -39,9 +39,9 @@ where
Renderer: text::Renderer,
{
/// Create a new fragment of [`Text`] with the given contents.
- pub fn new(content: impl Into<Cow<'a, str>>) -> Self {
+ pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Text {
- content: content.into(),
+ fragment: fragment.into_fragment(),
size: None,
line_height: LineHeight::default(),
font: None,
@@ -184,7 +184,7 @@ where
limits,
self.width,
self.height,
- &self.content,
+ &self.fragment,
self.line_height,
self.size,
self.font,
@@ -366,3 +366,81 @@ impl Catalog for Theme {
class(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);
diff --git a/examples/custom_shader/src/scene.rs b/examples/custom_shader/src/scene.rs
index a35efdd9..5fa42188 100644
--- a/examples/custom_shader/src/scene.rs
+++ b/examples/custom_shader/src/scene.rs
@@ -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,
);
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
index 3b11cb76..d6cc1e24 100644
--- a/examples/download_progress/src/download.rs
+++ b/examples/download_progress/src/download.rs
@@ -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) => {
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 16cdb86f..31b8a4ce 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -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;
@@ -79,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 {
@@ -130,6 +132,8 @@ mod rainbow {
0, 8, 1, // L
],
},
+ transformation: Transformation::IDENTITY,
+ clip_bounds: Rectangle::INFINITE,
};
renderer.with_translation(
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index 9cd801b2..c4b57ecf 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -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;
@@ -155,11 +155,9 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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 engine = Engine::new(&adapter, &device, &queue, format, None);
+ let mut renderer =
+ Renderer::new(&engine, Font::default(), Pixels::from(16));
let mut state = program::State::new(
controls,
@@ -228,19 +226,17 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
}
// And then iced on top
- renderer.with_primitives(|backend, primitive| {
- backend.present(
- &device,
- &queue,
- &mut encoder,
- None,
- frame.texture.format(),
- &view,
- primitive,
- &viewport,
- &debug.overlay(),
- );
- });
+ renderer.present(
+ &mut engine,
+ &device,
+ &queue,
+ &mut encoder,
+ None,
+ frame.texture.format(),
+ &view,
+ &viewport,
+ &debug.overlay(),
+ );
// Then we submit the work
queue.submit(Some(encoder.finish()));
diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs
index 2d53df93..c3f6b8de 100644
--- a/examples/lazy/src/main.rs
+++ b/examples/lazy/src/main.rs
@@ -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)
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index a88c0dba..e3a2ca87 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -357,7 +357,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 +589,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)
@@ -674,7 +674,7 @@ fn ferris<'a>(
.center_x()
}
-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])
}
diff --git a/examples/websocket/src/echo.rs b/examples/websocket/src/echo.rs
index 281ed4bd..cd32cb66 100644
--- a/examples/websocket/src/echo.rs
+++ b/examples/websocket/src/echo.rs
@@ -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())
}
}
diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs
index 460d9a08..b479fe89 100644
--- a/examples/websocket/src/main.rs
+++ b/examples/websocket/src/main.rs
@@ -96,10 +96,8 @@ impl WebSocket {
.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)
diff --git a/futures/Cargo.toml b/futures/Cargo.toml
index 69a915e4..a6fcfde1 100644
--- a/futures/Cargo.toml
+++ b/futures/Cargo.toml
@@ -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
diff --git a/futures/src/backend/native/async_std.rs b/futures/src/backend/native/async_std.rs
index 52b0e914..b7da5e90 100644
--- a/futures/src/backend/native/async_std.rs
+++ b/futures/src/backend/native/async_std.rs
@@ -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.
///
diff --git a/futures/src/backend/native/smol.rs b/futures/src/backend/native/smol.rs
index 00d13d35..aaf1518c 100644
--- a/futures/src/backend/native/smol.rs
+++ b/futures/src/backend/native/smol.rs
@@ -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.
///
diff --git a/futures/src/backend/native/tokio.rs b/futures/src/backend/native/tokio.rs
index 4698a105..3ab7f675 100644
--- a/futures/src/backend/native/tokio.rs
+++ b/futures/src/backend/native/tokio.rs
@@ -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.
///
diff --git a/futures/src/backend/wasm/wasm_bindgen.rs b/futures/src/backend/wasm/wasm_bindgen.rs
index ff7ea0f6..3228dd18 100644
--- a/futures/src/backend/wasm/wasm_bindgen.rs
+++ b/futures/src/backend/wasm/wasm_bindgen.rs
@@ -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.
diff --git a/futures/src/lib.rs b/futures/src/lib.rs
index b0acb76f..a874a618 100644
--- a/futures/src/lib.rs
+++ b/futures/src/lib.rs
@@ -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;
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index 7537c022..93e35608 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -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
diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs
index 15ed5b87..277a446b 100644
--- a/futures/src/subscription/tracker.rs
+++ b/futures/src/subscription/tracker.rs
@@ -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(),
}
}
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index 0ee6ff47..e8d27d07 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -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
diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs
deleted file mode 100644
index 7abc42c5..00000000
--- a/graphics/src/backend.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-//! Write a graphics backend.
-use crate::core::image;
-use crate::core::svg;
-use crate::core::Size;
-use crate::{Compositor, Mesh, Renderer};
-
-use std::borrow::Cow;
-
-/// The graphics backend of a [`Renderer`].
-///
-/// [`Renderer`]: crate::Renderer
-pub trait Backend: Sized {
- /// The custom kind of primitives this [`Backend`] supports.
- type Primitive: TryFrom<Mesh, Error = &'static str>;
-
- /// The default compositor of this [`Backend`].
- type Compositor: Compositor<Renderer = Renderer<Self>>;
-}
-
-/// 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>;
-}
diff --git a/graphics/src/cached.rs b/graphics/src/cached.rs
index b52f9d9d..c0e4e029 100644
--- a/graphics/src/cached.rs
+++ b/graphics/src/cached.rs
@@ -1,11 +1,7 @@
-use crate::Primitive;
-
-use std::sync::Arc;
-
/// A piece of data that can be cached.
pub trait Cached: Sized {
/// The type of cache produced.
- type Cache;
+ type Cache: Clone;
/// Loads the [`Cache`] into a proper instance.
///
@@ -15,21 +11,7 @@ pub trait Cached: Sized {
/// Caches this value, producing its corresponding [`Cache`].
///
/// [`Cache`]: Self::Cache
- fn cache(self) -> Self::Cache;
-}
-
-impl<T> Cached for Primitive<T> {
- type Cache = Arc<Self>;
-
- fn load(cache: &Arc<Self>) -> Self {
- Self::Cache {
- content: cache.clone(),
- }
- }
-
- fn cache(self) -> Arc<Self> {
- Arc::new(self)
- }
+ fn cache(self, previous: Option<Self::Cache>) -> Self::Cache;
}
#[cfg(debug_assertions)]
@@ -38,5 +20,5 @@ impl Cached for () {
fn load(_cache: &Self::Cache) -> Self {}
- fn cache(self) -> Self::Cache {}
+ fn cache(self, _previous: Option<Self::Cache>) -> Self::Cache {}
}
diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs
index 86472a58..47521eb0 100644
--- a/graphics/src/compositor.rs
+++ b/graphics/src/compositor.rs
@@ -5,9 +5,11 @@ use crate::futures::{MaybeSend, MaybeSync};
use crate::{Error, Settings, Viewport};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
-use std::future::Future;
use thiserror::Error;
+use std::borrow::Cow;
+use std::future::Future;
+
/// A graphics compositor that can draw to windows.
pub trait Compositor: Sized {
/// The iced renderer of the backend.
@@ -60,6 +62,14 @@ pub trait Compositor: Sized {
/// Returns [`Information`] used by this [`Compositor`].
fn fetch_information(&self) -> Information;
+ /// Loads a font from its bytes.
+ fn load_font(&mut self, font: Cow<'static, [u8]>) {
+ crate::text::font_system()
+ .write()
+ .expect("Write to font system")
+ .load_font(font);
+ }
+
/// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`].
///
/// [`Renderer`]: Self::Renderer
@@ -168,6 +178,8 @@ impl Compositor for () {
) {
}
+ fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
+
fn fetch_information(&self) -> Information {
Information {
adapter: String::from("Null Renderer"),
diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs
index 8edf69d7..17d60451 100644
--- a/graphics/src/damage.rs
+++ b/graphics/src/damage.rs
@@ -1,196 +1,14 @@
-//! Track and compute the damage of graphical primitives.
-use crate::core::alignment;
-use crate::core::{Rectangle, Size};
-use crate::Primitive;
-
-use std::sync::Arc;
-
-/// A type that has some damage bounds.
-pub trait Damage: PartialEq {
- /// Returns the bounds of the [`Damage`].
- fn bounds(&self) -> Rectangle;
-}
-
-impl<T: Damage> Damage for Primitive<T> {
- fn bounds(&self) -> Rectangle {
- match self {
- Self::Text {
- bounds,
- horizontal_alignment,
- vertical_alignment,
- ..
- } => {
- let mut bounds = *bounds;
-
- bounds.x = match horizontal_alignment {
- alignment::Horizontal::Left => bounds.x,
- alignment::Horizontal::Center => {
- bounds.x - bounds.width / 2.0
- }
- alignment::Horizontal::Right => bounds.x - bounds.width,
- };
-
- bounds.y = match vertical_alignment {
- alignment::Vertical::Top => bounds.y,
- alignment::Vertical::Center => {
- bounds.y - bounds.height / 2.0
- }
- alignment::Vertical::Bottom => bounds.y - bounds.height,
- };
-
- bounds.expand(1.5)
- }
- Self::Paragraph {
- paragraph,
- position,
- ..
- } => {
- let mut bounds =
- Rectangle::new(*position, paragraph.min_bounds);
-
- bounds.x = match paragraph.horizontal_alignment {
- alignment::Horizontal::Left => bounds.x,
- alignment::Horizontal::Center => {
- bounds.x - bounds.width / 2.0
- }
- alignment::Horizontal::Right => bounds.x - bounds.width,
- };
-
- bounds.y = match paragraph.vertical_alignment {
- alignment::Vertical::Top => bounds.y,
- alignment::Vertical::Center => {
- bounds.y - bounds.height / 2.0
- }
- alignment::Vertical::Bottom => bounds.y - bounds.height,
- };
-
- bounds.expand(1.5)
- }
- Self::Editor {
- editor, position, ..
- } => {
- let bounds = Rectangle::new(*position, editor.bounds);
-
- bounds.expand(1.5)
- }
- Self::RawText(raw) => {
- // TODO: Add `size` field to `raw` to compute more accurate
- // damage bounds (?)
- raw.clip_bounds.expand(1.5)
- }
- Self::Quad { bounds, shadow, .. } if shadow.color.a > 0.0 => {
- let bounds_with_shadow = Rectangle {
- x: bounds.x + shadow.offset.x.min(0.0) - shadow.blur_radius,
- y: bounds.y + shadow.offset.y.min(0.0) - shadow.blur_radius,
- width: bounds.width
- + shadow.offset.x.abs()
- + shadow.blur_radius * 2.0,
- height: bounds.height
- + shadow.offset.y.abs()
- + shadow.blur_radius * 2.0,
- };
-
- bounds_with_shadow.expand(1.0)
- }
- Self::Quad { bounds, .. }
- | Self::Image { bounds, .. }
- | Self::Svg { bounds, .. } => bounds.expand(1.0),
- Self::Clip { bounds, .. } => bounds.expand(1.0),
- Self::Group { primitives } => primitives
- .iter()
- .map(Self::bounds)
- .fold(Rectangle::with_size(Size::ZERO), |a, b| {
- Rectangle::union(&a, &b)
- }),
- Self::Transform {
- transformation,
- content,
- } => content.bounds() * *transformation,
- Self::Cache { content } => content.bounds(),
- Self::Custom(custom) => custom.bounds(),
- }
- }
-}
-
-fn regions<T: Damage>(a: &Primitive<T>, b: &Primitive<T>) -> Vec<Rectangle> {
- match (a, b) {
- (
- Primitive::Group {
- primitives: primitives_a,
- },
- Primitive::Group {
- primitives: primitives_b,
- },
- ) => return list(primitives_a, primitives_b),
- (
- Primitive::Clip {
- bounds: bounds_a,
- content: content_a,
- ..
- },
- Primitive::Clip {
- bounds: bounds_b,
- content: content_b,
- ..
- },
- ) => {
- if bounds_a == bounds_b {
- return regions(content_a, content_b)
- .into_iter()
- .filter_map(|r| r.intersection(&bounds_a.expand(1.0)))
- .collect();
- } else {
- return vec![bounds_a.expand(1.0), bounds_b.expand(1.0)];
- }
- }
- (
- Primitive::Transform {
- transformation: transformation_a,
- content: content_a,
- },
- Primitive::Transform {
- transformation: transformation_b,
- content: content_b,
- },
- ) => {
- if transformation_a == transformation_b {
- return regions(content_a, content_b)
- .into_iter()
- .map(|r| r * *transformation_a)
- .collect();
- }
- }
- (
- Primitive::Cache { content: content_a },
- Primitive::Cache { content: content_b },
- ) => {
- if Arc::ptr_eq(content_a, content_b) {
- return vec![];
- }
- }
- _ if a == b => return vec![],
- _ => {}
- }
-
- let bounds_a = a.bounds();
- let bounds_b = b.bounds();
-
- if bounds_a == bounds_b {
- vec![bounds_a]
- } else {
- vec![bounds_a, bounds_b]
- }
-}
-
-/// Computes the damage regions between the two given lists of primitives.
-pub fn list<T: Damage>(
- previous: &[Primitive<T>],
- current: &[Primitive<T>],
+//! Compute the damage between frames.
+use crate::core::{Point, Rectangle};
+
+/// Diffs the damage regions given some previous and current primitives.
+pub fn diff<T>(
+ previous: &[T],
+ current: &[T],
+ bounds: impl Fn(&T) -> Vec<Rectangle>,
+ diff: impl Fn(&T, &T) -> Vec<Rectangle>,
) -> Vec<Rectangle> {
- let damage = previous
- .iter()
- .zip(current)
- .flat_map(|(a, b)| regions(a, b));
+ let damage = previous.iter().zip(current).flat_map(|(a, b)| diff(a, b));
if previous.len() == current.len() {
damage.collect()
@@ -203,39 +21,45 @@ pub fn list<T: Damage>(
// Extend damage by the added/removed primitives
damage
- .chain(bigger[smaller.len()..].iter().map(Damage::bounds))
+ .chain(bigger[smaller.len()..].iter().flat_map(bounds))
.collect()
}
}
+/// Computes the damage regions given some previous and current primitives.
+pub fn list<T>(
+ previous: &[T],
+ current: &[T],
+ bounds: impl Fn(&T) -> Vec<Rectangle>,
+ are_equal: impl Fn(&T, &T) -> bool,
+) -> Vec<Rectangle> {
+ diff(previous, current, &bounds, |a, b| {
+ if are_equal(a, b) {
+ vec![]
+ } else {
+ bounds(a).into_iter().chain(bounds(b)).collect()
+ }
+ })
+}
+
/// Groups the given damage regions that are close together inside the given
/// bounds.
-pub fn group(
- mut damage: Vec<Rectangle>,
- scale_factor: f32,
- bounds: Size<u32>,
-) -> Vec<Rectangle> {
+pub fn group(mut damage: Vec<Rectangle>, bounds: Rectangle) -> Vec<Rectangle> {
use std::cmp::Ordering;
const AREA_THRESHOLD: f32 = 20_000.0;
- let bounds = Rectangle {
- x: 0.0,
- y: 0.0,
- width: bounds.width as f32,
- height: bounds.height as f32,
- };
-
damage.sort_by(|a, b| {
- a.x.partial_cmp(&b.x)
+ a.center()
+ .distance(Point::ORIGIN)
+ .partial_cmp(&b.center().distance(Point::ORIGIN))
.unwrap_or(Ordering::Equal)
- .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(Ordering::Equal))
});
let mut output = Vec::new();
let mut scaled = damage
.into_iter()
- .filter_map(|region| (region * scale_factor).intersection(&bounds))
+ .filter_map(|region| region.intersection(&bounds))
.filter(|region| region.width >= 1.0 && region.height >= 1.0);
if let Some(mut current) = scaled.next() {
diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs
index d251efb8..dd07097f 100644
--- a/graphics/src/geometry.rs
+++ b/graphics/src/geometry.rs
@@ -36,15 +36,6 @@ pub trait Renderer: core::Renderer {
fn draw_geometry(&mut self, geometry: Self::Geometry);
}
-/// The graphics backend of a geometry renderer.
-pub trait Backend {
- /// The kind of [`Frame`] this backend supports.
- type Frame: frame::Backend;
-
- /// Creates a new [`Self::Frame`].
- fn new_frame(&self, size: Size) -> Self::Frame;
-}
-
#[cfg(debug_assertions)]
impl Renderer for () {
type Geometry = ();
diff --git a/graphics/src/geometry/cache.rs b/graphics/src/geometry/cache.rs
index 37d433c2..665e996b 100644
--- a/graphics/src/geometry/cache.rs
+++ b/graphics/src/geometry/cache.rs
@@ -22,13 +22,20 @@ where
/// Creates a new empty [`Cache`].
pub fn new() -> Self {
Cache {
- state: RefCell::new(State::Empty),
+ state: RefCell::new(State::Empty { previous: None }),
}
}
/// Clears the [`Cache`], forcing a redraw the next time it is used.
pub fn clear(&self) {
- *self.state.borrow_mut() = State::Empty;
+ use std::ops::Deref;
+
+ let previous = match self.state.borrow().deref() {
+ State::Empty { previous } => previous.clone(),
+ State::Filled { geometry, .. } => Some(geometry.clone()),
+ };
+
+ *self.state.borrow_mut() = State::Empty { previous };
}
/// Draws geometry using the provided closure and stores it in the
@@ -49,20 +56,24 @@ where
) -> Renderer::Geometry {
use std::ops::Deref;
- if let State::Filled {
- bounds: cached_bounds,
- geometry,
- } = self.state.borrow().deref()
- {
- if *cached_bounds == bounds {
- return Cached::load(geometry);
+ let previous = match self.state.borrow().deref() {
+ State::Empty { previous } => previous.clone(),
+ State::Filled {
+ bounds: cached_bounds,
+ geometry,
+ } => {
+ if *cached_bounds == bounds {
+ return Cached::load(geometry);
+ }
+
+ Some(geometry.clone())
}
- }
+ };
let mut frame = Frame::new(renderer, bounds);
draw_fn(&mut frame);
- let geometry = frame.into_geometry().cache();
+ let geometry = frame.into_geometry().cache(previous);
let result = Cached::load(&geometry);
*self.state.borrow_mut() = State::Filled { bounds, geometry };
@@ -79,7 +90,7 @@ where
let state = self.state.borrow();
match *state {
- State::Empty => write!(f, "Cache::Empty"),
+ State::Empty { .. } => write!(f, "Cache::Empty"),
State::Filled { bounds, .. } => {
write!(f, "Cache::Filled {{ bounds: {bounds:?} }}")
}
@@ -100,7 +111,9 @@ enum State<Geometry>
where
Geometry: Cached,
{
- Empty,
+ Empty {
+ previous: Option<Geometry::Cache>,
+ },
Filled {
bounds: Size,
geometry: Geometry::Cache,
diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs
index ad35e8ec..377589d7 100644
--- a/graphics/src/geometry/frame.rs
+++ b/graphics/src/geometry/frame.rs
@@ -113,13 +113,11 @@ where
region: Rectangle,
f: impl FnOnce(&mut Self) -> R,
) -> R {
- let mut frame = self.draft(region.size());
+ let mut frame = self.draft(region);
let result = f(&mut frame);
- let origin = Point::new(region.x, region.y);
-
- self.paste(frame, origin);
+ self.paste(frame, Point::new(region.x, region.y));
result
}
@@ -129,14 +127,14 @@ where
/// Draw its contents back to this [`Frame`] with [`paste`].
///
/// [`paste`]: Self::paste
- pub fn draft(&mut self, size: Size) -> Self {
+ fn draft(&mut self, clip_bounds: Rectangle) -> Self {
Self {
- raw: self.raw.draft(size),
+ raw: self.raw.draft(clip_bounds),
}
}
/// Draws the contents of the given [`Frame`] with origin at the given [`Point`].
- pub fn paste(&mut self, frame: Self, at: Point) {
+ fn paste(&mut self, frame: Self, at: Point) {
self.raw.paste(frame.raw, at);
}
@@ -187,7 +185,7 @@ pub trait Backend: Sized {
fn scale(&mut self, scale: impl Into<f32>);
fn scale_nonuniform(&mut self, scale: impl Into<Vector>);
- fn draft(&mut self, size: Size) -> Self;
+ fn draft(&mut self, clip_bounds: Rectangle) -> Self;
fn paste(&mut self, frame: Self, at: Point);
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>);
@@ -232,7 +230,7 @@ impl Backend for () {
fn scale(&mut self, _scale: impl Into<f32>) {}
fn scale_nonuniform(&mut self, _scale: impl Into<Vector>) {}
- fn draft(&mut self, _size: Size) -> Self {}
+ fn draft(&mut self, _clip_bounds: Rectangle) -> Self {}
fn paste(&mut self, _frame: Self, _at: Point) {}
fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into<Stroke<'a>>) {}
diff --git a/graphics/src/image.rs b/graphics/src/image.rs
index d89caace..c6135e9e 100644
--- a/graphics/src/image.rs
+++ b/graphics/src/image.rs
@@ -1,14 +1,107 @@
//! Load and operate on images.
-use crate::core::image::{Data, Handle};
+#[cfg(feature = "image")]
+pub use ::image as image_rs;
-use bitflags::bitflags;
+use crate::core::image;
+use crate::core::svg;
+use crate::core::{Color, Rectangle};
-pub use ::image as image_rs;
+/// A raster or vector image.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Image {
+ /// A raster image.
+ Raster {
+ /// The handle of a raster image.
+ handle: image::Handle,
+
+ /// The filter method of a raster image.
+ filter_method: image::FilterMethod,
+
+ /// The bounds of the image.
+ bounds: Rectangle,
+ },
+ /// A vector image.
+ Vector {
+ /// The handle of a vector image.
+ handle: svg::Handle,
+
+ /// The [`Color`] filter
+ color: Option<Color>,
+
+ /// The bounds of the image.
+ bounds: Rectangle,
+ },
+}
+
+impl Image {
+ /// Returns the bounds of the [`Image`].
+ pub fn bounds(&self) -> Rectangle {
+ match self {
+ Image::Raster { bounds, .. } | Image::Vector { bounds, .. } => {
+ *bounds
+ }
+ }
+ }
+}
+#[cfg(feature = "image")]
/// Tries to load an image by its [`Handle`].
-pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> {
+///
+/// [`Handle`]: image::Handle
+pub fn load(
+ handle: &image::Handle,
+) -> ::image::ImageResult<::image::DynamicImage> {
+ use bitflags::bitflags;
+
+ bitflags! {
+ struct Operation: u8 {
+ const FLIP_HORIZONTALLY = 0b001;
+ const ROTATE_180 = 0b010;
+ const FLIP_DIAGONALLY = 0b100;
+ }
+ }
+
+ impl Operation {
+ // Meaning of the returned value is described e.g. at:
+ // https://magnushoff.com/articles/jpeg-orientation/
+ fn from_exif<R>(reader: &mut R) -> Result<Self, exif::Error>
+ where
+ R: std::io::BufRead + std::io::Seek,
+ {
+ let exif = exif::Reader::new().read_from_container(reader)?;
+
+ Ok(exif
+ .get_field(exif::Tag::Orientation, exif::In::PRIMARY)
+ .and_then(|field| field.value.get_uint(0))
+ .and_then(|value| u8::try_from(value).ok())
+ .and_then(|value| Self::from_bits(value.saturating_sub(1)))
+ .unwrap_or_else(Self::empty))
+ }
+
+ fn perform(
+ self,
+ mut image: ::image::DynamicImage,
+ ) -> ::image::DynamicImage {
+ use ::image::imageops;
+
+ if self.contains(Self::FLIP_DIAGONALLY) {
+ imageops::flip_vertical_in_place(&mut image);
+ }
+
+ if self.contains(Self::ROTATE_180) {
+ imageops::rotate180_in_place(&mut image);
+ }
+
+ if self.contains(Self::FLIP_HORIZONTALLY) {
+ imageops::flip_horizontal_in_place(&mut image);
+ }
+
+ image
+ }
+ }
+
match handle.data() {
- Data::Path(path) => {
+ image::Data::Path(path) => {
let image = ::image::open(path)?;
let operation = std::fs::File::open(path)
@@ -19,7 +112,7 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> {
Ok(operation.perform(image))
}
- Data::Bytes(bytes) => {
+ image::Data::Bytes(bytes) => {
let image = ::image::load_from_memory(bytes)?;
let operation =
Operation::from_exif(&mut std::io::Cursor::new(bytes))
@@ -28,68 +121,22 @@ pub fn load(handle: &Handle) -> image_rs::ImageResult<image_rs::DynamicImage> {
Ok(operation.perform(image))
}
- Data::Rgba {
+ image::Data::Rgba {
width,
height,
pixels,
} => {
- if let Some(image) = image_rs::ImageBuffer::from_vec(
- *width,
- *height,
- pixels.to_vec(),
- ) {
- Ok(image_rs::DynamicImage::ImageRgba8(image))
+ if let Some(image) =
+ ::image::ImageBuffer::from_vec(*width, *height, pixels.to_vec())
+ {
+ Ok(::image::DynamicImage::ImageRgba8(image))
} else {
- Err(image_rs::error::ImageError::Limits(
- image_rs::error::LimitError::from_kind(
- image_rs::error::LimitErrorKind::DimensionError,
+ Err(::image::error::ImageError::Limits(
+ ::image::error::LimitError::from_kind(
+ ::image::error::LimitErrorKind::DimensionError,
),
))
}
}
}
}
-
-bitflags! {
- struct Operation: u8 {
- const FLIP_HORIZONTALLY = 0b001;
- const ROTATE_180 = 0b010;
- const FLIP_DIAGONALLY = 0b100;
- }
-}
-
-impl Operation {
- // Meaning of the returned value is described e.g. at:
- // https://magnushoff.com/articles/jpeg-orientation/
- fn from_exif<R>(reader: &mut R) -> Result<Self, exif::Error>
- where
- R: std::io::BufRead + std::io::Seek,
- {
- let exif = exif::Reader::new().read_from_container(reader)?;
-
- Ok(exif
- .get_field(exif::Tag::Orientation, exif::In::PRIMARY)
- .and_then(|field| field.value.get_uint(0))
- .and_then(|value| u8::try_from(value).ok())
- .and_then(|value| Self::from_bits(value.saturating_sub(1)))
- .unwrap_or_else(Self::empty))
- }
-
- fn perform(self, mut image: image::DynamicImage) -> image::DynamicImage {
- use image::imageops;
-
- if self.contains(Self::FLIP_DIAGONALLY) {
- imageops::flip_vertical_in_place(&mut image);
- }
-
- if self.contains(Self::ROTATE_180) {
- imageops::rotate180_in_place(&mut image);
- }
-
- if self.contains(Self::FLIP_HORIZONTALLY) {
- imageops::flip_horizontal_in_place(&mut image);
- }
-
- image
- }
-}
diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs
new file mode 100644
index 00000000..c9a818fb
--- /dev/null
+++ b/graphics/src/layer.rs
@@ -0,0 +1,144 @@
+//! Draw and stack layers of graphical primitives.
+use crate::core::{Rectangle, Transformation};
+
+/// A layer of graphical primitives.
+///
+/// Layers normally dictate a set of primitives that are
+/// rendered in a specific order.
+pub trait Layer: Default {
+ /// Creates a new [`Layer`] with the given bounds.
+ fn with_bounds(bounds: Rectangle) -> Self;
+
+ /// Flushes and settles any pending group of primitives in the [`Layer`].
+ ///
+ /// This will be called when a [`Layer`] is finished. It allows layers to efficiently
+ /// record primitives together and defer grouping until the end.
+ fn flush(&mut self);
+
+ /// Resizes the [`Layer`] to the given bounds.
+ fn resize(&mut self, bounds: Rectangle);
+
+ /// Clears all the layers contents and resets its bounds.
+ fn reset(&mut self);
+}
+
+/// A stack of layers used for drawing.
+#[derive(Debug)]
+pub struct Stack<T: Layer> {
+ layers: Vec<T>,
+ transformations: Vec<Transformation>,
+ previous: Vec<usize>,
+ current: usize,
+ active_count: usize,
+}
+
+impl<T: Layer> Stack<T> {
+ /// Creates a new empty [`Stack`].
+ pub fn new() -> Self {
+ Self {
+ layers: vec![T::default()],
+ transformations: vec![Transformation::IDENTITY],
+ previous: vec![],
+ current: 0,
+ active_count: 1,
+ }
+ }
+
+ /// Returns a mutable reference to the current [`Layer`] of the [`Stack`], together with
+ /// the current [`Transformation`].
+ #[inline]
+ pub fn current_mut(&mut self) -> (&mut T, Transformation) {
+ let transformation = self.transformation();
+
+ (&mut self.layers[self.current], transformation)
+ }
+
+ /// Returns the current [`Transformation`] of the [`Stack`].
+ #[inline]
+ pub fn transformation(&self) -> Transformation {
+ self.transformations.last().copied().unwrap()
+ }
+
+ /// Pushes a new clipping region in the [`Stack`]; creating a new layer in the
+ /// process.
+ pub fn push_clip(&mut self, bounds: Rectangle) {
+ self.previous.push(self.current);
+
+ self.current = self.active_count;
+ self.active_count += 1;
+
+ let bounds = bounds * self.transformation();
+
+ if self.current == self.layers.len() {
+ self.layers.push(T::with_bounds(bounds));
+ } else {
+ self.layers[self.current].resize(bounds);
+ }
+ }
+
+ /// Pops the current clipping region from the [`Stack`] and restores the previous one.
+ ///
+ /// The current layer will be recorded for drawing.
+ pub fn pop_clip(&mut self) {
+ self.flush();
+
+ self.current = self.previous.pop().unwrap();
+ }
+
+ /// Pushes a new [`Transformation`] in the [`Stack`].
+ ///
+ /// Future drawing operations will be affected by this new [`Transformation`] until
+ /// it is popped using [`pop_transformation`].
+ ///
+ /// [`pop_transformation`]: Self::pop_transformation
+ pub fn push_transformation(&mut self, transformation: Transformation) {
+ self.transformations
+ .push(self.transformation() * transformation);
+ }
+
+ /// Pops the current [`Transformation`] in the [`Stack`].
+ pub fn pop_transformation(&mut self) {
+ let _ = self.transformations.pop();
+ }
+
+ /// Returns an iterator over mutable references to the layers in the [`Stack`].
+ pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
+ self.flush();
+
+ self.layers[..self.active_count].iter_mut()
+ }
+
+ /// Returns an iterator over immutable references to the layers in the [`Stack`].
+ pub fn iter(&self) -> impl Iterator<Item = &T> {
+ self.layers[..self.active_count].iter()
+ }
+
+ /// Returns the slice of layers in the [`Stack`].
+ pub fn as_slice(&self) -> &[T] {
+ &self.layers[..self.active_count]
+ }
+
+ /// Flushes and settles any primitives in the current layer of the [`Stack`].
+ pub fn flush(&mut self) {
+ self.layers[self.current].flush();
+ }
+
+ /// Clears the layers of the [`Stack`], allowing reuse.
+ ///
+ /// This will normally keep layer allocations for future drawing operations.
+ pub fn clear(&mut self) {
+ for layer in self.layers[..self.active_count].iter_mut() {
+ layer.reset();
+ }
+
+ self.current = 0;
+ self.active_count = 1;
+ self.previous.clear();
+ }
+}
+
+impl<T: Layer> Default for Stack<T> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
index d7f2f439..865ebd97 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -7,48 +7,35 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
-#![forbid(rust_2018_idioms)]
-#![deny(
- missing_debug_implementations,
- missing_docs,
- unsafe_code,
- unused_results,
- rustdoc::broken_intra_doc_links
-)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
mod antialiasing;
mod cached;
-mod primitive;
mod settings;
mod viewport;
-pub mod backend;
pub mod color;
pub mod compositor;
pub mod damage;
pub mod error;
pub mod gradient;
+pub mod image;
+pub mod layer;
pub mod mesh;
-pub mod renderer;
pub mod text;
#[cfg(feature = "geometry")]
pub mod geometry;
-#[cfg(feature = "image")]
-pub mod image;
-
pub use antialiasing::Antialiasing;
-pub use backend::Backend;
pub use cached::Cached;
pub use compositor::Compositor;
-pub use damage::Damage;
pub use error::Error;
pub use gradient::Gradient;
+pub use image::Image;
+pub use layer::Layer;
pub use mesh::Mesh;
-pub use primitive::Primitive;
-pub use renderer::Renderer;
pub use settings::Settings;
+pub use text::Text;
pub use viewport::Viewport;
pub use iced_core as core;
diff --git a/graphics/src/mesh.rs b/graphics/src/mesh.rs
index 20692b07..76602319 100644
--- a/graphics/src/mesh.rs
+++ b/graphics/src/mesh.rs
@@ -1,8 +1,7 @@
//! Draw triangles!
use crate::color;
-use crate::core::{Rectangle, Size};
+use crate::core::{Rectangle, Transformation};
use crate::gradient;
-use crate::Damage;
use bytemuck::{Pod, Zeroable};
@@ -14,29 +13,55 @@ pub enum Mesh {
/// The vertices and indices of the mesh.
buffers: Indexed<SolidVertex2D>,
- /// The size of the drawable region of the mesh.
- ///
- /// Any geometry that falls out of this region will be clipped.
- size: Size,
+ /// The [`Transformation`] for the vertices of the [`Mesh`].
+ transformation: Transformation,
+
+ /// The clip bounds of the [`Mesh`].
+ clip_bounds: Rectangle,
},
/// A mesh with a gradient.
Gradient {
/// The vertices and indices of the mesh.
buffers: Indexed<GradientVertex2D>,
- /// The size of the drawable region of the mesh.
- ///
- /// Any geometry that falls out of this region will be clipped.
- size: Size,
+ /// The [`Transformation`] for the vertices of the [`Mesh`].
+ transformation: Transformation,
+
+ /// The clip bounds of the [`Mesh`].
+ clip_bounds: Rectangle,
},
}
-impl Damage for Mesh {
- fn bounds(&self) -> Rectangle {
+impl Mesh {
+ /// Returns the indices of the [`Mesh`].
+ pub fn indices(&self) -> &[u32] {
+ match self {
+ Self::Solid { buffers, .. } => &buffers.indices,
+ Self::Gradient { buffers, .. } => &buffers.indices,
+ }
+ }
+
+ /// Returns the [`Transformation`] of the [`Mesh`].
+ pub fn transformation(&self) -> Transformation {
+ match self {
+ Self::Solid { transformation, .. }
+ | Self::Gradient { transformation, .. } => *transformation,
+ }
+ }
+
+ /// Returns the clip bounds of the [`Mesh`].
+ pub fn clip_bounds(&self) -> Rectangle {
match self {
- Self::Solid { size, .. } | Self::Gradient { size, .. } => {
- Rectangle::with_size(*size)
+ Self::Solid {
+ clip_bounds,
+ transformation,
+ ..
}
+ | Self::Gradient {
+ clip_bounds,
+ transformation,
+ ..
+ } => *clip_bounds * *transformation,
}
}
}
@@ -75,6 +100,47 @@ pub struct GradientVertex2D {
pub gradient: gradient::Packed,
}
+/// The result of counting the attributes of a set of meshes.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct AttributeCount {
+ /// The total amount of solid vertices.
+ pub solid_vertices: usize,
+
+ /// The total amount of solid meshes.
+ pub solids: usize,
+
+ /// The total amount of gradient vertices.
+ pub gradient_vertices: usize,
+
+ /// The total amount of gradient meshes.
+ pub gradients: usize,
+
+ /// The total amount of indices.
+ pub indices: usize,
+}
+
+/// Returns the number of total vertices & total indices of all [`Mesh`]es.
+pub fn attribute_count_of(meshes: &[Mesh]) -> AttributeCount {
+ meshes
+ .iter()
+ .fold(AttributeCount::default(), |mut count, mesh| {
+ match mesh {
+ Mesh::Solid { buffers, .. } => {
+ count.solids += 1;
+ count.solid_vertices += buffers.vertices.len();
+ count.indices += buffers.indices.len();
+ }
+ Mesh::Gradient { buffers, .. } => {
+ count.gradients += 1;
+ count.gradient_vertices += buffers.vertices.len();
+ count.indices += buffers.indices.len();
+ }
+ }
+
+ count
+ })
+}
+
/// A renderer capable of drawing a [`Mesh`].
pub trait Renderer {
/// Draws the given [`Mesh`].
diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs
deleted file mode 100644
index 6929b0a1..00000000
--- a/graphics/src/primitive.rs
+++ /dev/null
@@ -1,160 +0,0 @@
-//! Draw using different graphical primitives.
-use crate::core::alignment;
-use crate::core::image;
-use crate::core::svg;
-use crate::core::text;
-use crate::core::{
- Background, Border, Color, Font, Pixels, Point, Rectangle, Shadow,
- Transformation, Vector,
-};
-use crate::text::editor;
-use crate::text::paragraph;
-
-use std::sync::Arc;
-
-/// A rendering primitive.
-#[derive(Debug, Clone, PartialEq)]
-pub enum Primitive<T> {
- /// A text primitive
- Text {
- /// The contents of the text.
- content: String,
- /// The bounds of the text.
- bounds: Rectangle,
- /// The color of the text.
- color: Color,
- /// The size of the text in logical pixels.
- size: Pixels,
- /// The line height of the text.
- line_height: text::LineHeight,
- /// The font of the text.
- font: Font,
- /// The horizontal alignment of the text.
- horizontal_alignment: alignment::Horizontal,
- /// The vertical alignment of the text.
- vertical_alignment: alignment::Vertical,
- /// The shaping strategy of the text.
- shaping: text::Shaping,
- /// The clip bounds of the text.
- clip_bounds: Rectangle,
- },
- /// A paragraph primitive
- Paragraph {
- /// The [`paragraph::Weak`] reference.
- paragraph: paragraph::Weak,
- /// The position of the paragraph.
- position: Point,
- /// The color of the paragraph.
- color: Color,
- /// The clip bounds of the paragraph.
- clip_bounds: Rectangle,
- },
- /// An editor primitive
- Editor {
- /// The [`editor::Weak`] reference.
- editor: editor::Weak,
- /// The position of the editor.
- position: Point,
- /// The color of the editor.
- color: Color,
- /// The clip bounds of the editor.
- clip_bounds: Rectangle,
- },
- /// A raw `cosmic-text` primitive
- RawText(crate::text::Raw),
- /// A quad primitive
- Quad {
- /// The bounds of the quad
- bounds: Rectangle,
- /// The background of the quad
- background: Background,
- /// The [`Border`] of the quad
- border: Border,
- /// The [`Shadow`] of the quad
- shadow: Shadow,
- },
- /// An image primitive
- Image {
- /// The handle of the image
- handle: image::Handle,
- /// The filter method of the image
- filter_method: image::FilterMethod,
- /// The bounds of the image
- bounds: Rectangle,
- },
- /// An SVG primitive
- Svg {
- /// The path of the SVG file
- handle: svg::Handle,
-
- /// The [`Color`] filter
- color: Option<Color>,
-
- /// The bounds of the viewport
- bounds: Rectangle,
- },
- /// A group of primitives
- Group {
- /// The primitives of the group
- primitives: Vec<Primitive<T>>,
- },
- /// A clip primitive
- Clip {
- /// The bounds of the clip
- bounds: Rectangle,
- /// The content of the clip
- content: Box<Primitive<T>>,
- },
- /// A primitive that applies a [`Transformation`]
- Transform {
- /// The [`Transformation`]
- transformation: Transformation,
-
- /// The primitive to transform
- content: Box<Primitive<T>>,
- },
- /// A cached primitive.
- ///
- /// This can be useful if you are implementing a widget where primitive
- /// generation is expensive.
- Cache {
- /// The cached primitive
- content: Arc<Primitive<T>>,
- },
- /// A backend-specific primitive.
- Custom(T),
-}
-
-impl<T> Primitive<T> {
- /// Groups the current [`Primitive`].
- pub fn group(primitives: Vec<Self>) -> Self {
- Self::Group { primitives }
- }
-
- /// Clips the current [`Primitive`].
- pub fn clip(self, bounds: Rectangle) -> Self {
- Self::Clip {
- bounds,
- content: Box::new(self),
- }
- }
-
- /// Translates the current [`Primitive`].
- pub fn translate(self, translation: Vector) -> Self {
- Self::Transform {
- transformation: Transformation::translate(
- translation.x,
- translation.y,
- ),
- content: Box::new(self),
- }
- }
-
- /// Transforms the current [`Primitive`].
- pub fn transform(self, transformation: Transformation) -> Self {
- Self::Transform {
- transformation,
- content: Box::new(self),
- }
- }
-}
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
deleted file mode 100644
index f517ff3e..00000000
--- a/graphics/src/renderer.rs
+++ /dev/null
@@ -1,269 +0,0 @@
-//! Create a renderer from a [`Backend`].
-use crate::backend::{self, Backend};
-use crate::compositor;
-use crate::core;
-use crate::core::image;
-use crate::core::renderer;
-use crate::core::svg;
-use crate::core::text::Text;
-use crate::core::{
- Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
-};
-use crate::mesh;
-use crate::text;
-use crate::{Mesh, Primitive};
-
-use std::borrow::Cow;
-
-/// A backend-agnostic renderer that supports all the built-in widgets.
-#[derive(Debug)]
-pub struct Renderer<B: Backend> {
- backend: B,
- default_font: Font,
- default_text_size: Pixels,
- primitives: Vec<Primitive<B::Primitive>>,
- stack: Vec<Vec<Primitive<B::Primitive>>>,
-}
-
-impl<B: Backend> Renderer<B> {
- /// Creates a new [`Renderer`] from the given [`Backend`].
- pub fn new(
- backend: B,
- default_font: Font,
- default_text_size: Pixels,
- ) -> Self {
- Self {
- backend,
- default_font,
- default_text_size,
- primitives: Vec::new(),
- stack: Vec::new(),
- }
- }
-
- /// Returns a reference to the [`Backend`] of the [`Renderer`].
- pub fn backend(&self) -> &B {
- &self.backend
- }
-
- /// Enqueues the given [`Primitive`] in the [`Renderer`] for drawing.
- pub fn draw_primitive(&mut self, primitive: Primitive<B::Primitive>) {
- self.primitives.push(primitive);
- }
-
- /// Runs the given closure with the [`Backend`] and the recorded primitives
- /// of the [`Renderer`].
- pub fn with_primitives<O>(
- &mut self,
- f: impl FnOnce(&mut B, &[Primitive<B::Primitive>]) -> O,
- ) -> O {
- f(&mut self.backend, &self.primitives)
- }
-}
-
-impl<B: Backend> iced_core::Renderer for Renderer<B> {
- fn start_layer(&mut self) {
- self.stack.push(std::mem::take(&mut self.primitives));
- }
-
- fn end_layer(&mut self, bounds: Rectangle) {
- let layer = std::mem::replace(
- &mut self.primitives,
- self.stack.pop().expect("a layer should be recording"),
- );
-
- self.primitives.push(Primitive::group(layer).clip(bounds));
- }
-
- fn start_transformation(&mut self) {
- self.stack.push(std::mem::take(&mut self.primitives));
- }
-
- fn end_transformation(&mut self, transformation: Transformation) {
- let layer = std::mem::replace(
- &mut self.primitives,
- self.stack.pop().expect("a layer should be recording"),
- );
-
- self.primitives
- .push(Primitive::group(layer).transform(transformation));
- }
-
- fn fill_quad(
- &mut self,
- quad: renderer::Quad,
- background: impl Into<Background>,
- ) {
- self.primitives.push(Primitive::Quad {
- bounds: quad.bounds,
- background: background.into(),
- border: quad.border,
- shadow: quad.shadow,
- });
- }
-
- fn clear(&mut self) {
- self.primitives.clear();
- }
-}
-
-impl<B> core::text::Renderer for Renderer<B>
-where
- B: Backend + backend::Text,
-{
- type Font = Font;
- type Paragraph = text::Paragraph;
- type Editor = text::Editor;
-
- const ICON_FONT: Font = Font::with_name("Iced-Icons");
- const CHECKMARK_ICON: char = '\u{f00c}';
- const ARROW_DOWN_ICON: char = '\u{e800}';
-
- fn default_font(&self) -> Self::Font {
- self.default_font
- }
-
- fn default_size(&self) -> Pixels {
- self.default_text_size
- }
-
- fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- self.backend.load_font(bytes);
- }
-
- fn fill_paragraph(
- &mut self,
- paragraph: &Self::Paragraph,
- position: Point,
- color: Color,
- clip_bounds: Rectangle,
- ) {
- self.primitives.push(Primitive::Paragraph {
- paragraph: paragraph.downgrade(),
- position,
- color,
- clip_bounds,
- });
- }
-
- fn fill_editor(
- &mut self,
- editor: &Self::Editor,
- position: Point,
- color: Color,
- clip_bounds: Rectangle,
- ) {
- self.primitives.push(Primitive::Editor {
- editor: editor.downgrade(),
- position,
- color,
- clip_bounds,
- });
- }
-
- fn fill_text(
- &mut self,
- text: Text<'_, Self::Font>,
- position: Point,
- color: Color,
- clip_bounds: Rectangle,
- ) {
- self.primitives.push(Primitive::Text {
- content: text.content.to_string(),
- bounds: Rectangle::new(position, text.bounds),
- size: text.size,
- line_height: text.line_height,
- color,
- font: text.font,
- horizontal_alignment: text.horizontal_alignment,
- vertical_alignment: text.vertical_alignment,
- shaping: text.shaping,
- clip_bounds,
- });
- }
-}
-
-impl<B> image::Renderer for Renderer<B>
-where
- B: Backend + backend::Image,
-{
- type Handle = image::Handle;
-
- fn measure_image(&self, handle: &image::Handle) -> Size<u32> {
- self.backend().dimensions(handle)
- }
-
- fn draw_image(
- &mut self,
- handle: image::Handle,
- filter_method: image::FilterMethod,
- bounds: Rectangle,
- ) {
- self.primitives.push(Primitive::Image {
- handle,
- filter_method,
- bounds,
- });
- }
-}
-
-impl<B> svg::Renderer for Renderer<B>
-where
- B: Backend + backend::Svg,
-{
- fn measure_svg(&self, handle: &svg::Handle) -> Size<u32> {
- self.backend().viewport_dimensions(handle)
- }
-
- fn draw_svg(
- &mut self,
- handle: svg::Handle,
- color: Option<Color>,
- bounds: Rectangle,
- ) {
- self.primitives.push(Primitive::Svg {
- handle,
- color,
- bounds,
- });
- }
-}
-
-impl<B: Backend> mesh::Renderer for Renderer<B> {
- fn draw_mesh(&mut self, mesh: Mesh) {
- match B::Primitive::try_from(mesh) {
- Ok(primitive) => {
- self.draw_primitive(Primitive::Custom(primitive));
- }
- Err(error) => {
- log::warn!("mesh primitive could not be drawn: {error:?}");
- }
- }
- }
-}
-
-#[cfg(feature = "geometry")]
-impl<B> crate::geometry::Renderer for Renderer<B>
-where
- B: Backend + crate::geometry::Backend,
- B::Frame:
- crate::geometry::frame::Backend<Geometry = Primitive<B::Primitive>>,
-{
- type Frame = B::Frame;
- type Geometry = Primitive<B::Primitive>;
-
- fn new_frame(&self, size: Size) -> Self::Frame {
- self.backend.new_frame(size)
- }
-
- fn draw_geometry(&mut self, geometry: Self::Geometry) {
- self.draw_primitive(geometry);
- }
-}
-
-impl<B> compositor::Default for Renderer<B>
-where
- B: Backend,
-{
- type Compositor = B::Compositor;
-}
diff --git a/graphics/src/settings.rs b/graphics/src/settings.rs
index 68673536..2e8275c6 100644
--- a/graphics/src/settings.rs
+++ b/graphics/src/settings.rs
@@ -1,7 +1,7 @@
use crate::core::{Font, Pixels};
use crate::Antialiasing;
-/// The settings of a Backend.
+/// The settings of a renderer.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
/// The default [`Font`] to use.
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
index 0310ead7..30269e69 100644
--- a/graphics/src/text.rs
+++ b/graphics/src/text.rs
@@ -9,14 +9,141 @@ pub use paragraph::Paragraph;
pub use cosmic_text;
+use crate::core::alignment;
use crate::core::font::{self, Font};
use crate::core::text::Shaping;
-use crate::core::{Color, Point, Rectangle, Size};
+use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation};
use once_cell::sync::OnceCell;
use std::borrow::Cow;
use std::sync::{Arc, RwLock, Weak};
+/// A text primitive.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Text {
+ /// A paragraph.
+ #[allow(missing_docs)]
+ Paragraph {
+ paragraph: paragraph::Weak,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ },
+ /// An editor.
+ #[allow(missing_docs)]
+ Editor {
+ editor: editor::Weak,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ },
+ /// Some cached text.
+ Cached {
+ /// The contents of the text.
+ content: String,
+ /// The bounds of the text.
+ bounds: Rectangle,
+ /// The color of the text.
+ color: Color,
+ /// The size of the text in logical pixels.
+ size: Pixels,
+ /// The line height of the text.
+ line_height: Pixels,
+ /// The font of the text.
+ font: Font,
+ /// The horizontal alignment of the text.
+ horizontal_alignment: alignment::Horizontal,
+ /// The vertical alignment of the text.
+ vertical_alignment: alignment::Vertical,
+ /// The shaping strategy of the text.
+ shaping: Shaping,
+ /// The clip bounds of the text.
+ clip_bounds: Rectangle,
+ },
+ /// Some raw text.
+ #[allow(missing_docs)]
+ Raw {
+ raw: Raw,
+ transformation: Transformation,
+ },
+}
+
+impl Text {
+ /// Returns the visible bounds of the [`Text`].
+ pub fn visible_bounds(&self) -> Option<Rectangle> {
+ let (bounds, horizontal_alignment, vertical_alignment) = match self {
+ Text::Paragraph {
+ position,
+ paragraph,
+ clip_bounds,
+ transformation,
+ ..
+ } => (
+ Rectangle::new(*position, paragraph.min_bounds)
+ .intersection(clip_bounds)
+ .map(|bounds| bounds * *transformation),
+ Some(paragraph.horizontal_alignment),
+ Some(paragraph.vertical_alignment),
+ ),
+ Text::Editor {
+ editor,
+ position,
+ clip_bounds,
+ transformation,
+ ..
+ } => (
+ Rectangle::new(*position, editor.bounds)
+ .intersection(clip_bounds)
+ .map(|bounds| bounds * *transformation),
+ None,
+ None,
+ ),
+ Text::Cached {
+ bounds,
+ clip_bounds,
+ horizontal_alignment,
+ vertical_alignment,
+ ..
+ } => (
+ bounds.intersection(clip_bounds),
+ Some(*horizontal_alignment),
+ Some(*vertical_alignment),
+ ),
+ Text::Raw { raw, .. } => (Some(raw.clip_bounds), None, None),
+ };
+
+ let mut bounds = bounds?;
+
+ if let Some(alignment) = horizontal_alignment {
+ match alignment {
+ alignment::Horizontal::Left => {}
+ alignment::Horizontal::Center => {
+ bounds.x -= bounds.width / 2.0;
+ }
+ alignment::Horizontal::Right => {
+ bounds.x -= bounds.width;
+ }
+ }
+ }
+
+ if let Some(alignment) = vertical_alignment {
+ match alignment {
+ alignment::Vertical::Top => {}
+ alignment::Vertical::Center => {
+ bounds.y -= bounds.height / 2.0;
+ }
+ alignment::Vertical::Bottom => {
+ bounds.y -= bounds.height;
+ }
+ }
+ }
+
+ Some(bounds)
+ }
+}
+
/// The regular variant of the [Fira Sans] font.
///
/// It is loaded as part of the default fonts in Wasm builds.
diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs
index 7fb33567..822b61c4 100644
--- a/graphics/src/text/cache.rs
+++ b/graphics/src/text/cache.rs
@@ -2,22 +2,18 @@
use crate::core::{Font, Size};
use crate::text;
-use rustc_hash::{FxHashMap, FxHashSet};
+use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use std::collections::hash_map;
-use std::hash::{BuildHasher, Hash, Hasher};
+use std::hash::{Hash, Hasher};
/// A store of recently used sections of text.
-#[allow(missing_debug_implementations)]
-#[derive(Default)]
+#[derive(Debug, Default)]
pub struct Cache {
entries: FxHashMap<KeyHash, Entry>,
aliases: FxHashMap<KeyHash, KeyHash>,
recently_used: FxHashSet<KeyHash>,
- hasher: HashBuilder,
}
-type HashBuilder = xxhash_rust::xxh3::Xxh3Builder;
-
impl Cache {
/// Creates a new empty [`Cache`].
pub fn new() -> Self {
@@ -35,7 +31,7 @@ impl Cache {
font_system: &mut cosmic_text::FontSystem,
key: Key<'_>,
) -> (KeyHash, &mut Entry) {
- let hash = key.hash(self.hasher.build_hasher());
+ let hash = key.hash(FxHasher::default());
if let Some(hash) = self.aliases.get(&hash) {
let _ = self.recently_used.insert(*hash);
@@ -77,7 +73,7 @@ impl Cache {
] {
if key.bounds != bounds {
let _ = self.aliases.insert(
- Key { bounds, ..key }.hash(self.hasher.build_hasher()),
+ Key { bounds, ..key }.hash(FxHasher::default()),
hash,
);
}
@@ -138,7 +134,7 @@ impl Key<'_> {
pub type KeyHash = u64;
/// A cache entry.
-#[allow(missing_debug_implementations)]
+#[derive(Debug)]
pub struct Entry {
/// The buffer of text, ready for drawing.
pub buffer: cosmic_text::Buffer,
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
index 5d027542..31a323ac 100644
--- a/graphics/src/text/paragraph.rs
+++ b/graphics/src/text/paragraph.rs
@@ -61,7 +61,7 @@ impl Paragraph {
impl core::text::Paragraph for Paragraph {
type Font = Font;
- fn with_text(text: Text<'_, Font>) -> Self {
+ fn with_text(text: Text<&str>) -> Self {
log::trace!("Allocating paragraph: {}", text.content);
let mut font_system =
@@ -146,7 +146,7 @@ impl core::text::Paragraph for Paragraph {
}
}
- fn compare(&self, text: Text<'_, Font>) -> core::text::Difference {
+ fn compare(&self, text: Text<&str>) -> core::text::Difference {
let font_system = text::font_system().read().expect("Read font system");
let paragraph = self.internal();
let metrics = paragraph.buffer.metrics();
diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml
index 2d108d6f..7962b89d 100644
--- a/highlighter/Cargo.toml
+++ b/highlighter/Cargo.toml
@@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[dependencies]
iced_core.workspace = true
diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs
index 63f21fc0..7636a712 100644
--- a/highlighter/src/lib.rs
+++ b/highlighter/src/lib.rs
@@ -1,3 +1,4 @@
+//! A syntax highlighter for iced.
use iced_core as core;
use crate::core::text::highlighter::{self, Format};
@@ -16,6 +17,8 @@ static THEMES: Lazy<highlighting::ThemeSet> =
const LINES_PER_SNAPSHOT: usize = 50;
+/// A syntax highlighter.
+#[derive(Debug)]
pub struct Highlighter {
syntax: &'static parsing::SyntaxReference,
highlighter: highlighting::Highlighter<'static>,
@@ -131,25 +134,47 @@ impl highlighter::Highlighter for Highlighter {
}
}
+/// The settings of a [`Highlighter`].
#[derive(Debug, Clone, PartialEq)]
pub struct Settings {
+ /// The [`Theme`] of the [`Highlighter`].
+ ///
+ /// It dictates the color scheme that will be used for highlighting.
pub theme: Theme,
+ /// The extension of the file to highlight.
+ ///
+ /// The [`Highlighter`] will use the extension to automatically determine
+ /// the grammar to use for highlighting.
pub extension: String,
}
+/// A highlight produced by a [`Highlighter`].
+#[derive(Debug)]
pub struct Highlight(highlighting::StyleModifier);
impl Highlight {
+ /// Returns the color of this [`Highlight`].
+ ///
+ /// If `None`, the original text color should be unchanged.
pub fn color(&self) -> Option<Color> {
self.0.foreground.map(|color| {
Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0)
})
}
+ /// Returns the font of this [`Highlight`].
+ ///
+ /// If `None`, the original font should be unchanged.
pub fn font(&self) -> Option<Font> {
None
}
+ /// Returns the [`Format`] of the [`Highlight`].
+ ///
+ /// It contains both the [`color`] and the [`font`].
+ ///
+ /// [`color`]: Self::color
+ /// [`font`]: Self::font
pub fn to_format(&self) -> Format<Font> {
Format {
color: self.color(),
@@ -158,6 +183,8 @@ impl Highlight {
}
}
+/// A highlighting theme.
+#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Theme {
SolarizedDark,
@@ -168,6 +195,7 @@ pub enum Theme {
}
impl Theme {
+ /// A static slice containing all the available themes.
pub const ALL: &'static [Self] = &[
Self::SolarizedDark,
Self::Base16Mocha,
@@ -176,6 +204,7 @@ impl Theme {
Self::InspiredGitHub,
];
+ /// Returns `true` if the [`Theme`] is dark, and false otherwise.
pub fn is_dark(self) -> bool {
match self {
Self::SolarizedDark
@@ -209,7 +238,7 @@ impl std::fmt::Display for Theme {
}
}
-pub struct ScopeRangeIterator {
+struct ScopeRangeIterator {
ops: Vec<(usize, parsing::ScopeStackOp)>,
line_length: usize,
index: usize,
diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml
index 39c19fa3..458681dd 100644
--- a/renderer/Cargo.toml
+++ b/renderer/Cargo.toml
@@ -10,13 +10,15 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[features]
wgpu = ["iced_wgpu"]
tiny-skia = ["iced_tiny_skia"]
image = ["iced_tiny_skia?/image", "iced_wgpu?/image"]
svg = ["iced_tiny_skia?/svg", "iced_wgpu?/svg"]
geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/geometry"]
-tracing = ["iced_wgpu?/tracing"]
web-colors = ["iced_wgpu?/web-colors"]
webgl = ["iced_wgpu?/webgl"]
fira-sans = ["iced_graphics/fira-sans"]
diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs
index ef9cc9a9..c932de00 100644
--- a/renderer/src/fallback.rs
+++ b/renderer/src/fallback.rs
@@ -1,3 +1,4 @@
+//! Compose existing renderers and create type-safe fallback strategies.
use crate::core::image;
use crate::core::renderer;
use crate::core::svg;
@@ -8,24 +9,33 @@ use crate::graphics;
use crate::graphics::compositor;
use crate::graphics::mesh;
-pub enum Renderer<L, R> {
- Left(L),
- Right(R),
+use std::borrow::Cow;
+
+/// A renderer `A` with a fallback strategy `B`.
+///
+/// This type can be used to easily compose existing renderers and
+/// create custom, type-safe fallback strategies.
+#[derive(Debug)]
+pub enum Renderer<A, B> {
+ /// The primary rendering option.
+ Primary(A),
+ /// The secondary (or fallback) rendering option.
+ Secondary(B),
}
macro_rules! delegate {
($renderer:expr, $name:ident, $body:expr) => {
match $renderer {
- Self::Left($name) => $body,
- Self::Right($name) => $body,
+ Self::Primary($name) => $body,
+ Self::Secondary($name) => $body,
}
};
}
-impl<L, R> core::Renderer for Renderer<L, R>
+impl<A, B> core::Renderer for Renderer<A, B>
where
- L: core::Renderer,
- R: core::Renderer,
+ A: core::Renderer,
+ B: core::Renderer,
{
fn fill_quad(
&mut self,
@@ -39,39 +49,43 @@ where
delegate!(self, renderer, renderer.clear());
}
- fn start_layer(&mut self) {
- delegate!(self, renderer, renderer.start_layer());
+ fn start_layer(&mut self, bounds: Rectangle) {
+ delegate!(self, renderer, renderer.start_layer(bounds));
}
- fn end_layer(&mut self, bounds: Rectangle) {
- delegate!(self, renderer, renderer.end_layer(bounds));
+ fn end_layer(&mut self) {
+ delegate!(self, renderer, renderer.end_layer());
}
- fn start_transformation(&mut self) {
- delegate!(self, renderer, renderer.start_transformation());
+ fn start_transformation(&mut self, transformation: Transformation) {
+ delegate!(
+ self,
+ renderer,
+ renderer.start_transformation(transformation)
+ );
}
- fn end_transformation(&mut self, transformation: Transformation) {
- delegate!(self, renderer, renderer.end_transformation(transformation));
+ fn end_transformation(&mut self) {
+ delegate!(self, renderer, renderer.end_transformation());
}
}
-impl<L, R> core::text::Renderer for Renderer<L, R>
+impl<A, B> core::text::Renderer for Renderer<A, B>
where
- L: core::text::Renderer,
- R: core::text::Renderer<
- Font = L::Font,
- Paragraph = L::Paragraph,
- Editor = L::Editor,
+ A: core::text::Renderer,
+ B: core::text::Renderer<
+ Font = A::Font,
+ Paragraph = A::Paragraph,
+ Editor = A::Editor,
>,
{
- type Font = L::Font;
- type Paragraph = L::Paragraph;
- type Editor = L::Editor;
+ type Font = A::Font;
+ type Paragraph = A::Paragraph;
+ type Editor = A::Editor;
- const ICON_FONT: Self::Font = L::ICON_FONT;
- const CHECKMARK_ICON: char = L::CHECKMARK_ICON;
- const ARROW_DOWN_ICON: char = L::ARROW_DOWN_ICON;
+ const ICON_FONT: Self::Font = A::ICON_FONT;
+ const CHECKMARK_ICON: char = A::CHECKMARK_ICON;
+ const ARROW_DOWN_ICON: char = A::ARROW_DOWN_ICON;
fn default_font(&self) -> Self::Font {
delegate!(self, renderer, renderer.default_font())
@@ -81,10 +95,6 @@ where
delegate!(self, renderer, renderer.default_size())
}
- fn load_font(&mut self, font: std::borrow::Cow<'static, [u8]>) {
- delegate!(self, renderer, renderer.load_font(font));
- }
-
fn fill_paragraph(
&mut self,
text: &Self::Paragraph,
@@ -115,7 +125,7 @@ where
fn fill_text(
&mut self,
- text: core::Text<'_, Self::Font>,
+ text: core::Text<String, Self::Font>,
position: Point,
color: Color,
clip_bounds: Rectangle,
@@ -128,12 +138,12 @@ where
}
}
-impl<L, R> image::Renderer for Renderer<L, R>
+impl<A, B> image::Renderer for Renderer<A, B>
where
- L: image::Renderer,
- R: image::Renderer<Handle = L::Handle>,
+ A: image::Renderer,
+ B: image::Renderer<Handle = A::Handle>,
{
- type Handle = L::Handle;
+ type Handle = A::Handle;
fn measure_image(&self, handle: &Self::Handle) -> Size<u32> {
delegate!(self, renderer, renderer.measure_image(handle))
@@ -153,10 +163,10 @@ where
}
}
-impl<L, R> svg::Renderer for Renderer<L, R>
+impl<A, B> svg::Renderer for Renderer<A, B>
where
- L: svg::Renderer,
- R: svg::Renderer,
+ A: svg::Renderer,
+ B: svg::Renderer,
{
fn measure_svg(&self, handle: &svg::Handle) -> Size<u32> {
delegate!(self, renderer, renderer.measure_svg(handle))
@@ -172,37 +182,49 @@ where
}
}
-impl<L, R> mesh::Renderer for Renderer<L, R>
+impl<A, B> mesh::Renderer for Renderer<A, B>
where
- L: mesh::Renderer,
- R: mesh::Renderer,
+ A: mesh::Renderer,
+ B: mesh::Renderer,
{
fn draw_mesh(&mut self, mesh: graphics::Mesh) {
delegate!(self, renderer, renderer.draw_mesh(mesh));
}
}
-pub enum Compositor<L, R>
+/// A compositor `A` with a fallback strategy `B`.
+///
+/// It works analogously to [`Renderer`].
+#[derive(Debug)]
+pub enum Compositor<A, B>
where
- L: graphics::Compositor,
- R: graphics::Compositor,
+ A: graphics::Compositor,
+ B: graphics::Compositor,
{
- Left(L),
- Right(R),
+ /// The primary compositing option.
+ Primary(A),
+ /// The secondary (or fallback) compositing option.
+ Secondary(B),
}
-pub enum Surface<L, R> {
- Left(L),
- Right(R),
+/// A surface `A` with a fallback strategy `B`.
+///
+/// It works analogously to [`Renderer`].
+#[derive(Debug)]
+pub enum Surface<A, B> {
+ /// The primary surface option.
+ Primary(A),
+ /// The secondary (or fallback) surface option.
+ Secondary(B),
}
-impl<L, R> graphics::Compositor for Compositor<L, R>
+impl<A, B> graphics::Compositor for Compositor<A, B>
where
- L: graphics::Compositor,
- R: graphics::Compositor,
+ A: graphics::Compositor,
+ B: graphics::Compositor,
{
- type Renderer = Renderer<L::Renderer, R::Renderer>;
- type Surface = Surface<L::Surface, R::Surface>;
+ type Renderer = Renderer<A::Renderer, B::Renderer>;
+ type Surface = Surface<A::Surface, B::Surface>;
async fn with_backend<W: compositor::Window + Clone>(
settings: graphics::Settings,
@@ -233,19 +255,19 @@ where
let mut errors = vec![];
for backend in candidates.iter().map(Option::as_deref) {
- match L::with_backend(settings, compatible_window.clone(), backend)
+ match A::with_backend(settings, compatible_window.clone(), backend)
.await
{
- Ok(compositor) => return Ok(Self::Left(compositor)),
+ Ok(compositor) => return Ok(Self::Primary(compositor)),
Err(error) => {
errors.push(error);
}
}
- match R::with_backend(settings, compatible_window.clone(), backend)
+ match B::with_backend(settings, compatible_window.clone(), backend)
.await
{
- Ok(compositor) => return Ok(Self::Right(compositor)),
+ Ok(compositor) => return Ok(Self::Secondary(compositor)),
Err(error) => {
errors.push(error);
}
@@ -257,11 +279,11 @@ where
fn create_renderer(&self) -> Self::Renderer {
match self {
- Self::Left(compositor) => {
- Renderer::Left(compositor.create_renderer())
+ Self::Primary(compositor) => {
+ Renderer::Primary(compositor.create_renderer())
}
- Self::Right(compositor) => {
- Renderer::Right(compositor.create_renderer())
+ Self::Secondary(compositor) => {
+ Renderer::Secondary(compositor.create_renderer())
}
}
}
@@ -273,12 +295,12 @@ where
height: u32,
) -> Self::Surface {
match self {
- Self::Left(compositor) => {
- Surface::Left(compositor.create_surface(window, width, height))
- }
- Self::Right(compositor) => {
- Surface::Right(compositor.create_surface(window, width, height))
- }
+ Self::Primary(compositor) => Surface::Primary(
+ compositor.create_surface(window, width, height),
+ ),
+ Self::Secondary(compositor) => Surface::Secondary(
+ compositor.create_surface(window, width, height),
+ ),
}
}
@@ -289,16 +311,20 @@ where
height: u32,
) {
match (self, surface) {
- (Self::Left(compositor), Surface::Left(surface)) => {
+ (Self::Primary(compositor), Surface::Primary(surface)) => {
compositor.configure_surface(surface, width, height);
}
- (Self::Right(compositor), Surface::Right(surface)) => {
+ (Self::Secondary(compositor), Surface::Secondary(surface)) => {
compositor.configure_surface(surface, width, height);
}
_ => unreachable!(),
}
}
+ fn load_font(&mut self, font: Cow<'static, [u8]>) {
+ delegate!(self, compositor, compositor.load_font(font));
+ }
+
fn fetch_information(&self) -> compositor::Information {
delegate!(self, compositor, compositor.fetch_information())
}
@@ -313,9 +339,9 @@ where
) -> Result<(), compositor::SurfaceError> {
match (self, renderer, surface) {
(
- Self::Left(compositor),
- Renderer::Left(renderer),
- Surface::Left(surface),
+ Self::Primary(compositor),
+ Renderer::Primary(renderer),
+ Surface::Primary(surface),
) => compositor.present(
renderer,
surface,
@@ -324,9 +350,9 @@ where
overlay,
),
(
- Self::Right(compositor),
- Renderer::Right(renderer),
- Surface::Right(surface),
+ Self::Secondary(compositor),
+ Renderer::Secondary(renderer),
+ Surface::Secondary(surface),
) => compositor.present(
renderer,
surface,
@@ -348,9 +374,9 @@ where
) -> Vec<u8> {
match (self, renderer, surface) {
(
- Self::Left(compositor),
- Renderer::Left(renderer),
- Surface::Left(surface),
+ Self::Primary(compositor),
+ Renderer::Primary(renderer),
+ Surface::Primary(surface),
) => compositor.screenshot(
renderer,
surface,
@@ -359,9 +385,9 @@ where
overlay,
),
(
- Self::Right(compositor),
- Renderer::Right(renderer),
- Surface::Right(surface),
+ Self::Secondary(compositor),
+ Renderer::Secondary(renderer),
+ Surface::Secondary(surface),
) => compositor.screenshot(
renderer,
surface,
@@ -375,21 +401,21 @@ where
}
#[cfg(feature = "wgpu")]
-impl<L, R> iced_wgpu::primitive::pipeline::Renderer for Renderer<L, R>
+impl<A, B> iced_wgpu::primitive::Renderer for Renderer<A, B>
where
- L: iced_wgpu::primitive::pipeline::Renderer,
- R: core::Renderer,
+ A: iced_wgpu::primitive::Renderer,
+ B: core::Renderer,
{
- fn draw_pipeline_primitive(
+ fn draw_primitive(
&mut self,
bounds: Rectangle,
- primitive: impl iced_wgpu::primitive::pipeline::Primitive,
+ primitive: impl iced_wgpu::Primitive,
) {
match self {
- Self::Left(renderer) => {
- renderer.draw_pipeline_primitive(bounds, primitive);
+ Self::Primary(renderer) => {
+ renderer.draw_primitive(bounds, primitive);
}
- Self::Right(_) => {
+ Self::Secondary(_) => {
log::warn!(
"Custom shader primitive is not supported with this renderer."
);
@@ -401,31 +427,35 @@ where
#[cfg(feature = "geometry")]
mod geometry {
use super::Renderer;
- use crate::core::{Point, Radians, Size, Vector};
+ use crate::core::{Point, Radians, Rectangle, Size, Vector};
use crate::graphics::geometry::{self, Fill, Path, Stroke, Text};
use crate::graphics::Cached;
- impl<L, R> geometry::Renderer for Renderer<L, R>
+ impl<A, B> geometry::Renderer for Renderer<A, B>
where
- L: geometry::Renderer,
- R: geometry::Renderer,
+ A: geometry::Renderer,
+ B: geometry::Renderer,
{
- type Geometry = Geometry<L::Geometry, R::Geometry>;
- type Frame = Frame<L::Frame, R::Frame>;
+ type Geometry = Geometry<A::Geometry, B::Geometry>;
+ type Frame = Frame<A::Frame, B::Frame>;
fn new_frame(&self, size: iced_graphics::core::Size) -> Self::Frame {
match self {
- Self::Left(renderer) => Frame::Left(renderer.new_frame(size)),
- Self::Right(renderer) => Frame::Right(renderer.new_frame(size)),
+ Self::Primary(renderer) => {
+ Frame::Primary(renderer.new_frame(size))
+ }
+ Self::Secondary(renderer) => {
+ Frame::Secondary(renderer.new_frame(size))
+ }
}
}
fn draw_geometry(&mut self, geometry: Self::Geometry) {
match (self, geometry) {
- (Self::Left(renderer), Geometry::Left(geometry)) => {
+ (Self::Primary(renderer), Geometry::Primary(geometry)) => {
renderer.draw_geometry(geometry);
}
- (Self::Right(renderer), Geometry::Right(geometry)) => {
+ (Self::Secondary(renderer), Geometry::Secondary(geometry)) => {
renderer.draw_geometry(geometry);
}
_ => unreachable!(),
@@ -433,44 +463,59 @@ mod geometry {
}
}
- pub enum Geometry<L, R> {
- Left(L),
- Right(R),
+ #[derive(Debug, Clone)]
+ pub enum Geometry<A, B> {
+ Primary(A),
+ Secondary(B),
}
- impl<L, R> Cached for Geometry<L, R>
+ impl<A, B> Cached for Geometry<A, B>
where
- L: Cached,
- R: Cached,
+ A: Cached,
+ B: Cached,
{
- type Cache = Geometry<L::Cache, R::Cache>;
+ type Cache = Geometry<A::Cache, B::Cache>;
fn load(cache: &Self::Cache) -> Self {
match cache {
- Geometry::Left(cache) => Self::Left(L::load(cache)),
- Geometry::Right(cache) => Self::Right(R::load(cache)),
+ Geometry::Primary(cache) => Self::Primary(A::load(cache)),
+ Geometry::Secondary(cache) => Self::Secondary(B::load(cache)),
}
}
- fn cache(self) -> Self::Cache {
- match self {
- Self::Left(geometry) => Geometry::Left(geometry.cache()),
- Self::Right(geometry) => Geometry::Right(geometry.cache()),
+ fn cache(self, previous: Option<Self::Cache>) -> Self::Cache {
+ match (self, previous) {
+ (
+ Self::Primary(geometry),
+ Some(Geometry::Primary(previous)),
+ ) => Geometry::Primary(geometry.cache(Some(previous))),
+ (Self::Primary(geometry), None) => {
+ Geometry::Primary(geometry.cache(None))
+ }
+ (
+ Self::Secondary(geometry),
+ Some(Geometry::Secondary(previous)),
+ ) => Geometry::Secondary(geometry.cache(Some(previous))),
+ (Self::Secondary(geometry), None) => {
+ Geometry::Secondary(geometry.cache(None))
+ }
+ _ => unreachable!(),
}
}
}
- pub enum Frame<L, R> {
- Left(L),
- Right(R),
+ #[derive(Debug)]
+ pub enum Frame<A, B> {
+ Primary(A),
+ Secondary(B),
}
- impl<L, R> geometry::frame::Backend for Frame<L, R>
+ impl<A, B> geometry::frame::Backend for Frame<A, B>
where
- L: geometry::frame::Backend,
- R: geometry::frame::Backend,
+ A: geometry::frame::Backend,
+ B: geometry::frame::Backend,
{
- type Geometry = Geometry<L::Geometry, R::Geometry>;
+ type Geometry = Geometry<A::Geometry, B::Geometry>;
fn width(&self) -> f32 {
delegate!(self, frame, frame.width())
@@ -517,19 +562,19 @@ mod geometry {
delegate!(self, frame, frame.pop_transform());
}
- fn draft(&mut self, size: Size) -> Self {
+ fn draft(&mut self, bounds: Rectangle) -> Self {
match self {
- Self::Left(frame) => Self::Left(frame.draft(size)),
- Self::Right(frame) => Self::Right(frame.draft(size)),
+ Self::Primary(frame) => Self::Primary(frame.draft(bounds)),
+ Self::Secondary(frame) => Self::Secondary(frame.draft(bounds)),
}
}
fn paste(&mut self, frame: Self, at: Point) {
match (self, frame) {
- (Self::Left(target), Self::Left(source)) => {
+ (Self::Primary(target), Self::Primary(source)) => {
target.paste(source, at);
}
- (Self::Right(target), Self::Right(source)) => {
+ (Self::Secondary(target), Self::Secondary(source)) => {
target.paste(source, at);
}
_ => unreachable!(),
@@ -554,17 +599,21 @@ mod geometry {
fn into_geometry(self) -> Self::Geometry {
match self {
- Frame::Left(frame) => Geometry::Left(frame.into_geometry()),
- Frame::Right(frame) => Geometry::Right(frame.into_geometry()),
+ Frame::Primary(frame) => {
+ Geometry::Primary(frame.into_geometry())
+ }
+ Frame::Secondary(frame) => {
+ Geometry::Secondary(frame.into_geometry())
+ }
}
}
}
}
-impl<L, R> compositor::Default for Renderer<L, R>
+impl<A, B> compositor::Default for Renderer<A, B>
where
- L: compositor::Default,
- R: compositor::Default,
+ A: compositor::Default,
+ B: compositor::Default,
{
- type Compositor = Compositor<L::Compositor, R::Compositor>;
+ type Compositor = Compositor<A::Compositor, B::Compositor>;
}
diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs
index 7c48995d..056da5ed 100644
--- a/renderer/src/lib.rs
+++ b/renderer/src/lib.rs
@@ -1,5 +1,4 @@
-#![forbid(rust_2018_idioms)]
-#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)]
+//! The official renderer for iced.
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "wgpu")]
pub use iced_wgpu as wgpu;
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index 3a47a971..21503462 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[features]
debug = []
multi-window = []
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index 5c2836a5..5f054c46 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -8,13 +8,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 mod clipboard;
pub mod command;
diff --git a/src/advanced.rs b/src/advanced.rs
index 306c3559..5826ba0f 100644
--- a/src/advanced.rs
+++ b/src/advanced.rs
@@ -9,10 +9,12 @@ pub use crate::core::renderer::{self, Renderer};
pub use crate::core::svg;
pub use crate::core::text::{self, Text};
pub use crate::core::widget::{self, Widget};
-pub use crate::core::{Hasher, Shell};
+pub use crate::core::Shell;
pub use crate::renderer::graphics;
pub mod subscription {
//! Write your own subscriptions.
- pub use crate::runtime::futures::subscription::{EventStream, Recipe};
+ pub use crate::runtime::futures::subscription::{
+ EventStream, Hasher, Recipe,
+ };
}
diff --git a/src/lib.rs b/src/lib.rs
index e67b46e3..c96c28a3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -165,13 +165,6 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
-#![forbid(rust_2018_idioms, unsafe_code)]
-#![deny(
- missing_debug_implementations,
- missing_docs,
- unused_results,
- rustdoc::broken_intra_doc_links
-)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
use iced_widget::graphics;
diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml
index 68b2a03a..32ead3e0 100644
--- a/tiny_skia/Cargo.toml
+++ b/tiny_skia/Cargo.toml
@@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[features]
image = ["iced_graphics/image"]
svg = ["resvg"]
@@ -25,7 +28,6 @@ log.workspace = true
rustc-hash.workspace = true
softbuffer.workspace = true
tiny-skia.workspace = true
-xxhash-rust.workspace = true
resvg.workspace = true
resvg.optional = true
diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs
deleted file mode 100644
index 0c913c02..00000000
--- a/tiny_skia/src/backend.rs
+++ /dev/null
@@ -1,1032 +0,0 @@
-use crate::core::{
- Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
-};
-use crate::graphics::backend;
-use crate::graphics::text;
-use crate::graphics::{Damage, Viewport};
-use crate::primitive::{self, Primitive};
-use crate::window;
-
-use std::borrow::Cow;
-
-pub struct Backend {
- text_pipeline: crate::text::Pipeline,
-
- #[cfg(feature = "image")]
- raster_pipeline: crate::raster::Pipeline,
-
- #[cfg(feature = "svg")]
- vector_pipeline: crate::vector::Pipeline,
-}
-
-impl Backend {
- pub fn new() -> Self {
- Self {
- text_pipeline: crate::text::Pipeline::new(),
-
- #[cfg(feature = "image")]
- raster_pipeline: crate::raster::Pipeline::new(),
-
- #[cfg(feature = "svg")]
- vector_pipeline: crate::vector::Pipeline::new(),
- }
- }
-
- pub fn draw<T: AsRef<str>>(
- &mut self,
- pixels: &mut tiny_skia::PixmapMut<'_>,
- clip_mask: &mut tiny_skia::Mask,
- primitives: &[Primitive],
- viewport: &Viewport,
- damage: &[Rectangle],
- background_color: Color,
- overlay: &[T],
- ) {
- let physical_size = viewport.physical_size();
- let scale_factor = viewport.scale_factor() as f32;
-
- if !overlay.is_empty() {
- let path = tiny_skia::PathBuilder::from_rect(
- tiny_skia::Rect::from_xywh(
- 0.0,
- 0.0,
- physical_size.width as f32,
- physical_size.height as f32,
- )
- .expect("Create damage rectangle"),
- );
-
- pixels.fill_path(
- &path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(into_color(Color {
- a: 0.1,
- ..background_color
- })),
- anti_alias: false,
- ..Default::default()
- },
- tiny_skia::FillRule::default(),
- tiny_skia::Transform::identity(),
- None,
- );
- }
-
- for &region in damage {
- let path = tiny_skia::PathBuilder::from_rect(
- tiny_skia::Rect::from_xywh(
- region.x,
- region.y,
- region.width,
- region.height,
- )
- .expect("Create damage rectangle"),
- );
-
- pixels.fill_path(
- &path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(into_color(
- background_color,
- )),
- anti_alias: false,
- blend_mode: tiny_skia::BlendMode::Source,
- ..Default::default()
- },
- tiny_skia::FillRule::default(),
- tiny_skia::Transform::identity(),
- None,
- );
-
- adjust_clip_mask(clip_mask, region);
-
- for primitive in primitives {
- self.draw_primitive(
- primitive,
- pixels,
- clip_mask,
- region,
- scale_factor,
- Transformation::IDENTITY,
- );
- }
-
- if !overlay.is_empty() {
- pixels.stroke_path(
- &path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(into_color(
- Color::from_rgb(1.0, 0.0, 0.0),
- )),
- anti_alias: false,
- ..tiny_skia::Paint::default()
- },
- &tiny_skia::Stroke {
- width: 1.0,
- ..tiny_skia::Stroke::default()
- },
- tiny_skia::Transform::identity(),
- None,
- );
- }
- }
-
- self.text_pipeline.trim_cache();
-
- #[cfg(feature = "image")]
- self.raster_pipeline.trim_cache();
-
- #[cfg(feature = "svg")]
- self.vector_pipeline.trim_cache();
- }
-
- fn draw_primitive(
- &mut self,
- primitive: &Primitive,
- pixels: &mut tiny_skia::PixmapMut<'_>,
- clip_mask: &mut tiny_skia::Mask,
- clip_bounds: Rectangle,
- scale_factor: f32,
- transformation: Transformation,
- ) {
- match primitive {
- Primitive::Quad {
- bounds,
- background,
- border,
- shadow,
- } => {
- debug_assert!(
- bounds.width.is_normal(),
- "Quad with non-normal width!"
- );
- debug_assert!(
- bounds.height.is_normal(),
- "Quad with non-normal height!"
- );
-
- let physical_bounds = (*bounds * transformation) * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- let transform = into_transform(transformation)
- .post_scale(scale_factor, scale_factor);
-
- // Make sure the border radius is not larger than the bounds
- let border_width = border
- .width
- .min(bounds.width / 2.0)
- .min(bounds.height / 2.0);
-
- let mut fill_border_radius = <[f32; 4]>::from(border.radius);
- for radius in &mut fill_border_radius {
- *radius = (*radius)
- .min(bounds.width / 2.0)
- .min(bounds.height / 2.0);
- }
- let path = rounded_rectangle(*bounds, fill_border_radius);
-
- if shadow.color.a > 0.0 {
- let shadow_bounds = (Rectangle {
- x: bounds.x + shadow.offset.x - shadow.blur_radius,
- y: bounds.y + shadow.offset.y - shadow.blur_radius,
- width: bounds.width + shadow.blur_radius * 2.0,
- height: bounds.height + shadow.blur_radius * 2.0,
- } * transformation)
- * scale_factor;
-
- let radii = fill_border_radius
- .into_iter()
- .map(|radius| radius * scale_factor)
- .collect::<Vec<_>>();
- let (x, y, width, height) = (
- shadow_bounds.x as u32,
- shadow_bounds.y as u32,
- shadow_bounds.width as u32,
- shadow_bounds.height as u32,
- );
- let half_width = physical_bounds.width / 2.0;
- let half_height = physical_bounds.height / 2.0;
-
- let colors = (y..y + height)
- .flat_map(|y| {
- (x..x + width).map(move |x| (x as f32, y as f32))
- })
- .filter_map(|(x, y)| {
- tiny_skia::Size::from_wh(half_width, half_height)
- .map(|size| {
- let shadow_distance = rounded_box_sdf(
- Vector::new(
- x - physical_bounds.position().x
- - (shadow.offset.x
- * scale_factor)
- - half_width,
- y - physical_bounds.position().y
- - (shadow.offset.y
- * scale_factor)
- - half_height,
- ),
- size,
- &radii,
- )
- .max(0.0);
- let shadow_alpha = 1.0
- - smoothstep(
- -shadow.blur_radius * scale_factor,
- shadow.blur_radius * scale_factor,
- shadow_distance,
- );
-
- let mut color = into_color(shadow.color);
- color.apply_opacity(shadow_alpha);
-
- color.to_color_u8().premultiply()
- })
- })
- .collect();
-
- if let Some(pixmap) = tiny_skia::IntSize::from_wh(
- width, height,
- )
- .and_then(|size| {
- tiny_skia::Pixmap::from_vec(
- bytemuck::cast_vec(colors),
- size,
- )
- }) {
- pixels.draw_pixmap(
- x as i32,
- y as i32,
- pixmap.as_ref(),
- &tiny_skia::PixmapPaint::default(),
- tiny_skia::Transform::default(),
- None,
- );
- }
- }
-
- pixels.fill_path(
- &path,
- &tiny_skia::Paint {
- shader: match background {
- Background::Color(color) => {
- tiny_skia::Shader::SolidColor(into_color(
- *color,
- ))
- }
- Background::Gradient(Gradient::Linear(linear)) => {
- let (start, end) =
- linear.angle.to_distance(bounds);
-
- let stops: Vec<tiny_skia::GradientStop> =
- linear
- .stops
- .into_iter()
- .flatten()
- .map(|stop| {
- tiny_skia::GradientStop::new(
- stop.offset,
- tiny_skia::Color::from_rgba(
- stop.color.b,
- stop.color.g,
- stop.color.r,
- stop.color.a,
- )
- .expect("Create color"),
- )
- })
- .collect();
-
- tiny_skia::LinearGradient::new(
- tiny_skia::Point {
- x: start.x,
- y: start.y,
- },
- tiny_skia::Point { x: end.x, y: end.y },
- if stops.is_empty() {
- vec![tiny_skia::GradientStop::new(
- 0.0,
- tiny_skia::Color::BLACK,
- )]
- } else {
- stops
- },
- tiny_skia::SpreadMode::Pad,
- tiny_skia::Transform::identity(),
- )
- .expect("Create linear gradient")
- }
- },
- anti_alias: true,
- ..tiny_skia::Paint::default()
- },
- tiny_skia::FillRule::EvenOdd,
- transform,
- clip_mask,
- );
-
- if border_width > 0.0 {
- // Border path is offset by half the border width
- let border_bounds = Rectangle {
- x: bounds.x + border_width / 2.0,
- y: bounds.y + border_width / 2.0,
- width: bounds.width - border_width,
- height: bounds.height - border_width,
- };
-
- // Make sure the border radius is correct
- let mut border_radius = <[f32; 4]>::from(border.radius);
- let mut is_simple_border = true;
-
- for radius in &mut border_radius {
- *radius = if *radius == 0.0 {
- // Path should handle this fine
- 0.0
- } else if *radius > border_width / 2.0 {
- *radius - border_width / 2.0
- } else {
- is_simple_border = false;
- 0.0
- }
- .min(border_bounds.width / 2.0)
- .min(border_bounds.height / 2.0);
- }
-
- // Stroking a path works well in this case
- if is_simple_border {
- let border_path =
- rounded_rectangle(border_bounds, border_radius);
-
- pixels.stroke_path(
- &border_path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(
- into_color(border.color),
- ),
- anti_alias: true,
- ..tiny_skia::Paint::default()
- },
- &tiny_skia::Stroke {
- width: border_width,
- ..tiny_skia::Stroke::default()
- },
- transform,
- clip_mask,
- );
- } else {
- // Draw corners that have too small border radii as having no border radius,
- // but mask them with the rounded rectangle with the correct border radius.
- let mut temp_pixmap = tiny_skia::Pixmap::new(
- bounds.width as u32,
- bounds.height as u32,
- )
- .unwrap();
-
- let mut quad_mask = tiny_skia::Mask::new(
- bounds.width as u32,
- bounds.height as u32,
- )
- .unwrap();
-
- let zero_bounds = Rectangle {
- x: 0.0,
- y: 0.0,
- width: bounds.width,
- height: bounds.height,
- };
- let path =
- rounded_rectangle(zero_bounds, fill_border_radius);
-
- quad_mask.fill_path(
- &path,
- tiny_skia::FillRule::EvenOdd,
- true,
- transform,
- );
- let path_bounds = Rectangle {
- x: border_width / 2.0,
- y: border_width / 2.0,
- width: bounds.width - border_width,
- height: bounds.height - border_width,
- };
-
- let border_radius_path =
- rounded_rectangle(path_bounds, border_radius);
-
- temp_pixmap.stroke_path(
- &border_radius_path,
- &tiny_skia::Paint {
- shader: tiny_skia::Shader::SolidColor(
- into_color(border.color),
- ),
- anti_alias: true,
- ..tiny_skia::Paint::default()
- },
- &tiny_skia::Stroke {
- width: border_width,
- ..tiny_skia::Stroke::default()
- },
- transform,
- Some(&quad_mask),
- );
-
- pixels.draw_pixmap(
- bounds.x as i32,
- bounds.y as i32,
- temp_pixmap.as_ref(),
- &tiny_skia::PixmapPaint::default(),
- transform,
- clip_mask,
- );
- }
- }
- }
- Primitive::Paragraph {
- paragraph,
- position,
- color,
- clip_bounds: _, // TODO: Support text clip bounds
- } => {
- let physical_bounds =
- Rectangle::new(*position, paragraph.min_bounds)
- * transformation
- * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- self.text_pipeline.draw_paragraph(
- paragraph,
- *position,
- *color,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- Primitive::Editor {
- editor,
- position,
- color,
- clip_bounds: _, // TODO: Support text clip bounds
- } => {
- let physical_bounds = Rectangle::new(*position, editor.bounds)
- * transformation
- * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- self.text_pipeline.draw_editor(
- editor,
- *position,
- *color,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- Primitive::Text {
- content,
- bounds,
- color,
- size,
- line_height,
- font,
- horizontal_alignment,
- vertical_alignment,
- shaping,
- clip_bounds: _, // TODO: Support text clip bounds
- } => {
- let physical_bounds =
- primitive.bounds() * transformation * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- self.text_pipeline.draw_cached(
- content,
- *bounds,
- *color,
- *size,
- *line_height,
- *font,
- *horizontal_alignment,
- *vertical_alignment,
- *shaping,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- Primitive::RawText(text::Raw {
- buffer,
- position,
- color,
- clip_bounds: _, // TODO: Support text clip bounds
- }) => {
- let Some(buffer) = buffer.upgrade() else {
- return;
- };
-
- let (width, height) = buffer.size();
-
- let physical_bounds =
- Rectangle::new(*position, Size::new(width, height))
- * transformation
- * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- self.text_pipeline.draw_raw(
- &buffer,
- *position,
- *color,
- scale_factor,
- pixels,
- clip_mask,
- transformation,
- );
- }
- #[cfg(feature = "image")]
- Primitive::Image {
- handle,
- filter_method,
- bounds,
- } => {
- let physical_bounds = (*bounds * transformation) * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- let transform = into_transform(transformation)
- .post_scale(scale_factor, scale_factor);
-
- self.raster_pipeline.draw(
- handle,
- *filter_method,
- *bounds,
- pixels,
- transform,
- clip_mask,
- );
- }
- #[cfg(not(feature = "image"))]
- Primitive::Image { .. } => {
- log::warn!(
- "Unsupported primitive in `iced_tiny_skia`: {primitive:?}",
- );
- }
- #[cfg(feature = "svg")]
- Primitive::Svg {
- handle,
- bounds,
- color,
- } => {
- let physical_bounds = (*bounds * transformation) * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- self.vector_pipeline.draw(
- handle,
- *color,
- (*bounds * transformation) * scale_factor,
- pixels,
- clip_mask,
- );
- }
- #[cfg(not(feature = "svg"))]
- Primitive::Svg { .. } => {
- log::warn!(
- "Unsupported primitive in `iced_tiny_skia`: {primitive:?}",
- );
- }
- Primitive::Custom(primitive::Custom::Fill {
- path,
- paint,
- rule,
- }) => {
- let bounds = path.bounds();
-
- let physical_bounds = (Rectangle {
- x: bounds.x(),
- y: bounds.y(),
- width: bounds.width(),
- height: bounds.height(),
- } * transformation)
- * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- pixels.fill_path(
- path,
- paint,
- *rule,
- into_transform(transformation)
- .post_scale(scale_factor, scale_factor),
- clip_mask,
- );
- }
- Primitive::Custom(primitive::Custom::Stroke {
- path,
- paint,
- stroke,
- }) => {
- let bounds = path.bounds();
-
- let physical_bounds = (Rectangle {
- x: bounds.x(),
- y: bounds.y(),
- width: bounds.width().max(1.0),
- height: bounds.height().max(1.0),
- } * transformation)
- * scale_factor;
-
- if !clip_bounds.intersects(&physical_bounds) {
- return;
- }
-
- let clip_mask = (!physical_bounds.is_within(&clip_bounds))
- .then_some(clip_mask as &_);
-
- pixels.stroke_path(
- path,
- paint,
- stroke,
- into_transform(transformation)
- .post_scale(scale_factor, scale_factor),
- clip_mask,
- );
- }
- Primitive::Group { primitives } => {
- for primitive in primitives {
- self.draw_primitive(
- primitive,
- pixels,
- clip_mask,
- clip_bounds,
- scale_factor,
- transformation,
- );
- }
- }
- Primitive::Transform {
- transformation: new_transformation,
- content,
- } => {
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- clip_bounds,
- scale_factor,
- transformation * *new_transformation,
- );
- }
- Primitive::Clip { bounds, content } => {
- let bounds = (*bounds * transformation) * scale_factor;
-
- if bounds == clip_bounds {
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- bounds,
- scale_factor,
- transformation,
- );
- } else if let Some(bounds) = clip_bounds.intersection(&bounds) {
- if bounds.x + bounds.width <= 0.0
- || bounds.y + bounds.height <= 0.0
- || bounds.x as u32 >= pixels.width()
- || bounds.y as u32 >= pixels.height()
- || bounds.width <= 1.0
- || bounds.height <= 1.0
- {
- return;
- }
-
- adjust_clip_mask(clip_mask, bounds);
-
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- bounds,
- scale_factor,
- transformation,
- );
-
- adjust_clip_mask(clip_mask, clip_bounds);
- }
- }
- Primitive::Cache { content } => {
- self.draw_primitive(
- content,
- pixels,
- clip_mask,
- clip_bounds,
- scale_factor,
- transformation,
- );
- }
- }
- }
-}
-
-impl Default for Backend {
- fn default() -> Self {
- Self::new()
- }
-}
-
-fn into_color(color: Color) -> tiny_skia::Color {
- tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
- .expect("Convert color from iced to tiny_skia")
-}
-
-fn into_transform(transformation: Transformation) -> tiny_skia::Transform {
- let translation = transformation.translation();
-
- tiny_skia::Transform {
- sx: transformation.scale_factor(),
- kx: 0.0,
- ky: 0.0,
- sy: transformation.scale_factor(),
- tx: translation.x,
- ty: translation.y,
- }
-}
-
-fn rounded_rectangle(
- bounds: Rectangle,
- border_radius: [f32; 4],
-) -> tiny_skia::Path {
- let [top_left, top_right, bottom_right, bottom_left] = border_radius;
-
- if top_left == 0.0
- && top_right == 0.0
- && bottom_right == 0.0
- && bottom_left == 0.0
- {
- return tiny_skia::PathBuilder::from_rect(
- tiny_skia::Rect::from_xywh(
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
- )
- .expect("Build quad rectangle"),
- );
- }
-
- if top_left == top_right
- && top_left == bottom_right
- && top_left == bottom_left
- && top_left == bounds.width / 2.0
- && top_left == bounds.height / 2.0
- {
- return tiny_skia::PathBuilder::from_circle(
- bounds.x + bounds.width / 2.0,
- bounds.y + bounds.height / 2.0,
- top_left,
- )
- .expect("Build circle path");
- }
-
- let mut builder = tiny_skia::PathBuilder::new();
-
- builder.move_to(bounds.x + top_left, bounds.y);
- builder.line_to(bounds.x + bounds.width - top_right, bounds.y);
-
- if top_right > 0.0 {
- arc_to(
- &mut builder,
- bounds.x + bounds.width - top_right,
- bounds.y,
- bounds.x + bounds.width,
- bounds.y + top_right,
- top_right,
- );
- }
-
- maybe_line_to(
- &mut builder,
- bounds.x + bounds.width,
- bounds.y + bounds.height - bottom_right,
- );
-
- if bottom_right > 0.0 {
- arc_to(
- &mut builder,
- bounds.x + bounds.width,
- bounds.y + bounds.height - bottom_right,
- bounds.x + bounds.width - bottom_right,
- bounds.y + bounds.height,
- bottom_right,
- );
- }
-
- maybe_line_to(
- &mut builder,
- bounds.x + bottom_left,
- bounds.y + bounds.height,
- );
-
- if bottom_left > 0.0 {
- arc_to(
- &mut builder,
- bounds.x + bottom_left,
- bounds.y + bounds.height,
- bounds.x,
- bounds.y + bounds.height - bottom_left,
- bottom_left,
- );
- }
-
- maybe_line_to(&mut builder, bounds.x, bounds.y + top_left);
-
- if top_left > 0.0 {
- arc_to(
- &mut builder,
- bounds.x,
- bounds.y + top_left,
- bounds.x + top_left,
- bounds.y,
- top_left,
- );
- }
-
- builder.finish().expect("Build rounded rectangle path")
-}
-
-fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) {
- if path.last_point() != Some(tiny_skia::Point { x, y }) {
- path.line_to(x, y);
- }
-}
-
-fn arc_to(
- path: &mut tiny_skia::PathBuilder,
- x_from: f32,
- y_from: f32,
- x_to: f32,
- y_to: f32,
- radius: f32,
-) {
- let svg_arc = kurbo::SvgArc {
- from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)),
- to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)),
- radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)),
- x_rotation: 0.0,
- large_arc: false,
- sweep: true,
- };
-
- match kurbo::Arc::from_svg_arc(&svg_arc) {
- Some(arc) => {
- arc.to_cubic_beziers(0.1, |p1, p2, p| {
- path.cubic_to(
- p1.x as f32,
- p1.y as f32,
- p2.x as f32,
- p2.y as f32,
- p.x as f32,
- p.y as f32,
- );
- });
- }
- None => {
- path.line_to(x_to, y_to);
- }
- }
-}
-
-fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) {
- clip_mask.clear();
-
- let path = {
- let mut builder = tiny_skia::PathBuilder::new();
- builder.push_rect(
- tiny_skia::Rect::from_xywh(
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
- )
- .unwrap(),
- );
-
- builder.finish().unwrap()
- };
-
- clip_mask.fill_path(
- &path,
- tiny_skia::FillRule::EvenOdd,
- false,
- tiny_skia::Transform::default(),
- );
-}
-
-fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
- let x = ((x - a) / (b - a)).clamp(0.0, 1.0);
-
- x * x * (3.0 - 2.0 * x)
-}
-
-fn rounded_box_sdf(
- to_center: Vector,
- size: tiny_skia::Size,
- radii: &[f32],
-) -> f32 {
- let radius = match (to_center.x > 0.0, to_center.y > 0.0) {
- (true, true) => radii[2],
- (true, false) => radii[1],
- (false, true) => radii[3],
- (false, false) => radii[0],
- };
-
- let x = (to_center.x.abs() - size.width() + radius).max(0.0);
- let y = (to_center.y.abs() - size.height() + radius).max(0.0);
-
- (x.powf(2.0) + y.powf(2.0)).sqrt() - radius
-}
-
-impl backend::Backend for Backend {
- type Primitive = primitive::Custom;
- type Compositor = window::Compositor;
-}
-
-impl backend::Text for Backend {
- fn load_font(&mut self, font: Cow<'static, [u8]>) {
- self.text_pipeline.load_font(font);
- }
-}
-
-#[cfg(feature = "image")]
-impl backend::Image for Backend {
- fn dimensions(
- &self,
- handle: &crate::core::image::Handle,
- ) -> crate::core::Size<u32> {
- self.raster_pipeline.dimensions(handle)
- }
-}
-
-#[cfg(feature = "svg")]
-impl backend::Svg for Backend {
- fn viewport_dimensions(
- &self,
- handle: &crate::core::svg::Handle,
- ) -> crate::core::Size<u32> {
- self.vector_pipeline.viewport_dimensions(handle)
- }
-}
-
-#[cfg(feature = "geometry")]
-impl crate::graphics::geometry::Backend for Backend {
- type Frame = crate::geometry::Frame;
-
- fn new_frame(&self, size: Size) -> Self::Frame {
- crate::geometry::Frame::new(size)
- }
-}
diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs
new file mode 100644
index 00000000..fbca1274
--- /dev/null
+++ b/tiny_skia/src/engine.rs
@@ -0,0 +1,831 @@
+use crate::core::renderer::Quad;
+use crate::core::{
+ Background, Color, Gradient, Rectangle, Size, Transformation, Vector,
+};
+use crate::graphics::{Image, Text};
+use crate::text;
+use crate::Primitive;
+
+#[derive(Debug)]
+pub struct Engine {
+ text_pipeline: text::Pipeline,
+
+ #[cfg(feature = "image")]
+ pub(crate) raster_pipeline: crate::raster::Pipeline,
+ #[cfg(feature = "svg")]
+ pub(crate) vector_pipeline: crate::vector::Pipeline,
+}
+
+impl Engine {
+ pub fn new() -> Self {
+ Self {
+ text_pipeline: text::Pipeline::new(),
+ #[cfg(feature = "image")]
+ raster_pipeline: crate::raster::Pipeline::new(),
+ #[cfg(feature = "svg")]
+ vector_pipeline: crate::vector::Pipeline::new(),
+ }
+ }
+
+ pub fn draw_quad(
+ &mut self,
+ quad: &Quad,
+ background: &Background,
+ transformation: Transformation,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ clip_bounds: Rectangle,
+ ) {
+ debug_assert!(
+ quad.bounds.width.is_normal(),
+ "Quad with non-normal width!"
+ );
+ debug_assert!(
+ quad.bounds.height.is_normal(),
+ "Quad with non-normal height!"
+ );
+
+ let physical_bounds = quad.bounds * transformation;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ let transform = into_transform(transformation);
+
+ // Make sure the border radius is not larger than the bounds
+ let border_width = quad
+ .border
+ .width
+ .min(quad.bounds.width / 2.0)
+ .min(quad.bounds.height / 2.0);
+
+ let mut fill_border_radius = <[f32; 4]>::from(quad.border.radius);
+
+ for radius in &mut fill_border_radius {
+ *radius = (*radius)
+ .min(quad.bounds.width / 2.0)
+ .min(quad.bounds.height / 2.0);
+ }
+
+ let path = rounded_rectangle(quad.bounds, fill_border_radius);
+
+ let shadow = quad.shadow;
+
+ if shadow.color.a > 0.0 {
+ let shadow_bounds = Rectangle {
+ x: quad.bounds.x + shadow.offset.x - shadow.blur_radius,
+ y: quad.bounds.y + shadow.offset.y - shadow.blur_radius,
+ width: quad.bounds.width + shadow.blur_radius * 2.0,
+ height: quad.bounds.height + shadow.blur_radius * 2.0,
+ } * transformation;
+
+ let radii = fill_border_radius
+ .into_iter()
+ .map(|radius| radius * transformation.scale_factor())
+ .collect::<Vec<_>>();
+ let (x, y, width, height) = (
+ shadow_bounds.x as u32,
+ shadow_bounds.y as u32,
+ shadow_bounds.width as u32,
+ shadow_bounds.height as u32,
+ );
+ let half_width = physical_bounds.width / 2.0;
+ let half_height = physical_bounds.height / 2.0;
+
+ let colors = (y..y + height)
+ .flat_map(|y| (x..x + width).map(move |x| (x as f32, y as f32)))
+ .filter_map(|(x, y)| {
+ tiny_skia::Size::from_wh(half_width, half_height).map(
+ |size| {
+ let shadow_distance = rounded_box_sdf(
+ Vector::new(
+ x - physical_bounds.position().x
+ - (shadow.offset.x
+ * transformation.scale_factor())
+ - half_width,
+ y - physical_bounds.position().y
+ - (shadow.offset.y
+ * transformation.scale_factor())
+ - half_height,
+ ),
+ size,
+ &radii,
+ )
+ .max(0.0);
+ let shadow_alpha = 1.0
+ - smoothstep(
+ -shadow.blur_radius
+ * transformation.scale_factor(),
+ shadow.blur_radius
+ * transformation.scale_factor(),
+ shadow_distance,
+ );
+
+ let mut color = into_color(shadow.color);
+ color.apply_opacity(shadow_alpha);
+
+ color.to_color_u8().premultiply()
+ },
+ )
+ })
+ .collect();
+
+ if let Some(pixmap) = tiny_skia::IntSize::from_wh(width, height)
+ .and_then(|size| {
+ tiny_skia::Pixmap::from_vec(
+ bytemuck::cast_vec(colors),
+ size,
+ )
+ })
+ {
+ pixels.draw_pixmap(
+ x as i32,
+ y as i32,
+ pixmap.as_ref(),
+ &tiny_skia::PixmapPaint::default(),
+ tiny_skia::Transform::default(),
+ None,
+ );
+ }
+ }
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: match background {
+ Background::Color(color) => {
+ tiny_skia::Shader::SolidColor(into_color(*color))
+ }
+ Background::Gradient(Gradient::Linear(linear)) => {
+ let (start, end) =
+ linear.angle.to_distance(&quad.bounds);
+
+ let stops: Vec<tiny_skia::GradientStop> = linear
+ .stops
+ .into_iter()
+ .flatten()
+ .map(|stop| {
+ tiny_skia::GradientStop::new(
+ stop.offset,
+ tiny_skia::Color::from_rgba(
+ stop.color.b,
+ stop.color.g,
+ stop.color.r,
+ stop.color.a,
+ )
+ .expect("Create color"),
+ )
+ })
+ .collect();
+
+ tiny_skia::LinearGradient::new(
+ tiny_skia::Point {
+ x: start.x,
+ y: start.y,
+ },
+ tiny_skia::Point { x: end.x, y: end.y },
+ if stops.is_empty() {
+ vec![tiny_skia::GradientStop::new(
+ 0.0,
+ tiny_skia::Color::BLACK,
+ )]
+ } else {
+ stops
+ },
+ tiny_skia::SpreadMode::Pad,
+ tiny_skia::Transform::identity(),
+ )
+ .expect("Create linear gradient")
+ }
+ },
+ anti_alias: true,
+ ..tiny_skia::Paint::default()
+ },
+ tiny_skia::FillRule::EvenOdd,
+ transform,
+ clip_mask,
+ );
+
+ if border_width > 0.0 {
+ // Border path is offset by half the border width
+ let border_bounds = Rectangle {
+ x: quad.bounds.x + border_width / 2.0,
+ y: quad.bounds.y + border_width / 2.0,
+ width: quad.bounds.width - border_width,
+ height: quad.bounds.height - border_width,
+ };
+
+ // Make sure the border radius is correct
+ let mut border_radius = <[f32; 4]>::from(quad.border.radius);
+ let mut is_simple_border = true;
+
+ for radius in &mut border_radius {
+ *radius = if *radius == 0.0 {
+ // Path should handle this fine
+ 0.0
+ } else if *radius > border_width / 2.0 {
+ *radius - border_width / 2.0
+ } else {
+ is_simple_border = false;
+ 0.0
+ }
+ .min(border_bounds.width / 2.0)
+ .min(border_bounds.height / 2.0);
+ }
+
+ // Stroking a path works well in this case
+ if is_simple_border {
+ let border_path =
+ rounded_rectangle(border_bounds, border_radius);
+
+ pixels.stroke_path(
+ &border_path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(into_color(
+ quad.border.color,
+ )),
+ anti_alias: true,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: border_width,
+ ..tiny_skia::Stroke::default()
+ },
+ transform,
+ clip_mask,
+ );
+ } else {
+ // Draw corners that have too small border radii as having no border radius,
+ // but mask them with the rounded rectangle with the correct border radius.
+ let mut temp_pixmap = tiny_skia::Pixmap::new(
+ quad.bounds.width as u32,
+ quad.bounds.height as u32,
+ )
+ .unwrap();
+
+ let mut quad_mask = tiny_skia::Mask::new(
+ quad.bounds.width as u32,
+ quad.bounds.height as u32,
+ )
+ .unwrap();
+
+ let zero_bounds = Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: quad.bounds.width,
+ height: quad.bounds.height,
+ };
+ let path = rounded_rectangle(zero_bounds, fill_border_radius);
+
+ quad_mask.fill_path(
+ &path,
+ tiny_skia::FillRule::EvenOdd,
+ true,
+ transform,
+ );
+ let path_bounds = Rectangle {
+ x: border_width / 2.0,
+ y: border_width / 2.0,
+ width: quad.bounds.width - border_width,
+ height: quad.bounds.height - border_width,
+ };
+
+ let border_radius_path =
+ rounded_rectangle(path_bounds, border_radius);
+
+ temp_pixmap.stroke_path(
+ &border_radius_path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(into_color(
+ quad.border.color,
+ )),
+ anti_alias: true,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: border_width,
+ ..tiny_skia::Stroke::default()
+ },
+ transform,
+ Some(&quad_mask),
+ );
+
+ pixels.draw_pixmap(
+ quad.bounds.x as i32,
+ quad.bounds.y as i32,
+ temp_pixmap.as_ref(),
+ &tiny_skia::PixmapPaint::default(),
+ transform,
+ clip_mask,
+ );
+ }
+ }
+ }
+
+ pub fn draw_text(
+ &mut self,
+ text: &Text,
+ transformation: Transformation,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ clip_bounds: Rectangle,
+ ) {
+ match text {
+ Text::Paragraph {
+ paragraph,
+ position,
+ color,
+ clip_bounds: _, // TODO
+ transformation: local_transformation,
+ } => {
+ let transformation = transformation * *local_transformation;
+
+ let physical_bounds =
+ Rectangle::new(*position, paragraph.min_bounds)
+ * transformation;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_paragraph(
+ paragraph,
+ *position,
+ *color,
+ pixels,
+ clip_mask,
+ transformation,
+ );
+ }
+ Text::Editor {
+ editor,
+ position,
+ color,
+ clip_bounds: _, // TODO
+ transformation: local_transformation,
+ } => {
+ let transformation = transformation * *local_transformation;
+
+ let physical_bounds =
+ Rectangle::new(*position, editor.bounds) * transformation;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_editor(
+ editor,
+ *position,
+ *color,
+ pixels,
+ clip_mask,
+ transformation,
+ );
+ }
+ Text::Cached {
+ content,
+ bounds,
+ color,
+ size,
+ line_height,
+ font,
+ horizontal_alignment,
+ vertical_alignment,
+ shaping,
+ clip_bounds: text_bounds, // TODO
+ } => {
+ let physical_bounds = *text_bounds * transformation;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_cached(
+ content,
+ *bounds,
+ *color,
+ *size,
+ *line_height,
+ *font,
+ *horizontal_alignment,
+ *vertical_alignment,
+ *shaping,
+ pixels,
+ clip_mask,
+ transformation,
+ );
+ }
+ Text::Raw {
+ raw,
+ transformation: local_transformation,
+ } => {
+ let Some(buffer) = raw.buffer.upgrade() else {
+ return;
+ };
+
+ let transformation = transformation * *local_transformation;
+ let (width, height) = buffer.size();
+
+ let physical_bounds =
+ Rectangle::new(raw.position, Size::new(width, height))
+ * transformation;
+
+ if !clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&clip_bounds))
+ .then_some(clip_mask as &_);
+
+ self.text_pipeline.draw_raw(
+ &buffer,
+ raw.position,
+ raw.color,
+ pixels,
+ clip_mask,
+ transformation,
+ );
+ }
+ }
+ }
+
+ pub fn draw_primitive(
+ &mut self,
+ primitive: &Primitive,
+ transformation: Transformation,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ layer_bounds: Rectangle,
+ ) {
+ match primitive {
+ Primitive::Fill { path, paint, rule } => {
+ let physical_bounds = {
+ let bounds = path.bounds();
+
+ Rectangle {
+ x: bounds.x(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
+ } * transformation
+ };
+
+ let Some(clip_bounds) =
+ layer_bounds.intersection(&physical_bounds)
+ else {
+ return;
+ };
+
+ let clip_mask =
+ (physical_bounds != clip_bounds).then_some(clip_mask as &_);
+
+ pixels.fill_path(
+ path,
+ paint,
+ *rule,
+ into_transform(transformation),
+ clip_mask,
+ );
+ }
+ Primitive::Stroke {
+ path,
+ paint,
+ stroke,
+ } => {
+ let physical_bounds = {
+ let bounds = path.bounds();
+
+ Rectangle {
+ x: bounds.x(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
+ } * transformation
+ };
+
+ let Some(clip_bounds) =
+ layer_bounds.intersection(&physical_bounds)
+ else {
+ return;
+ };
+
+ let clip_mask =
+ (physical_bounds != clip_bounds).then_some(clip_mask as &_);
+
+ pixels.stroke_path(
+ path,
+ paint,
+ stroke,
+ into_transform(transformation),
+ clip_mask,
+ );
+ }
+ }
+ }
+
+ pub fn draw_image(
+ &mut self,
+ image: &Image,
+ _transformation: Transformation,
+ _pixels: &mut tiny_skia::PixmapMut<'_>,
+ _clip_mask: &mut tiny_skia::Mask,
+ _clip_bounds: Rectangle,
+ ) {
+ match image {
+ #[cfg(feature = "image")]
+ Image::Raster {
+ handle,
+ filter_method,
+ bounds,
+ } => {
+ let physical_bounds = *bounds * _transformation;
+
+ if !_clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
+ .then_some(_clip_mask as &_);
+
+ self.raster_pipeline.draw(
+ handle,
+ *filter_method,
+ *bounds,
+ _pixels,
+ into_transform(_transformation),
+ clip_mask,
+ );
+ }
+ #[cfg(feature = "svg")]
+ Image::Vector {
+ handle,
+ color,
+ bounds,
+ } => {
+ let physical_bounds = *bounds * _transformation;
+
+ if !_clip_bounds.intersects(&physical_bounds) {
+ return;
+ }
+
+ let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
+ .then_some(_clip_mask as &_);
+
+ self.vector_pipeline.draw(
+ handle,
+ *color,
+ physical_bounds,
+ _pixels,
+ clip_mask,
+ );
+ }
+ #[cfg(not(feature = "image"))]
+ Image::Raster { .. } => {
+ log::warn!(
+ "Unsupported primitive in `iced_tiny_skia`: {image:?}",
+ );
+ }
+ #[cfg(not(feature = "svg"))]
+ Image::Vector { .. } => {
+ log::warn!(
+ "Unsupported primitive in `iced_tiny_skia`: {image:?}",
+ );
+ }
+ }
+ }
+
+ pub fn trim(&mut self) {
+ self.text_pipeline.trim_cache();
+
+ #[cfg(feature = "image")]
+ self.raster_pipeline.trim_cache();
+
+ #[cfg(feature = "svg")]
+ self.vector_pipeline.trim_cache();
+ }
+}
+
+pub fn into_color(color: Color) -> tiny_skia::Color {
+ tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a)
+ .expect("Convert color from iced to tiny_skia")
+}
+
+fn into_transform(transformation: Transformation) -> tiny_skia::Transform {
+ let translation = transformation.translation();
+
+ tiny_skia::Transform {
+ sx: transformation.scale_factor(),
+ kx: 0.0,
+ ky: 0.0,
+ sy: transformation.scale_factor(),
+ tx: translation.x,
+ ty: translation.y,
+ }
+}
+
+fn rounded_rectangle(
+ bounds: Rectangle,
+ border_radius: [f32; 4],
+) -> tiny_skia::Path {
+ let [top_left, top_right, bottom_right, bottom_left] = border_radius;
+
+ if top_left == 0.0
+ && top_right == 0.0
+ && bottom_right == 0.0
+ && bottom_left == 0.0
+ {
+ return tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ )
+ .expect("Build quad rectangle"),
+ );
+ }
+
+ if top_left == top_right
+ && top_left == bottom_right
+ && top_left == bottom_left
+ && top_left == bounds.width / 2.0
+ && top_left == bounds.height / 2.0
+ {
+ return tiny_skia::PathBuilder::from_circle(
+ bounds.x + bounds.width / 2.0,
+ bounds.y + bounds.height / 2.0,
+ top_left,
+ )
+ .expect("Build circle path");
+ }
+
+ let mut builder = tiny_skia::PathBuilder::new();
+
+ builder.move_to(bounds.x + top_left, bounds.y);
+ builder.line_to(bounds.x + bounds.width - top_right, bounds.y);
+
+ if top_right > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x + bounds.width - top_right,
+ bounds.y,
+ bounds.x + bounds.width,
+ bounds.y + top_right,
+ top_right,
+ );
+ }
+
+ maybe_line_to(
+ &mut builder,
+ bounds.x + bounds.width,
+ bounds.y + bounds.height - bottom_right,
+ );
+
+ if bottom_right > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x + bounds.width,
+ bounds.y + bounds.height - bottom_right,
+ bounds.x + bounds.width - bottom_right,
+ bounds.y + bounds.height,
+ bottom_right,
+ );
+ }
+
+ maybe_line_to(
+ &mut builder,
+ bounds.x + bottom_left,
+ bounds.y + bounds.height,
+ );
+
+ if bottom_left > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x + bottom_left,
+ bounds.y + bounds.height,
+ bounds.x,
+ bounds.y + bounds.height - bottom_left,
+ bottom_left,
+ );
+ }
+
+ maybe_line_to(&mut builder, bounds.x, bounds.y + top_left);
+
+ if top_left > 0.0 {
+ arc_to(
+ &mut builder,
+ bounds.x,
+ bounds.y + top_left,
+ bounds.x + top_left,
+ bounds.y,
+ top_left,
+ );
+ }
+
+ builder.finish().expect("Build rounded rectangle path")
+}
+
+fn maybe_line_to(path: &mut tiny_skia::PathBuilder, x: f32, y: f32) {
+ if path.last_point() != Some(tiny_skia::Point { x, y }) {
+ path.line_to(x, y);
+ }
+}
+
+fn arc_to(
+ path: &mut tiny_skia::PathBuilder,
+ x_from: f32,
+ y_from: f32,
+ x_to: f32,
+ y_to: f32,
+ radius: f32,
+) {
+ let svg_arc = kurbo::SvgArc {
+ from: kurbo::Point::new(f64::from(x_from), f64::from(y_from)),
+ to: kurbo::Point::new(f64::from(x_to), f64::from(y_to)),
+ radii: kurbo::Vec2::new(f64::from(radius), f64::from(radius)),
+ x_rotation: 0.0,
+ large_arc: false,
+ sweep: true,
+ };
+
+ match kurbo::Arc::from_svg_arc(&svg_arc) {
+ Some(arc) => {
+ arc.to_cubic_beziers(0.1, |p1, p2, p| {
+ path.cubic_to(
+ p1.x as f32,
+ p1.y as f32,
+ p2.x as f32,
+ p2.y as f32,
+ p.x as f32,
+ p.y as f32,
+ );
+ });
+ }
+ None => {
+ path.line_to(x_to, y_to);
+ }
+ }
+}
+
+fn smoothstep(a: f32, b: f32, x: f32) -> f32 {
+ let x = ((x - a) / (b - a)).clamp(0.0, 1.0);
+
+ x * x * (3.0 - 2.0 * x)
+}
+
+fn rounded_box_sdf(
+ to_center: Vector,
+ size: tiny_skia::Size,
+ radii: &[f32],
+) -> f32 {
+ let radius = match (to_center.x > 0.0, to_center.y > 0.0) {
+ (true, true) => radii[2],
+ (true, false) => radii[1],
+ (false, true) => radii[3],
+ (false, false) => radii[0],
+ };
+
+ let x = (to_center.x.abs() - size.width() + radius).max(0.0);
+ let y = (to_center.y.abs() - size.height() + radius).max(0.0);
+
+ (x.powf(2.0) + y.powf(2.0)).sqrt() - radius
+}
+
+pub fn adjust_clip_mask(clip_mask: &mut tiny_skia::Mask, bounds: Rectangle) {
+ clip_mask.clear();
+
+ let path = {
+ let mut builder = tiny_skia::PathBuilder::new();
+ builder.push_rect(
+ tiny_skia::Rect::from_xywh(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ )
+ .unwrap(),
+ );
+
+ builder.finish().unwrap()
+ };
+
+ clip_mask.fill_path(
+ &path,
+ tiny_skia::FillRule::EvenOdd,
+ false,
+ tiny_skia::Transform::default(),
+ );
+}
diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs
index 76482e12..117daf41 100644
--- a/tiny_skia/src/geometry.rs
+++ b/tiny_skia/src/geometry.rs
@@ -1,57 +1,98 @@
use crate::core::text::LineHeight;
-use crate::core::{
- Pixels, Point, Radians, Rectangle, Size, Transformation, Vector,
-};
+use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector};
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::stroke::{self, Stroke};
-use crate::graphics::geometry::{self, Path, Style, Text};
-use crate::graphics::Gradient;
-use crate::primitive::{self, Primitive};
+use crate::graphics::geometry::{self, Path, Style};
+use crate::graphics::{Cached, Gradient, Text};
+use crate::Primitive;
+
+use std::rc::Rc;
+
+#[derive(Debug)]
+pub enum Geometry {
+ Live {
+ text: Vec<Text>,
+ primitives: Vec<Primitive>,
+ clip_bounds: Rectangle,
+ },
+ Cache(Cache),
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+ pub text: Rc<[Text]>,
+ pub primitives: Rc<[Primitive]>,
+ pub clip_bounds: Rectangle,
+}
+
+impl Cached for Geometry {
+ type Cache = Cache;
+
+ fn load(cache: &Cache) -> Self {
+ Self::Cache(cache.clone())
+ }
+
+ fn cache(self, _previous: Option<Cache>) -> Cache {
+ match self {
+ Self::Live {
+ primitives,
+ text,
+ clip_bounds,
+ } => Cache {
+ primitives: Rc::from(primitives),
+ text: Rc::from(text),
+ clip_bounds,
+ },
+ Self::Cache(cache) => cache,
+ }
+ }
+}
+#[derive(Debug)]
pub struct Frame {
- size: Size,
+ clip_bounds: Rectangle,
transform: tiny_skia::Transform,
stack: Vec<tiny_skia::Transform>,
primitives: Vec<Primitive>,
+ text: Vec<Text>,
}
impl Frame {
pub fn new(size: Size) -> Self {
+ Self::with_clip(Rectangle::with_size(size))
+ }
+
+ pub fn with_clip(clip_bounds: Rectangle) -> Self {
Self {
- size,
- transform: tiny_skia::Transform::identity(),
+ clip_bounds,
stack: Vec::new(),
primitives: Vec::new(),
- }
- }
-
- pub fn into_primitive(self) -> Primitive {
- Primitive::Clip {
- bounds: Rectangle::new(Point::ORIGIN, self.size),
- content: Box::new(Primitive::Group {
- primitives: self.primitives,
- }),
+ text: Vec::new(),
+ transform: tiny_skia::Transform::from_translate(
+ clip_bounds.x,
+ clip_bounds.y,
+ ),
}
}
}
impl geometry::frame::Backend for Frame {
- type Geometry = Primitive;
+ type Geometry = Geometry;
fn width(&self) -> f32 {
- self.size.width
+ self.clip_bounds.width
}
fn height(&self) -> f32 {
- self.size.height
+ self.clip_bounds.height
}
fn size(&self) -> Size {
- self.size
+ self.clip_bounds.size()
}
fn center(&self) -> Point {
- Point::new(self.size.width / 2.0, self.size.height / 2.0)
+ Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0)
}
fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
@@ -66,12 +107,11 @@ impl geometry::frame::Backend for Frame {
let mut paint = into_paint(fill.style);
paint.shader.transform(self.transform);
- self.primitives
- .push(Primitive::Custom(primitive::Custom::Fill {
- path,
- paint,
- rule: into_fill_rule(fill.rule),
- }));
+ self.primitives.push(Primitive::Fill {
+ path,
+ paint,
+ rule: into_fill_rule(fill.rule),
+ });
}
fn fill_rectangle(
@@ -94,12 +134,11 @@ impl geometry::frame::Backend for Frame {
};
paint.shader.transform(self.transform);
- self.primitives
- .push(Primitive::Custom(primitive::Custom::Fill {
- path,
- paint,
- rule: into_fill_rule(fill.rule),
- }));
+ self.primitives.push(Primitive::Fill {
+ path,
+ paint,
+ rule: into_fill_rule(fill.rule),
+ });
}
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
@@ -115,20 +154,19 @@ impl geometry::frame::Backend for Frame {
let mut paint = into_paint(stroke.style);
paint.shader.transform(self.transform);
- self.primitives
- .push(Primitive::Custom(primitive::Custom::Stroke {
- path,
- paint,
- stroke: skia_stroke,
- }));
+ self.primitives.push(Primitive::Stroke {
+ path,
+ paint,
+ stroke: skia_stroke,
+ });
}
- fn fill_text(&mut self, text: impl Into<Text>) {
+ fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transform.get_scale();
- if self.transform.is_scale_translate()
+ if !self.transform.has_skew()
&& scale_x == scale_y
&& scale_x > 0.0
&& scale_y > 0.0
@@ -170,12 +208,12 @@ impl geometry::frame::Backend for Frame {
};
// TODO: Honor layering!
- self.primitives.push(Primitive::Text {
+ self.text.push(Text::Cached {
content: text.content,
bounds,
color: text.color,
size,
- line_height,
+ line_height: line_height.to_absolute(size),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
@@ -195,15 +233,13 @@ impl geometry::frame::Backend for Frame {
self.transform = self.stack.pop().expect("Pop transform");
}
- fn draft(&mut self, size: Size) -> Self {
- Self::new(size)
+ fn draft(&mut self, clip_bounds: Rectangle) -> Self {
+ Self::with_clip(clip_bounds)
}
- fn paste(&mut self, frame: Self, at: Point) {
- self.primitives.push(Primitive::Transform {
- transformation: Transformation::translate(at.x, at.y),
- content: Box::new(frame.into_primitive()),
- });
+ fn paste(&mut self, frame: Self, _at: Point) {
+ self.primitives.extend(frame.primitives);
+ self.text.extend(frame.text);
}
fn translate(&mut self, translation: Vector) {
@@ -229,8 +265,12 @@ impl geometry::frame::Backend for Frame {
self.transform = self.transform.pre_scale(scale.x, scale.y);
}
- fn into_geometry(self) -> Self::Geometry {
- self.into_primitive()
+ fn into_geometry(self) -> Geometry {
+ Geometry::Live {
+ primitives: self.primitives,
+ text: self.text,
+ clip_bounds: self.clip_bounds,
+ }
}
}
diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs
new file mode 100644
index 00000000..3e42e4aa
--- /dev/null
+++ b/tiny_skia/src/layer.rs
@@ -0,0 +1,333 @@
+use crate::core::image;
+use crate::core::renderer::Quad;
+use crate::core::svg;
+use crate::core::{Background, Color, Point, Rectangle, Transformation};
+use crate::graphics::damage;
+use crate::graphics::layer;
+use crate::graphics::text::{Editor, Paragraph, Text};
+use crate::graphics::{self, Image};
+use crate::Primitive;
+
+use std::rc::Rc;
+
+pub type Stack = layer::Stack<Layer>;
+
+#[derive(Debug, Clone)]
+pub struct Layer {
+ pub bounds: Rectangle,
+ pub quads: Vec<(Quad, Background)>,
+ pub primitives: Vec<Item<Primitive>>,
+ pub text: Vec<Item<Text>>,
+ pub images: Vec<Image>,
+}
+
+impl Layer {
+ pub fn draw_quad(
+ &mut self,
+ mut quad: Quad,
+ background: Background,
+ transformation: Transformation,
+ ) {
+ quad.bounds = quad.bounds * transformation;
+ self.quads.push((quad, background));
+ }
+
+ pub fn draw_paragraph(
+ &mut self,
+ paragraph: &Paragraph,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let paragraph = Text::Paragraph {
+ paragraph: paragraph.downgrade(),
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ };
+
+ self.text.push(Item::Live(paragraph));
+ }
+
+ pub fn draw_editor(
+ &mut self,
+ editor: &Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let editor = Text::Editor {
+ editor: editor.downgrade(),
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ };
+
+ self.text.push(Item::Live(editor));
+ }
+
+ pub fn draw_text(
+ &mut self,
+ text: crate::core::Text,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let text = Text::Cached {
+ content: text.content,
+ bounds: Rectangle::new(position, text.bounds) * transformation,
+ color,
+ size: text.size * transformation.scale_factor(),
+ line_height: text.line_height.to_absolute(text.size)
+ * transformation.scale_factor(),
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ clip_bounds: clip_bounds * transformation,
+ };
+
+ self.text.push(Item::Live(text));
+ }
+
+ pub fn draw_text_group(
+ &mut self,
+ text: Vec<Text>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.text
+ .push(Item::Group(text, clip_bounds, transformation));
+ }
+
+ pub fn draw_text_cache(
+ &mut self,
+ text: Rc<[Text]>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.text
+ .push(Item::Cached(text, clip_bounds, transformation));
+ }
+
+ pub fn draw_image(
+ &mut self,
+ handle: image::Handle,
+ filter_method: image::FilterMethod,
+ bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let image = Image::Raster {
+ handle,
+ filter_method,
+ bounds: bounds * transformation,
+ };
+
+ self.images.push(image);
+ }
+
+ pub fn draw_svg(
+ &mut self,
+ handle: svg::Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let svg = Image::Vector {
+ handle,
+ color,
+ bounds: bounds * transformation,
+ };
+
+ self.images.push(svg);
+ }
+
+ pub fn draw_primitive_group(
+ &mut self,
+ primitives: Vec<Primitive>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.primitives.push(Item::Group(
+ primitives,
+ clip_bounds,
+ transformation,
+ ));
+ }
+
+ pub fn draw_primitive_cache(
+ &mut self,
+ primitives: Rc<[Primitive]>,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ self.primitives.push(Item::Cached(
+ primitives,
+ clip_bounds,
+ transformation,
+ ));
+ }
+
+ pub fn damage(previous: &Self, current: &Self) -> Vec<Rectangle> {
+ if previous.bounds != current.bounds {
+ return vec![previous.bounds, current.bounds];
+ }
+
+ let mut damage = damage::list(
+ &previous.quads,
+ &current.quads,
+ |(quad, _)| {
+ quad.bounds
+ .expand(1.0)
+ .intersection(&current.bounds)
+ .into_iter()
+ .collect()
+ },
+ |(quad_a, background_a), (quad_b, background_b)| {
+ quad_a == quad_b && background_a == background_b
+ },
+ );
+
+ let text = damage::diff(
+ &previous.text,
+ &current.text,
+ |item| {
+ item.as_slice()
+ .iter()
+ .filter_map(Text::visible_bounds)
+ .map(|bounds| bounds * item.transformation())
+ .collect()
+ },
+ |text_a, text_b| {
+ damage::list(
+ text_a.as_slice(),
+ text_b.as_slice(),
+ |text| {
+ text.visible_bounds()
+ .into_iter()
+ .map(|bounds| bounds * text_a.transformation())
+ .collect()
+ },
+ |text_a, text_b| text_a == text_b,
+ )
+ },
+ );
+
+ let primitives = damage::list(
+ &previous.primitives,
+ &current.primitives,
+ |item| match item {
+ Item::Live(primitive) => vec![primitive.visible_bounds()],
+ Item::Group(primitives, group_bounds, transformation) => {
+ primitives
+ .as_slice()
+ .iter()
+ .map(Primitive::visible_bounds)
+ .map(|bounds| bounds * *transformation)
+ .filter_map(|bounds| bounds.intersection(group_bounds))
+ .collect()
+ }
+ Item::Cached(_, bounds, _) => {
+ vec![*bounds]
+ }
+ },
+ |primitive_a, primitive_b| match (primitive_a, primitive_b) {
+ (
+ Item::Cached(cache_a, bounds_a, transformation_a),
+ Item::Cached(cache_b, bounds_b, transformation_b),
+ ) => {
+ Rc::ptr_eq(cache_a, cache_b)
+ && bounds_a == bounds_b
+ && transformation_a == transformation_b
+ }
+ _ => false,
+ },
+ );
+
+ let images = damage::list(
+ &previous.images,
+ &current.images,
+ |image| vec![image.bounds().expand(1.0)],
+ Image::eq,
+ );
+
+ damage.extend(text);
+ damage.extend(primitives);
+ damage.extend(images);
+ damage
+ }
+}
+
+impl Default for Layer {
+ fn default() -> Self {
+ Self {
+ bounds: Rectangle::INFINITE,
+ quads: Vec::new(),
+ primitives: Vec::new(),
+ text: Vec::new(),
+ images: Vec::new(),
+ }
+ }
+}
+
+impl graphics::Layer for Layer {
+ fn with_bounds(bounds: Rectangle) -> Self {
+ Self {
+ bounds,
+ ..Self::default()
+ }
+ }
+
+ fn flush(&mut self) {}
+
+ fn resize(&mut self, bounds: graphics::core::Rectangle) {
+ self.bounds = bounds;
+ }
+
+ fn reset(&mut self) {
+ self.bounds = Rectangle::INFINITE;
+
+ self.quads.clear();
+ self.primitives.clear();
+ self.text.clear();
+ self.images.clear();
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Item<T> {
+ Live(T),
+ Group(Vec<T>, Rectangle, Transformation),
+ Cached(Rc<[T]>, Rectangle, Transformation),
+}
+
+impl<T> Item<T> {
+ pub fn transformation(&self) -> Transformation {
+ match self {
+ Item::Live(_) => Transformation::IDENTITY,
+ Item::Group(_, _, transformation)
+ | Item::Cached(_, _, transformation) => *transformation,
+ }
+ }
+
+ pub fn clip_bounds(&self) -> Rectangle {
+ match self {
+ Item::Live(_) => Rectangle::INFINITE,
+ Item::Group(_, clip_bounds, _)
+ | Item::Cached(_, clip_bounds, _) => *clip_bounds,
+ }
+ }
+
+ pub fn as_slice(&self) -> &[T] {
+ match self {
+ Item::Live(item) => std::slice::from_ref(item),
+ Item::Group(group, _, _) => group.as_slice(),
+ Item::Cached(cache, _, _) => cache,
+ }
+ }
+}
diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs
index e7294f9b..4c2c9430 100644
--- a/tiny_skia/src/lib.rs
+++ b/tiny_skia/src/lib.rs
@@ -1,9 +1,9 @@
-#![forbid(rust_2018_idioms)]
-#![deny(unsafe_code, unused_results, rustdoc::broken_intra_doc_links)]
+#![allow(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod window;
-mod backend;
+mod engine;
+mod layer;
mod primitive;
mod settings;
mod text;
@@ -20,12 +20,389 @@ pub mod geometry;
pub use iced_graphics as graphics;
pub use iced_graphics::core;
-pub use backend::Backend;
+pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
+#[cfg(feature = "geometry")]
+pub use geometry::Geometry;
+
+use crate::core::renderer;
+use crate::core::{
+ Background, Color, Font, Pixels, Point, Rectangle, Transformation,
+};
+use crate::engine::Engine;
+use crate::graphics::compositor;
+use crate::graphics::text::{Editor, Paragraph};
+use crate::graphics::Viewport;
+
/// A [`tiny-skia`] graphics renderer for [`iced`].
///
/// [`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
/// [`iced`]: https://github.com/iced-rs/iced
-pub type Renderer = iced_graphics::Renderer<Backend>;
+#[derive(Debug)]
+pub struct Renderer {
+ default_font: Font,
+ default_text_size: Pixels,
+ layers: layer::Stack,
+ engine: Engine, // TODO: Shared engine
+}
+
+impl Renderer {
+ pub fn new(default_font: Font, default_text_size: Pixels) -> Self {
+ Self {
+ default_font,
+ default_text_size,
+ layers: layer::Stack::new(),
+ engine: Engine::new(),
+ }
+ }
+
+ pub fn layers(&mut self) -> &[Layer] {
+ self.layers.flush();
+ self.layers.as_slice()
+ }
+
+ pub fn draw<T: AsRef<str>>(
+ &mut self,
+ pixels: &mut tiny_skia::PixmapMut<'_>,
+ clip_mask: &mut tiny_skia::Mask,
+ viewport: &Viewport,
+ damage: &[Rectangle],
+ background_color: Color,
+ overlay: &[T],
+ ) {
+ let physical_size = viewport.physical_size();
+ let scale_factor = viewport.scale_factor() as f32;
+
+ if !overlay.is_empty() {
+ let path = tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ 0.0,
+ 0.0,
+ physical_size.width as f32,
+ physical_size.height as f32,
+ )
+ .expect("Create damage rectangle"),
+ );
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(engine::into_color(
+ Color {
+ a: 0.1,
+ ..background_color
+ },
+ )),
+ anti_alias: false,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+
+ self.layers.flush();
+
+ for &region in damage {
+ let region = region * scale_factor;
+
+ let path = tiny_skia::PathBuilder::from_rect(
+ tiny_skia::Rect::from_xywh(
+ region.x,
+ region.y,
+ region.width,
+ region.height,
+ )
+ .expect("Create damage rectangle"),
+ );
+
+ pixels.fill_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(engine::into_color(
+ background_color,
+ )),
+ anti_alias: false,
+ blend_mode: tiny_skia::BlendMode::Source,
+ ..Default::default()
+ },
+ tiny_skia::FillRule::default(),
+ tiny_skia::Transform::identity(),
+ None,
+ );
+
+ for layer in self.layers.iter() {
+ let Some(clip_bounds) =
+ region.intersection(&(layer.bounds * scale_factor))
+ else {
+ continue;
+ };
+
+ engine::adjust_clip_mask(clip_mask, clip_bounds);
+
+ for (quad, background) in &layer.quads {
+ self.engine.draw_quad(
+ quad,
+ background,
+ Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+
+ for group in &layer.primitives {
+ let Some(new_clip_bounds) = (group.clip_bounds()
+ * scale_factor)
+ .intersection(&clip_bounds)
+ else {
+ continue;
+ };
+
+ engine::adjust_clip_mask(clip_mask, new_clip_bounds);
+
+ for primitive in group.as_slice() {
+ self.engine.draw_primitive(
+ primitive,
+ group.transformation()
+ * Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+
+ engine::adjust_clip_mask(clip_mask, clip_bounds);
+ }
+
+ for group in &layer.text {
+ for text in group.as_slice() {
+ self.engine.draw_text(
+ text,
+ group.transformation()
+ * Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+ }
+
+ for image in &layer.images {
+ self.engine.draw_image(
+ image,
+ Transformation::scale(scale_factor),
+ pixels,
+ clip_mask,
+ clip_bounds,
+ );
+ }
+ }
+
+ if !overlay.is_empty() {
+ pixels.stroke_path(
+ &path,
+ &tiny_skia::Paint {
+ shader: tiny_skia::Shader::SolidColor(
+ engine::into_color(Color::from_rgb(1.0, 0.0, 0.0)),
+ ),
+ anti_alias: false,
+ ..tiny_skia::Paint::default()
+ },
+ &tiny_skia::Stroke {
+ width: 1.0,
+ ..tiny_skia::Stroke::default()
+ },
+ tiny_skia::Transform::identity(),
+ None,
+ );
+ }
+ }
+
+ self.engine.trim();
+ }
+}
+
+impl core::Renderer for Renderer {
+ fn start_layer(&mut self, bounds: Rectangle) {
+ self.layers.push_clip(bounds);
+ }
+
+ fn end_layer(&mut self) {
+ self.layers.pop_clip();
+ }
+
+ fn start_transformation(&mut self, transformation: Transformation) {
+ self.layers.push_transformation(transformation);
+ }
+
+ fn end_transformation(&mut self) {
+ self.layers.pop_transformation();
+ }
+
+ fn fill_quad(
+ &mut self,
+ quad: renderer::Quad,
+ background: impl Into<Background>,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_quad(quad, background.into(), transformation);
+ }
+
+ fn clear(&mut self) {
+ self.layers.clear();
+ }
+}
+
+impl core::text::Renderer for Renderer {
+ type Font = Font;
+ type Paragraph = Paragraph;
+ type Editor = Editor;
+
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
+
+ fn default_font(&self) -> Self::Font {
+ self.default_font
+ }
+
+ fn default_size(&self) -> Pixels {
+ self.default_text_size
+ }
+
+ fn fill_paragraph(
+ &mut self,
+ text: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+
+ layer.draw_paragraph(
+ text,
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ );
+ }
+
+ fn fill_editor(
+ &mut self,
+ editor: &Self::Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_editor(editor, position, color, clip_bounds, transformation);
+ }
+
+ fn fill_text(
+ &mut self,
+ text: core::Text,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_text(text, position, color, clip_bounds, transformation);
+ }
+}
+
+#[cfg(feature = "geometry")]
+impl graphics::geometry::Renderer for Renderer {
+ type Geometry = Geometry;
+ type Frame = geometry::Frame;
+
+ fn new_frame(&self, size: core::Size) -> Self::Frame {
+ geometry::Frame::new(size)
+ }
+
+ fn draw_geometry(&mut self, geometry: Self::Geometry) {
+ let (layer, transformation) = self.layers.current_mut();
+
+ match geometry {
+ Geometry::Live {
+ primitives,
+ text,
+ clip_bounds,
+ } => {
+ layer.draw_primitive_group(
+ primitives,
+ clip_bounds,
+ transformation,
+ );
+
+ layer.draw_text_group(text, clip_bounds, transformation);
+ }
+ Geometry::Cache(cache) => {
+ layer.draw_primitive_cache(
+ cache.primitives,
+ cache.clip_bounds,
+ transformation,
+ );
+
+ layer.draw_text_cache(
+ cache.text,
+ cache.clip_bounds,
+ transformation,
+ );
+ }
+ }
+ }
+}
+
+impl graphics::mesh::Renderer for Renderer {
+ fn draw_mesh(&mut self, _mesh: graphics::Mesh) {
+ log::warn!("iced_tiny_skia does not support drawing meshes");
+ }
+}
+
+#[cfg(feature = "image")]
+impl core::image::Renderer for Renderer {
+ type Handle = core::image::Handle;
+
+ fn measure_image(&self, handle: &Self::Handle) -> crate::core::Size<u32> {
+ self.engine.raster_pipeline.dimensions(handle)
+ }
+
+ fn draw_image(
+ &mut self,
+ handle: Self::Handle,
+ filter_method: core::image::FilterMethod,
+ bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_image(handle, filter_method, bounds, transformation);
+ }
+}
+
+#[cfg(feature = "svg")]
+impl core::svg::Renderer for Renderer {
+ fn measure_svg(
+ &self,
+ handle: &core::svg::Handle,
+ ) -> crate::core::Size<u32> {
+ self.engine.vector_pipeline.viewport_dimensions(handle)
+ }
+
+ fn draw_svg(
+ &mut self,
+ handle: core::svg::Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_svg(handle, color, bounds, transformation);
+ }
+}
+
+impl compositor::Default for Renderer {
+ type Compositor = window::Compositor;
+}
diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs
index b7c428e4..5de51047 100644
--- a/tiny_skia/src/primitive.rs
+++ b/tiny_skia/src/primitive.rs
@@ -1,10 +1,7 @@
use crate::core::Rectangle;
-use crate::graphics::{Damage, Mesh};
-
-pub type Primitive = crate::graphics::Primitive<Custom>;
#[derive(Debug, Clone, PartialEq)]
-pub enum Custom {
+pub enum Primitive {
/// A path filled with some paint.
Fill {
/// The path to fill.
@@ -25,28 +22,19 @@ pub enum Custom {
},
}
-impl Damage for Custom {
- fn bounds(&self) -> Rectangle {
- match self {
- Self::Fill { path, .. } | Self::Stroke { path, .. } => {
- let bounds = path.bounds();
+impl Primitive {
+ /// Returns the visible bounds of the [`Primitive`].
+ pub fn visible_bounds(&self) -> Rectangle {
+ let bounds = match self {
+ Primitive::Fill { path, .. } => path.bounds(),
+ Primitive::Stroke { path, .. } => path.bounds(),
+ };
- Rectangle {
- x: bounds.x(),
- y: bounds.y(),
- width: bounds.width(),
- height: bounds.height(),
- }
- .expand(1.0)
- }
+ Rectangle {
+ x: bounds.x(),
+ y: bounds.y(),
+ width: bounds.width(),
+ height: bounds.height(),
}
}
}
-
-impl TryFrom<Mesh> for Custom {
- type Error = &'static str;
-
- fn try_from(_mesh: Mesh) -> Result<Self, Self::Error> {
- Err("unsupported")
- }
-}
diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs
index 5f17ae60..176b0da9 100644
--- a/tiny_skia/src/raster.rs
+++ b/tiny_skia/src/raster.rs
@@ -6,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
use std::collections::hash_map;
+#[derive(Debug)]
pub struct Pipeline {
cache: RefCell<Cache>,
}
@@ -68,7 +69,7 @@ impl Pipeline {
}
}
-#[derive(Default)]
+#[derive(Debug, Default)]
struct Cache {
entries: FxHashMap<u64, Option<Entry>>,
hits: FxHashSet<u64>,
@@ -119,6 +120,7 @@ impl Cache {
}
}
+#[derive(Debug)]
struct Entry {
width: u32,
height: u32,
diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs
index 01d015b4..672c49f3 100644
--- a/tiny_skia/src/settings.rs
+++ b/tiny_skia/src/settings.rs
@@ -1,9 +1,9 @@
use crate::core::{Font, Pixels};
use crate::graphics;
-/// The settings of a [`Backend`].
+/// The settings of a [`Compositor`].
///
-/// [`Backend`]: crate::Backend
+/// [`Compositor`]: crate::window::Compositor
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
/// The default [`Font`] to use.
diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs
index d28cc483..c71deb10 100644
--- a/tiny_skia/src/text.rs
+++ b/tiny_skia/src/text.rs
@@ -1,5 +1,5 @@
use crate::core::alignment;
-use crate::core::text::{LineHeight, Shaping};
+use crate::core::text::Shaping;
use crate::core::{
Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
@@ -13,7 +13,7 @@ use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::hash_map;
-#[allow(missing_debug_implementations)]
+#[derive(Debug)]
pub struct Pipeline {
glyph_cache: GlyphCache,
cache: RefCell<Cache>,
@@ -27,6 +27,8 @@ impl Pipeline {
}
}
+ // TODO: Shared engine
+ #[allow(dead_code)]
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
font_system()
.write()
@@ -41,7 +43,6 @@ impl Pipeline {
paragraph: &paragraph::Weak,
position: Point,
color: Color,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
@@ -62,7 +63,6 @@ impl Pipeline {
color,
paragraph.horizontal_alignment(),
paragraph.vertical_alignment(),
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -74,7 +74,6 @@ impl Pipeline {
editor: &editor::Weak,
position: Point,
color: Color,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
@@ -95,7 +94,6 @@ impl Pipeline {
color,
alignment::Horizontal::Left,
alignment::Vertical::Top,
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -108,17 +106,16 @@ impl Pipeline {
bounds: Rectangle,
color: Color,
size: Pixels,
- line_height: LineHeight,
+ line_height: Pixels,
font: Font,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
shaping: Shaping,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
) {
- let line_height = f32::from(line_height.to_absolute(size));
+ let line_height = f32::from(line_height);
let mut font_system = font_system().write().expect("Write font system");
let font_system = font_system.raw();
@@ -149,7 +146,6 @@ impl Pipeline {
color,
horizontal_alignment,
vertical_alignment,
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -161,7 +157,6 @@ impl Pipeline {
buffer: &cosmic_text::Buffer,
position: Point,
color: Color,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
@@ -178,7 +173,6 @@ impl Pipeline {
color,
alignment::Horizontal::Left,
alignment::Vertical::Top,
- scale_factor,
pixels,
clip_mask,
transformation,
@@ -199,12 +193,11 @@ fn draw(
color: Color,
horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical,
- scale_factor: f32,
pixels: &mut tiny_skia::PixmapMut<'_>,
clip_mask: Option<&tiny_skia::Mask>,
transformation: Transformation,
) {
- let bounds = bounds * transformation * scale_factor;
+ let bounds = bounds * transformation;
let x = match horizontal_alignment {
alignment::Horizontal::Left => bounds.x,
@@ -222,8 +215,8 @@ fn draw(
for run in buffer.layout_runs() {
for glyph in run.glyphs {
- let physical_glyph = glyph
- .physical((x, y), scale_factor * transformation.scale_factor());
+ let physical_glyph =
+ glyph.physical((x, y), transformation.scale_factor());
if let Some((buffer, placement)) = glyph_cache.allocate(
physical_glyph.cache_key,
@@ -247,10 +240,8 @@ fn draw(
pixels.draw_pixmap(
physical_glyph.x + placement.left,
physical_glyph.y - placement.top
- + (run.line_y
- * scale_factor
- * transformation.scale_factor())
- .round() as i32,
+ + (run.line_y * transformation.scale_factor()).round()
+ as i32,
pixmap,
&tiny_skia::PixmapPaint {
opacity,
diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs
index fd1ab3de..5150cffe 100644
--- a/tiny_skia/src/vector.rs
+++ b/tiny_skia/src/vector.rs
@@ -9,6 +9,7 @@ use std::cell::RefCell;
use std::collections::hash_map;
use std::fs;
+#[derive(Debug)]
pub struct Pipeline {
cache: RefCell<Cache>,
}
@@ -203,3 +204,13 @@ impl Cache {
self.raster_hits.clear();
}
}
+
+impl std::fmt::Debug for Cache {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Cache")
+ .field("tree_hits", &self.tree_hits)
+ .field("rasters", &self.rasters)
+ .field("raster_hits", &self.raster_hits)
+ .finish_non_exhaustive()
+ }
+}
diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs
index 25c57dc1..153af6d5 100644
--- a/tiny_skia/src/window/compositor.rs
+++ b/tiny_skia/src/window/compositor.rs
@@ -3,23 +3,25 @@ use crate::graphics::compositor::{self, Information};
use crate::graphics::damage;
use crate::graphics::error::{self, Error};
use crate::graphics::{self, Viewport};
-use crate::{Backend, Primitive, Renderer, Settings};
+use crate::{Layer, Renderer, Settings};
use std::collections::VecDeque;
use std::num::NonZeroU32;
+#[allow(missing_debug_implementations)]
pub struct Compositor {
context: softbuffer::Context<Box<dyn compositor::Window>>,
settings: Settings,
}
+#[allow(missing_debug_implementations)]
pub struct Surface {
window: softbuffer::Surface<
Box<dyn compositor::Window>,
Box<dyn compositor::Window>,
>,
clip_mask: tiny_skia::Mask,
- primitive_stack: VecDeque<Vec<Primitive>>,
+ layer_stack: VecDeque<Vec<Layer>>,
background_color: Color,
max_age: u8,
}
@@ -48,7 +50,6 @@ impl crate::graphics::Compositor for Compositor {
fn create_renderer(&self) -> Self::Renderer {
Renderer::new(
- Backend::new(),
self.settings.default_font,
self.settings.default_text_size,
)
@@ -70,7 +71,7 @@ impl crate::graphics::Compositor for Compositor {
window,
clip_mask: tiny_skia::Mask::new(width, height)
.expect("Create clip mask"),
- primitive_stack: VecDeque::new(),
+ layer_stack: VecDeque::new(),
background_color: Color::BLACK,
max_age: 0,
};
@@ -96,7 +97,7 @@ impl crate::graphics::Compositor for Compositor {
surface.clip_mask =
tiny_skia::Mask::new(width, height).expect("Create clip mask");
- surface.primitive_stack.clear();
+ surface.layer_stack.clear();
}
fn fetch_information(&self) -> Information {
@@ -114,16 +115,7 @@ impl crate::graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
- renderer.with_primitives(|backend, primitives| {
- present(
- backend,
- surface,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ present(renderer, surface, viewport, background_color, overlay)
}
fn screenshot<T: AsRef<str>>(
@@ -134,16 +126,7 @@ impl crate::graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
- renderer.with_primitives(|backend, primitives| {
- screenshot(
- surface,
- backend,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ screenshot(renderer, surface, viewport, background_color, overlay)
}
}
@@ -159,38 +142,42 @@ pub fn new<W: compositor::Window>(
}
pub fn present<T: AsRef<str>>(
- backend: &mut Backend,
+ renderer: &mut Renderer,
surface: &mut Surface,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
let physical_size = viewport.physical_size();
- let scale_factor = viewport.scale_factor() as f32;
let mut buffer = surface
.window
.buffer_mut()
.map_err(|_| compositor::SurfaceError::Lost)?;
- let last_primitives = {
+ let last_layers = {
let age = buffer.age();
surface.max_age = surface.max_age.max(age);
- surface.primitive_stack.truncate(surface.max_age as usize);
+ surface.layer_stack.truncate(surface.max_age as usize);
if age > 0 {
- surface.primitive_stack.get(age as usize - 1)
+ surface.layer_stack.get(age as usize - 1)
} else {
None
}
};
- let damage = last_primitives
- .and_then(|last_primitives| {
- (surface.background_color == background_color)
- .then(|| damage::list(last_primitives, primitives))
+ let damage = last_layers
+ .and_then(|last_layers| {
+ (surface.background_color == background_color).then(|| {
+ damage::diff(
+ last_layers,
+ renderer.layers(),
+ |layer| vec![layer.bounds],
+ Layer::damage,
+ )
+ })
})
.unwrap_or_else(|| vec![Rectangle::with_size(viewport.logical_size())]);
@@ -198,10 +185,11 @@ pub fn present<T: AsRef<str>>(
return Ok(());
}
- surface.primitive_stack.push_front(primitives.to_vec());
+ surface.layer_stack.push_front(renderer.layers().to_vec());
surface.background_color = background_color;
- let damage = damage::group(damage, scale_factor, physical_size);
+ let damage =
+ damage::group(damage, Rectangle::with_size(viewport.logical_size()));
let mut pixels = tiny_skia::PixmapMut::from_bytes(
bytemuck::cast_slice_mut(&mut buffer),
@@ -210,10 +198,9 @@ pub fn present<T: AsRef<str>>(
)
.expect("Create pixel map");
- backend.draw(
+ renderer.draw(
&mut pixels,
&mut surface.clip_mask,
- primitives,
viewport,
&damage,
background_color,
@@ -224,9 +211,8 @@ pub fn present<T: AsRef<str>>(
}
pub fn screenshot<T: AsRef<str>>(
+ renderer: &mut Renderer,
surface: &mut Surface,
- backend: &mut Backend,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
@@ -236,7 +222,7 @@ pub fn screenshot<T: AsRef<str>>(
let mut offscreen_buffer: Vec<u32> =
vec![0; size.width as usize * size.height as usize];
- backend.draw(
+ renderer.draw(
&mut tiny_skia::PixmapMut::from_bytes(
bytemuck::cast_slice_mut(&mut offscreen_buffer),
size.width,
@@ -244,7 +230,6 @@ pub fn screenshot<T: AsRef<str>>(
)
.expect("Create offscreen pixel map"),
&mut surface.clip_mask,
- primitives,
viewport,
&[Rectangle::with_size(Size::new(
size.width as f32,
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index f6162e0f..30545fa2 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -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
@@ -32,6 +35,7 @@ glyphon.workspace = true
guillotiere.workspace = true
log.workspace = true
once_cell.workspace = true
+rustc-hash.workspace = true
thiserror.workspace = true
wgpu.workspace = true
@@ -40,6 +44,3 @@ lyon.optional = true
resvg.workspace = true
resvg.optional = true
-
-tracing.workspace = true
-tracing.optional = true
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
deleted file mode 100644
index 5019191c..00000000
--- a/wgpu/src/backend.rs
+++ /dev/null
@@ -1,410 +0,0 @@
-use crate::core::{Color, Size, Transformation};
-use crate::graphics::backend;
-use crate::graphics::color;
-use crate::graphics::Viewport;
-use crate::primitive::pipeline;
-use crate::primitive::{self, Primitive};
-use crate::quad;
-use crate::text;
-use crate::triangle;
-use crate::window;
-use crate::{Layer, Settings};
-
-#[cfg(feature = "tracing")]
-use tracing::info_span;
-
-#[cfg(any(feature = "image", feature = "svg"))]
-use crate::image;
-
-use std::borrow::Cow;
-
-/// A [`wgpu`] graphics backend for [`iced`].
-///
-/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
-/// [`iced`]: https://github.com/iced-rs/iced
-#[allow(missing_debug_implementations)]
-pub struct Backend {
- quad_pipeline: quad::Pipeline,
- text_pipeline: text::Pipeline,
- triangle_pipeline: triangle::Pipeline,
- pipeline_storage: pipeline::Storage,
- #[cfg(any(feature = "image", feature = "svg"))]
- image_pipeline: image::Pipeline,
-}
-
-impl Backend {
- /// Creates a new [`Backend`].
- pub fn new(
- _adapter: &wgpu::Adapter,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- settings: Settings,
- format: wgpu::TextureFormat,
- ) -> Self {
- let text_pipeline = text::Pipeline::new(device, queue, format);
- let quad_pipeline = quad::Pipeline::new(device, format);
- let triangle_pipeline =
- triangle::Pipeline::new(device, format, settings.antialiasing);
-
- #[cfg(any(feature = "image", feature = "svg"))]
- let image_pipeline = {
- let backend = _adapter.get_info().backend;
-
- image::Pipeline::new(device, format, backend)
- };
-
- Self {
- quad_pipeline,
- text_pipeline,
- triangle_pipeline,
- pipeline_storage: pipeline::Storage::default(),
-
- #[cfg(any(feature = "image", feature = "svg"))]
- image_pipeline,
- }
- }
-
- /// Draws the provided primitives in the given `TextureView`.
- ///
- /// The text provided as overlay will be rendered on top of the primitives.
- /// This is useful for rendering debug information.
- pub fn present<T: AsRef<str>>(
- &mut self,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- encoder: &mut wgpu::CommandEncoder,
- clear_color: Option<Color>,
- format: wgpu::TextureFormat,
- frame: &wgpu::TextureView,
- primitives: &[Primitive],
- viewport: &Viewport,
- overlay_text: &[T],
- ) {
- log::debug!("Drawing");
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Backend", "PRESENT").entered();
-
- let target_size = viewport.physical_size();
- let scale_factor = viewport.scale_factor() as f32;
- let transformation = viewport.projection();
-
- let mut layers = Layer::generate(primitives, viewport);
-
- if !overlay_text.is_empty() {
- layers.push(Layer::overlay(overlay_text, viewport));
- }
-
- self.prepare(
- device,
- queue,
- format,
- encoder,
- scale_factor,
- target_size,
- transformation,
- &layers,
- );
-
- self.render(
- device,
- encoder,
- frame,
- clear_color,
- scale_factor,
- target_size,
- &layers,
- );
-
- self.quad_pipeline.end_frame();
- self.text_pipeline.end_frame();
- self.triangle_pipeline.end_frame();
-
- #[cfg(any(feature = "image", feature = "svg"))]
- self.image_pipeline.end_frame();
- }
-
- fn prepare(
- &mut self,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- format: wgpu::TextureFormat,
- _encoder: &mut wgpu::CommandEncoder,
- scale_factor: f32,
- target_size: Size<u32>,
- transformation: Transformation,
- layers: &[Layer<'_>],
- ) {
- for layer in layers {
- let bounds = (layer.bounds * scale_factor).snap();
-
- if bounds.width < 1 || bounds.height < 1 {
- continue;
- }
-
- if !layer.quads.is_empty() {
- self.quad_pipeline.prepare(
- device,
- queue,
- &layer.quads,
- transformation,
- scale_factor,
- );
- }
-
- if !layer.meshes.is_empty() {
- let scaled =
- transformation * Transformation::scale(scale_factor);
-
- self.triangle_pipeline.prepare(
- device,
- queue,
- &layer.meshes,
- scaled,
- );
- }
-
- #[cfg(any(feature = "image", feature = "svg"))]
- {
- if !layer.images.is_empty() {
- let scaled =
- transformation * Transformation::scale(scale_factor);
-
- self.image_pipeline.prepare(
- device,
- queue,
- _encoder,
- &layer.images,
- scaled,
- scale_factor,
- );
- }
- }
-
- if !layer.text.is_empty() {
- self.text_pipeline.prepare(
- device,
- queue,
- &layer.text,
- layer.bounds,
- scale_factor,
- target_size,
- );
- }
-
- if !layer.pipelines.is_empty() {
- for pipeline in &layer.pipelines {
- pipeline.primitive.prepare(
- format,
- device,
- queue,
- pipeline.bounds,
- target_size,
- scale_factor,
- &mut self.pipeline_storage,
- );
- }
- }
- }
- }
-
- fn render(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- target: &wgpu::TextureView,
- clear_color: Option<Color>,
- scale_factor: f32,
- target_size: Size<u32>,
- layers: &[Layer<'_>],
- ) {
- use std::mem::ManuallyDrop;
-
- let mut quad_layer = 0;
- let mut triangle_layer = 0;
- #[cfg(any(feature = "image", feature = "svg"))]
- let mut image_layer = 0;
- let mut text_layer = 0;
-
- let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
- &wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu render pass"),
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: match clear_color {
- Some(background_color) => wgpu::LoadOp::Clear({
- let [r, g, b, a] =
- color::pack(background_color).components();
-
- wgpu::Color {
- r: f64::from(r),
- g: f64::from(g),
- b: f64::from(b),
- a: f64::from(a),
- }
- }),
- None => wgpu::LoadOp::Load,
- },
- store: wgpu::StoreOp::Store,
- },
- })],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- },
- ));
-
- for layer in layers {
- let bounds = (layer.bounds * scale_factor).snap();
-
- if bounds.width < 1 || bounds.height < 1 {
- continue;
- }
-
- if !layer.quads.is_empty() {
- self.quad_pipeline.render(
- quad_layer,
- bounds,
- &layer.quads,
- &mut render_pass,
- );
-
- quad_layer += 1;
- }
-
- if !layer.meshes.is_empty() {
- let _ = ManuallyDrop::into_inner(render_pass);
-
- self.triangle_pipeline.render(
- device,
- encoder,
- target,
- triangle_layer,
- target_size,
- &layer.meshes,
- scale_factor,
- );
-
- triangle_layer += 1;
-
- render_pass = ManuallyDrop::new(encoder.begin_render_pass(
- &wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu render pass"),
- color_attachments: &[Some(
- wgpu::RenderPassColorAttachment {
- view: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- },
- )],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- },
- ));
- }
-
- #[cfg(any(feature = "image", feature = "svg"))]
- {
- if !layer.images.is_empty() {
- self.image_pipeline.render(
- image_layer,
- bounds,
- &mut render_pass,
- );
-
- image_layer += 1;
- }
- }
-
- if !layer.text.is_empty() {
- self.text_pipeline
- .render(text_layer, bounds, &mut render_pass);
-
- text_layer += 1;
- }
-
- if !layer.pipelines.is_empty() {
- let _ = ManuallyDrop::into_inner(render_pass);
-
- for pipeline in &layer.pipelines {
- let viewport = (pipeline.viewport * scale_factor).snap();
-
- if viewport.width < 1 || viewport.height < 1 {
- continue;
- }
-
- pipeline.primitive.render(
- &self.pipeline_storage,
- target,
- target_size,
- viewport,
- encoder,
- );
- }
-
- render_pass = ManuallyDrop::new(encoder.begin_render_pass(
- &wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu render pass"),
- color_attachments: &[Some(
- wgpu::RenderPassColorAttachment {
- view: target,
- resolve_target: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Load,
- store: wgpu::StoreOp::Store,
- },
- },
- )],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- },
- ));
- }
- }
-
- let _ = ManuallyDrop::into_inner(render_pass);
- }
-}
-
-impl backend::Backend for Backend {
- type Primitive = primitive::Custom;
- type Compositor = window::Compositor;
-}
-
-impl backend::Text for Backend {
- fn load_font(&mut self, font: Cow<'static, [u8]>) {
- self.text_pipeline.load_font(font);
- }
-}
-
-#[cfg(feature = "image")]
-impl backend::Image for Backend {
- fn dimensions(&self, handle: &crate::core::image::Handle) -> Size<u32> {
- self.image_pipeline.dimensions(handle)
- }
-}
-
-#[cfg(feature = "svg")]
-impl backend::Svg for Backend {
- fn viewport_dimensions(
- &self,
- handle: &crate::core::svg::Handle,
- ) -> Size<u32> {
- self.image_pipeline.viewport_dimensions(handle)
- }
-}
-
-#[cfg(feature = "geometry")]
-impl crate::graphics::geometry::Backend for Backend {
- type Frame = crate::geometry::Frame;
-
- fn new_frame(&self, size: Size) -> Self::Frame {
- crate::geometry::Frame::new(size)
- }
-}
diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs
index ef00c58f..463ea24a 100644
--- a/wgpu/src/buffer.rs
+++ b/wgpu/src/buffer.rs
@@ -1,6 +1,13 @@
use std::marker::PhantomData;
+use std::num::NonZeroU64;
use std::ops::RangeBounds;
+pub const MAX_WRITE_SIZE: usize = 100 * 1024;
+
+#[allow(unsafe_code)]
+const MAX_WRITE_SIZE_U64: NonZeroU64 =
+ unsafe { NonZeroU64::new_unchecked(MAX_WRITE_SIZE as u64) };
+
#[derive(Debug)]
pub struct Buffer<T> {
label: &'static str,
@@ -61,12 +68,46 @@ impl<T: bytemuck::Pod> Buffer<T> {
/// Returns the size of the written bytes.
pub fn write(
&mut self,
- queue: &wgpu::Queue,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
offset: usize,
contents: &[T],
) -> usize {
let bytes: &[u8] = bytemuck::cast_slice(contents);
- queue.write_buffer(&self.raw, offset as u64, bytes);
+ let mut bytes_written = 0;
+
+ // Split write into multiple chunks if necessary
+ while bytes_written + MAX_WRITE_SIZE < bytes.len() {
+ belt.write_buffer(
+ encoder,
+ &self.raw,
+ (offset + bytes_written) as u64,
+ MAX_WRITE_SIZE_U64,
+ device,
+ )
+ .copy_from_slice(
+ &bytes[bytes_written..bytes_written + MAX_WRITE_SIZE],
+ );
+
+ bytes_written += MAX_WRITE_SIZE;
+ }
+
+ // There will always be some bytes left, since the previous
+ // loop guarantees `bytes_written < bytes.len()`
+ let bytes_left = ((bytes.len() - bytes_written) as u64)
+ .try_into()
+ .expect("non-empty write");
+
+ // Write them
+ belt.write_buffer(
+ encoder,
+ &self.raw,
+ (offset + bytes_written) as u64,
+ bytes_left,
+ device,
+ )
+ .copy_from_slice(&bytes[bytes_written..]);
self.offsets.push(offset as u64);
diff --git a/wgpu/src/engine.rs b/wgpu/src/engine.rs
new file mode 100644
index 00000000..96cd6db8
--- /dev/null
+++ b/wgpu/src/engine.rs
@@ -0,0 +1,84 @@
+use crate::buffer;
+use crate::graphics::Antialiasing;
+use crate::primitive;
+use crate::quad;
+use crate::text;
+use crate::triangle;
+
+#[allow(missing_debug_implementations)]
+pub struct Engine {
+ pub(crate) staging_belt: wgpu::util::StagingBelt,
+ pub(crate) format: wgpu::TextureFormat,
+
+ pub(crate) quad_pipeline: quad::Pipeline,
+ pub(crate) text_pipeline: text::Pipeline,
+ pub(crate) triangle_pipeline: triangle::Pipeline,
+ #[cfg(any(feature = "image", feature = "svg"))]
+ pub(crate) image_pipeline: crate::image::Pipeline,
+ pub(crate) primitive_storage: primitive::Storage,
+}
+
+impl Engine {
+ pub fn new(
+ _adapter: &wgpu::Adapter,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
+ antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily
+ ) -> Self {
+ let text_pipeline = text::Pipeline::new(device, queue, format);
+ let quad_pipeline = quad::Pipeline::new(device, format);
+ let triangle_pipeline =
+ triangle::Pipeline::new(device, format, antialiasing);
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ let image_pipeline = {
+ let backend = _adapter.get_info().backend;
+
+ crate::image::Pipeline::new(device, format, backend)
+ };
+
+ Self {
+ // TODO: Resize belt smartly (?)
+ // It would be great if the `StagingBelt` API exposed methods
+ // for introspection to detect when a resize may be worth it.
+ staging_belt: wgpu::util::StagingBelt::new(
+ buffer::MAX_WRITE_SIZE as u64,
+ ),
+ format,
+
+ quad_pipeline,
+ text_pipeline,
+ triangle_pipeline,
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ image_pipeline,
+
+ primitive_storage: primitive::Storage::default(),
+ }
+ }
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ pub fn image_cache(&self) -> &crate::image::cache::Shared {
+ self.image_pipeline.cache()
+ }
+
+ pub fn submit(
+ &mut self,
+ queue: &wgpu::Queue,
+ encoder: wgpu::CommandEncoder,
+ ) -> wgpu::SubmissionIndex {
+ self.staging_belt.finish();
+ let index = queue.submit(Some(encoder.finish()));
+ self.staging_belt.recall();
+
+ self.quad_pipeline.end_frame();
+ self.text_pipeline.end_frame();
+ self.triangle_pipeline.end_frame();
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ self.image_pipeline.end_frame();
+
+ index
+ }
+}
diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs
index ba56c59d..60967082 100644
--- a/wgpu/src/geometry.rs
+++ b/wgpu/src/geometry.rs
@@ -6,23 +6,74 @@ use crate::core::{
use crate::graphics::color;
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::{
- self, LineCap, LineDash, LineJoin, Path, Stroke, Style, Text,
+ self, LineCap, LineDash, LineJoin, Path, Stroke, Style,
};
use crate::graphics::gradient::{self, Gradient};
use crate::graphics::mesh::{self, Mesh};
-use crate::primitive::{self, Primitive};
+use crate::graphics::{self, Cached, Text};
+use crate::text;
+use crate::triangle;
use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
+#[derive(Debug)]
+pub enum Geometry {
+ Live { meshes: Vec<Mesh>, text: Vec<Text> },
+ Cached(Cache),
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+ pub meshes: Option<triangle::Cache>,
+ pub text: Option<text::Cache>,
+}
+
+impl Cached for Geometry {
+ type Cache = Cache;
+
+ fn load(cache: &Self::Cache) -> Self {
+ Geometry::Cached(cache.clone())
+ }
+
+ fn cache(self, previous: Option<Self::Cache>) -> Self::Cache {
+ match self {
+ Self::Live { meshes, text } => {
+ if let Some(mut previous) = previous {
+ if let Some(cache) = &mut previous.meshes {
+ cache.update(meshes);
+ } else {
+ previous.meshes = triangle::Cache::new(meshes);
+ }
+
+ if let Some(cache) = &mut previous.text {
+ cache.update(text);
+ } else {
+ previous.text = text::Cache::new(text);
+ }
+
+ previous
+ } else {
+ Cache {
+ meshes: triangle::Cache::new(meshes),
+ text: text::Cache::new(text),
+ }
+ }
+ }
+ Self::Cached(cache) => cache,
+ }
+ }
+}
+
/// A frame for drawing some geometry.
#[allow(missing_debug_implementations)]
pub struct Frame {
- size: Size,
+ clip_bounds: Rectangle,
buffers: BufferStack,
- primitives: Vec<Primitive>,
+ meshes: Vec<Mesh>,
+ text: Vec<Text>,
transforms: Transforms,
fill_tessellator: tessellation::FillTessellator,
stroke_tessellator: tessellation::StrokeTessellator,
@@ -31,81 +82,49 @@ pub struct Frame {
impl Frame {
/// Creates a new [`Frame`] with the given [`Size`].
pub fn new(size: Size) -> Frame {
+ Self::with_clip(Rectangle::with_size(size))
+ }
+
+ /// Creates a new [`Frame`] with the given clip bounds.
+ pub fn with_clip(bounds: Rectangle) -> Frame {
Frame {
- size,
+ clip_bounds: bounds,
buffers: BufferStack::new(),
- primitives: Vec::new(),
+ meshes: Vec::new(),
+ text: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
- current: Transform(lyon::math::Transform::identity()),
+ current: Transform(lyon::math::Transform::translation(
+ bounds.x, bounds.y,
+ )),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
-
- fn into_primitives(mut self) -> Vec<Primitive> {
- for buffer in self.buffers.stack {
- match buffer {
- Buffer::Solid(buffer) => {
- if !buffer.indices.is_empty() {
- self.primitives.push(Primitive::Custom(
- primitive::Custom::Mesh(Mesh::Solid {
- buffers: mesh::Indexed {
- vertices: buffer.vertices,
- indices: buffer.indices,
- },
- size: self.size,
- }),
- ));
- }
- }
- Buffer::Gradient(buffer) => {
- if !buffer.indices.is_empty() {
- self.primitives.push(Primitive::Custom(
- primitive::Custom::Mesh(Mesh::Gradient {
- buffers: mesh::Indexed {
- vertices: buffer.vertices,
- indices: buffer.indices,
- },
- size: self.size,
- }),
- ));
- }
- }
- }
- }
-
- self.primitives
- }
}
impl geometry::frame::Backend for Frame {
- type Geometry = Primitive;
-
- /// Creates a new empty [`Frame`] with the given dimensions.
- ///
- /// The default coordinate system of a [`Frame`] has its origin at the
- /// top-left corner of its bounds.
+ type Geometry = Geometry;
#[inline]
fn width(&self) -> f32 {
- self.size.width
+ self.clip_bounds.width
}
#[inline]
fn height(&self) -> f32 {
- self.size.height
+ self.clip_bounds.height
}
#[inline]
fn size(&self) -> Size {
- self.size
+ self.clip_bounds.size()
}
#[inline]
fn center(&self) -> Point {
- Point::new(self.size.width / 2.0, self.size.height / 2.0)
+ Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0)
}
fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
@@ -208,7 +227,7 @@ impl geometry::frame::Backend for Frame {
.expect("Stroke path");
}
- fn fill_text(&mut self, text: impl Into<Text>) {
+ fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transforms.current.scale();
@@ -246,18 +265,17 @@ impl geometry::frame::Backend for Frame {
height: f32::INFINITY,
};
- // TODO: Honor layering!
- self.primitives.push(Primitive::Text {
+ self.text.push(graphics::Text::Cached {
content: text.content,
bounds,
color: text.color,
size,
- line_height,
+ line_height: line_height.to_absolute(size),
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
- clip_bounds: Rectangle::with_size(Size::INFINITY),
+ clip_bounds: self.clip_bounds,
});
} else {
text.draw_with(|path, color| self.fill(&path, color));
@@ -308,41 +326,24 @@ impl geometry::frame::Backend for Frame {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
- fn draft(&mut self, size: Size) -> Frame {
- Frame::new(size)
+ fn draft(&mut self, clip_bounds: Rectangle) -> Frame {
+ Frame::with_clip(clip_bounds)
}
- fn paste(&mut self, frame: Frame, at: Point) {
- let size = frame.size();
- let primitives = frame.into_primitives();
- let transformation = Transformation::translate(at.x, at.y);
-
- let (text, meshes) = primitives
- .into_iter()
- .partition(|primitive| matches!(primitive, Primitive::Text { .. }));
+ fn paste(&mut self, frame: Frame, _at: Point) {
+ self.meshes
+ .extend(frame.buffers.into_meshes(frame.clip_bounds));
- self.primitives.push(Primitive::Group {
- primitives: vec![
- Primitive::Transform {
- transformation,
- content: Box::new(Primitive::Group { primitives: meshes }),
- },
- Primitive::Transform {
- transformation,
- content: Box::new(Primitive::Clip {
- bounds: Rectangle::with_size(size),
- content: Box::new(Primitive::Group {
- primitives: text,
- }),
- }),
- },
- ],
- });
+ self.text.extend(frame.text);
}
- fn into_geometry(self) -> Self::Geometry {
- Primitive::Group {
- primitives: self.into_primitives(),
+ fn into_geometry(mut self) -> Self::Geometry {
+ self.meshes
+ .extend(self.buffers.into_meshes(self.clip_bounds));
+
+ Geometry::Live {
+ meshes: self.meshes,
+ text: self.text,
}
}
}
@@ -429,6 +430,34 @@ impl BufferStack {
_ => unreachable!(),
}
}
+
+ fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> {
+ self.stack
+ .into_iter()
+ .filter_map(move |buffer| match buffer {
+ Buffer::Solid(buffer) if !buffer.indices.is_empty() => {
+ Some(Mesh::Solid {
+ buffers: mesh::Indexed {
+ vertices: buffer.vertices,
+ indices: buffer.indices,
+ },
+ clip_bounds,
+ transformation: Transformation::IDENTITY,
+ })
+ }
+ Buffer::Gradient(buffer) if !buffer.indices.is_empty() => {
+ Some(Mesh::Gradient {
+ buffers: mesh::Indexed {
+ vertices: buffer.vertices,
+ indices: buffer.indices,
+ },
+ clip_bounds,
+ transformation: Transformation::IDENTITY,
+ })
+ }
+ _ => None,
+ })
+ }
}
#[derive(Debug)]
@@ -591,7 +620,9 @@ pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path {
let mut draw_line = false;
walk_along_path(
- path.raw().iter().flattened(0.01),
+ path.raw().iter().flattened(
+ lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
+ ),
0.0,
lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
&mut RepeatedPattern {
diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs
new file mode 100644
index 00000000..32118ed6
--- /dev/null
+++ b/wgpu/src/image/cache.rs
@@ -0,0 +1,107 @@
+use crate::core::{self, Size};
+use crate::image::atlas::{self, Atlas};
+
+use std::cell::{RefCell, RefMut};
+use std::rc::Rc;
+
+#[derive(Debug)]
+pub struct Cache {
+ atlas: Atlas,
+ #[cfg(feature = "image")]
+ raster: crate::image::raster::Cache,
+ #[cfg(feature = "svg")]
+ vector: crate::image::vector::Cache,
+}
+
+impl Cache {
+ pub fn new(device: &wgpu::Device, backend: wgpu::Backend) -> Self {
+ Self {
+ atlas: Atlas::new(device, backend),
+ #[cfg(feature = "image")]
+ raster: crate::image::raster::Cache::default(),
+ #[cfg(feature = "svg")]
+ vector: crate::image::vector::Cache::default(),
+ }
+ }
+
+ pub fn layer_count(&self) -> usize {
+ self.atlas.layer_count()
+ }
+
+ #[cfg(feature = "image")]
+ pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size<u32> {
+ self.raster.load(handle).dimensions()
+ }
+
+ #[cfg(feature = "svg")]
+ pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> {
+ self.vector.load(handle).viewport_dimensions()
+ }
+
+ #[cfg(feature = "image")]
+ pub fn upload_raster(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ handle: &core::image::Handle,
+ ) -> Option<&atlas::Entry> {
+ self.raster.upload(device, encoder, handle, &mut self.atlas)
+ }
+
+ #[cfg(feature = "svg")]
+ pub fn upload_vector(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ handle: &core::svg::Handle,
+ color: Option<core::Color>,
+ size: [f32; 2],
+ scale: f32,
+ ) -> Option<&atlas::Entry> {
+ self.vector.upload(
+ device,
+ encoder,
+ handle,
+ color,
+ size,
+ scale,
+ &mut self.atlas,
+ )
+ }
+
+ pub fn create_bind_group(
+ &self,
+ device: &wgpu::Device,
+ layout: &wgpu::BindGroupLayout,
+ ) -> wgpu::BindGroup {
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image texture atlas bind group"),
+ layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(self.atlas.view()),
+ }],
+ })
+ }
+
+ pub fn trim(&mut self) {
+ #[cfg(feature = "image")]
+ self.raster.trim(&mut self.atlas);
+
+ #[cfg(feature = "svg")]
+ self.vector.trim(&mut self.atlas);
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Shared(Rc<RefCell<Cache>>);
+
+impl Shared {
+ pub fn new(cache: Cache) -> Self {
+ Self(Rc::new(RefCell::new(cache)))
+ }
+
+ pub fn lock(&self) -> RefMut<'_, Cache> {
+ self.0.borrow_mut()
+ }
+}
diff --git a/wgpu/src/image.rs b/wgpu/src/image/mod.rs
index 067b77ab..86731cbf 100644
--- a/wgpu/src/image.rs
+++ b/wgpu/src/image/mod.rs
@@ -1,3 +1,6 @@
+pub(crate) mod cache;
+pub(crate) use cache::Cache;
+
mod atlas;
#[cfg(feature = "image")]
@@ -6,183 +9,30 @@ mod raster;
#[cfg(feature = "svg")]
mod vector;
-use atlas::Atlas;
-
use crate::core::{Rectangle, Size, Transformation};
-use crate::layer;
use crate::Buffer;
-use std::cell::RefCell;
-use std::mem;
-
use bytemuck::{Pod, Zeroable};
+use std::mem;
-#[cfg(feature = "image")]
-use crate::core::image;
-
-#[cfg(feature = "svg")]
-use crate::core::svg;
+pub use crate::graphics::Image;
-#[cfg(feature = "tracing")]
-use tracing::info_span;
+pub type Batch = Vec<Image>;
#[derive(Debug)]
pub struct Pipeline {
- #[cfg(feature = "image")]
- raster_cache: RefCell<raster::Cache>,
- #[cfg(feature = "svg")]
- vector_cache: RefCell<vector::Cache>,
-
pipeline: wgpu::RenderPipeline,
nearest_sampler: wgpu::Sampler,
linear_sampler: wgpu::Sampler,
texture: wgpu::BindGroup,
texture_version: usize,
- texture_atlas: Atlas,
texture_layout: wgpu::BindGroupLayout,
constant_layout: wgpu::BindGroupLayout,
-
+ cache: cache::Shared,
layers: Vec<Layer>,
prepare_layer: usize,
}
-#[derive(Debug)]
-struct Layer {
- uniforms: wgpu::Buffer,
- nearest: Data,
- linear: Data,
-}
-
-impl Layer {
- fn new(
- device: &wgpu::Device,
- constant_layout: &wgpu::BindGroupLayout,
- nearest_sampler: &wgpu::Sampler,
- linear_sampler: &wgpu::Sampler,
- ) -> Self {
- let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("iced_wgpu::image uniforms buffer"),
- size: mem::size_of::<Uniforms>() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
- let nearest =
- Data::new(device, constant_layout, nearest_sampler, &uniforms);
-
- let linear =
- Data::new(device, constant_layout, linear_sampler, &uniforms);
-
- Self {
- uniforms,
- nearest,
- linear,
- }
- }
-
- fn prepare(
- &mut self,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- nearest_instances: &[Instance],
- linear_instances: &[Instance],
- transformation: Transformation,
- ) {
- queue.write_buffer(
- &self.uniforms,
- 0,
- bytemuck::bytes_of(&Uniforms {
- transform: transformation.into(),
- }),
- );
-
- self.nearest.upload(device, queue, nearest_instances);
- self.linear.upload(device, queue, linear_instances);
- }
-
- fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
- self.nearest.render(render_pass);
- self.linear.render(render_pass);
- }
-}
-
-#[derive(Debug)]
-struct Data {
- constants: wgpu::BindGroup,
- instances: Buffer<Instance>,
- instance_count: usize,
-}
-
-impl Data {
- pub fn new(
- device: &wgpu::Device,
- constant_layout: &wgpu::BindGroupLayout,
- sampler: &wgpu::Sampler,
- uniforms: &wgpu::Buffer,
- ) -> Self {
- let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::image constants bind group"),
- layout: constant_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Buffer(
- wgpu::BufferBinding {
- buffer: uniforms,
- offset: 0,
- size: None,
- },
- ),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::Sampler(sampler),
- },
- ],
- });
-
- let instances = Buffer::new(
- device,
- "iced_wgpu::image instance buffer",
- Instance::INITIAL,
- wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
- );
-
- Self {
- constants,
- instances,
- instance_count: 0,
- }
- }
-
- fn upload(
- &mut self,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- instances: &[Instance],
- ) {
- self.instance_count = instances.len();
-
- if self.instance_count == 0 {
- return;
- }
-
- let _ = self.instances.resize(device, instances.len());
- let _ = self.instances.write(queue, 0, instances);
- }
-
- fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
- if self.instance_count == 0 {
- return;
- }
-
- render_pass.set_bind_group(0, &self.constants, &[]);
- render_pass.set_vertex_buffer(0, self.instances.slice(..));
-
- render_pass.draw(0..6, 0..self.instance_count as u32);
- }
-}
-
impl Pipeline {
pub fn new(
device: &wgpu::Device,
@@ -265,9 +115,9 @@ impl Pipeline {
label: Some("iced_wgpu image shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
concat!(
- include_str!("shader/vertex.wgsl"),
+ include_str!("../shader/vertex.wgsl"),
"\n",
- include_str!("shader/image.wgsl"),
+ include_str!("../shader/image.wgsl"),
),
)),
});
@@ -330,126 +180,82 @@ impl Pipeline {
multiview: None,
});
- let texture_atlas = Atlas::new(device, backend);
-
- let texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::image texture atlas bind group"),
- layout: &texture_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(
- texture_atlas.view(),
- ),
- }],
- });
+ let cache = Cache::new(device, backend);
+ let texture = cache.create_bind_group(device, &texture_layout);
Pipeline {
- #[cfg(feature = "image")]
- raster_cache: RefCell::new(raster::Cache::default()),
-
- #[cfg(feature = "svg")]
- vector_cache: RefCell::new(vector::Cache::default()),
-
pipeline,
nearest_sampler,
linear_sampler,
texture,
- texture_version: texture_atlas.layer_count(),
- texture_atlas,
+ texture_version: cache.layer_count(),
texture_layout,
constant_layout,
-
+ cache: cache::Shared::new(cache),
layers: Vec::new(),
prepare_layer: 0,
}
}
- #[cfg(feature = "image")]
- pub fn dimensions(&self, handle: &image::Handle) -> Size<u32> {
- let mut cache = self.raster_cache.borrow_mut();
- let memory = cache.load(handle);
-
- memory.dimensions()
- }
-
- #[cfg(feature = "svg")]
- pub fn viewport_dimensions(&self, handle: &svg::Handle) -> Size<u32> {
- let mut cache = self.vector_cache.borrow_mut();
- let svg = cache.load(handle);
-
- svg.viewport_dimensions()
+ pub fn cache(&self) -> &cache::Shared {
+ &self.cache
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
- queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
- images: &[layer::Image],
+ belt: &mut wgpu::util::StagingBelt,
+ images: &Batch,
transformation: Transformation,
- _scale: f32,
+ scale: f32,
) {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Image", "PREPARE").entered();
-
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Image", "DRAW").entered();
+ let transformation = transformation * Transformation::scale(scale);
let nearest_instances: &mut Vec<Instance> = &mut Vec::new();
let linear_instances: &mut Vec<Instance> = &mut Vec::new();
- #[cfg(feature = "image")]
- let mut raster_cache = self.raster_cache.borrow_mut();
-
- #[cfg(feature = "svg")]
- let mut vector_cache = self.vector_cache.borrow_mut();
+ let mut cache = self.cache.lock();
for image in images {
match &image {
#[cfg(feature = "image")]
- layer::Image::Raster {
+ Image::Raster {
handle,
filter_method,
bounds,
} => {
- if let Some(atlas_entry) = raster_cache.upload(
- device,
- encoder,
- handle,
- &mut self.texture_atlas,
- ) {
+ if let Some(atlas_entry) =
+ cache.upload_raster(device, encoder, handle)
+ {
add_instances(
[bounds.x, bounds.y],
[bounds.width, bounds.height],
atlas_entry,
match filter_method {
- image::FilterMethod::Nearest => {
+ crate::core::image::FilterMethod::Nearest => {
nearest_instances
}
- image::FilterMethod::Linear => linear_instances,
+ crate::core::image::FilterMethod::Linear => {
+ linear_instances
+ }
},
);
}
}
#[cfg(not(feature = "image"))]
- layer::Image::Raster { .. } => {}
+ Image::Raster { .. } => {}
#[cfg(feature = "svg")]
- layer::Image::Vector {
+ Image::Vector {
handle,
color,
bounds,
} => {
let size = [bounds.width, bounds.height];
- if let Some(atlas_entry) = vector_cache.upload(
- device,
- encoder,
- handle,
- *color,
- size,
- _scale,
- &mut self.texture_atlas,
+ if let Some(atlas_entry) = cache.upload_vector(
+ device, encoder, handle, *color, size, scale,
) {
add_instances(
[bounds.x, bounds.y],
@@ -460,7 +266,7 @@ impl Pipeline {
}
}
#[cfg(not(feature = "svg"))]
- layer::Image::Vector { .. } => {}
+ Image::Vector { .. } => {}
}
}
@@ -468,23 +274,13 @@ impl Pipeline {
return;
}
- let texture_version = self.texture_atlas.layer_count();
+ let texture_version = cache.layer_count();
if self.texture_version != texture_version {
log::info!("Atlas has grown. Recreating bind group...");
self.texture =
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("iced_wgpu::image texture atlas bind group"),
- layout: &self.texture_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(
- self.texture_atlas.view(),
- ),
- }],
- });
-
+ cache.create_bind_group(device, &self.texture_layout);
self.texture_version = texture_version;
}
@@ -501,7 +297,8 @@ impl Pipeline {
layer.prepare(
device,
- queue,
+ encoder,
+ belt,
nearest_instances,
linear_instances,
transformation,
@@ -533,13 +330,156 @@ impl Pipeline {
}
pub fn end_frame(&mut self) {
- #[cfg(feature = "image")]
- self.raster_cache.borrow_mut().trim(&mut self.texture_atlas);
+ self.cache.lock().trim();
+ self.prepare_layer = 0;
+ }
+}
+
+#[derive(Debug)]
+struct Layer {
+ uniforms: wgpu::Buffer,
+ nearest: Data,
+ linear: Data,
+}
- #[cfg(feature = "svg")]
- self.vector_cache.borrow_mut().trim(&mut self.texture_atlas);
+impl Layer {
+ fn new(
+ device: &wgpu::Device,
+ constant_layout: &wgpu::BindGroupLayout,
+ nearest_sampler: &wgpu::Sampler,
+ linear_sampler: &wgpu::Sampler,
+ ) -> Self {
+ let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::image uniforms buffer"),
+ size: mem::size_of::<Uniforms>() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
- self.prepare_layer = 0;
+ let nearest =
+ Data::new(device, constant_layout, nearest_sampler, &uniforms);
+
+ let linear =
+ Data::new(device, constant_layout, linear_sampler, &uniforms);
+
+ Self {
+ uniforms,
+ nearest,
+ linear,
+ }
+ }
+
+ fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ nearest_instances: &[Instance],
+ linear_instances: &[Instance],
+ transformation: Transformation,
+ ) {
+ let uniforms = Uniforms {
+ transform: transformation.into(),
+ };
+
+ let bytes = bytemuck::bytes_of(&uniforms);
+
+ belt.write_buffer(
+ encoder,
+ &self.uniforms,
+ 0,
+ (bytes.len() as u64).try_into().expect("Sized uniforms"),
+ device,
+ )
+ .copy_from_slice(bytes);
+
+ self.nearest
+ .upload(device, encoder, belt, nearest_instances);
+
+ self.linear.upload(device, encoder, belt, linear_instances);
+ }
+
+ fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
+ self.nearest.render(render_pass);
+ self.linear.render(render_pass);
+ }
+}
+
+#[derive(Debug)]
+struct Data {
+ constants: wgpu::BindGroup,
+ instances: Buffer<Instance>,
+ instance_count: usize,
+}
+
+impl Data {
+ pub fn new(
+ device: &wgpu::Device,
+ constant_layout: &wgpu::BindGroupLayout,
+ sampler: &wgpu::Sampler,
+ uniforms: &wgpu::Buffer,
+ ) -> Self {
+ let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::image constants bind group"),
+ layout: constant_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Buffer(
+ wgpu::BufferBinding {
+ buffer: uniforms,
+ offset: 0,
+ size: None,
+ },
+ ),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(sampler),
+ },
+ ],
+ });
+
+ let instances = Buffer::new(
+ device,
+ "iced_wgpu::image instance buffer",
+ Instance::INITIAL,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ Self {
+ constants,
+ instances,
+ instance_count: 0,
+ }
+ }
+
+ fn upload(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ instances: &[Instance],
+ ) {
+ self.instance_count = instances.len();
+
+ if self.instance_count == 0 {
+ return;
+ }
+
+ let _ = self.instances.resize(device, instances.len());
+ let _ = self.instances.write(device, encoder, belt, 0, instances);
+ }
+
+ fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
+ if self.instance_count == 0 {
+ return;
+ }
+
+ render_pass.set_bind_group(0, &self.constants, &[]);
+ render_pass.set_vertex_buffer(0, self.instances.slice(..));
+
+ render_pass.draw(0..6, 0..self.instance_count as u32);
}
}
diff --git a/wgpu/src/image/null.rs b/wgpu/src/image/null.rs
new file mode 100644
index 00000000..c06d56be
--- /dev/null
+++ b/wgpu/src/image/null.rs
@@ -0,0 +1,10 @@
+pub use crate::graphics::Image;
+
+#[derive(Debug, Default)]
+pub struct Batch;
+
+impl Batch {
+ pub fn push(&mut self, _image: Image) {}
+
+ pub fn clear(&mut self) {}
+}
diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs
index a6cba76a..441b294f 100644
--- a/wgpu/src/image/raster.rs
+++ b/wgpu/src/image/raster.rs
@@ -4,7 +4,7 @@ use crate::graphics;
use crate::graphics::image::image_rs;
use crate::image::atlas::{self, Atlas};
-use std::collections::{HashMap, HashSet};
+use rustc_hash::{FxHashMap, FxHashSet};
/// Entry in cache corresponding to an image handle
#[derive(Debug)]
@@ -38,8 +38,8 @@ impl Memory {
/// Caches image raster data
#[derive(Debug, Default)]
pub struct Cache {
- map: HashMap<u64, Memory>,
- hits: HashSet<u64>,
+ map: FxHashMap<u64, Memory>,
+ hits: FxHashSet<u64>,
}
impl Cache {
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index d9be50d7..d681b2e6 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -5,7 +5,7 @@ use crate::image::atlas::{self, Atlas};
use resvg::tiny_skia;
use resvg::usvg::{self, TreeTextToPath};
-use std::collections::{HashMap, HashSet};
+use rustc_hash::{FxHashMap, FxHashSet};
use std::fs;
/// Entry in cache corresponding to an svg handle
@@ -33,10 +33,10 @@ impl Svg {
/// Caches svg vector and raster data
#[derive(Debug, Default)]
pub struct Cache {
- svgs: HashMap<u64, Svg>,
- rasterized: HashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
- svg_hits: HashSet<u64>,
- rasterized_hits: HashSet<(u64, u32, u32, ColorFilter)>,
+ svgs: FxHashMap<u64, Svg>,
+ rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
+ svg_hits: FxHashSet<u64>,
+ rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
}
type ColorFilter = Option<[u8; 4]>;
diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs
index cc767c25..9526c5a8 100644
--- a/wgpu/src/layer.rs
+++ b/wgpu/src/layer.rs
@@ -1,343 +1,293 @@
-//! Organize rendering primitives into a flattened list of layers.
-mod image;
-mod pipeline;
-mod text;
-
-pub mod mesh;
-
-pub use image::Image;
-pub use mesh::Mesh;
-pub use pipeline::Pipeline;
-pub use text::Text;
-
-use crate::core;
-use crate::core::alignment;
-use crate::core::{
- Color, Font, Pixels, Point, Rectangle, Size, Transformation, Vector,
-};
+use crate::core::renderer;
+use crate::core::{Background, Color, Point, Rectangle, Transformation};
use crate::graphics;
use crate::graphics::color;
-use crate::graphics::Viewport;
+use crate::graphics::layer;
+use crate::graphics::text::{Editor, Paragraph};
+use crate::graphics::Mesh;
+use crate::image::{self, Image};
use crate::primitive::{self, Primitive};
use crate::quad::{self, Quad};
+use crate::text::{self, Text};
+use crate::triangle;
+
+pub type Stack = layer::Stack<Layer>;
-/// A group of primitives that should be clipped together.
#[derive(Debug)]
-pub struct Layer<'a> {
- /// The clipping bounds of the [`Layer`].
+pub struct Layer {
pub bounds: Rectangle,
-
- /// The quads of the [`Layer`].
pub quads: quad::Batch,
+ pub triangles: triangle::Batch,
+ pub primitives: primitive::Batch,
+ pub text: text::Batch,
+ pub images: image::Batch,
+ pending_meshes: Vec<Mesh>,
+ pending_text: Vec<Text>,
+}
- /// The triangle meshes of the [`Layer`].
- pub meshes: Vec<Mesh<'a>>,
-
- /// The text of the [`Layer`].
- pub text: Vec<Text<'a>>,
+impl Layer {
+ pub fn draw_quad(
+ &mut self,
+ quad: renderer::Quad,
+ background: Background,
+ transformation: Transformation,
+ ) {
+ let bounds = quad.bounds * transformation;
+
+ let quad = Quad {
+ position: [bounds.x, bounds.y],
+ size: [bounds.width, bounds.height],
+ border_color: color::pack(quad.border.color),
+ border_radius: quad.border.radius.into(),
+ border_width: quad.border.width,
+ shadow_color: color::pack(quad.shadow.color),
+ shadow_offset: quad.shadow.offset.into(),
+ shadow_blur_radius: quad.shadow.blur_radius,
+ };
+
+ self.quads.add(quad, &background);
+ }
- /// The images of the [`Layer`].
- pub images: Vec<Image>,
+ pub fn draw_paragraph(
+ &mut self,
+ paragraph: &Paragraph,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let paragraph = Text::Paragraph {
+ paragraph: paragraph.downgrade(),
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ };
+
+ self.pending_text.push(paragraph);
+ }
- /// The custom pipelines of this [`Layer`].
- pub pipelines: Vec<Pipeline>,
-}
+ pub fn draw_editor(
+ &mut self,
+ editor: &Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let editor = Text::Editor {
+ editor: editor.downgrade(),
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ };
+
+ self.pending_text.push(editor);
+ }
-impl<'a> Layer<'a> {
- /// Creates a new [`Layer`] with the given clipping bounds.
- pub fn new(bounds: Rectangle) -> Self {
- Self {
- bounds,
- quads: quad::Batch::default(),
- meshes: Vec::new(),
- text: Vec::new(),
- images: Vec::new(),
- pipelines: Vec::new(),
- }
+ pub fn draw_text(
+ &mut self,
+ text: crate::core::Text,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let text = Text::Cached {
+ content: text.content,
+ bounds: Rectangle::new(position, text.bounds) * transformation,
+ color,
+ size: text.size * transformation.scale_factor(),
+ line_height: text.line_height.to_absolute(text.size)
+ * transformation.scale_factor(),
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ clip_bounds: clip_bounds * transformation,
+ };
+
+ self.pending_text.push(text);
}
- /// Creates a new [`Layer`] for the provided overlay text.
- ///
- /// This can be useful for displaying debug information.
- pub fn overlay(lines: &'a [impl AsRef<str>], viewport: &Viewport) -> Self {
- let mut overlay =
- Layer::new(Rectangle::with_size(viewport.logical_size()));
-
- for (i, line) in lines.iter().enumerate() {
- let text = text::Cached {
- content: line.as_ref(),
- bounds: Rectangle::new(
- Point::new(11.0, 11.0 + 25.0 * i as f32),
- Size::INFINITY,
- ),
- color: Color::new(0.9, 0.9, 0.9, 1.0),
- size: Pixels(20.0),
- line_height: core::text::LineHeight::default(),
- font: Font::MONOSPACE,
- horizontal_alignment: alignment::Horizontal::Left,
- vertical_alignment: alignment::Vertical::Top,
- shaping: core::text::Shaping::Basic,
- clip_bounds: Rectangle::with_size(Size::INFINITY),
- };
-
- overlay.text.push(Text::Cached(text.clone()));
-
- overlay.text.push(Text::Cached(text::Cached {
- bounds: text.bounds + Vector::new(-1.0, -1.0),
- color: Color::BLACK,
- ..text
- }));
- }
+ pub fn draw_image(
+ &mut self,
+ handle: crate::core::image::Handle,
+ filter_method: crate::core::image::FilterMethod,
+ bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let image = Image::Raster {
+ handle,
+ filter_method,
+ bounds: bounds * transformation,
+ };
- overlay
+ self.images.push(image);
}
- /// Distributes the given [`Primitive`] and generates a list of layers based
- /// on its contents.
- pub fn generate(
- primitives: &'a [Primitive],
- viewport: &Viewport,
- ) -> Vec<Self> {
- let first_layer =
- Layer::new(Rectangle::with_size(viewport.logical_size()));
-
- let mut layers = vec![first_layer];
-
- for primitive in primitives {
- Self::process_primitive(
- &mut layers,
- Transformation::IDENTITY,
- primitive,
- 0,
- );
- }
+ pub fn draw_svg(
+ &mut self,
+ handle: crate::core::svg::Handle,
+ color: Option<Color>,
+ bounds: Rectangle,
+ transformation: Transformation,
+ ) {
+ let svg = Image::Vector {
+ handle,
+ color,
+ bounds: bounds * transformation,
+ };
- layers
+ self.images.push(svg);
}
- fn process_primitive(
- layers: &mut Vec<Self>,
+ pub fn draw_mesh(
+ &mut self,
+ mut mesh: Mesh,
transformation: Transformation,
- primitive: &'a Primitive,
- current_layer: usize,
) {
- match primitive {
- Primitive::Paragraph {
- paragraph,
- position,
- color,
- clip_bounds,
- } => {
- let layer = &mut layers[current_layer];
-
- layer.text.push(Text::Paragraph {
- paragraph: paragraph.clone(),
- position: *position,
- color: *color,
- clip_bounds: *clip_bounds,
- transformation,
- });
- }
- Primitive::Editor {
- editor,
- position,
- color,
- clip_bounds,
- } => {
- let layer = &mut layers[current_layer];
-
- layer.text.push(Text::Editor {
- editor: editor.clone(),
- position: *position,
- color: *color,
- clip_bounds: *clip_bounds,
- transformation,
- });
+ match &mut mesh {
+ Mesh::Solid {
+ transformation: local_transformation,
+ ..
}
- Primitive::Text {
- content,
- bounds,
- size,
- line_height,
- color,
- font,
- horizontal_alignment,
- vertical_alignment,
- shaping,
- clip_bounds,
+ | Mesh::Gradient {
+ transformation: local_transformation,
+ ..
} => {
- let layer = &mut layers[current_layer];
-
- layer.text.push(Text::Cached(text::Cached {
- content,
- bounds: *bounds + transformation.translation(),
- size: *size * transformation.scale_factor(),
- line_height: *line_height,
- color: *color,
- font: *font,
- horizontal_alignment: *horizontal_alignment,
- vertical_alignment: *vertical_alignment,
- shaping: *shaping,
- clip_bounds: *clip_bounds * transformation,
- }));
+ *local_transformation = *local_transformation * transformation;
}
- graphics::Primitive::RawText(raw) => {
- let layer = &mut layers[current_layer];
+ }
- layer.text.push(Text::Raw {
- raw: raw.clone(),
- transformation,
- });
- }
- Primitive::Quad {
- bounds,
- background,
- border,
- shadow,
- } => {
- let layer = &mut layers[current_layer];
- let bounds = *bounds * transformation;
-
- let quad = Quad {
- position: [bounds.x, bounds.y],
- size: [bounds.width, bounds.height],
- border_color: color::pack(border.color),
- border_radius: border.radius.into(),
- border_width: border.width,
- shadow_color: shadow.color.into_linear(),
- shadow_offset: shadow.offset.into(),
- shadow_blur_radius: shadow.blur_radius,
- };
-
- layer.quads.add(quad, background);
- }
- Primitive::Image {
- handle,
- filter_method,
- bounds,
- } => {
- let layer = &mut layers[current_layer];
+ self.pending_meshes.push(mesh);
+ }
- layer.images.push(Image::Raster {
- handle: handle.clone(),
- filter_method: *filter_method,
- bounds: *bounds * transformation,
- });
- }
- Primitive::Svg {
- handle,
- color,
- bounds,
- } => {
- let layer = &mut layers[current_layer];
+ pub fn draw_mesh_group(
+ &mut self,
+ meshes: Vec<Mesh>,
+ transformation: Transformation,
+ ) {
+ self.flush_meshes();
- layer.images.push(Image::Vector {
- handle: handle.clone(),
- color: *color,
- bounds: *bounds * transformation,
- });
- }
- Primitive::Group { primitives } => {
- // TODO: Inspect a bit and regroup (?)
- for primitive in primitives {
- Self::process_primitive(
- layers,
- transformation,
- primitive,
- current_layer,
- );
- }
- }
- Primitive::Clip { bounds, content } => {
- let layer = &mut layers[current_layer];
- let translated_bounds = *bounds * transformation;
-
- // Only draw visible content
- if let Some(clip_bounds) =
- layer.bounds.intersection(&translated_bounds)
- {
- let clip_layer = Layer::new(clip_bounds);
- layers.push(clip_layer);
-
- Self::process_primitive(
- layers,
- transformation,
- content,
- layers.len() - 1,
- );
- }
- }
- Primitive::Transform {
- transformation: new_transformation,
- content,
- } => {
- Self::process_primitive(
- layers,
- transformation * *new_transformation,
- content,
- current_layer,
- );
- }
- Primitive::Cache { content } => {
- Self::process_primitive(
- layers,
- transformation,
- content,
- current_layer,
- );
- }
- Primitive::Custom(custom) => match custom {
- primitive::Custom::Mesh(mesh) => match mesh {
- graphics::Mesh::Solid { buffers, size } => {
- let layer = &mut layers[current_layer];
-
- let bounds =
- Rectangle::with_size(*size) * transformation;
-
- // Only draw visible content
- if let Some(clip_bounds) =
- layer.bounds.intersection(&bounds)
- {
- layer.meshes.push(Mesh::Solid {
- transformation,
- buffers,
- clip_bounds,
- });
- }
- }
- graphics::Mesh::Gradient { buffers, size } => {
- let layer = &mut layers[current_layer];
-
- let bounds =
- Rectangle::with_size(*size) * transformation;
-
- // Only draw visible content
- if let Some(clip_bounds) =
- layer.bounds.intersection(&bounds)
- {
- layer.meshes.push(Mesh::Gradient {
- transformation,
- buffers,
- clip_bounds,
- });
- }
- }
- },
- primitive::Custom::Pipeline(pipeline) => {
- let layer = &mut layers[current_layer];
- let bounds = pipeline.bounds * transformation;
-
- if let Some(clip_bounds) =
- layer.bounds.intersection(&bounds)
- {
- layer.pipelines.push(Pipeline {
- bounds,
- viewport: clip_bounds,
- primitive: pipeline.primitive.clone(),
- });
- }
- }
- },
+ self.triangles.push(triangle::Item::Group {
+ meshes,
+ transformation,
+ });
+ }
+
+ pub fn draw_mesh_cache(
+ &mut self,
+ cache: triangle::Cache,
+ transformation: Transformation,
+ ) {
+ self.flush_meshes();
+
+ self.triangles.push(triangle::Item::Cached {
+ cache,
+ transformation,
+ });
+ }
+
+ pub fn draw_text_group(
+ &mut self,
+ text: Vec<Text>,
+ transformation: Transformation,
+ ) {
+ self.flush_text();
+
+ self.text.push(text::Item::Group {
+ text,
+ transformation,
+ });
+ }
+
+ pub fn draw_text_cache(
+ &mut self,
+ cache: text::Cache,
+ transformation: Transformation,
+ ) {
+ self.flush_text();
+
+ self.text.push(text::Item::Cached {
+ cache,
+ transformation,
+ });
+ }
+
+ pub fn draw_primitive(
+ &mut self,
+ bounds: Rectangle,
+ primitive: Box<dyn Primitive>,
+ transformation: Transformation,
+ ) {
+ let bounds = bounds * transformation;
+
+ self.primitives
+ .push(primitive::Instance { bounds, primitive });
+ }
+
+ fn flush_meshes(&mut self) {
+ if !self.pending_meshes.is_empty() {
+ self.triangles.push(triangle::Item::Group {
+ transformation: Transformation::IDENTITY,
+ meshes: self.pending_meshes.drain(..).collect(),
+ });
+ }
+ }
+
+ fn flush_text(&mut self) {
+ if !self.pending_text.is_empty() {
+ self.text.push(text::Item::Group {
+ transformation: Transformation::IDENTITY,
+ text: self.pending_text.drain(..).collect(),
+ });
+ }
+ }
+}
+
+impl graphics::Layer for Layer {
+ fn with_bounds(bounds: Rectangle) -> Self {
+ Self {
+ bounds,
+ ..Self::default()
+ }
+ }
+
+ fn flush(&mut self) {
+ self.flush_meshes();
+ self.flush_text();
+ }
+
+ fn resize(&mut self, bounds: Rectangle) {
+ self.bounds = bounds;
+ }
+
+ fn reset(&mut self) {
+ self.bounds = Rectangle::INFINITE;
+
+ self.quads.clear();
+ self.triangles.clear();
+ self.primitives.clear();
+ self.text.clear();
+ self.images.clear();
+ self.pending_meshes.clear();
+ self.pending_text.clear();
+ }
+}
+
+impl Default for Layer {
+ fn default() -> Self {
+ Self {
+ bounds: Rectangle::INFINITE,
+ quads: quad::Batch::default(),
+ triangles: triangle::Batch::default(),
+ primitives: primitive::Batch::default(),
+ text: text::Batch::default(),
+ images: image::Batch::default(),
+ pending_meshes: Vec::new(),
+ pending_text: Vec::new(),
}
}
}
diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs
deleted file mode 100644
index facbe192..00000000
--- a/wgpu/src/layer/image.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use crate::core::image;
-use crate::core::svg;
-use crate::core::{Color, Rectangle};
-
-/// A raster or vector image.
-#[derive(Debug, Clone)]
-pub enum Image {
- /// A raster image.
- Raster {
- /// The handle of a raster image.
- handle: image::Handle,
-
- /// The filter method of a raster image.
- filter_method: image::FilterMethod,
-
- /// The bounds of the image.
- bounds: Rectangle,
- },
- /// A vector image.
- Vector {
- /// The handle of a vector image.
- handle: svg::Handle,
-
- /// The [`Color`] filter
- color: Option<Color>,
-
- /// The bounds of the image.
- bounds: Rectangle,
- },
-}
diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs
deleted file mode 100644
index 5ed7c654..00000000
--- a/wgpu/src/layer/mesh.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-//! A collection of triangle primitives.
-use crate::core::{Rectangle, Transformation};
-use crate::graphics::mesh;
-
-/// A mesh of triangles.
-#[derive(Debug, Clone, Copy)]
-pub enum Mesh<'a> {
- /// A mesh of triangles with a solid color.
- Solid {
- /// The [`Transformation`] for the vertices of the [`Mesh`].
- transformation: Transformation,
-
- /// The vertex and index buffers of the [`Mesh`].
- buffers: &'a mesh::Indexed<mesh::SolidVertex2D>,
-
- /// The clipping bounds of the [`Mesh`].
- clip_bounds: Rectangle<f32>,
- },
- /// A mesh of triangles with a gradient color.
- Gradient {
- /// The [`Transformation`] for the vertices of the [`Mesh`].
- transformation: Transformation,
-
- /// The vertex and index buffers of the [`Mesh`].
- buffers: &'a mesh::Indexed<mesh::GradientVertex2D>,
-
- /// The clipping bounds of the [`Mesh`].
- clip_bounds: Rectangle<f32>,
- },
-}
-
-impl Mesh<'_> {
- /// Returns the origin of the [`Mesh`].
- pub fn transformation(&self) -> Transformation {
- match self {
- Self::Solid { transformation, .. }
- | Self::Gradient { transformation, .. } => *transformation,
- }
- }
-
- /// Returns the indices of the [`Mesh`].
- pub fn indices(&self) -> &[u32] {
- match self {
- Self::Solid { buffers, .. } => &buffers.indices,
- Self::Gradient { buffers, .. } => &buffers.indices,
- }
- }
-
- /// Returns the clip bounds of the [`Mesh`].
- pub fn clip_bounds(&self) -> Rectangle<f32> {
- match self {
- Self::Solid { clip_bounds, .. }
- | Self::Gradient { clip_bounds, .. } => *clip_bounds,
- }
- }
-}
-
-/// The result of counting the attributes of a set of meshes.
-#[derive(Debug, Clone, Copy, Default)]
-pub struct AttributeCount {
- /// The total amount of solid vertices.
- pub solid_vertices: usize,
-
- /// The total amount of solid meshes.
- pub solids: usize,
-
- /// The total amount of gradient vertices.
- pub gradient_vertices: usize,
-
- /// The total amount of gradient meshes.
- pub gradients: usize,
-
- /// The total amount of indices.
- pub indices: usize,
-}
-
-/// Returns the number of total vertices & total indices of all [`Mesh`]es.
-pub fn attribute_count_of<'a>(meshes: &'a [Mesh<'a>]) -> AttributeCount {
- meshes
- .iter()
- .fold(AttributeCount::default(), |mut count, mesh| {
- match mesh {
- Mesh::Solid { buffers, .. } => {
- count.solids += 1;
- count.solid_vertices += buffers.vertices.len();
- count.indices += buffers.indices.len();
- }
- Mesh::Gradient { buffers, .. } => {
- count.gradients += 1;
- count.gradient_vertices += buffers.vertices.len();
- count.indices += buffers.indices.len();
- }
- }
-
- count
- })
-}
diff --git a/wgpu/src/layer/pipeline.rs b/wgpu/src/layer/pipeline.rs
deleted file mode 100644
index 6dfe6750..00000000
--- a/wgpu/src/layer/pipeline.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-use crate::core::Rectangle;
-use crate::primitive::pipeline::Primitive;
-
-use std::sync::Arc;
-
-#[derive(Clone, Debug)]
-/// A custom primitive which can be used to render primitives associated with a custom pipeline.
-pub struct Pipeline {
- /// The bounds of the [`Pipeline`].
- pub bounds: Rectangle,
-
- /// The viewport of the [`Pipeline`].
- pub viewport: Rectangle,
-
- /// The [`Primitive`] to render.
- pub primitive: Arc<dyn Primitive>,
-}
diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs
deleted file mode 100644
index b3a00130..00000000
--- a/wgpu/src/layer/text.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use crate::core::alignment;
-use crate::core::text;
-use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation};
-use crate::graphics;
-use crate::graphics::text::editor;
-use crate::graphics::text::paragraph;
-
-/// A text primitive.
-#[derive(Debug, Clone)]
-pub enum Text<'a> {
- /// A paragraph.
- #[allow(missing_docs)]
- Paragraph {
- paragraph: paragraph::Weak,
- position: Point,
- color: Color,
- clip_bounds: Rectangle,
- transformation: Transformation,
- },
- /// An editor.
- #[allow(missing_docs)]
- Editor {
- editor: editor::Weak,
- position: Point,
- color: Color,
- clip_bounds: Rectangle,
- transformation: Transformation,
- },
- /// Some cached text.
- Cached(Cached<'a>),
- /// Some raw text.
- #[allow(missing_docs)]
- Raw {
- raw: graphics::text::Raw,
- transformation: Transformation,
- },
-}
-
-#[derive(Debug, Clone)]
-pub struct Cached<'a> {
- /// The content of the [`Text`].
- pub content: &'a str,
-
- /// The layout bounds of the [`Text`].
- pub bounds: Rectangle,
-
- /// The color of the [`Text`], in __linear RGB_.
- pub color: Color,
-
- /// The size of the [`Text`] in logical pixels.
- pub size: Pixels,
-
- /// The line height of the [`Text`].
- pub line_height: text::LineHeight,
-
- /// The font of the [`Text`].
- pub font: Font,
-
- /// The horizontal alignment of the [`Text`].
- pub horizontal_alignment: alignment::Horizontal,
-
- /// The vertical alignment of the [`Text`].
- pub vertical_alignment: alignment::Vertical,
-
- /// The shaping strategy of the text.
- pub shaping: text::Shaping,
-
- /// The clip bounds of the text.
- pub clip_bounds: Rectangle,
-}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index b00e5c3c..178522de 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -20,15 +20,8 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
-#![forbid(rust_2018_idioms)]
-#![deny(
- missing_debug_implementations,
- missing_docs,
- unsafe_code,
- unused_results,
- rustdoc::broken_intra_doc_links
-)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+#![allow(missing_docs)]
pub mod layer;
pub mod primitive;
pub mod settings;
@@ -37,13 +30,21 @@ pub mod window;
#[cfg(feature = "geometry")]
pub mod geometry;
-mod backend;
mod buffer;
mod color;
+mod engine;
mod quad;
mod text;
mod triangle;
+#[cfg(any(feature = "image", feature = "svg"))]
+#[path = "image/mod.rs"]
+mod image;
+
+#[cfg(not(any(feature = "image", feature = "svg")))]
+#[path = "image/null.rs"]
+mod image;
+
use buffer::Buffer;
pub use iced_graphics as graphics;
@@ -51,16 +52,538 @@ pub use iced_graphics::core;
pub use wgpu;
-pub use backend::Backend;
+pub use engine::Engine;
pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
-#[cfg(any(feature = "image", feature = "svg"))]
-mod image;
+#[cfg(feature = "geometry")]
+pub use geometry::Geometry;
+
+use crate::core::{
+ Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
+};
+use crate::graphics::text::{Editor, Paragraph};
+use crate::graphics::Viewport;
/// A [`wgpu`] graphics renderer for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
-pub type Renderer = iced_graphics::Renderer<Backend>;
+#[allow(missing_debug_implementations)]
+pub struct Renderer {
+ default_font: Font,
+ default_text_size: Pixels,
+ layers: layer::Stack,
+
+ triangle_storage: triangle::Storage,
+ text_storage: text::Storage,
+
+ // TODO: Centralize all the image feature handling
+ #[cfg(any(feature = "svg", feature = "image"))]
+ image_cache: image::cache::Shared,
+}
+
+impl Renderer {
+ pub fn new(
+ _engine: &Engine,
+ default_font: Font,
+ default_text_size: Pixels,
+ ) -> Self {
+ Self {
+ default_font,
+ default_text_size,
+ layers: layer::Stack::new(),
+
+ triangle_storage: triangle::Storage::new(),
+ text_storage: text::Storage::new(),
+
+ #[cfg(any(feature = "svg", feature = "image"))]
+ image_cache: _engine.image_cache().clone(),
+ }
+ }
+
+ pub fn present<T: AsRef<str>>(
+ &mut self,
+ engine: &mut Engine,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ clear_color: Option<Color>,
+ format: wgpu::TextureFormat,
+ frame: &wgpu::TextureView,
+ viewport: &Viewport,
+ overlay: &[T],
+ ) {
+ self.draw_overlay(overlay, viewport);
+ self.prepare(engine, device, queue, format, encoder, viewport);
+ self.render(engine, encoder, frame, clear_color, viewport);
+
+ self.triangle_storage.trim();
+ self.text_storage.trim();
+ }
+
+ fn prepare(
+ &mut self,
+ engine: &mut Engine,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ _format: wgpu::TextureFormat,
+ encoder: &mut wgpu::CommandEncoder,
+ viewport: &Viewport,
+ ) {
+ let scale_factor = viewport.scale_factor() as f32;
+
+ for layer in self.layers.iter_mut() {
+ if !layer.quads.is_empty() {
+ engine.quad_pipeline.prepare(
+ device,
+ encoder,
+ &mut engine.staging_belt,
+ &layer.quads,
+ viewport.projection(),
+ scale_factor,
+ );
+ }
+
+ if !layer.triangles.is_empty() {
+ engine.triangle_pipeline.prepare(
+ device,
+ encoder,
+ &mut engine.staging_belt,
+ &mut self.triangle_storage,
+ &layer.triangles,
+ Transformation::scale(scale_factor),
+ viewport.physical_size(),
+ );
+ }
+
+ if !layer.primitives.is_empty() {
+ for instance in &layer.primitives {
+ instance.primitive.prepare(
+ device,
+ queue,
+ engine.format,
+ &mut engine.primitive_storage,
+ &instance.bounds,
+ viewport,
+ );
+ }
+ }
+
+ if !layer.text.is_empty() {
+ engine.text_pipeline.prepare(
+ device,
+ queue,
+ encoder,
+ &mut self.text_storage,
+ &layer.text,
+ layer.bounds,
+ Transformation::scale(scale_factor),
+ viewport.physical_size(),
+ );
+ }
+
+ #[cfg(any(feature = "svg", feature = "image"))]
+ if !layer.images.is_empty() {
+ engine.image_pipeline.prepare(
+ device,
+ encoder,
+ &mut engine.staging_belt,
+ &layer.images,
+ viewport.projection(),
+ scale_factor,
+ );
+ }
+ }
+ }
+
+ fn render(
+ &mut self,
+ engine: &mut Engine,
+ encoder: &mut wgpu::CommandEncoder,
+ frame: &wgpu::TextureView,
+ clear_color: Option<Color>,
+ viewport: &Viewport,
+ ) {
+ use std::mem::ManuallyDrop;
+
+ let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu render pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: frame,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: match clear_color {
+ Some(background_color) => wgpu::LoadOp::Clear({
+ let [r, g, b, a] =
+ graphics::color::pack(background_color)
+ .components();
+
+ wgpu::Color {
+ r: f64::from(r),
+ g: f64::from(g),
+ b: f64::from(b),
+ a: f64::from(a),
+ }
+ }),
+ None => wgpu::LoadOp::Load,
+ },
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ },
+ ));
+
+ let mut quad_layer = 0;
+ let mut mesh_layer = 0;
+ let mut text_layer = 0;
+
+ #[cfg(any(feature = "svg", feature = "image"))]
+ let mut image_layer = 0;
+
+ let scale_factor = viewport.scale_factor() as f32;
+ let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
+ viewport.physical_size(),
+ ));
+
+ let scale = Transformation::scale(scale_factor);
+
+ for layer in self.layers.iter() {
+ let Some(physical_bounds) =
+ physical_bounds.intersection(&(layer.bounds * scale))
+ else {
+ continue;
+ };
+
+ let Some(scissor_rect) = physical_bounds.snap() else {
+ continue;
+ };
+
+ if !layer.quads.is_empty() {
+ engine.quad_pipeline.render(
+ quad_layer,
+ scissor_rect,
+ &layer.quads,
+ &mut render_pass,
+ );
+
+ quad_layer += 1;
+ }
+
+ if !layer.triangles.is_empty() {
+ let _ = ManuallyDrop::into_inner(render_pass);
+
+ mesh_layer += engine.triangle_pipeline.render(
+ encoder,
+ frame,
+ &self.triangle_storage,
+ mesh_layer,
+ &layer.triangles,
+ physical_bounds,
+ scale,
+ );
+
+ render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu render pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: frame,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ },
+ )],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ },
+ ));
+ }
+
+ if !layer.primitives.is_empty() {
+ let _ = ManuallyDrop::into_inner(render_pass);
+
+ for instance in &layer.primitives {
+ if let Some(clip_bounds) = (instance.bounds * scale)
+ .intersection(&physical_bounds)
+ .and_then(Rectangle::snap)
+ {
+ instance.primitive.render(
+ encoder,
+ &engine.primitive_storage,
+ frame,
+ &clip_bounds,
+ );
+ }
+ }
+
+ render_pass = ManuallyDrop::new(encoder.begin_render_pass(
+ &wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu render pass"),
+ color_attachments: &[Some(
+ wgpu::RenderPassColorAttachment {
+ view: frame,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Load,
+ store: wgpu::StoreOp::Store,
+ },
+ },
+ )],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ },
+ ));
+ }
+
+ if !layer.text.is_empty() {
+ text_layer += engine.text_pipeline.render(
+ &self.text_storage,
+ text_layer,
+ &layer.text,
+ scissor_rect,
+ &mut render_pass,
+ );
+ }
+
+ #[cfg(any(feature = "svg", feature = "image"))]
+ if !layer.images.is_empty() {
+ engine.image_pipeline.render(
+ image_layer,
+ scissor_rect,
+ &mut render_pass,
+ );
+
+ image_layer += 1;
+ }
+ }
+
+ let _ = ManuallyDrop::into_inner(render_pass);
+ }
+
+ fn draw_overlay(
+ &mut self,
+ overlay: &[impl AsRef<str>],
+ viewport: &Viewport,
+ ) {
+ use crate::core::alignment;
+ use crate::core::text::Renderer as _;
+ use crate::core::Renderer as _;
+ use crate::core::Vector;
+
+ self.with_layer(
+ Rectangle::with_size(viewport.logical_size()),
+ |renderer| {
+ for (i, line) in overlay.iter().enumerate() {
+ let text = crate::core::Text {
+ content: line.as_ref().to_owned(),
+ bounds: viewport.logical_size(),
+ size: Pixels(20.0),
+ line_height: core::text::LineHeight::default(),
+ font: Font::MONOSPACE,
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ shaping: core::text::Shaping::Basic,
+ };
+
+ renderer.fill_text(
+ text.clone(),
+ Point::new(11.0, 11.0 + 25.0 * i as f32),
+ Color::new(0.9, 0.9, 0.9, 1.0),
+ Rectangle::with_size(Size::INFINITY),
+ );
+
+ renderer.fill_text(
+ text,
+ Point::new(11.0, 11.0 + 25.0 * i as f32)
+ + Vector::new(-1.0, -1.0),
+ Color::BLACK,
+ Rectangle::with_size(Size::INFINITY),
+ );
+ }
+ },
+ );
+ }
+}
+
+impl core::Renderer for Renderer {
+ fn start_layer(&mut self, bounds: Rectangle) {
+ self.layers.push_clip(bounds);
+ }
+
+ fn end_layer(&mut self) {
+ self.layers.pop_clip();
+ }
+
+ fn start_transformation(&mut self, transformation: Transformation) {
+ self.layers.push_transformation(transformation);
+ }
+
+ fn end_transformation(&mut self) {
+ self.layers.pop_transformation();
+ }
+
+ fn fill_quad(
+ &mut self,
+ quad: core::renderer::Quad,
+ background: impl Into<Background>,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_quad(quad, background.into(), transformation);
+ }
+
+ fn clear(&mut self) {
+ self.layers.clear();
+ }
+}
+
+impl core::text::Renderer for Renderer {
+ type Font = Font;
+ type Paragraph = Paragraph;
+ type Editor = Editor;
+
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
+
+ fn default_font(&self) -> Self::Font {
+ self.default_font
+ }
+
+ fn default_size(&self) -> Pixels {
+ self.default_text_size
+ }
+
+ fn fill_paragraph(
+ &mut self,
+ text: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+
+ layer.draw_paragraph(
+ text,
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ );
+ }
+
+ fn fill_editor(
+ &mut self,
+ editor: &Self::Editor,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_editor(editor, position, color, clip_bounds, transformation);
+ }
+
+ fn fill_text(
+ &mut self,
+ text: core::Text,
+ position: Point,
+ color: Color,
+ clip_bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_text(text, position, color, clip_bounds, transformation);
+ }
+}
+
+#[cfg(feature = "image")]
+impl core::image::Renderer for Renderer {
+ type Handle = core::image::Handle;
+
+ fn measure_image(&self, handle: &Self::Handle) -> Size<u32> {
+ self.image_cache.lock().measure_image(handle)
+ }
+
+ fn draw_image(
+ &mut self,
+ handle: Self::Handle,
+ filter_method: core::image::FilterMethod,
+ bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_image(handle, filter_method, bounds, transformation);
+ }
+}
+
+#[cfg(feature = "svg")]
+impl core::svg::Renderer for Renderer {
+ fn measure_svg(&self, handle: &core::svg::Handle) -> Size<u32> {
+ self.image_cache.lock().measure_svg(handle)
+ }
+
+ fn draw_svg(
+ &mut self,
+ handle: core::svg::Handle,
+ color_filter: Option<Color>,
+ bounds: Rectangle,
+ ) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_svg(handle, color_filter, bounds, transformation);
+ }
+}
+
+impl graphics::mesh::Renderer for Renderer {
+ fn draw_mesh(&mut self, mesh: graphics::Mesh) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_mesh(mesh, transformation);
+ }
+}
+
+#[cfg(feature = "geometry")]
+impl graphics::geometry::Renderer for Renderer {
+ type Geometry = Geometry;
+ type Frame = geometry::Frame;
+
+ fn new_frame(&self, size: Size) -> Self::Frame {
+ geometry::Frame::new(size)
+ }
+
+ fn draw_geometry(&mut self, geometry: Self::Geometry) {
+ let (layer, transformation) = self.layers.current_mut();
+
+ match geometry {
+ Geometry::Live { meshes, text } => {
+ layer.draw_mesh_group(meshes, transformation);
+ layer.draw_text_group(text, transformation);
+ }
+ Geometry::Cached(cache) => {
+ if let Some(meshes) = cache.meshes {
+ layer.draw_mesh_cache(meshes, transformation);
+ }
+
+ if let Some(text) = cache.text {
+ layer.draw_text_cache(text, transformation);
+ }
+ }
+ }
+ }
+}
+
+impl primitive::Renderer for Renderer {
+ fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) {
+ let (layer, transformation) = self.layers.current_mut();
+ layer.draw_primitive(bounds, Box::new(primitive), transformation);
+ }
+}
+
+impl graphics::compositor::Default for crate::Renderer {
+ type Compositor = window::Compositor;
+}
diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs
index ee9af93c..1313e752 100644
--- a/wgpu/src/primitive.rs
+++ b/wgpu/src/primitive.rs
@@ -1,38 +1,95 @@
-//! Draw using different graphical primitives.
-pub mod pipeline;
+//! Draw custom primitives.
+use crate::core::{self, Rectangle};
+use crate::graphics::Viewport;
-pub use pipeline::Pipeline;
+use rustc_hash::FxHashMap;
+use std::any::{Any, TypeId};
+use std::fmt::Debug;
-use crate::core::Rectangle;
-use crate::graphics::{Damage, Mesh};
+/// A batch of primitives.
+pub type Batch = Vec<Instance>;
-use std::fmt::Debug;
+/// A set of methods which allows a [`Primitive`] to be rendered.
+pub trait Primitive: Debug + Send + Sync + 'static {
+ /// Processes the [`Primitive`], allowing for GPU buffer allocation.
+ fn prepare(
+ &self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
+ storage: &mut Storage,
+ bounds: &Rectangle,
+ viewport: &Viewport,
+ );
+
+ /// Renders the [`Primitive`].
+ fn render(
+ &self,
+ encoder: &mut wgpu::CommandEncoder,
+ storage: &Storage,
+ target: &wgpu::TextureView,
+ clip_bounds: &Rectangle<u32>,
+ );
+}
-/// The graphical primitives supported by `iced_wgpu`.
-pub type Primitive = crate::graphics::Primitive<Custom>;
+#[derive(Debug)]
+/// An instance of a specific [`Primitive`].
+pub struct Instance {
+ /// The bounds of the [`Instance`].
+ pub bounds: Rectangle,
-/// The custom primitives supported by `iced_wgpu`.
-#[derive(Debug, Clone, PartialEq)]
-pub enum Custom {
- /// A mesh primitive.
- Mesh(Mesh),
- /// A custom pipeline primitive.
- Pipeline(Pipeline),
+ /// The [`Primitive`] to render.
+ pub primitive: Box<dyn Primitive>,
}
-impl Damage for Custom {
- fn bounds(&self) -> Rectangle {
- match self {
- Self::Mesh(mesh) => mesh.bounds(),
- Self::Pipeline(pipeline) => pipeline.bounds,
+impl Instance {
+ /// Creates a new [`Instance`] with the given [`Primitive`].
+ pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
+ Instance {
+ bounds,
+ primitive: Box::new(primitive),
}
}
}
-impl TryFrom<Mesh> for Custom {
- type Error = &'static str;
+/// A renderer than can draw custom primitives.
+pub trait Renderer: core::Renderer {
+ /// Draws a custom primitive.
+ fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive);
+}
+
+/// Stores custom, user-provided types.
+#[derive(Default, Debug)]
+pub struct Storage {
+ pipelines: FxHashMap<TypeId, Box<dyn Any + Send>>,
+}
+
+impl Storage {
+ /// Returns `true` if `Storage` contains a type `T`.
+ pub fn has<T: 'static>(&self) -> bool {
+ self.pipelines.get(&TypeId::of::<T>()).is_some()
+ }
+
+ /// Inserts the data `T` in to [`Storage`].
+ pub fn store<T: 'static + Send>(&mut self, data: T) {
+ let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(data));
+ }
+
+ /// Returns a reference to the data with type `T` if it exists in [`Storage`].
+ pub fn get<T: 'static>(&self) -> Option<&T> {
+ self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| {
+ pipeline
+ .downcast_ref::<T>()
+ .expect("Value with this type does not exist in Storage.")
+ })
+ }
- fn try_from(mesh: Mesh) -> Result<Self, Self::Error> {
- Ok(Custom::Mesh(mesh))
+ /// Returns a mutable reference to the data with type `T` if it exists in [`Storage`].
+ pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
+ self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| {
+ pipeline
+ .downcast_mut::<T>()
+ .expect("Value with this type does not exist in Storage.")
+ })
}
}
diff --git a/wgpu/src/primitive/pipeline.rs b/wgpu/src/primitive/pipeline.rs
deleted file mode 100644
index 814440ba..00000000
--- a/wgpu/src/primitive/pipeline.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-//! Draw primitives using custom pipelines.
-use crate::core::{self, Rectangle, Size};
-
-use std::any::{Any, TypeId};
-use std::collections::HashMap;
-use std::fmt::Debug;
-use std::sync::Arc;
-
-#[derive(Clone, Debug)]
-/// A custom primitive which can be used to render primitives associated with a custom pipeline.
-pub struct Pipeline {
- /// The bounds of the [`Pipeline`].
- pub bounds: Rectangle,
-
- /// The [`Primitive`] to render.
- pub primitive: Arc<dyn Primitive>,
-}
-
-impl Pipeline {
- /// Creates a new [`Pipeline`] with the given [`Primitive`].
- pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
- Pipeline {
- bounds,
- primitive: Arc::new(primitive),
- }
- }
-}
-
-impl PartialEq for Pipeline {
- fn eq(&self, other: &Self) -> bool {
- self.primitive.type_id() == other.primitive.type_id()
- }
-}
-
-/// A set of methods which allows a [`Primitive`] to be rendered.
-pub trait Primitive: Debug + Send + Sync + 'static {
- /// Processes the [`Primitive`], allowing for GPU buffer allocation.
- fn prepare(
- &self,
- format: wgpu::TextureFormat,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- bounds: Rectangle,
- target_size: Size<u32>,
- scale_factor: f32,
- storage: &mut Storage,
- );
-
- /// Renders the [`Primitive`].
- fn render(
- &self,
- storage: &Storage,
- target: &wgpu::TextureView,
- target_size: Size<u32>,
- viewport: Rectangle<u32>,
- encoder: &mut wgpu::CommandEncoder,
- );
-}
-
-/// A renderer than can draw custom pipeline primitives.
-pub trait Renderer: core::Renderer {
- /// Draws a custom pipeline primitive.
- fn draw_pipeline_primitive(
- &mut self,
- bounds: Rectangle,
- primitive: impl Primitive,
- );
-}
-
-impl Renderer for crate::Renderer {
- fn draw_pipeline_primitive(
- &mut self,
- bounds: Rectangle,
- primitive: impl Primitive,
- ) {
- self.draw_primitive(super::Primitive::Custom(super::Custom::Pipeline(
- Pipeline::new(bounds, primitive),
- )));
- }
-}
-
-/// Stores custom, user-provided pipelines.
-#[derive(Default, Debug)]
-pub struct Storage {
- pipelines: HashMap<TypeId, Box<dyn Any + Send>>,
-}
-
-impl Storage {
- /// Returns `true` if `Storage` contains a pipeline with type `T`.
- pub fn has<T: 'static>(&self) -> bool {
- self.pipelines.get(&TypeId::of::<T>()).is_some()
- }
-
- /// Inserts the pipeline `T` in to [`Storage`].
- pub fn store<T: 'static + Send>(&mut self, pipeline: T) {
- let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));
- }
-
- /// Returns a reference to pipeline with type `T` if it exists in [`Storage`].
- pub fn get<T: 'static>(&self) -> Option<&T> {
- self.pipelines.get(&TypeId::of::<T>()).map(|pipeline| {
- pipeline
- .downcast_ref::<T>()
- .expect("Pipeline with this type does not exist in Storage.")
- })
- }
-
- /// Returns a mutable reference to pipeline `T` if it exists in [`Storage`].
- pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
- self.pipelines.get_mut(&TypeId::of::<T>()).map(|pipeline| {
- pipeline
- .downcast_mut::<T>()
- .expect("Pipeline with this type does not exist in Storage.")
- })
- }
-}
diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs
index b932f54f..de432d2f 100644
--- a/wgpu/src/quad.rs
+++ b/wgpu/src/quad.rs
@@ -12,11 +12,37 @@ use bytemuck::{Pod, Zeroable};
use std::mem;
-#[cfg(feature = "tracing")]
-use tracing::info_span;
-
const INITIAL_INSTANCES: usize = 2_000;
+/// The properties of a quad.
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+#[repr(C)]
+pub struct Quad {
+ /// The position of the [`Quad`].
+ pub position: [f32; 2],
+
+ /// The size of the [`Quad`].
+ pub size: [f32; 2],
+
+ /// The border color of the [`Quad`], in __linear RGB__.
+ pub border_color: color::Packed,
+
+ /// The border radii of the [`Quad`].
+ pub border_radius: [f32; 4],
+
+ /// The border width of the [`Quad`].
+ pub border_width: f32,
+
+ /// The shadow color of the [`Quad`].
+ pub shadow_color: color::Packed,
+
+ /// The shadow offset of the [`Quad`].
+ pub shadow_offset: [f32; 2],
+
+ /// The shadow blur radius of the [`Quad`].
+ pub shadow_blur_radius: f32,
+}
+
#[derive(Debug)]
pub struct Pipeline {
solid: solid::Pipeline,
@@ -57,7 +83,8 @@ impl Pipeline {
pub fn prepare(
&mut self,
device: &wgpu::Device,
- queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
quads: &Batch,
transformation: Transformation,
scale: f32,
@@ -67,7 +94,7 @@ impl Pipeline {
}
let layer = &mut self.layers[self.prepare_layer];
- layer.prepare(device, queue, quads, transformation, scale);
+ layer.prepare(device, encoder, belt, quads, transformation, scale);
self.prepare_layer += 1;
}
@@ -123,7 +150,7 @@ impl Pipeline {
}
#[derive(Debug)]
-struct Layer {
+pub struct Layer {
constants: wgpu::BindGroup,
constants_buffer: wgpu::Buffer,
solid: solid::Layer,
@@ -162,56 +189,46 @@ impl Layer {
pub fn prepare(
&mut self,
device: &wgpu::Device,
- queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
quads: &Batch,
transformation: Transformation,
scale: f32,
) {
- #[cfg(feature = "tracing")]
- let _ = info_span!("Wgpu::Quad", "PREPARE").entered();
+ self.update(device, encoder, belt, transformation, scale);
+ if !quads.solids.is_empty() {
+ self.solid.prepare(device, encoder, belt, &quads.solids);
+ }
+
+ if !quads.gradients.is_empty() {
+ self.gradient
+ .prepare(device, encoder, belt, &quads.gradients);
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ transformation: Transformation,
+ scale: f32,
+ ) {
let uniforms = Uniforms::new(transformation, scale);
+ let bytes = bytemuck::bytes_of(&uniforms);
- queue.write_buffer(
+ belt.write_buffer(
+ encoder,
&self.constants_buffer,
0,
- bytemuck::bytes_of(&uniforms),
- );
-
- self.solid.prepare(device, queue, &quads.solids);
- self.gradient.prepare(device, queue, &quads.gradients);
+ (bytes.len() as u64).try_into().expect("Sized uniforms"),
+ device,
+ )
+ .copy_from_slice(bytes);
}
}
-/// The properties of a quad.
-#[derive(Clone, Copy, Debug, Pod, Zeroable)]
-#[repr(C)]
-pub struct Quad {
- /// The position of the [`Quad`].
- pub position: [f32; 2],
-
- /// The size of the [`Quad`].
- pub size: [f32; 2],
-
- /// The border color of the [`Quad`], in __linear RGB__.
- pub border_color: color::Packed,
-
- /// The border radii of the [`Quad`].
- pub border_radius: [f32; 4],
-
- /// The border width of the [`Quad`].
- pub border_width: f32,
-
- /// The shadow color of the [`Quad`].
- pub shadow_color: [f32; 4],
-
- /// The shadow offset of the [`Quad`].
- pub shadow_offset: [f32; 2],
-
- /// The shadow blur radius of the [`Quad`].
- pub shadow_blur_radius: f32,
-}
-
/// A group of [`Quad`]s rendered together.
#[derive(Default, Debug)]
pub struct Batch {
@@ -221,10 +238,13 @@ pub struct Batch {
/// The gradient quads of the [`Layer`].
gradients: Vec<Gradient>,
- /// The quad order of the [`Layer`]; stored as a tuple of the quad type & its count.
- order: Vec<(Kind, usize)>,
+ /// The quad order of the [`Layer`].
+ order: Order,
}
+/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count.
+type Order = Vec<(Kind, usize)>;
+
impl Batch {
/// Returns true if there are no quads of any type in [`Quads`].
pub fn is_empty(&self) -> bool {
@@ -264,6 +284,12 @@ impl Batch {
}
}
}
+
+ pub fn clear(&mut self) {
+ self.solids.clear();
+ self.gradients.clear();
+ self.order.clear();
+ }
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs
index 560fcad2..5b32c52a 100644
--- a/wgpu/src/quad/gradient.rs
+++ b/wgpu/src/quad/gradient.rs
@@ -46,11 +46,12 @@ impl Layer {
pub fn prepare(
&mut self,
device: &wgpu::Device,
- queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
instances: &[Gradient],
) {
let _ = self.instances.resize(device, instances.len());
- let _ = self.instances.write(queue, 0, instances);
+ let _ = self.instances.write(device, encoder, belt, 0, instances);
self.instance_count = instances.len();
}
diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs
index 771eee34..1cead367 100644
--- a/wgpu/src/quad/solid.rs
+++ b/wgpu/src/quad/solid.rs
@@ -40,11 +40,12 @@ impl Layer {
pub fn prepare(
&mut self,
device: &wgpu::Device,
- queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
instances: &[Solid],
) {
let _ = self.instances.resize(device, instances.len());
- let _ = self.instances.write(queue, 0, instances);
+ let _ = self.instances.write(device, encoder, belt, 0, instances);
self.instance_count = instances.len();
}
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index 828d9e09..a6aea0a5 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -2,18 +2,18 @@
use crate::core::{Font, Pixels};
use crate::graphics::{self, Antialiasing};
-/// The settings of a [`Backend`].
+/// The settings of a [`Renderer`].
///
-/// [`Backend`]: crate::Backend
+/// [`Renderer`]: crate::Renderer
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
- /// The present mode of the [`Backend`].
+ /// The present mode of the [`Renderer`].
///
- /// [`Backend`]: crate::Backend
+ /// [`Renderer`]: crate::Renderer
pub present_mode: wgpu::PresentMode,
- /// The internal graphics backend to use.
- pub internal_backend: wgpu::Backends,
+ /// The graphics backends to use.
+ pub backends: wgpu::Backends,
/// The default [`Font`] to use.
pub default_font: Font,
@@ -33,7 +33,7 @@ impl Default for Settings {
fn default() -> Settings {
Settings {
present_mode: wgpu::PresentMode::AutoVsync,
- internal_backend: wgpu::Backends::all(),
+ backends: wgpu::Backends::all(),
default_font: Font::default(),
default_text_size: Pixels(16.0),
antialiasing: None,
diff --git a/wgpu/src/shader/blit.wgsl b/wgpu/src/shader/blit.wgsl
index c2ea223f..d7633808 100644
--- a/wgpu/src/shader/blit.wgsl
+++ b/wgpu/src/shader/blit.wgsl
@@ -1,22 +1,14 @@
-var<private> positions: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
- vec2<f32>(-1.0, 1.0),
- vec2<f32>(-1.0, -1.0),
- vec2<f32>(1.0, -1.0),
- vec2<f32>(-1.0, 1.0),
- vec2<f32>(1.0, 1.0),
- vec2<f32>(1.0, -1.0)
-);
-
var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
- vec2<f32>(0.0, 1.0),
+ vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
- vec2<f32>(1.0, 0.0),
+ vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 1.0)
);
@group(0) @binding(0) var u_sampler: sampler;
+@group(0) @binding(1) var<uniform> u_ratio: vec2<f32>;
@group(1) @binding(0) var u_texture: texture_2d<f32>;
struct VertexInput {
@@ -30,9 +22,11 @@ struct VertexOutput {
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
+ let uv = uvs[input.vertex_index];
+
var out: VertexOutput;
- out.uv = uvs[input.vertex_index];
- out.position = vec4<f32>(positions[input.vertex_index], 0.0, 1.0);
+ out.uv = uv * u_ratio;
+ out.position = vec4<f32>(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0);
return out;
}
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index 6fa1922d..0d01faca 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -1,20 +1,193 @@
use crate::core::alignment;
use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::color;
-use crate::graphics::text::cache::{self, Cache};
+use crate::graphics::text::cache::{self, Cache as BufferCache};
use crate::graphics::text::{font_system, to_color, Editor, Paragraph};
-use crate::layer::Text;
-use std::borrow::Cow;
-use std::cell::RefCell;
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::rc::Rc;
+use std::sync::atomic::{self, AtomicU64};
use std::sync::Arc;
+pub use crate::graphics::Text;
+
+const COLOR_MODE: glyphon::ColorMode = if color::GAMMA_CORRECTION {
+ glyphon::ColorMode::Accurate
+} else {
+ glyphon::ColorMode::Web
+};
+
+pub type Batch = Vec<Item>;
+
+#[derive(Debug)]
+pub enum Item {
+ Group {
+ transformation: Transformation,
+ text: Vec<Text>,
+ },
+ Cached {
+ transformation: Transformation,
+ cache: Cache,
+ },
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+ id: Id,
+ text: Rc<[Text]>,
+ version: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(u64);
+
+impl Cache {
+ pub fn new(text: Vec<Text>) -> Option<Self> {
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
+ if text.is_empty() {
+ return None;
+ }
+
+ Some(Self {
+ id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+ text: Rc::from(text),
+ version: 0,
+ })
+ }
+
+ pub fn update(&mut self, text: Vec<Text>) {
+ self.text = Rc::from(text);
+ self.version += 1;
+ }
+}
+
+struct Upload {
+ renderer: glyphon::TextRenderer,
+ atlas: glyphon::TextAtlas,
+ buffer_cache: BufferCache,
+ transformation: Transformation,
+ version: usize,
+}
+
+#[derive(Default)]
+pub struct Storage {
+ uploads: FxHashMap<Id, Upload>,
+ recently_used: FxHashSet<Id>,
+}
+
+impl Storage {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn get(&self, cache: &Cache) -> Option<&Upload> {
+ if cache.text.is_empty() {
+ return None;
+ }
+
+ self.uploads.get(&cache.id)
+ }
+
+ fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ format: wgpu::TextureFormat,
+ cache: &Cache,
+ new_transformation: Transformation,
+ bounds: Rectangle,
+ target_size: Size<u32>,
+ ) {
+ match self.uploads.entry(cache.id) {
+ hash_map::Entry::Occupied(entry) => {
+ let upload = entry.into_mut();
+
+ if !cache.text.is_empty()
+ && (upload.version != cache.version
+ || upload.transformation != new_transformation)
+ {
+ let _ = prepare(
+ device,
+ queue,
+ encoder,
+ &mut upload.renderer,
+ &mut upload.atlas,
+ &mut upload.buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ target_size,
+ );
+
+ upload.version = cache.version;
+ upload.transformation = new_transformation;
+
+ upload.buffer_cache.trim();
+ upload.atlas.trim();
+ }
+ }
+ hash_map::Entry::Vacant(entry) => {
+ let mut atlas = glyphon::TextAtlas::with_color_mode(
+ device, queue, format, COLOR_MODE,
+ );
+
+ let mut renderer = glyphon::TextRenderer::new(
+ &mut atlas,
+ device,
+ wgpu::MultisampleState::default(),
+ None,
+ );
+
+ let mut buffer_cache = BufferCache::new();
+
+ let _ = prepare(
+ device,
+ queue,
+ encoder,
+ &mut renderer,
+ &mut atlas,
+ &mut buffer_cache,
+ &cache.text,
+ bounds,
+ new_transformation,
+ target_size,
+ );
+
+ let _ = entry.insert(Upload {
+ renderer,
+ atlas,
+ buffer_cache,
+ transformation: new_transformation,
+ version: 0,
+ });
+
+ log::info!(
+ "New text upload: {} (total: {})",
+ cache.id.0,
+ self.uploads.len()
+ );
+ }
+ }
+
+ let _ = self.recently_used.insert(cache.id);
+ }
+
+ pub fn trim(&mut self) {
+ self.uploads.retain(|id, _| self.recently_used.contains(id));
+ self.recently_used.clear();
+ }
+}
+
#[allow(missing_debug_implementations)]
pub struct Pipeline {
- renderers: Vec<glyphon::TextRenderer>,
+ format: wgpu::TextureFormat,
atlas: glyphon::TextAtlas,
+ renderers: Vec<glyphon::TextRenderer>,
prepare_layer: usize,
- cache: RefCell<Cache>,
+ cache: BufferCache,
}
impl Pipeline {
@@ -24,273 +197,95 @@ impl Pipeline {
format: wgpu::TextureFormat,
) -> Self {
Pipeline {
+ format,
renderers: Vec::new(),
atlas: glyphon::TextAtlas::with_color_mode(
- device,
- queue,
- format,
- if color::GAMMA_CORRECTION {
- glyphon::ColorMode::Accurate
- } else {
- glyphon::ColorMode::Web
- },
+ device, queue, format, COLOR_MODE,
),
prepare_layer: 0,
- cache: RefCell::new(Cache::new()),
+ cache: BufferCache::new(),
}
}
- pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- font_system()
- .write()
- .expect("Write font system")
- .load_font(bytes);
-
- self.cache = RefCell::new(Cache::new());
- }
-
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
- sections: &[Text<'_>],
+ encoder: &mut wgpu::CommandEncoder,
+ storage: &mut Storage,
+ batch: &Batch,
layer_bounds: Rectangle,
- scale_factor: f32,
+ layer_transformation: Transformation,
target_size: Size<u32>,
) {
- if self.renderers.len() <= self.prepare_layer {
- self.renderers.push(glyphon::TextRenderer::new(
- &mut self.atlas,
- device,
- wgpu::MultisampleState::default(),
- None,
- ));
- }
-
- let mut font_system = font_system().write().expect("Write font system");
- let font_system = font_system.raw();
-
- let renderer = &mut self.renderers[self.prepare_layer];
- let cache = self.cache.get_mut();
-
- enum Allocation {
- Paragraph(Paragraph),
- Editor(Editor),
- Cache(cache::KeyHash),
- Raw(Arc<glyphon::Buffer>),
- }
-
- let allocations: Vec<_> = sections
- .iter()
- .map(|section| match section {
- Text::Paragraph { paragraph, .. } => {
- paragraph.upgrade().map(Allocation::Paragraph)
- }
- Text::Editor { editor, .. } => {
- editor.upgrade().map(Allocation::Editor)
- }
- Text::Cached(text) => {
- let (key, _) = cache.allocate(
- font_system,
- cache::Key {
- content: text.content,
- size: text.size.into(),
- line_height: f32::from(
- text.line_height.to_absolute(text.size),
- ),
- font: text.font,
- bounds: Size {
- width: text.bounds.width,
- height: text.bounds.height,
- },
- shaping: text.shaping,
- },
- );
-
- Some(Allocation::Cache(key))
- }
- Text::Raw { raw, .. } => {
- raw.buffer.upgrade().map(Allocation::Raw)
- }
- })
- .collect();
-
- let layer_bounds = layer_bounds * scale_factor;
-
- let text_areas = sections.iter().zip(allocations.iter()).filter_map(
- |(section, allocation)| {
- let (
- buffer,
- bounds,
- horizontal_alignment,
- vertical_alignment,
- color,
- clip_bounds,
+ for item in batch {
+ match item {
+ Item::Group {
transformation,
- ) = match section {
- Text::Paragraph {
- position,
- color,
- clip_bounds,
- transformation,
- ..
- } => {
- use crate::core::text::Paragraph as _;
-
- let Some(Allocation::Paragraph(paragraph)) = allocation
- else {
- return None;
- };
-
- (
- paragraph.buffer(),
- Rectangle::new(*position, paragraph.min_bounds()),
- paragraph.horizontal_alignment(),
- paragraph.vertical_alignment(),
- *color,
- *clip_bounds,
- *transformation,
- )
- }
- Text::Editor {
- position,
- color,
- clip_bounds,
- transformation,
- ..
- } => {
- use crate::core::text::Editor as _;
-
- let Some(Allocation::Editor(editor)) = allocation
- else {
- return None;
- };
-
- (
- editor.buffer(),
- Rectangle::new(*position, editor.bounds()),
- alignment::Horizontal::Left,
- alignment::Vertical::Top,
- *color,
- *clip_bounds,
- *transformation,
- )
- }
- Text::Cached(text) => {
- let Some(Allocation::Cache(key)) = allocation else {
- return None;
- };
-
- let entry = cache.get(key).expect("Get cached buffer");
-
- (
- &entry.buffer,
- Rectangle::new(
- text.bounds.position(),
- entry.min_bounds,
- ),
- text.horizontal_alignment,
- text.vertical_alignment,
- text.color,
- text.clip_bounds,
- Transformation::IDENTITY,
- )
- }
- Text::Raw {
- raw,
- transformation,
- } => {
- let Some(Allocation::Raw(buffer)) = allocation else {
- return None;
- };
-
- let (width, height) = buffer.size();
-
- (
- buffer.as_ref(),
- Rectangle::new(
- raw.position,
- Size::new(width, height),
- ),
- alignment::Horizontal::Left,
- alignment::Vertical::Top,
- raw.color,
- raw.clip_bounds,
- *transformation,
- )
+ text,
+ } => {
+ if self.renderers.len() <= self.prepare_layer {
+ self.renderers.push(glyphon::TextRenderer::new(
+ &mut self.atlas,
+ device,
+ wgpu::MultisampleState::default(),
+ None,
+ ));
}
- };
- let bounds = bounds * transformation * scale_factor;
-
- let left = match horizontal_alignment {
- alignment::Horizontal::Left => bounds.x,
- alignment::Horizontal::Center => {
- bounds.x - bounds.width / 2.0
- }
- alignment::Horizontal::Right => bounds.x - bounds.width,
- };
+ let renderer = &mut self.renderers[self.prepare_layer];
+ let result = prepare(
+ device,
+ queue,
+ encoder,
+ renderer,
+ &mut self.atlas,
+ &mut self.cache,
+ text,
+ layer_bounds * layer_transformation,
+ layer_transformation * *transformation,
+ target_size,
+ );
- let top = match vertical_alignment {
- alignment::Vertical::Top => bounds.y,
- alignment::Vertical::Center => {
- bounds.y - bounds.height / 2.0
+ match result {
+ Ok(()) => {
+ self.prepare_layer += 1;
+ }
+ Err(glyphon::PrepareError::AtlasFull) => {
+ // If the atlas cannot grow, then all bets are off.
+ // Instead of panicking, we will just pray that the result
+ // will be somewhat readable...
+ }
}
- alignment::Vertical::Bottom => bounds.y - bounds.height,
- };
-
- let clip_bounds = layer_bounds.intersection(
- &(clip_bounds * transformation * scale_factor),
- )?;
-
- Some(glyphon::TextArea {
- buffer,
- left,
- top,
- scale: scale_factor * transformation.scale_factor(),
- bounds: glyphon::TextBounds {
- left: clip_bounds.x as i32,
- top: clip_bounds.y as i32,
- right: (clip_bounds.x + clip_bounds.width) as i32,
- bottom: (clip_bounds.y + clip_bounds.height) as i32,
- },
- default_color: to_color(color),
- })
- },
- );
-
- let result = renderer.prepare(
- device,
- queue,
- font_system,
- &mut self.atlas,
- glyphon::Resolution {
- width: target_size.width,
- height: target_size.height,
- },
- text_areas,
- &mut glyphon::SwashCache::new(),
- );
-
- match result {
- Ok(()) => {
- self.prepare_layer += 1;
- }
- Err(glyphon::PrepareError::AtlasFull) => {
- // If the atlas cannot grow, then all bets are off.
- // Instead of panicking, we will just pray that the result
- // will be somewhat readable...
+ }
+ Item::Cached {
+ transformation,
+ cache,
+ } => {
+ storage.prepare(
+ device,
+ queue,
+ encoder,
+ self.format,
+ cache,
+ layer_transformation * *transformation,
+ layer_bounds * layer_transformation,
+ target_size,
+ );
+ }
}
}
}
pub fn render<'a>(
&'a self,
- layer: usize,
+ storage: &'a Storage,
+ start: usize,
+ batch: &'a Batch,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
- ) {
- let renderer = &self.renderers[layer];
+ ) -> usize {
+ let mut layer_count = 0;
render_pass.set_scissor_rect(
bounds.x,
@@ -299,15 +294,251 @@ impl Pipeline {
bounds.height,
);
- renderer
- .render(&self.atlas, render_pass)
- .expect("Render text");
+ for item in batch {
+ match item {
+ Item::Group { .. } => {
+ let renderer = &self.renderers[start + layer_count];
+
+ renderer
+ .render(&self.atlas, render_pass)
+ .expect("Render text");
+
+ layer_count += 1;
+ }
+ Item::Cached { cache, .. } => {
+ if let Some(upload) = storage.get(cache) {
+ upload
+ .renderer
+ .render(&upload.atlas, render_pass)
+ .expect("Render cached text");
+ }
+ }
+ }
+ }
+
+ layer_count
}
pub fn end_frame(&mut self) {
self.atlas.trim();
- self.cache.get_mut().trim();
+ self.cache.trim();
self.prepare_layer = 0;
}
}
+
+fn prepare(
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ renderer: &mut glyphon::TextRenderer,
+ atlas: &mut glyphon::TextAtlas,
+ buffer_cache: &mut BufferCache,
+ sections: &[Text],
+ layer_bounds: Rectangle,
+ layer_transformation: Transformation,
+ target_size: Size<u32>,
+) -> Result<(), glyphon::PrepareError> {
+ let mut font_system = font_system().write().expect("Write font system");
+ let font_system = font_system.raw();
+
+ enum Allocation {
+ Paragraph(Paragraph),
+ Editor(Editor),
+ Cache(cache::KeyHash),
+ Raw(Arc<glyphon::Buffer>),
+ }
+
+ let allocations: Vec<_> = sections
+ .iter()
+ .map(|section| match section {
+ Text::Paragraph { paragraph, .. } => {
+ paragraph.upgrade().map(Allocation::Paragraph)
+ }
+ Text::Editor { editor, .. } => {
+ editor.upgrade().map(Allocation::Editor)
+ }
+ Text::Cached {
+ content,
+ bounds,
+ size,
+ line_height,
+ font,
+ shaping,
+ ..
+ } => {
+ let (key, _) = buffer_cache.allocate(
+ font_system,
+ cache::Key {
+ content,
+ size: f32::from(*size),
+ line_height: f32::from(*line_height),
+ font: *font,
+ bounds: Size {
+ width: bounds.width,
+ height: bounds.height,
+ },
+ shaping: *shaping,
+ },
+ );
+
+ Some(Allocation::Cache(key))
+ }
+ Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw),
+ })
+ .collect();
+
+ let text_areas = sections.iter().zip(allocations.iter()).filter_map(
+ |(section, allocation)| {
+ let (
+ buffer,
+ bounds,
+ horizontal_alignment,
+ vertical_alignment,
+ color,
+ clip_bounds,
+ transformation,
+ ) = match section {
+ Text::Paragraph {
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ ..
+ } => {
+ use crate::core::text::Paragraph as _;
+
+ let Some(Allocation::Paragraph(paragraph)) = allocation
+ else {
+ return None;
+ };
+
+ (
+ paragraph.buffer(),
+ Rectangle::new(*position, paragraph.min_bounds()),
+ paragraph.horizontal_alignment(),
+ paragraph.vertical_alignment(),
+ *color,
+ *clip_bounds,
+ *transformation,
+ )
+ }
+ Text::Editor {
+ position,
+ color,
+ clip_bounds,
+ transformation,
+ ..
+ } => {
+ use crate::core::text::Editor as _;
+
+ let Some(Allocation::Editor(editor)) = allocation else {
+ return None;
+ };
+
+ (
+ editor.buffer(),
+ Rectangle::new(*position, editor.bounds()),
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ *color,
+ *clip_bounds,
+ *transformation,
+ )
+ }
+ Text::Cached {
+ bounds,
+ horizontal_alignment,
+ vertical_alignment,
+ color,
+ clip_bounds,
+ ..
+ } => {
+ let Some(Allocation::Cache(key)) = allocation else {
+ return None;
+ };
+
+ let entry =
+ buffer_cache.get(key).expect("Get cached buffer");
+
+ (
+ &entry.buffer,
+ Rectangle::new(bounds.position(), entry.min_bounds),
+ *horizontal_alignment,
+ *vertical_alignment,
+ *color,
+ *clip_bounds,
+ Transformation::IDENTITY,
+ )
+ }
+ Text::Raw {
+ raw,
+ transformation,
+ } => {
+ let Some(Allocation::Raw(buffer)) = allocation else {
+ return None;
+ };
+
+ let (width, height) = buffer.size();
+
+ (
+ buffer.as_ref(),
+ Rectangle::new(raw.position, Size::new(width, height)),
+ alignment::Horizontal::Left,
+ alignment::Vertical::Top,
+ raw.color,
+ raw.clip_bounds,
+ *transformation,
+ )
+ }
+ };
+
+ let bounds = bounds * transformation * layer_transformation;
+
+ let left = match horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => bounds.x - bounds.width / 2.0,
+ alignment::Horizontal::Right => bounds.x - bounds.width,
+ };
+
+ let top = match vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => bounds.y - bounds.height / 2.0,
+ alignment::Vertical::Bottom => bounds.y - bounds.height,
+ };
+
+ let clip_bounds = layer_bounds.intersection(
+ &(clip_bounds * transformation * layer_transformation),
+ )?;
+
+ Some(glyphon::TextArea {
+ buffer,
+ left,
+ top,
+ scale: transformation.scale_factor()
+ * layer_transformation.scale_factor(),
+ bounds: glyphon::TextBounds {
+ left: clip_bounds.x as i32,
+ top: clip_bounds.y as i32,
+ right: (clip_bounds.x + clip_bounds.width) as i32,
+ bottom: (clip_bounds.y + clip_bounds.height) as i32,
+ },
+ default_color: to_color(color),
+ })
+ },
+ );
+
+ renderer.prepare(
+ device,
+ queue,
+ encoder,
+ font_system,
+ atlas,
+ glyphon::Resolution {
+ width: target_size.width,
+ height: target_size.height,
+ },
+ text_areas,
+ &mut glyphon::SwashCache::new(),
+ )
+}
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index 2bb6f307..8470ea39 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -1,14 +1,158 @@
//! Draw meshes of triangles.
mod msaa;
-use crate::core::{Size, Transformation};
+use crate::core::{Rectangle, Size, Transformation};
+use crate::graphics::mesh::{self, Mesh};
use crate::graphics::Antialiasing;
-use crate::layer::mesh::{self, Mesh};
use crate::Buffer;
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::rc::Rc;
+use std::sync::atomic::{self, AtomicU64};
+
const INITIAL_INDEX_COUNT: usize = 1_000;
const INITIAL_VERTEX_COUNT: usize = 1_000;
+pub type Batch = Vec<Item>;
+
+#[derive(Debug)]
+pub enum Item {
+ Group {
+ transformation: Transformation,
+ meshes: Vec<Mesh>,
+ },
+ Cached {
+ transformation: Transformation,
+ cache: Cache,
+ },
+}
+
+#[derive(Debug, Clone)]
+pub struct Cache {
+ id: Id,
+ batch: Rc<[Mesh]>,
+ version: usize,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Id(u64);
+
+impl Cache {
+ pub fn new(meshes: Vec<Mesh>) -> Option<Self> {
+ static NEXT_ID: AtomicU64 = AtomicU64::new(0);
+
+ if meshes.is_empty() {
+ return None;
+ }
+
+ Some(Self {
+ id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
+ batch: Rc::from(meshes),
+ version: 0,
+ })
+ }
+
+ pub fn update(&mut self, meshes: Vec<Mesh>) {
+ self.batch = Rc::from(meshes);
+ self.version += 1;
+ }
+}
+
+#[derive(Debug)]
+struct Upload {
+ layer: Layer,
+ transformation: Transformation,
+ version: usize,
+}
+
+#[derive(Debug, Default)]
+pub struct Storage {
+ uploads: FxHashMap<Id, Upload>,
+ recently_used: FxHashSet<Id>,
+}
+
+impl Storage {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ fn get(&self, cache: &Cache) -> Option<&Upload> {
+ if cache.batch.is_empty() {
+ return None;
+ }
+
+ self.uploads.get(&cache.id)
+ }
+
+ fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ solid: &solid::Pipeline,
+ gradient: &gradient::Pipeline,
+ cache: &Cache,
+ new_transformation: Transformation,
+ ) {
+ match self.uploads.entry(cache.id) {
+ hash_map::Entry::Occupied(entry) => {
+ let upload = entry.into_mut();
+
+ if !cache.batch.is_empty()
+ && (upload.version != cache.version
+ || upload.transformation != new_transformation)
+ {
+ upload.layer.prepare(
+ device,
+ encoder,
+ belt,
+ solid,
+ gradient,
+ &cache.batch,
+ new_transformation,
+ );
+
+ upload.version = cache.version;
+ upload.transformation = new_transformation;
+ }
+ }
+ hash_map::Entry::Vacant(entry) => {
+ let mut layer = Layer::new(device, solid, gradient);
+
+ layer.prepare(
+ device,
+ encoder,
+ belt,
+ solid,
+ gradient,
+ &cache.batch,
+ new_transformation,
+ );
+
+ let _ = entry.insert(Upload {
+ layer,
+ transformation: new_transformation,
+ version: 0,
+ });
+
+ log::info!(
+ "New mesh upload: {} (total: {})",
+ cache.id.0,
+ self.uploads.len()
+ );
+ }
+ }
+
+ let _ = self.recently_used.insert(cache.id);
+ }
+
+ pub fn trim(&mut self) {
+ self.uploads.retain(|id, _| self.recently_used.contains(id));
+ self.recently_used.clear();
+ }
+}
+
#[derive(Debug)]
pub struct Pipeline {
blit: Option<msaa::Blit>,
@@ -18,8 +162,198 @@ pub struct Pipeline {
prepare_layer: usize,
}
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ antialiasing: Option<Antialiasing>,
+ ) -> Pipeline {
+ Pipeline {
+ blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
+ solid: solid::Pipeline::new(device, format, antialiasing),
+ gradient: gradient::Pipeline::new(device, format, antialiasing),
+ layers: Vec::new(),
+ prepare_layer: 0,
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ storage: &mut Storage,
+ items: &[Item],
+ scale: Transformation,
+ target_size: Size<u32>,
+ ) {
+ let projection = if let Some(blit) = &mut self.blit {
+ blit.prepare(device, encoder, belt, target_size) * scale
+ } else {
+ Transformation::orthographic(target_size.width, target_size.height)
+ * scale
+ };
+
+ for item in items {
+ match item {
+ Item::Group {
+ transformation,
+ meshes,
+ } => {
+ if self.layers.len() <= self.prepare_layer {
+ self.layers.push(Layer::new(
+ device,
+ &self.solid,
+ &self.gradient,
+ ));
+ }
+
+ let layer = &mut self.layers[self.prepare_layer];
+ layer.prepare(
+ device,
+ encoder,
+ belt,
+ &self.solid,
+ &self.gradient,
+ meshes,
+ projection * *transformation,
+ );
+
+ self.prepare_layer += 1;
+ }
+ Item::Cached {
+ transformation,
+ cache,
+ } => {
+ storage.prepare(
+ device,
+ encoder,
+ belt,
+ &self.solid,
+ &self.gradient,
+ cache,
+ projection * *transformation,
+ );
+ }
+ }
+ }
+ }
+
+ pub fn render(
+ &mut self,
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ storage: &Storage,
+ start: usize,
+ batch: &Batch,
+ bounds: Rectangle,
+ screen_transformation: Transformation,
+ ) -> usize {
+ let mut layer_count = 0;
+
+ let items = batch.iter().filter_map(|item| match item {
+ Item::Group {
+ transformation,
+ meshes,
+ } => {
+ let layer = &self.layers[start + layer_count];
+ layer_count += 1;
+
+ Some((
+ layer,
+ meshes.as_slice(),
+ screen_transformation * *transformation,
+ ))
+ }
+ Item::Cached {
+ transformation,
+ cache,
+ } => {
+ let upload = storage.get(cache)?;
+
+ Some((
+ &upload.layer,
+ &cache.batch,
+ screen_transformation * *transformation,
+ ))
+ }
+ });
+
+ render(
+ encoder,
+ target,
+ self.blit.as_mut(),
+ &self.solid,
+ &self.gradient,
+ bounds,
+ items,
+ );
+
+ layer_count
+ }
+
+ pub fn end_frame(&mut self) {
+ self.prepare_layer = 0;
+ }
+}
+
+fn render<'a>(
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ mut blit: Option<&mut msaa::Blit>,
+ solid: &solid::Pipeline,
+ gradient: &gradient::Pipeline,
+ bounds: Rectangle,
+ group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>,
+) {
+ {
+ let (attachment, resolve_target, load) = if let Some(blit) = &mut blit {
+ let (attachment, resolve_target) = blit.targets();
+
+ (
+ attachment,
+ Some(resolve_target),
+ wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ )
+ } else {
+ (target, None, wgpu::LoadOp::Load)
+ };
+
+ let mut render_pass =
+ encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("iced_wgpu.triangle.render_pass"),
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: attachment,
+ resolve_target,
+ ops: wgpu::Operations {
+ load,
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ depth_stencil_attachment: None,
+ timestamp_writes: None,
+ occlusion_query_set: None,
+ });
+
+ for (layer, meshes, transformation) in group {
+ layer.render(
+ solid,
+ gradient,
+ meshes,
+ bounds,
+ transformation,
+ &mut render_pass,
+ );
+ }
+ }
+
+ if let Some(blit) = blit {
+ blit.draw(encoder, target);
+ }
+}
+
#[derive(Debug)]
-struct Layer {
+pub struct Layer {
index_buffer: Buffer<u32>,
index_strides: Vec<u32>,
solid: solid::Layer,
@@ -48,10 +382,11 @@ impl Layer {
fn prepare(
&mut self,
device: &wgpu::Device,
- queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
- meshes: &[Mesh<'_>],
+ meshes: &[Mesh],
transformation: Transformation,
) {
// Count the total amount of vertices & indices we need to handle
@@ -103,33 +438,47 @@ impl Layer {
let uniforms =
Uniforms::new(transformation * mesh.transformation());
- index_offset +=
- self.index_buffer.write(queue, index_offset, indices);
+ index_offset += self.index_buffer.write(
+ device,
+ encoder,
+ belt,
+ index_offset,
+ indices,
+ );
+
self.index_strides.push(indices.len() as u32);
match mesh {
Mesh::Solid { buffers, .. } => {
solid_vertex_offset += self.solid.vertices.write(
- queue,
+ device,
+ encoder,
+ belt,
solid_vertex_offset,
&buffers.vertices,
);
solid_uniform_offset += self.solid.uniforms.write(
- queue,
+ device,
+ encoder,
+ belt,
solid_uniform_offset,
&[uniforms],
);
}
Mesh::Gradient { buffers, .. } => {
gradient_vertex_offset += self.gradient.vertices.write(
- queue,
+ device,
+ encoder,
+ belt,
gradient_vertex_offset,
&buffers.vertices,
);
gradient_uniform_offset += self.gradient.uniforms.write(
- queue,
+ device,
+ encoder,
+ belt,
gradient_uniform_offset,
&[uniforms],
);
@@ -142,8 +491,9 @@ impl Layer {
&'a self,
solid: &'a solid::Pipeline,
gradient: &'a gradient::Pipeline,
- meshes: &[Mesh<'_>],
- scale_factor: f32,
+ meshes: &[Mesh],
+ bounds: Rectangle,
+ transformation: Transformation,
render_pass: &mut wgpu::RenderPass<'a>,
) {
let mut num_solids = 0;
@@ -151,11 +501,12 @@ impl Layer {
let mut last_is_solid = None;
for (index, mesh) in meshes.iter().enumerate() {
- let clip_bounds = (mesh.clip_bounds() * scale_factor).snap();
-
- if clip_bounds.width < 1 || clip_bounds.height < 1 {
+ let Some(clip_bounds) = bounds
+ .intersection(&(mesh.clip_bounds() * transformation))
+ .and_then(Rectangle::snap)
+ else {
continue;
- }
+ };
render_pass.set_scissor_rect(
clip_bounds.x,
@@ -219,117 +570,6 @@ impl Layer {
}
}
-impl Pipeline {
- pub fn new(
- device: &wgpu::Device,
- format: wgpu::TextureFormat,
- antialiasing: Option<Antialiasing>,
- ) -> Pipeline {
- Pipeline {
- blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
- solid: solid::Pipeline::new(device, format, antialiasing),
- gradient: gradient::Pipeline::new(device, format, antialiasing),
- layers: Vec::new(),
- prepare_layer: 0,
- }
- }
-
- pub fn prepare(
- &mut self,
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- meshes: &[Mesh<'_>],
- transformation: Transformation,
- ) {
- #[cfg(feature = "tracing")]
- let _ = tracing::info_span!("Wgpu::Triangle", "PREPARE").entered();
-
- if self.layers.len() <= self.prepare_layer {
- self.layers
- .push(Layer::new(device, &self.solid, &self.gradient));
- }
-
- let layer = &mut self.layers[self.prepare_layer];
- layer.prepare(
- device,
- queue,
- &self.solid,
- &self.gradient,
- meshes,
- transformation,
- );
-
- self.prepare_layer += 1;
- }
-
- pub fn render(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- target: &wgpu::TextureView,
- layer: usize,
- target_size: Size<u32>,
- meshes: &[Mesh<'_>],
- scale_factor: f32,
- ) {
- #[cfg(feature = "tracing")]
- let _ = tracing::info_span!("Wgpu::Triangle", "DRAW").entered();
-
- {
- let (attachment, resolve_target, load) = if let Some(blit) =
- &mut self.blit
- {
- let (attachment, resolve_target) =
- blit.targets(device, target_size.width, target_size.height);
-
- (
- attachment,
- Some(resolve_target),
- wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
- )
- } else {
- (target, None, wgpu::LoadOp::Load)
- };
-
- let mut render_pass =
- encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("iced_wgpu.triangle.render_pass"),
- color_attachments: &[Some(
- wgpu::RenderPassColorAttachment {
- view: attachment,
- resolve_target,
- ops: wgpu::Operations {
- load,
- store: wgpu::StoreOp::Store,
- },
- },
- )],
- depth_stencil_attachment: None,
- timestamp_writes: None,
- occlusion_query_set: None,
- });
-
- let layer = &mut self.layers[layer];
-
- layer.render(
- &self.solid,
- &self.gradient,
- meshes,
- scale_factor,
- &mut render_pass,
- );
- }
-
- if let Some(blit) = &mut self.blit {
- blit.draw(encoder, target);
- }
- }
-
- pub fn end_frame(&mut self) {
- self.prepare_layer = 0;
- }
-}
-
fn fragment_target(
texture_format: wgpu::TextureFormat,
) -> wgpu::ColorTargetState {
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index 14abd20b..71c16925 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -1,13 +1,18 @@
+use crate::core::{Size, Transformation};
use crate::graphics;
+use std::num::NonZeroU64;
+
#[derive(Debug)]
pub struct Blit {
format: wgpu::TextureFormat,
pipeline: wgpu::RenderPipeline,
constants: wgpu::BindGroup,
+ ratio: wgpu::Buffer,
texture_layout: wgpu::BindGroupLayout,
sample_count: u32,
targets: Option<Targets>,
+ last_region: Option<Size<u32>>,
}
impl Blit {
@@ -19,27 +24,52 @@ impl Blit {
let sampler =
device.create_sampler(&wgpu::SamplerDescriptor::default());
+ let ratio = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced-wgpu::triangle::msaa ratio"),
+ size: std::mem::size_of::<Ratio>() as u64,
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
+ mapped_at_creation: false,
+ });
+
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::triangle:msaa uniforms layout"),
- entries: &[wgpu::BindGroupLayoutEntry {
- binding: 0,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: wgpu::BindingType::Sampler(
- wgpu::SamplerBindingType::NonFiltering,
- ),
- count: None,
- }],
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(
+ wgpu::SamplerBindingType::NonFiltering,
+ ),
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ },
+ ],
});
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::triangle::msaa uniforms bind group"),
layout: &constant_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: wgpu::BindingResource::Sampler(&sampler),
- }],
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::Sampler(&sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: ratio.as_entire_binding(),
+ },
+ ],
});
let texture_layout =
@@ -112,43 +142,61 @@ impl Blit {
format,
pipeline,
constants: constant_bind_group,
+ ratio,
texture_layout,
sample_count: antialiasing.sample_count(),
targets: None,
+ last_region: None,
}
}
- pub fn targets(
+ pub fn prepare(
&mut self,
device: &wgpu::Device,
- width: u32,
- height: u32,
- ) -> (&wgpu::TextureView, &wgpu::TextureView) {
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ region_size: Size<u32>,
+ ) -> Transformation {
match &mut self.targets {
- None => {
+ Some(targets)
+ if region_size.width <= targets.size.width
+ && region_size.height <= targets.size.height => {}
+ _ => {
self.targets = Some(Targets::new(
device,
self.format,
&self.texture_layout,
self.sample_count,
- width,
- height,
+ region_size,
));
}
- Some(targets) => {
- if targets.width != width || targets.height != height {
- self.targets = Some(Targets::new(
- device,
- self.format,
- &self.texture_layout,
- self.sample_count,
- width,
- height,
- ));
- }
- }
}
+ let targets = self.targets.as_mut().unwrap();
+
+ if Some(region_size) != self.last_region {
+ let ratio = Ratio {
+ u: region_size.width as f32 / targets.size.width as f32,
+ v: region_size.height as f32 / targets.size.height as f32,
+ };
+
+ belt.write_buffer(
+ encoder,
+ &self.ratio,
+ 0,
+ NonZeroU64::new(std::mem::size_of::<Ratio>() as u64)
+ .expect("non-empty ratio"),
+ device,
+ )
+ .copy_from_slice(bytemuck::bytes_of(&ratio));
+
+ self.last_region = Some(region_size);
+ }
+
+ Transformation::orthographic(targets.size.width, targets.size.height)
+ }
+
+ pub fn targets(&self) -> (&wgpu::TextureView, &wgpu::TextureView) {
let targets = self.targets.as_ref().unwrap();
(&targets.attachment, &targets.resolve)
@@ -191,8 +239,7 @@ struct Targets {
attachment: wgpu::TextureView,
resolve: wgpu::TextureView,
bind_group: wgpu::BindGroup,
- width: u32,
- height: u32,
+ size: Size<u32>,
}
impl Targets {
@@ -201,12 +248,11 @@ impl Targets {
format: wgpu::TextureFormat,
texture_layout: &wgpu::BindGroupLayout,
sample_count: u32,
- width: u32,
- height: u32,
+ size: Size<u32>,
) -> Targets {
let extent = wgpu::Extent3d {
- width,
- height,
+ width: size.width,
+ height: size.height,
depth_or_array_layers: 1,
};
@@ -252,8 +298,14 @@ impl Targets {
attachment,
resolve,
bind_group,
- width,
- height,
+ size,
}
}
}
+
+#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+#[repr(C)]
+struct Ratio {
+ u: f32,
+ v: f32,
+}
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 9a3e3b34..095afd48 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -4,18 +4,19 @@ use crate::graphics::color;
use crate::graphics::compositor;
use crate::graphics::error;
use crate::graphics::{self, Viewport};
-use crate::{Backend, Primitive, Renderer, Settings};
+use crate::{Engine, Renderer, Settings};
/// A window graphics backend for iced powered by `wgpu`.
#[allow(missing_debug_implementations)]
pub struct Compositor {
- settings: Settings,
instance: wgpu::Instance,
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
format: wgpu::TextureFormat,
alpha_mode: wgpu::CompositeAlphaMode,
+ engine: Engine,
+ settings: Settings,
}
/// A compositor error.
@@ -53,7 +54,7 @@ impl Compositor {
compatible_window: Option<W>,
) -> Result<Self, Error> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
- backends: settings.internal_backend,
+ backends: settings.backends,
..Default::default()
});
@@ -62,7 +63,7 @@ impl Compositor {
#[cfg(not(target_arch = "wasm32"))]
if log::max_level() >= log::LevelFilter::Info {
let available_adapters: Vec<_> = instance
- .enumerate_adapters(settings.internal_backend)
+ .enumerate_adapters(settings.backends)
.iter()
.map(wgpu::Adapter::get_info)
.collect();
@@ -167,15 +168,24 @@ impl Compositor {
match result {
Ok((device, queue)) => {
+ let engine = Engine::new(
+ &adapter,
+ &device,
+ &queue,
+ format,
+ settings.antialiasing,
+ );
+
return Ok(Compositor {
instance,
- settings,
adapter,
device,
queue,
format,
alpha_mode,
- })
+ engine,
+ settings,
+ });
}
Err(error) => {
errors.push((required_limits, error));
@@ -185,21 +195,9 @@ impl Compositor {
Err(Error::RequestDeviceFailed(errors))
}
-
- /// Creates a new rendering [`Backend`] for this [`Compositor`].
- pub fn create_backend(&self) -> Backend {
- Backend::new(
- &self.adapter,
- &self.device,
- &self.queue,
- self.settings,
- self.format,
- )
- }
}
-/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and
-/// window.
+/// Creates a [`Compositor`] with the given [`Settings`] and window.
pub async fn new<W: compositor::Window>(
settings: Settings,
compatible_window: W,
@@ -207,12 +205,11 @@ pub async fn new<W: compositor::Window>(
Compositor::request(settings, Some(compatible_window)).await
}
-/// Presents the given primitives with the given [`Compositor`] and [`Backend`].
+/// Presents the given primitives with the given [`Compositor`].
pub fn present<T: AsRef<str>>(
compositor: &mut Compositor,
- backend: &mut Backend,
+ renderer: &mut Renderer,
surface: &mut wgpu::Surface<'static>,
- primitives: &[Primitive],
viewport: &Viewport,
background_color: Color,
overlay: &[T],
@@ -229,20 +226,21 @@ pub fn present<T: AsRef<str>>(
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
- backend.present(
+ renderer.present(
+ &mut compositor.engine,
&compositor.device,
&compositor.queue,
&mut encoder,
Some(background_color),
frame.texture.format(),
view,
- primitives,
viewport,
overlay,
);
- // Submit work
- let _submission = compositor.queue.submit(Some(encoder.finish()));
+ let _ = compositor.engine.submit(&compositor.queue, encoder);
+
+ // Present the frame
frame.present();
Ok(())
@@ -274,7 +272,7 @@ impl graphics::Compositor for Compositor {
match backend {
None | Some("wgpu") => Ok(new(
Settings {
- internal_backend: wgpu::util::backend_bits_from_env()
+ backends: wgpu::util::backend_bits_from_env()
.unwrap_or(wgpu::Backends::all()),
..settings.into()
},
@@ -292,7 +290,7 @@ impl graphics::Compositor for Compositor {
fn create_renderer(&self) -> Self::Renderer {
Renderer::new(
- self.create_backend(),
+ &self.engine,
self.settings.default_font,
self.settings.default_text_size,
)
@@ -332,7 +330,7 @@ impl graphics::Compositor for Compositor {
height,
alpha_mode: self.alpha_mode,
view_formats: vec![],
- desired_maximum_frame_latency: 2,
+ desired_maximum_frame_latency: 1,
},
);
}
@@ -354,17 +352,7 @@ impl graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Result<(), compositor::SurfaceError> {
- renderer.with_primitives(|backend, primitives| {
- present(
- self,
- backend,
- surface,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ present(self, renderer, surface, viewport, background_color, overlay)
}
fn screenshot<T: AsRef<str>>(
@@ -375,16 +363,7 @@ impl graphics::Compositor for Compositor {
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
- renderer.with_primitives(|backend, primitives| {
- screenshot(
- self,
- backend,
- primitives,
- viewport,
- background_color,
- overlay,
- )
- })
+ screenshot(self, renderer, viewport, background_color, overlay)
}
}
@@ -392,19 +371,12 @@ impl graphics::Compositor for Compositor {
///
/// Returns RGBA bytes of the texture data.
pub fn screenshot<T: AsRef<str>>(
- compositor: &Compositor,
- backend: &mut Backend,
- primitives: &[Primitive],
+ compositor: &mut Compositor,
+ renderer: &mut Renderer,
viewport: &Viewport,
background_color: Color,
overlay: &[T],
) -> Vec<u8> {
- let mut encoder = compositor.device.create_command_encoder(
- &wgpu::CommandEncoderDescriptor {
- label: Some("iced_wgpu.offscreen.encoder"),
- },
- );
-
let dimensions = BufferDimensions::new(viewport.physical_size());
let texture_extent = wgpu::Extent3d {
@@ -428,14 +400,20 @@ pub fn screenshot<T: AsRef<str>>(
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
- backend.present(
+ let mut encoder = compositor.device.create_command_encoder(
+ &wgpu::CommandEncoderDescriptor {
+ label: Some("iced_wgpu.offscreen.encoder"),
+ },
+ );
+
+ renderer.present(
+ &mut compositor.engine,
&compositor.device,
&compositor.queue,
&mut encoder,
Some(background_color),
texture.format(),
&view,
- primitives,
viewport,
overlay,
);
@@ -473,7 +451,7 @@ pub fn screenshot<T: AsRef<str>>(
texture_extent,
);
- let index = compositor.queue.submit(Some(encoder.finish()));
+ let index = compositor.engine.submit(&compositor.queue, encoder);
let slice = output_buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
diff --git a/widget/Cargo.toml b/widget/Cargo.toml
index a45f47ef..3c9f6a54 100644
--- a/widget/Cargo.toml
+++ b/widget/Cargo.toml
@@ -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
@@ -28,6 +31,7 @@ iced_renderer.workspace = true
iced_runtime.workspace = true
num-traits.workspace = true
+rustc-hash.workspace = true
thiserror.workspace = true
unicode-segmentation.workspace = true
diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs
index 7a21895a..42f92de0 100644
--- a/widget/src/canvas.rs
+++ b/widget/src/canvas.rs
@@ -17,7 +17,7 @@ use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
- Clipboard, Element, Length, Rectangle, Shell, Size, Transformation, Widget,
+ Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget,
};
use crate::graphics::geometry;
@@ -222,8 +222,8 @@ where
let state = tree.state.downcast_ref::<P::State>();
- renderer.with_transformation(
- Transformation::translate(bounds.x, bounds.y),
+ renderer.with_translation(
+ Vector::new(bounds.x, bounds.y),
|renderer| {
let layers =
self.program.draw(state, renderer, theme, bounds, cursor);
diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs
index 48f6abf6..225c316d 100644
--- a/widget/src/checkbox.rs
+++ b/widget/src/checkbox.rs
@@ -340,7 +340,7 @@ where
if self.is_checked {
renderer.fill_text(
text::Text {
- content: &code_point.to_string(),
+ content: code_point.to_string(),
font: *font,
size,
line_height: *line_height,
diff --git a/widget/src/column.rs b/widget/src/column.rs
index d37ef695..df7829b3 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -33,11 +33,18 @@ where
Self::from_vec(Vec::new())
}
+ /// Creates a [`Column`] with the given capacity.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self::from_vec(Vec::with_capacity(capacity))
+ }
+
/// Creates a [`Column`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
- Self::new().extend(children)
+ let iterator = children.into_iter();
+
+ Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Column`] from an already allocated [`Vec`].
diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs
index e4f4a41f..253850df 100644
--- a/widget/src/combo_box.rs
+++ b/widget/src/combo_box.rs
@@ -700,38 +700,47 @@ where
..
} = tree.state.downcast_mut::<Menu<T>>();
- let bounds = layout.bounds();
-
self.state.sync_filtered_options(filtered_options);
- let mut menu = menu::Menu::new(
- menu,
- &filtered_options.options,
- hovered_option,
- |x| {
- tree.children[0]
- .state
- .downcast_mut::<text_input::State<Renderer::Paragraph>>(
- )
- .unfocus();
-
- (self.on_selected)(x)
- },
- self.on_option_hovered.as_deref(),
- &self.menu_class,
- )
- .width(bounds.width)
- .padding(self.padding);
-
- if let Some(font) = self.font {
- menu = menu.font(font);
- }
+ if filtered_options.options.is_empty() {
+ None
+ } else {
+ let bounds = layout.bounds();
+
+ let mut menu = menu::Menu::new(
+ menu,
+ &filtered_options.options,
+ hovered_option,
+ |x| {
+ tree.children[0]
+ .state
+ .downcast_mut::<text_input::State<Renderer::Paragraph>>(
+ )
+ .unfocus();
+
+ (self.on_selected)(x)
+ },
+ self.on_option_hovered.as_deref(),
+ &self.menu_class,
+ )
+ .width(bounds.width)
+ .padding(self.padding);
+
+ if let Some(font) = self.font {
+ menu = menu.font(font);
+ }
- if let Some(size) = self.size {
- menu = menu.text_size(size);
- }
+ if let Some(size) = self.size {
+ menu = menu.text_size(size);
+ }
- Some(menu.overlay(layout.position() + translation, bounds.height))
+ Some(
+ menu.overlay(
+ layout.position() + translation,
+ bounds.height,
+ ),
+ )
+ }
} else {
None
}
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs
index 77b30882..61789c19 100644
--- a/widget/src/helpers.rs
+++ b/widget/src/helpers.rs
@@ -145,13 +145,26 @@ where
///
/// [`Text`]: core::widget::Text
pub fn text<'a, Theme, Renderer>(
- text: impl ToString,
+ text: impl text::IntoFragment<'a>,
) -> Text<'a, Theme, Renderer>
where
Theme: text::Catalog + 'a,
Renderer: core::text::Renderer,
{
- Text::new(text.to_string())
+ Text::new(text)
+}
+
+/// Creates a new [`Text`] widget that displays the provided value.
+///
+/// [`Text`]: core::widget::Text
+pub fn value<'a, Theme, Renderer>(
+ value: impl ToString,
+) -> Text<'a, Theme, Renderer>
+where
+ Theme: text::Catalog + 'a,
+ Renderer: core::text::Renderer,
+{
+ Text::new(value.to_string())
}
/// Creates a new [`Checkbox`].
diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs
index 8a8d5fe7..a34ce9e6 100644
--- a/widget/src/keyed/column.rs
+++ b/widget/src/keyed/column.rs
@@ -40,27 +40,49 @@ where
{
/// Creates an empty [`Column`].
pub fn new() -> Self {
- Column {
+ Self::from_vecs(Vec::new(), Vec::new())
+ }
+
+ /// Creates a [`Column`] from already allocated [`Vec`]s.
+ ///
+ /// Keep in mind that the [`Column`] will not inspect the [`Vec`]s, which means
+ /// it won't automatically adapt to the sizing strategy of its contents.
+ ///
+ /// If any of the children have a [`Length::Fill`] strategy, you will need to
+ /// call [`Column::width`] or [`Column::height`] accordingly.
+ pub fn from_vecs(
+ keys: Vec<Key>,
+ children: Vec<Element<'a, Message, Theme, Renderer>>,
+ ) -> Self {
+ Self {
spacing: 0.0,
padding: Padding::ZERO,
width: Length::Shrink,
height: Length::Shrink,
max_width: f32::INFINITY,
align_items: Alignment::Start,
- keys: Vec::new(),
- children: Vec::new(),
+ keys,
+ children,
}
}
+ /// Creates a [`Column`] with the given capacity.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self::from_vecs(
+ Vec::with_capacity(capacity),
+ Vec::with_capacity(capacity),
+ )
+ }
+
/// Creates a [`Column`] with the given elements.
pub fn with_children(
children: impl IntoIterator<
Item = (Key, Element<'a, Message, Theme, Renderer>),
>,
) -> Self {
- children
- .into_iter()
- .fold(Self::new(), |column, (key, child)| column.push(key, child))
+ let iterator = children.into_iter();
+
+ Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Sets the vertical spacing _between_ elements.
@@ -132,6 +154,18 @@ where
self
}
}
+
+ /// Extends the [`Column`] with the given children.
+ pub fn extend(
+ self,
+ children: impl IntoIterator<
+ Item = (Key, Element<'a, Message, Theme, Renderer>),
+ >,
+ ) -> Self {
+ children
+ .into_iter()
+ .fold(self, |column, (key, child)| column.push(key, child))
+ }
}
impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer>
diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs
index eb663ea5..04783dbe 100644
--- a/widget/src/lazy.rs
+++ b/widget/src/lazy.rs
@@ -18,11 +18,12 @@ use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::Element;
use crate::core::{
- self, Clipboard, Hasher, Length, Point, Rectangle, Shell, Size, Vector,
+ self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector,
};
use crate::runtime::overlay::Nested;
use ouroboros::self_referencing;
+use rustc_hash::FxHasher;
use std::cell::RefCell;
use std::hash::{Hash, Hasher as H};
use std::rc::Rc;
@@ -106,9 +107,12 @@ where
}
fn state(&self) -> tree::State {
- let mut hasher = Hasher::default();
- self.dependency.hash(&mut hasher);
- let hash = hasher.finish();
+ let hash = {
+ let mut hasher = FxHasher::default();
+ self.dependency.hash(&mut hasher);
+
+ hasher.finish()
+ };
let element =
Rc::new(RefCell::new(Some((self.view)(&self.dependency).into())));
@@ -127,9 +131,12 @@ where
.state
.downcast_mut::<Internal<Message, Theme, Renderer>>();
- let mut hasher = Hasher::default();
- self.dependency.hash(&mut hasher);
- let new_hash = hasher.finish();
+ let new_hash = {
+ let mut hasher = FxHasher::default();
+ self.dependency.hash(&mut hasher);
+
+ hasher.finish()
+ };
if current.hash != new_hash {
current.hash = new_hash;
diff --git a/widget/src/lib.rs b/widget/src/lib.rs
index 209dfad9..1eeacbae 100644
--- a/widget/src/lib.rs
+++ b/widget/src/lib.rs
@@ -2,13 +2,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 iced_renderer as renderer;
pub use iced_renderer::graphics;
diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs
index d76caa8a..98efe305 100644
--- a/widget/src/overlay/menu.rs
+++ b/widget/src/overlay/menu.rs
@@ -526,7 +526,7 @@ where
renderer.fill_text(
Text {
- content: &option.to_string(),
+ content: option.to_string(),
bounds: Size::new(f32::INFINITY, bounds.height),
size: text_size,
line_height: self.text_line_height,
diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs
index 481cd770..c20c3b9c 100644
--- a/widget/src/pane_grid/state.rs
+++ b/widget/src/pane_grid/state.rs
@@ -6,7 +6,7 @@ use crate::pane_grid::{
Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target,
};
-use std::collections::HashMap;
+use rustc_hash::FxHashMap;
/// The state of a [`PaneGrid`].
///
@@ -25,7 +25,7 @@ pub struct State<T> {
/// The panes of the [`PaneGrid`].
///
/// [`PaneGrid`]: super::PaneGrid
- pub panes: HashMap<Pane, T>,
+ pub panes: FxHashMap<Pane, T>,
/// The internal state of the [`PaneGrid`].
///
@@ -52,7 +52,7 @@ impl<T> State<T> {
/// Creates a new [`State`] with the given [`Configuration`].
pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {
- let mut panes = HashMap::new();
+ let mut panes = FxHashMap::default();
let internal =
Internal::from_configuration(&mut panes, config.into(), 0);
@@ -353,7 +353,7 @@ impl Internal {
///
/// [`PaneGrid`]: super::PaneGrid
pub fn from_configuration<T>(
- panes: &mut HashMap<Pane, T>,
+ panes: &mut FxHashMap<Pane, T>,
content: Configuration<T>,
next_id: usize,
) -> Self {
diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs
index 801e792b..edccfdaa 100644
--- a/widget/src/pick_list.rs
+++ b/widget/src/pick_list.rs
@@ -479,7 +479,7 @@ where
renderer.fill_text(
Text {
- content: &code_point.to_string(),
+ content: code_point.to_string(),
size,
line_height,
font,
@@ -502,7 +502,7 @@ where
let label = selected.map(ToString::to_string);
- if let Some(label) = label.as_deref().or(self.placeholder.as_deref()) {
+ if let Some(label) = label.or_else(|| self.placeholder.clone()) {
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 47feff9c..fa352171 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -31,11 +31,18 @@ where
Self::from_vec(Vec::new())
}
+ /// Creates a [`Row`] with the given capacity.
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self::from_vec(Vec::with_capacity(capacity))
+ }
+
/// Creates a [`Row`] with the given elements.
pub fn with_children(
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
) -> Self {
- Self::new().extend(children)
+ let iterator = children.into_iter();
+
+ Self::with_capacity(iterator.size_hint().0).extend(iterator)
}
/// Creates a [`Row`] from an already allocated [`Vec`].
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index 84e9ac15..668c5372 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -651,7 +651,7 @@ where
defaults: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
- _viewport: &Rectangle,
+ viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
@@ -767,8 +767,8 @@ where
renderer.with_layer(
Rectangle {
- width: bounds.width + 2.0,
- height: bounds.height + 2.0,
+ width: (bounds.width + 2.0).min(viewport.width),
+ height: (bounds.height + 2.0).min(viewport.height),
..bounds
},
|renderer| {
diff --git a/widget/src/shader.rs b/widget/src/shader.rs
index 68112f83..fad2f4eb 100644
--- a/widget/src/shader.rs
+++ b/widget/src/shader.rs
@@ -13,12 +13,13 @@ use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Widget};
use crate::core::window;
use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size};
-use crate::renderer::wgpu::primitive::pipeline;
+use crate::renderer::wgpu::primitive;
use std::marker::PhantomData;
+pub use crate::graphics::Viewport;
pub use crate::renderer::wgpu::wgpu;
-pub use pipeline::{Primitive, Storage};
+pub use primitive::{Primitive, Storage};
/// A widget which can render custom shaders with Iced's `wgpu` backend.
///
@@ -60,7 +61,7 @@ impl<P, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Shader<Message, P>
where
P: Program<Message>,
- Renderer: pipeline::Renderer,
+ Renderer: primitive::Renderer,
{
fn tag(&self) -> tree::Tag {
struct Tag<T>(T);
@@ -160,7 +161,7 @@ where
let bounds = layout.bounds();
let state = tree.state.downcast_ref::<P::State>();
- renderer.draw_pipeline_primitive(
+ renderer.draw_primitive(
bounds,
self.program.draw(state, cursor_position, bounds),
);
@@ -171,7 +172,7 @@ impl<'a, Message, Theme, Renderer, P> From<Shader<Message, P>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
- Renderer: pipeline::Renderer,
+ Renderer: primitive::Renderer,
P: Program<Message> + 'a,
{
fn from(
diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs
index 6dd50404..902c7c3b 100644
--- a/widget/src/shader/program.rs
+++ b/widget/src/shader/program.rs
@@ -1,7 +1,7 @@
use crate::core::event;
use crate::core::mouse;
use crate::core::{Rectangle, Shell};
-use crate::renderer::wgpu::primitive::pipeline;
+use crate::renderer::wgpu::Primitive;
use crate::shader;
/// The state and logic of a [`Shader`] widget.
@@ -15,7 +15,7 @@ pub trait Program<Message> {
type State: Default + 'static;
/// The type of primitive this [`Program`] can draw.
- type Primitive: pipeline::Primitive + 'static;
+ type Primitive: Primitive + 'static;
/// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes
/// based on mouse & other events. You can use the [`Shell`] to publish messages, request a
diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs
index a00df3c7..92cdb251 100644
--- a/widget/src/text_editor.rs
+++ b/widget/src/text_editor.rs
@@ -110,6 +110,21 @@ where
self
}
+ /// Sets the text size of the [`TextEditor`].
+ pub fn size(mut self, size: impl Into<Pixels>) -> Self {
+ self.text_size = Some(size.into());
+ self
+ }
+
+ /// Sets the [`text::LineHeight`] of the [`TextEditor`].
+ pub fn line_height(
+ mut self,
+ line_height: impl Into<text::LineHeight>,
+ ) -> Self {
+ self.line_height = line_height.into();
+ self
+ }
+
/// Sets the [`Padding`] of the [`TextEditor`].
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs
index dafe2fca..e9f07838 100644
--- a/widget/src/text_input.rs
+++ b/widget/src/text_input.rs
@@ -232,7 +232,7 @@ where
let placeholder_text = Text {
font,
line_height: self.line_height,
- content: &self.placeholder,
+ content: self.placeholder.as_str(),
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
@@ -251,9 +251,11 @@ where
});
if let Some(icon) = &self.icon {
+ let mut content = [0; 4];
+
let icon_text = Text {
line_height: self.line_height,
- content: &icon.code_point.to_string(),
+ content: icon.code_point.encode_utf8(&mut content) as &_,
font: icon.font,
size: icon.size.unwrap_or_else(|| renderer.default_size()),
bounds: Size::new(f32::INFINITY, text_bounds.height),
@@ -366,7 +368,7 @@ where
let text = value.to_string();
- let (cursor, offset) = if let Some(focus) = state
+ let (cursor, offset, is_selecting) = if let Some(focus) = state
.is_focused
.as_ref()
.filter(|focus| focus.is_window_focused)
@@ -404,7 +406,7 @@ where
None
};
- (cursor, offset)
+ (cursor, offset, false)
}
cursor::State::Selection { start, end } => {
let left = start.min(end);
@@ -444,11 +446,12 @@ where
} else {
left_offset
},
+ true,
)
}
}
} else {
- (None, 0.0)
+ (None, 0.0, false)
};
let draw = |renderer: &mut Renderer, viewport| {
@@ -480,7 +483,7 @@ where
);
};
- if cursor.is_some() {
+ if is_selecting {
renderer
.with_layer(text_bounds, |renderer| draw(renderer, *viewport));
} else {
@@ -710,7 +713,8 @@ where
match key.as_ref() {
keyboard::Key::Character("c")
- if state.keyboard_modifiers.command() =>
+ if state.keyboard_modifiers.command()
+ && !self.is_secure =>
{
if let Some((start, end)) =
state.cursor.selection(&self.value)
@@ -724,7 +728,8 @@ where
return event::Status::Captured;
}
keyboard::Key::Character("x")
- if state.keyboard_modifiers.command() =>
+ if state.keyboard_modifiers.command()
+ && !self.is_secure =>
{
if let Some((start, end)) =
state.cursor.selection(&self.value)
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 9d65cc1b..dccb7c07 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -10,6 +10,9 @@ homepage.workspace = true
categories.workspace = true
keywords.workspace = true
+[lints]
+workspace = true
+
[features]
default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
debug = ["iced_runtime/debug"]
@@ -26,6 +29,7 @@ iced_graphics.workspace = true
iced_runtime.workspace = true
log.workspace = true
+rustc-hash.workspace = true
thiserror.workspace = true
tracing.workspace = true
window_clipboard.workspace = true
@@ -40,3 +44,4 @@ winapi.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys.workspace = true
web-sys.features = ["Document", "Window"]
+
diff --git a/winit/src/application.rs b/winit/src/application.rs
index d68523fa..1ca80609 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -220,13 +220,11 @@ where
};
}
- let compositor = C::new(graphics_settings, window.clone()).await?;
- let mut renderer = compositor.create_renderer();
+ let mut compositor = C::new(graphics_settings, window.clone()).await?;
+ let renderer = compositor.create_renderer();
for font in settings.fonts {
- use crate::core::text::Renderer;
-
- renderer.load_font(font);
+ compositor.load_font(font);
}
let (mut event_sender, event_receiver) = mpsc::unbounded();
@@ -950,10 +948,8 @@ pub fn run_command<A, C, E>(
*cache = current_cache;
}
command::Action::LoadFont { bytes, tagger } => {
- use crate::core::text::Renderer;
-
// TODO: Error handling (?)
- renderer.load_font(bytes);
+ compositor.load_font(bytes);
proxy
.send_event(tagger(Ok(())))
diff --git a/winit/src/lib.rs b/winit/src/lib.rs
index 64912b3f..3619cde8 100644
--- a/winit/src/lib.rs
+++ b/winit/src/lib.rs
@@ -17,14 +17,6 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
-#![forbid(rust_2018_idioms)]
-#![deny(
- missing_debug_implementations,
- missing_docs,
- unused_results,
- unsafe_code,
- rustdoc::broken_intra_doc_links
-)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub use iced_graphics as graphics;
pub use iced_runtime as runtime;
diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs
index b4c25411..3537ac18 100644
--- a/winit/src/multi_window.rs
+++ b/winit/src/multi_window.rs
@@ -27,7 +27,7 @@ use crate::{Clipboard, Error, Proxy, Settings};
pub use crate::application::{default, Appearance, DefaultStyle};
-use std::collections::HashMap;
+use rustc_hash::FxHashMap;
use std::mem::ManuallyDrop;
use std::sync::Arc;
use std::time::Instant;
@@ -381,12 +381,12 @@ async fn run_instance<A, E, C>(
)]
};
- let mut ui_caches = HashMap::new();
+ let mut ui_caches = FxHashMap::default();
let mut user_interfaces = ManuallyDrop::new(build_user_interfaces(
&application,
&mut debug,
&mut window_manager,
- HashMap::from_iter([(
+ FxHashMap::from_iter([(
window::Id::MAIN,
user_interface::Cache::default(),
)]),
@@ -759,7 +759,7 @@ async fn run_instance<A, E, C>(
// TODO mw application update returns which window IDs to update
if !messages.is_empty() || uis_stale {
- let mut cached_interfaces: HashMap<
+ let mut cached_interfaces: FxHashMap<
window::Id,
user_interface::Cache,
> = ManuallyDrop::into_inner(user_interfaces)
@@ -849,7 +849,7 @@ fn update<A: Application, C, E: Executor>(
debug: &mut Debug,
messages: &mut Vec<A::Message>,
window_manager: &mut WindowManager<A, C>,
- ui_caches: &mut HashMap<window::Id, user_interface::Cache>,
+ ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>,
) where
C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: DefaultStyle,
@@ -890,7 +890,7 @@ fn run_command<A, C, E>(
proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
debug: &mut Debug,
window_manager: &mut WindowManager<A, C>,
- ui_caches: &mut HashMap<window::Id, user_interface::Cache>,
+ ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>,
) where
A: Application,
E: Executor,
@@ -1194,13 +1194,8 @@ fn run_command<A, C, E>(
uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect();
}
command::Action::LoadFont { bytes, tagger } => {
- use crate::core::text::Renderer;
-
- // TODO change this once we change each renderer to having a single backend reference.. :pain:
// TODO: Error handling (?)
- for (_, window) in window_manager.iter_mut() {
- window.renderer.load_font(bytes.clone());
- }
+ compositor.load_font(bytes.clone());
proxy
.send_event(tagger(Ok(())))
@@ -1218,8 +1213,8 @@ pub fn build_user_interfaces<'a, A: Application, C: Compositor>(
application: &'a A,
debug: &mut Debug,
window_manager: &mut WindowManager<A, C>,
- mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>,
-) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>>
+ mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>,
+) -> FxHashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>>
where
C: Compositor<Renderer = A::Renderer>,
A::Theme: DefaultStyle,