From 7d2d813343baae10c2667590463385d50abb784c Mon Sep 17 00:00:00 2001 From: Giuliano Bellini s294739 Date: Mon, 23 Jan 2023 16:57:24 +0100 Subject: added new style for scrollable, to be applied when mouse is over the scrollable area --- native/src/widget/scrollable.rs | 4 ++++ style/src/scrollable.rs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 82286036..de6eacb5 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -856,6 +856,8 @@ pub fn draw( theme.dragging(style) } else if mouse_over_y_scrollbar { theme.hovered(style) + } else if mouse_over_scrollable { + theme.focused(style) } else { theme.active(style) }; @@ -869,6 +871,8 @@ pub fn draw( theme.dragging_horizontal(style) } else if mouse_over_x_scrollbar { theme.hovered_horizontal(style) + } else if mouse_over_scrollable { + theme.focused_horizontal(style) } else { theme.active_horizontal(style) }; diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs index 64ed8462..a3f3db20 100644 --- a/style/src/scrollable.rs +++ b/style/src/scrollable.rs @@ -45,6 +45,11 @@ pub trait StyleSheet { self.hovered(style) } + /// Produces the style of a scrollbar when mouse is over the scrollable area. + fn focused(&self, style: &Self::Style) -> Scrollbar { + self.active(style) + } + /// Produces the style of an active horizontal scrollbar. fn active_horizontal(&self, style: &Self::Style) -> Scrollbar { self.active(style) @@ -59,4 +64,9 @@ pub trait StyleSheet { fn dragging_horizontal(&self, style: &Self::Style) -> Scrollbar { self.hovered_horizontal(style) } + + /// Produces the style of a horizontal scrollbar when mouse is over the scrollable area. + fn focused_horizontal(&self, style: &Self::Style) -> Scrollbar { + self.active_horizontal(style) + } } -- cgit From eaa2238600a0d3055b379592c7a3f8e6453b9dc7 Mon Sep 17 00:00:00 2001 From: Giuliano Bellini s294739 Date: Mon, 23 Jan 2023 17:32:08 +0100 Subject: debugging focused style not working --- native/src/widget/scrollable.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index de6eacb5..71814034 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -859,7 +859,7 @@ pub fn draw( } else if mouse_over_scrollable { theme.focused(style) } else { - theme.active(style) + theme.focused(style) }; draw_scrollbar(renderer, style, &scrollbar); @@ -874,7 +874,7 @@ pub fn draw( } else if mouse_over_scrollable { theme.focused_horizontal(style) } else { - theme.active_horizontal(style) + theme.focused_horizontal(style) }; draw_scrollbar(renderer, style, &scrollbar); -- cgit From 49e9a9a5379c1e9a9469045ca9a51ffb860ee620 Mon Sep 17 00:00:00 2001 From: Giuliano Bellini s294739 Date: Mon, 23 Jan 2023 17:56:39 +0100 Subject: added function focused and focused_horizontal to theme.rs --- native/src/widget/scrollable.rs | 4 ++-- style/src/theme.rs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 71814034..de6eacb5 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -859,7 +859,7 @@ pub fn draw( } else if mouse_over_scrollable { theme.focused(style) } else { - theme.focused(style) + theme.active(style) }; draw_scrollbar(renderer, style, &scrollbar); @@ -874,7 +874,7 @@ pub fn draw( } else if mouse_over_scrollable { theme.focused_horizontal(style) } else { - theme.focused_horizontal(style) + theme.active_horizontal(style) }; draw_scrollbar(renderer, style, &scrollbar); diff --git a/style/src/theme.rs b/style/src/theme.rs index 55bfa4ca..8d40bda1 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -935,6 +935,13 @@ impl scrollable::StyleSheet for Theme { } } + fn focused(&self, style: &Self::Style) -> scrollable::Scrollbar { + match style { + Scrollable::Default => self.active(style), + Scrollable::Custom(custom) => custom.focused(self), + } + } + fn active_horizontal(&self, style: &Self::Style) -> scrollable::Scrollbar { match style { Scrollable::Default => self.active(style), @@ -958,6 +965,16 @@ impl scrollable::StyleSheet for Theme { Scrollable::Custom(custom) => custom.dragging_horizontal(self), } } + + fn focused_horizontal( + &self, + style: &Self::Style, + ) -> scrollable::Scrollbar { + match style { + Scrollable::Default => self.active_horizontal(style), + Scrollable::Custom(custom) => custom.focused_horizontal(self), + } + } } /// The style of text. -- cgit From b9a9576207ddfc7afd89da30b7cfc7ca0d7e335c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 6 Jan 2023 23:29:38 +0100 Subject: Remove `iced_glow`, `glyph-brush`, and `wgpu_glyph` dependencies --- Cargo.toml | 11 +- examples/integration/.gitignore | 2 + examples/integration/Cargo.toml | 22 + examples/integration/README.md | 36 ++ examples/integration/index.html | 21 + examples/integration/src/controls.rs | 112 +++++ examples/integration/src/main.rs | 289 ++++++++++++ examples/integration/src/scene.rs | 102 +++++ examples/integration/src/shader/frag.wgsl | 4 + examples/integration/src/shader/vert.wgsl | 6 + examples/integration_opengl/Cargo.toml | 12 - examples/integration_opengl/README.md | 16 - examples/integration_opengl/src/controls.rs | 101 ----- examples/integration_opengl/src/main.rs | 187 -------- examples/integration_opengl/src/scene.rs | 102 ----- examples/integration_wgpu/.gitignore | 2 - examples/integration_wgpu/Cargo.toml | 22 - examples/integration_wgpu/README.md | 36 -- examples/integration_wgpu/index.html | 21 - examples/integration_wgpu/src/controls.rs | 112 ----- examples/integration_wgpu/src/main.rs | 289 ------------ examples/integration_wgpu/src/scene.rs | 102 ----- examples/integration_wgpu/src/shader/frag.wgsl | 4 - examples/integration_wgpu/src/shader/vert.wgsl | 6 - glow/Cargo.toml | 51 --- glow/README.md | 51 --- glow/src/backend.rs | 280 ------------ glow/src/image.rs | 254 ----------- glow/src/image/storage.rs | 78 ---- glow/src/lib.rs | 53 --- glow/src/program.rs | 133 ------ glow/src/quad.rs | 74 --- glow/src/quad/compatibility.rs | 349 --------------- glow/src/quad/core.rs | 244 ---------- glow/src/settings.rs | 61 --- glow/src/shader/common/gradient.frag | 59 --- glow/src/shader/common/gradient.vert | 9 - glow/src/shader/common/image.frag | 22 - glow/src/shader/common/image.vert | 9 - glow/src/shader/common/solid.frag | 18 - glow/src/shader/common/solid.vert | 11 - glow/src/shader/compatibility/quad.frag | 83 ---- glow/src/shader/compatibility/quad.vert | 46 -- glow/src/shader/core/quad.frag | 95 ---- glow/src/shader/core/quad.vert | 52 --- glow/src/text.rs | 257 ----------- glow/src/triangle.rs | 595 ------------------------- glow/src/window.rs | 4 - glow/src/window/compositor.rs | 111 ----- graphics/Cargo.toml | 7 - graphics/src/font.rs | 35 -- graphics/src/font/source.rs | 45 -- graphics/src/lib.rs | 1 - wgpu/Cargo.toml | 4 - wgpu/src/backend.rs | 86 +--- wgpu/src/text.rs | 264 +---------- 56 files changed, 621 insertions(+), 4437 deletions(-) create mode 100644 examples/integration/.gitignore create mode 100644 examples/integration/Cargo.toml create mode 100644 examples/integration/README.md create mode 100644 examples/integration/index.html create mode 100644 examples/integration/src/controls.rs create mode 100644 examples/integration/src/main.rs create mode 100644 examples/integration/src/scene.rs create mode 100644 examples/integration/src/shader/frag.wgsl create mode 100644 examples/integration/src/shader/vert.wgsl delete mode 100644 examples/integration_opengl/Cargo.toml delete mode 100644 examples/integration_opengl/README.md delete mode 100644 examples/integration_opengl/src/controls.rs delete mode 100644 examples/integration_opengl/src/main.rs delete mode 100644 examples/integration_opengl/src/scene.rs delete mode 100644 examples/integration_wgpu/.gitignore delete mode 100644 examples/integration_wgpu/Cargo.toml delete mode 100644 examples/integration_wgpu/README.md delete mode 100644 examples/integration_wgpu/index.html delete mode 100644 examples/integration_wgpu/src/controls.rs delete mode 100644 examples/integration_wgpu/src/main.rs delete mode 100644 examples/integration_wgpu/src/scene.rs delete mode 100644 examples/integration_wgpu/src/shader/frag.wgsl delete mode 100644 examples/integration_wgpu/src/shader/vert.wgsl delete mode 100644 glow/Cargo.toml delete mode 100644 glow/README.md delete mode 100644 glow/src/backend.rs delete mode 100644 glow/src/image.rs delete mode 100644 glow/src/image/storage.rs delete mode 100644 glow/src/lib.rs delete mode 100644 glow/src/program.rs delete mode 100644 glow/src/quad.rs delete mode 100644 glow/src/quad/compatibility.rs delete mode 100644 glow/src/quad/core.rs delete mode 100644 glow/src/settings.rs delete mode 100644 glow/src/shader/common/gradient.frag delete mode 100644 glow/src/shader/common/gradient.vert delete mode 100644 glow/src/shader/common/image.frag delete mode 100644 glow/src/shader/common/image.vert delete mode 100644 glow/src/shader/common/solid.frag delete mode 100644 glow/src/shader/common/solid.vert delete mode 100644 glow/src/shader/compatibility/quad.frag delete mode 100644 glow/src/shader/compatibility/quad.vert delete mode 100644 glow/src/shader/core/quad.frag delete mode 100644 glow/src/shader/core/quad.vert delete mode 100644 glow/src/text.rs delete mode 100644 glow/src/triangle.rs delete mode 100644 glow/src/window.rs delete mode 100644 glow/src/window/compositor.rs delete mode 100644 graphics/src/font.rs delete mode 100644 graphics/src/font/source.rs diff --git a/Cargo.toml b/Cargo.toml index d26ec2b6..42c9488c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,19 +14,15 @@ categories = ["gui"] [features] default = ["wgpu"] # Enables the `Image` widget -image = ["iced_wgpu?/image", "iced_glow?/image", "image_rs"] +image = ["iced_wgpu?/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_wgpu?/svg", "iced_glow?/svg"] +svg = ["iced_wgpu?/svg"] # Enables the `Canvas` widget canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget qr_code = ["iced_graphics/qr_code"] # Enables the `iced_wgpu` renderer wgpu = ["iced_wgpu"] -# Enables using system fonts -default_system_font = ["iced_wgpu?/default_system_font", "iced_glow?/default_system_font"] -# Enables the `iced_glow` renderer. Overrides `iced_wgpu` -glow = ["iced_glow", "iced_glutin"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -44,7 +40,6 @@ chrome-trace = [ "iced_winit/chrome-trace", "iced_glutin?/trace", "iced_wgpu?/tracing", - "iced_glow?/tracing", ] [badges] @@ -55,7 +50,6 @@ members = [ "core", "futures", "graphics", - "glow", "glutin", "lazy", "native", @@ -72,7 +66,6 @@ iced_native = { version = "0.9", path = "native" } iced_graphics = { version = "0.7", path = "graphics" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } iced_glutin = { version = "0.7", path = "glutin", optional = true } -iced_glow = { version = "0.7", path = "glow", optional = true } thiserror = "1.0" [dependencies.image_rs] diff --git a/examples/integration/.gitignore b/examples/integration/.gitignore new file mode 100644 index 00000000..e188dc28 --- /dev/null +++ b/examples/integration/.gitignore @@ -0,0 +1,2 @@ +*.wasm +*.js diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml new file mode 100644 index 00000000..200306aa --- /dev/null +++ b/examples/integration/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "integration" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2021" +publish = false + +[dependencies] +iced_winit = { path = "../../winit" } +iced_wgpu = { path = "../../wgpu", features = ["webgl"] } +env_logger = "0.8" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1.7" +console_log = "0.2.0" +log = "0.4" +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } +# This dependency a little bit quirky, it is deep in the tree and without `js` feature it +# refuses to work with `wasm32-unknown-unknown target`. Unfortunately, we need this patch +# to make it work +getrandom = { version = "0.2", features = ["js"] } diff --git a/examples/integration/README.md b/examples/integration/README.md new file mode 100644 index 00000000..ece9ba1e --- /dev/null +++ b/examples/integration/README.md @@ -0,0 +1,36 @@ +## `wgpu` integration + +A demonstration of how to integrate Iced in an existing [`wgpu`] application. + +The __[`main`]__ file contains all the code of the example. + + + +You can run it with `cargo run`: +``` +cargo run --package integration_wgpu +``` + +### How to run this example with WebGL backend +NOTE: Currently, WebGL backend is is still experimental, so expect bugs. + +```sh +# 0. Install prerequisites +cargo install wasm-bindgen-cli https +# 1. cd to the current folder +# 2. Compile wasm module +cargo build -p integration_wgpu --target wasm32-unknown-unknown +# 3. Invoke wasm-bindgen +wasm-bindgen ../../target/wasm32-unknown-unknown/debug/integration_wgpu.wasm --out-dir . --target web --no-typescript +# 4. run http server +http +# 5. Open 127.0.0.1:8000 in browser +``` + + +[`main`]: src/main.rs +[`wgpu`]: https://github.com/gfx-rs/wgpu diff --git a/examples/integration/index.html b/examples/integration/index.html new file mode 100644 index 00000000..920bc4a0 --- /dev/null +++ b/examples/integration/index.html @@ -0,0 +1,21 @@ + + + + + Iced - wgpu + WebGL integration + + +

integration_wgpu

+ + + + + diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs new file mode 100644 index 00000000..533cb6e2 --- /dev/null +++ b/examples/integration/src/controls.rs @@ -0,0 +1,112 @@ +use iced_wgpu::Renderer; +use iced_winit::widget::{slider, text_input, Column, Row, Text}; +use iced_winit::{Alignment, Color, Command, Element, Length, Program}; + +pub struct Controls { + background_color: Color, + text: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + BackgroundColorChanged(Color), + TextChanged(String), +} + +impl Controls { + pub fn new() -> Controls { + Controls { + background_color: Color::BLACK, + text: Default::default(), + } + } + + pub fn background_color(&self) -> Color { + self.background_color + } +} + +impl Program for Controls { + type Renderer = Renderer; + type Message = Message; + + fn update(&mut self, message: Message) -> Command { + match message { + Message::BackgroundColorChanged(color) => { + self.background_color = color; + } + Message::TextChanged(text) => { + self.text = text; + } + } + + Command::none() + } + + fn view(&self) -> Element { + let background_color = self.background_color; + let text = &self.text; + + let sliders = Row::new() + .width(500) + .spacing(20) + .push( + slider(0.0..=1.0, background_color.r, move |r| { + Message::BackgroundColorChanged(Color { + r, + ..background_color + }) + }) + .step(0.01), + ) + .push( + slider(0.0..=1.0, background_color.g, move |g| { + Message::BackgroundColorChanged(Color { + g, + ..background_color + }) + }) + .step(0.01), + ) + .push( + slider(0.0..=1.0, background_color.b, move |b| { + Message::BackgroundColorChanged(Color { + b, + ..background_color + }) + }) + .step(0.01), + ); + + Row::new() + .width(Length::Fill) + .height(Length::Fill) + .align_items(Alignment::End) + .push( + Column::new() + .width(Length::Fill) + .align_items(Alignment::End) + .push( + Column::new() + .padding(10) + .spacing(10) + .push( + Text::new("Background color") + .style(Color::WHITE), + ) + .push(sliders) + .push( + Text::new(format!("{background_color:?}")) + .size(14) + .style(Color::WHITE), + ) + .push(text_input( + "Placeholder", + text, + Message::TextChanged, + )), + ), + ) + .into() + } +} diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs new file mode 100644 index 00000000..2a56b6fa --- /dev/null +++ b/examples/integration/src/main.rs @@ -0,0 +1,289 @@ +mod controls; +mod scene; + +use controls::Controls; +use scene::Scene; + +use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; +use iced_winit::{ + conversion, futures, program, renderer, winit, Clipboard, Color, Debug, + Size, +}; + +use winit::{ + dpi::PhysicalPosition, + event::{Event, ModifiersState, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, +}; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; +#[cfg(target_arch = "wasm32")] +use web_sys::HtmlCanvasElement; +#[cfg(target_arch = "wasm32")] +use winit::platform::web::WindowBuilderExtWebSys; + +pub fn main() { + #[cfg(target_arch = "wasm32")] + let canvas_element = { + console_log::init_with_level(log::Level::Debug) + .expect("could not initialize logger"); + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.get_element_by_id("iced_canvas")) + .and_then(|element| element.dyn_into::().ok()) + .expect("Canvas with id `iced_canvas` is missing") + }; + #[cfg(not(target_arch = "wasm32"))] + env_logger::init(); + + // Initialize winit + let event_loop = EventLoop::new(); + + #[cfg(target_arch = "wasm32")] + let window = winit::window::WindowBuilder::new() + .with_canvas(Some(canvas_element)) + .build(&event_loop) + .expect("Failed to build winit window"); + + #[cfg(not(target_arch = "wasm32"))] + let window = winit::window::Window::new(&event_loop).unwrap(); + + let physical_size = window.inner_size(); + let mut viewport = Viewport::with_physical_size( + Size::new(physical_size.width, physical_size.height), + window.scale_factor(), + ); + let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); + let mut modifiers = ModifiersState::default(); + let mut clipboard = Clipboard::connect(&window); + + // Initialize wgpu + + #[cfg(target_arch = "wasm32")] + let default_backend = wgpu::Backends::GL; + #[cfg(not(target_arch = "wasm32"))] + let default_backend = wgpu::Backends::PRIMARY; + + let backend = + wgpu::util::backend_bits_from_env().unwrap_or(default_backend); + + let instance = wgpu::Instance::new(backend); + let surface = unsafe { instance.create_surface(&window) }; + + let (format, (device, queue)) = futures::executor::block_on(async { + let adapter = wgpu::util::initialize_adapter_from_env_or_default( + &instance, + backend, + Some(&surface), + ) + .await + .expect("No suitable GPU adapters found on the system!"); + + let adapter_features = adapter.features(); + + #[cfg(target_arch = "wasm32")] + let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()); + + #[cfg(not(target_arch = "wasm32"))] + let needed_limits = wgpu::Limits::default(); + + ( + surface + .get_supported_formats(&adapter) + .first() + .copied() + .expect("Get preferred format"), + adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: adapter_features & wgpu::Features::default(), + limits: needed_limits, + }, + None, + ) + .await + .expect("Request device"), + ) + }); + + surface.configure( + &device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: physical_size.width, + height: physical_size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + }, + ); + + let mut resized = false; + + // Initialize staging belt + let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); + + // Initialize scene and GUI controls + let scene = Scene::new(&device, format); + let controls = Controls::new(); + + // Initialize iced + let mut debug = Debug::new(); + let mut renderer = + Renderer::new(Backend::new(&device, Settings::default(), format)); + + let mut state = program::State::new( + controls, + viewport.logical_size(), + &mut renderer, + &mut debug, + ); + + // Run event loop + event_loop.run(move |event, _, control_flow| { + // You should change this if you want to render continuosly + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CursorMoved { position, .. } => { + cursor_position = position; + } + WindowEvent::ModifiersChanged(new_modifiers) => { + modifiers = new_modifiers; + } + WindowEvent::Resized(_) => { + resized = true; + } + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; + } + _ => {} + } + + // Map window event to iced event + if let Some(event) = iced_winit::conversion::window_event( + &event, + window.scale_factor(), + modifiers, + ) { + state.queue_event(event); + } + } + Event::MainEventsCleared => { + // If there are events pending + if !state.is_queue_empty() { + // We update iced + let _ = state.update( + viewport.logical_size(), + conversion::cursor_position( + cursor_position, + viewport.scale_factor(), + ), + &mut renderer, + &iced_wgpu::Theme::Dark, + &renderer::Style { text_color: Color::WHITE }, + &mut clipboard, + &mut debug, + ); + + // and request a redraw + window.request_redraw(); + } + } + Event::RedrawRequested(_) => { + if resized { + let size = window.inner_size(); + + viewport = Viewport::with_physical_size( + Size::new(size.width, size.height), + window.scale_factor(), + ); + + surface.configure( + &device, + &wgpu::SurfaceConfiguration { + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: wgpu::CompositeAlphaMode::Auto + }, + ); + + resized = false; + } + + match surface.get_current_texture() { + Ok(frame) => { + let mut encoder = device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + ); + + let program = state.program(); + + let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); + + { + // We clear the frame + let mut render_pass = scene.clear( + &view, + &mut encoder, + program.background_color(), + ); + + // Draw the scene + scene.draw(&mut render_pass); + } + + // And then iced on top + renderer.with_primitives(|backend, primitive| { + backend.present( + &device, + &mut staging_belt, + &mut encoder, + &view, + primitive, + &viewport, + &debug.overlay(), + ); + }); + + // Then we submit the work + staging_belt.finish(); + queue.submit(Some(encoder.finish())); + frame.present(); + + // Update the mouse cursor + window.set_cursor_icon( + iced_winit::conversion::mouse_interaction( + state.mouse_interaction(), + ), + ); + + // And recall staging buffers + staging_belt.recall(); + + } + Err(error) => match error { + wgpu::SurfaceError::OutOfMemory => { + panic!("Swapchain error: {error}. Rendering cannot continue.") + } + _ => { + // Try rendering again next frame. + window.request_redraw(); + } + }, + } + } + _ => {} + } + }) +} diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs new file mode 100644 index 00000000..3e41fbda --- /dev/null +++ b/examples/integration/src/scene.rs @@ -0,0 +1,102 @@ +use iced_wgpu::wgpu; +use iced_winit::Color; + +pub struct Scene { + pipeline: wgpu::RenderPipeline, +} + +impl Scene { + pub fn new( + device: &wgpu::Device, + texture_format: wgpu::TextureFormat, + ) -> Scene { + let pipeline = build_pipeline(device, texture_format); + + Scene { pipeline } + } + + pub fn clear<'a>( + &self, + target: &'a wgpu::TextureView, + encoder: &'a mut wgpu::CommandEncoder, + background_color: Color, + ) -> wgpu::RenderPass<'a> { + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear({ + let [r, g, b, a] = background_color.into_linear(); + + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } + }), + store: true, + }, + })], + depth_stencil_attachment: None, + }) + } + + pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_pipeline(&self.pipeline); + render_pass.draw(0..3, 0..1); + } +} + +fn build_pipeline( + device: &wgpu::Device, + texture_format: wgpu::TextureFormat, +) -> wgpu::RenderPipeline { + let (vs_module, fs_module) = ( + device.create_shader_module(wgpu::include_wgsl!("shader/vert.wgsl")), + device.create_shader_module(wgpu::include_wgsl!("shader/frag.wgsl")), + ); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + push_constant_ranges: &[], + bind_group_layouts: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs_module, + entry_point: "main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &fs_module, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: texture_format, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent::REPLACE, + alpha: wgpu::BlendComponent::REPLACE, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + front_face: wgpu::FrontFace::Ccw, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }) +} diff --git a/examples/integration/src/shader/frag.wgsl b/examples/integration/src/shader/frag.wgsl new file mode 100644 index 00000000..cf27bb56 --- /dev/null +++ b/examples/integration/src/shader/frag.wgsl @@ -0,0 +1,4 @@ +@fragment +fn main() -> @location(0) vec4 { + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/examples/integration/src/shader/vert.wgsl b/examples/integration/src/shader/vert.wgsl new file mode 100644 index 00000000..e353e6ba --- /dev/null +++ b/examples/integration/src/shader/vert.wgsl @@ -0,0 +1,6 @@ +@vertex +fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 { + let x = f32(1 - i32(in_vertex_index)) * 0.5; + let y = f32(1 - i32(in_vertex_index & 1u) * 2) * 0.5; + return vec4(x, y, 0.0, 1.0); +} diff --git a/examples/integration_opengl/Cargo.toml b/examples/integration_opengl/Cargo.toml deleted file mode 100644 index 6dac999c..00000000 --- a/examples/integration_opengl/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "integration_opengl" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -publish = false - -[dependencies] -iced_glutin = { path = "../../glutin" } -iced_glow = { path = "../../glow" } -iced_winit = { path = "../../winit" } -env_logger = "0.8" diff --git a/examples/integration_opengl/README.md b/examples/integration_opengl/README.md deleted file mode 100644 index b7c2c074..00000000 --- a/examples/integration_opengl/README.md +++ /dev/null @@ -1,16 +0,0 @@ -## OpenGL integration - -A demonstration of how to integrate Iced in an existing graphical OpenGL application. - -The __[`main`]__ file contains all the code of the example. - -
- image host -
- -You can run it with `cargo run`: -``` -cargo run --package integration_opengl -``` - -[`main`]: src/main.rs diff --git a/examples/integration_opengl/src/controls.rs b/examples/integration_opengl/src/controls.rs deleted file mode 100644 index c3648f44..00000000 --- a/examples/integration_opengl/src/controls.rs +++ /dev/null @@ -1,101 +0,0 @@ -use iced_glow::Renderer; -use iced_glutin::widget::Slider; -use iced_glutin::widget::{Column, Row, Text}; -use iced_glutin::{Alignment, Color, Command, Element, Length, Program}; - -pub struct Controls { - background_color: Color, -} - -#[derive(Debug, Clone)] -pub enum Message { - BackgroundColorChanged(Color), -} - -impl Controls { - pub fn new() -> Controls { - Controls { - background_color: Color::BLACK, - } - } - - pub fn background_color(&self) -> Color { - self.background_color - } -} - -impl Program for Controls { - type Renderer = Renderer; - type Message = Message; - - fn update(&mut self, message: Message) -> Command { - match message { - Message::BackgroundColorChanged(color) => { - self.background_color = color; - } - } - - Command::none() - } - - fn view(&self) -> Element { - let background_color = self.background_color; - - let sliders = Row::new() - .width(500) - .spacing(20) - .push( - Slider::new(0.0..=1.0, background_color.r, move |r| { - Message::BackgroundColorChanged(Color { - r, - ..background_color - }) - }) - .step(0.01), - ) - .push( - Slider::new(0.0..=1.0, background_color.g, move |g| { - Message::BackgroundColorChanged(Color { - g, - ..background_color - }) - }) - .step(0.01), - ) - .push( - Slider::new(0.0..=1.0, background_color.b, move |b| { - Message::BackgroundColorChanged(Color { - b, - ..background_color - }) - }) - .step(0.01), - ); - - Row::new() - .width(Length::Fill) - .height(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new() - .width(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new() - .padding(10) - .spacing(10) - .push( - Text::new("Background color") - .style(Color::WHITE), - ) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ), - ), - ) - .into() - } -} diff --git a/examples/integration_opengl/src/main.rs b/examples/integration_opengl/src/main.rs deleted file mode 100644 index f161c8a0..00000000 --- a/examples/integration_opengl/src/main.rs +++ /dev/null @@ -1,187 +0,0 @@ -mod controls; -mod scene; - -use controls::Controls; -use scene::Scene; - -use glow::*; -use glutin::dpi::PhysicalPosition; -use glutin::event::{Event, ModifiersState, WindowEvent}; -use glutin::event_loop::ControlFlow; -use iced_glow::glow; -use iced_glow::{Backend, Renderer, Settings, Viewport}; -use iced_glutin::conversion; -use iced_glutin::glutin; -use iced_glutin::renderer; -use iced_glutin::{program, Clipboard, Color, Debug, Size}; - -pub fn main() { - env_logger::init(); - let (gl, event_loop, windowed_context, shader_version) = { - let el = glutin::event_loop::EventLoop::new(); - - let wb = glutin::window::WindowBuilder::new() - .with_title("OpenGL integration example") - .with_inner_size(glutin::dpi::LogicalSize::new(1024.0, 768.0)); - - let windowed_context = glutin::ContextBuilder::new() - .with_vsync(true) - .build_windowed(wb, &el) - .unwrap(); - - unsafe { - let windowed_context = windowed_context.make_current().unwrap(); - - let gl = glow::Context::from_loader_function(|s| { - windowed_context.get_proc_address(s) as *const _ - }); - - // Enable auto-conversion from/to sRGB - gl.enable(glow::FRAMEBUFFER_SRGB); - - // Enable alpha blending - gl.enable(glow::BLEND); - gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA); - - // Disable multisampling by default - gl.disable(glow::MULTISAMPLE); - - (gl, el, windowed_context, "#version 410") - } - }; - - let physical_size = windowed_context.window().inner_size(); - let mut viewport = Viewport::with_physical_size( - Size::new(physical_size.width, physical_size.height), - windowed_context.window().scale_factor(), - ); - - let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); - let mut modifiers = ModifiersState::default(); - let mut clipboard = Clipboard::connect(windowed_context.window()); - - let mut renderer = Renderer::new(Backend::new(&gl, Settings::default())); - - let mut debug = Debug::new(); - - let controls = Controls::new(); - let mut state = program::State::new( - controls, - viewport.logical_size(), - &mut renderer, - &mut debug, - ); - let mut resized = false; - - let scene = Scene::new(&gl, shader_version); - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CursorMoved { position, .. } => { - cursor_position = position; - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers; - } - WindowEvent::Resized(physical_size) => { - viewport = Viewport::with_physical_size( - Size::new( - physical_size.width, - physical_size.height, - ), - windowed_context.window().scale_factor(), - ); - - resized = true; - } - WindowEvent::CloseRequested => { - scene.cleanup(&gl); - *control_flow = ControlFlow::Exit - } - _ => (), - } - - // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( - &event, - windowed_context.window().scale_factor(), - modifiers, - ) { - state.queue_event(event); - } - } - Event::MainEventsCleared => { - // If there are events pending - if !state.is_queue_empty() { - // We update iced - let _ = state.update( - viewport.logical_size(), - conversion::cursor_position( - cursor_position, - viewport.scale_factor(), - ), - &mut renderer, - &iced_glow::Theme::Dark, - &renderer::Style { - text_color: Color::WHITE, - }, - &mut clipboard, - &mut debug, - ); - - // and request a redraw - windowed_context.window().request_redraw(); - } - } - Event::RedrawRequested(_) => { - if resized { - let size = windowed_context.window().inner_size(); - - unsafe { - gl.viewport( - 0, - 0, - size.width as i32, - size.height as i32, - ); - } - - resized = false; - } - - let program = state.program(); - { - // We clear the frame - scene.clear(&gl, program.background_color()); - - // Draw the scene - scene.draw(&gl); - } - - // And then iced on top - renderer.with_primitives(|backend, primitive| { - backend.present( - &gl, - primitive, - &viewport, - &debug.overlay(), - ); - }); - - // Update the mouse cursor - windowed_context.window().set_cursor_icon( - iced_winit::conversion::mouse_interaction( - state.mouse_interaction(), - ), - ); - - windowed_context.swap_buffers().unwrap(); - } - _ => (), - } - }); -} diff --git a/examples/integration_opengl/src/scene.rs b/examples/integration_opengl/src/scene.rs deleted file mode 100644 index c1d05b65..00000000 --- a/examples/integration_opengl/src/scene.rs +++ /dev/null @@ -1,102 +0,0 @@ -use glow::*; -use iced_glow::glow; -use iced_glow::Color; - -pub struct Scene { - program: glow::Program, - vertex_array: glow::VertexArray, -} - -impl Scene { - pub fn new(gl: &glow::Context, shader_version: &str) -> Self { - unsafe { - let vertex_array = gl - .create_vertex_array() - .expect("Cannot create vertex array"); - gl.bind_vertex_array(Some(vertex_array)); - - let program = gl.create_program().expect("Cannot create program"); - - let (vertex_shader_source, fragment_shader_source) = ( - r#"const vec2 verts[3] = vec2[3]( - vec2(0.5f, 1.0f), - vec2(0.0f, 0.0f), - vec2(1.0f, 0.0f) - ); - out vec2 vert; - void main() { - vert = verts[gl_VertexID]; - gl_Position = vec4(vert - 0.5, 0.0, 1.0); - }"#, - r#"precision highp float; - in vec2 vert; - out vec4 color; - void main() { - color = vec4(vert, 0.5, 1.0); - }"#, - ); - - let shader_sources = [ - (glow::VERTEX_SHADER, vertex_shader_source), - (glow::FRAGMENT_SHADER, fragment_shader_source), - ]; - - let mut shaders = Vec::with_capacity(shader_sources.len()); - - for (shader_type, shader_source) in shader_sources.iter() { - let shader = gl - .create_shader(*shader_type) - .expect("Cannot create shader"); - gl.shader_source( - shader, - &format!("{shader_version}\n{shader_source}"), - ); - gl.compile_shader(shader); - if !gl.get_shader_compile_status(shader) { - panic!("{}", gl.get_shader_info_log(shader)); - } - gl.attach_shader(program, shader); - shaders.push(shader); - } - - gl.link_program(program); - if !gl.get_program_link_status(program) { - panic!("{}", gl.get_program_info_log(program)); - } - - for shader in shaders { - gl.detach_shader(program, shader); - gl.delete_shader(shader); - } - - gl.use_program(Some(program)); - Self { - program, - vertex_array, - } - } - } - - pub fn clear(&self, gl: &glow::Context, background_color: Color) { - let [r, g, b, a] = background_color.into_linear(); - unsafe { - gl.clear_color(r, g, b, a); - gl.clear(glow::COLOR_BUFFER_BIT); - } - } - - pub fn draw(&self, gl: &glow::Context) { - unsafe { - gl.bind_vertex_array(Some(self.vertex_array)); - gl.use_program(Some(self.program)); - gl.draw_arrays(glow::TRIANGLES, 0, 3); - } - } - - pub fn cleanup(&self, gl: &glow::Context) { - unsafe { - gl.delete_program(self.program); - gl.delete_vertex_array(self.vertex_array); - } - } -} diff --git a/examples/integration_wgpu/.gitignore b/examples/integration_wgpu/.gitignore deleted file mode 100644 index e188dc28..00000000 --- a/examples/integration_wgpu/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.wasm -*.js diff --git a/examples/integration_wgpu/Cargo.toml b/examples/integration_wgpu/Cargo.toml deleted file mode 100644 index eaa1df7e..00000000 --- a/examples/integration_wgpu/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "integration_wgpu" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -publish = false - -[dependencies] -iced_winit = { path = "../../winit" } -iced_wgpu = { path = "../../wgpu", features = ["webgl"] } -env_logger = "0.8" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -console_error_panic_hook = "0.1.7" -console_log = "0.2.0" -log = "0.4" -wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["Element", "HtmlCanvasElement", "Window", "Document"] } -# This dependency a little bit quirky, it is deep in the tree and without `js` feature it -# refuses to work with `wasm32-unknown-unknown target`. Unfortunately, we need this patch -# to make it work -getrandom = { version = "0.2", features = ["js"] } diff --git a/examples/integration_wgpu/README.md b/examples/integration_wgpu/README.md deleted file mode 100644 index ece9ba1e..00000000 --- a/examples/integration_wgpu/README.md +++ /dev/null @@ -1,36 +0,0 @@ -## `wgpu` integration - -A demonstration of how to integrate Iced in an existing [`wgpu`] application. - -The __[`main`]__ file contains all the code of the example. - - - -You can run it with `cargo run`: -``` -cargo run --package integration_wgpu -``` - -### How to run this example with WebGL backend -NOTE: Currently, WebGL backend is is still experimental, so expect bugs. - -```sh -# 0. Install prerequisites -cargo install wasm-bindgen-cli https -# 1. cd to the current folder -# 2. Compile wasm module -cargo build -p integration_wgpu --target wasm32-unknown-unknown -# 3. Invoke wasm-bindgen -wasm-bindgen ../../target/wasm32-unknown-unknown/debug/integration_wgpu.wasm --out-dir . --target web --no-typescript -# 4. run http server -http -# 5. Open 127.0.0.1:8000 in browser -``` - - -[`main`]: src/main.rs -[`wgpu`]: https://github.com/gfx-rs/wgpu diff --git a/examples/integration_wgpu/index.html b/examples/integration_wgpu/index.html deleted file mode 100644 index 461e67a4..00000000 --- a/examples/integration_wgpu/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Iced - wgpu + WebGL integration - - -

integration_wgpu

- - - - - diff --git a/examples/integration_wgpu/src/controls.rs b/examples/integration_wgpu/src/controls.rs deleted file mode 100644 index 533cb6e2..00000000 --- a/examples/integration_wgpu/src/controls.rs +++ /dev/null @@ -1,112 +0,0 @@ -use iced_wgpu::Renderer; -use iced_winit::widget::{slider, text_input, Column, Row, Text}; -use iced_winit::{Alignment, Color, Command, Element, Length, Program}; - -pub struct Controls { - background_color: Color, - text: String, -} - -#[derive(Debug, Clone)] -pub enum Message { - BackgroundColorChanged(Color), - TextChanged(String), -} - -impl Controls { - pub fn new() -> Controls { - Controls { - background_color: Color::BLACK, - text: Default::default(), - } - } - - pub fn background_color(&self) -> Color { - self.background_color - } -} - -impl Program for Controls { - type Renderer = Renderer; - type Message = Message; - - fn update(&mut self, message: Message) -> Command { - match message { - Message::BackgroundColorChanged(color) => { - self.background_color = color; - } - Message::TextChanged(text) => { - self.text = text; - } - } - - Command::none() - } - - fn view(&self) -> Element { - let background_color = self.background_color; - let text = &self.text; - - let sliders = Row::new() - .width(500) - .spacing(20) - .push( - slider(0.0..=1.0, background_color.r, move |r| { - Message::BackgroundColorChanged(Color { - r, - ..background_color - }) - }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.g, move |g| { - Message::BackgroundColorChanged(Color { - g, - ..background_color - }) - }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.b, move |b| { - Message::BackgroundColorChanged(Color { - b, - ..background_color - }) - }) - .step(0.01), - ); - - Row::new() - .width(Length::Fill) - .height(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new() - .width(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new() - .padding(10) - .spacing(10) - .push( - Text::new("Background color") - .style(Color::WHITE), - ) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ) - .push(text_input( - "Placeholder", - text, - Message::TextChanged, - )), - ), - ) - .into() - } -} diff --git a/examples/integration_wgpu/src/main.rs b/examples/integration_wgpu/src/main.rs deleted file mode 100644 index 2a56b6fa..00000000 --- a/examples/integration_wgpu/src/main.rs +++ /dev/null @@ -1,289 +0,0 @@ -mod controls; -mod scene; - -use controls::Controls; -use scene::Scene; - -use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; -use iced_winit::{ - conversion, futures, program, renderer, winit, Clipboard, Color, Debug, - Size, -}; - -use winit::{ - dpi::PhysicalPosition, - event::{Event, ModifiersState, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, -}; - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsCast; -#[cfg(target_arch = "wasm32")] -use web_sys::HtmlCanvasElement; -#[cfg(target_arch = "wasm32")] -use winit::platform::web::WindowBuilderExtWebSys; - -pub fn main() { - #[cfg(target_arch = "wasm32")] - let canvas_element = { - console_log::init_with_level(log::Level::Debug) - .expect("could not initialize logger"); - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - - web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.get_element_by_id("iced_canvas")) - .and_then(|element| element.dyn_into::().ok()) - .expect("Canvas with id `iced_canvas` is missing") - }; - #[cfg(not(target_arch = "wasm32"))] - env_logger::init(); - - // Initialize winit - let event_loop = EventLoop::new(); - - #[cfg(target_arch = "wasm32")] - let window = winit::window::WindowBuilder::new() - .with_canvas(Some(canvas_element)) - .build(&event_loop) - .expect("Failed to build winit window"); - - #[cfg(not(target_arch = "wasm32"))] - let window = winit::window::Window::new(&event_loop).unwrap(); - - let physical_size = window.inner_size(); - let mut viewport = Viewport::with_physical_size( - Size::new(physical_size.width, physical_size.height), - window.scale_factor(), - ); - let mut cursor_position = PhysicalPosition::new(-1.0, -1.0); - let mut modifiers = ModifiersState::default(); - let mut clipboard = Clipboard::connect(&window); - - // Initialize wgpu - - #[cfg(target_arch = "wasm32")] - let default_backend = wgpu::Backends::GL; - #[cfg(not(target_arch = "wasm32"))] - let default_backend = wgpu::Backends::PRIMARY; - - let backend = - wgpu::util::backend_bits_from_env().unwrap_or(default_backend); - - let instance = wgpu::Instance::new(backend); - let surface = unsafe { instance.create_surface(&window) }; - - let (format, (device, queue)) = futures::executor::block_on(async { - let adapter = wgpu::util::initialize_adapter_from_env_or_default( - &instance, - backend, - Some(&surface), - ) - .await - .expect("No suitable GPU adapters found on the system!"); - - let adapter_features = adapter.features(); - - #[cfg(target_arch = "wasm32")] - let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()); - - #[cfg(not(target_arch = "wasm32"))] - let needed_limits = wgpu::Limits::default(); - - ( - surface - .get_supported_formats(&adapter) - .first() - .copied() - .expect("Get preferred format"), - adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - features: adapter_features & wgpu::Features::default(), - limits: needed_limits, - }, - None, - ) - .await - .expect("Request device"), - ) - }); - - surface.configure( - &device, - &wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format, - width: physical_size.width, - height: physical_size.height, - present_mode: wgpu::PresentMode::AutoVsync, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - }, - ); - - let mut resized = false; - - // Initialize staging belt - let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); - - // Initialize scene and GUI controls - let scene = Scene::new(&device, format); - let controls = Controls::new(); - - // Initialize iced - let mut debug = Debug::new(); - let mut renderer = - Renderer::new(Backend::new(&device, Settings::default(), format)); - - let mut state = program::State::new( - controls, - viewport.logical_size(), - &mut renderer, - &mut debug, - ); - - // Run event loop - event_loop.run(move |event, _, control_flow| { - // You should change this if you want to render continuosly - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CursorMoved { position, .. } => { - cursor_position = position; - } - WindowEvent::ModifiersChanged(new_modifiers) => { - modifiers = new_modifiers; - } - WindowEvent::Resized(_) => { - resized = true; - } - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - } - _ => {} - } - - // Map window event to iced event - if let Some(event) = iced_winit::conversion::window_event( - &event, - window.scale_factor(), - modifiers, - ) { - state.queue_event(event); - } - } - Event::MainEventsCleared => { - // If there are events pending - if !state.is_queue_empty() { - // We update iced - let _ = state.update( - viewport.logical_size(), - conversion::cursor_position( - cursor_position, - viewport.scale_factor(), - ), - &mut renderer, - &iced_wgpu::Theme::Dark, - &renderer::Style { text_color: Color::WHITE }, - &mut clipboard, - &mut debug, - ); - - // and request a redraw - window.request_redraw(); - } - } - Event::RedrawRequested(_) => { - if resized { - let size = window.inner_size(); - - viewport = Viewport::with_physical_size( - Size::new(size.width, size.height), - window.scale_factor(), - ); - - surface.configure( - &device, - &wgpu::SurfaceConfiguration { - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::AutoVsync, - alpha_mode: wgpu::CompositeAlphaMode::Auto - }, - ); - - resized = false; - } - - match surface.get_current_texture() { - Ok(frame) => { - let mut encoder = device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { label: None }, - ); - - let program = state.program(); - - let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); - - { - // We clear the frame - let mut render_pass = scene.clear( - &view, - &mut encoder, - program.background_color(), - ); - - // Draw the scene - scene.draw(&mut render_pass); - } - - // And then iced on top - renderer.with_primitives(|backend, primitive| { - backend.present( - &device, - &mut staging_belt, - &mut encoder, - &view, - primitive, - &viewport, - &debug.overlay(), - ); - }); - - // Then we submit the work - staging_belt.finish(); - queue.submit(Some(encoder.finish())); - frame.present(); - - // Update the mouse cursor - window.set_cursor_icon( - iced_winit::conversion::mouse_interaction( - state.mouse_interaction(), - ), - ); - - // And recall staging buffers - staging_belt.recall(); - - } - Err(error) => match error { - wgpu::SurfaceError::OutOfMemory => { - panic!("Swapchain error: {error}. Rendering cannot continue.") - } - _ => { - // Try rendering again next frame. - window.request_redraw(); - } - }, - } - } - _ => {} - } - }) -} diff --git a/examples/integration_wgpu/src/scene.rs b/examples/integration_wgpu/src/scene.rs deleted file mode 100644 index 3e41fbda..00000000 --- a/examples/integration_wgpu/src/scene.rs +++ /dev/null @@ -1,102 +0,0 @@ -use iced_wgpu::wgpu; -use iced_winit::Color; - -pub struct Scene { - pipeline: wgpu::RenderPipeline, -} - -impl Scene { - pub fn new( - device: &wgpu::Device, - texture_format: wgpu::TextureFormat, - ) -> Scene { - let pipeline = build_pipeline(device, texture_format); - - Scene { pipeline } - } - - pub fn clear<'a>( - &self, - target: &'a wgpu::TextureView, - encoder: &'a mut wgpu::CommandEncoder, - background_color: Color, - ) -> wgpu::RenderPass<'a> { - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear({ - let [r, g, b, a] = background_color.into_linear(); - - wgpu::Color { - r: r as f64, - g: g as f64, - b: b as f64, - a: a as f64, - } - }), - store: true, - }, - })], - depth_stencil_attachment: None, - }) - } - - pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - render_pass.set_pipeline(&self.pipeline); - render_pass.draw(0..3, 0..1); - } -} - -fn build_pipeline( - device: &wgpu::Device, - texture_format: wgpu::TextureFormat, -) -> wgpu::RenderPipeline { - let (vs_module, fs_module) = ( - device.create_shader_module(wgpu::include_wgsl!("shader/vert.wgsl")), - device.create_shader_module(wgpu::include_wgsl!("shader/frag.wgsl")), - ); - - let pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - push_constant_ranges: &[], - bind_group_layouts: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &vs_module, - entry_point: "main", - buffers: &[], - }, - fragment: Some(wgpu::FragmentState { - module: &fs_module, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: texture_format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent::REPLACE, - alpha: wgpu::BlendComponent::REPLACE, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - front_face: wgpu::FrontFace::Ccw, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: 1, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - }) -} diff --git a/examples/integration_wgpu/src/shader/frag.wgsl b/examples/integration_wgpu/src/shader/frag.wgsl deleted file mode 100644 index cf27bb56..00000000 --- a/examples/integration_wgpu/src/shader/frag.wgsl +++ /dev/null @@ -1,4 +0,0 @@ -@fragment -fn main() -> @location(0) vec4 { - return vec4(1.0, 0.0, 0.0, 1.0); -} diff --git a/examples/integration_wgpu/src/shader/vert.wgsl b/examples/integration_wgpu/src/shader/vert.wgsl deleted file mode 100644 index e353e6ba..00000000 --- a/examples/integration_wgpu/src/shader/vert.wgsl +++ /dev/null @@ -1,6 +0,0 @@ -@vertex -fn main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 { - let x = f32(1 - i32(in_vertex_index)) * 0.5; - let y = f32(1 - i32(in_vertex_index & 1u) * 2) * 0.5; - return vec4(x, y, 0.0, 1.0); -} diff --git a/glow/Cargo.toml b/glow/Cargo.toml deleted file mode 100644 index 1a848ab7..00000000 --- a/glow/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "iced_glow" -version = "0.7.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -description = "A glow renderer for iced" -license = "MIT AND OFL-1.1" -repository = "https://github.com/iced-rs/iced" - -[features] -svg = ["iced_graphics/svg"] -image = ["iced_graphics/image"] -png = ["iced_graphics/png"] -jpeg = ["iced_graphics/jpeg"] -jpeg_rayon = ["iced_graphics/jpeg_rayon"] -gif = ["iced_graphics/gif"] -webp = ["iced_graphics/webp"] -pnm = ["iced_graphics/pnm"] -ico = ["iced_graphics/ico"] -bmp = ["iced_graphics/bmp"] -hdr = ["iced_graphics/hdr"] -dds = ["iced_graphics/dds"] -farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas"] -qr_code = ["iced_graphics/qr_code"] -default_system_font = ["iced_graphics/font-source"] - -[dependencies] -glow = "0.11.1" -glow_glyph = "0.5.0" -glyph_brush = "0.7" -euclid = "0.22" -bytemuck = "1.4" -log = "0.4" - -[dependencies.iced_native] -version = "0.9" -path = "../native" - -[dependencies.iced_graphics] -version = "0.7" -path = "../graphics" -features = ["font-fallback", "font-icons", "opengl"] - -[dependencies.tracing] -version = "0.1.6" -optional = true - -[package.metadata.docs.rs] -rustdoc-args = ["--cfg", "docsrs"] -all-features = true diff --git a/glow/README.md b/glow/README.md deleted file mode 100644 index 95c9d62a..00000000 --- a/glow/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# `iced_glow` -[![Documentation](https://docs.rs/iced_glow/badge.svg)][documentation] -[![Crates.io](https://img.shields.io/crates/v/iced_glow.svg)](https://crates.io/crates/iced_glow) -[![License](https://img.shields.io/crates/l/iced_glow.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) -[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) - -`iced_glow` is a [`glow`] renderer for [`iced_native`]. This renderer supports OpenGL 3.0+ and OpenGL ES 2.0. - -This renderer is mostly used as a fallback for hardware that doesn't support [`wgpu`] (Vulkan, Metal or DX12). - -Currently, `iced_glow` supports the following primitives: -- Text, which is rendered using [`glow_glyph`]. No shaping at all. -- Quads or rectangles, with rounded borders and a solid background color. -- Clip areas, useful to implement scrollables or hide overflowing content. -- Meshes of triangles, useful to draw geometry freely. - -

- The native target -

- -[documentation]: https://docs.rs/iced_glow -[`iced_native`]: ../native -[`glow`]: https://github.com/grovesNL/glow -[`wgpu`]: https://github.com/gfx-rs/wgpu -[`glow_glyph`]: https://github.com/hecrj/glow_glyph - -## Installation -Add `iced_glow` as a dependency in your `Cargo.toml`: - -```toml -iced_glow = "0.7" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases - -## Current limitations - -The current implementation is quite naive, it uses: - -- A different pipeline/shader for each primitive -- A very simplistic layer model: every `Clip` primitive will generate new layers -- _Many_ render passes instead of preparing everything upfront -- A glyph cache that is trimmed incorrectly when there are multiple layers (a [`glyph_brush`] limitation) - -Some of these issues are already being worked on! If you want to help, [get in touch!] - -[get in touch!]: ../CONTRIBUTING.md -[`glyph_brush`]: https://github.com/alexheretic/glyph-brush diff --git a/glow/src/backend.rs b/glow/src/backend.rs deleted file mode 100644 index 36a34eda..00000000 --- a/glow/src/backend.rs +++ /dev/null @@ -1,280 +0,0 @@ -#[cfg(any(feature = "image", feature = "svg"))] -use crate::image; -use crate::quad; -use crate::text; -use crate::{program, triangle}; -use crate::{Settings, Transformation, Viewport}; - -use iced_graphics::backend; -use iced_graphics::font; -use iced_graphics::{Layer, Primitive}; -use iced_native::alignment; -use iced_native::{Font, Size}; - -/// A [`glow`] graphics backend for [`iced`]. -/// -/// [`glow`]: https://github.com/grovesNL/glow -/// [`iced`]: https://github.com/iced-rs/iced -#[derive(Debug)] -pub struct Backend { - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline: image::Pipeline, - quad_pipeline: quad::Pipeline, - text_pipeline: text::Pipeline, - triangle_pipeline: triangle::Pipeline, - default_text_size: f32, -} - -impl Backend { - /// Creates a new [`Backend`]. - pub fn new(gl: &glow::Context, settings: Settings) -> Self { - let text_pipeline = text::Pipeline::new( - gl, - settings.default_font, - settings.text_multithreading, - ); - - let shader_version = program::Version::new(gl); - - #[cfg(any(feature = "image", feature = "svg"))] - let image_pipeline = image::Pipeline::new(gl, &shader_version); - let quad_pipeline = quad::Pipeline::new(gl, &shader_version); - let triangle_pipeline = triangle::Pipeline::new(gl, &shader_version); - - Self { - #[cfg(any(feature = "image", feature = "svg"))] - image_pipeline, - quad_pipeline, - text_pipeline, - triangle_pipeline, - default_text_size: settings.default_text_size, - } - } - - /// Draws the provided primitives in the default framebuffer. - /// - /// The text provided as overlay will be rendered on top of the primitives. - /// This is useful for rendering debug information. - pub fn present>( - &mut self, - gl: &glow::Context, - primitives: &[Primitive], - viewport: &Viewport, - overlay_text: &[T], - ) { - let viewport_size = viewport.physical_size(); - let scale_factor = viewport.scale_factor() as f32; - let projection = viewport.projection(); - - let mut layers = Layer::generate(primitives, viewport); - layers.push(Layer::overlay(overlay_text, viewport)); - - for layer in layers { - self.flush( - gl, - scale_factor, - projection, - &layer, - viewport_size.height, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.trim_cache(gl); - } - - fn flush( - &mut self, - gl: &glow::Context, - scale_factor: f32, - transformation: Transformation, - layer: &Layer<'_>, - target_height: u32, - ) { - let mut bounds = (layer.bounds * scale_factor).snap(); - - if bounds.width < 1 || bounds.height < 1 { - return; - } - - bounds.height = bounds.height.min(target_height); - - if !layer.quads.is_empty() { - self.quad_pipeline.draw( - gl, - target_height, - &layer.quads, - transformation, - scale_factor, - bounds, - ); - } - - if !layer.meshes.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.triangle_pipeline.draw( - &layer.meshes, - gl, - target_height, - scaled, - scale_factor, - ); - } - - #[cfg(any(feature = "image", feature = "svg"))] - if !layer.images.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.image_pipeline.draw( - gl, - target_height, - scaled, - scale_factor, - &layer.images, - bounds, - ); - } - - if !layer.text.is_empty() { - for text in layer.text.iter() { - // Target physical coordinates directly to avoid blurry text - let text = glow_glyph::Section { - // TODO: We `round` here to avoid rerasterizing text when - // its position changes slightly. This can make text feel a - // bit "jumpy". We may be able to do better once we improve - // our text rendering/caching pipeline. - screen_position: ( - (text.bounds.x * scale_factor).round(), - (text.bounds.y * scale_factor).round(), - ), - // TODO: Fix precision issues with some scale factors. - // - // The `ceil` here can cause some words to render on the - // same line when they should not. - // - // Ideally, `wgpu_glyph` should be able to compute layout - // using logical positions, and then apply the proper - // scaling when rendering. This would ensure that both - // measuring and rendering follow the same layout rules. - bounds: ( - (text.bounds.width * scale_factor).ceil(), - (text.bounds.height * scale_factor).ceil(), - ), - text: vec![glow_glyph::Text { - text: text.content, - scale: glow_glyph::ab_glyph::PxScale { - x: text.size * scale_factor, - y: text.size * scale_factor, - }, - font_id: self.text_pipeline.find_font(text.font), - extra: glow_glyph::Extra { - color: text.color, - z: 0.0, - }, - }], - layout: glow_glyph::Layout::default() - .h_align(match text.horizontal_alignment { - alignment::Horizontal::Left => { - glow_glyph::HorizontalAlign::Left - } - alignment::Horizontal::Center => { - glow_glyph::HorizontalAlign::Center - } - alignment::Horizontal::Right => { - glow_glyph::HorizontalAlign::Right - } - }) - .v_align(match text.vertical_alignment { - alignment::Vertical::Top => { - glow_glyph::VerticalAlign::Top - } - alignment::Vertical::Center => { - glow_glyph::VerticalAlign::Center - } - alignment::Vertical::Bottom => { - glow_glyph::VerticalAlign::Bottom - } - }), - }; - - self.text_pipeline.queue(text); - } - - self.text_pipeline.draw_queued( - gl, - transformation, - glow_glyph::Region { - x: bounds.x, - y: target_height - (bounds.y + bounds.height), - width: bounds.width, - height: bounds.height, - }, - ); - } - } -} - -impl iced_graphics::Backend for Backend { - fn trim_measurements(&mut self) { - self.text_pipeline.trim_measurement_cache() - } -} - -impl backend::Text for Backend { - const ICON_FONT: Font = font::ICONS; - const CHECKMARK_ICON: char = font::CHECKMARK_ICON; - const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON; - - fn default_size(&self) -> f32 { - self.default_text_size - } - - fn measure( - &self, - contents: &str, - size: f32, - font: Font, - bounds: Size, - ) -> (f32, f32) { - self.text_pipeline.measure(contents, size, font, bounds) - } - - fn hit_test( - &self, - contents: &str, - size: f32, - font: Font, - bounds: Size, - point: iced_native::Point, - nearest_only: bool, - ) -> Option { - self.text_pipeline.hit_test( - contents, - size, - font, - bounds, - point, - nearest_only, - ) - } -} - -#[cfg(feature = "image")] -impl backend::Image for Backend { - fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { - self.image_pipeline.dimensions(handle) - } -} - -#[cfg(feature = "svg")] -impl backend::Svg for Backend { - fn viewport_dimensions( - &self, - handle: &iced_native::svg::Handle, - ) -> Size { - self.image_pipeline.viewport_dimensions(handle) - } -} diff --git a/glow/src/image.rs b/glow/src/image.rs deleted file mode 100644 index d3a25b5b..00000000 --- a/glow/src/image.rs +++ /dev/null @@ -1,254 +0,0 @@ -mod storage; - -use storage::Storage; - -pub use iced_graphics::triangle::{Mesh2D, Vertex2D}; - -use crate::program::{self, Shader}; -use crate::Transformation; - -#[cfg(feature = "image")] -use iced_graphics::image::raster; - -#[cfg(feature = "svg")] -use iced_graphics::image::vector; - -use iced_graphics::layer; -use iced_graphics::Rectangle; -use iced_graphics::Size; - -use glow::HasContext; - -use std::cell::RefCell; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[derive(Debug)] -pub(crate) struct Pipeline { - program: ::Program, - vertex_array: ::VertexArray, - vertex_buffer: ::Buffer, - transform_location: ::UniformLocation, - storage: Storage, - #[cfg(feature = "image")] - raster_cache: RefCell>, - #[cfg(feature = "svg")] - vector_cache: RefCell>, -} - -impl Pipeline { - pub fn new( - gl: &glow::Context, - shader_version: &program::Version, - ) -> Pipeline { - let program = unsafe { - let vertex_shader = Shader::vertex( - gl, - shader_version, - include_str!("shader/common/image.vert"), - ); - let fragment_shader = Shader::fragment( - gl, - shader_version, - include_str!("shader/common/image.frag"), - ); - - program::create( - gl, - &[vertex_shader, fragment_shader], - &[(0, "i_Position")], - ) - }; - - let transform_location = - unsafe { gl.get_uniform_location(program, "u_Transform") } - .expect("Get transform location"); - - unsafe { - gl.use_program(Some(program)); - - let transform: [f32; 16] = Transformation::identity().into(); - gl.uniform_matrix_4_f32_slice( - Some(&transform_location), - false, - &transform, - ); - - gl.use_program(None); - } - - let vertex_buffer = - unsafe { gl.create_buffer().expect("Create vertex buffer") }; - let vertex_array = - unsafe { gl.create_vertex_array().expect("Create vertex array") }; - - unsafe { - gl.bind_vertex_array(Some(vertex_array)); - gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer)); - - let vertices = &[0u8, 0, 1, 0, 0, 1, 1, 1]; - gl.buffer_data_size( - glow::ARRAY_BUFFER, - vertices.len() as i32, - glow::STATIC_DRAW, - ); - gl.buffer_sub_data_u8_slice( - glow::ARRAY_BUFFER, - 0, - bytemuck::cast_slice(vertices), - ); - - gl.enable_vertex_attrib_array(0); - gl.vertex_attrib_pointer_f32( - 0, - 2, - glow::UNSIGNED_BYTE, - false, - 0, - 0, - ); - - gl.bind_buffer(glow::ARRAY_BUFFER, None); - gl.bind_vertex_array(None); - } - - Pipeline { - program, - vertex_array, - vertex_buffer, - transform_location, - storage: Storage::default(), - #[cfg(feature = "image")] - raster_cache: RefCell::new(raster::Cache::default()), - #[cfg(feature = "svg")] - vector_cache: RefCell::new(vector::Cache::default()), - } - } - - #[cfg(feature = "image")] - pub fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { - self.raster_cache.borrow_mut().load(handle).dimensions() - } - - #[cfg(feature = "svg")] - pub fn viewport_dimensions( - &self, - handle: &iced_native::svg::Handle, - ) -> Size { - let mut cache = self.vector_cache.borrow_mut(); - let svg = cache.load(handle); - - svg.viewport_dimensions() - } - - pub fn draw( - &mut self, - mut gl: &glow::Context, - target_height: u32, - transformation: Transformation, - _scale_factor: f32, - images: &[layer::Image], - layer_bounds: Rectangle, - ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Glow::Image", "DRAW").entered(); - - unsafe { - gl.use_program(Some(self.program)); - gl.bind_vertex_array(Some(self.vertex_array)); - gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer)); - gl.enable(glow::SCISSOR_TEST); - } - - #[cfg(feature = "image")] - let mut raster_cache = self.raster_cache.borrow_mut(); - - #[cfg(feature = "svg")] - let mut vector_cache = self.vector_cache.borrow_mut(); - - for image in images { - let (entry, bounds) = match &image { - #[cfg(feature = "image")] - layer::Image::Raster { handle, bounds } => ( - raster_cache.upload(handle, &mut gl, &mut self.storage), - bounds, - ), - #[cfg(not(feature = "image"))] - layer::Image::Raster { handle: _, bounds } => (None, bounds), - - #[cfg(feature = "svg")] - layer::Image::Vector { - handle, - color, - bounds, - } => { - let size = [bounds.width, bounds.height]; - ( - vector_cache.upload( - handle, - *color, - size, - _scale_factor, - &mut gl, - &mut self.storage, - ), - bounds, - ) - } - - #[cfg(not(feature = "svg"))] - layer::Image::Vector { bounds, .. } => (None, bounds), - }; - - unsafe { - gl.scissor( - layer_bounds.x as i32, - (target_height - (layer_bounds.y + layer_bounds.height)) - as i32, - layer_bounds.width as i32, - layer_bounds.height as i32, - ); - - if let Some(storage::Entry { texture, .. }) = entry { - gl.bind_texture(glow::TEXTURE_2D, Some(*texture)) - } else { - continue; - } - - let translate = Transformation::translate(bounds.x, bounds.y); - let scale = Transformation::scale(bounds.width, bounds.height); - let transformation = transformation * translate * scale; - let matrix: [f32; 16] = transformation.into(); - gl.uniform_matrix_4_f32_slice( - Some(&self.transform_location), - false, - &matrix, - ); - - gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4); - - gl.bind_texture(glow::TEXTURE_2D, None); - } - } - - unsafe { - gl.bind_buffer(glow::ARRAY_BUFFER, None); - gl.bind_vertex_array(None); - gl.use_program(None); - gl.disable(glow::SCISSOR_TEST); - } - } - - pub fn trim_cache(&mut self, mut gl: &glow::Context) { - #[cfg(feature = "image")] - self.raster_cache - .borrow_mut() - .trim(&mut self.storage, &mut gl); - - #[cfg(feature = "svg")] - self.vector_cache - .borrow_mut() - .trim(&mut self.storage, &mut gl); - } -} diff --git a/glow/src/image/storage.rs b/glow/src/image/storage.rs deleted file mode 100644 index 9bc20641..00000000 --- a/glow/src/image/storage.rs +++ /dev/null @@ -1,78 +0,0 @@ -use iced_graphics::image; -use iced_graphics::Size; - -use glow::HasContext; - -#[derive(Debug, Default)] -pub struct Storage; - -impl image::Storage for Storage { - type Entry = Entry; - type State<'a> = &'a glow::Context; - - fn upload( - &mut self, - width: u32, - height: u32, - data: &[u8], - gl: &mut &glow::Context, - ) -> Option { - unsafe { - let texture = gl.create_texture().expect("create texture"); - gl.bind_texture(glow::TEXTURE_2D, Some(texture)); - gl.tex_image_2d( - glow::TEXTURE_2D, - 0, - glow::SRGB8_ALPHA8 as i32, - width as i32, - height as i32, - 0, - glow::RGBA, - glow::UNSIGNED_BYTE, - Some(data), - ); - gl.tex_parameter_i32( - glow::TEXTURE_2D, - glow::TEXTURE_WRAP_S, - glow::CLAMP_TO_EDGE as _, - ); - gl.tex_parameter_i32( - glow::TEXTURE_2D, - glow::TEXTURE_WRAP_T, - glow::CLAMP_TO_EDGE as _, - ); - gl.tex_parameter_i32( - glow::TEXTURE_2D, - glow::TEXTURE_MIN_FILTER, - glow::LINEAR as _, - ); - gl.tex_parameter_i32( - glow::TEXTURE_2D, - glow::TEXTURE_MAG_FILTER, - glow::LINEAR as _, - ); - gl.bind_texture(glow::TEXTURE_2D, None); - - Some(Entry { - size: Size::new(width, height), - texture, - }) - } - } - - fn remove(&mut self, entry: &Entry, gl: &mut &glow::Context) { - unsafe { gl.delete_texture(entry.texture) } - } -} - -#[derive(Debug)] -pub struct Entry { - size: Size, - pub(super) texture: glow::NativeTexture, -} - -impl image::storage::Entry for Entry { - fn size(&self) -> Size { - self.size - } -} diff --git a/glow/src/lib.rs b/glow/src/lib.rs deleted file mode 100644 index 9e7de0d9..00000000 --- a/glow/src/lib.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! A [`glow`] renderer for [`iced_native`]. -//! -//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) -//! -//! [`glow`]: https://github.com/grovesNL/glow -//! [`iced_native`]: https://github.com/iced-rs/iced/tree/0.8/native -#![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" -)] -#![deny( - missing_debug_implementations, - missing_docs, - unused_results, - clippy::extra_unused_lifetimes, - clippy::from_over_into, - clippy::needless_borrow, - clippy::new_without_default, - clippy::useless_conversion -)] -#![forbid(rust_2018_idioms)] -#![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] - -pub use glow; - -mod backend; -#[cfg(any(feature = "image", feature = "svg"))] -mod image; -mod program; -mod quad; -mod text; -mod triangle; - -pub mod settings; -pub mod window; - -pub use backend::Backend; -pub use settings::Settings; - -pub(crate) use iced_graphics::Transformation; - -pub use iced_graphics::{Error, Viewport}; -pub use iced_native::Theme; - -pub use iced_native::alignment; -pub use iced_native::{Alignment, Background, Color, Command, Length, Vector}; - -/// A [`glow`] graphics renderer for [`iced`]. -/// -/// [`glow`]: https://github.com/grovesNL/glow -/// [`iced`]: https://github.com/iced-rs/iced -pub type Renderer = - iced_graphics::Renderer; diff --git a/glow/src/program.rs b/glow/src/program.rs deleted file mode 100644 index 95437fcd..00000000 --- a/glow/src/program.rs +++ /dev/null @@ -1,133 +0,0 @@ -use glow::HasContext; - -/// The [`Version`] of a `Program`. -pub struct Version { - vertex: String, - fragment: String, -} - -impl Version { - pub fn new(gl: &glow::Context) -> Version { - let version = gl.version(); - - let (vertex, fragment) = match ( - version.major, - version.minor, - version.is_embedded, - ) { - // OpenGL 3.0+ - (3, 0 | 1 | 2, false) => ( - format!("#version 1{}0\n#extension GL_ARB_explicit_attrib_location : enable", version.minor + 3), - format!( - "#version 1{}0\n#extension GL_ARB_explicit_attrib_location : enable\n#define HIGHER_THAN_300 1", - version.minor + 3 - ), - ), - // OpenGL 3.3+ - (3 | 4, _, false) => ( - format!("#version {}{}0\n#extension GL_ARB_explicit_attrib_location : enable", version.major, version.minor), - format!( - "#version {}{}0\n#extension GL_ARB_explicit_attrib_location : enable\n#define HIGHER_THAN_300 1", - version.major, version.minor - ), - ), - // OpenGL ES 3.0+ - (3, _, true) => ( - format!("#version 3{}0 es", version.minor), - format!( - "#version 3{}0 es\n#define HIGHER_THAN_300 1", - version.minor - ), - ), - // OpenGL ES 2.0+ - (2, _, true) => ( - String::from( - "#version 100\n#define in attribute\n#define out varying", - ), - String::from("#version 100\n#define in varying"), - ), - // OpenGL 2.1 - (2, _, false) => ( - String::from( - "#version 120\n#define in attribute\n#define out varying", - ), - String::from("#version 120\n#define in varying"), - ), - // OpenGL 1.1+ - _ => panic!("Incompatible context version: {version:?}"), - }; - - log::info!("Shader directive: {}", vertex.lines().next().unwrap()); - - Version { vertex, fragment } - } -} - -pub struct Shader(::Shader); - -impl Shader { - fn compile(gl: &glow::Context, stage: u32, content: &str) -> Shader { - unsafe { - let shader = gl.create_shader(stage).expect("Cannot create shader"); - - gl.shader_source(shader, content); - gl.compile_shader(shader); - - if !gl.get_shader_compile_status(shader) { - panic!("{}", gl.get_shader_info_log(shader)); - } - - Shader(shader) - } - } - - /// Creates a vertex [`Shader`]. - pub fn vertex( - gl: &glow::Context, - version: &Version, - content: &'static str, - ) -> Self { - let content = format!("{}\n{}", version.vertex, content); - - Shader::compile(gl, glow::VERTEX_SHADER, &content) - } - - /// Creates a fragment [`Shader`]. - pub fn fragment( - gl: &glow::Context, - version: &Version, - content: &'static str, - ) -> Self { - let content = format!("{}\n{}", version.fragment, content); - - Shader::compile(gl, glow::FRAGMENT_SHADER, &content) - } -} - -pub unsafe fn create( - gl: &glow::Context, - shaders: &[Shader], - attributes: &[(u32, &str)], -) -> ::Program { - let program = gl.create_program().expect("Cannot create program"); - - for shader in shaders { - gl.attach_shader(program, shader.0); - } - - for (i, name) in attributes { - gl.bind_attrib_location(program, *i, name); - } - - gl.link_program(program); - if !gl.get_program_link_status(program) { - panic!("{}", gl.get_program_info_log(program)); - } - - for shader in shaders { - gl.detach_shader(program, shader.0); - gl.delete_shader(shader.0); - } - - program -} diff --git a/glow/src/quad.rs b/glow/src/quad.rs deleted file mode 100644 index 67d9a098..00000000 --- a/glow/src/quad.rs +++ /dev/null @@ -1,74 +0,0 @@ -mod compatibility; -mod core; - -use crate::program; -use crate::Transformation; -use glow::HasContext; -use iced_graphics::layer; -use iced_native::Rectangle; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -#[derive(Debug)] -pub enum Pipeline { - Core(core::Pipeline), - Compatibility(compatibility::Pipeline), -} - -impl Pipeline { - pub fn new( - gl: &glow::Context, - shader_version: &program::Version, - ) -> Pipeline { - let gl_version = gl.version(); - - // OpenGL 3.0+ and OpenGL ES 3.0+ have instancing (which is what separates `core` from `compatibility`) - if gl_version.major >= 3 { - log::info!("Mode: core"); - Pipeline::Core(core::Pipeline::new(gl, shader_version)) - } else { - log::info!("Mode: compatibility"); - Pipeline::Compatibility(compatibility::Pipeline::new( - gl, - shader_version, - )) - } - } - - pub fn draw( - &mut self, - gl: &glow::Context, - target_height: u32, - instances: &[layer::Quad], - transformation: Transformation, - scale: f32, - bounds: Rectangle, - ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Glow::Quad", "DRAW").enter(); - - match self { - Pipeline::Core(pipeline) => { - pipeline.draw( - gl, - target_height, - instances, - transformation, - scale, - bounds, - ); - } - Pipeline::Compatibility(pipeline) => { - pipeline.draw( - gl, - target_height, - instances, - transformation, - scale, - bounds, - ); - } - } - } -} diff --git a/glow/src/quad/compatibility.rs b/glow/src/quad/compatibility.rs deleted file mode 100644 index e909162c..00000000 --- a/glow/src/quad/compatibility.rs +++ /dev/null @@ -1,349 +0,0 @@ -use crate::program::{self, Shader}; -use crate::Transformation; -use glow::HasContext; -use iced_graphics::layer; -use iced_native::Rectangle; - -// Only change `MAX_QUADS`, otherwise you could cause problems -// by splitting a triangle into different render passes. -const MAX_QUADS: usize = 100_000; -const MAX_VERTICES: usize = MAX_QUADS * 4; -const MAX_INDICES: usize = MAX_QUADS * 6; - -#[derive(Debug)] -pub struct Pipeline { - program: ::Program, - vertex_array: ::VertexArray, - vertex_buffer: ::Buffer, - index_buffer: ::Buffer, - transform_location: ::UniformLocation, - scale_location: ::UniformLocation, - screen_height_location: ::UniformLocation, - current_transform: Transformation, - current_scale: f32, - current_target_height: u32, -} - -impl Pipeline { - pub fn new( - gl: &glow::Context, - shader_version: &program::Version, - ) -> Pipeline { - let program = unsafe { - let vertex_shader = Shader::vertex( - gl, - shader_version, - include_str!("../shader/compatibility/quad.vert"), - ); - let fragment_shader = Shader::fragment( - gl, - shader_version, - include_str!("../shader/compatibility/quad.frag"), - ); - - program::create( - gl, - &[vertex_shader, fragment_shader], - &[ - (0, "i_Pos"), - (1, "i_Scale"), - (2, "i_Color"), - (3, "i_BorderColor"), - (4, "i_BorderRadius"), - (5, "i_BorderWidth"), - ], - ) - }; - - let transform_location = - unsafe { gl.get_uniform_location(program, "u_Transform") } - .expect("Get transform location"); - - let scale_location = - unsafe { gl.get_uniform_location(program, "u_Scale") } - .expect("Get scale location"); - - let screen_height_location = - unsafe { gl.get_uniform_location(program, "u_ScreenHeight") } - .expect("Get target height location"); - - unsafe { - gl.use_program(Some(program)); - - gl.uniform_matrix_4_f32_slice( - Some(&transform_location), - false, - Transformation::identity().as_ref(), - ); - - gl.uniform_1_f32(Some(&scale_location), 1.0); - gl.uniform_1_f32(Some(&screen_height_location), 0.0); - - gl.use_program(None); - } - - let (vertex_array, vertex_buffer, index_buffer) = - unsafe { create_buffers(gl, MAX_VERTICES) }; - - Pipeline { - program, - vertex_array, - vertex_buffer, - index_buffer, - transform_location, - scale_location, - screen_height_location, - current_transform: Transformation::identity(), - current_scale: 1.0, - current_target_height: 0, - } - } - - pub fn draw( - &mut self, - gl: &glow::Context, - target_height: u32, - instances: &[layer::Quad], - transformation: Transformation, - scale: f32, - bounds: Rectangle, - ) { - // TODO: Remove this allocation (probably by changing the shader and removing the need of two `position`) - let vertices: Vec = - instances.iter().flat_map(Vertex::from_quad).collect(); - - // TODO: Remove this allocation (or allocate only when needed) - let indices: Vec = (0..instances.len().min(MAX_QUADS) as i32) - .flat_map(|i| { - [i * 4, 1 + i * 4, 2 + i * 4, 2 + i * 4, 1 + i * 4, 3 + i * 4] - }) - .cycle() - .take(instances.len() * 6) - .collect(); - - unsafe { - gl.enable(glow::SCISSOR_TEST); - gl.scissor( - bounds.x as i32, - (target_height - (bounds.y + bounds.height)) as i32, - bounds.width as i32, - bounds.height as i32, - ); - - gl.use_program(Some(self.program)); - gl.bind_vertex_array(Some(self.vertex_array)); - gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer)); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.index_buffer)); - } - - if transformation != self.current_transform { - unsafe { - gl.uniform_matrix_4_f32_slice( - Some(&self.transform_location), - false, - transformation.as_ref(), - ); - - self.current_transform = transformation; - } - } - - if scale != self.current_scale { - unsafe { - gl.uniform_1_f32(Some(&self.scale_location), scale); - } - - self.current_scale = scale; - } - - if target_height != self.current_target_height { - unsafe { - gl.uniform_1_f32( - Some(&self.screen_height_location), - target_height as f32, - ); - } - - self.current_target_height = target_height; - } - - let passes = vertices - .chunks(MAX_VERTICES) - .zip(indices.chunks(MAX_INDICES)); - - for (vertices, indices) in passes { - unsafe { - gl.buffer_sub_data_u8_slice( - glow::ARRAY_BUFFER, - 0, - bytemuck::cast_slice(vertices), - ); - - gl.buffer_sub_data_u8_slice( - glow::ELEMENT_ARRAY_BUFFER, - 0, - bytemuck::cast_slice(indices), - ); - - gl.draw_elements( - glow::TRIANGLES, - indices.len() as i32, - glow::UNSIGNED_INT, - 0, - ); - } - } - - unsafe { - gl.bind_vertex_array(None); - gl.use_program(None); - gl.disable(glow::SCISSOR_TEST); - } - } -} - -unsafe fn create_buffers( - gl: &glow::Context, - size: usize, -) -> ( - ::VertexArray, - ::Buffer, - ::Buffer, -) { - let vertex_array = gl.create_vertex_array().expect("Create vertex array"); - let vertex_buffer = gl.create_buffer().expect("Create vertex buffer"); - let index_buffer = gl.create_buffer().expect("Create index buffer"); - - gl.bind_vertex_array(Some(vertex_array)); - - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(index_buffer)); - gl.buffer_data_size( - glow::ELEMENT_ARRAY_BUFFER, - 12 * size as i32, - glow::DYNAMIC_DRAW, - ); - - gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer)); - gl.buffer_data_size( - glow::ARRAY_BUFFER, - (size * Vertex::SIZE) as i32, - glow::DYNAMIC_DRAW, - ); - - let stride = Vertex::SIZE as i32; - - gl.enable_vertex_attrib_array(0); - gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); - - gl.enable_vertex_attrib_array(1); - gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, stride, 4 * 2); - - gl.enable_vertex_attrib_array(2); - gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2)); - - gl.enable_vertex_attrib_array(3); - gl.vertex_attrib_pointer_f32( - 3, - 4, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4), - ); - - gl.enable_vertex_attrib_array(4); - gl.vertex_attrib_pointer_f32( - 4, - 4, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4 + 4), - ); - - gl.enable_vertex_attrib_array(5); - gl.vertex_attrib_pointer_f32( - 5, - 1, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4 + 4 + 4), - ); - - gl.enable_vertex_attrib_array(6); - gl.vertex_attrib_pointer_f32( - 6, - 2, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4 + 4 + 4 + 1), - ); - - gl.bind_vertex_array(None); - gl.bind_buffer(glow::ARRAY_BUFFER, None); - gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None); - - (vertex_array, vertex_buffer, index_buffer) -} - -/// The vertex of a colored rectangle with a border. -/// -/// This type can be directly uploaded to GPU memory. -#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -pub struct Vertex { - /// The position of the [`Vertex`]. - pub position: [f32; 2], - - /// The size of the [`Vertex`]. - pub size: [f32; 2], - - /// The color of the [`Vertex`], in __linear RGB__. - pub color: [f32; 4], - - /// The border color of the [`Vertex`], in __linear RGB__. - pub border_color: [f32; 4], - - /// The border radius of the [`Vertex`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Vertex`]. - pub border_width: f32, - - /// The __quad__ position of the [`Vertex`]. - pub q_position: [f32; 2], -} - -impl Vertex { - const SIZE: usize = std::mem::size_of::(); - - fn from_quad(quad: &layer::Quad) -> [Vertex; 4] { - let base = Vertex { - position: quad.position, - size: quad.size, - color: quad.color, - border_color: quad.color, - border_radius: quad.border_radius, - border_width: quad.border_width, - q_position: [0.0, 0.0], - }; - - [ - base, - Self { - q_position: [0.0, 1.0], - ..base - }, - Self { - q_position: [1.0, 0.0], - ..base - }, - Self { - q_position: [1.0, 1.0], - ..base - }, - ] - } -} diff --git a/glow/src/quad/core.rs b/glow/src/quad/core.rs deleted file mode 100644 index 89036530..00000000 --- a/glow/src/quad/core.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::program::{self, Shader}; -use crate::Transformation; -use glow::HasContext; -use iced_graphics::layer; -use iced_native::Rectangle; - -const MAX_INSTANCES: usize = 100_000; - -#[derive(Debug)] -pub struct Pipeline { - program: ::Program, - vertex_array: ::VertexArray, - instances: ::Buffer, - transform_location: ::UniformLocation, - scale_location: ::UniformLocation, - screen_height_location: ::UniformLocation, - current_transform: Transformation, - current_scale: f32, - current_target_height: u32, -} - -impl Pipeline { - pub fn new( - gl: &glow::Context, - shader_version: &program::Version, - ) -> Pipeline { - let program = unsafe { - let vertex_shader = Shader::vertex( - gl, - shader_version, - include_str!("../shader/core/quad.vert"), - ); - let fragment_shader = Shader::fragment( - gl, - shader_version, - include_str!("../shader/core/quad.frag"), - ); - - program::create( - gl, - &[vertex_shader, fragment_shader], - &[ - (0, "i_Pos"), - (1, "i_Scale"), - (2, "i_Color"), - (3, "i_BorderColor"), - (4, "i_BorderRadius"), - (5, "i_BorderWidth"), - ], - ) - }; - - let transform_location = - unsafe { gl.get_uniform_location(program, "u_Transform") } - .expect("Get transform location"); - - let scale_location = - unsafe { gl.get_uniform_location(program, "u_Scale") } - .expect("Get scale location"); - - let screen_height_location = - unsafe { gl.get_uniform_location(program, "u_ScreenHeight") } - .expect("Get target height location"); - - unsafe { - gl.use_program(Some(program)); - - gl.uniform_matrix_4_f32_slice( - Some(&transform_location), - false, - Transformation::identity().as_ref(), - ); - - gl.uniform_1_f32(Some(&scale_location), 1.0); - gl.uniform_1_f32(Some(&screen_height_location), 0.0); - - gl.use_program(None); - } - - let (vertex_array, instances) = - unsafe { create_instance_buffer(gl, MAX_INSTANCES) }; - - Pipeline { - program, - vertex_array, - instances, - transform_location, - scale_location, - screen_height_location, - current_transform: Transformation::identity(), - current_scale: 1.0, - current_target_height: 0, - } - } - - pub fn draw( - &mut self, - gl: &glow::Context, - target_height: u32, - instances: &[layer::Quad], - transformation: Transformation, - scale: f32, - bounds: Rectangle, - ) { - unsafe { - gl.enable(glow::SCISSOR_TEST); - gl.scissor( - bounds.x as i32, - (target_height - (bounds.y + bounds.height)) as i32, - bounds.width as i32, - bounds.height as i32, - ); - - gl.use_program(Some(self.program)); - gl.bind_vertex_array(Some(self.vertex_array)); - gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instances)); - } - - if transformation != self.current_transform { - unsafe { - gl.uniform_matrix_4_f32_slice( - Some(&self.transform_location), - false, - transformation.as_ref(), - ); - - self.current_transform = transformation; - } - } - - if scale != self.current_scale { - unsafe { - gl.uniform_1_f32(Some(&self.scale_location), scale); - } - - self.current_scale = scale; - } - - if target_height != self.current_target_height { - unsafe { - gl.uniform_1_f32( - Some(&self.screen_height_location), - target_height as f32, - ); - } - - self.current_target_height = target_height; - } - - for instances in instances.chunks(MAX_INSTANCES) { - unsafe { - gl.buffer_sub_data_u8_slice( - glow::ARRAY_BUFFER, - 0, - bytemuck::cast_slice(instances), - ); - - gl.draw_arrays_instanced( - glow::TRIANGLE_STRIP, - 0, - 4, - instances.len() as i32, - ); - } - } - - unsafe { - gl.bind_vertex_array(None); - gl.use_program(None); - gl.disable(glow::SCISSOR_TEST); - } - } -} - -unsafe fn create_instance_buffer( - gl: &glow::Context, - size: usize, -) -> ( - ::VertexArray, - ::Buffer, -) { - let vertex_array = gl.create_vertex_array().expect("Create vertex array"); - let buffer = gl.create_buffer().expect("Create instance buffer"); - - gl.bind_vertex_array(Some(vertex_array)); - gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer)); - gl.buffer_data_size( - glow::ARRAY_BUFFER, - (size * std::mem::size_of::()) as i32, - glow::DYNAMIC_DRAW, - ); - - let stride = std::mem::size_of::() as i32; - - gl.enable_vertex_attrib_array(0); - gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, stride, 0); - gl.vertex_attrib_divisor(0, 1); - - gl.enable_vertex_attrib_array(1); - gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, stride, 4 * 2); - gl.vertex_attrib_divisor(1, 1); - - gl.enable_vertex_attrib_array(2); - gl.vertex_attrib_pointer_f32(2, 4, glow::FLOAT, false, stride, 4 * (2 + 2)); - gl.vertex_attrib_divisor(2, 1); - - gl.enable_vertex_attrib_array(3); - gl.vertex_attrib_pointer_f32( - 3, - 4, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4), - ); - gl.vertex_attrib_divisor(3, 1); - - gl.enable_vertex_attrib_array(4); - gl.vertex_attrib_pointer_f32( - 4, - 4, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4 + 4), - ); - gl.vertex_attrib_divisor(4, 1); - - gl.enable_vertex_attrib_array(5); - gl.vertex_attrib_pointer_f32( - 5, - 1, - glow::FLOAT, - false, - stride, - 4 * (2 + 2 + 4 + 4 + 4), - ); - gl.vertex_attrib_divisor(5, 1); - - gl.bind_vertex_array(None); - gl.bind_buffer(glow::ARRAY_BUFFER, None); - - (vertex_array, buffer) -} diff --git a/glow/src/settings.rs b/glow/src/settings.rs deleted file mode 100644 index 6aaa0d55..00000000 --- a/glow/src/settings.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Configure a renderer. -pub use iced_graphics::Antialiasing; - -/// The settings of a [`Backend`]. -/// -/// [`Backend`]: crate::Backend -#[derive(Clone, Copy, PartialEq)] -pub struct Settings { - /// The bytes of the font that will be used by default. - /// - /// If `None` is provided, a default system font will be chosen. - pub default_font: Option<&'static [u8]>, - - /// The default size of text. - /// - /// By default, it will be set to `20.0`. - pub default_text_size: f32, - - /// If enabled, spread text workload in multiple threads when multiple cores - /// are available. - /// - /// By default, it is disabled. - pub text_multithreading: bool, - - /// The antialiasing strategy that will be used for triangle primitives. - /// - /// By default, it is `None`. - pub antialiasing: Option, -} - -impl Default for Settings { - fn default() -> Settings { - Settings { - default_font: None, - default_text_size: 20.0, - text_multithreading: false, - antialiasing: None, - } - } -} - -impl std::fmt::Debug for Settings { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Settings") - // Instead of printing the font bytes, we simply show a `bool` indicating if using a default font or not. - .field("default_font", &self.default_font.is_some()) - .field("default_text_size", &self.default_text_size) - .field("text_multithreading", &self.text_multithreading) - .field("antialiasing", &self.antialiasing) - .finish() - } -} - -impl Settings { - /// Creates new [`Settings`] using environment configuration. - /// - /// Currently, this is equivalent to calling [`Settings::default`]. - pub fn from_env() -> Self { - Self::default() - } -} diff --git a/glow/src/shader/common/gradient.frag b/glow/src/shader/common/gradient.frag deleted file mode 100644 index 9af0cb6e..00000000 --- a/glow/src/shader/common/gradient.frag +++ /dev/null @@ -1,59 +0,0 @@ -#ifdef GL_ES -#ifdef GL_FRAGMENT_PRECISION_HIGH -precision highp float; -#else -precision mediump float; -#endif -#endif - -#ifdef HIGHER_THAN_300 -layout (location = 0) out vec4 fragColor; -#define gl_FragColor fragColor -#endif - -in vec2 raw_position; - -uniform vec4 gradient_direction; -uniform int color_stops_size; -// GLSL does not support dynamically sized arrays without SSBOs so this is capped to 16 stops -//stored as color(vec4) -> offset(vec4) sequentially; -uniform vec4 color_stops[32]; - -//TODO: rewrite without branching to make ALUs happy -void main() { - vec2 start = gradient_direction.xy; - vec2 end = gradient_direction.zw; - vec2 gradient_vec = vec2(end - start); - vec2 current_vec = vec2(raw_position.xy - start); - vec2 unit = normalize(gradient_vec); - float coord_offset = dot(unit, current_vec) / length(gradient_vec); - //if a gradient has a start/end stop that is identical, the mesh will have a transparent fill - gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); - - float min_offset = color_stops[1].x; - float max_offset = color_stops[color_stops_size - 1].x; - - for (int i = 0; i < color_stops_size - 2; i += 2) { - float curr_offset = color_stops[i+1].x; - float next_offset = color_stops[i+3].x; - - if (coord_offset <= min_offset) { - //current coordinate is before the first defined offset, set it to the start color - gl_FragColor = color_stops[0]; - } - - if (curr_offset <= coord_offset && coord_offset <= next_offset) { - //current fragment is between the current offset processing & the next one, interpolate colors - gl_FragColor = mix(color_stops[i], color_stops[i+2], smoothstep( - curr_offset, - next_offset, - coord_offset - )); - } - - if (coord_offset >= max_offset) { - //current coordinate is before the last defined offset, set it to the last color - gl_FragColor = color_stops[color_stops_size - 2]; - } - } -} diff --git a/glow/src/shader/common/gradient.vert b/glow/src/shader/common/gradient.vert deleted file mode 100644 index fe505997..00000000 --- a/glow/src/shader/common/gradient.vert +++ /dev/null @@ -1,9 +0,0 @@ -uniform mat4 u_Transform; - -in vec2 i_Position; -out vec2 raw_position; - -void main() { - gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); - raw_position = i_Position; -} diff --git a/glow/src/shader/common/image.frag b/glow/src/shader/common/image.frag deleted file mode 100644 index 5e05abdf..00000000 --- a/glow/src/shader/common/image.frag +++ /dev/null @@ -1,22 +0,0 @@ -#ifdef GL_ES -#ifdef GL_FRAGMENT_PRECISION_HIGH -precision highp float; -#else -precision mediump float; -#endif -#endif - -uniform sampler2D tex; -in vec2 tex_pos; - -#ifdef HIGHER_THAN_300 -out vec4 fragColor; -#define gl_FragColor fragColor -#endif -#ifdef GL_ES -#define texture texture2D -#endif - -void main() { - gl_FragColor = texture(tex, tex_pos); -} diff --git a/glow/src/shader/common/image.vert b/glow/src/shader/common/image.vert deleted file mode 100644 index 93e541f2..00000000 --- a/glow/src/shader/common/image.vert +++ /dev/null @@ -1,9 +0,0 @@ -uniform mat4 u_Transform; - -in vec2 i_Position; -out vec2 tex_pos; - -void main() { - gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); - tex_pos = i_Position; -} diff --git a/glow/src/shader/common/solid.frag b/glow/src/shader/common/solid.frag deleted file mode 100644 index 174ffdd3..00000000 --- a/glow/src/shader/common/solid.frag +++ /dev/null @@ -1,18 +0,0 @@ -#ifdef GL_ES -#ifdef GL_FRAGMENT_PRECISION_HIGH -precision highp float; -#else -precision mediump float; -#endif -#endif - -#ifdef HIGHER_THAN_300 -out vec4 fragColor; -#define gl_FragColor fragColor -#endif - -in vec4 v_Color; - -void main() { - gl_FragColor = v_Color; -} diff --git a/glow/src/shader/common/solid.vert b/glow/src/shader/common/solid.vert deleted file mode 100644 index 59ed88e5..00000000 --- a/glow/src/shader/common/solid.vert +++ /dev/null @@ -1,11 +0,0 @@ -uniform mat4 u_Transform; - -in vec2 i_Position; -in vec4 i_Color; - -out vec4 v_Color; - -void main() { - gl_Position = u_Transform * vec4(i_Position, 0.0, 1.0); - v_Color = i_Color; -} diff --git a/glow/src/shader/compatibility/quad.frag b/glow/src/shader/compatibility/quad.frag deleted file mode 100644 index bb9d8122..00000000 --- a/glow/src/shader/compatibility/quad.frag +++ /dev/null @@ -1,83 +0,0 @@ -#ifdef GL_ES -#ifdef GL_FRAGMENT_PRECISION_HIGH -precision highp float; -#else -precision mediump float; -#endif -#endif - -uniform float u_ScreenHeight; - -varying vec4 v_Color; -varying vec4 v_BorderColor; -varying vec2 v_Pos; -varying vec2 v_Scale; -varying vec4 v_BorderRadius; -varying float v_BorderWidth; - -float _distance(vec2 frag_coord, vec2 position, vec2 size, float radius) -{ - // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN - vec2 inner_size = size - vec2(radius, radius) * 2.0; - vec2 top_left = position + vec2(radius, radius); - vec2 bottom_right = top_left + inner_size; - - vec2 top_left_distance = top_left - frag_coord; - vec2 bottom_right_distance = frag_coord - bottom_right; - - vec2 distance = vec2( - max(max(top_left_distance.x, bottom_right_distance.x), 0.0), - max(max(top_left_distance.y, bottom_right_distance.y), 0.0) - ); - - return sqrt(distance.x * distance.x + distance.y * distance.y); -} - -float selectBorderRadius(vec4 radi, vec2 position, vec2 center) -{ - float rx = radi.x; - float ry = radi.y; - rx = position.x > center.x ? radi.y : radi.x; - ry = position.x > center.x ? radi.z : radi.w; - rx = position.y > center.y ? ry : rx; - return rx; -} - -void main() { - vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); - - float border_radius = selectBorderRadius( - v_BorderRadius, - fragCoord, - (v_Pos + v_Scale * 0.5).xy - ); - - float internal_border = max(border_radius - v_BorderWidth, 0.0); - - float internal_distance = _distance( - fragCoord, - v_Pos + vec2(v_BorderWidth), - v_Scale - vec2(v_BorderWidth * 2.0), - internal_border - ); - - float border_mix = smoothstep( - max(internal_border - 0.5, 0.0), - internal_border + 0.5, - internal_distance - ); - - vec4 mixed_color = mix(v_Color, v_BorderColor, border_mix); - - float d = _distance( - fragCoord, - v_Pos, - v_Scale, - border_radius - ); - - float radius_alpha = - 1.0 - smoothstep(max(border_radius - 0.5, 0.0), border_radius + 0.5, d); - - gl_FragColor = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); -} diff --git a/glow/src/shader/compatibility/quad.vert b/glow/src/shader/compatibility/quad.vert deleted file mode 100644 index 89931f06..00000000 --- a/glow/src/shader/compatibility/quad.vert +++ /dev/null @@ -1,46 +0,0 @@ -uniform mat4 u_Transform; -uniform float u_Scale; - -attribute vec2 i_Pos; -attribute vec2 i_Scale; -attribute vec4 i_Color; -attribute vec4 i_BorderColor; -attribute vec4 i_BorderRadius; -attribute float i_BorderWidth; -attribute vec2 q_Pos; - -varying vec4 v_Color; -varying vec4 v_BorderColor; -varying vec2 v_Pos; -varying vec2 v_Scale; -varying vec4 v_BorderRadius; -varying float v_BorderWidth; - - -void main() { - vec2 p_Pos = i_Pos * u_Scale; - vec2 p_Scale = i_Scale * u_Scale; - - vec4 i_BorderRadius = vec4( - min(i_BorderRadius.x, min(i_Scale.x, i_Scale.y) / 2.0), - min(i_BorderRadius.y, min(i_Scale.x, i_Scale.y) / 2.0), - min(i_BorderRadius.z, min(i_Scale.x, i_Scale.y) / 2.0), - min(i_BorderRadius.w, min(i_Scale.x, i_Scale.y) / 2.0) - ); - - mat4 i_Transform = mat4( - vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0), - vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(p_Pos - vec2(0.5, 0.5), 0.0, 1.0) - ); - - v_Color = i_Color; - v_BorderColor = i_BorderColor; - v_Pos = p_Pos; - v_Scale = p_Scale; - v_BorderRadius = i_BorderRadius * u_Scale; - v_BorderWidth = i_BorderWidth * u_Scale; - - gl_Position = u_Transform * i_Transform * vec4(q_Pos, 0.0, 1.0); -} diff --git a/glow/src/shader/core/quad.frag b/glow/src/shader/core/quad.frag deleted file mode 100644 index 71147aa5..00000000 --- a/glow/src/shader/core/quad.frag +++ /dev/null @@ -1,95 +0,0 @@ -#ifdef GL_ES -#ifdef GL_FRAGMENT_PRECISION_HIGH -precision highp float; -#else -precision mediump float; -#endif -#endif - -#ifdef HIGHER_THAN_300 -out vec4 fragColor; -#define gl_FragColor fragColor -#endif - -uniform float u_ScreenHeight; - -in vec4 v_Color; -in vec4 v_BorderColor; -in vec2 v_Pos; -in vec2 v_Scale; -in vec4 v_BorderRadius; -in float v_BorderWidth; - -float fDistance(vec2 frag_coord, vec2 position, vec2 size, float radius) -{ - // TODO: Try SDF approach: https://www.shadertoy.com/view/wd3XRN - vec2 inner_size = size - vec2(radius, radius) * 2.0; - vec2 top_left = position + vec2(radius, radius); - vec2 bottom_right = top_left + inner_size; - - vec2 top_left_distance = top_left - frag_coord; - vec2 bottom_right_distance = frag_coord - bottom_right; - - vec2 distance = vec2( - max(max(top_left_distance.x, bottom_right_distance.x), 0.0), - max(max(top_left_distance.y, bottom_right_distance.y), 0.0) - ); - - return sqrt(distance.x * distance.x + distance.y * distance.y); -} - -float selectBorderRadius(vec4 radi, vec2 position, vec2 center) -{ - float rx = radi.x; - float ry = radi.y; - rx = position.x > center.x ? radi.y : radi.x; - ry = position.x > center.x ? radi.z : radi.w; - rx = position.y > center.y ? ry : rx; - return rx; -} - -void main() { - vec4 mixed_color; - - vec2 fragCoord = vec2(gl_FragCoord.x, u_ScreenHeight - gl_FragCoord.y); - - float border_radius = selectBorderRadius( - v_BorderRadius, - fragCoord, - (v_Pos + v_Scale * 0.5).xy - ); - - // TODO: Remove branching (?) - if(v_BorderWidth > 0.0) { - float internal_border = max(border_radius - v_BorderWidth, 0.0); - - float internal_distance = fDistance( - fragCoord, - v_Pos + vec2(v_BorderWidth), - v_Scale - vec2(v_BorderWidth * 2.0), - internal_border - ); - - float border_mix = smoothstep( - max(internal_border - 0.5, 0.0), - internal_border + 0.5, - internal_distance - ); - - mixed_color = mix(v_Color, v_BorderColor, border_mix); - } else { - mixed_color = v_Color; - } - - float d = fDistance( - fragCoord, - v_Pos, - v_Scale, - border_radius - ); - - float radius_alpha = - 1.0 - smoothstep(max(border_radius - 0.5, 0.0), border_radius + 0.5, d); - - gl_FragColor = vec4(mixed_color.xyz, mixed_color.w * radius_alpha); -} diff --git a/glow/src/shader/core/quad.vert b/glow/src/shader/core/quad.vert deleted file mode 100644 index 17c3e641..00000000 --- a/glow/src/shader/core/quad.vert +++ /dev/null @@ -1,52 +0,0 @@ -uniform mat4 u_Transform; -uniform float u_Scale; - -in vec2 i_Pos; -in vec2 i_Scale; -in vec4 i_Color; -in vec4 i_BorderColor; -in vec4 i_BorderRadius; -in float i_BorderWidth; - -out vec4 v_Color; -out vec4 v_BorderColor; -out vec2 v_Pos; -out vec2 v_Scale; -out vec4 v_BorderRadius; -out float v_BorderWidth; - -vec2 positions[4] = vec2[]( - vec2(0.0, 0.0), - vec2(0.0, 1.0), - vec2(1.0, 0.0), - vec2(1.0, 1.0) -); - -void main() { - vec2 q_Pos = positions[gl_VertexID]; - vec2 p_Pos = i_Pos * u_Scale; - vec2 p_Scale = i_Scale * u_Scale; - - vec4 i_BorderRadius = vec4( - min(i_BorderRadius.x, min(i_Scale.x, i_Scale.y) / 2.0), - min(i_BorderRadius.y, min(i_Scale.x, i_Scale.y) / 2.0), - min(i_BorderRadius.z, min(i_Scale.x, i_Scale.y) / 2.0), - min(i_BorderRadius.w, min(i_Scale.x, i_Scale.y) / 2.0) - ); - - mat4 i_Transform = mat4( - vec4(p_Scale.x + 1.0, 0.0, 0.0, 0.0), - vec4(0.0, p_Scale.y + 1.0, 0.0, 0.0), - vec4(0.0, 0.0, 1.0, 0.0), - vec4(p_Pos - vec2(0.5, 0.5), 0.0, 1.0) - ); - - v_Color = i_Color; - v_BorderColor = i_BorderColor; - v_Pos = p_Pos; - v_Scale = p_Scale; - v_BorderRadius = i_BorderRadius * u_Scale; - v_BorderWidth = i_BorderWidth * u_Scale; - - gl_Position = u_Transform * i_Transform * vec4(q_Pos, 0.0, 1.0); -} diff --git a/glow/src/text.rs b/glow/src/text.rs deleted file mode 100644 index 37ccdece..00000000 --- a/glow/src/text.rs +++ /dev/null @@ -1,257 +0,0 @@ -use crate::Transformation; - -use iced_graphics::font; - -use glow_glyph::ab_glyph; -use std::{cell::RefCell, collections::HashMap}; - -pub use iced_native::text::Hit; - -#[derive(Debug)] -pub struct Pipeline { - draw_brush: RefCell, - draw_font_map: RefCell>, - measure_brush: RefCell>, -} - -impl Pipeline { - pub fn new( - gl: &glow::Context, - default_font: Option<&[u8]>, - multithreading: bool, - ) -> Self { - let default_font = default_font.map(|slice| slice.to_vec()); - - // TODO: Font customization - #[cfg(feature = "default_system_font")] - let default_font = { - default_font.or_else(|| { - font::Source::new() - .load(&[font::Family::SansSerif, font::Family::Serif]) - .ok() - }) - }; - - let default_font = - default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); - - let font = ab_glyph::FontArc::try_from_vec(default_font) - .unwrap_or_else(|_| { - log::warn!( - "System font failed to load. Falling back to \ - embedded font..." - ); - - ab_glyph::FontArc::try_from_slice(font::FALLBACK) - .expect("Load fallback font") - }); - - let draw_brush_builder = - glow_glyph::GlyphBrushBuilder::using_font(font.clone()) - .initial_cache_size((2048, 2048)) - .draw_cache_multithread(multithreading); - - #[cfg(target_arch = "wasm32")] - let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true); - - let draw_brush = draw_brush_builder.build(gl); - - let measure_brush = - glyph_brush::GlyphBrushBuilder::using_font(font).build(); - - Pipeline { - draw_brush: RefCell::new(draw_brush), - draw_font_map: RefCell::new(HashMap::new()), - measure_brush: RefCell::new(measure_brush), - } - } - - pub fn queue(&mut self, section: glow_glyph::Section<'_>) { - self.draw_brush.borrow_mut().queue(section); - } - - pub fn draw_queued( - &mut self, - gl: &glow::Context, - transformation: Transformation, - region: glow_glyph::Region, - ) { - self.draw_brush - .borrow_mut() - .draw_queued_with_transform_and_scissoring( - gl, - transformation.into(), - region, - ) - .expect("Draw text"); - } - - pub fn measure( - &self, - content: &str, - size: f32, - font: iced_native::Font, - bounds: iced_native::Size, - ) -> (f32, f32) { - use glow_glyph::GlyphCruncher; - - let glow_glyph::FontId(font_id) = self.find_font(font); - - let section = glow_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![glow_glyph::Text { - text: content, - scale: size.into(), - font_id: glow_glyph::FontId(font_id), - extra: glow_glyph::Extra::default(), - }], - ..Default::default() - }; - - if let Some(bounds) = - self.measure_brush.borrow_mut().glyph_bounds(section) - { - (bounds.width().ceil(), bounds.height().ceil()) - } else { - (0.0, 0.0) - } - } - - pub fn hit_test( - &self, - content: &str, - size: f32, - font: iced_native::Font, - bounds: iced_native::Size, - point: iced_native::Point, - nearest_only: bool, - ) -> Option { - use glow_glyph::GlyphCruncher; - - let glow_glyph::FontId(font_id) = self.find_font(font); - - let section = glow_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![glow_glyph::Text { - text: content, - scale: size.into(), - font_id: glow_glyph::FontId(font_id), - extra: glow_glyph::Extra::default(), - }], - ..Default::default() - }; - - let mut mb = self.measure_brush.borrow_mut(); - - // The underlying type is FontArc, so clones are cheap. - use ab_glyph::{Font, ScaleFont}; - let font = mb.fonts()[font_id].clone().into_scaled(size); - - // Implements an iterator over the glyph bounding boxes. - let bounds = mb.glyphs(section).map( - |glow_glyph::SectionGlyph { - byte_index, glyph, .. - }| { - ( - *byte_index, - iced_native::Rectangle::new( - iced_native::Point::new( - glyph.position.x - font.h_side_bearing(glyph.id), - glyph.position.y - font.ascent(), - ), - iced_native::Size::new( - font.h_advance(glyph.id), - font.ascent() - font.descent(), - ), - ), - ) - }, - ); - - // Implements computation of the character index based on the byte index - // within the input string. - let char_index = |byte_index| { - let mut b_count = 0; - for (i, utf8_len) in - content.chars().map(|c| c.len_utf8()).enumerate() - { - if byte_index < (b_count + utf8_len) { - return i; - } - b_count += utf8_len; - } - - byte_index - }; - - if !nearest_only { - for (idx, bounds) in bounds.clone() { - if bounds.contains(point) { - return Some(Hit::CharOffset(char_index(idx))); - } - } - } - - let nearest = bounds - .map(|(index, bounds)| (index, bounds.center())) - .min_by(|(_, center_a), (_, center_b)| { - center_a - .distance(point) - .partial_cmp(¢er_b.distance(point)) - .unwrap_or(std::cmp::Ordering::Greater) - }); - - nearest.map(|(idx, center)| { - Hit::NearestCharOffset(char_index(idx), point - center) - }) - } - - pub fn trim_measurement_cache(&mut self) { - // TODO: We should probably use a `GlyphCalculator` for this. However, - // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. - // This makes stuff quite inconvenient. A manual method for trimming the - // cache would make our lives easier. - loop { - let action = self - .measure_brush - .borrow_mut() - .process_queued(|_, _| {}, |_| {}); - - match action { - Ok(_) => break, - Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { - let (width, height) = suggested; - - self.measure_brush - .borrow_mut() - .resize_texture(width, height); - } - } - } - } - - pub fn find_font(&self, font: iced_native::Font) -> glow_glyph::FontId { - match font { - iced_native::Font::Default => glow_glyph::FontId(0), - iced_native::Font::External { name, bytes } => { - if let Some(font_id) = self.draw_font_map.borrow().get(name) { - return *font_id; - } - - let font = ab_glyph::FontArc::try_from_slice(bytes) - .expect("Load font"); - - let _ = self.measure_brush.borrow_mut().add_font(font.clone()); - - let font_id = self.draw_brush.borrow_mut().add_font(font); - - let _ = self - .draw_font_map - .borrow_mut() - .insert(String::from(name), font_id); - - font_id - } - } - } -} diff --git a/glow/src/triangle.rs b/glow/src/triangle.rs deleted file mode 100644 index 42c88455..00000000 --- a/glow/src/triangle.rs +++ /dev/null @@ -1,595 +0,0 @@ -//! Draw meshes of triangles. -use crate::program; -use crate::Transformation; - -use iced_graphics::gradient::Gradient; -use iced_graphics::layer::mesh::{self, Mesh}; -use iced_graphics::triangle::{ColoredVertex2D, Vertex2D}; - -use glow::HasContext; -use std::marker::PhantomData; - -#[cfg(feature = "tracing")] -use tracing::info_span; - -const DEFAULT_VERTICES: usize = 1_000; -const DEFAULT_INDICES: usize = 1_000; - -#[derive(Debug)] -pub(crate) struct Pipeline { - indices: Buffer, - solid: solid::Program, - gradient: gradient::Program, -} - -impl Pipeline { - pub fn new(gl: &glow::Context, shader_version: &program::Version) -> Self { - let mut indices = unsafe { - Buffer::new( - gl, - glow::ELEMENT_ARRAY_BUFFER, - glow::DYNAMIC_DRAW, - DEFAULT_INDICES, - ) - }; - - let solid = solid::Program::new(gl, shader_version); - let gradient = gradient::Program::new(gl, shader_version); - - unsafe { - gl.bind_vertex_array(Some(solid.vertex_array)); - indices.bind(gl, 0); - - gl.bind_vertex_array(Some(gradient.vertex_array)); - indices.bind(gl, 0); - - gl.bind_vertex_array(None); - } - - Self { - indices, - solid, - gradient, - } - } - - pub fn draw( - &mut self, - meshes: &[Mesh<'_>], - gl: &glow::Context, - target_height: u32, - transformation: Transformation, - scale_factor: f32, - ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Glow::Triangle", "DRAW").enter(); - - unsafe { - gl.enable(glow::MULTISAMPLE); - gl.enable(glow::SCISSOR_TEST); - } - - // Count the total amount of vertices & indices we need to handle - let count = mesh::attribute_count_of(meshes); - - // Then we ensure the current attribute buffers are big enough, resizing if necessary - unsafe { - self.indices.bind(gl, count.indices); - } - - // We upload all the vertices and indices upfront - let mut solid_vertex_offset = 0; - let mut gradient_vertex_offset = 0; - let mut index_offset = 0; - - for mesh in meshes { - let indices = mesh.indices(); - - unsafe { - gl.buffer_sub_data_u8_slice( - glow::ELEMENT_ARRAY_BUFFER, - (index_offset * std::mem::size_of::()) as i32, - bytemuck::cast_slice(indices), - ); - - index_offset += indices.len(); - } - - match mesh { - Mesh::Solid { buffers, .. } => { - unsafe { - self.solid.vertices.bind(gl, count.solid_vertices); - - gl.buffer_sub_data_u8_slice( - glow::ARRAY_BUFFER, - (solid_vertex_offset - * std::mem::size_of::()) - as i32, - bytemuck::cast_slice(&buffers.vertices), - ); - } - - solid_vertex_offset += buffers.vertices.len(); - } - Mesh::Gradient { buffers, .. } => { - unsafe { - self.gradient - .vertices - .bind(gl, count.gradient_vertices); - - gl.buffer_sub_data_u8_slice( - glow::ARRAY_BUFFER, - (gradient_vertex_offset - * std::mem::size_of::()) - as i32, - bytemuck::cast_slice(&buffers.vertices), - ); - } - - gradient_vertex_offset += buffers.vertices.len(); - } - } - } - - // Then we draw each mesh using offsets - let mut last_solid_vertex = 0; - let mut last_gradient_vertex = 0; - let mut last_index = 0; - - for mesh in meshes { - let indices = mesh.indices(); - let origin = mesh.origin(); - - let transform = - transformation * Transformation::translate(origin.x, origin.y); - - let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); - - unsafe { - gl.scissor( - clip_bounds.x as i32, - (target_height - (clip_bounds.y + clip_bounds.height)) - as i32, - clip_bounds.width as i32, - clip_bounds.height as i32, - ); - } - - match mesh { - Mesh::Solid { buffers, .. } => unsafe { - gl.use_program(Some(self.solid.program)); - gl.bind_vertex_array(Some(self.solid.vertex_array)); - - if transform != self.solid.uniforms.transform { - gl.uniform_matrix_4_f32_slice( - Some(&self.solid.uniforms.transform_location), - false, - transform.as_ref(), - ); - - self.solid.uniforms.transform = transform; - } - - gl.draw_elements_base_vertex( - glow::TRIANGLES, - indices.len() as i32, - glow::UNSIGNED_INT, - (last_index * std::mem::size_of::()) as i32, - last_solid_vertex as i32, - ); - - last_solid_vertex += buffers.vertices.len(); - }, - Mesh::Gradient { - buffers, gradient, .. - } => unsafe { - gl.use_program(Some(self.gradient.program)); - gl.bind_vertex_array(Some(self.gradient.vertex_array)); - - if transform != self.gradient.uniforms.transform { - gl.uniform_matrix_4_f32_slice( - Some(&self.gradient.uniforms.locations.transform), - false, - transform.as_ref(), - ); - - self.gradient.uniforms.transform = transform; - } - - if &self.gradient.uniforms.gradient != *gradient { - match gradient { - Gradient::Linear(linear) => { - gl.uniform_4_f32( - Some( - &self - .gradient - .uniforms - .locations - .gradient_direction, - ), - linear.start.x, - linear.start.y, - linear.end.x, - linear.end.y, - ); - - gl.uniform_1_i32( - Some( - &self - .gradient - .uniforms - .locations - .color_stops_size, - ), - (linear.color_stops.len() * 2) as i32, - ); - - let mut stops = [0.0; 128]; - - for (index, stop) in linear - .color_stops - .iter() - .enumerate() - .take(16) - { - let [r, g, b, a] = stop.color.into_linear(); - - stops[index * 8] = r; - stops[(index * 8) + 1] = g; - stops[(index * 8) + 2] = b; - stops[(index * 8) + 3] = a; - stops[(index * 8) + 4] = stop.offset; - stops[(index * 8) + 5] = 0.; - stops[(index * 8) + 6] = 0.; - stops[(index * 8) + 7] = 0.; - } - - gl.uniform_4_f32_slice( - Some( - &self - .gradient - .uniforms - .locations - .color_stops, - ), - &stops, - ); - } - } - - self.gradient.uniforms.gradient = (*gradient).clone(); - } - - gl.draw_elements_base_vertex( - glow::TRIANGLES, - indices.len() as i32, - glow::UNSIGNED_INT, - (last_index * std::mem::size_of::()) as i32, - last_gradient_vertex as i32, - ); - - last_gradient_vertex += buffers.vertices.len(); - }, - } - - last_index += indices.len(); - } - - unsafe { - gl.bind_vertex_array(None); - gl.disable(glow::SCISSOR_TEST); - gl.disable(glow::MULTISAMPLE); - } - } -} - -#[derive(Debug)] -pub struct Buffer { - raw: ::Buffer, - target: u32, - usage: u32, - size: usize, - phantom: PhantomData, -} - -impl Buffer { - pub unsafe fn new( - gl: &glow::Context, - target: u32, - usage: u32, - size: usize, - ) -> Self { - let raw = gl.create_buffer().expect("Create buffer"); - - let mut buffer = Buffer { - raw, - target, - usage, - size: 0, - phantom: PhantomData, - }; - - buffer.bind(gl, size); - - buffer - } - - pub unsafe fn bind(&mut self, gl: &glow::Context, size: usize) { - gl.bind_buffer(self.target, Some(self.raw)); - - if self.size < size { - gl.buffer_data_size( - self.target, - (size * std::mem::size_of::()) as i32, - self.usage, - ); - - self.size = size; - } - } -} - -mod solid { - use crate::program; - use crate::triangle; - use glow::{Context, HasContext, NativeProgram}; - use iced_graphics::triangle::ColoredVertex2D; - use iced_graphics::Transformation; - - #[derive(Debug)] - pub struct Program { - pub program: ::Program, - pub vertex_array: ::VertexArray, - pub vertices: triangle::Buffer, - pub uniforms: Uniforms, - } - - impl Program { - pub fn new(gl: &Context, shader_version: &program::Version) -> Self { - let program = unsafe { - let vertex_shader = program::Shader::vertex( - gl, - shader_version, - include_str!("shader/common/solid.vert"), - ); - - let fragment_shader = program::Shader::fragment( - gl, - shader_version, - include_str!("shader/common/solid.frag"), - ); - - program::create( - gl, - &[vertex_shader, fragment_shader], - &[(0, "i_Position"), (1, "i_Color")], - ) - }; - - let vertex_array = unsafe { - gl.create_vertex_array().expect("Create vertex array") - }; - - let vertices = unsafe { - triangle::Buffer::new( - gl, - glow::ARRAY_BUFFER, - glow::DYNAMIC_DRAW, - super::DEFAULT_VERTICES, - ) - }; - - unsafe { - gl.bind_vertex_array(Some(vertex_array)); - - let stride = std::mem::size_of::() as i32; - - gl.enable_vertex_attrib_array(0); - gl.vertex_attrib_pointer_f32( - 0, - 2, - glow::FLOAT, - false, - stride, - 0, - ); - - gl.enable_vertex_attrib_array(1); - gl.vertex_attrib_pointer_f32( - 1, - 4, - glow::FLOAT, - false, - stride, - 4 * 2, - ); - - gl.bind_vertex_array(None); - }; - - Self { - program, - vertex_array, - vertices, - uniforms: Uniforms::new(gl, program), - } - } - } - - #[derive(Debug)] - pub struct Uniforms { - pub transform: Transformation, - pub transform_location: ::UniformLocation, - } - - impl Uniforms { - fn new(gl: &Context, program: NativeProgram) -> Self { - let transform = Transformation::identity(); - let transform_location = - unsafe { gl.get_uniform_location(program, "u_Transform") } - .expect("Solid - Get u_Transform."); - - unsafe { - gl.use_program(Some(program)); - - gl.uniform_matrix_4_f32_slice( - Some(&transform_location), - false, - transform.as_ref(), - ); - - gl.use_program(None); - } - - Self { - transform, - transform_location, - } - } - } -} - -mod gradient { - use crate::program; - use crate::triangle; - use glow::{Context, HasContext, NativeProgram}; - use iced_graphics::gradient::{self, Gradient}; - use iced_graphics::triangle::Vertex2D; - use iced_graphics::Transformation; - - #[derive(Debug)] - pub struct Program { - pub program: ::Program, - pub vertex_array: ::VertexArray, - pub vertices: triangle::Buffer, - pub uniforms: Uniforms, - } - - impl Program { - pub fn new(gl: &Context, shader_version: &program::Version) -> Self { - let program = unsafe { - let vertex_shader = program::Shader::vertex( - gl, - shader_version, - include_str!("shader/common/gradient.vert"), - ); - - let fragment_shader = program::Shader::fragment( - gl, - shader_version, - include_str!("shader/common/gradient.frag"), - ); - - program::create( - gl, - &[vertex_shader, fragment_shader], - &[(0, "i_Position")], - ) - }; - - let vertex_array = unsafe { - gl.create_vertex_array().expect("Create vertex array") - }; - - let vertices = unsafe { - triangle::Buffer::new( - gl, - glow::ARRAY_BUFFER, - glow::DYNAMIC_DRAW, - super::DEFAULT_VERTICES, - ) - }; - - unsafe { - gl.bind_vertex_array(Some(vertex_array)); - - let stride = std::mem::size_of::() as i32; - - gl.enable_vertex_attrib_array(0); - gl.vertex_attrib_pointer_f32( - 0, - 2, - glow::FLOAT, - false, - stride, - 0, - ); - - gl.bind_vertex_array(None); - }; - - Self { - program, - vertex_array, - vertices, - uniforms: Uniforms::new(gl, program), - } - } - } - - #[derive(Debug)] - pub struct Uniforms { - pub gradient: Gradient, - pub transform: Transformation, - pub locations: Locations, - } - - #[derive(Debug)] - pub struct Locations { - pub gradient_direction: ::UniformLocation, - pub color_stops_size: ::UniformLocation, - //currently the maximum number of stops is 16 due to lack of SSBO in GL2.1 - pub color_stops: ::UniformLocation, - pub transform: ::UniformLocation, - } - - impl Uniforms { - fn new(gl: &Context, program: NativeProgram) -> Self { - let gradient_direction = unsafe { - gl.get_uniform_location(program, "gradient_direction") - } - .expect("Gradient - Get gradient_direction."); - - let color_stops_size = - unsafe { gl.get_uniform_location(program, "color_stops_size") } - .expect("Gradient - Get color_stops_size."); - - let color_stops = unsafe { - gl.get_uniform_location(program, "color_stops") - .expect("Gradient - Get color_stops.") - }; - - let transform = Transformation::identity(); - let transform_location = - unsafe { gl.get_uniform_location(program, "u_Transform") } - .expect("Solid - Get u_Transform."); - - unsafe { - gl.use_program(Some(program)); - - gl.uniform_matrix_4_f32_slice( - Some(&transform_location), - false, - transform.as_ref(), - ); - - gl.use_program(None); - } - - Self { - gradient: Gradient::Linear(gradient::Linear { - start: Default::default(), - end: Default::default(), - color_stops: vec![], - }), - transform: Transformation::identity(), - locations: Locations { - gradient_direction, - color_stops_size, - color_stops, - transform: transform_location, - }, - } - } - } -} diff --git a/glow/src/window.rs b/glow/src/window.rs deleted file mode 100644 index aac5fb9e..00000000 --- a/glow/src/window.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Display rendering results on windows. -mod compositor; - -pub use compositor::Compositor; diff --git a/glow/src/window/compositor.rs b/glow/src/window/compositor.rs deleted file mode 100644 index 20756032..00000000 --- a/glow/src/window/compositor.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; - -use glow::HasContext; -use iced_graphics::{compositor, Antialiasing, Size}; - -use core::ffi::c_void; -use std::marker::PhantomData; - -/// A window graphics backend for iced powered by `glow`. -#[allow(missing_debug_implementations)] -pub struct Compositor { - gl: glow::Context, - theme: PhantomData, -} - -impl iced_graphics::window::GLCompositor for Compositor { - type Settings = Settings; - type Renderer = Renderer; - - unsafe fn new( - settings: Self::Settings, - loader_function: impl FnMut(&str) -> *const c_void, - ) -> Result<(Self, Self::Renderer), Error> { - let gl = glow::Context::from_loader_function(loader_function); - - log::info!("{:#?}", settings); - - let version = gl.version(); - log::info!( - "OpenGL version: {:?} (Embedded: {})", - version, - version.is_embedded - ); - - let renderer = gl.get_parameter_string(glow::RENDERER); - log::info!("Renderer: {}", renderer); - - // Enable auto-conversion from/to sRGB - gl.enable(glow::FRAMEBUFFER_SRGB); - - // Enable alpha blending - gl.enable(glow::BLEND); - gl.blend_func_separate( - glow::SRC_ALPHA, - glow::ONE_MINUS_SRC_ALPHA, - glow::ONE, - glow::ONE_MINUS_SRC_ALPHA, - ); - - // Disable multisampling by default - gl.disable(glow::MULTISAMPLE); - - let renderer = Renderer::new(Backend::new(&gl, settings)); - - Ok(( - Self { - gl, - theme: PhantomData, - }, - renderer, - )) - } - - fn sample_count(settings: &Settings) -> u32 { - settings - .antialiasing - .map(Antialiasing::sample_count) - .unwrap_or(0) - } - - fn resize_viewport(&mut self, physical_size: Size) { - unsafe { - self.gl.viewport( - 0, - 0, - physical_size.width as i32, - physical_size.height as i32, - ); - } - } - - fn fetch_information(&self) -> compositor::Information { - let adapter = unsafe { self.gl.get_parameter_string(glow::RENDERER) }; - - compositor::Information { - backend: format!("{:?}", self.gl.version()), - adapter, - } - } - - fn present>( - &mut self, - renderer: &mut Self::Renderer, - viewport: &Viewport, - color: Color, - overlay: &[T], - ) { - let gl = &self.gl; - - let [r, g, b, a] = color.into_linear(); - - unsafe { - gl.clear_color(r, g, b, a); - gl.clear(glow::COLOR_BUFFER_BIT); - } - - renderer.with_primitives(|backend, primitive| { - backend.present(gl, primitive, viewport, overlay); - }); - } -} diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 13ab61d8..19d6af0c 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -26,9 +26,6 @@ dds = ["image_rs/dds"] farbfeld = ["image_rs/farbfeld"] canvas = ["lyon"] qr_code = ["qrcode", "canvas"] -font-source = ["font-kit"] -font-fallback = [] -font-icons = [] opengl = [] image_rs = ["kamadak-exif"] @@ -60,10 +57,6 @@ version = "0.12" optional = true default-features = false -[dependencies.font-kit] -version = "0.10" -optional = true - [dependencies.image_rs] version = "0.24" package = "image" diff --git a/graphics/src/font.rs b/graphics/src/font.rs deleted file mode 100644 index d55d0faf..00000000 --- a/graphics/src/font.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Find system fonts or use the built-in ones. -#[cfg(feature = "font-source")] -mod source; - -#[cfg(feature = "font-source")] -#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] -pub use source::Source; - -#[cfg(feature = "font-source")] -#[cfg_attr(docsrs, doc(cfg(feature = "font-source")))] -pub use font_kit::{ - error::SelectionError as LoadError, family_name::FamilyName as Family, -}; - -/// A built-in fallback font, for convenience. -#[cfg(feature = "font-fallback")] -#[cfg_attr(docsrs, doc(cfg(feature = "font-fallback")))] -pub const FALLBACK: &[u8] = include_bytes!("../fonts/Lato-Regular.ttf"); - -/// A built-in icon font, for convenience. -#[cfg(feature = "font-icons")] -#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] -pub const ICONS: iced_native::Font = iced_native::Font::External { - name: "iced_wgpu icons", - bytes: include_bytes!("../fonts/Icons.ttf"), -}; - -/// The `char` representing a ✔ icon in the built-in [`ICONS`] font. -#[cfg(feature = "font-icons")] -#[cfg_attr(docsrs, doc(cfg(feature = "font-icons")))] -pub const CHECKMARK_ICON: char = '\u{F00C}'; - -/// The `char` representing a ▼ icon in the built-in [`ICONS`] font. -#[cfg(feature = "font-icons")] -pub const ARROW_DOWN_ICON: char = '\u{E800}'; diff --git a/graphics/src/font/source.rs b/graphics/src/font/source.rs deleted file mode 100644 index c0b50e1d..00000000 --- a/graphics/src/font/source.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::font::{Family, LoadError}; - -/// A font source that can find and load system fonts. -#[allow(missing_debug_implementations)] -pub struct Source { - raw: font_kit::source::SystemSource, -} - -impl Source { - /// Creates a new [`Source`]. - pub fn new() -> Self { - Source { - raw: font_kit::source::SystemSource::new(), - } - } - - /// Finds and loads a font matching the set of provided family priorities. - pub fn load(&self, families: &[Family]) -> Result, LoadError> { - let font = self.raw.select_best_match( - families, - &font_kit::properties::Properties::default(), - )?; - - match font { - font_kit::handle::Handle::Path { path, .. } => { - use std::io::Read; - - let mut buf = Vec::new(); - let mut reader = std::fs::File::open(path).expect("Read font"); - let _ = reader.read_to_end(&mut buf); - - Ok(buf) - } - font_kit::handle::Handle::Memory { bytes, .. } => { - Ok(bytes.as_ref().clone()) - } - } - } -} - -impl Default for Source { - fn default() -> Self { - Self::new() - } -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index d39dd90c..41bef2c3 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -28,7 +28,6 @@ mod transformation; mod viewport; pub mod backend; -pub mod font; pub mod gradient; pub mod image; pub mod layer; diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index f1e22cf6..5badeae6 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -23,14 +23,11 @@ dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] canvas = ["iced_graphics/canvas"] qr_code = ["iced_graphics/qr_code"] -default_system_font = ["iced_graphics/font-source"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] [dependencies] wgpu = "0.14" -wgpu_glyph = "0.18" -glyph_brush = "0.7" raw-window-handle = "0.5" log = "0.4" guillotiere = "0.6" @@ -48,7 +45,6 @@ path = "../native" [dependencies.iced_graphics] version = "0.7" path = "../graphics" -features = ["font-fallback", "font-icons"] [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 6a299425..e9e23e80 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -4,10 +4,8 @@ use crate::triangle; use crate::{Settings, Transformation}; use iced_graphics::backend; -use iced_graphics::font; use iced_graphics::layer::Layer; use iced_graphics::{Primitive, Viewport}; -use iced_native::alignment; use iced_native::{Font, Size}; #[cfg(feature = "tracing")] @@ -173,83 +171,11 @@ impl Backend { } if !layer.text.is_empty() { - for text in layer.text.iter() { - // Target physical coordinates directly to avoid blurry text - let text = wgpu_glyph::Section { - // TODO: We `round` here to avoid rerasterizing text when - // its position changes slightly. This can make text feel a - // bit "jumpy". We may be able to do better once we improve - // our text rendering/caching pipeline. - screen_position: ( - (text.bounds.x * scale_factor).round(), - (text.bounds.y * scale_factor).round(), - ), - // TODO: Fix precision issues with some scale factors. - // - // The `ceil` here can cause some words to render on the - // same line when they should not. - // - // Ideally, `wgpu_glyph` should be able to compute layout - // using logical positions, and then apply the proper - // scaling when rendering. This would ensure that both - // measuring and rendering follow the same layout rules. - bounds: ( - (text.bounds.width * scale_factor).ceil(), - (text.bounds.height * scale_factor).ceil(), - ), - text: vec![wgpu_glyph::Text { - text: text.content, - scale: wgpu_glyph::ab_glyph::PxScale { - x: text.size * scale_factor, - y: text.size * scale_factor, - }, - font_id: self.text_pipeline.find_font(text.font), - extra: wgpu_glyph::Extra { - color: text.color, - z: 0.0, - }, - }], - layout: wgpu_glyph::Layout::default() - .h_align(match text.horizontal_alignment { - alignment::Horizontal::Left => { - wgpu_glyph::HorizontalAlign::Left - } - alignment::Horizontal::Center => { - wgpu_glyph::HorizontalAlign::Center - } - alignment::Horizontal::Right => { - wgpu_glyph::HorizontalAlign::Right - } - }) - .v_align(match text.vertical_alignment { - alignment::Vertical::Top => { - wgpu_glyph::VerticalAlign::Top - } - alignment::Vertical::Center => { - wgpu_glyph::VerticalAlign::Center - } - alignment::Vertical::Bottom => { - wgpu_glyph::VerticalAlign::Bottom - } - }), - }; - - self.text_pipeline.queue(text); + for _text in layer.text.iter() { + // TODO: Queue text sections } - self.text_pipeline.draw_queued( - device, - staging_belt, - encoder, - target, - transformation, - wgpu_glyph::Region { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - ); + // TODO: Draw queued } } } @@ -261,9 +187,9 @@ impl iced_graphics::Backend for Backend { } impl backend::Text for Backend { - const ICON_FONT: Font = font::ICONS; - const CHECKMARK_ICON: char = font::CHECKMARK_ICON; - const ARROW_DOWN_ICON: char = font::ARROW_DOWN_ICON; + const ICON_FONT: Font = Font::Default; // TODO + const CHECKMARK_ICON: char = '✓'; + const ARROW_DOWN_ICON: char = '▼'; fn default_size(&self) -> f32 { self.default_text_size diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e17b84c1..125e6be0 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,265 +1,39 @@ -use crate::Transformation; - -use iced_graphics::font; - -use std::{cell::RefCell, collections::HashMap}; -use wgpu_glyph::ab_glyph; - pub use iced_native::text::Hit; #[derive(Debug)] -pub struct Pipeline { - draw_brush: RefCell>, - draw_font_map: RefCell>, - measure_brush: RefCell>, -} +pub struct Pipeline; impl Pipeline { pub fn new( - device: &wgpu::Device, - format: wgpu::TextureFormat, - default_font: Option<&[u8]>, - multithreading: bool, + _device: &wgpu::Device, + _format: wgpu::TextureFormat, + _default_font: Option<&[u8]>, + _multithreading: bool, ) -> Self { - let default_font = default_font.map(|slice| slice.to_vec()); - - // TODO: Font customization - #[cfg(not(target_os = "ios"))] - #[cfg(feature = "default_system_font")] - let default_font = { - default_font.or_else(|| { - font::Source::new() - .load(&[font::Family::SansSerif, font::Family::Serif]) - .ok() - }) - }; - - let default_font = - default_font.unwrap_or_else(|| font::FALLBACK.to_vec()); - - let font = ab_glyph::FontArc::try_from_vec(default_font) - .unwrap_or_else(|_| { - log::warn!( - "System font failed to load. Falling back to \ - embedded font..." - ); - - ab_glyph::FontArc::try_from_slice(font::FALLBACK) - .expect("Load fallback font") - }); - - let draw_brush_builder = - wgpu_glyph::GlyphBrushBuilder::using_font(font.clone()) - .initial_cache_size((2048, 2048)) - .draw_cache_multithread(multithreading); - - #[cfg(target_arch = "wasm32")] - let draw_brush_builder = draw_brush_builder.draw_cache_align_4x4(true); - - let draw_brush = draw_brush_builder.build(device, format); - - let measure_brush = - glyph_brush::GlyphBrushBuilder::using_font(font).build(); - - Pipeline { - draw_brush: RefCell::new(draw_brush), - draw_font_map: RefCell::new(HashMap::new()), - measure_brush: RefCell::new(measure_brush), - } - } - - pub fn queue(&mut self, section: wgpu_glyph::Section<'_>) { - self.draw_brush.borrow_mut().queue(section); - } - - pub fn draw_queued( - &mut self, - device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - transformation: Transformation, - region: wgpu_glyph::Region, - ) { - self.draw_brush - .borrow_mut() - .draw_queued_with_transform_and_scissoring( - device, - staging_belt, - encoder, - target, - transformation.into(), - region, - ) - .expect("Draw text"); + Pipeline } pub fn measure( &self, - content: &str, - size: f32, - font: iced_native::Font, - bounds: iced_native::Size, + _content: &str, + _size: f32, + _font: iced_native::Font, + _bounds: iced_native::Size, ) -> (f32, f32) { - use wgpu_glyph::GlyphCruncher; - - let wgpu_glyph::FontId(font_id) = self.find_font(font); - - let section = wgpu_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![wgpu_glyph::Text { - text: content, - scale: size.into(), - font_id: wgpu_glyph::FontId(font_id), - extra: wgpu_glyph::Extra::default(), - }], - ..Default::default() - }; - - if let Some(bounds) = - self.measure_brush.borrow_mut().glyph_bounds(section) - { - (bounds.width().ceil(), bounds.height().ceil()) - } else { - (0.0, 0.0) - } + (0.0, 0.0) } pub fn hit_test( &self, - content: &str, - size: f32, - font: iced_native::Font, - bounds: iced_native::Size, - point: iced_native::Point, - nearest_only: bool, + _content: &str, + _size: f32, + _font: iced_native::Font, + _bounds: iced_native::Size, + _point: iced_native::Point, + _nearest_only: bool, ) -> Option { - use wgpu_glyph::GlyphCruncher; - - let wgpu_glyph::FontId(font_id) = self.find_font(font); - - let section = wgpu_glyph::Section { - bounds: (bounds.width, bounds.height), - text: vec![wgpu_glyph::Text { - text: content, - scale: size.into(), - font_id: wgpu_glyph::FontId(font_id), - extra: wgpu_glyph::Extra::default(), - }], - ..Default::default() - }; - - let mut mb = self.measure_brush.borrow_mut(); - - // The underlying type is FontArc, so clones are cheap. - use wgpu_glyph::ab_glyph::{Font, ScaleFont}; - let font = mb.fonts()[font_id].clone().into_scaled(size); - - // Implements an iterator over the glyph bounding boxes. - let bounds = mb.glyphs(section).map( - |wgpu_glyph::SectionGlyph { - byte_index, glyph, .. - }| { - ( - *byte_index, - iced_native::Rectangle::new( - iced_native::Point::new( - glyph.position.x - font.h_side_bearing(glyph.id), - glyph.position.y - font.ascent(), - ), - iced_native::Size::new( - font.h_advance(glyph.id), - font.ascent() - font.descent(), - ), - ), - ) - }, - ); - - // Implements computation of the character index based on the byte index - // within the input string. - let char_index = |byte_index| { - let mut b_count = 0; - for (i, utf8_len) in - content.chars().map(|c| c.len_utf8()).enumerate() - { - if byte_index < (b_count + utf8_len) { - return i; - } - b_count += utf8_len; - } - - byte_index - }; - - if !nearest_only { - for (idx, bounds) in bounds.clone() { - if bounds.contains(point) { - return Some(Hit::CharOffset(char_index(idx))); - } - } - } - - let nearest = bounds - .map(|(index, bounds)| (index, bounds.center())) - .min_by(|(_, center_a), (_, center_b)| { - center_a - .distance(point) - .partial_cmp(¢er_b.distance(point)) - .unwrap_or(std::cmp::Ordering::Greater) - }); - - nearest.map(|(idx, center)| { - Hit::NearestCharOffset(char_index(idx), point - center) - }) + None } - pub fn trim_measurement_cache(&mut self) { - // TODO: We should probably use a `GlyphCalculator` for this. However, - // it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop. - // This makes stuff quite inconvenient. A manual method for trimming the - // cache would make our lives easier. - loop { - let action = self - .measure_brush - .borrow_mut() - .process_queued(|_, _| {}, |_| {}); - - match action { - Ok(_) => break, - Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => { - let (width, height) = suggested; - - self.measure_brush - .borrow_mut() - .resize_texture(width, height); - } - } - } - } - - pub fn find_font(&self, font: iced_native::Font) -> wgpu_glyph::FontId { - match font { - iced_native::Font::Default => wgpu_glyph::FontId(0), - iced_native::Font::External { name, bytes } => { - if let Some(font_id) = self.draw_font_map.borrow().get(name) { - return *font_id; - } - - let font = ab_glyph::FontArc::try_from_slice(bytes) - .expect("Load font"); - - let _ = self.measure_brush.borrow_mut().add_font(font.clone()); - - let font_id = self.draw_brush.borrow_mut().add_font(font); - - let _ = self - .draw_font_map - .borrow_mut() - .insert(String::from(name), font_id); - - font_id - } - } - } + pub fn trim_measurement_cache(&mut self) {} } -- cgit From baf51a8fcffc78e4ca20f7dcbba18ca3655f2840 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 31 Jan 2023 06:29:21 +0100 Subject: Draft `glyphon` implementation of text pipeline for `iced_wgpu` --- examples/integration/src/main.rs | 9 ++- graphics/src/layer.rs | 9 ++- graphics/src/layer/text.rs | 4 +- src/settings.rs | 4 +- wgpu/Cargo.toml | 5 ++ wgpu/src/backend.rs | 19 +++-- wgpu/src/text.rs | 160 ++++++++++++++++++++++++++++++++++++--- wgpu/src/window/compositor.rs | 3 +- 8 files changed, 187 insertions(+), 26 deletions(-) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 2a56b6fa..69f6961e 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -134,8 +134,12 @@ pub fn main() { // Initialize iced let mut debug = Debug::new(); - let mut renderer = - Renderer::new(Backend::new(&device, Settings::default(), format)); + let mut renderer = Renderer::new(Backend::new( + &device, + &queue, + Settings::default(), + format, + )); let mut state = program::State::new( controls, @@ -247,6 +251,7 @@ pub fn main() { renderer.with_primitives(|backend, primitive| { backend.present( &device, + &queue, &mut staging_belt, &mut encoder, &view, diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index 1d453caa..cc8f299c 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -12,7 +12,8 @@ pub use text::Text; use crate::alignment; use crate::{ - Background, Font, Point, Primitive, Rectangle, Size, Vector, Viewport, + Background, Color, Font, Point, Primitive, Rectangle, Size, Vector, + Viewport, }; /// A group of primitives that should be clipped together. @@ -60,7 +61,7 @@ impl<'a> Layer<'a> { Point::new(11.0, 11.0 + 25.0 * i as f32), Size::INFINITY, ), - color: [0.9, 0.9, 0.9, 1.0], + color: Color::new(0.9, 0.9, 0.9, 1.0), size: 20.0, font: Font::Default, horizontal_alignment: alignment::Horizontal::Left, @@ -71,7 +72,7 @@ impl<'a> Layer<'a> { overlay.text.push(Text { bounds: text.bounds + Vector::new(-1.0, -1.0), - color: [0.0, 0.0, 0.0, 1.0], + color: Color::BLACK, ..text }); } @@ -136,7 +137,7 @@ impl<'a> Layer<'a> { content, bounds: *bounds + translation, size: *size, - color: color.into_linear(), + color: *color, font: *font, horizontal_alignment: *horizontal_alignment, vertical_alignment: *vertical_alignment, diff --git a/graphics/src/layer/text.rs b/graphics/src/layer/text.rs index 74f7a676..38d62616 100644 --- a/graphics/src/layer/text.rs +++ b/graphics/src/layer/text.rs @@ -1,4 +1,4 @@ -use crate::{alignment, Font, Rectangle}; +use crate::{alignment, Color, Font, Rectangle}; /// A paragraph of text. #[derive(Debug, Clone, Copy)] @@ -10,7 +10,7 @@ pub struct Text<'a> { pub bounds: Rectangle, /// The color of the [`Text`], in __linear RGB_. - pub color: [f32; 4], + pub color: Color, /// The size of the [`Text`]. pub size: f32, diff --git a/src/settings.rs b/src/settings.rs index 0eb3e62d..266ad404 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -28,7 +28,7 @@ pub struct Settings { /// The text size that will be used by default. /// - /// The default value is `20.0`. + /// The default value is `16.0`. pub default_text_size: f32, /// If enabled, spread text workload in multiple threads when multiple cores @@ -97,7 +97,7 @@ where window: Default::default(), flags: Default::default(), default_font: Default::default(), - default_text_size: 20.0, + default_text_size: 16.0, text_multithreading: false, antialiasing: false, exit_on_close_request: true, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 5badeae6..d29c1129 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -33,6 +33,7 @@ log = "0.4" guillotiere = "0.6" futures = "0.3" bitflags = "1.2" +once_cell = "1.0" [dependencies.bytemuck] version = "1.9" @@ -46,6 +47,10 @@ path = "../native" version = "0.7" path = "../graphics" +[dependencies.glyphon] +version = "0.2" +path = "../../glyphon" + [dependencies.tracing] version = "0.1.6" optional = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index e9e23e80..77785760 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -18,7 +18,7 @@ use crate::image; /// /// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs /// [`iced`]: https://github.com/iced-rs/iced -#[derive(Debug)] +#[allow(missing_debug_implementations)] pub struct Backend { quad_pipeline: quad::Pipeline, text_pipeline: text::Pipeline, @@ -34,11 +34,13 @@ impl Backend { /// Creates a new [`Backend`]. pub fn new( device: &wgpu::Device, + queue: &wgpu::Queue, settings: Settings, format: wgpu::TextureFormat, ) -> Self { let text_pipeline = text::Pipeline::new( device, + queue, format, settings.default_font, settings.text_multithreading, @@ -70,6 +72,7 @@ impl Backend { pub fn present>( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, frame: &wgpu::TextureView, @@ -91,6 +94,7 @@ impl Backend { for layer in layers { self.flush( device, + queue, scale_factor, transformation, &layer, @@ -108,6 +112,7 @@ impl Backend { fn flush( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, scale_factor: f32, transformation: Transformation, layer: &Layer<'_>, @@ -171,11 +176,15 @@ impl Backend { } if !layer.text.is_empty() { - for _text in layer.text.iter() { - // TODO: Queue text sections - } + self.text_pipeline.prepare( + device, + queue, + &layer.text, + scale_factor, + target_size, + ); - // TODO: Draw queued + self.text_pipeline.render(encoder, target); } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 125e6be0..ccb627cd 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,26 +1,166 @@ pub use iced_native::text::Hit; -#[derive(Debug)] -pub struct Pipeline; +use iced_graphics::layer::Text; +use iced_native::{Font, Size}; + +#[allow(missing_debug_implementations)] +pub struct Pipeline { + renderer: glyphon::TextRenderer, + atlas: glyphon::TextAtlas, + cache: glyphon::SwashCache<'static>, +} + +// TODO: Share with `iced_graphics` +static FONT_SYSTEM: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(glyphon::FontSystem::new); impl Pipeline { pub fn new( - _device: &wgpu::Device, - _format: wgpu::TextureFormat, + device: &wgpu::Device, + queue: &wgpu::Queue, + format: wgpu::TextureFormat, _default_font: Option<&[u8]>, _multithreading: bool, ) -> Self { - Pipeline + Pipeline { + renderer: glyphon::TextRenderer::new(device, queue), + atlas: glyphon::TextAtlas::new(device, queue, format), + cache: glyphon::SwashCache::new(&FONT_SYSTEM), + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + sections: &[Text<'_>], + scale_factor: f32, + target_size: Size, + ) { + let buffers: Vec<_> = sections + .iter() + .map(|section| { + let metrics = glyphon::Metrics::new( + (section.size * scale_factor) as i32, + (section.size * 1.2 * scale_factor) as i32, + ); + + let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + + buffer.set_size( + (section.bounds.width * scale_factor).ceil() as i32, + (section.bounds.height * scale_factor).ceil() as i32, + ); + + buffer.set_text( + section.content, + glyphon::Attrs::new() + .color({ + let [r, g, b, a] = section.color.into_rgba8(); + glyphon::Color::rgba(r, g, b, a) + }) + .family(match section.font { + Font::Default => glyphon::Family::SansSerif, + Font::External { name, .. } => { + glyphon::Family::Name(name) + } + }), + ); + + buffer.shape_until_scroll(); + + buffer + }) + .collect(); + + let text_areas: Vec<_> = sections + .iter() + .zip(buffers.iter()) + .map(|(section, buffer)| glyphon::TextArea { + buffer, + left: (section.bounds.x * scale_factor) as i32, + top: (section.bounds.y * scale_factor) as i32, + bounds: glyphon::TextBounds { + left: (section.bounds.x * scale_factor) as i32, + top: (section.bounds.y * scale_factor) as i32, + right: ((section.bounds.x + section.bounds.width) + * scale_factor) as i32, + bottom: ((section.bounds.y + section.bounds.height) + * scale_factor) as i32, + }, + }) + .collect(); + + self.renderer + .prepare( + device, + queue, + &mut self.atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + &text_areas, + glyphon::Color::rgb(0, 0, 0), + &mut self.cache, + ) + .expect("Prepare text sections"); + } + + pub fn render( + &mut self, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + ) { + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + self.renderer + .render(&self.atlas, &mut render_pass) + .expect("Render text"); } pub fn measure( &self, - _content: &str, - _size: f32, - _font: iced_native::Font, - _bounds: iced_native::Size, + content: &str, + size: f32, + font: Font, + bounds: Size, ) -> (f32, f32) { - (0.0, 0.0) + let attrs = match font { + Font::Default => glyphon::Attrs::new(), + Font::External { name, .. } => glyphon::Attrs { + family: glyphon::Family::Name(name), + ..glyphon::Attrs::new() + }, + }; + + let mut paragraph = + glyphon::BufferLine::new(content, glyphon::AttrsList::new(attrs)); + + // TODO: Cache layout + let layout = paragraph.layout( + &FONT_SYSTEM, + size as i32, + bounds.width as i32, + glyphon::Wrap::Word, + ); + + ( + layout.iter().fold(0.0, |max, line| line.w.max(max)), + size * 1.2 * layout.len() as f32, + ) } pub fn hit_test( diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 6d0c36f6..50231f7c 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -114,7 +114,7 @@ impl Compositor { /// Creates a new rendering [`Backend`] for this [`Compositor`]. pub fn create_backend(&self) -> Backend { - Backend::new(&self.device, self.settings, self.format) + Backend::new(&self.device, &self.queue, self.settings, self.format) } } @@ -227,6 +227,7 @@ impl iced_graphics::window::Compositor for Compositor { renderer.with_primitives(|backend, primitives| { backend.present( &self.device, + &self.queue, &mut self.staging_belt, &mut encoder, view, -- cgit From ba258f8fbcf72eaabefe193b6fbee6484b44e569 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 03:24:14 +0100 Subject: Implement support for multiple text layers in `iced_wgpu` --- wgpu/src/backend.rs | 3 +++ wgpu/src/text.rs | 45 ++++++++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 77785760..9bf464b3 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -105,6 +105,8 @@ impl Backend { ); } + self.text_pipeline.end_frame(); + #[cfg(any(feature = "image", feature = "svg"))] self.image_pipeline.trim_cache(device, encoder); } @@ -180,6 +182,7 @@ impl Backend { device, queue, &layer.text, + layer.bounds, scale_factor, target_size, ); diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index ccb627cd..2b18299f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,13 +1,14 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; -use iced_native::{Font, Size}; +use iced_native::{Font, Rectangle, Size}; #[allow(missing_debug_implementations)] pub struct Pipeline { - renderer: glyphon::TextRenderer, + renderers: Vec, atlas: glyphon::TextAtlas, cache: glyphon::SwashCache<'static>, + layer: usize, } // TODO: Share with `iced_graphics` @@ -23,9 +24,10 @@ impl Pipeline { _multithreading: bool, ) -> Self { Pipeline { - renderer: glyphon::TextRenderer::new(device, queue), + renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), cache: glyphon::SwashCache::new(&FONT_SYSTEM), + layer: 0, } } @@ -34,9 +36,17 @@ impl Pipeline { device: &wgpu::Device, queue: &wgpu::Queue, sections: &[Text<'_>], + bounds: Rectangle, scale_factor: f32, target_size: Size, ) { + if self.renderers.len() <= self.layer { + self.renderers + .push(glyphon::TextRenderer::new(device, queue)); + } + + let renderer = &mut self.renderers[self.layer]; + let buffers: Vec<_> = sections .iter() .map(|section| { @@ -73,6 +83,13 @@ impl Pipeline { }) .collect(); + let bounds = glyphon::TextBounds { + left: (bounds.x * scale_factor) as i32, + top: (bounds.y * scale_factor) as i32, + right: ((bounds.x + bounds.width) * scale_factor) as i32, + bottom: ((bounds.y + bounds.height) * scale_factor) as i32, + }; + let text_areas: Vec<_> = sections .iter() .zip(buffers.iter()) @@ -80,18 +97,11 @@ impl Pipeline { buffer, left: (section.bounds.x * scale_factor) as i32, top: (section.bounds.y * scale_factor) as i32, - bounds: glyphon::TextBounds { - left: (section.bounds.x * scale_factor) as i32, - top: (section.bounds.y * scale_factor) as i32, - right: ((section.bounds.x + section.bounds.width) - * scale_factor) as i32, - bottom: ((section.bounds.y + section.bounds.height) - * scale_factor) as i32, - }, + bounds, }) .collect(); - self.renderer + renderer .prepare( device, queue, @@ -126,9 +136,18 @@ impl Pipeline { depth_stencil_attachment: None, }); - self.renderer + let renderer = &mut self.renderers[self.layer]; + + renderer .render(&self.atlas, &mut render_pass) .expect("Render text"); + + self.layer += 1; + } + + pub fn end_frame(&mut self) { + self.renderers.truncate(self.layer); + self.layer = 0; } pub fn measure( -- cgit From 98a16fd670d501a0072fee13b023be686ff30cf8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 03:39:15 +0100 Subject: Implement proper text alignment support in `iced_wgpu` --- wgpu/src/text.rs | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2b18299f..19d0782a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,6 +1,7 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; +use iced_native::alignment; use iced_native::{Font, Rectangle, Size}; #[allow(missing_debug_implementations)] @@ -93,11 +94,37 @@ impl Pipeline { let text_areas: Vec<_> = sections .iter() .zip(buffers.iter()) - .map(|(section, buffer)| glyphon::TextArea { - buffer, - left: (section.bounds.x * scale_factor) as i32, - top: (section.bounds.y * scale_factor) as i32, - bounds, + .map(|(section, buffer)| { + let x = section.bounds.x * scale_factor; + let y = section.bounds.y * scale_factor; + + let max_width = buffer + .layout_runs() + .fold(0.0f32, |max, run| max.max(run.line_w)); + + let total_height = buffer.visible_lines() as f32 + * section.size + * 1.2 + * scale_factor; + + let left = match section.horizontal_alignment { + alignment::Horizontal::Left => x, + alignment::Horizontal::Center => x - max_width / 2.0, + alignment::Horizontal::Right => x - max_width, + }; + + let top = match section.vertical_alignment { + alignment::Vertical::Top => y, + alignment::Vertical::Center => y - total_height / 2.0, + alignment::Vertical::Bottom => y - total_height, + }; + + glyphon::TextArea { + buffer, + left: left as i32, + top: top as i32, + bounds, + } }) .collect(); -- cgit From c68edb3278fdfa3c5ea6b29b9e2213fed56d4e66 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 04:05:51 +0100 Subject: Fix `TextInput` line height --- native/src/widget/text_input.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index ee0473ea..5e198b8f 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -387,7 +387,11 @@ where let text_size = size.unwrap_or_else(|| renderer.default_size()); let padding = padding.fit(Size::ZERO, limits.max()); - let limits = limits.width(width).pad(padding).height(text_size); + + let limits = limits + .width(width) + .pad(padding) + .height(text_size as f32 * 1.2); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); text.move_to(Point::new(padding.left, padding.top)); -- cgit From bb27982009d89a1cf4874697f7c17ff0af71d93d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 04:06:29 +0100 Subject: Convert sRGB to linear RGB for text in `iced_wgpu` --- wgpu/src/text.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 19d0782a..3f828da1 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -67,8 +67,14 @@ impl Pipeline { section.content, glyphon::Attrs::new() .color({ - let [r, g, b, a] = section.color.into_rgba8(); - glyphon::Color::rgba(r, g, b, a) + let [r, g, b, a] = section.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) }) .family(match section.font { Font::Default => glyphon::Family::SansSerif, -- cgit From 032e860f13a562719faf128238abe7ffb7f2a610 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Feb 2023 04:14:11 +0100 Subject: Fix `PickList` line height --- native/src/widget/pick_list.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 17528db4..a128d1ae 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -384,7 +384,7 @@ where let size = { let intrinsic = - Size::new(max_width + text_size + padding.left, text_size); + Size::new(max_width + text_size + padding.left, text_size * 1.2); limits.resolve(intrinsic).pad(padding) }; @@ -620,12 +620,12 @@ pub fn draw<'a, T, Renderer>( color: style.handle_color, bounds: Rectangle { x: bounds.x + bounds.width - padding.horizontal(), - y: bounds.center_y() - size / 2.0, - height: size, + y: bounds.center_y(), + height: size * 1.2, ..bounds }, horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Top, + vertical_alignment: alignment::Vertical::Center, }); } @@ -645,12 +645,12 @@ pub fn draw<'a, T, Renderer>( }, bounds: Rectangle { x: bounds.x + padding.left, - y: bounds.center_y() - text_size / 2.0, + y: bounds.center_y(), width: bounds.width - padding.horizontal(), - height: text_size, + height: text_size * 1.2, }, horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + vertical_alignment: alignment::Vertical::Center, }); } } -- cgit From 1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:24:27 +0100 Subject: Implement basic text caching in `iced_wgpu` --- core/src/font.rs | 16 +++++ wgpu/Cargo.toml | 2 + wgpu/src/text.rs | 201 ++++++++++++++++++++++++++++++++++++++----------------- 3 files changed, 159 insertions(+), 60 deletions(-) diff --git a/core/src/font.rs b/core/src/font.rs index 3f9ad2b5..130e378e 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,3 +1,5 @@ +use std::hash::{Hash, Hasher}; + /// A font. #[derive(Debug, Clone, Copy)] pub enum Font { @@ -22,3 +24,17 @@ impl Default for Font { Font::Default } } + +impl Hash for Font { + fn hash(&self, hasher: &mut H) { + match self { + Self::Default => { + 0.hash(hasher); + } + Self::External { name, .. } => { + 1.hash(hasher); + name.hash(hasher); + } + } + } +} diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index d29c1129..1a94c6a3 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -34,6 +34,8 @@ guillotiere = "0.6" futures = "0.3" bitflags = "1.2" once_cell = "1.0" +rustc-hash = "1.1" +twox-hash = "1.6" [dependencies.bytemuck] version = "1.9" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 3f828da1..a1d621d4 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,16 +2,107 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; use iced_native::alignment; -use iced_native::{Font, Rectangle, Size}; +use iced_native::{Color, Font, Rectangle, Size}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::cell::RefCell; +use std::hash::{BuildHasher, Hash, Hasher}; +use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { renderers: Vec, atlas: glyphon::TextAtlas, cache: glyphon::SwashCache<'static>, + measurement_cache: RefCell, + render_cache: Cache, layer: usize, } +struct Cache { + entries: FxHashMap>, + recently_used: FxHashSet, + hasher: RandomXxHashBuilder64, +} + +impl Cache { + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: RandomXxHashBuilder64::default(), + } + } + + fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> { + self.entries.get(key) + } + + fn allocate( + &mut self, + key: Key<'_>, + ) -> (KeyHash, &mut glyphon::Buffer<'static>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + (key.size as i32).hash(&mut hasher); + key.font.hash(&mut hasher); + (key.bounds.width as i32).hash(&mut hasher); + (key.bounds.height as i32).hash(&mut hasher); + key.color.into_rgba8().hash(&mut hasher); + + hasher.finish() + }; + + if !self.entries.contains_key(&hash) { + let metrics = + glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + + let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + + buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_text( + key.content, + glyphon::Attrs::new().family(to_family(key.font)).color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }), + ); + + let _ = self.entries.insert(hash, buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, + color: Color, +} + +type KeyHash = u64; + // TODO: Share with `iced_graphics` static FONT_SYSTEM: once_cell::sync::Lazy = once_cell::sync::Lazy::new(glyphon::FontSystem::new); @@ -28,6 +119,8 @@ impl Pipeline { renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), cache: glyphon::SwashCache::new(&FONT_SYSTEM), + measurement_cache: RefCell::new(Cache::new()), + render_cache: Cache::new(), layer: 0, } } @@ -48,48 +141,29 @@ impl Pipeline { let renderer = &mut self.renderers[self.layer]; - let buffers: Vec<_> = sections + let keys: Vec<_> = sections .iter() .map(|section| { - let metrics = glyphon::Metrics::new( - (section.size * scale_factor) as i32, - (section.size * 1.2 * scale_factor) as i32, - ); - - let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); - - buffer.set_size( - (section.bounds.width * scale_factor).ceil() as i32, - (section.bounds.height * scale_factor).ceil() as i32, - ); - - buffer.set_text( - section.content, - glyphon::Attrs::new() - .color({ - let [r, g, b, a] = section.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }) - .family(match section.font { - Font::Default => glyphon::Family::SansSerif, - Font::External { name, .. } => { - glyphon::Family::Name(name) - } - }), - ); - - buffer.shape_until_scroll(); - - buffer + let (key, _) = self.render_cache.allocate(Key { + content: section.content, + size: section.size * scale_factor, + font: section.font, + bounds: Size { + width: section.bounds.width * scale_factor, + height: section.bounds.height * scale_factor, + }, + color: section.color, + }); + + key }) .collect(); + let buffers: Vec<_> = keys + .iter() + .map(|key| self.render_cache.get(key).expect("Get cached buffer")) + .collect(); + let bounds = glyphon::TextBounds { left: (bounds.x * scale_factor) as i32, top: (bounds.y * scale_factor) as i32, @@ -190,29 +264,27 @@ impl Pipeline { font: Font, bounds: Size, ) -> (f32, f32) { - let attrs = match font { - Font::Default => glyphon::Attrs::new(), - Font::External { name, .. } => glyphon::Attrs { - family: glyphon::Family::Name(name), - ..glyphon::Attrs::new() + let mut measurement_cache = self.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate(Key { + content, + size: size, + font, + bounds: Size { + width: bounds.width, + height: f32::INFINITY, }, - }; + color: Color::BLACK, + }); + + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - let mut paragraph = - glyphon::BufferLine::new(content, glyphon::AttrsList::new(attrs)); - - // TODO: Cache layout - let layout = paragraph.layout( - &FONT_SYSTEM, - size as i32, - bounds.width as i32, - glyphon::Wrap::Word, - ); - - ( - layout.iter().fold(0.0, |max, line| line.w.max(max)), - size * 1.2 * layout.len() as f32, - ) + (max_width, size * 1.2 * total_lines as f32) } pub fn hit_test( @@ -227,5 +299,14 @@ impl Pipeline { None } - pub fn trim_measurement_cache(&mut self) {} + pub fn trim_measurement_cache(&mut self) { + self.measurement_cache.borrow_mut().trim(); + } +} + +fn to_family(font: Font) -> glyphon::Family<'static> { + match font { + Font::Default => glyphon::Family::SansSerif, + Font::External { name, .. } => glyphon::Family::Name(name), + } } -- cgit From 02fc7e6e89e0a098a2a8cae8490f36d3bdf8126c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:30:49 +0100 Subject: Trim text `render_cache` after rendering in `iced_wgpu` --- wgpu/src/text.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index a1d621d4..cca00435 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -254,6 +254,8 @@ impl Pipeline { pub fn end_frame(&mut self) { self.renderers.truncate(self.layer); + self.render_cache.trim(); + self.layer = 0; } -- cgit From 6b707711469c7298bb363029a0c3d12a7834278c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:34:25 +0100 Subject: Avoid unnecessary `Vec` allocation in text pipeline --- wgpu/src/text.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index cca00435..1e2bf5c2 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -159,11 +159,6 @@ impl Pipeline { }) .collect(); - let buffers: Vec<_> = keys - .iter() - .map(|key| self.render_cache.get(key).expect("Get cached buffer")) - .collect(); - let bounds = glyphon::TextBounds { left: (bounds.x * scale_factor) as i32, top: (bounds.y * scale_factor) as i32, @@ -173,8 +168,11 @@ impl Pipeline { let text_areas: Vec<_> = sections .iter() - .zip(buffers.iter()) - .map(|(section, buffer)| { + .zip(keys.iter()) + .map(|(section, key)| { + let buffer = + self.render_cache.get(key).expect("Get cached buffer"); + let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; -- cgit From c8e8b1a7ba47ab13cef2dc7f300fbfcefe1e8a48 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 01:51:08 +0100 Subject: Use `bounds` directly for `measure` in text pipeline --- wgpu/src/text.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 1e2bf5c2..026a6d27 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -270,10 +270,7 @@ impl Pipeline { content, size: size, font, - bounds: Size { - width: bounds.width, - height: f32::INFINITY, - }, + bounds, color: Color::BLACK, }); -- cgit From 0a324f0aebdee06c9cce8ef107b44b847dace05b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Feb 2023 02:02:41 +0100 Subject: Implement `hit_test` for `text::Pipeline` in `iced_wgpu` --- wgpu/src/text.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 026a6d27..41787136 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -286,14 +286,26 @@ impl Pipeline { pub fn hit_test( &self, - _content: &str, - _size: f32, - _font: iced_native::Font, - _bounds: iced_native::Size, - _point: iced_native::Point, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + point: iced_native::Point, _nearest_only: bool, ) -> Option { - None + let mut measurement_cache = self.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate(Key { + content, + size: size, + font, + bounds, + color: Color::BLACK, + }); + + let cursor = paragraph.hit(point.x as i32, point.y as i32)?; + + Some(Hit::CharOffset(cursor.index)) } pub fn trim_measurement_cache(&mut self) { -- cgit From a7580e0696a1a0ba76a89db3f78bc99ba3fbb361 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 06:50:38 +0100 Subject: Count `layout_runs` instead of using `visible_lines` in `text::Pipeline::prepare` --- wgpu/src/text.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 41787136..fa7fd5df 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -176,14 +176,15 @@ impl Pipeline { let x = section.bounds.x * scale_factor; let y = section.bounds.y * scale_factor; - let max_width = buffer + let (total_lines, max_width) = buffer .layout_runs() - .fold(0.0f32, |max, run| max.max(run.line_w)); + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - let total_height = buffer.visible_lines() as f32 - * section.size - * 1.2 - * scale_factor; + let total_height = + total_lines as f32 * section.size * 1.2 * scale_factor; let left = match section.horizontal_alignment { alignment::Horizontal::Left => x, -- cgit From b29de28d1f0f608f8029c93d154cfd1b0f8b8cbb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 07:33:33 +0100 Subject: Overhaul `Font` type to allow font family selection --- core/src/font.rs | 53 +++++++++------------ examples/todos/fonts/icons.ttf | Bin 5596 -> 5732 bytes examples/todos/src/main.rs | 5 +- graphics/fonts/Icons.ttf | Bin 5032 -> 0 bytes graphics/fonts/Lato-Regular.ttf | Bin 75136 -> 0 bytes graphics/fonts/OFL.txt | 93 ------------------------------------- graphics/src/backend.rs | 3 ++ graphics/src/layer.rs | 2 +- graphics/src/renderer.rs | 4 ++ graphics/src/widget/canvas/text.rs | 2 +- native/src/overlay/menu.rs | 12 ++--- native/src/renderer/null.rs | 8 +++- native/src/text.rs | 5 +- native/src/widget/checkbox.rs | 13 +++--- native/src/widget/pick_list.rs | 25 +++++----- native/src/widget/radio.rs | 11 +++-- native/src/widget/text.rs | 20 +++++--- native/src/widget/text_input.rs | 39 +++++++++------- native/src/widget/toggler.rs | 13 +++--- src/application.rs | 1 - src/settings.rs | 18 ++----- wgpu/fonts/Iced-Icons.ttf | Bin 0 -> 5108 bytes wgpu/src/backend.rs | 21 ++++----- wgpu/src/lib.rs | 4 +- wgpu/src/settings.rs | 37 +++------------ wgpu/src/text.rs | 10 ++-- 26 files changed, 145 insertions(+), 254 deletions(-) delete mode 100644 graphics/fonts/Icons.ttf delete mode 100644 graphics/fonts/Lato-Regular.ttf delete mode 100644 graphics/fonts/OFL.txt create mode 100644 wgpu/fonts/Iced-Icons.ttf diff --git a/core/src/font.rs b/core/src/font.rs index 130e378e..1f774a70 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,40 +1,29 @@ -use std::hash::{Hash, Hasher}; +use std::hash::Hash; /// A font. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Font { - /// The default font. - /// - /// This is normally a font configured in a renderer or loaded from the - /// system. - Default, + /// The name of a font family of choice. + Name(&'static str), - /// An external font. - External { - /// The name of the external font - name: &'static str, + /// Serif fonts represent the formal text style for a script. + Serif, - /// The bytes of the external font - bytes: &'static [u8], - }, -} + /// Glyphs in sans-serif fonts, as the term is used in CSS, are generally low + /// contrast and have stroke endings that are plain — without any flaring, + /// cross stroke, or other ornamentation. + SansSerif, -impl Default for Font { - fn default() -> Font { - Font::Default - } -} + /// Glyphs in cursive fonts generally use a more informal script style, and + /// the result looks more like handwritten pen or brush writing than printed + /// letterwork. + Cursive, + + /// Fantasy fonts are primarily decorative or expressive fonts that contain + /// decorative or expressive representations of characters. + Fantasy, -impl Hash for Font { - fn hash(&self, hasher: &mut H) { - match self { - Self::Default => { - 0.hash(hasher); - } - Self::External { name, .. } => { - 1.hash(hasher); - name.hash(hasher); - } - } - } + /// The sole criterion of a monospace font is that all glyphs have the same + /// fixed width. + Monospace, } diff --git a/examples/todos/fonts/icons.ttf b/examples/todos/fonts/icons.ttf index 4498299d..7b65fd36 100644 Binary files a/examples/todos/fonts/icons.ttf and b/examples/todos/fonts/icons.ttf differ diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 6408f09c..5509cf0e 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -466,10 +466,7 @@ fn empty_message(message: &str) -> Element<'_, Message> { } // Fonts -const ICONS: Font = Font::External { - name: "Icons", - bytes: include_bytes!("../../todos/fonts/icons.ttf"), -}; +const ICONS: Font = Font::Name("Iced-Todos-Icons"); fn icon(unicode: char) -> Text<'static> { text(unicode.to_string()) diff --git a/graphics/fonts/Icons.ttf b/graphics/fonts/Icons.ttf deleted file mode 100644 index 5e455b69..00000000 Binary files a/graphics/fonts/Icons.ttf and /dev/null differ diff --git a/graphics/fonts/Lato-Regular.ttf b/graphics/fonts/Lato-Regular.ttf deleted file mode 100644 index 33eba8b1..00000000 Binary files a/graphics/fonts/Lato-Regular.ttf and /dev/null differ diff --git a/graphics/fonts/OFL.txt b/graphics/fonts/OFL.txt deleted file mode 100644 index dfca0da4..00000000 --- a/graphics/fonts/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 256b7ab5..7bbdee95 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -31,6 +31,9 @@ pub trait Text { /// [`ICON_FONT`]: Self::ICON_FONT const ARROW_DOWN_ICON: char; + /// Returns the default [`Font`]. + fn default_font(&self) -> Font; + /// Returns the default size of text. fn default_size(&self) -> f32; diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs index cc8f299c..f6eb2fdd 100644 --- a/graphics/src/layer.rs +++ b/graphics/src/layer.rs @@ -63,7 +63,7 @@ impl<'a> Layer<'a> { ), color: Color::new(0.9, 0.9, 0.9, 1.0), size: 20.0, - font: Font::Default, + font: Font::Monospace, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, }; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 34b6eb1d..7ad53dec 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -130,6 +130,10 @@ where const CHECKMARK_ICON: char = B::CHECKMARK_ICON; const ARROW_DOWN_ICON: char = B::ARROW_DOWN_ICON; + fn default_font(&self) -> Self::Font { + self.backend().default_font() + } + fn default_size(&self) -> f32 { self.backend().default_size() } diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs index 056f8204..8c0b2dfb 100644 --- a/graphics/src/widget/canvas/text.rs +++ b/graphics/src/widget/canvas/text.rs @@ -34,7 +34,7 @@ impl Default for Text { position: Point::ORIGIN, color: Color::BLACK, size: 16.0, - font: Font::Default, + font: Font::SansSerif, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, } diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 50f741ef..9c3a8a44 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -31,7 +31,7 @@ where width: f32, padding: Padding, text_size: Option, - font: Renderer::Font, + font: Option, style: ::Style, } @@ -58,7 +58,7 @@ where width: 0.0, padding: Padding::ZERO, text_size: None, - font: Default::default(), + font: None, style: Default::default(), } } @@ -82,8 +82,8 @@ where } /// Sets the font of the [`Menu`]. - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); self } @@ -311,7 +311,7 @@ where last_selection: &'a mut Option, padding: Padding, text_size: Option, - font: Renderer::Font, + font: Option, style: ::Style, } @@ -491,7 +491,7 @@ where ..bounds }, size: text_size, - font: self.font.clone(), + font: self.font.unwrap_or_else(|| renderer.default_font()), color: if is_selected { appearance.selected_text_color } else { diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 9376d540..50d7d6d6 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -40,12 +40,16 @@ impl Renderer for Null { impl text::Renderer for Null { type Font = Font; - const ICON_FONT: Font = Font::Default; + const ICON_FONT: Font = Font::SansSerif; const CHECKMARK_ICON: char = '0'; const ARROW_DOWN_ICON: char = '0'; + fn default_font(&self) -> Self::Font { + Font::SansSerif + } + fn default_size(&self) -> f32 { - 20.0 + 16.0 } fn measure( diff --git a/native/src/text.rs b/native/src/text.rs index 55c3cfd3..b7915a55 100644 --- a/native/src/text.rs +++ b/native/src/text.rs @@ -57,7 +57,7 @@ impl Hit { /// A renderer capable of measuring and drawing [`Text`]. pub trait Renderer: crate::Renderer { /// The font type used. - type Font: Default + Clone; + type Font: Copy; /// The icon font of the backend. const ICON_FONT: Self::Font; @@ -72,6 +72,9 @@ pub trait Renderer: crate::Renderer { /// [`ICON_FONT`]: Self::ICON_FONT const ARROW_DOWN_ICON: char; + /// Returns the default [`Font`]. + fn default_font(&self) -> Self::Font; + /// Returns the default size of [`Text`]. fn default_size(&self) -> f32; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 9b69e574..138c458c 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -55,7 +55,7 @@ where size: f32, spacing: f32, text_size: Option, - font: Renderer::Font, + font: Option, icon: Icon, style: ::Style, } @@ -91,7 +91,7 @@ where size: Self::DEFAULT_SIZE, spacing: Self::DEFAULT_SPACING, text_size: None, - font: Renderer::Font::default(), + font: None, icon: Icon { font: Renderer::ICON_FONT, code_point: Renderer::CHECKMARK_ICON, @@ -128,8 +128,8 @@ where /// Sets the [`Font`] of the text of the [`Checkbox`]. /// /// [`Font`]: crate::text::Renderer::Font - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); self } @@ -175,7 +175,7 @@ where .push(Row::new().width(self.size).height(self.size)) .push( Text::new(&self.label) - .font(self.font.clone()) + .font(self.font.unwrap_or_else(|| renderer.default_font())) .width(self.width) .size( self.text_size @@ -288,6 +288,7 @@ where { let label_layout = children.next().unwrap(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); widget::text::draw( renderer, @@ -295,7 +296,7 @@ where label_layout, &self.label, self.text_size, - self.font.clone(), + font, widget::text::Appearance { color: custom_style.text_color, }, diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index a128d1ae..c1ff0004 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -35,7 +35,7 @@ where width: Length, padding: Padding, text_size: Option, - font: Renderer::Font, + font: Option, handle: Handle, style: ::Style, } @@ -70,7 +70,7 @@ where width: Length::Shrink, padding: Self::DEFAULT_PADDING, text_size: None, - font: Default::default(), + font: None, handle: Default::default(), style: Default::default(), } @@ -101,8 +101,8 @@ where } /// Sets the font of the [`PickList`]. - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); self } @@ -163,7 +163,7 @@ where self.width, self.padding, self.text_size, - &self.font, + self.font.unwrap_or_else(|| renderer.default_font()), self.placeholder.as_deref(), &self.options, ) @@ -212,6 +212,7 @@ where cursor_position: Point, _viewport: &Rectangle, ) { + let font = self.font.unwrap_or_else(|| renderer.default_font()); draw( renderer, theme, @@ -219,7 +220,7 @@ where cursor_position, self.padding, self.text_size, - &self.font, + font, self.placeholder.as_deref(), self.selected.as_ref(), &self.handle, @@ -232,7 +233,7 @@ where &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, - _renderer: &Renderer, + renderer: &Renderer, ) -> Option> { let state = tree.state.downcast_mut::>(); @@ -241,7 +242,7 @@ where state, self.padding, self.text_size, - self.font.clone(), + self.font.unwrap_or_else(|| renderer.default_font()), &self.options, self.style.clone(), ) @@ -343,7 +344,7 @@ pub fn layout( width: Length, padding: Padding, text_size: Option, - font: &Renderer::Font, + font: Renderer::Font, placeholder: Option<&str>, options: &[T], ) -> layout::Node @@ -362,7 +363,7 @@ where let (width, _) = renderer.measure( label, text_size, - font.clone(), + font, Size::new(f32::INFINITY, f32::INFINITY), ); @@ -560,7 +561,7 @@ pub fn draw<'a, T, Renderer>( cursor_position: Point, padding: Padding, text_size: Option, - font: &Renderer::Font, + font: Renderer::Font, placeholder: Option<&str>, selected: Option<&T>, handle: &Handle, @@ -637,7 +638,7 @@ pub fn draw<'a, T, Renderer>( renderer.fill_text(Text { content: label, size: text_size, - font: font.clone(), + font, color: if is_selected { style.text_color } else { diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index 9daddfbc..bd803910 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -53,7 +53,7 @@ where size: f32, spacing: f32, text_size: Option, - font: Renderer::Font, + font: Option, style: ::Style, } @@ -95,7 +95,7 @@ where size: Self::DEFAULT_SIZE, spacing: Self::DEFAULT_SPACING, //15 text_size: None, - font: Default::default(), + font: None, style: Default::default(), } } @@ -125,8 +125,8 @@ where } /// Sets the text font of the [`Radio`] button. - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); self } @@ -268,6 +268,7 @@ where { let label_layout = children.next().unwrap(); + let font = self.font.unwrap_or(renderer.default_font()); widget::text::draw( renderer, @@ -275,7 +276,7 @@ where label_layout, &self.label, self.text_size, - self.font.clone(), + font, widget::text::Appearance { color: custom_style.text_color, }, diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 3fee48f2..235a027e 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -37,7 +37,7 @@ where height: Length, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - font: Renderer::Font, + font: Option, style: ::Style, } @@ -51,7 +51,7 @@ where Text { content: content.into(), size: None, - font: Default::default(), + font: None, width: Length::Shrink, height: Length::Shrink, horizontal_alignment: alignment::Horizontal::Left, @@ -70,7 +70,7 @@ where /// /// [`Font`]: crate::text::Renderer::Font pub fn font(mut self, font: impl Into) -> Self { - self.font = font.into(); + self.font = Some(font.into()); self } @@ -138,8 +138,12 @@ where let bounds = limits.max(); - let (width, height) = - renderer.measure(&self.content, size, self.font.clone(), bounds); + let (width, height) = renderer.measure( + &self.content, + size, + self.font.unwrap_or_else(|| renderer.default_font()), + bounds, + ); let size = limits.resolve(Size::new(width, height)); @@ -156,13 +160,15 @@ where _cursor_position: Point, _viewport: &Rectangle, ) { + let font = self.font.unwrap_or_else(|| renderer.default_font()); + draw( renderer, style, layout, &self.content, self.size, - self.font.clone(), + font, theme.appearance(self.style), self.horizontal_alignment, self.vertical_alignment, @@ -242,7 +248,7 @@ where height: self.height, horizontal_alignment: self.horizontal_alignment, vertical_alignment: self.vertical_alignment, - font: self.font.clone(), + font: self.font, style: self.style, } } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 5e198b8f..e6b70db2 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -61,7 +61,7 @@ where placeholder: String, value: Value, is_secure: bool, - font: Renderer::Font, + font: Option, width: Length, padding: Padding, size: Option, @@ -92,7 +92,7 @@ where placeholder: String::from(placeholder), value: Value::new(value), is_secure: false, - font: Default::default(), + font: None, width: Length::Fill, padding: Padding::new(5.0), size: None, @@ -129,7 +129,7 @@ where /// /// [`Font`]: text::Renderer::Font pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; + self.font = Some(font); self } /// Sets the width of the [`TextInput`]. @@ -179,6 +179,8 @@ where cursor_position: Point, value: Option<&Value>, ) { + let font = self.font.unwrap_or(renderer.default_font()); + draw( renderer, theme, @@ -188,7 +190,7 @@ where value.unwrap_or(&self.value), &self.placeholder, self.size, - &self.font, + font, self.is_secure, &self.style, ) @@ -258,7 +260,7 @@ where shell, &mut self.value, self.size, - &self.font, + self.font.unwrap_or(renderer.default_font()), self.is_secure, self.on_change.as_ref(), self.on_paste.as_deref(), @@ -277,6 +279,8 @@ where cursor_position: Point, _viewport: &Rectangle, ) { + let font = self.font.unwrap_or(renderer.default_font()); + draw( renderer, theme, @@ -286,7 +290,7 @@ where &self.value, &self.placeholder, self.size, - &self.font, + font, self.is_secure, &self.style, ) @@ -410,7 +414,7 @@ pub fn update<'a, Message, Renderer>( shell: &mut Shell<'_, Message>, value: &mut Value, size: Option, - font: &Renderer::Font, + font: Renderer::Font, is_secure: bool, on_change: &dyn Fn(String) -> Message, on_paste: Option<&dyn Fn(String) -> Message>, @@ -459,7 +463,7 @@ where find_cursor_position( renderer, text_layout.bounds(), - font.clone(), + font, size, &value, state, @@ -487,7 +491,7 @@ where let position = find_cursor_position( renderer, text_layout.bounds(), - font.clone(), + font, size, value, state, @@ -536,7 +540,7 @@ where let position = find_cursor_position( renderer, text_layout.bounds(), - font.clone(), + font, size, &value, state, @@ -816,7 +820,7 @@ pub fn draw( value: &Value, placeholder: &str, size: Option, - font: &Renderer::Font, + font: Renderer::Font, is_secure: bool, style: &::Style, ) where @@ -862,7 +866,7 @@ pub fn draw( value, size, position, - font.clone(), + font, ); let is_cursor_visible = ((focus.now - focus.updated_at) @@ -903,7 +907,7 @@ pub fn draw( value, size, left, - font.clone(), + font, ); let (right_position, right_offset) = @@ -913,7 +917,7 @@ pub fn draw( value, size, right, - font.clone(), + font, ); let width = right_position - left_position; @@ -948,7 +952,7 @@ pub fn draw( let text_width = renderer.measure_width( if text.is_empty() { placeholder } else { &text }, size, - font.clone(), + font, ); let render = |renderer: &mut Renderer| { @@ -963,7 +967,7 @@ pub fn draw( } else { theme.value_color(style) }, - font: font.clone(), + font: font, bounds: Rectangle { y: text_bounds.center_y(), width: f32::INFINITY, @@ -1195,8 +1199,7 @@ where { let size = size.unwrap_or_else(|| renderer.default_size()); - let offset = - offset(renderer, text_bounds, font.clone(), size, value, state); + let offset = offset(renderer, text_bounds, font, size, value, state); renderer .hit_test( diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index a434af65..495406db 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -42,7 +42,7 @@ where text_size: Option, text_alignment: alignment::Horizontal, spacing: f32, - font: Renderer::Font, + font: Option, style: ::Style, } @@ -79,7 +79,7 @@ where text_size: None, text_alignment: alignment::Horizontal::Left, spacing: 0.0, - font: Renderer::Font::default(), + font: None, style: Default::default(), } } @@ -117,8 +117,8 @@ where /// Sets the [`Font`] of the text of the [`Toggler`] /// /// [`Font`]: crate::text::Renderer::Font - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = font; + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); self } @@ -160,7 +160,7 @@ where row = row.push( Text::new(label) .horizontal_alignment(self.text_alignment) - .font(self.font.clone()) + .font(self.font.unwrap_or_else(|| renderer.default_font())) .width(self.width) .size( self.text_size @@ -236,6 +236,7 @@ where if let Some(label) = &self.label { let label_layout = children.next().unwrap(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); crate::widget::text::draw( renderer, @@ -243,7 +244,7 @@ where label_layout, label, self.text_size, - self.font.clone(), + font, Default::default(), self.text_alignment, alignment::Vertical::Center, diff --git a/src/application.rs b/src/application.rs index 1db5c93f..9a1c1855 100644 --- a/src/application.rs +++ b/src/application.rs @@ -197,7 +197,6 @@ pub trait Application: Sized { let renderer_settings = crate::renderer::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, - text_multithreading: settings.text_multithreading, antialiasing: if settings.antialiasing { Some(crate::renderer::settings::Antialiasing::MSAAx4) } else { diff --git a/src/settings.rs b/src/settings.rs index 266ad404..ce07b0dd 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,6 @@ //! Configure your application. use crate::window; +use crate::Font; /// The settings of an application. #[derive(Debug, Clone)] @@ -20,23 +21,16 @@ pub struct Settings { /// [`Application`]: crate::Application pub flags: Flags, - /// The bytes of the font that will be used by default. + /// The default [`Font`] to be used. /// - /// If `None` is provided, a default system font will be chosen. - // TODO: Add `name` for web compatibility - pub default_font: Option<&'static [u8]>, + /// By default, it uses [`Font::SansSerif`]. + pub default_font: Font, /// The text size that will be used by default. /// /// The default value is `16.0`. pub default_text_size: f32, - /// If enabled, spread text workload in multiple threads when multiple cores - /// are available. - /// - /// By default, it is disabled. - pub text_multithreading: bool, - /// If set to true, the renderer will try to perform antialiasing for some /// primitives. /// @@ -79,7 +73,6 @@ impl Settings { window: default_settings.window, default_font: default_settings.default_font, default_text_size: default_settings.default_text_size, - text_multithreading: default_settings.text_multithreading, antialiasing: default_settings.antialiasing, exit_on_close_request: default_settings.exit_on_close_request, try_opengles_first: default_settings.try_opengles_first, @@ -96,9 +89,8 @@ where id: None, window: Default::default(), flags: Default::default(), - default_font: Default::default(), + default_font: Font::SansSerif, default_text_size: 16.0, - text_multithreading: false, antialiasing: false, exit_on_close_request: true, try_opengles_first: false, diff --git a/wgpu/fonts/Iced-Icons.ttf b/wgpu/fonts/Iced-Icons.ttf new file mode 100644 index 00000000..7112f086 Binary files /dev/null and b/wgpu/fonts/Iced-Icons.ttf differ diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 9bf464b3..874edb96 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -27,6 +27,7 @@ pub struct Backend { #[cfg(any(feature = "image", feature = "svg"))] image_pipeline: image::Pipeline, + default_font: Font, default_text_size: f32, } @@ -38,14 +39,7 @@ impl Backend { settings: Settings, format: wgpu::TextureFormat, ) -> Self { - let text_pipeline = text::Pipeline::new( - device, - queue, - format, - settings.default_font, - settings.text_multithreading, - ); - + 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); @@ -61,6 +55,7 @@ impl Backend { #[cfg(any(feature = "image", feature = "svg"))] image_pipeline, + default_font: settings.default_font, default_text_size: settings.default_text_size, } } @@ -199,9 +194,13 @@ impl iced_graphics::Backend for Backend { } impl backend::Text for Backend { - const ICON_FONT: Font = Font::Default; // TODO - const CHECKMARK_ICON: char = '✓'; - const ARROW_DOWN_ICON: char = '▼'; + const ICON_FONT: Font = Font::Name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{e800}'; + const ARROW_DOWN_ICON: char = '\u{f00c}'; + + fn default_font(&self) -> Font { + self.default_font + } fn default_size(&self) -> f32 { self.default_text_size diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 1a293681..6d6d3fd6 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -47,7 +47,9 @@ mod quad; mod text; mod triangle; -pub use iced_graphics::{Antialiasing, Color, Error, Primitive, Viewport}; +pub use iced_graphics::{ + Antialiasing, Color, Error, Font, Primitive, Viewport, +}; pub use iced_native::Theme; pub use wgpu; diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs index 5ef79499..bd9cf473 100644 --- a/wgpu/src/settings.rs +++ b/wgpu/src/settings.rs @@ -1,12 +1,12 @@ //! Configure a renderer. -use std::fmt; - pub use crate::Antialiasing; +use crate::Font; + /// The settings of a [`Backend`]. /// /// [`Backend`]: crate::Backend -#[derive(Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Settings { /// The present mode of the [`Backend`]. /// @@ -16,42 +16,20 @@ pub struct Settings { /// The internal graphics backend to use. pub internal_backend: wgpu::Backends, - /// The bytes of the font that will be used by default. - /// - /// If `None` is provided, a default system font will be chosen. - pub default_font: Option<&'static [u8]>, + /// The default [`Font`] to use. + pub default_font: Font, /// The default size of text. /// /// By default, it will be set to `16.0`. pub default_text_size: f32, - /// If enabled, spread text workload in multiple threads when multiple cores - /// are available. - /// - /// By default, it is disabled. - pub text_multithreading: bool, - /// The antialiasing strategy that will be used for triangle primitives. /// /// By default, it is `None`. pub antialiasing: Option, } -impl fmt::Debug for Settings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Settings") - .field("present_mode", &self.present_mode) - .field("internal_backend", &self.internal_backend) - // Instead of printing the font bytes, we simply show a `bool` indicating if using a default font or not. - .field("default_font", &self.default_font.is_some()) - .field("default_text_size", &self.default_text_size) - .field("text_multithreading", &self.text_multithreading) - .field("antialiasing", &self.antialiasing) - .finish() - } -} - impl Settings { /// Creates new [`Settings`] using environment configuration. /// @@ -81,9 +59,8 @@ impl Default for Settings { Settings { present_mode: wgpu::PresentMode::AutoVsync, internal_backend: wgpu::Backends::all(), - default_font: None, - default_text_size: 20.0, - text_multithreading: false, + default_font: Font::SansSerif, + default_text_size: 16.0, antialiasing: None, } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index fa7fd5df..083d9d2b 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -112,8 +112,6 @@ impl Pipeline { device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat, - _default_font: Option<&[u8]>, - _multithreading: bool, ) -> Self { Pipeline { renderers: Vec::new(), @@ -316,7 +314,11 @@ impl Pipeline { fn to_family(font: Font) -> glyphon::Family<'static> { match font { - Font::Default => glyphon::Family::SansSerif, - Font::External { name, .. } => glyphon::Family::Name(name), + Font::Name(name) => glyphon::Family::Name(name), + Font::SansSerif => glyphon::Family::SansSerif, + Font::Serif => glyphon::Family::Serif, + Font::Cursive => glyphon::Family::Cursive, + Font::Fantasy => glyphon::Family::Fantasy, + Font::Monospace => glyphon::Family::Monospace, } } -- cgit From 238154af4ac8dda7f12dd90aa7be106e933bcb30 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 11:12:15 +0100 Subject: Implement `font::load` command in `iced_native` --- examples/todos/src/main.rs | 15 +- graphics/src/backend.rs | 5 + graphics/src/renderer.rs | 5 + native/src/command/action.rs | 16 ++ native/src/font.rs | 19 ++ native/src/lib.rs | 6 +- native/src/program.rs | 3 +- native/src/renderer/null.rs | 4 + native/src/text.rs | 7 +- src/lib.rs | 6 +- wgpu/Cargo.toml | 1 + wgpu/src/backend.rs | 6 + wgpu/src/text.rs | 470 ++++++++++++++++++++++++------------------- winit/src/application.rs | 10 + 14 files changed, 358 insertions(+), 215 deletions(-) create mode 100644 native/src/font.rs diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 5509cf0e..5df4e968 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,5 +1,6 @@ use iced::alignment::{self, Alignment}; use iced::event::{self, Event}; +use iced::font::{self, Font}; use iced::keyboard; use iced::subscription; use iced::theme::{self, Theme}; @@ -9,7 +10,7 @@ use iced::widget::{ }; use iced::window; use iced::{Application, Element}; -use iced::{Color, Command, Font, Length, Settings, Subscription}; +use iced::{Color, Command, Length, Settings, Subscription}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -44,6 +45,7 @@ struct State { #[derive(Debug, Clone)] enum Message { Loaded(Result), + FontLoaded(Result<(), font::Error>), Saved(Result<(), SaveError>), InputChanged(String), CreateTask, @@ -61,7 +63,11 @@ impl Application for Todos { fn new(_flags: ()) -> (Todos, Command) { ( Todos::Loading, - Command::perform(SavedState::load(), Message::Loaded), + Command::batch(vec![ + font::load(include_bytes!("../fonts/icons.ttf").as_slice()) + .map(Message::FontLoaded), + Command::perform(SavedState::load(), Message::Loaded), + ]), ) } @@ -384,7 +390,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { let tasks_left = tasks.iter().filter(|task| !task.completed).count(); let filter_button = |label, filter, current_filter| { - let label = text(label).size(16); + let label = text(label); let button = button(label).style(if filter == current_filter { theme::Button::Primary @@ -401,8 +407,7 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element { tasks_left, if tasks_left == 1 { "task" } else { "tasks" } )) - .width(Length::Fill) - .size(16), + .width(Length::Fill), row![ filter_button("All", Filter::All, current_filter), filter_button("Active", Filter::Active, current_filter), diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 7bbdee95..8658cffe 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -4,6 +4,8 @@ use iced_native::svg; use iced_native::text; use iced_native::{Font, Point, Size}; +use std::borrow::Cow; + /// The graphics backend of a [`Renderer`]. /// /// [`Renderer`]: crate::Renderer @@ -64,6 +66,9 @@ pub trait Text { point: Point, nearest_only: bool, ) -> Option; + + /// Loads a [`Font`] from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>); } /// A graphics backend that supports image rendering. diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 7ad53dec..b052c094 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -10,6 +10,7 @@ use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; pub use iced_native::renderer::Style; +use std::borrow::Cow; use std::marker::PhantomData; /// A backend-agnostic renderer that supports all the built-in widgets. @@ -167,6 +168,10 @@ where ) } + fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + self.backend.load_font(bytes); + } + fn fill_text(&mut self, text: Text<'_, Self::Font>) { self.primitives.push(Primitive::Text { content: text.content.to_string(), diff --git a/native/src/command/action.rs b/native/src/command/action.rs index a51b8c21..d1589c05 100644 --- a/native/src/command/action.rs +++ b/native/src/command/action.rs @@ -1,10 +1,12 @@ use crate::clipboard; +use crate::font; use crate::system; use crate::widget; use crate::window; use iced_futures::MaybeSend; +use std::borrow::Cow; use std::fmt; /// An action that a [`Command`] can perform. @@ -27,6 +29,15 @@ pub enum Action { /// Run a widget action. Widget(widget::Action), + + /// Load a font from its bytes. + LoadFont { + /// The bytes of the font to load. + bytes: Cow<'static, [u8]>, + + /// The message to produce when the font has been loaded. + tagger: Box) -> T>, + }, } impl Action { @@ -49,6 +60,10 @@ impl Action { Self::Window(window) => Action::Window(window.map(f)), Self::System(system) => Action::System(system.map(f)), Self::Widget(widget) => Action::Widget(widget.map(f)), + Self::LoadFont { bytes, tagger } => Action::LoadFont { + bytes, + tagger: Box::new(move |result| f(tagger(result))), + }, } } } @@ -63,6 +78,7 @@ impl fmt::Debug for Action { Self::Window(action) => write!(f, "Action::Window({action:?})"), Self::System(action) => write!(f, "Action::System({action:?})"), Self::Widget(_action) => write!(f, "Action::Widget"), + Self::LoadFont { .. } => write!(f, "Action::LoadFont"), } } } diff --git a/native/src/font.rs b/native/src/font.rs new file mode 100644 index 00000000..6840a25f --- /dev/null +++ b/native/src/font.rs @@ -0,0 +1,19 @@ +//! Load and use fonts. +pub use iced_core::Font; + +use crate::command::{self, Command}; +use std::borrow::Cow; + +/// An error while loading a font. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error {} + +/// Load a font from its bytes. +pub fn load( + bytes: impl Into>, +) -> Command> { + Command::single(command::Action::LoadFont { + bytes: bytes.into(), + tagger: Box::new(std::convert::identity), + }) +} diff --git a/native/src/lib.rs b/native/src/lib.rs index ebdc8490..27b6fc0d 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -47,6 +47,7 @@ pub mod clipboard; pub mod command; pub mod event; +pub mod font; pub mod image; pub mod keyboard; pub mod layout; @@ -80,8 +81,8 @@ mod debug; pub use iced_core::alignment; pub use iced_core::time; pub use iced_core::{ - color, Alignment, Background, Color, ContentFit, Font, Length, Padding, - Pixels, Point, Rectangle, Size, Vector, + color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, + Point, Rectangle, Size, Vector, }; pub use iced_futures::{executor, futures}; pub use iced_style::application; @@ -95,6 +96,7 @@ pub use command::Command; pub use debug::Debug; pub use element::Element; pub use event::Event; +pub use font::Font; pub use hasher::Hasher; pub use layout::Layout; pub use overlay::Overlay; diff --git a/native/src/program.rs b/native/src/program.rs index c71c237f..25cab332 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -1,4 +1,5 @@ //! Build interactive programs using The Elm Architecture. +use crate::text; use crate::{Command, Element, Renderer}; mod state; @@ -8,7 +9,7 @@ pub use state::State; /// The core of a user interface application following The Elm Architecture. pub trait Program: Sized { /// The graphics backend to use to draw the [`Program`]. - type Renderer: Renderer; + type Renderer: Renderer + text::Renderer; /// The type of __messages__ your [`Program`] will produce. type Message: std::fmt::Debug + Send; diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 50d7d6d6..150ee786 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -2,6 +2,8 @@ use crate::renderer::{self, Renderer}; use crate::text::{self, Text}; use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector}; +use std::borrow::Cow; + /// A renderer that does nothing. /// /// It can be useful if you are writing tests! @@ -52,6 +54,8 @@ impl text::Renderer for Null { 16.0 } + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + fn measure( &self, _content: &str, diff --git a/native/src/text.rs b/native/src/text.rs index b7915a55..1bbd36cc 100644 --- a/native/src/text.rs +++ b/native/src/text.rs @@ -2,6 +2,8 @@ use crate::alignment; use crate::{Color, Point, Rectangle, Size, Vector}; +use std::borrow::Cow; + /// A paragraph. #[derive(Debug, Clone, Copy)] pub struct Text<'a, Font> { @@ -72,7 +74,7 @@ pub trait Renderer: crate::Renderer { /// [`ICON_FONT`]: Self::ICON_FONT const ARROW_DOWN_ICON: char; - /// Returns the default [`Font`]. + /// Returns the default [`Self::Font`]. fn default_font(&self) -> Self::Font; /// Returns the default size of [`Text`]. @@ -112,6 +114,9 @@ pub trait Renderer: crate::Renderer { nearest_only: bool, ) -> Option; + /// Loads a [`Self::Font`] from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>); + /// Draws the given [`Text`]. fn fill_text(&mut self, text: Text<'_, Self::Font>); } diff --git a/src/lib.rs b/src/lib.rs index 318852f9..17c5ab97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,6 +196,7 @@ use iced_glow as renderer; pub use iced_native::theme; pub use runtime::event; +pub use runtime::font; pub use runtime::subscription; pub use application::Application; @@ -203,6 +204,7 @@ pub use element::Element; pub use error::Error; pub use event::Event; pub use executor::Executor; +pub use font::Font; pub use renderer::Renderer; pub use result::Result; pub use sandbox::Sandbox; @@ -213,8 +215,8 @@ pub use theme::Theme; pub use runtime::alignment; pub use runtime::futures; pub use runtime::{ - color, Alignment, Background, Color, Command, ContentFit, Font, Length, - Padding, Point, Rectangle, Size, Vector, + color, Alignment, Background, Color, Command, ContentFit, Length, Padding, + Point, Rectangle, Size, Vector, }; #[cfg(feature = "system")] diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 1a94c6a3..dffbbab0 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -36,6 +36,7 @@ bitflags = "1.2" once_cell = "1.0" rustc-hash = "1.1" twox-hash = "1.6" +ouroboros = "0.15" [dependencies.bytemuck] version = "1.9" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 874edb96..5a275c8a 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -14,6 +14,8 @@ 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 @@ -234,6 +236,10 @@ impl backend::Text for Backend { nearest_only, ) } + + fn load_font(&mut self, font: Cow<'static, [u8]>) { + self.text_pipeline.load_font(font); + } } #[cfg(feature = "image")] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 083d9d2b..cdfcd576 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -5,108 +5,37 @@ use iced_native::alignment; use iced_native::{Color, Font, Rectangle, Size}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::borrow::Cow; use std::cell::RefCell; use std::hash::{BuildHasher, Hash, Hasher}; +use std::sync::Arc; use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { + system: Option, renderers: Vec, atlas: glyphon::TextAtlas, - cache: glyphon::SwashCache<'static>, - measurement_cache: RefCell, - render_cache: Cache, layer: usize, } -struct Cache { - entries: FxHashMap>, - recently_used: FxHashSet, - hasher: RandomXxHashBuilder64, -} - -impl Cache { - fn new() -> Self { - Self { - entries: FxHashMap::default(), - recently_used: FxHashSet::default(), - hasher: RandomXxHashBuilder64::default(), - } - } - - fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> { - self.entries.get(key) - } - - fn allocate( - &mut self, - key: Key<'_>, - ) -> (KeyHash, &mut glyphon::Buffer<'static>) { - let hash = { - let mut hasher = self.hasher.build_hasher(); - - key.content.hash(&mut hasher); - (key.size as i32).hash(&mut hasher); - key.font.hash(&mut hasher); - (key.bounds.width as i32).hash(&mut hasher); - (key.bounds.height as i32).hash(&mut hasher); - key.color.into_rgba8().hash(&mut hasher); - - hasher.finish() - }; +#[ouroboros::self_referencing] +struct System { + fonts: glyphon::FontSystem, - if !self.entries.contains_key(&hash) { - let metrics = - glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + #[borrows(fonts)] + #[not_covariant] + cache: glyphon::SwashCache<'this>, - let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + #[borrows(fonts)] + #[not_covariant] + measurement_cache: RefCell>, - buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); - buffer.set_text( - key.content, - glyphon::Attrs::new().family(to_family(key.font)).color({ - let [r, g, b, a] = key.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }), - ); - - let _ = self.entries.insert(hash, buffer); - } - - let _ = self.recently_used.insert(hash); - - (hash, self.entries.get_mut(&hash).unwrap()) - } - - fn trim(&mut self) { - self.entries - .retain(|key, _| self.recently_used.contains(key)); - - self.recently_used.clear(); - } + #[borrows(fonts)] + #[not_covariant] + render_cache: Cache<'this>, } -#[derive(Debug, Clone, Copy)] -struct Key<'a> { - content: &'a str, - size: f32, - font: Font, - bounds: Size, - color: Color, -} - -type KeyHash = u64; - -// TODO: Share with `iced_graphics` -static FONT_SYSTEM: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(glyphon::FontSystem::new); - impl Pipeline { pub fn new( device: &wgpu::Device, @@ -114,15 +43,41 @@ impl Pipeline { format: wgpu::TextureFormat, ) -> Self { Pipeline { + system: Some( + SystemBuilder { + fonts: glyphon::FontSystem::new(), + cache_builder: |fonts| glyphon::SwashCache::new(fonts), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ), renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), - cache: glyphon::SwashCache::new(&FONT_SYSTEM), - measurement_cache: RefCell::new(Cache::new()), - render_cache: Cache::new(), layer: 0, } } + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let heads = self.system.take().unwrap().into_heads(); + + let (locale, mut db) = heads.fonts.into_locale_and_db(); + + db.load_font_source(glyphon::fontdb::Source::Binary(Arc::new( + bytes.to_owned(), + ))); + + self.system = Some( + SystemBuilder { + fonts: glyphon::FontSystem::new_with_locale_and_db(locale, db), + cache_builder: |fonts| glyphon::SwashCache::new(fonts), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ); + } + pub fn prepare( &mut self, device: &wgpu::Device, @@ -132,93 +87,100 @@ impl Pipeline { scale_factor: f32, target_size: Size, ) { - if self.renderers.len() <= self.layer { - self.renderers - .push(glyphon::TextRenderer::new(device, queue)); - } - - let renderer = &mut self.renderers[self.layer]; - - let keys: Vec<_> = sections - .iter() - .map(|section| { - let (key, _) = self.render_cache.allocate(Key { - content: section.content, - size: section.size * scale_factor, - font: section.font, - bounds: Size { - width: section.bounds.width * scale_factor, - height: section.bounds.height * scale_factor, + self.system.as_mut().unwrap().with_mut(|fields| { + if self.renderers.len() <= self.layer { + self.renderers + .push(glyphon::TextRenderer::new(device, queue)); + } + + let renderer = &mut self.renderers[self.layer]; + + let keys: Vec<_> = sections + .iter() + .map(|section| { + let (key, _) = fields.render_cache.allocate( + fields.fonts, + Key { + content: section.content, + size: section.size * scale_factor, + font: section.font, + bounds: Size { + width: section.bounds.width * scale_factor, + height: section.bounds.height * scale_factor, + }, + color: section.color, + }, + ); + + key + }) + .collect(); + + let bounds = glyphon::TextBounds { + left: (bounds.x * scale_factor) as i32, + top: (bounds.y * scale_factor) as i32, + right: ((bounds.x + bounds.width) * scale_factor) as i32, + bottom: ((bounds.y + bounds.height) * scale_factor) as i32, + }; + + let text_areas: Vec<_> = sections + .iter() + .zip(keys.iter()) + .map(|(section, key)| { + let buffer = fields + .render_cache + .get(key) + .expect("Get cached buffer"); + + let x = section.bounds.x * scale_factor; + let y = section.bounds.y * scale_factor; + + let (total_lines, max_width) = buffer + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + let total_height = + total_lines as f32 * section.size * 1.2 * scale_factor; + + let left = match section.horizontal_alignment { + alignment::Horizontal::Left => x, + alignment::Horizontal::Center => x - max_width / 2.0, + alignment::Horizontal::Right => x - max_width, + }; + + let top = match section.vertical_alignment { + alignment::Vertical::Top => y, + alignment::Vertical::Center => y - total_height / 2.0, + alignment::Vertical::Bottom => y - total_height, + }; + + glyphon::TextArea { + buffer, + left: left as i32, + top: top as i32, + bounds, + } + }) + .collect(); + + renderer + .prepare( + device, + queue, + &mut self.atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, }, - color: section.color, - }); - - key - }) - .collect(); - - let bounds = glyphon::TextBounds { - left: (bounds.x * scale_factor) as i32, - top: (bounds.y * scale_factor) as i32, - right: ((bounds.x + bounds.width) * scale_factor) as i32, - bottom: ((bounds.y + bounds.height) * scale_factor) as i32, - }; - - let text_areas: Vec<_> = sections - .iter() - .zip(keys.iter()) - .map(|(section, key)| { - let buffer = - self.render_cache.get(key).expect("Get cached buffer"); - - let x = section.bounds.x * scale_factor; - let y = section.bounds.y * scale_factor; - - let (total_lines, max_width) = buffer - .layout_runs() - .enumerate() - .fold((0, 0.0), |(_, max), (i, buffer)| { - (i + 1, buffer.line_w.max(max)) - }); - - let total_height = - total_lines as f32 * section.size * 1.2 * scale_factor; - - let left = match section.horizontal_alignment { - alignment::Horizontal::Left => x, - alignment::Horizontal::Center => x - max_width / 2.0, - alignment::Horizontal::Right => x - max_width, - }; - - let top = match section.vertical_alignment { - alignment::Vertical::Top => y, - alignment::Vertical::Center => y - total_height / 2.0, - alignment::Vertical::Bottom => y - total_height, - }; - - glyphon::TextArea { - buffer, - left: left as i32, - top: top as i32, - bounds, - } - }) - .collect(); - - renderer - .prepare( - device, - queue, - &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - &text_areas, - glyphon::Color::rgb(0, 0, 0), - &mut self.cache, - ) - .expect("Prepare text sections"); + &text_areas, + glyphon::Color::rgb(0, 0, 0), + fields.cache, + ) + .expect("Prepare text sections"); + }); } pub fn render( @@ -251,7 +213,10 @@ impl Pipeline { pub fn end_frame(&mut self) { self.renderers.truncate(self.layer); - self.render_cache.trim(); + self.system + .as_mut() + .unwrap() + .with_render_cache_mut(|cache| cache.trim()); self.layer = 0; } @@ -263,24 +228,29 @@ impl Pipeline { font: Font, bounds: Size, ) -> (f32, f32) { - let mut measurement_cache = self.measurement_cache.borrow_mut(); - - let (_, paragraph) = measurement_cache.allocate(Key { - content, - size: size, - font, - bounds, - color: Color::BLACK, - }); + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size: size, + font, + bounds, + color: Color::BLACK, + }, + ); - let (total_lines, max_width) = paragraph - .layout_runs() - .enumerate() - .fold((0, 0.0), |(_, max), (i, buffer)| { - (i + 1, buffer.line_w.max(max)) - }); + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - (max_width, size * 1.2 * total_lines as f32) + (max_width, size * 1.2 * total_lines as f32) + }) } pub fn hit_test( @@ -292,23 +262,31 @@ impl Pipeline { point: iced_native::Point, _nearest_only: bool, ) -> Option { - let mut measurement_cache = self.measurement_cache.borrow_mut(); - - let (_, paragraph) = measurement_cache.allocate(Key { - content, - size: size, - font, - bounds, - color: Color::BLACK, - }); + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size: size, + font, + bounds, + color: Color::BLACK, + }, + ); - let cursor = paragraph.hit(point.x as i32, point.y as i32)?; + let cursor = paragraph.hit(point.x as i32, point.y as i32)?; - Some(Hit::CharOffset(cursor.index)) + Some(Hit::CharOffset(cursor.index)) + }) } pub fn trim_measurement_cache(&mut self) { - self.measurement_cache.borrow_mut().trim(); + self.system + .as_mut() + .unwrap() + .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); } } @@ -322,3 +300,87 @@ fn to_family(font: Font) -> glyphon::Family<'static> { Font::Monospace => glyphon::Family::Monospace, } } + +struct Cache<'a> { + entries: FxHashMap>, + recently_used: FxHashSet, + hasher: RandomXxHashBuilder64, +} + +impl<'a> Cache<'a> { + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: RandomXxHashBuilder64::default(), + } + } + + fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'a>> { + self.entries.get(key) + } + + fn allocate( + &mut self, + fonts: &'a glyphon::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut glyphon::Buffer<'a>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + (key.size as i32).hash(&mut hasher); + key.font.hash(&mut hasher); + (key.bounds.width as i32).hash(&mut hasher); + (key.bounds.height as i32).hash(&mut hasher); + key.color.into_rgba8().hash(&mut hasher); + + hasher.finish() + }; + + if !self.entries.contains_key(&hash) { + let metrics = + glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + let mut buffer = glyphon::Buffer::new(&fonts, metrics); + + buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_text( + key.content, + glyphon::Attrs::new().family(to_family(key.font)).color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }), + ); + + let _ = self.entries.insert(hash, buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, + color: Color, +} + +type KeyHash = u64; diff --git a/winit/src/application.rs b/winit/src/application.rs index 9781a453..889becad 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -851,6 +851,16 @@ pub fn run_command( current_cache = user_interface.into_cache(); *cache = current_cache; } + command::Action::LoadFont { bytes, tagger } => { + use crate::text::Renderer; + + // TODO: Error handling (?) + renderer.load_font(bytes); + + proxy + .send_event(tagger(Ok(()))) + .expect("Send message to event loop"); + } } } } -- cgit From 5a82fc654e2933c4c93dac5393685861feb07b1f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 11:21:35 +0100 Subject: Use floating coordinates directly in `text::Pipeline` --- wgpu/Cargo.toml | 3 ++- wgpu/src/text.rs | 13 ++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index dffbbab0..280ce29b 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -52,7 +52,8 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" -path = "../../glyphon" +git = "https://github.com/hecrj/glyphon.git" +rev = "bffca9b958af11d1bfd0c0d1a281fc8799cc5a5b" [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index cdfcd576..2ca3f0d8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -276,7 +276,7 @@ impl Pipeline { }, ); - let cursor = paragraph.hit(point.x as i32, point.y as i32)?; + let cursor = paragraph.hit(point.x, point.y)?; Some(Hit::CharOffset(cursor.index)) }) @@ -329,21 +329,20 @@ impl<'a> Cache<'a> { let mut hasher = self.hasher.build_hasher(); key.content.hash(&mut hasher); - (key.size as i32).hash(&mut hasher); + key.size.to_bits().hash(&mut hasher); key.font.hash(&mut hasher); - (key.bounds.width as i32).hash(&mut hasher); - (key.bounds.height as i32).hash(&mut hasher); + key.bounds.width.to_bits().hash(&mut hasher); + key.bounds.height.to_bits().hash(&mut hasher); key.color.into_rgba8().hash(&mut hasher); hasher.finish() }; if !self.entries.contains_key(&hash) { - let metrics = - glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); let mut buffer = glyphon::Buffer::new(&fonts, metrics); - buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_size(key.bounds.width, key.bounds.height); buffer.set_text( key.content, glyphon::Attrs::new().family(to_family(key.font)).color({ -- cgit From d2825360a75600bb6b4097737c987e2d9e05da6a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 11:35:12 +0100 Subject: Load `Iced-Icons.ttf` font in `text::Pipeline::new` --- wgpu/Cargo.toml | 2 +- wgpu/src/backend.rs | 4 ++-- wgpu/src/text.rs | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 280ce29b..1ab3d93f 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -53,7 +53,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "bffca9b958af11d1bfd0c0d1a281fc8799cc5a5b" +rev = "4b5e5106f05332dc324bae2095468845a61f36b9" [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 5a275c8a..f19959bd 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -197,8 +197,8 @@ impl iced_graphics::Backend for Backend { impl backend::Text for Backend { const ICON_FONT: Font = Font::Name("Iced-Icons"); - const CHECKMARK_ICON: char = '\u{e800}'; - const ARROW_DOWN_ICON: char = '\u{f00c}'; + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; fn default_font(&self) -> Font { self.default_font diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2ca3f0d8..4af57af3 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -45,7 +45,13 @@ impl Pipeline { Pipeline { system: Some( SystemBuilder { - fonts: glyphon::FontSystem::new(), + fonts: glyphon::FontSystem::new_with_fonts( + [glyphon::fontdb::Source::Binary(Arc::new( + include_bytes!("../fonts/Iced-Icons.ttf") + .as_slice(), + ))] + .into_iter(), + ), cache_builder: |fonts| glyphon::SwashCache::new(fonts), measurement_cache_builder: |_| RefCell::new(Cache::new()), render_cache_builder: |_| Cache::new(), -- cgit From 17470bf7d36ee164311020b9d8c79662ac49c166 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 12:35:10 +0100 Subject: Fix `clippy` lints :tada: --- native/src/widget/text_input.rs | 2 +- wgpu/src/text.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index e6b70db2..0656be62 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -967,7 +967,7 @@ pub fn draw( } else { theme.value_color(style) }, - font: font, + font, bounds: Rectangle { y: text_bounds.center_y(), width: f32::INFINITY, diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 4af57af3..967596f2 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -7,6 +7,7 @@ use iced_native::{Color, Font, Rectangle, Size}; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::cell::RefCell; +use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; use std::sync::Arc; use twox_hash::RandomXxHashBuilder64; @@ -70,7 +71,7 @@ impl Pipeline { let (locale, mut db) = heads.fonts.into_locale_and_db(); db.load_font_source(glyphon::fontdb::Source::Binary(Arc::new( - bytes.to_owned(), + bytes.into_owned(), ))); self.system = Some( @@ -241,7 +242,7 @@ impl Pipeline { fields.fonts, Key { content, - size: size, + size, font, bounds, color: Color::BLACK, @@ -275,7 +276,7 @@ impl Pipeline { fields.fonts, Key { content, - size: size, + size, font, bounds, color: Color::BLACK, @@ -344,9 +345,9 @@ impl<'a> Cache<'a> { hasher.finish() }; - if !self.entries.contains_key(&hash) { + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); - let mut buffer = glyphon::Buffer::new(&fonts, metrics); + let mut buffer = glyphon::Buffer::new(fonts, metrics); buffer.set_size(key.bounds.width, key.bounds.height); buffer.set_text( @@ -363,7 +364,7 @@ impl<'a> Cache<'a> { }), ); - let _ = self.entries.insert(hash, buffer); + let _ = entry.insert(buffer); } let _ = self.recently_used.insert(hash); -- cgit From da4182099db703d59006ca72de4cb4d54c9d7855 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 12:42:02 +0100 Subject: Disable `std` feature for `twox-hash` to fix Wasm build --- wgpu/Cargo.toml | 9 ++++++++- wgpu/src/text.rs | 11 ++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 1ab3d93f..0d9fb75b 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -35,9 +35,16 @@ futures = "0.3" bitflags = "1.2" once_cell = "1.0" rustc-hash = "1.1" -twox-hash = "1.6" ouroboros = "0.15" +[dependencies.twox-hash] +version = "1.6" +default-features = false + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash] +version = "1.6.1" +features = ["std"] + [dependencies.bytemuck] version = "1.9" features = ["derive"] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 967596f2..6c88acc3 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -10,7 +10,6 @@ use std::cell::RefCell; use std::collections::hash_map; use std::hash::{BuildHasher, Hash, Hasher}; use std::sync::Arc; -use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { @@ -311,15 +310,21 @@ fn to_family(font: Font) -> glyphon::Family<'static> { struct Cache<'a> { entries: FxHashMap>, recently_used: FxHashSet, - hasher: RandomXxHashBuilder64, + hasher: HashBuilder, } +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault; + impl<'a> Cache<'a> { fn new() -> Self { Self { entries: FxHashMap::default(), recently_used: FxHashSet::default(), - hasher: RandomXxHashBuilder64::default(), + hasher: HashBuilder::default(), } } -- cgit From b4dd9b6d93479f28304ba23fec59816d6fcec564 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 13:24:11 +0100 Subject: Update `glyphon` fork (sRGB support) --- wgpu/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 0d9fb75b..da8cdf9f 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -60,7 +60,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "4b5e5106f05332dc324bae2095468845a61f36b9" +rev = "304dcaf8443bec7194891fdef3df468a6eaeb7d9" [dependencies.tracing] version = "0.1.6" -- cgit From de7fc637cf72eb14da17dac12510b41e8e3c4a71 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 14:44:42 +0100 Subject: Fix `test` workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38b81842..390e7bf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,4 +38,4 @@ jobs: - name: Check compilation of `todos` example run: cargo build --package todos --target wasm32-unknown-unknown - name: Check compilation of `integration_wgpu` example - run: cargo build --package integration_wgpu --target wasm32-unknown-unknown + run: cargo build --package integration --target wasm32-unknown-unknown -- cgit From a2ab9e939502ff36fd51115d9828fcdcd7bc104d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Feb 2023 16:41:18 +0100 Subject: Use `Pixels` for `Text::size` --- native/src/overlay/menu.rs | 3 ++- native/src/widget/checkbox.rs | 3 +-- native/src/widget/pick_list.rs | 6 +++--- native/src/widget/radio.rs | 3 +-- native/src/widget/text.rs | 8 +++----- native/src/widget/text_input.rs | 18 ++++++++---------- native/src/widget/toggler.rs | 3 +-- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 9c3a8a44..4ccccbf7 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -450,7 +450,8 @@ where let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); - let option_height = (text_size + self.padding.vertical()) as usize; + let option_height = + (text_size + f32::from(self.padding.vertical())) as usize; let offset = viewport.y - bounds.y; let start = (offset / option_height as f32) as usize; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 138c458c..0e21e995 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -288,7 +288,6 @@ where { let label_layout = children.next().unwrap(); - let font = self.font.unwrap_or_else(|| renderer.default_font()); widget::text::draw( renderer, @@ -296,7 +295,7 @@ where label_layout, &self.label, self.text_size, - font, + self.font, widget::text::Appearance { color: custom_style.text_color, }, diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index c1ff0004..b4cda748 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -163,7 +163,7 @@ where self.width, self.padding, self.text_size, - self.font.unwrap_or_else(|| renderer.default_font()), + self.font, self.placeholder.as_deref(), &self.options, ) @@ -344,7 +344,7 @@ pub fn layout( width: Length, padding: Padding, text_size: Option, - font: Renderer::Font, + font: Option, placeholder: Option<&str>, options: &[T], ) -> layout::Node @@ -363,7 +363,7 @@ where let (width, _) = renderer.measure( label, text_size, - font, + font.unwrap_or_else(|| renderer.default_font()), Size::new(f32::INFINITY, f32::INFINITY), ); diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index bd803910..5f60eaef 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -268,7 +268,6 @@ where { let label_layout = children.next().unwrap(); - let font = self.font.unwrap_or(renderer.default_font()); widget::text::draw( renderer, @@ -276,7 +275,7 @@ where label_layout, &self.label, self.text_size, - font, + self.font, widget::text::Appearance { color: custom_style.text_color, }, diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 235a027e..aede754a 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -160,15 +160,13 @@ where _cursor_position: Point, _viewport: &Rectangle, ) { - let font = self.font.unwrap_or_else(|| renderer.default_font()); - draw( renderer, style, layout, &self.content, self.size, - font, + self.font, theme.appearance(self.style), self.horizontal_alignment, self.vertical_alignment, @@ -192,7 +190,7 @@ pub fn draw( layout: Layout<'_>, content: &str, size: Option, - font: Renderer::Font, + font: Option, appearance: Appearance, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, @@ -218,7 +216,7 @@ pub fn draw( size: size.unwrap_or_else(|| renderer.default_size()), bounds: Rectangle { x, y, ..bounds }, color: appearance.color.unwrap_or(style.text_color), - font, + font: font.unwrap_or_else(|| renderer.default_font()), horizontal_alignment, vertical_alignment, }); diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 0656be62..f71b4503 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -179,8 +179,6 @@ where cursor_position: Point, value: Option<&Value>, ) { - let font = self.font.unwrap_or(renderer.default_font()); - draw( renderer, theme, @@ -190,7 +188,7 @@ where value.unwrap_or(&self.value), &self.placeholder, self.size, - font, + self.font, self.is_secure, &self.style, ) @@ -260,7 +258,7 @@ where shell, &mut self.value, self.size, - self.font.unwrap_or(renderer.default_font()), + self.font, self.is_secure, self.on_change.as_ref(), self.on_paste.as_deref(), @@ -279,8 +277,6 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let font = self.font.unwrap_or(renderer.default_font()); - draw( renderer, theme, @@ -290,7 +286,7 @@ where &self.value, &self.placeholder, self.size, - font, + self.font, self.is_secure, &self.style, ) @@ -414,7 +410,7 @@ pub fn update<'a, Message, Renderer>( shell: &mut Shell<'_, Message>, value: &mut Value, size: Option, - font: Renderer::Font, + font: Option, is_secure: bool, on_change: &dyn Fn(String) -> Message, on_paste: Option<&dyn Fn(String) -> Message>, @@ -820,7 +816,7 @@ pub fn draw( value: &Value, placeholder: &str, size: Option, - font: Renderer::Font, + font: Option, is_secure: bool, style: &::Style, ) where @@ -854,6 +850,7 @@ pub fn draw( ); let text = value.to_string(); + let font = font.unwrap_or_else(|| renderer.default_font()); let size = size.unwrap_or_else(|| renderer.default_size()); let (cursor, offset) = if let Some(focus) = &state.is_focused { @@ -1188,7 +1185,7 @@ where fn find_cursor_position( renderer: &Renderer, text_bounds: Rectangle, - font: Renderer::Font, + font: Option, size: Option, value: &Value, state: &State, @@ -1197,6 +1194,7 @@ fn find_cursor_position( where Renderer: text::Renderer, { + let font = font.unwrap_or_else(|| renderer.default_font()); let size = size.unwrap_or_else(|| renderer.default_size()); let offset = offset(renderer, text_bounds, font, size, value, state); diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 495406db..d9c80ebe 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -236,7 +236,6 @@ where if let Some(label) = &self.label { let label_layout = children.next().unwrap(); - let font = self.font.unwrap_or_else(|| renderer.default_font()); crate::widget::text::draw( renderer, @@ -244,7 +243,7 @@ where label_layout, label, self.text_size, - font, + self.font, Default::default(), self.text_alignment, alignment::Vertical::Center, -- cgit From eb3cd3a321db48a4a4555575f022e9a0ed85063b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:24:58 +0100 Subject: Fix `overlay::Menu` line height --- native/src/overlay/menu.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 4ccccbf7..0509f7bc 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -344,7 +344,7 @@ where let size = { let intrinsic = Size::new( 0.0, - (text_size + self.padding.vertical()) + (text_size * 1.2 + self.padding.vertical()) * self.options.len() as f32, ); @@ -386,7 +386,7 @@ where *self.hovered_option = Some( ((cursor_position.y - bounds.y) - / (text_size + self.padding.vertical())) + / (text_size * 1.2 + self.padding.vertical())) as usize, ); } @@ -401,7 +401,7 @@ where *self.hovered_option = Some( ((cursor_position.y - bounds.y) - / (text_size + self.padding.vertical())) + / (text_size * 1.2 + self.padding.vertical())) as usize, ); @@ -451,7 +451,7 @@ where let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let option_height = - (text_size + f32::from(self.padding.vertical())) as usize; + (text_size * 1.2 + f32::from(self.padding.vertical())) as usize; let offset = viewport.y - bounds.y; let start = (offset / option_height as f32) as usize; @@ -468,7 +468,7 @@ where x: bounds.x, y: bounds.y + (option_height * i) as f32, width: bounds.width, - height: text_size + self.padding.vertical(), + height: text_size * 1.2 + self.padding.vertical(), }; if is_selected { -- cgit From 32309f0140efb4ea92e3e35e3adc5c740909f196 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:26:09 +0100 Subject: Introduce `Weight` enum to `font` --- core/src/font.rs | 16 ++++++++++++++++ core/src/lib.rs | 2 +- native/src/font.rs | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/core/src/font.rs b/core/src/font.rs index 1f774a70..b67c08af 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,3 +1,4 @@ +//! Load and use fonts. use std::hash::Hash; /// A font. @@ -27,3 +28,18 @@ pub enum Font { /// fixed width. Monospace, } + +/// The weight of some text. +#[allow(missing_docs)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Weight { + Thin, + ExtraLight, + Light, + Normal, + Medium, + Semibold, + Bold, + ExtraBold, + Black, +} diff --git a/core/src/lib.rs b/core/src/lib.rs index d3596b4d..d7314851 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,6 +25,7 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; +pub mod font; pub mod keyboard; pub mod mouse; pub mod time; @@ -32,7 +33,6 @@ pub mod time; mod background; mod color; mod content_fit; -mod font; mod length; mod padding; mod pixels; diff --git a/native/src/font.rs b/native/src/font.rs index 6840a25f..15359694 100644 --- a/native/src/font.rs +++ b/native/src/font.rs @@ -1,5 +1,5 @@ //! Load and use fonts. -pub use iced_core::Font; +pub use iced_core::font::*; use crate::command::{self, Command}; use std::borrow::Cow; -- cgit From 6cf4a10906e777645b6bf47faf0e7b8f3c36549e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:28:46 +0100 Subject: Stop reusing `SwashCache` in `text::Pipeline` `SwashCache` can't be easily trimmed and it's not really getting us anything since `glyphon` already caches using a glyph atlas anyways. --- wgpu/src/text.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6c88acc3..59a840ca 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -23,10 +23,6 @@ pub struct Pipeline { struct System { fonts: glyphon::FontSystem, - #[borrows(fonts)] - #[not_covariant] - cache: glyphon::SwashCache<'this>, - #[borrows(fonts)] #[not_covariant] measurement_cache: RefCell>, @@ -52,7 +48,6 @@ impl Pipeline { ))] .into_iter(), ), - cache_builder: |fonts| glyphon::SwashCache::new(fonts), measurement_cache_builder: |_| RefCell::new(Cache::new()), render_cache_builder: |_| Cache::new(), } @@ -76,7 +71,6 @@ impl Pipeline { self.system = Some( SystemBuilder { fonts: glyphon::FontSystem::new_with_locale_and_db(locale, db), - cache_builder: |fonts| glyphon::SwashCache::new(fonts), measurement_cache_builder: |_| RefCell::new(Cache::new()), render_cache_builder: |_| Cache::new(), } @@ -183,7 +177,7 @@ impl Pipeline { }, &text_areas, glyphon::Color::rgb(0, 0, 0), - fields.cache, + &mut glyphon::SwashCache::new(fields.fonts), ) .expect("Prepare text sections"); }); -- cgit From f37b87fbabf09296ad7fea695baded25020d5fbc Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:30:11 +0100 Subject: Avoid allocating `text_areas` in `text::Pipeline` --- wgpu/Cargo.toml | 2 +- wgpu/src/text.rs | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index da8cdf9f..b3fbfc00 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -60,7 +60,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "304dcaf8443bec7194891fdef3df468a6eaeb7d9" +rev = "3c2acb9dea5b9fcb0fa650b3c73b3a3242c62f4a" [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 59a840ca..e8c3e385 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -123,10 +123,8 @@ impl Pipeline { bottom: ((bounds.y + bounds.height) * scale_factor) as i32, }; - let text_areas: Vec<_> = sections - .iter() - .zip(keys.iter()) - .map(|(section, key)| { + let text_areas = + sections.iter().zip(keys.iter()).map(|(section, key)| { let buffer = fields .render_cache .get(key) @@ -163,8 +161,7 @@ impl Pipeline { top: top as i32, bounds, } - }) - .collect(); + }); renderer .prepare( @@ -175,7 +172,7 @@ impl Pipeline { width: target_size.width, height: target_size.height, }, - &text_areas, + text_areas, glyphon::Color::rgb(0, 0, 0), &mut glyphon::SwashCache::new(fields.fonts), ) -- cgit From 15d257a52a994f0110d775eb49ca7e6100b0e1a8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:30:32 +0100 Subject: Stop truncating the `renderers` in `text::Pipeline` We avoid recreating buffers this way, and the amount of layers should stay relatively low anyways. --- wgpu/src/text.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index e8c3e385..16eea234 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -209,7 +209,6 @@ impl Pipeline { } pub fn end_frame(&mut self) { - self.renderers.truncate(self.layer); self.system .as_mut() .unwrap() -- cgit From dd80772da9ce89230d5a96c96923837f9887befa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:31:53 +0100 Subject: Set a minimum `height` for `Buffer` of `size * 1.2` This avoids text from misteriously disappearing, even if the user uses a `height` that isn't enough to fit the text. --- wgpu/src/text.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 16eea234..f1171b5f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -344,7 +344,10 @@ impl<'a> Cache<'a> { let metrics = glyphon::Metrics::new(key.size, key.size * 1.2); let mut buffer = glyphon::Buffer::new(fonts, metrics); - buffer.set_size(key.bounds.width, key.bounds.height); + buffer.set_size( + key.bounds.width, + key.bounds.height.max(key.size * 1.2), + ); buffer.set_text( key.content, glyphon::Attrs::new().family(to_family(key.font)).color({ -- cgit From 7d4c63d4119c505801daf138be44e65599e748d0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:33:09 +0100 Subject: Set `Attrs::monospaced` if `Font::Monospace` is selected --- wgpu/src/text.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index f1171b5f..6e7e60d8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -350,16 +350,19 @@ impl<'a> Cache<'a> { ); buffer.set_text( key.content, - glyphon::Attrs::new().family(to_family(key.font)).color({ - let [r, g, b, a] = key.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }), + glyphon::Attrs::new() + .family(to_family(key.font)) + .color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }) + .monospaced(matches!(key.font, Font::Monospace)), ); let _ = entry.insert(buffer); -- cgit From c8befa8d95890eb22972f487e08974e962028eda Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 5 Feb 2023 18:35:23 +0100 Subject: Fix useless `f32` conversions and please `clippy` --- native/src/widget/text_input.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index f71b4503..f51416e1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -385,13 +385,8 @@ where Renderer: text::Renderer, { let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - - let limits = limits - .width(width) - .pad(padding) - .height(text_size as f32 * 1.2); + let limits = limits.width(width).pad(padding).height(text_size * 1.2); let mut text = layout::Node::new(limits.resolve(Size::ZERO)); text.move_to(Point::new(padding.left, padding.top)); -- cgit From 680ea5dcca37ecf71ab2e5194e907d9414715d22 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 6 Feb 2023 22:21:27 +0100 Subject: Refactor `quad::Pipeline` to `prepare` and `render` architecture --- wgpu/src/backend.rs | 31 ++++++-- wgpu/src/buffer.rs | 86 ++++++++++++++++++++ wgpu/src/quad.rs | 225 +++++++++++++++++++++++++++------------------------- 3 files changed, 230 insertions(+), 112 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index f19959bd..2c35936c 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -88,13 +88,14 @@ impl Backend { let mut layers = Layer::generate(primitives, viewport); layers.push(Layer::overlay(overlay_text, viewport)); - for layer in layers { + for (i, layer) in layers.iter().enumerate() { self.flush( device, queue, scale_factor, transformation, &layer, + i, staging_belt, encoder, frame, @@ -102,6 +103,7 @@ impl Backend { ); } + self.quad_pipeline.end_frame(); self.text_pipeline.end_frame(); #[cfg(any(feature = "image", feature = "svg"))] @@ -115,6 +117,7 @@ impl Backend { scale_factor: f32, transformation: Transformation, layer: &Layer<'_>, + layer_index: usize, staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, @@ -127,16 +130,32 @@ impl Backend { } if !layer.quads.is_empty() { - self.quad_pipeline.draw( + self.quad_pipeline.prepare( device, - staging_belt, - encoder, + queue, &layer.quads, transformation, scale_factor, - bounds, - target, ); + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::quad render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + }, + )], + depth_stencil_attachment: None, + }); + + self.quad_pipeline + .render(layer_index, bounds, &mut render_pass); } if !layer.meshes.is_empty() { diff --git a/wgpu/src/buffer.rs b/wgpu/src/buffer.rs index 7c092d0b..c210dd4e 100644 --- a/wgpu/src/buffer.rs +++ b/wgpu/src/buffer.rs @@ -1,3 +1,89 @@ //! Utilities for buffer operations. pub mod dynamic; pub mod r#static; + +use std::marker::PhantomData; +use std::ops::RangeBounds; + +#[derive(Debug)] +pub struct Buffer { + label: &'static str, + size: u64, + usage: wgpu::BufferUsages, + raw: wgpu::Buffer, + type_: PhantomData, +} + +impl Buffer { + pub fn new( + device: &wgpu::Device, + label: &'static str, + amount: usize, + usage: wgpu::BufferUsages, + ) -> Self { + let size = next_copy_size::(amount); + + let raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(label), + size, + usage, + mapped_at_creation: false, + }); + + Self { + label, + size, + usage, + raw, + type_: PhantomData, + } + } + + pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool { + let new_size = (std::mem::size_of::() * new_count) as u64; + + if self.size < new_size { + self.raw = device.create_buffer(&wgpu::BufferDescriptor { + label: Some(self.label), + size: new_size, + usage: self.usage, + mapped_at_creation: false, + }); + + self.size = new_size; + + true + } else { + false + } + } + + pub fn write( + &self, + queue: &wgpu::Queue, + offset_count: usize, + contents: &[T], + ) { + queue.write_buffer( + &self.raw, + (std::mem::size_of::() * offset_count) as u64, + bytemuck::cast_slice(contents), + ); + } + + pub fn slice( + &self, + bounds: impl RangeBounds, + ) -> wgpu::BufferSlice<'_> { + self.raw.slice(bounds) + } +} + +fn next_copy_size(amount: usize) -> u64 { + let align_mask = wgpu::COPY_BUFFER_ALIGNMENT - 1; + + (((std::mem::size_of::() * amount).next_power_of_two() as u64 + + align_mask) + & !align_mask) + .max(wgpu::COPY_BUFFER_ALIGNMENT) +} diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 2f5fcc6b..ebbe7a9d 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,3 +1,4 @@ +use crate::buffer::Buffer; use crate::Transformation; use iced_graphics::layer; use iced_native::Rectangle; @@ -12,11 +13,11 @@ use tracing::info_span; #[derive(Debug)] pub struct Pipeline { pipeline: wgpu::RenderPipeline, - constants: wgpu::BindGroup, - constants_buffer: wgpu::Buffer, + constant_layout: wgpu::BindGroupLayout, vertices: wgpu::Buffer, indices: wgpu::Buffer, - instances: wgpu::Buffer, + layers: Vec, + current_layer: usize, } impl Pipeline { @@ -38,22 +39,6 @@ impl Pipeline { }], }); - let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::quad uniforms buffer"), - size: mem::size_of::() as wgpu::BufferAddress, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::quad uniforms bind group"), - layout: &constant_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: constants_buffer.as_entire_binding(), - }], - }); - let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("iced_wgpu::quad pipeline layout"), @@ -148,118 +133,146 @@ impl Pipeline { usage: wgpu::BufferUsages::INDEX, }); - let instances = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::quad instance buffer"), - size: mem::size_of::() as u64 * MAX_INSTANCES as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - Pipeline { pipeline, - constants, - constants_buffer, + constant_layout, vertices, indices, - instances, + layers: Vec::new(), + current_layer: 0, } } - pub fn draw( + pub fn prepare( &mut self, device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, + queue: &wgpu::Queue, instances: &[layer::Quad], transformation: Transformation, scale: f32, - bounds: Rectangle, - target: &wgpu::TextureView, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "DRAW").entered(); + if self.layers.len() <= self.current_layer { + self.layers.push(Layer::new(device, &self.constant_layout)); + } - let uniforms = Uniforms::new(transformation, scale); + let layer = &mut self.layers[self.current_layer]; + layer.prepare(device, queue, instances, transformation, scale); - { - let mut constants_buffer = staging_belt.write_buffer( - encoder, - &self.constants_buffer, - 0, - wgpu::BufferSize::new(mem::size_of::() as u64) - .unwrap(), - device, + self.current_layer += 1; + } + + pub fn render<'a>( + &'a self, + layer: usize, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + if let Some(layer) = self.layers.get(layer) { + render_pass.set_pipeline(&self.pipeline); + + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + + render_pass.set_index_buffer( + self.indices.slice(..), + wgpu::IndexFormat::Uint16, ); + render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - constants_buffer.copy_from_slice(bytemuck::bytes_of(&uniforms)); + layer.draw(render_pass); } + } - let mut i = 0; - let total = instances.len(); + pub fn end_frame(&mut self) { + self.current_layer = 0; + } +} - while i < total { - let end = (i + MAX_INSTANCES).min(total); - let amount = end - i; +#[derive(Debug)] +struct Layer { + constants: wgpu::BindGroup, + constants_buffer: wgpu::Buffer, + instances: Buffer, + instance_count: usize, +} - let instance_bytes = bytemuck::cast_slice(&instances[i..end]); +impl Layer { + pub fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + ) -> Self { + let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::quad uniforms buffer"), + size: mem::size_of::() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); - let mut instance_buffer = staging_belt.write_buffer( - encoder, - &self.instances, - 0, - wgpu::BufferSize::new(instance_bytes.len() as u64).unwrap(), - device, - ); + let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::quad uniforms bind group"), + layout: &constant_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: constants_buffer.as_entire_binding(), + }], + }); - instance_buffer.copy_from_slice(instance_bytes); - - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Quad", "BEGIN_RENDER_PASS").enter(); - - { - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }, - )], - depth_stencil_attachment: None, - }); - - render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_index_buffer( - self.indices.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - render_pass.set_vertex_buffer(1, self.instances.slice(..)); - - render_pass.set_scissor_rect( - bounds.x, - bounds.y, - bounds.width, - // TODO: Address anti-aliasing adjustments properly - bounds.height, - ); - - render_pass.draw_indexed( - 0..QUAD_INDICES.len() as u32, - 0, - 0..amount as u32, - ); - } - - i += MAX_INSTANCES; + let instances = Buffer::new( + device, + "iced_wgpu::quad instance buffer", + MAX_INSTANCES, + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + Self { + constants, + constants_buffer, + instances, + instance_count: 0, } } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + instances: &[layer::Quad], + transformation: Transformation, + scale: f32, + ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Wgpu::Quad", "PREPARE").entered(); + + let uniforms = Uniforms::new(transformation, scale); + + queue.write_buffer( + &self.constants_buffer, + 0, + bytemuck::bytes_of(&uniforms), + ); + + let _ = self.instances.resize(device, instances.len()); + self.instances.write(queue, 0, instances); + self.instance_count = instances.len(); + } + + pub fn draw<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + #[cfg(feature = "tracing")] + let _ = info_span!("Wgpu::Quad", "DRAW").entered(); + + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_vertex_buffer(1, self.instances.slice(..)); + + render_pass.draw_indexed( + 0..QUAD_INDICES.len() as u32, + 0, + 0..self.instance_count as u32, + ); + } } #[repr(C)] -- cgit From a970f34cb4968bfe913c1f77b5087f44f29aba8a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 18:35:05 +0100 Subject: Apply `ceil` to text bounds when drawing --- wgpu/src/text.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 6e7e60d8..95994a4e 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -105,8 +105,10 @@ impl Pipeline { size: section.size * scale_factor, font: section.font, bounds: Size { - width: section.bounds.width * scale_factor, - height: section.bounds.height * scale_factor, + width: (section.bounds.width * scale_factor) + .ceil(), + height: (section.bounds.height * scale_factor) + .ceil(), }, color: section.color, }, -- cgit From 77b59496b0a138510938aca8367572e08019ddb6 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 19:26:41 +0100 Subject: Fix rendering order for `quad::Pipeline` --- wgpu/src/backend.rs | 7 ++----- wgpu/src/quad.rs | 22 +++++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 2c35936c..395d28d5 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -88,14 +88,13 @@ impl Backend { let mut layers = Layer::generate(primitives, viewport); layers.push(Layer::overlay(overlay_text, viewport)); - for (i, layer) in layers.iter().enumerate() { + for layer in layers { self.flush( device, queue, scale_factor, transformation, &layer, - i, staging_belt, encoder, frame, @@ -117,7 +116,6 @@ impl Backend { scale_factor: f32, transformation: Transformation, layer: &Layer<'_>, - layer_index: usize, staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, @@ -154,8 +152,7 @@ impl Backend { depth_stencil_attachment: None, }); - self.quad_pipeline - .render(layer_index, bounds, &mut render_pass); + self.quad_pipeline.render(bounds, &mut render_pass); } if !layer.meshes.is_empty() { diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index ebbe7a9d..e4173b12 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -17,7 +17,8 @@ pub struct Pipeline { vertices: wgpu::Buffer, indices: wgpu::Buffer, layers: Vec, - current_layer: usize, + prepare_layer: usize, + render_layer: usize, } impl Pipeline { @@ -139,7 +140,8 @@ impl Pipeline { vertices, indices, layers: Vec::new(), - current_layer: 0, + prepare_layer: 0, + render_layer: 0, } } @@ -151,23 +153,22 @@ impl Pipeline { transformation: Transformation, scale: f32, ) { - if self.layers.len() <= self.current_layer { + if self.layers.len() <= self.prepare_layer { self.layers.push(Layer::new(device, &self.constant_layout)); } - let layer = &mut self.layers[self.current_layer]; + let layer = &mut self.layers[self.prepare_layer]; layer.prepare(device, queue, instances, transformation, scale); - self.current_layer += 1; + self.prepare_layer += 1; } pub fn render<'a>( - &'a self, - layer: usize, + &'a mut self, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, ) { - if let Some(layer) = self.layers.get(layer) { + if let Some(layer) = self.layers.get(self.render_layer) { render_pass.set_pipeline(&self.pipeline); render_pass.set_scissor_rect( @@ -184,11 +185,14 @@ impl Pipeline { render_pass.set_vertex_buffer(0, self.vertices.slice(..)); layer.draw(render_pass); + + self.render_layer += 1; } } pub fn end_frame(&mut self) { - self.current_layer = 0; + self.prepare_layer = 0; + self.render_layer = 0; } } -- cgit From 34c963f7b39e3f16b55665a978948ead5b869f0f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 20:08:26 +0100 Subject: Remove unnecessary borrow in `quad::Pipeline` --- wgpu/src/quad.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index e4173b12..94bcec92 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -218,7 +218,7 @@ impl Layer { let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::quad uniforms bind group"), - layout: &constant_layout, + layout: constant_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: constants_buffer.as_entire_binding(), -- cgit From 363966ee9e7aa81a3679eaea776d2c867aa6c3b0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 22:53:08 +0100 Subject: Refactor `image::Pipeline` into `prepare` and `render` architecture --- wgpu/src/backend.rs | 26 ++++- wgpu/src/image.rs | 259 ++++++++++++++++++++++++++---------------------- wgpu/src/image/atlas.rs | 49 +++++---- wgpu/src/lib.rs | 3 +- 4 files changed, 190 insertions(+), 147 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 395d28d5..90a511ef 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -106,7 +106,7 @@ impl Backend { self.text_pipeline.end_frame(); #[cfg(any(feature = "image", feature = "svg"))] - self.image_pipeline.trim_cache(device, encoder); + self.image_pipeline.end_frame(device, queue, encoder); } fn flush( @@ -177,16 +177,32 @@ impl Backend { let scaled = transformation * Transformation::scale(scale_factor, scale_factor); - self.image_pipeline.draw( + self.image_pipeline.prepare( device, - staging_belt, + queue, encoder, &layer.images, scaled, - bounds, - target, scale_factor, ); + + let mut render_pass = + encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::image render pass"), + color_attachments: &[Some( + wgpu::RenderPassColorAttachment { + view: target, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: true, + }, + }, + )], + depth_stencil_attachment: None, + }); + + self.image_pipeline.render(bounds, &mut render_pass); } } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index a5e63b17..f6c39f1a 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -6,7 +6,7 @@ use iced_graphics::image::raster; #[cfg(feature = "svg")] use iced_graphics::image::vector; -use crate::Transformation; +use crate::{Buffer, Transformation}; use atlas::Atlas; use iced_graphics::layer; @@ -34,15 +34,108 @@ pub struct Pipeline { vector_cache: RefCell>, pipeline: wgpu::RenderPipeline, - uniforms: wgpu::Buffer, vertices: wgpu::Buffer, indices: wgpu::Buffer, - instances: wgpu::Buffer, - constants: wgpu::BindGroup, + sampler: wgpu::Sampler, texture: wgpu::BindGroup, texture_version: usize, - texture_layout: wgpu::BindGroupLayout, texture_atlas: Atlas, + texture_layout: wgpu::BindGroupLayout, + constant_layout: wgpu::BindGroupLayout, + + layers: Vec, + prepare_layer: usize, + render_layer: usize, +} + +#[derive(Debug)] +struct Layer { + uniforms: wgpu::Buffer, + constants: wgpu::BindGroup, + instances: Buffer, + instance_count: usize, +} + +impl Layer { + fn new( + device: &wgpu::Device, + constant_layout: &wgpu::BindGroupLayout, + sampler: &wgpu::Sampler, + ) -> Self { + let uniforms = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("iced_wgpu::image uniforms buffer"), + size: mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + 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 { + uniforms, + constants, + instances, + instance_count: 0, + } + } + + fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + instances: &[Instance], + transformation: Transformation, + ) { + queue.write_buffer( + &self.uniforms, + 0, + bytemuck::bytes_of(&Uniforms { + transform: transformation.into(), + }), + ); + + let _ = self.instances.resize(device, instances.len()); + self.instances.write(queue, 0, instances); + + self.instance_count = instances.len(); + } + + fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { + render_pass.set_bind_group(0, &self.constants, &[]); + render_pass.set_vertex_buffer(1, self.instances.slice(..)); + + render_pass.draw_indexed( + 0..QUAD_INDICES.len() as u32, + 0, + 0..self.instance_count as u32, + ); + } } impl Pipeline { @@ -86,35 +179,6 @@ impl Pipeline { ], }); - let uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::image uniforms buffer"), - size: mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let constant_bind_group = - 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_buffer, - offset: 0, - size: None, - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&sampler), - }, - ], - }); - let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::image texture atlas layout"), @@ -225,13 +289,6 @@ impl Pipeline { usage: wgpu::BufferUsages::INDEX, }); - let instances = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("iced_wgpu::image instance buffer"), - size: mem::size_of::() as u64 * Instance::MAX as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let texture_atlas = Atlas::new(device); let texture = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -253,15 +310,18 @@ impl Pipeline { vector_cache: RefCell::new(vector::Cache::default()), pipeline, - uniforms: uniforms_buffer, vertices, indices, - instances, - constants: constant_bind_group, + sampler, texture, texture_version: texture_atlas.layer_count(), - texture_layout, texture_atlas, + texture_layout, + constant_layout, + + layers: Vec::new(), + prepare_layer: 0, + render_layer: 0, } } @@ -281,17 +341,18 @@ impl Pipeline { svg.viewport_dimensions() } - pub fn draw( + pub fn prepare( &mut self, device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, + queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, images: &[layer::Image], transformation: Transformation, - bounds: Rectangle, - target: &wgpu::TextureView, _scale: f32, ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Wgpu::Image", "PREPARE").entered(); + #[cfg(feature = "tracing")] let _ = info_span!("Wgpu::Image", "DRAW").entered(); @@ -309,7 +370,7 @@ impl Pipeline { layer::Image::Raster { handle, bounds } => { if let Some(atlas_entry) = raster_cache.upload( handle, - &mut (device, encoder), + &mut (device, queue, encoder), &mut self.texture_atlas, ) { add_instances( @@ -336,7 +397,7 @@ impl Pipeline { *color, size, _scale, - &mut (device, encoder), + &mut (device, queue, encoder), &mut self.texture_atlas, ) { add_instances( @@ -376,68 +437,27 @@ impl Pipeline { self.texture_version = texture_version; } - { - let mut uniforms_buffer = staging_belt.write_buffer( - encoder, - &self.uniforms, - 0, - wgpu::BufferSize::new(mem::size_of::() as u64) - .unwrap(), + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( device, - ); - - uniforms_buffer.copy_from_slice(bytemuck::bytes_of(&Uniforms { - transform: transformation.into(), - })); + &self.constant_layout, + &self.sampler, + )); } - let mut i = 0; - let total = instances.len(); - - while i < total { - let end = (i + Instance::MAX).min(total); - let amount = end - i; - - let mut instances_buffer = staging_belt.write_buffer( - encoder, - &self.instances, - 0, - wgpu::BufferSize::new( - (amount * std::mem::size_of::()) as u64, - ) - .unwrap(), - device, - ); - - instances_buffer.copy_from_slice(bytemuck::cast_slice( - &instances[i..i + amount], - )); + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare(device, queue, instances, transformation); - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::image render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }, - )], - depth_stencil_attachment: None, - }); + self.prepare_layer += 1; + } + pub fn render<'a>( + &'a mut self, + bounds: Rectangle, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + if let Some(layer) = self.layers.get(self.render_layer) { render_pass.set_pipeline(&self.pipeline); - render_pass.set_bind_group(0, &self.constants, &[]); - render_pass.set_bind_group(1, &self.texture, &[]); - render_pass.set_index_buffer( - self.indices.slice(..), - wgpu::IndexFormat::Uint16, - ); - render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - render_pass.set_vertex_buffer(1, self.instances.slice(..)); render_pass.set_scissor_rect( bounds.x, @@ -446,30 +466,37 @@ impl Pipeline { bounds.height, ); - render_pass.draw_indexed( - 0..QUAD_INDICES.len() as u32, - 0, - 0..amount as u32, + render_pass.set_bind_group(1, &self.texture, &[]); + render_pass.set_index_buffer( + self.indices.slice(..), + wgpu::IndexFormat::Uint16, ); + render_pass.set_vertex_buffer(0, self.vertices.slice(..)); - i += Instance::MAX; + layer.render(render_pass); + + self.render_layer += 1; } } - pub fn trim_cache( + pub fn end_frame( &mut self, device: &wgpu::Device, + queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, ) { #[cfg(feature = "image")] self.raster_cache .borrow_mut() - .trim(&mut self.texture_atlas, &mut (device, encoder)); + .trim(&mut self.texture_atlas, &mut (device, queue, encoder)); #[cfg(feature = "svg")] self.vector_cache .borrow_mut() - .trim(&mut self.texture_atlas, &mut (device, encoder)); + .trim(&mut self.texture_atlas, &mut (device, queue, encoder)); + + self.prepare_layer = 0; + self.render_layer = 0; } } @@ -507,7 +534,7 @@ struct Instance { } impl Instance { - pub const MAX: usize = 1_000; + pub const INITIAL: usize = 1_000; } #[repr(C)] diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs index eafe2f96..7df67abd 100644 --- a/wgpu/src/image/atlas.rs +++ b/wgpu/src/image/atlas.rs @@ -185,13 +185,13 @@ impl Atlas { fn upload_allocation( &mut self, - buffer: &wgpu::Buffer, + data: &[u8], image_width: u32, image_height: u32, padding: u32, offset: usize, allocation: &Allocation, - encoder: &mut wgpu::CommandEncoder, + queue: &wgpu::Queue, ) { let (x, y) = allocation.position(); let Size { width, height } = allocation.size(); @@ -203,15 +203,7 @@ impl Atlas { depth_or_array_layers: 1, }; - encoder.copy_buffer_to_texture( - wgpu::ImageCopyBuffer { - buffer, - layout: wgpu::ImageDataLayout { - offset: offset as u64, - bytes_per_row: NonZeroU32::new(4 * image_width + padding), - rows_per_image: NonZeroU32::new(image_height), - }, - }, + queue.write_texture( wgpu::ImageCopyTexture { texture: &self.texture, mip_level: 0, @@ -222,6 +214,12 @@ impl Atlas { }, aspect: wgpu::TextureAspect::default(), }, + data, + wgpu::ImageDataLayout { + offset: offset as u64, + bytes_per_row: NonZeroU32::new(4 * image_width + padding), + rows_per_image: NonZeroU32::new(image_height), + }, extent, ); } @@ -301,17 +299,19 @@ impl Atlas { impl image::Storage for Atlas { type Entry = Entry; - type State<'a> = (&'a wgpu::Device, &'a mut wgpu::CommandEncoder); + type State<'a> = ( + &'a wgpu::Device, + &'a wgpu::Queue, + &'a mut wgpu::CommandEncoder, + ); fn upload( &mut self, width: u32, height: u32, data: &[u8], - (device, encoder): &mut Self::State<'_>, + (device, queue, encoder): &mut Self::State<'_>, ) -> Option { - use wgpu::util::DeviceExt; - let entry = { let current_size = self.layers.len(); let entry = self.allocate(width, height)?; @@ -344,17 +344,16 @@ impl image::Storage for Atlas { ) } - let buffer = - device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("iced_wgpu::image staging buffer"), - contents: &padded_data, - usage: wgpu::BufferUsages::COPY_SRC, - }); - match &entry { Entry::Contiguous(allocation) => { self.upload_allocation( - &buffer, width, height, padding, 0, allocation, encoder, + &padded_data, + width, + height, + padding, + 0, + allocation, + queue, ); } Entry::Fragmented { fragments, .. } => { @@ -363,13 +362,13 @@ impl image::Storage for Atlas { let offset = (y * padded_width as u32 + 4 * x) as usize; self.upload_allocation( - &buffer, + &padded_data, width, height, padding, offset, &fragment.allocation, - encoder, + queue, ); } } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 6d6d3fd6..9da40572 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -56,7 +56,8 @@ pub use wgpu; pub use backend::Backend; pub use settings::Settings; -pub(crate) use iced_graphics::Transformation; +use crate::buffer::Buffer; +use iced_graphics::Transformation; #[cfg(any(feature = "image", feature = "svg"))] mod image; -- cgit From 23ed352e83dcb8a13acdac1cd4c7e2a9df492ebd Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 22:53:41 +0100 Subject: Fix needless borrows in `image::Pipeline` --- wgpu/src/image.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index f6c39f1a..5d1ae8d7 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -71,7 +71,7 @@ impl Layer { let constants = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("iced_wgpu::image constants bind group"), - layout: &constant_layout, + layout: constant_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -85,7 +85,7 @@ impl Layer { }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::Sampler(&sampler), + resource: wgpu::BindingResource::Sampler(sampler), }, ], }); -- cgit From b8c1809ea101cece6943432fd3597f785c39af09 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 7 Feb 2023 23:55:16 +0100 Subject: Refactor `triangle::Pipeline` into `prepare` and `render` architecture And get rid of the staging belt! :tada: --- examples/integration/src/main.rs | 9 - wgpu/src/backend.rs | 17 +- wgpu/src/buffer/dynamic.rs | 23 +- wgpu/src/buffer/static.rs | 21 +- wgpu/src/quad.rs | 7 +- wgpu/src/triangle.rs | 561 +++++++++++++++++++++++---------------- wgpu/src/window/compositor.rs | 11 - 7 files changed, 358 insertions(+), 291 deletions(-) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 69f6961e..5af30ab1 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -125,9 +125,6 @@ pub fn main() { let mut resized = false; - // Initialize staging belt - let mut staging_belt = wgpu::util::StagingBelt::new(5 * 1024); - // Initialize scene and GUI controls let scene = Scene::new(&device, format); let controls = Controls::new(); @@ -252,7 +249,6 @@ pub fn main() { backend.present( &device, &queue, - &mut staging_belt, &mut encoder, &view, primitive, @@ -262,7 +258,6 @@ pub fn main() { }); // Then we submit the work - staging_belt.finish(); queue.submit(Some(encoder.finish())); frame.present(); @@ -272,10 +267,6 @@ pub fn main() { state.mouse_interaction(), ), ); - - // And recall staging buffers - staging_belt.recall(); - } Err(error) => match error { wgpu::SurfaceError::OutOfMemory => { diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 90a511ef..8c4c0daa 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -70,7 +70,6 @@ impl Backend { &mut self, device: &wgpu::Device, queue: &wgpu::Queue, - staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, frame: &wgpu::TextureView, primitives: &[Primitive], @@ -95,7 +94,6 @@ impl Backend { scale_factor, transformation, &layer, - staging_belt, encoder, frame, target_size, @@ -104,6 +102,7 @@ impl Backend { 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(device, queue, encoder); @@ -116,7 +115,6 @@ impl Backend { scale_factor: f32, transformation: Transformation, layer: &Layer<'_>, - staging_belt: &mut wgpu::util::StagingBelt, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, target_size: Size, @@ -159,15 +157,20 @@ impl Backend { let scaled = transformation * Transformation::scale(scale_factor, scale_factor); - self.triangle_pipeline.draw( + self.triangle_pipeline.prepare( + device, + queue, + &layer.meshes, + scaled, + ); + + self.triangle_pipeline.render( device, - staging_belt, encoder, target, target_size, - scaled, - scale_factor, &layer.meshes, + scale_factor, ); } diff --git a/wgpu/src/buffer/dynamic.rs b/wgpu/src/buffer/dynamic.rs index 88289b98..43fc47ac 100644 --- a/wgpu/src/buffer/dynamic.rs +++ b/wgpu/src/buffer/dynamic.rs @@ -112,25 +112,8 @@ impl Buffer { } /// Write the contents of this dynamic buffer to the GPU via staging belt command. - pub fn write( - &mut self, - device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, - ) { - let size = self.cpu.get_ref().len(); - - if let Some(buffer_size) = wgpu::BufferSize::new(size as u64) { - let mut buffer = staging_belt.write_buffer( - encoder, - &self.gpu, - 0, - buffer_size, - device, - ); - - buffer.copy_from_slice(self.cpu.get_ref()); - } + pub fn write(&mut self, queue: &wgpu::Queue) { + queue.write_buffer(&self.gpu, 0, self.cpu.get_ref()); } // Gets the aligned offset at the given index from the CPU buffer. @@ -184,7 +167,7 @@ impl Internal { } /// Returns bytearray of aligned CPU buffer. - pub(super) fn get_ref(&self) -> &Vec { + pub(super) fn get_ref(&self) -> &[u8] { match self { Internal::Uniform(buf) => buf.as_ref(), #[cfg(not(target_arch = "wasm32"))] diff --git a/wgpu/src/buffer/static.rs b/wgpu/src/buffer/static.rs index ef87422f..7ae7edd2 100644 --- a/wgpu/src/buffer/static.rs +++ b/wgpu/src/buffer/static.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use std::mem; //128 triangles/indices -const DEFAULT_STATIC_BUFFER_COUNT: wgpu::BufferAddress = 128; +const DEFAULT_STATIC_BUFFER_COUNT: wgpu::BufferAddress = 1_000; /// A generic buffer struct useful for items which have no alignment requirements /// (e.g. Vertex, Index buffers) & no dynamic offsets. @@ -71,28 +71,15 @@ impl Buffer { /// Returns the size of the written bytes. pub fn write( &mut self, - device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, + queue: &wgpu::Queue, offset: u64, content: &[T], ) -> u64 { let bytes = bytemuck::cast_slice(content); let bytes_size = bytes.len() as u64; - if let Some(buffer_size) = wgpu::BufferSize::new(bytes_size) { - let mut buffer = staging_belt.write_buffer( - encoder, - &self.gpu, - offset, - buffer_size, - device, - ); - - buffer.copy_from_slice(bytes); - - self.offsets.push(offset); - } + queue.write_buffer(&self.gpu, offset, bytes); + self.offsets.push(offset); bytes_size } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 94bcec92..c1aa49c4 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,5 +1,4 @@ -use crate::buffer::Buffer; -use crate::Transformation; +use crate::{Buffer, Transformation}; use iced_graphics::layer; use iced_native::Rectangle; @@ -228,7 +227,7 @@ impl Layer { let instances = Buffer::new( device, "iced_wgpu::quad instance buffer", - MAX_INSTANCES, + INITIAL_INSTANCES, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, ); @@ -302,7 +301,7 @@ const QUAD_VERTS: [Vertex; 4] = [ }, ]; -const MAX_INSTANCES: usize = 100_000; +const INITIAL_INSTANCES: usize = 10_000; #[repr(C)] #[derive(Debug, Clone, Copy, Zeroable, Pod)] diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index efdd214b..572af1e8 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -14,50 +14,56 @@ use tracing::info_span; #[derive(Debug)] pub struct Pipeline { blit: Option, - index_buffer: Buffer, - index_strides: Vec, solid: solid::Pipeline, /// Gradients are currently not supported on WASM targets due to their need of storage buffers. #[cfg(not(target_arch = "wasm32"))] gradient: gradient::Pipeline, + + layers: Vec, + prepare_layer: usize, + render_layer: usize, } -impl Pipeline { - pub fn new( +#[derive(Debug)] +struct Layer { + index_buffer: Buffer, + index_strides: Vec, + solid: solid::Layer, + + #[cfg(not(target_arch = "wasm32"))] + gradient: gradient::Layer, +} + +impl Layer { + fn new( device: &wgpu::Device, - format: wgpu::TextureFormat, - antialiasing: Option, - ) -> Pipeline { - Pipeline { - blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + solid: &solid::Pipeline, + #[cfg(not(target_arch = "wasm32"))] gradient: &gradient::Pipeline, + ) -> Self { + Self { index_buffer: Buffer::new( device, "iced_wgpu::triangle vertex buffer", wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, ), index_strides: Vec::new(), - solid: solid::Pipeline::new(device, format, antialiasing), + solid: solid::Layer::new(device, &solid.constants_layout), #[cfg(not(target_arch = "wasm32"))] - gradient: gradient::Pipeline::new(device, format, antialiasing), + gradient: gradient::Layer::new(device, &gradient.constants_layout), } } - pub fn draw( + fn prepare( &mut self, device: &wgpu::Device, - staging_belt: &mut wgpu::util::StagingBelt, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, - target_size: Size, - transformation: Transformation, - scale_factor: f32, + queue: &wgpu::Queue, + solid: &solid::Pipeline, + #[cfg(not(target_arch = "wasm32"))] gradient: &gradient::Pipeline, meshes: &[Mesh<'_>], + transformation: Transformation, ) { - #[cfg(feature = "tracing")] - let _ = info_span!("Wgpu::Triangle", "DRAW").entered(); - // Count the total amount of vertices & indices we need to handle let count = mesh::attribute_count_of(meshes); @@ -75,6 +81,7 @@ impl Pipeline { .resize(device, count.gradient_vertices); // Prepare dynamic buffers & data store for writing + self.index_buffer.clear(); self.index_strides.clear(); self.solid.vertices.clear(); self.solid.uniforms.clear(); @@ -99,13 +106,8 @@ impl Pipeline { let transform = transformation * Transformation::translate(origin.x, origin.y); - let new_index_offset = self.index_buffer.write( - device, - staging_belt, - encoder, - index_offset, - indices, - ); + let new_index_offset = + self.index_buffer.write(queue, index_offset, indices); index_offset += new_index_offset; self.index_strides.push(indices.len() as u32); @@ -116,9 +118,7 @@ impl Pipeline { self.solid.uniforms.push(&solid::Uniforms::new(transform)); let written_bytes = self.solid.vertices.write( - device, - staging_belt, - encoder, + queue, solid_vertex_offset, &buffers.vertices, ); @@ -130,9 +130,7 @@ impl Pipeline { buffers, gradient, .. } => { let written_bytes = self.gradient.vertices.write( - device, - staging_belt, - encoder, + queue, gradient_vertex_offset, &buffers.vertices, ); @@ -196,14 +194,14 @@ impl Pipeline { let uniforms_resized = self.solid.uniforms.resize(device); if uniforms_resized { - self.solid.bind_group = solid::Pipeline::bind_group( + self.solid.constants = solid::Layer::bind_group( device, self.solid.uniforms.raw(), - &self.solid.bind_group_layout, + &solid.constants_layout, ) } - self.solid.uniforms.write(device, staging_belt, encoder); + self.solid.uniforms.write(queue); } #[cfg(not(target_arch = "wasm32"))] @@ -218,22 +216,169 @@ impl Pipeline { let storage_resized = self.gradient.storage.resize(device); if uniforms_resized || storage_resized { - self.gradient.bind_group = gradient::Pipeline::bind_group( + self.gradient.constants = gradient::Layer::bind_group( device, self.gradient.uniforms.raw(), self.gradient.storage.raw(), - &self.gradient.bind_group_layout, + &gradient.constants_layout, ); } // Write to GPU - self.gradient.uniforms.write(device, staging_belt, encoder); - self.gradient.storage.write(device, staging_belt, encoder); + self.gradient.uniforms.write(queue); + self.gradient.storage.write(queue); // Cleanup self.gradient.color_stop_offset = 0; self.gradient.color_stops_pending_write.color_stops.clear(); } + } + + fn render<'a>( + &'a mut self, + solid: &'a solid::Pipeline, + #[cfg(not(target_arch = "wasm32"))] gradient: &'a gradient::Pipeline, + meshes: &[Mesh<'_>], + scale_factor: f32, + render_pass: &mut wgpu::RenderPass<'a>, + ) { + let mut num_solids = 0; + #[cfg(not(target_arch = "wasm32"))] + let mut num_gradients = 0; + let mut last_is_solid = None; + + for (index, mesh) in meshes.iter().enumerate() { + let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); + + render_pass.set_scissor_rect( + clip_bounds.x, + clip_bounds.y, + clip_bounds.width, + clip_bounds.height, + ); + + match mesh { + Mesh::Solid { .. } => { + if !last_is_solid.unwrap_or(false) { + render_pass.set_pipeline(&solid.pipeline); + + last_is_solid = Some(true); + } + + render_pass.set_bind_group( + 0, + &self.solid.constants, + &[self.solid.uniforms.offset_at_index(num_solids)], + ); + + render_pass.set_vertex_buffer( + 0, + self.solid.vertices.slice_from_index(num_solids), + ); + + num_solids += 1; + } + #[cfg(not(target_arch = "wasm32"))] + Mesh::Gradient { .. } => { + if last_is_solid.unwrap_or(true) { + render_pass.set_pipeline(&gradient.pipeline); + + last_is_solid = Some(false); + } + + render_pass.set_bind_group( + 0, + &self.gradient.constants, + &[self + .gradient + .uniforms + .offset_at_index(num_gradients)], + ); + + render_pass.set_vertex_buffer( + 0, + self.gradient.vertices.slice_from_index(num_gradients), + ); + + num_gradients += 1; + } + #[cfg(target_arch = "wasm32")] + Mesh::Gradient { .. } => {} + }; + + render_pass.set_index_buffer( + self.index_buffer.slice_from_index(index), + wgpu::IndexFormat::Uint32, + ); + + render_pass.draw_indexed(0..self.index_strides[index], 0, 0..1); + } + } +} + +impl Pipeline { + pub fn new( + device: &wgpu::Device, + format: wgpu::TextureFormat, + antialiasing: Option, + ) -> Pipeline { + Pipeline { + blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)), + solid: solid::Pipeline::new(device, format, antialiasing), + + #[cfg(not(target_arch = "wasm32"))] + gradient: gradient::Pipeline::new(device, format, antialiasing), + + layers: Vec::new(), + prepare_layer: 0, + render_layer: 0, + } + } + + pub fn prepare( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + meshes: &[Mesh<'_>], + transformation: Transformation, + ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Wgpu::Triangle", "PREPARE").entered(); + + if self.layers.len() <= self.prepare_layer { + self.layers.push(Layer::new( + device, + &self.solid, + #[cfg(not(target_arch = "wasm32"))] + &self.gradient, + )); + } + + let layer = &mut self.layers[self.prepare_layer]; + layer.prepare( + device, + queue, + &self.solid, + #[cfg(not(target_arch = "wasm32"))] + &self.gradient, + meshes, + transformation, + ); + + self.prepare_layer += 1; + } + + pub fn render( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + target_size: Size, + meshes: &[Mesh<'_>], + scale_factor: f32, + ) { + #[cfg(feature = "tracing")] + let _ = info_span!("Wgpu::Triangle", "DRAW").entered(); // Configure render pass { @@ -268,87 +413,29 @@ impl Pipeline { depth_stencil_attachment: None, }); - let mut num_solids = 0; - #[cfg(not(target_arch = "wasm32"))] - let mut num_gradients = 0; - let mut last_is_solid = None; - - for (index, mesh) in meshes.iter().enumerate() { - let clip_bounds = (mesh.clip_bounds() * scale_factor).snap(); - - render_pass.set_scissor_rect( - clip_bounds.x, - clip_bounds.y, - clip_bounds.width, - clip_bounds.height, - ); - - match mesh { - Mesh::Solid { .. } => { - if !last_is_solid.unwrap_or(false) { - render_pass.set_pipeline(&self.solid.pipeline); - - last_is_solid = Some(true); - } - - render_pass.set_bind_group( - 0, - &self.solid.bind_group, - &[self.solid.uniforms.offset_at_index(num_solids)], - ); - - render_pass.set_vertex_buffer( - 0, - self.solid.vertices.slice_from_index(num_solids), - ); - - num_solids += 1; - } - #[cfg(not(target_arch = "wasm32"))] - Mesh::Gradient { .. } => { - if last_is_solid.unwrap_or(true) { - render_pass.set_pipeline(&self.gradient.pipeline); - - last_is_solid = Some(false); - } - - render_pass.set_bind_group( - 0, - &self.gradient.bind_group, - &[self - .gradient - .uniforms - .offset_at_index(num_gradients)], - ); - - render_pass.set_vertex_buffer( - 0, - self.gradient - .vertices - .slice_from_index(num_gradients), - ); - - num_gradients += 1; - } - #[cfg(target_arch = "wasm32")] - Mesh::Gradient { .. } => {} - }; - - render_pass.set_index_buffer( - self.index_buffer.slice_from_index(index), - wgpu::IndexFormat::Uint32, - ); + let layer = &mut self.layers[self.render_layer]; - render_pass.draw_indexed(0..self.index_strides[index], 0, 0..1); - } + layer.render( + &self.solid, + #[cfg(not(target_arch = "wasm32"))] + &self.gradient, + meshes, + scale_factor, + &mut render_pass, + ); } - self.index_buffer.clear(); + self.render_layer += 1; if let Some(blit) = &mut self.blit { blit.draw(encoder, target); } } + + pub fn end_frame(&mut self) { + self.prepare_layer = 0; + self.render_layer = 0; + } } fn fragment_target( @@ -390,10 +477,62 @@ mod solid { #[derive(Debug)] pub struct Pipeline { pub pipeline: wgpu::RenderPipeline, + pub constants_layout: wgpu::BindGroupLayout, + } + + #[derive(Debug)] + pub struct Layer { pub vertices: Buffer, pub uniforms: dynamic::Buffer, - pub bind_group_layout: wgpu::BindGroupLayout, - pub bind_group: wgpu::BindGroup, + pub constants: wgpu::BindGroup, + } + + impl Layer { + pub fn new( + device: &wgpu::Device, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let vertices = Buffer::new( + device, + "iced_wgpu::triangle::solid vertex buffer", + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + let uniforms = dynamic::Buffer::uniform( + device, + "iced_wgpu::triangle::solid uniforms", + ); + + let constants = + Self::bind_group(device, uniforms.raw(), constants_layout); + + Self { + vertices, + uniforms, + constants, + } + } + + pub fn bind_group( + device: &wgpu::Device, + buffer: &wgpu::Buffer, + layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::triangle::solid bind group"), + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer, + offset: 0, + size: Some(Uniforms::min_size()), + }, + ), + }], + }) + } } #[derive(Debug, Clone, Copy, ShaderType)] @@ -416,18 +555,7 @@ mod solid { format: wgpu::TextureFormat, antialiasing: Option, ) -> Self { - let vertices = Buffer::new( - device, - "iced_wgpu::triangle::solid vertex buffer", - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - let uniforms = dynamic::Buffer::uniform( - device, - "iced_wgpu::triangle::solid uniforms", - ); - - let bind_group_layout = device.create_bind_group_layout( + let constants_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { label: Some("iced_wgpu::triangle::solid bind group layout"), entries: &[wgpu::BindGroupLayoutEntry { @@ -443,13 +571,10 @@ mod solid { }, ); - let bind_group = - Self::bind_group(device, uniforms.raw(), &bind_group_layout); - let layout = device.create_pipeline_layout( &wgpu::PipelineLayoutDescriptor { label: Some("iced_wgpu::triangle::solid pipeline layout"), - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[&constants_layout], push_constant_ranges: &[], }, ); @@ -501,33 +626,9 @@ mod solid { Self { pipeline, - vertices, - uniforms, - bind_group_layout, - bind_group, + constants_layout, } } - - pub fn bind_group( - device: &wgpu::Device, - buffer: &wgpu::Buffer, - layout: &wgpu::BindGroupLayout, - ) -> wgpu::BindGroup { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::triangle::solid bind group"), - layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer, - offset: 0, - size: Some(Uniforms::min_size()), - }, - ), - }], - }) - } } } @@ -545,15 +646,90 @@ mod gradient { #[derive(Debug)] pub struct Pipeline { pub pipeline: wgpu::RenderPipeline, + pub constants_layout: wgpu::BindGroupLayout, + } + + #[derive(Debug)] + pub struct Layer { pub vertices: Buffer, pub uniforms: dynamic::Buffer, pub storage: dynamic::Buffer, + pub constants: wgpu::BindGroup, pub color_stop_offset: i32, //Need to store these and then write them all at once //or else they will be padded to 256 and cause gaps in the storage buffer pub color_stops_pending_write: Storage, - pub bind_group_layout: wgpu::BindGroupLayout, - pub bind_group: wgpu::BindGroup, + } + + impl Layer { + pub fn new( + device: &wgpu::Device, + constants_layout: &wgpu::BindGroupLayout, + ) -> Self { + let vertices = Buffer::new( + device, + "iced_wgpu::triangle::gradient vertex buffer", + wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + ); + + let uniforms = dynamic::Buffer::uniform( + device, + "iced_wgpu::triangle::gradient uniforms", + ); + + // Note: with a WASM target storage buffers are not supported. Will need to use UBOs & static + // sized array (eg like the 32-sized array on OpenGL side right now) to make gradients work + let storage = dynamic::Buffer::storage( + device, + "iced_wgpu::triangle::gradient storage", + ); + + let constants = Self::bind_group( + device, + uniforms.raw(), + storage.raw(), + constants_layout, + ); + + Self { + vertices, + uniforms, + storage, + constants, + color_stop_offset: 0, + color_stops_pending_write: Storage { + color_stops: vec![], + }, + } + } + + pub fn bind_group( + device: &wgpu::Device, + uniform_buffer: &wgpu::Buffer, + storage_buffer: &wgpu::Buffer, + layout: &wgpu::BindGroupLayout, + ) -> wgpu::BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("iced_wgpu::triangle::gradient bind group"), + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer( + wgpu::BufferBinding { + buffer: uniform_buffer, + offset: 0, + size: Some(Uniforms::min_size()), + }, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: storage_buffer.as_entire_binding(), + }, + ], + }) + } } #[derive(Debug, ShaderType)] @@ -584,25 +760,7 @@ mod gradient { format: wgpu::TextureFormat, antialiasing: Option, ) -> Self { - let vertices = Buffer::new( - device, - "iced_wgpu::triangle::gradient vertex buffer", - wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - ); - - let uniforms = dynamic::Buffer::uniform( - device, - "iced_wgpu::triangle::gradient uniforms", - ); - - //Note: with a WASM target storage buffers are not supported. Will need to use UBOs & static - // sized array (eg like the 32-sized array on OpenGL side right now) to make gradients work - let storage = dynamic::Buffer::storage( - device, - "iced_wgpu::triangle::gradient storage", - ); - - let bind_group_layout = device.create_bind_group_layout( + let constants_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { label: Some( "iced_wgpu::triangle::gradient bind group layout", @@ -634,19 +792,12 @@ mod gradient { }, ); - let bind_group = Pipeline::bind_group( - device, - uniforms.raw(), - storage.raw(), - &bind_group_layout, - ); - let layout = device.create_pipeline_layout( &wgpu::PipelineLayoutDescriptor { label: Some( "iced_wgpu::triangle::gradient pipeline layout", ), - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[&constants_layout], push_constant_ranges: &[], }, ); @@ -694,44 +845,8 @@ mod gradient { Self { pipeline, - vertices, - uniforms, - storage, - color_stop_offset: 0, - color_stops_pending_write: Storage { - color_stops: vec![], - }, - bind_group_layout, - bind_group, + constants_layout, } } - - pub fn bind_group( - device: &wgpu::Device, - uniform_buffer: &wgpu::Buffer, - storage_buffer: &wgpu::Buffer, - layout: &wgpu::BindGroupLayout, - ) -> wgpu::BindGroup { - device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("iced_wgpu::triangle::gradient bind group"), - layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer( - wgpu::BufferBinding { - buffer: uniform_buffer, - offset: 0, - size: Some(Uniforms::min_size()), - }, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: storage_buffer.as_entire_binding(), - }, - ], - }) - } } } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 50231f7c..6e1acc06 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -16,14 +16,11 @@ pub struct Compositor { adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, - staging_belt: wgpu::util::StagingBelt, format: wgpu::TextureFormat, theme: PhantomData, } impl Compositor { - const CHUNK_SIZE: u64 = 10 * 1024; - /// Requests a new [`Compositor`] with the given [`Settings`]. /// /// Returns `None` if no compatible graphics adapter could be found. @@ -98,15 +95,12 @@ impl Compositor { .next() .await?; - let staging_belt = wgpu::util::StagingBelt::new(Self::CHUNK_SIZE); - Some(Compositor { instance, settings, adapter, device, queue, - staging_belt, format, theme: PhantomData, }) @@ -228,7 +222,6 @@ impl iced_graphics::window::Compositor for Compositor { backend.present( &self.device, &self.queue, - &mut self.staging_belt, &mut encoder, view, primitives, @@ -238,13 +231,9 @@ impl iced_graphics::window::Compositor for Compositor { }); // Submit work - self.staging_belt.finish(); let _submission = self.queue.submit(Some(encoder.finish())); frame.present(); - // Recall staging buffers - self.staging_belt.recall(); - Ok(()) } Err(error) => match error { -- cgit From 730d6a07564d014c470e02f233394ec98325d463 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 00:47:16 +0100 Subject: Reuse a `RenderPass` as much as possible in `iced_wgpu` --- examples/integration/src/main.rs | 1 + wgpu/Cargo.toml | 5 +- wgpu/src/backend.rs | 261 +++++++++++++++++++++++++-------------- wgpu/src/image.rs | 10 +- wgpu/src/quad.rs | 10 +- wgpu/src/text.rs | 40 ++---- wgpu/src/triangle.rs | 10 +- wgpu/src/window/compositor.rs | 29 +---- 8 files changed, 196 insertions(+), 170 deletions(-) diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 5af30ab1..a7a90ced 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -250,6 +250,7 @@ pub fn main() { &device, &queue, &mut encoder, + None, &view, primitive, &viewport, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index b3fbfc00..7a6b75f0 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -59,8 +59,9 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" -git = "https://github.com/hecrj/glyphon.git" -rev = "3c2acb9dea5b9fcb0fa650b3c73b3a3242c62f4a" +# git = "https://github.com/hecrj/glyphon.git" +# rev = "3c2acb9dea5b9fcb0fa650b3c73b3a3242c62f4a" +path = "../../glyphon" [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 8c4c0daa..9d1e3d1c 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -5,8 +5,7 @@ use crate::{Settings, Transformation}; use iced_graphics::backend; use iced_graphics::layer::Layer; -use iced_graphics::{Primitive, Viewport}; -use iced_native::{Font, Size}; +use iced_graphics::{Color, Font, Primitive, Size, Viewport}; #[cfg(feature = "tracing")] use tracing::info_span; @@ -71,6 +70,7 @@ impl Backend { device: &wgpu::Device, queue: &wgpu::Queue, encoder: &mut wgpu::CommandEncoder, + clear_color: Option, frame: &wgpu::TextureView, primitives: &[Primitive], viewport: &Viewport, @@ -87,18 +87,25 @@ impl Backend { let mut layers = Layer::generate(primitives, viewport); layers.push(Layer::overlay(overlay_text, viewport)); - for layer in layers { - self.flush( - device, - queue, - scale_factor, - transformation, - &layer, - encoder, - frame, - target_size, - ); - } + self.prepare( + device, + queue, + encoder, + scale_factor, + transformation, + target_size, + &layers, + ); + + self.render( + device, + encoder, + frame, + clear_color, + scale_factor, + target_size, + &layers, + ); self.quad_pipeline.end_frame(); self.text_pipeline.end_frame(); @@ -108,90 +115,153 @@ impl Backend { self.image_pipeline.end_frame(device, queue, encoder); } - fn flush( + fn prepare( &mut self, device: &wgpu::Device, queue: &wgpu::Queue, + _encoder: &mut wgpu::CommandEncoder, scale_factor: f32, transformation: Transformation, - layer: &Layer<'_>, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, target_size: Size, + layers: &[Layer<'_>], ) { - let bounds = (layer.bounds * scale_factor).snap(); + for layer in layers { + let bounds = (layer.bounds * scale_factor).snap(); - if bounds.width < 1 || bounds.height < 1 { - return; - } + if bounds.width < 1 || bounds.height < 1 { + return; + } - if !layer.quads.is_empty() { - self.quad_pipeline.prepare( - device, - queue, - &layer.quads, - transformation, - scale_factor, - ); - - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::quad render pass"), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - }, - )], - depth_stencil_attachment: None, - }); + if !layer.quads.is_empty() { + self.quad_pipeline.prepare( + device, + queue, + &layer.quads, + transformation, + scale_factor, + ); + } - self.quad_pipeline.render(bounds, &mut render_pass); - } + if !layer.meshes.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, scale_factor); + + self.triangle_pipeline.prepare( + device, + queue, + &layer.meshes, + scaled, + ); + } - if !layer.meshes.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); - - self.triangle_pipeline.prepare( - device, - queue, - &layer.meshes, - scaled, - ); - - self.triangle_pipeline.render( - device, - encoder, - target, - target_size, - &layer.meshes, - scale_factor, - ); + #[cfg(any(feature = "image", feature = "svg"))] + { + if !layer.images.is_empty() { + let scaled = transformation + * Transformation::scale(scale_factor, 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, + ); + } } + } + fn render( + &mut self, + device: &wgpu::Device, + encoder: &mut wgpu::CommandEncoder, + target: &wgpu::TextureView, + clear_color: Option, + scale_factor: f32, + target_size: Size, + layers: &[Layer<'_>], + ) { + use std::mem::ManuallyDrop; + + let mut quad_layer = 0; + let mut triangle_layer = 0; #[cfg(any(feature = "image", feature = "svg"))] - { - if !layer.images.is_empty() { - let scaled = transformation - * Transformation::scale(scale_factor, scale_factor); + 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::quad 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] = + background_color.into_linear(); + + wgpu::Color { + r: f64::from(r), + g: f64::from(g), + b: f64::from(b), + a: f64::from(a), + } + }), + None => wgpu::LoadOp::Load, + }, + store: true, + }, + })], + depth_stencil_attachment: None, + }, + )); + + for layer in layers { + let bounds = (layer.bounds * scale_factor).snap(); - self.image_pipeline.prepare( + if bounds.width < 1 || bounds.height < 1 { + return; + } + + if !layer.quads.is_empty() { + self.quad_pipeline + .render(quad_layer, bounds, &mut render_pass); + + quad_layer += 1; + } + + if !layer.meshes.is_empty() { + let _ = ManuallyDrop::into_inner(render_pass); + + self.triangle_pipeline.render( device, - queue, encoder, - &layer.images, - scaled, + target, + triangle_layer, + target_size, + &layer.meshes, scale_factor, ); - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("iced_wgpu::image render pass"), + triangle_layer += 1; + + render_pass = ManuallyDrop::new(encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some("iced_wgpu::quad render pass"), color_attachments: &[Some( wgpu::RenderPassColorAttachment { view: target, @@ -203,24 +273,31 @@ impl Backend { }, )], depth_stencil_attachment: None, - }); + }, + )); + } - self.image_pipeline.render(bounds, &mut render_pass); + #[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.prepare( - device, - queue, - &layer.text, - layer.bounds, - scale_factor, - target_size, - ); - - self.text_pipeline.render(encoder, target); + if !layer.text.is_empty() { + self.text_pipeline.render(text_layer, &mut render_pass); + + text_layer += 1; + } } + + let _ = ManuallyDrop::into_inner(render_pass); } } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index 5d1ae8d7..db05d2ff 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -45,7 +45,6 @@ pub struct Pipeline { layers: Vec, prepare_layer: usize, - render_layer: usize, } #[derive(Debug)] @@ -321,7 +320,6 @@ impl Pipeline { layers: Vec::new(), prepare_layer: 0, - render_layer: 0, } } @@ -452,11 +450,12 @@ impl Pipeline { } pub fn render<'a>( - &'a mut self, + &'a self, + layer: usize, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, ) { - if let Some(layer) = self.layers.get(self.render_layer) { + if let Some(layer) = self.layers.get(layer) { render_pass.set_pipeline(&self.pipeline); render_pass.set_scissor_rect( @@ -474,8 +473,6 @@ impl Pipeline { render_pass.set_vertex_buffer(0, self.vertices.slice(..)); layer.render(render_pass); - - self.render_layer += 1; } } @@ -496,7 +493,6 @@ impl Pipeline { .trim(&mut self.texture_atlas, &mut (device, queue, encoder)); self.prepare_layer = 0; - self.render_layer = 0; } } diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index c1aa49c4..246cc5e1 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -17,7 +17,6 @@ pub struct Pipeline { indices: wgpu::Buffer, layers: Vec, prepare_layer: usize, - render_layer: usize, } impl Pipeline { @@ -140,7 +139,6 @@ impl Pipeline { indices, layers: Vec::new(), prepare_layer: 0, - render_layer: 0, } } @@ -163,11 +161,12 @@ impl Pipeline { } pub fn render<'a>( - &'a mut self, + &'a self, + layer: usize, bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, ) { - if let Some(layer) = self.layers.get(self.render_layer) { + if let Some(layer) = self.layers.get(layer) { render_pass.set_pipeline(&self.pipeline); render_pass.set_scissor_rect( @@ -184,14 +183,11 @@ impl Pipeline { render_pass.set_vertex_buffer(0, self.vertices.slice(..)); layer.draw(render_pass); - - self.render_layer += 1; } } pub fn end_frame(&mut self) { self.prepare_layer = 0; - self.render_layer = 0; } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 95994a4e..73708fd8 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -16,7 +16,7 @@ pub struct Pipeline { system: Option, renderers: Vec, atlas: glyphon::TextAtlas, - layer: usize, + prepare_layer: usize, } #[ouroboros::self_referencing] @@ -55,7 +55,7 @@ impl Pipeline { ), renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), - layer: 0, + prepare_layer: 0, } } @@ -88,12 +88,12 @@ impl Pipeline { target_size: Size, ) { self.system.as_mut().unwrap().with_mut(|fields| { - if self.renderers.len() <= self.layer { + if self.renderers.len() <= self.prepare_layer { self.renderers .push(glyphon::TextRenderer::new(device, queue)); } - let renderer = &mut self.renderers[self.layer]; + let renderer = &mut self.renderers[self.prepare_layer]; let keys: Vec<_> = sections .iter() @@ -179,35 +179,21 @@ impl Pipeline { &mut glyphon::SwashCache::new(fields.fonts), ) .expect("Prepare text sections"); + + self.prepare_layer += 1; }); } - pub fn render( - &mut self, - encoder: &mut wgpu::CommandEncoder, - target: &wgpu::TextureView, + pub fn render<'a>( + &'a self, + layer: usize, + render_pass: &mut wgpu::RenderPass<'a>, ) { - let mut render_pass = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: true, - }, - })], - depth_stencil_attachment: None, - }); - - let renderer = &mut self.renderers[self.layer]; + let renderer = &self.renderers[layer]; renderer - .render(&self.atlas, &mut render_pass) + .render(&self.atlas, render_pass) .expect("Render text"); - - self.layer += 1; } pub fn end_frame(&mut self) { @@ -216,7 +202,7 @@ impl Pipeline { .unwrap() .with_render_cache_mut(|cache| cache.trim()); - self.layer = 0; + self.prepare_layer = 0; } pub fn measure( diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 572af1e8..1b537bf4 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -22,7 +22,6 @@ pub struct Pipeline { layers: Vec, prepare_layer: usize, - render_layer: usize, } #[derive(Debug)] @@ -235,7 +234,7 @@ impl Layer { } fn render<'a>( - &'a mut self, + &'a self, solid: &'a solid::Pipeline, #[cfg(not(target_arch = "wasm32"))] gradient: &'a gradient::Pipeline, meshes: &[Mesh<'_>], @@ -331,7 +330,6 @@ impl Pipeline { layers: Vec::new(), prepare_layer: 0, - render_layer: 0, } } @@ -373,6 +371,7 @@ impl Pipeline { device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, target: &wgpu::TextureView, + layer: usize, target_size: Size, meshes: &[Mesh<'_>], scale_factor: f32, @@ -413,7 +412,7 @@ impl Pipeline { depth_stencil_attachment: None, }); - let layer = &mut self.layers[self.render_layer]; + let layer = &mut self.layers[layer]; layer.render( &self.solid, @@ -425,8 +424,6 @@ impl Pipeline { ); } - self.render_layer += 1; - if let Some(blit) = &mut self.blit { blit.draw(encoder, target); } @@ -434,7 +431,6 @@ impl Pipeline { pub fn end_frame(&mut self) { self.prepare_layer = 0; - self.render_layer = 0; } } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 6e1acc06..365cb603 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -190,39 +190,12 @@ impl iced_graphics::window::Compositor for Compositor { .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let _ = - encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some( - "iced_wgpu::window::Compositor render pass", - ), - color_attachments: &[Some( - wgpu::RenderPassColorAttachment { - view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear({ - let [r, g, b, a] = - background_color.into_linear(); - - wgpu::Color { - r: f64::from(r), - g: f64::from(g), - b: f64::from(b), - a: f64::from(a), - } - }), - store: true, - }, - }, - )], - depth_stencil_attachment: None, - }); - renderer.with_primitives(|backend, primitives| { backend.present( &self.device, &self.queue, &mut encoder, + Some(background_color), view, primitives, viewport, -- cgit From 21886d7e9cde17904719f1642f0b3af9791102ad Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 00:48:38 +0100 Subject: Use my GitHub fork of `glyphon` --- wgpu/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 7a6b75f0..46115a3c 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -59,9 +59,8 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" -# git = "https://github.com/hecrj/glyphon.git" -# rev = "3c2acb9dea5b9fcb0fa650b3c73b3a3242c62f4a" -path = "../../glyphon" +git = "https://github.com/hecrj/glyphon.git" +rev = "ccf19c67e8a4564263626bc6b86b6154540768c4" [dependencies.tracing] version = "0.1.6" -- cgit From ddbf93a82ff2ee0ca3265baf2f5b4442717b9101 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 01:23:40 +0100 Subject: Set scissoring properly in `text::Pipeline` --- wgpu/src/backend.rs | 3 ++- wgpu/src/text.rs | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 9d1e3d1c..d07898f7 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -291,7 +291,8 @@ impl Backend { } if !layer.text.is_empty() { - self.text_pipeline.render(text_layer, &mut render_pass); + self.text_pipeline + .render(text_layer, bounds, &mut render_pass); text_layer += 1; } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 73708fd8..655ad987 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -187,10 +187,18 @@ impl Pipeline { pub fn render<'a>( &'a self, layer: usize, + bounds: Rectangle, render_pass: &mut wgpu::RenderPass<'a>, ) { let renderer = &self.renderers[layer]; + render_pass.set_scissor_rect( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ); + renderer .render(&self.atlas, render_pass) .expect("Render text"); -- cgit From 05c787c2efbd8c8bc11925e1605b8b09ba744268 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 8 Feb 2023 23:21:04 +0100 Subject: Grow atlas in `text::Pipeline` when necessary --- wgpu/Cargo.toml | 2 +- wgpu/src/backend.rs | 55 +++++++++++++++++++++++++++++++++++++++-------------- wgpu/src/text.rs | 54 ++++++++++++++++++++++++++++++++++------------------ 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 46115a3c..6132768d 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -60,7 +60,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "ccf19c67e8a4564263626bc6b86b6154540768c4" +rev = "541efc5df644b1a25e96113f602a3f6803ce8a07" [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index d07898f7..7f44fafa 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -93,10 +93,17 @@ impl Backend { encoder, scale_factor, transformation, - target_size, &layers, ); + while !self.prepare_text( + device, + queue, + scale_factor, + target_size, + &layers, + ) {} + self.render( device, encoder, @@ -115,6 +122,38 @@ impl Backend { self.image_pipeline.end_frame(device, queue, encoder); } + fn prepare_text( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + scale_factor: f32, + target_size: Size, + layers: &[Layer<'_>], + ) -> bool { + for layer in layers { + let bounds = (layer.bounds * scale_factor).snap(); + + if bounds.width < 1 || bounds.height < 1 { + continue; + } + + if !layer.text.is_empty() { + if !self.text_pipeline.prepare( + device, + queue, + &layer.text, + layer.bounds, + scale_factor, + target_size, + ) { + return false; + } + } + } + + true + } + fn prepare( &mut self, device: &wgpu::Device, @@ -122,14 +161,13 @@ impl Backend { _encoder: &mut wgpu::CommandEncoder, scale_factor: f32, transformation: Transformation, - target_size: Size, layers: &[Layer<'_>], ) { for layer in layers { let bounds = (layer.bounds * scale_factor).snap(); if bounds.width < 1 || bounds.height < 1 { - return; + continue; } if !layer.quads.is_empty() { @@ -170,17 +208,6 @@ impl Backend { ); } } - - if !layer.text.is_empty() { - self.text_pipeline.prepare( - device, - queue, - &layer.text, - layer.bounds, - scale_factor, - target_size, - ); - } } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 655ad987..4406177a 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -86,7 +86,7 @@ impl Pipeline { bounds: Rectangle, scale_factor: f32, target_size: Size, - ) { + ) -> bool { self.system.as_mut().unwrap().with_mut(|fields| { if self.renderers.len() <= self.prepare_layer { self.renderers @@ -165,23 +165,39 @@ impl Pipeline { } }); - renderer - .prepare( - device, - queue, - &mut self.atlas, - glyphon::Resolution { - width: target_size.width, - height: target_size.height, - }, - text_areas, - glyphon::Color::rgb(0, 0, 0), - &mut glyphon::SwashCache::new(fields.fonts), - ) - .expect("Prepare text sections"); - - self.prepare_layer += 1; - }); + let result = renderer.prepare( + device, + queue, + &mut self.atlas, + glyphon::Resolution { + width: target_size.width, + height: target_size.height, + }, + text_areas, + glyphon::Color::rgb(0, 0, 0), + &mut glyphon::SwashCache::new(fields.fonts), + ); + + match result { + Ok(()) => { + self.prepare_layer += 1; + + true + } + Err(glyphon::PrepareError::AtlasFull(content_type)) => { + self.prepare_layer = 0; + + if self.atlas.grow(device, content_type) { + false + } else { + // If the atlas cannot grow, then all bets are off. + // Instead of panicking, we will just pray that the result + // will be somewhat readable... + true + } + } + } + }) } pub fn render<'a>( @@ -205,6 +221,8 @@ impl Pipeline { } pub fn end_frame(&mut self) { + self.atlas.trim(); + self.system .as_mut() .unwrap() -- cgit From 0715d7d0c781a4c7ead4dfdbf0f4656022df67e1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 9 Feb 2023 00:20:07 +0100 Subject: Update `glyphon` in `iced_wgpu` --- wgpu/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 6132768d..632873a3 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -60,7 +60,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "541efc5df644b1a25e96113f602a3f6803ce8a07" +rev = "65b481d758f50fd13fc21af2cc5ef62ddee64955" [dependencies.tracing] version = "0.1.6" -- cgit From 2097a56b58d8fe1784a625f69c1cdd38fcfb9a52 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 9 Feb 2023 06:54:51 +0100 Subject: Provide some margin to static buffers when growing --- wgpu/src/buffer/static.rs | 13 ++++++++----- wgpu/src/triangle.rs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/wgpu/src/buffer/static.rs b/wgpu/src/buffer/static.rs index 7ae7edd2..d8ae116e 100644 --- a/wgpu/src/buffer/static.rs +++ b/wgpu/src/buffer/static.rs @@ -2,8 +2,7 @@ use bytemuck::{Pod, Zeroable}; use std::marker::PhantomData; use std::mem; -//128 triangles/indices -const DEFAULT_STATIC_BUFFER_COUNT: wgpu::BufferAddress = 1_000; +const DEFAULT_COUNT: wgpu::BufferAddress = 128; /// A generic buffer struct useful for items which have no alignment requirements /// (e.g. Vertex, Index buffers) & no dynamic offsets. @@ -25,7 +24,7 @@ impl Buffer { label: &'static str, usages: wgpu::BufferUsages, ) -> Self { - let size = (mem::size_of::() as u64) * DEFAULT_STATIC_BUFFER_COUNT; + let size = (mem::size_of::() as u64) * DEFAULT_COUNT; Self { offsets: Vec::new(), @@ -57,9 +56,13 @@ impl Buffer { let size = (mem::size_of::() * new_count) as u64; if self.size < size { + self.size = + (mem::size_of::() * (new_count + new_count / 2)) as u64; + + self.gpu = + Self::gpu_buffer(device, self.label, self.size, self.usages); + self.offsets.clear(); - self.size = size; - self.gpu = Self::gpu_buffer(device, self.label, size, self.usages); true } else { false diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 1b537bf4..4b4fa16d 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -43,7 +43,7 @@ impl Layer { Self { index_buffer: Buffer::new( device, - "iced_wgpu::triangle vertex buffer", + "iced_wgpu::triangle index buffer", wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, ), index_strides: Vec::new(), -- cgit From 17a4d817c45da732e9fdc6559b6bbc7d5be94052 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 9 Feb 2023 07:00:21 +0100 Subject: Collapse conditional and please `clippy` --- wgpu/src/backend.rs | 10 +++++----- wgpu/src/text.rs | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 7f44fafa..e650d9a5 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -137,17 +137,17 @@ impl Backend { continue; } - if !layer.text.is_empty() { - if !self.text_pipeline.prepare( + if !layer.text.is_empty() + && !self.text_pipeline.prepare( device, queue, &layer.text, layer.bounds, scale_factor, target_size, - ) { - return false; - } + ) + { + return false; } } diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 4406177a..c0f49417 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -187,6 +187,7 @@ impl Pipeline { Err(glyphon::PrepareError::AtlasFull(content_type)) => { self.prepare_layer = 0; + #[allow(clippy::needless_bool)] if self.atlas.grow(device, content_type) { false } else { -- cgit From 51844c5d0c7f7c9b65c56330862b69f0baa0e3c1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 10 Feb 2023 18:52:35 +0100 Subject: Trim `Cache` every 300 frames in `text::Pipeline` --- wgpu/src/text.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index c0f49417..dea6ab18 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -316,6 +316,7 @@ struct Cache<'a> { entries: FxHashMap>, recently_used: FxHashSet, hasher: HashBuilder, + trim_count: usize, } #[cfg(not(target_arch = "wasm32"))] @@ -325,11 +326,14 @@ type HashBuilder = twox_hash::RandomXxHashBuilder64; type HashBuilder = std::hash::BuildHasherDefault; impl<'a> Cache<'a> { + const TRIM_INTERVAL: usize = 300; + fn new() -> Self { Self { entries: FxHashMap::default(), recently_used: FxHashSet::default(), hasher: HashBuilder::default(), + trim_count: 0, } } @@ -389,10 +393,16 @@ impl<'a> Cache<'a> { } fn trim(&mut self) { - self.entries - .retain(|key, _| self.recently_used.contains(key)); + if self.trim_count >= Self::TRIM_INTERVAL { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); - self.recently_used.clear(); + self.trim_count = 0; + } else { + self.trim_count += 1; + } } } -- cgit From 26e902f7d84290f8163a25c37488d29db4fc0708 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 10 Feb 2023 20:03:33 +0100 Subject: Compute grapheme index in `find_cursor_position` for `TextInput` --- native/src/text.rs | 13 +------------ native/src/widget/text_input.rs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/native/src/text.rs b/native/src/text.rs index 1bbd36cc..4c72abc3 100644 --- a/native/src/text.rs +++ b/native/src/text.rs @@ -1,6 +1,6 @@ //! Draw and interact with text. use crate::alignment; -use crate::{Color, Point, Rectangle, Size, Vector}; +use crate::{Color, Point, Rectangle, Size}; use std::borrow::Cow; @@ -34,10 +34,6 @@ pub struct Text<'a, Font> { pub enum Hit { /// The point was within the bounds of the returned character index. CharOffset(usize), - /// The provided point was not within the bounds of a glyph. The index - /// of the character with the closest centeroid position is returned, - /// as well as its delta. - NearestCharOffset(usize, Vector), } impl Hit { @@ -45,13 +41,6 @@ impl Hit { pub fn cursor(self) -> usize { match self { Self::CharOffset(i) => i, - Self::NearestCharOffset(i, delta) => { - if delta.x > f32::EPSILON { - i + 1 - } else { - i - } - } } } } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index f51416e1..65a9bd3b 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -1193,17 +1193,26 @@ where let size = size.unwrap_or_else(|| renderer.default_size()); let offset = offset(renderer, text_bounds, font, size, value, state); + let value = value.to_string(); - renderer + let char_offset = renderer .hit_test( - &value.to_string(), + &value, size, font, Size::INFINITY, Point::new(x + offset, text_bounds.height / 2.0), true, ) - .map(text::Hit::cursor) + .map(text::Hit::cursor)?; + + Some( + unicode_segmentation::UnicodeSegmentation::graphemes( + &value[..char_offset], + true, + ) + .count(), + ) } const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; -- cgit From a0597471b81b122b43e1bb90e43e1bcde1e8a892 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 10 Feb 2023 20:24:18 +0100 Subject: Remove `iced_glutin` and `iced_glow` leftovers --- Cargo.toml | 3 - glutin/Cargo.toml | 42 ---- glutin/README.md | 29 --- glutin/src/application.rs | 508 ---------------------------------------------- glutin/src/lib.rs | 33 --- src/lib.rs | 11 +- src/settings.rs | 12 -- winit/src/settings.rs | 8 - 8 files changed, 1 insertion(+), 645 deletions(-) delete mode 100644 glutin/Cargo.toml delete mode 100644 glutin/README.md delete mode 100644 glutin/src/application.rs delete mode 100644 glutin/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 42c9488c..5fc0cc10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ system = ["iced_winit/system"] # Enables chrome traces chrome-trace = [ "iced_winit/chrome-trace", - "iced_glutin?/trace", "iced_wgpu?/tracing", ] @@ -50,7 +49,6 @@ members = [ "core", "futures", "graphics", - "glutin", "lazy", "native", "style", @@ -65,7 +63,6 @@ iced_futures = { version = "0.6", path = "futures" } iced_native = { version = "0.9", path = "native" } iced_graphics = { version = "0.7", path = "graphics" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } -iced_glutin = { version = "0.7", path = "glutin", optional = true } thiserror = "1.0" [dependencies.image_rs] diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml deleted file mode 100644 index 10d3778b..00000000 --- a/glutin/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "iced_glutin" -version = "0.7.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -description = "A glutin runtime for Iced" -license = "MIT" -repository = "https://github.com/iced-rs/iced" -documentation = "https://docs.rs/iced_glutin" -keywords = ["gui", "ui", "graphics", "interface", "widgets"] -categories = ["gui"] - -[features] -trace = ["iced_winit/trace"] -debug = ["iced_winit/debug"] -system = ["iced_winit/system"] - -[dependencies] -log = "0.4" - -[dependencies.glutin] -version = "0.29" -git = "https://github.com/iced-rs/glutin" -rev = "da8d291486b4c9bec12487a46c119c4b1d386abf" - -[dependencies.iced_native] -version = "0.9" -path = "../native" - -[dependencies.iced_winit] -version = "0.8" -path = "../winit" -features = ["application"] - -[dependencies.iced_graphics] -version = "0.7" -path = "../graphics" -features = ["opengl"] - -[dependencies.tracing] -version = "0.1.6" -optional = true diff --git a/glutin/README.md b/glutin/README.md deleted file mode 100644 index 45e8ee6b..00000000 --- a/glutin/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# `iced_glutin` -[![Documentation](https://docs.rs/iced_glutin/badge.svg)][documentation] -[![Crates.io](https://img.shields.io/crates/v/iced_glutin.svg)](https://crates.io/crates/iced_glutin) -[![License](https://img.shields.io/crates/l/iced_glutin.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE) -[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd) - -`iced_glutin` offers some convenient abstractions on top of [`iced_native`] to quickstart development when using [`glutin`]. - -It exposes a renderer-agnostic `Application` trait that can be implemented and then run with a simple call. The use of this trait is optional. A `conversion` module is provided for users that decide to implement a custom event loop. - -

- The native target -

- -[documentation]: https://docs.rs/iced_glutin -[`iced_native`]: ../native -[`glutin`]: https://github.com/rust-windowing/glutin - -## Installation -Add `iced_glutin` as a dependency in your `Cargo.toml`: - -```toml -iced_glutin = "0.7" -``` - -__Iced moves fast and the `master` branch can contain breaking changes!__ If -you want to learn about a specific release, check out [the release list]. - -[the release list]: https://github.com/iced-rs/iced/releases diff --git a/glutin/src/application.rs b/glutin/src/application.rs deleted file mode 100644 index 5921bdd0..00000000 --- a/glutin/src/application.rs +++ /dev/null @@ -1,508 +0,0 @@ -//! Create interactive, native cross-platform applications. -use crate::mouse; -use crate::{Error, Executor, Runtime}; - -pub use iced_winit::application::StyleSheet; -pub use iced_winit::Application; - -use iced_graphics::window; -use iced_winit::application; -use iced_winit::conversion; -use iced_winit::futures; -use iced_winit::futures::channel::mpsc; -use iced_winit::renderer; -use iced_winit::time::Instant; -use iced_winit::user_interface; -use iced_winit::{Clipboard, Command, Debug, Event, Proxy, Settings}; - -use glutin::window::Window; -use std::mem::ManuallyDrop; - -#[cfg(feature = "tracing")] -use tracing::{info_span, instrument::Instrument}; - -/// Runs an [`Application`] with an executor, compositor, and the provided -/// settings. -pub fn run( - settings: Settings, - compositor_settings: C::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: window::GLCompositor + 'static, - ::Theme: StyleSheet, -{ - use futures::task; - use futures::Future; - use glutin::event_loop::EventLoopBuilder; - use glutin::platform::run_return::EventLoopExtRunReturn; - use glutin::ContextBuilder; - - #[cfg(feature = "trace")] - let _guard = iced_winit::Profiler::init(); - - let mut debug = Debug::new(); - debug.startup_started(); - - #[cfg(feature = "tracing")] - let _ = info_span!("Application::Glutin", "RUN").entered(); - - let mut event_loop = EventLoopBuilder::with_user_event().build(); - let proxy = event_loop.create_proxy(); - - let runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - let proxy = Proxy::new(event_loop.create_proxy()); - - Runtime::new(executor, proxy) - }; - - let (application, init_command) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - let context = { - let builder = settings.window.into_builder( - &application.title(), - event_loop.primary_monitor(), - settings.id, - ); - - log::debug!("Window builder: {:#?}", builder); - - let opengl_builder = ContextBuilder::new() - .with_vsync(true) - .with_multisampling(C::sample_count(&compositor_settings) as u16); - - let opengles_builder = opengl_builder.clone().with_gl( - glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (2, 0)), - ); - - let (first_builder, second_builder) = if settings.try_opengles_first { - (opengles_builder, opengl_builder) - } else { - (opengl_builder, opengles_builder) - }; - - log::info!("Trying first builder: {:#?}", first_builder); - - let context = first_builder - .build_windowed(builder.clone(), &event_loop) - .or_else(|_| { - log::info!("Trying second builder: {:#?}", second_builder); - second_builder.build_windowed(builder, &event_loop) - }) - .map_err(|error| { - use glutin::CreationError; - use iced_graphics::Error as ContextError; - - match error { - CreationError::Window(error) => { - Error::WindowCreationFailed(error) - } - CreationError::OpenGlVersionNotSupported => { - Error::GraphicsCreationFailed( - ContextError::VersionNotSupported, - ) - } - CreationError::NoAvailablePixelFormat => { - Error::GraphicsCreationFailed( - ContextError::NoAvailablePixelFormat, - ) - } - error => Error::GraphicsCreationFailed( - ContextError::BackendError(error.to_string()), - ), - } - })?; - - #[allow(unsafe_code)] - unsafe { - context.make_current().expect("Make OpenGL context current") - } - }; - - #[allow(unsafe_code)] - let (compositor, renderer) = unsafe { - C::new(compositor_settings, |address| { - context.get_proc_address(address) - })? - }; - - let (mut event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, mut control_receiver) = mpsc::unbounded(); - - let mut instance = Box::pin({ - let run_instance = run_instance::( - application, - compositor, - renderer, - runtime, - proxy, - debug, - event_receiver, - control_sender, - context, - init_command, - settings.exit_on_close_request, - ); - - #[cfg(feature = "tracing")] - let run_instance = - run_instance.instrument(info_span!("Application", "LOOP")); - - run_instance - }); - - let mut context = task::Context::from_waker(task::noop_waker_ref()); - - let _ = event_loop.run_return(move |event, _, control_flow| { - use glutin::event_loop::ControlFlow; - - if let ControlFlow::ExitWithCode(_) = control_flow { - return; - } - - let event = match event { - glutin::event::Event::WindowEvent { - event: - glutin::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - }, - window_id, - } => Some(glutin::event::Event::WindowEvent { - event: glutin::event::WindowEvent::Resized(*new_inner_size), - window_id, - }), - _ => event.to_static(), - }; - - if let Some(event) = event { - event_sender.start_send(event).expect("Send event"); - - let poll = instance.as_mut().poll(&mut context); - - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = control_receiver.try_next() { - *control_flow = flow; - } - } - task::Poll::Ready(_) => { - *control_flow = ControlFlow::Exit; - } - } - } - }); - - Ok(()) -} - -async fn run_instance( - mut application: A, - mut compositor: C, - mut renderer: A::Renderer, - mut runtime: Runtime, A::Message>, - mut proxy: glutin::event_loop::EventLoopProxy, - mut debug: Debug, - mut event_receiver: mpsc::UnboundedReceiver< - glutin::event::Event<'_, A::Message>, - >, - mut control_sender: mpsc::UnboundedSender, - mut context: glutin::ContextWrapper, - init_command: Command, - exit_on_close_request: bool, -) where - A: Application + 'static, - E: Executor + 'static, - C: window::GLCompositor + 'static, - ::Theme: StyleSheet, -{ - use glutin::event; - use glutin::event_loop::ControlFlow; - use iced_winit::futures::stream::StreamExt; - - let mut clipboard = Clipboard::connect(context.window()); - let mut cache = user_interface::Cache::default(); - let mut state = application::State::new(&application, context.window()); - let mut viewport_version = state.viewport_version(); - let mut should_exit = false; - - application::run_command( - &application, - &mut cache, - &state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - context.window(), - || compositor.fetch_information(), - ); - runtime.track(application.subscription()); - - let mut user_interface = - ManuallyDrop::new(application::build_user_interface( - &application, - user_interface::Cache::default(), - &mut renderer, - state.logical_size(), - &mut debug, - )); - - let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); - let mut messages = Vec::new(); - let mut redraw_pending = false; - - debug.startup_finished(); - - while let Some(event) = event_receiver.next().await { - match event { - event::Event::NewEvents(start_cause) => { - redraw_pending = matches!( - start_cause, - event::StartCause::Init - | event::StartCause::Poll - | event::StartCause::ResumeTimeReached { .. } - ); - } - event::Event::MainEventsCleared => { - if !redraw_pending && events.is_empty() && messages.is_empty() { - continue; - } - - debug.event_processing_started(); - - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor_position(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - debug.event_processing_finished(); - - for event in events.drain(..).zip(statuses.into_iter()) { - runtime.broadcast(event); - } - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - application::update( - &mut application, - &mut cache, - &state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut debug, - &mut messages, - context.window(), - || compositor.fetch_information(), - ); - - // Update window - state.synchronize(&application, context.window()); - - user_interface = - ManuallyDrop::new(application::build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - &mut debug, - )); - - if should_exit { - break; - } - } - - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = Event::Window( - crate::window::Event::RedrawRequested(Instant::now()), - ); - - let (interface_state, _) = user_interface.update( - &[redraw_event.clone()], - state.cursor_position(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - context.window().set_cursor_icon( - conversion::mouse_interaction(new_mouse_interaction), - ); - - mouse_interaction = new_mouse_interaction; - } - - context.window().request_redraw(); - runtime - .broadcast((redraw_event, crate::event::Status::Ignored)); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - crate::window::RedrawRequest::NextFrame => { - ControlFlow::Poll - } - crate::window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - redraw_pending = false; - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use iced_native::event; - events.push(iced_native::Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - )); - } - event::Event::UserEvent(message) => { - messages.push(message); - } - event::Event::RedrawRequested(_) => { - #[cfg(feature = "tracing")] - let _ = info_span!("Application", "FRAME").entered(); - - debug.render_started(); - - #[allow(unsafe_code)] - unsafe { - if !context.is_current() { - context = context - .make_current() - .expect("Make OpenGL context current"); - } - } - - let current_viewport_version = state.viewport_version(); - - if viewport_version != current_viewport_version { - let physical_size = state.physical_size(); - let logical_size = state.logical_size(); - - debug.layout_started(); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - debug.layout_finished(); - - debug.draw_started(); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor_position(), - ); - debug.draw_finished(); - - if new_mouse_interaction != mouse_interaction { - context.window().set_cursor_icon( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - mouse_interaction = new_mouse_interaction; - } - - context.resize(glutin::dpi::PhysicalSize::new( - physical_size.width, - physical_size.height, - )); - - compositor.resize_viewport(physical_size); - - viewport_version = current_viewport_version; - } - - compositor.present( - &mut renderer, - state.viewport(), - state.background_color(), - &debug.overlay(), - ); - - context.swap_buffers().expect("Swap buffers"); - - debug.render_finished(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - event::Event::WindowEvent { - event: window_event, - .. - } => { - if application::requests_exit(&window_event, state.modifiers()) - && exit_on_close_request - { - break; - } - - state.update(context.window(), &window_event, &mut debug); - - if let Some(event) = conversion::window_event( - &window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(event); - } - } - _ => {} - } - } - - // Manually drop the user interface - drop(ManuallyDrop::into_inner(user_interface)); -} diff --git a/glutin/src/lib.rs b/glutin/src/lib.rs deleted file mode 100644 index 33afd664..00000000 --- a/glutin/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! A windowing shell for [`iced`], on top of [`glutin`]. -//! -//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true) -//! -//! [`iced`]: https://github.com/iced-rs/iced -//! [`glutin`]: https://github.com/rust-windowing/glutin -#![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" -)] -#![deny( - missing_docs, - missing_debug_implementations, - unsafe_code, - unused_results, - clippy::extra_unused_lifetimes, - clippy::from_over_into, - clippy::needless_borrow, - clippy::new_without_default, - clippy::useless_conversion -)] -#![forbid(rust_2018_idioms)] -#![allow(clippy::inherent_to_string, clippy::type_complexity)] -#![cfg_attr(docsrs, feature(doc_cfg))] - -pub use glutin; - -#[doc(no_inline)] -pub use iced_winit::*; - -pub mod application; - -#[doc(no_inline)] -pub use application::Application; diff --git a/src/lib.rs b/src/lib.rs index 17c5ab97..31ec4f48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,17 +182,8 @@ pub mod touch; pub mod widget; pub mod window; -#[cfg(all(not(feature = "glow"), feature = "wgpu"))] -use iced_winit as runtime; - -#[cfg(feature = "glow")] -use iced_glutin as runtime; - -#[cfg(all(not(feature = "glow"), feature = "wgpu"))] use iced_wgpu as renderer; - -#[cfg(feature = "glow")] -use iced_glow as renderer; +use iced_winit as runtime; pub use iced_native::theme; pub use runtime::event; diff --git a/src/settings.rs b/src/settings.rs index ce07b0dd..13b3af7c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -49,15 +49,6 @@ pub struct Settings { /// /// [`Application`]: crate::Application pub exit_on_close_request: bool, - - /// Whether the [`Application`] should try to build the context - /// using OpenGL ES first then OpenGL. - /// - /// By default, it is disabled. - /// **Note:** Only works for the `glow` backend. - /// - /// [`Application`]: crate::Application - pub try_opengles_first: bool, } impl Settings { @@ -75,7 +66,6 @@ impl Settings { default_text_size: default_settings.default_text_size, antialiasing: default_settings.antialiasing, exit_on_close_request: default_settings.exit_on_close_request, - try_opengles_first: default_settings.try_opengles_first, } } } @@ -93,7 +83,6 @@ where default_text_size: 16.0, antialiasing: false, exit_on_close_request: true, - try_opengles_first: false, } } } @@ -105,7 +94,6 @@ impl From> for iced_winit::Settings { window: settings.window.into(), flags: settings.flags, exit_on_close_request: settings.exit_on_close_request, - try_opengles_first: settings.try_opengles_first, } } } diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 78d58000..5c64727b 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -51,14 +51,6 @@ pub struct Settings { /// /// [`Application`]: crate::Application pub exit_on_close_request: bool, - - /// Whether the [`Application`] should try to build the context - /// using OpenGL ES first then OpenGL. - /// - /// NOTE: Only works for the `glow` backend. - /// - /// [`Application`]: crate::Application - pub try_opengles_first: bool, } /// The window settings of an application. -- cgit From b2c87cdd238baa1562dc0422c00d1bf62132700f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 10 Feb 2023 22:36:02 +0100 Subject: Make `iced_wgpu` a mandatory dependency --- Cargo.toml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5fc0cc10..551e12ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,17 +12,14 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"] categories = ["gui"] [features] -default = ["wgpu"] # Enables the `Image` widget -image = ["iced_wgpu?/image", "image_rs"] +image = ["iced_wgpu/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_wgpu?/svg"] +svg = ["iced_wgpu/svg"] # Enables the `Canvas` widget canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget qr_code = ["iced_graphics/qr_code"] -# Enables the `iced_wgpu` renderer -wgpu = ["iced_wgpu"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -38,7 +35,7 @@ system = ["iced_winit/system"] # Enables chrome traces chrome-trace = [ "iced_winit/chrome-trace", - "iced_wgpu?/tracing", + "iced_wgpu/tracing", ] [badges] @@ -71,10 +68,10 @@ package = "image" optional = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_wgpu = { version = "0.9", path = "wgpu", optional = true } +iced_wgpu = { version = "0.9", path = "wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] -iced_wgpu = { version = "0.9", path = "wgpu", features = ["webgl"], optional = true } +iced_wgpu = { version = "0.9", path = "wgpu", features = ["webgl"] } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] -- cgit From 700262e05c76e003158acfeb8edd9f6b026d78cf Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 24 Feb 2023 13:56:37 +0100 Subject: Fix `checkbox` example --- examples/checkbox/fonts/icons.ttf | Bin 1272 -> 1784 bytes examples/checkbox/src/main.rs | 28 +++++++++++++++++++--------- native/src/widget/checkbox.rs | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/examples/checkbox/fonts/icons.ttf b/examples/checkbox/fonts/icons.ttf index a2046844..82f28481 100644 Binary files a/examples/checkbox/fonts/icons.ttf and b/examples/checkbox/fonts/icons.ttf differ diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index 09950bb8..77111490 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -1,10 +1,9 @@ +use iced::executor; +use iced::font::{self, Font}; use iced::widget::{checkbox, column, container}; -use iced::{Element, Font, Length, Sandbox, Settings}; +use iced::{Application, Command, Element, Length, Settings, Theme}; -const ICON_FONT: Font = Font::External { - name: "Icons", - bytes: include_bytes!("../fonts/icons.ttf"), -}; +const ICON_FONT: Font = Font::Name("icons"); pub fn main() -> iced::Result { Example::run(Settings::default()) @@ -20,24 +19,35 @@ struct Example { enum Message { DefaultChecked(bool), CustomChecked(bool), + FontLoaded(Result<(), font::Error>), } -impl Sandbox for Example { +impl Application for Example { type Message = Message; + type Flags = (); + type Executor = executor::Default; + type Theme = Theme; - fn new() -> Self { - Default::default() + fn new(_flags: Self::Flags) -> (Self, Command) { + ( + Self::default(), + font::load(include_bytes!("../fonts/icons.ttf").as_ref()) + .map(Message::FontLoaded), + ) } fn title(&self) -> String { String::from("Checkbox - Iced") } - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Command { match message { Message::DefaultChecked(value) => self.default_checkbox = value, Message::CustomChecked(value) => self.custom_checkbox = value, + Message::FontLoaded(_) => (), } + + Command::none() } fn view(&self) -> Element { diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 0e21e995..6ba06d3b 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -267,7 +267,7 @@ where code_point, size, } = &self.icon; - let size = size.map(f32::from).unwrap_or(bounds.height * 0.7); + let size = size.unwrap_or(bounds.height * 0.7); if self.is_checked { renderer.fill_text(text::Text { -- cgit From 8059c40142d601588e01c152ce1bb72a1295dde8 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 24 Feb 2023 13:58:17 +0100 Subject: Fix `clippy` lints --- native/src/overlay/menu.rs | 2 +- native/src/widget/checkbox.rs | 2 +- native/src/widget/pick_list.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index 0509f7bc..bd58a122 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -451,7 +451,7 @@ where let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let option_height = - (text_size * 1.2 + f32::from(self.padding.vertical())) as usize; + (text_size * 1.2 + self.padding.vertical()) as usize; let offset = viewport.y - bounds.y; let start = (offset / option_height as f32) as usize; diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 6ba06d3b..cd8b9c6b 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -272,7 +272,7 @@ where if self.is_checked { renderer.fill_text(text::Text { content: &code_point.to_string(), - font: font.clone(), + font: *font, size, bounds: Rectangle { x: bounds.center_x(), diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index b4cda748..8ff82f3b 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -600,12 +600,12 @@ pub fn draw<'a, T, Renderer>( font, code_point, size, - }) => Some((font.clone(), *code_point, *size)), + }) => Some((*font, *code_point, *size)), Handle::Dynamic { open, closed } => { if state().is_open { - Some((open.font.clone(), open.code_point, open.size)) + Some((open.font, open.code_point, open.size)) } else { - Some((closed.font.clone(), closed.code_point, closed.size)) + Some((closed.font, closed.code_point, closed.size)) } } Handle::None => None, -- cgit From 5100b5d0a1f654ec1254b7765ceadfb9091d6939 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 24 Feb 2023 23:24:48 +0100 Subject: Introduce `iced_renderer` subcrate featuring runtime renderer fallback --- Cargo.toml | 14 ++-- graphics/src/lib.rs | 2 +- graphics/src/renderer.rs | 7 +- graphics/src/window/gl_compositor.rs | 2 +- renderer/Cargo.toml | 26 +++++++ renderer/src/backend.rs | 94 +++++++++++++++++++++++ renderer/src/lib.rs | 17 +++++ renderer/src/settings.rs | 30 ++++++++ renderer/src/window.rs | 3 + renderer/src/window/compositor.rs | 96 ++++++++++++++++++++++++ src/application.rs | 4 +- src/lib.rs | 2 +- wgpu/src/window.rs | 3 +- wgpu/src/window/compositor.rs | 139 ++++++++++++++++++++++------------- winit/src/application.rs | 2 +- winit/src/system.rs | 2 +- 16 files changed, 371 insertions(+), 72 deletions(-) create mode 100644 renderer/Cargo.toml create mode 100644 renderer/src/backend.rs create mode 100644 renderer/src/lib.rs create mode 100644 renderer/src/settings.rs create mode 100644 renderer/src/window.rs create mode 100644 renderer/src/window/compositor.rs diff --git a/Cargo.toml b/Cargo.toml index 551e12ac..d999febe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,9 @@ categories = ["gui"] [features] # Enables the `Image` widget -image = ["iced_wgpu/image", "image_rs"] +image = ["iced_renderer/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_wgpu/svg"] +svg = ["iced_renderer/svg"] # Enables the `Canvas` widget canvas = ["iced_graphics/canvas"] # Enables the `QRCode` widget @@ -35,7 +35,7 @@ system = ["iced_winit/system"] # Enables chrome traces chrome-trace = [ "iced_winit/chrome-trace", - "iced_wgpu/tracing", + "iced_renderer/tracing", ] [badges] @@ -48,6 +48,7 @@ members = [ "graphics", "lazy", "native", + "renderer", "style", "wgpu", "winit", @@ -59,6 +60,7 @@ iced_core = { version = "0.8", path = "core" } iced_futures = { version = "0.6", path = "futures" } iced_native = { version = "0.9", path = "native" } iced_graphics = { version = "0.7", path = "graphics" } +iced_renderer = { version = "0.1", path = "renderer" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } thiserror = "1.0" @@ -67,12 +69,6 @@ version = "0.24" package = "image" optional = true -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -iced_wgpu = { version = "0.9", path = "wgpu" } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -iced_wgpu = { version = "0.9", path = "wgpu", features = ["webgl"] } - [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 41bef2c3..bbbdfa0e 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -46,9 +46,9 @@ pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; -pub use window::compositor; pub use iced_native::alignment; +pub use iced_native::text; pub use iced_native::{ Alignment, Background, Color, Font, Point, Rectangle, Size, Vector, }; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index b052c094..859ebc04 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -43,8 +43,11 @@ impl Renderer { /// Runs the given closure with the [`Backend`] and the recorded primitives /// of the [`Renderer`]. - pub fn with_primitives(&mut self, f: impl FnOnce(&mut B, &[Primitive])) { - f(&mut self.backend, &self.primitives); + pub fn with_primitives( + &mut self, + f: impl FnOnce(&mut B, &[Primitive]) -> O, + ) -> O { + f(&mut self.backend, &self.primitives) } } diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs index a45a7ca1..3e6dfd9e 100644 --- a/graphics/src/window/gl_compositor.rs +++ b/graphics/src/window/gl_compositor.rs @@ -1,6 +1,6 @@ //! A compositor is responsible for initializing a renderer and managing window //! surfaces. -use crate::compositor::Information; +use crate::window::compositor::Information; use crate::{Color, Error, Size, Viewport}; use core::ffi::c_void; diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml new file mode 100644 index 00000000..2a179f3a --- /dev/null +++ b/renderer/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "iced_renderer" +version = "0.1.0" +edition = "2021" + +[features] +image = ["iced_wgpu/image"] +svg = ["iced_wgpu/svg"] +tracing = ["iced_wgpu/tracing"] + +[dependencies] +raw-window-handle = "0.5" + +[dependencies.iced_native] +version = "0.9" +path = "../native" + +[dependencies.iced_graphics] +version = "0.7" +path = "../graphics" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +iced_wgpu = { version = "0.9", path = "../wgpu" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] } diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs new file mode 100644 index 00000000..a9a09593 --- /dev/null +++ b/renderer/src/backend.rs @@ -0,0 +1,94 @@ +use crate::{Font, Point, Size}; + +use iced_graphics::backend; +use iced_graphics::text; + +use std::borrow::Cow; + +pub enum Backend { + Wgpu(iced_wgpu::Backend), +} + +impl iced_graphics::Backend for Backend {} + +impl backend::Text for Backend { + const ICON_FONT: Font = Font::Name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Font { + match self { + Self::Wgpu(backend) => backend.default_font(), + } + } + + fn default_size(&self) -> f32 { + match self { + Self::Wgpu(backend) => backend.default_size(), + } + } + + fn measure( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + match self { + Self::Wgpu(backend) => { + backend.measure(contents, size, font, bounds) + } + } + } + + fn hit_test( + &self, + contents: &str, + size: f32, + font: Font, + bounds: Size, + position: Point, + nearest_only: bool, + ) -> Option { + match self { + Self::Wgpu(backend) => backend.hit_test( + contents, + size, + font, + bounds, + position, + nearest_only, + ), + } + } + + fn load_font(&mut self, font: Cow<'static, [u8]>) { + match self { + Self::Wgpu(backend) => { + backend.load_font(font); + } + } + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { + match self { + Self::Wgpu(backend) => backend.dimensions(handle), + } + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + handle: &iced_native::svg::Handle, + ) -> Size { + match self { + Self::Wgpu(backend) => backend.viewport_dimensions(handle), + } + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs new file mode 100644 index 00000000..d0ba6793 --- /dev/null +++ b/renderer/src/lib.rs @@ -0,0 +1,17 @@ +pub mod window; + +mod backend; +mod settings; + +pub use backend::Backend; +pub use settings::Settings; + +pub use iced_graphics::{ + Antialiasing, Color, Error, Font, Point, Size, Viewport, +}; + +/// The default graphics renderer for [`iced`]. +/// +/// [`iced`]: https://github.com/iced-rs/iced +pub type Renderer = + iced_graphics::Renderer; diff --git a/renderer/src/settings.rs b/renderer/src/settings.rs new file mode 100644 index 00000000..c4dc248b --- /dev/null +++ b/renderer/src/settings.rs @@ -0,0 +1,30 @@ +use crate::{Antialiasing, Font}; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: f32, + + /// The antialiasing strategy that will be used for triangle primitives. + /// + /// By default, it is `None`. + pub antialiasing: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::SansSerif, + default_text_size: 16.0, + antialiasing: None, + } + } +} diff --git a/renderer/src/window.rs b/renderer/src/window.rs new file mode 100644 index 00000000..a7c8911b --- /dev/null +++ b/renderer/src/window.rs @@ -0,0 +1,3 @@ +mod compositor; + +pub use compositor::Compositor; diff --git a/renderer/src/window/compositor.rs b/renderer/src/window/compositor.rs new file mode 100644 index 00000000..ad78d8bf --- /dev/null +++ b/renderer/src/window/compositor.rs @@ -0,0 +1,96 @@ +use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; + +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + +pub use iced_graphics::window::compositor::{Information, SurfaceError}; + +pub enum Compositor { + Wgpu(iced_wgpu::window::Compositor), +} + +pub enum Surface { + Wgpu(iced_wgpu::window::Surface), +} + +impl iced_graphics::window::Compositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + type Surface = Surface; + + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + let (compositor, backend) = iced_wgpu::window::compositor::new( + iced_wgpu::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + ..iced_wgpu::Settings::from_env() + }, + compatible_window, + )?; + + Ok(( + Self::Wgpu(compositor), + Renderer::new(Backend::Wgpu(backend)), + )) + } + + fn create_surface( + &mut self, + window: &W, + ) -> Surface { + match self { + Self::Wgpu(compositor) => { + Surface::Wgpu(compositor.create_surface(window)) + } + } + } + + fn configure_surface( + &mut self, + surface: &mut Surface, + width: u32, + height: u32, + ) { + match (self, surface) { + (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { + compositor.configure_surface(surface, width, height); + } + } + } + + fn fetch_information(&self) -> Information { + match self { + Self::Wgpu(compositor) => compositor.fetch_information(), + } + } + + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + renderer.with_primitives(|backend, primitives| { + match (self, backend, surface) { + ( + Self::Wgpu(compositor), + Backend::Wgpu(backend), + Surface::Wgpu(surface), + ) => iced_wgpu::window::compositor::present( + compositor, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ), + } + }) + } +} diff --git a/src/application.rs b/src/application.rs index 9a1c1855..b9871556 100644 --- a/src/application.rs +++ b/src/application.rs @@ -198,11 +198,11 @@ pub trait Application: Sized { default_font: settings.default_font, default_text_size: settings.default_text_size, antialiasing: if settings.antialiasing { - Some(crate::renderer::settings::Antialiasing::MSAAx4) + Some(crate::renderer::Antialiasing::MSAAx4) } else { None }, - ..crate::renderer::Settings::from_env() + ..crate::renderer::Settings::default() }; Ok(crate::runtime::application::run::< diff --git a/src/lib.rs b/src/lib.rs index 31ec4f48..bb162f2d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,7 +182,7 @@ pub mod touch; pub mod widget; pub mod window; -use iced_wgpu as renderer; +use iced_renderer as renderer; use iced_winit as runtime; pub use iced_native::theme; diff --git a/wgpu/src/window.rs b/wgpu/src/window.rs index aac5fb9e..9545a14e 100644 --- a/wgpu/src/window.rs +++ b/wgpu/src/window.rs @@ -1,4 +1,5 @@ //! Display rendering results on windows. -mod compositor; +pub mod compositor; pub use compositor::Compositor; +pub use wgpu::Surface; diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 365cb603..7406bfb8 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -1,8 +1,9 @@ -use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; +//! Connect a window with a renderer. +use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport}; use futures::stream::{self, StreamExt}; -use iced_graphics::compositor; +use iced_graphics::window::compositor; use iced_native::futures; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; @@ -112,6 +113,77 @@ impl Compositor { } } +/// Creates a [`Compositor`] and its [`Backend`] for the given [`Settings`] and +/// window. +pub fn new( + settings: Settings, + compatible_window: Option<&W>, +) -> Result<(Compositor, Backend), Error> { + let compositor = futures::executor::block_on(Compositor::request( + settings, + compatible_window, + )) + .ok_or(Error::GraphicsAdapterNotFound)?; + + let backend = compositor.create_backend(); + + Ok((compositor, backend)) +} + +/// Presents the given primitives with the given [`Compositor`] and [`Backend`]. +pub fn present>( + compositor: &mut Compositor, + backend: &mut Backend, + surface: &mut wgpu::Surface, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], +) -> Result<(), compositor::SurfaceError> { + match surface.get_current_texture() { + Ok(frame) => { + let mut encoder = compositor.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some("iced_wgpu encoder"), + }, + ); + + let view = &frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + backend.present( + &compositor.device, + &compositor.queue, + &mut encoder, + Some(background_color), + view, + primitives, + viewport, + overlay, + ); + + // Submit work + let _submission = compositor.queue.submit(Some(encoder.finish())); + frame.present(); + + Ok(()) + } + Err(error) => match error { + wgpu::SurfaceError::Timeout => { + Err(compositor::SurfaceError::Timeout) + } + wgpu::SurfaceError::Outdated => { + Err(compositor::SurfaceError::Outdated) + } + wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), + wgpu::SurfaceError::OutOfMemory => { + Err(compositor::SurfaceError::OutOfMemory) + } + }, + } +} + impl iced_graphics::window::Compositor for Compositor { type Settings = Settings; type Renderer = Renderer; @@ -121,13 +193,7 @@ impl iced_graphics::window::Compositor for Compositor { settings: Self::Settings, compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error> { - let compositor = futures::executor::block_on(Self::request( - settings, - compatible_window, - )) - .ok_or(Error::GraphicsAdapterNotFound)?; - - let backend = compositor.create_backend(); + let (compositor, backend) = new(settings, compatible_window)?; Ok((compositor, Renderer::new(backend))) } @@ -178,49 +244,16 @@ impl iced_graphics::window::Compositor for Compositor { background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { - match surface.get_current_texture() { - Ok(frame) => { - let mut encoder = self.device.create_command_encoder( - &wgpu::CommandEncoderDescriptor { - label: Some("iced_wgpu encoder"), - }, - ); - - let view = &frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - - renderer.with_primitives(|backend, primitives| { - backend.present( - &self.device, - &self.queue, - &mut encoder, - Some(background_color), - view, - primitives, - viewport, - overlay, - ); - }); - - // Submit work - let _submission = self.queue.submit(Some(encoder.finish())); - frame.present(); - - Ok(()) - } - Err(error) => match error { - wgpu::SurfaceError::Timeout => { - Err(compositor::SurfaceError::Timeout) - } - wgpu::SurfaceError::Outdated => { - Err(compositor::SurfaceError::Outdated) - } - wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost), - wgpu::SurfaceError::OutOfMemory => { - Err(compositor::SurfaceError::OutOfMemory) - } - }, - } + renderer.with_primitives(|backend, primitives| { + present( + self, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ) + }) } } diff --git a/winit/src/application.rs b/winit/src/application.rs index 889becad..1bfce3a1 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -17,8 +17,8 @@ use crate::{ use iced_futures::futures; use iced_futures::futures::channel::mpsc; -use iced_graphics::compositor; use iced_graphics::window; +use iced_graphics::window::compositor; use iced_native::program::Program; use iced_native::time::Instant; use iced_native::user_interface::{self, UserInterface}; diff --git a/winit/src/system.rs b/winit/src/system.rs index 619086b8..8a7b2a11 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -2,7 +2,7 @@ use crate::command::{self, Command}; pub use iced_native::system::*; -use iced_graphics::compositor; +use iced_graphics::window::compositor; /// Query for available system information. pub fn fetch_information( -- cgit From a01bc865a0561ff1daf255c4746746acf67524f0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 15:11:35 +0100 Subject: Trim measurements in `renderer::Backend` --- renderer/src/backend.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index a9a09593..7b09eea8 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -9,7 +9,13 @@ pub enum Backend { Wgpu(iced_wgpu::Backend), } -impl iced_graphics::Backend for Backend {} +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + match self { + Self::Wgpu(backend) => backend.trim_measurements(), + } + } +} impl backend::Text for Backend { const ICON_FONT: Font = Font::Name("Iced-Icons"); -- cgit From 8c373cd497e370d356b480380482779397bdb510 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 15:38:25 +0100 Subject: Scaffold `iced_tiny_skia` and connect it to `iced_renderer` --- Cargo.toml | 1 + renderer/Cargo.toml | 8 +++- renderer/src/backend.rs | 20 ++++++++ renderer/src/window/compositor.rs | 54 +++++++++++++++++---- tiny_skia/Cargo.toml | 21 +++++++++ tiny_skia/src/backend.rs | 87 ++++++++++++++++++++++++++++++++++ tiny_skia/src/lib.rs | 16 +++++++ tiny_skia/src/settings.rs | 24 ++++++++++ tiny_skia/src/window.rs | 3 ++ tiny_skia/src/window/compositor.rs | 96 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 tiny_skia/Cargo.toml create mode 100644 tiny_skia/src/backend.rs create mode 100644 tiny_skia/src/lib.rs create mode 100644 tiny_skia/src/settings.rs create mode 100644 tiny_skia/src/window.rs create mode 100644 tiny_skia/src/window/compositor.rs diff --git a/Cargo.toml b/Cargo.toml index d999febe..b1f5cf66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ members = [ "native", "renderer", "style", + "tiny_skia", "wgpu", "winit", "examples/*", diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 2a179f3a..5ba5d426 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [features] -image = ["iced_wgpu/image"] -svg = ["iced_wgpu/svg"] +image = ["iced_wgpu/image", "iced_tiny_skia/image"] +svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] tracing = ["iced_wgpu/tracing"] [dependencies] @@ -19,6 +19,10 @@ path = "../native" version = "0.7" path = "../graphics" +[dependencies.iced_tiny_skia] +version = "0.1" +path = "../tiny_skia" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] iced_wgpu = { version = "0.9", path = "../wgpu" } diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index 7b09eea8..a46d6f9b 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -7,12 +7,14 @@ use std::borrow::Cow; pub enum Backend { Wgpu(iced_wgpu::Backend), + TinySkia(iced_tiny_skia::Backend), } impl iced_graphics::Backend for Backend { fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), + Self::TinySkia(backend) => backend.trim_measurements(), } } } @@ -25,12 +27,14 @@ impl backend::Text for Backend { fn default_font(&self) -> Font { match self { Self::Wgpu(backend) => backend.default_font(), + Self::TinySkia(backend) => backend.default_font(), } } fn default_size(&self) -> f32 { match self { Self::Wgpu(backend) => backend.default_size(), + Self::TinySkia(backend) => backend.default_size(), } } @@ -45,6 +49,9 @@ impl backend::Text for Backend { Self::Wgpu(backend) => { backend.measure(contents, size, font, bounds) } + Self::TinySkia(backend) => { + backend.measure(contents, size, font, bounds) + } } } @@ -66,6 +73,14 @@ impl backend::Text for Backend { position, nearest_only, ), + Self::TinySkia(backend) => backend.hit_test( + contents, + size, + font, + bounds, + position, + nearest_only, + ), } } @@ -74,6 +89,9 @@ impl backend::Text for Backend { Self::Wgpu(backend) => { backend.load_font(font); } + Self::TinySkia(backend) => { + backend.load_font(font); + } } } } @@ -83,6 +101,7 @@ impl backend::Image for Backend { fn dimensions(&self, handle: &iced_native::image::Handle) -> Size { match self { Self::Wgpu(backend) => backend.dimensions(handle), + Self::TinySkia(backend) => backend.dimensions(handle), } } } @@ -95,6 +114,7 @@ impl backend::Svg for Backend { ) -> Size { match self { Self::Wgpu(backend) => backend.viewport_dimensions(handle), + Self::TinySkia(backend) => backend.viewport_dimensions(handle), } } } diff --git a/renderer/src/window/compositor.rs b/renderer/src/window/compositor.rs index ad78d8bf..42afddc4 100644 --- a/renderer/src/window/compositor.rs +++ b/renderer/src/window/compositor.rs @@ -6,10 +6,12 @@ pub use iced_graphics::window::compositor::{Information, SurfaceError}; pub enum Compositor { Wgpu(iced_wgpu::window::Compositor), + TinySkia(iced_tiny_skia::window::Compositor), } pub enum Surface { Wgpu(iced_wgpu::window::Surface), + TinySkia(iced_tiny_skia::window::Surface), } impl iced_graphics::window::Compositor for Compositor { @@ -19,21 +21,31 @@ impl iced_graphics::window::Compositor for Compositor { fn new( settings: Self::Settings, - compatible_window: Option<&W>, + _compatible_window: Option<&W>, ) -> Result<(Self, Self::Renderer), Error> { - let (compositor, backend) = iced_wgpu::window::compositor::new( - iced_wgpu::Settings { + //let (compositor, backend) = iced_wgpu::window::compositor::new( + // iced_wgpu::Settings { + // default_font: settings.default_font, + // default_text_size: settings.default_text_size, + // antialiasing: settings.antialiasing, + // ..iced_wgpu::Settings::from_env() + // }, + // compatible_window, + //)?; + + //Ok(( + // Self::Wgpu(compositor), + // Renderer::new(Backend::Wgpu(backend)), + //)) + let (compositor, backend) = + iced_tiny_skia::window::compositor::new(iced_tiny_skia::Settings { default_font: settings.default_font, default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - ..iced_wgpu::Settings::from_env() - }, - compatible_window, - )?; + }); Ok(( - Self::Wgpu(compositor), - Renderer::new(Backend::Wgpu(backend)), + Self::TinySkia(compositor), + Renderer::new(Backend::TinySkia(backend)), )) } @@ -45,6 +57,9 @@ impl iced_graphics::window::Compositor for Compositor { Self::Wgpu(compositor) => { Surface::Wgpu(compositor.create_surface(window)) } + Self::TinySkia(compositor) => { + Surface::TinySkia(compositor.create_surface(window)) + } } } @@ -58,12 +73,17 @@ impl iced_graphics::window::Compositor for Compositor { (Self::Wgpu(compositor), Surface::Wgpu(surface)) => { compositor.configure_surface(surface, width, height); } + (Self::TinySkia(compositor), Surface::TinySkia(surface)) => { + compositor.configure_surface(surface, width, height); + } + _ => unreachable!(), } } fn fetch_information(&self) -> Information { match self { Self::Wgpu(compositor) => compositor.fetch_information(), + Self::TinySkia(compositor) => compositor.fetch_information(), } } @@ -90,6 +110,20 @@ impl iced_graphics::window::Compositor for Compositor { background_color, overlay, ), + ( + Self::TinySkia(compositor), + Backend::TinySkia(backend), + Surface::TinySkia(surface), + ) => iced_tiny_skia::window::compositor::present( + compositor, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ), + _ => unreachable!(), } }) } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml new file mode 100644 index 00000000..7f4b0f8c --- /dev/null +++ b/tiny_skia/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "iced_tiny_skia" +version = "0.1.0" +edition = "2021" + +[features] +image = [] +svg = [] + +[dependencies] +raw-window-handle = "0.5" +softbuffer = "0.2" +tiny-skia = "0.8" + +[dependencies.iced_native] +version = "0.9" +path = "../native" + +[dependencies.iced_graphics] +version = "0.7" +path = "../graphics" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs new file mode 100644 index 00000000..4282a745 --- /dev/null +++ b/tiny_skia/src/backend.rs @@ -0,0 +1,87 @@ +use crate::{Font, Settings, Size}; + +use iced_graphics::backend; +use iced_graphics::text; + +use std::borrow::Cow; + +pub struct Backend { + default_font: Font, + default_text_size: f32, +} + +impl Backend { + pub fn new(settings: Settings) -> Self { + Self { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + } + } +} + +impl iced_graphics::Backend for Backend { + fn trim_measurements(&mut self) { + // TODO + } +} + +impl backend::Text for Backend { + const ICON_FONT: Font = Font::Name("Iced-Icons"); + const CHECKMARK_ICON: char = '\u{f00c}'; + const ARROW_DOWN_ICON: char = '\u{e800}'; + + fn default_font(&self) -> Font { + self.default_font + } + + fn default_size(&self) -> f32 { + self.default_text_size + } + + fn measure( + &self, + _contents: &str, + _size: f32, + _font: Font, + _bounds: Size, + ) -> (f32, f32) { + // TODO + (0.0, 0.0) + } + + fn hit_test( + &self, + _contents: &str, + _size: f32, + _font: Font, + _bounds: Size, + _point: iced_native::Point, + _nearest_only: bool, + ) -> Option { + // TODO + None + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) { + // TODO + } +} + +#[cfg(feature = "image")] +impl backend::Image for Backend { + fn dimensions(&self, _handle: &iced_native::image::Handle) -> Size { + // TODO + Size::new(0, 0) + } +} + +#[cfg(feature = "svg")] +impl backend::Svg for Backend { + fn viewport_dimensions( + &self, + _handle: &iced_native::svg::Handle, + ) -> Size { + // TODO + Size::new(0, 0) + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs new file mode 100644 index 00000000..fce44e9e --- /dev/null +++ b/tiny_skia/src/lib.rs @@ -0,0 +1,16 @@ +pub mod window; + +mod backend; +mod settings; + +pub use backend::Backend; +pub use settings::Settings; + +pub use iced_graphics::{Color, Error, Font, Point, Size, Vector, 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; diff --git a/tiny_skia/src/settings.rs b/tiny_skia/src/settings.rs new file mode 100644 index 00000000..88098345 --- /dev/null +++ b/tiny_skia/src/settings.rs @@ -0,0 +1,24 @@ +use crate::Font; + +/// The settings of a [`Backend`]. +/// +/// [`Backend`]: crate::Backend +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Settings { + /// The default [`Font`] to use. + pub default_font: Font, + + /// The default size of text. + /// + /// By default, it will be set to `16.0`. + pub default_text_size: f32, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + default_font: Font::SansSerif, + default_text_size: 16.0, + } + } +} diff --git a/tiny_skia/src/window.rs b/tiny_skia/src/window.rs new file mode 100644 index 00000000..d8d9378e --- /dev/null +++ b/tiny_skia/src/window.rs @@ -0,0 +1,3 @@ +pub mod compositor; + +pub use compositor::{Compositor, Surface}; diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs new file mode 100644 index 00000000..053bb29b --- /dev/null +++ b/tiny_skia/src/window/compositor.rs @@ -0,0 +1,96 @@ +use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; + +use iced_graphics::window::compositor::{self, Information, SurfaceError}; +use iced_graphics::Primitive; + +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use std::marker::PhantomData; + +pub struct Compositor { + _theme: PhantomData, +} + +pub struct Surface; + +impl iced_graphics::window::Compositor for Compositor { + type Settings = Settings; + type Renderer = Renderer; + type Surface = Surface; + + fn new( + settings: Self::Settings, + _compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error> { + let (compositor, backend) = new(settings); + + Ok((compositor, Renderer::new(backend))) + } + + fn create_surface( + &mut self, + _window: &W, + ) -> Surface { + // TODO + Surface + } + + fn configure_surface( + &mut self, + _surface: &mut Surface, + _width: u32, + _height: u32, + ) { + // TODO + } + + fn fetch_information(&self) -> Information { + Information { + adapter: String::from("CPU"), + backend: String::from("tiny-skia"), + } + } + + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError> { + renderer.with_primitives(|backend, primitives| { + present( + self, + backend, + surface, + primitives, + viewport, + background_color, + overlay, + ) + }) + } +} + +pub fn new(settings: Settings) -> (Compositor, Backend) { + // TODO + ( + Compositor { + _theme: PhantomData, + }, + Backend::new(settings), + ) +} + +pub fn present>( + _compositor: &mut Compositor, + _backend: &mut Backend, + _surface: &mut Surface, + _primitives: &[Primitive], + _viewport: &Viewport, + _background_color: Color, + _overlay: &[T], +) -> Result<(), compositor::SurfaceError> { + // TODO + Ok(()) +} -- cgit From 535d7a4d57e131e661587b36e41820dd6ccccc3e Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 16:05:42 +0100 Subject: Implement basic presentation with `softbuffer` for `iced_tiny_skia` --- graphics/src/window/compositor.rs | 2 ++ renderer/src/window/compositor.rs | 10 +++--- tiny_skia/src/backend.rs | 19 ++++++++++- tiny_skia/src/window/compositor.rs | 66 ++++++++++++++++++++++++++++++-------- wgpu/src/window/compositor.rs | 10 ++++-- winit/src/application.rs | 13 +++----- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs index db4ba45d..15f8dab5 100644 --- a/graphics/src/window/compositor.rs +++ b/graphics/src/window/compositor.rs @@ -28,6 +28,8 @@ pub trait Compositor: Sized { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> Self::Surface; /// Configures a new [`Surface`] with the given dimensions. diff --git a/renderer/src/window/compositor.rs b/renderer/src/window/compositor.rs index 42afddc4..a11374ed 100644 --- a/renderer/src/window/compositor.rs +++ b/renderer/src/window/compositor.rs @@ -52,14 +52,16 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> Surface { match self { Self::Wgpu(compositor) => { - Surface::Wgpu(compositor.create_surface(window)) - } - Self::TinySkia(compositor) => { - Surface::TinySkia(compositor.create_surface(window)) + Surface::Wgpu(compositor.create_surface(window, width, height)) } + Self::TinySkia(compositor) => Surface::TinySkia( + compositor.create_surface(window, width, height), + ), } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 4282a745..62373ec7 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,7 +1,8 @@ -use crate::{Font, Settings, Size}; +use crate::{Color, Font, Settings, Size, Viewport}; use iced_graphics::backend; use iced_graphics::text; +use iced_graphics::Primitive; use std::borrow::Cow; @@ -17,6 +18,22 @@ impl Backend { default_text_size: settings.default_text_size, } } + + pub fn draw>( + &mut self, + pixels: &mut tiny_skia::Pixmap, + _primitives: &[Primitive], + _viewport: &Viewport, + background_color: Color, + _overlay: &[T], + ) { + pixels.fill(into_color(background_color)); + } +} + +fn into_color(color: Color) -> tiny_skia::Color { + tiny_skia::Color::from_rgba(color.r, color.g, color.b, color.a) + .expect("Convert color from iced to tiny_skia") } impl iced_graphics::Backend for Backend { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 053bb29b..164d99c6 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -10,7 +10,11 @@ pub struct Compositor { _theme: PhantomData, } -pub struct Surface; +pub struct Surface { + window: softbuffer::GraphicsContext, + pixels: tiny_skia::Pixmap, + buffer: Vec, +} impl iced_graphics::window::Compositor for Compositor { type Settings = Settings; @@ -28,19 +32,33 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, - _window: &W, + window: &W, + width: u32, + height: u32, ) -> Surface { - // TODO - Surface + let window = + unsafe { softbuffer::GraphicsContext::new(window, window) } + .expect("Create softbuffer for window"); + + let pixels = tiny_skia::Pixmap::new(width, height) + .expect("Create pixmap for window"); + + Surface { + window, + pixels, + buffer: vec![0; (width * height) as usize], + } } fn configure_surface( &mut self, - _surface: &mut Surface, - _width: u32, - _height: u32, + surface: &mut Surface, + width: u32, + height: u32, ) { - // TODO + surface.pixels = tiny_skia::Pixmap::new(width, height) + .expect("Create pixmap for window"); + surface.buffer = vec![0; (width * height) as usize]; } fn fetch_information(&self) -> Information { @@ -84,13 +102,33 @@ pub fn new(settings: Settings) -> (Compositor, Backend) { pub fn present>( _compositor: &mut Compositor, - _backend: &mut Backend, - _surface: &mut Surface, - _primitives: &[Primitive], - _viewport: &Viewport, - _background_color: Color, - _overlay: &[T], + backend: &mut Backend, + surface: &mut Surface, + primitives: &[Primitive], + viewport: &Viewport, + background_color: Color, + overlay: &[T], ) -> Result<(), compositor::SurfaceError> { + backend.draw( + &mut surface.pixels, + primitives, + viewport, + background_color, + overlay, + ); + + for (i, pixel) in surface.pixels.pixels_mut().iter().enumerate() { + surface.buffer[i] = u32::from(pixel.red()) << 16 + | u32::from(pixel.green()) << 8 + | u32::from(pixel.blue()); + } + + surface.window.set_buffer( + &surface.buffer, + surface.pixels.width() as u16, + surface.pixels.height() as u16, + ); + // TODO Ok(()) } diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index 7406bfb8..3a4a7123 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -201,11 +201,15 @@ impl iced_graphics::window::Compositor for Compositor { fn create_surface( &mut self, window: &W, + width: u32, + height: u32, ) -> wgpu::Surface { #[allow(unsafe_code)] - unsafe { - self.instance.create_surface(window) - } + let mut surface = unsafe { self.instance.create_surface(window) }; + + self.configure_surface(&mut surface, width, height); + + surface } fn configure_surface( diff --git a/winit/src/application.rs b/winit/src/application.rs index 1bfce3a1..b52f0197 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -285,21 +285,18 @@ async fn run_instance( use winit::event; use winit::event_loop::ControlFlow; - let mut clipboard = Clipboard::connect(&window); - let mut cache = user_interface::Cache::default(); - let mut surface = compositor.create_surface(&window); - let mut should_exit = false; - let mut state = State::new(&application, &window); let mut viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - compositor.configure_surface( - &mut surface, + let mut clipboard = Clipboard::connect(&window); + let mut cache = user_interface::Cache::default(); + let mut surface = compositor.create_surface( + &window, physical_size.width, physical_size.height, ); + let mut should_exit = false; if should_be_visible { window.set_visible(true); -- cgit From 445b31c6c5f16ecc9f07bd072f246e827aa5b854 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 25 Feb 2023 21:06:58 +0100 Subject: Resize `Surface::buffer` instead of reallocating in `iced_tiny_skia` --- tiny_skia/src/window/compositor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 164d99c6..8bb52a03 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -58,7 +58,8 @@ impl iced_graphics::window::Compositor for Compositor { ) { surface.pixels = tiny_skia::Pixmap::new(width, height) .expect("Create pixmap for window"); - surface.buffer = vec![0; (width * height) as usize]; + + surface.buffer.resize((width * height) as usize, 0); } fn fetch_information(&self) -> Information { -- cgit From df5d66423de141a009bbed993d99d491ed6373c9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 00:38:46 +0100 Subject: Draft support for `Quad` and `Clip` primitives in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 62373ec7..54752a21 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -2,7 +2,7 @@ use crate::{Color, Font, Settings, Size, Viewport}; use iced_graphics::backend; use iced_graphics::text; -use iced_graphics::Primitive; +use iced_graphics::{Background, Primitive, Rectangle, Vector}; use std::borrow::Cow; @@ -22,12 +22,133 @@ impl Backend { pub fn draw>( &mut self, pixels: &mut tiny_skia::Pixmap, - _primitives: &[Primitive], - _viewport: &Viewport, + primitives: &[Primitive], + viewport: &Viewport, background_color: Color, _overlay: &[T], ) { pixels.fill(into_color(background_color)); + + let scale_factor = viewport.scale_factor() as f32; + + for primitive in primitives { + draw_primitive(primitive, pixels, None, scale_factor, Vector::ZERO); + } + } +} + +fn draw_primitive( + primitive: &Primitive, + pixels: &mut tiny_skia::Pixmap, + clip_mask: Option<&tiny_skia::ClipMask>, + scale_factor: f32, + translation: Vector, +) { + match primitive { + Primitive::None => {} + Primitive::Quad { + bounds, + background, + border_radius: _, // TODO + border_width, + border_color, + } => { + let transform = tiny_skia::Transform::from_translate( + translation.x, + translation.y, + ) + .post_scale(scale_factor, scale_factor); + + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Create quad rectangle"), + ); + + pixels.fill_path( + &path, + &tiny_skia::Paint { + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color(*color)) + } + }, + anti_alias: true, + ..tiny_skia::Paint::default() + }, + tiny_skia::FillRule::EvenOdd, + transform, + clip_mask, + ); + + if *border_width > 0.0 { + pixels.stroke_path( + &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, + ); + } + } + Primitive::Text { .. } => { + // TODO + } + Primitive::Image { .. } => { + // TODO + } + Primitive::Svg { .. } => { + // TODO + } + Primitive::Group { primitives } => { + for primitive in primitives { + draw_primitive( + primitive, + pixels, + clip_mask, + scale_factor, + translation, + ); + } + } + Primitive::Translate { + translation: offset, + content, + } => { + draw_primitive( + content, + pixels, + clip_mask, + scale_factor, + translation + *offset, + ); + } + Primitive::Clip { bounds, content } => { + draw_primitive( + content, + pixels, + Some(&rectangular_clip_mask(pixels, *bounds * scale_factor)), + scale_factor, + translation, + ); + } + Primitive::Cached { cache } => { + draw_primitive(cache, pixels, clip_mask, scale_factor, translation); + } + Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} } } @@ -36,6 +157,32 @@ fn into_color(color: Color) -> tiny_skia::Color { .expect("Convert color from iced to tiny_skia") } +fn rectangular_clip_mask( + pixels: &tiny_skia::Pixmap, + bounds: Rectangle, +) -> tiny_skia::ClipMask { + let mut clip_mask = tiny_skia::ClipMask::new(); + + let path = { + let mut builder = tiny_skia::PathBuilder::new(); + builder.push_rect(bounds.x, bounds.y, bounds.width, bounds.height); + + builder.finish().unwrap() + }; + + clip_mask + .set_path( + pixels.width(), + pixels.height(), + &path, + tiny_skia::FillRule::EvenOdd, + true, + ) + .expect("Set path of clipping area"); + + clip_mask +} + impl iced_graphics::Backend for Backend { fn trim_measurements(&mut self) { // TODO -- cgit From 744f3028f484c44899fed56d9190387569828a95 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 00:49:27 +0100 Subject: Use `Surface::buffer` directly for drawing in `iced_tiny_skia` ... with a nice little color trick :^) --- tiny_skia/Cargo.toml | 1 + tiny_skia/src/backend.rs | 8 ++++---- tiny_skia/src/window/compositor.rs | 30 +++++++++++------------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 7f4b0f8c..70b1f5e7 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -11,6 +11,7 @@ svg = [] raw-window-handle = "0.5" softbuffer = "0.2" tiny-skia = "0.8" +bytemuck = "1" [dependencies.iced_native] version = "0.9" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 54752a21..9eea1a32 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -21,7 +21,7 @@ impl Backend { pub fn draw>( &mut self, - pixels: &mut tiny_skia::Pixmap, + pixels: &mut tiny_skia::PixmapMut<'_>, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -39,7 +39,7 @@ impl Backend { fn draw_primitive( primitive: &Primitive, - pixels: &mut tiny_skia::Pixmap, + pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::ClipMask>, scale_factor: f32, translation: Vector, @@ -153,12 +153,12 @@ fn draw_primitive( } fn into_color(color: Color) -> tiny_skia::Color { - tiny_skia::Color::from_rgba(color.r, color.g, color.b, color.a) + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) .expect("Convert color from iced to tiny_skia") } fn rectangular_clip_mask( - pixels: &tiny_skia::Pixmap, + pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, ) -> tiny_skia::ClipMask { let mut clip_mask = tiny_skia::ClipMask::new(); diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 8bb52a03..2bd5831e 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -12,7 +12,6 @@ pub struct Compositor { pub struct Surface { window: softbuffer::GraphicsContext, - pixels: tiny_skia::Pixmap, buffer: Vec, } @@ -40,13 +39,9 @@ impl iced_graphics::window::Compositor for Compositor { unsafe { softbuffer::GraphicsContext::new(window, window) } .expect("Create softbuffer for window"); - let pixels = tiny_skia::Pixmap::new(width, height) - .expect("Create pixmap for window"); - Surface { window, - pixels, - buffer: vec![0; (width * height) as usize], + buffer: vec![0; width as usize * height as usize], } } @@ -56,9 +51,6 @@ impl iced_graphics::window::Compositor for Compositor { width: u32, height: u32, ) { - surface.pixels = tiny_skia::Pixmap::new(width, height) - .expect("Create pixmap for window"); - surface.buffer.resize((width * height) as usize, 0); } @@ -110,26 +102,26 @@ pub fn present>( background_color: Color, overlay: &[T], ) -> Result<(), compositor::SurfaceError> { + let physical_size = viewport.physical_size(); + backend.draw( - &mut surface.pixels, + &mut tiny_skia::PixmapMut::from_bytes( + bytemuck::cast_slice_mut(&mut surface.buffer), + physical_size.width, + physical_size.height, + ) + .expect("Create pixel map"), primitives, viewport, background_color, overlay, ); - for (i, pixel) in surface.pixels.pixels_mut().iter().enumerate() { - surface.buffer[i] = u32::from(pixel.red()) << 16 - | u32::from(pixel.green()) << 8 - | u32::from(pixel.blue()); - } - surface.window.set_buffer( &surface.buffer, - surface.pixels.width() as u16, - surface.pixels.height() as u16, + physical_size.width as u16, + physical_size.height as u16, ); - // TODO Ok(()) } -- cgit From 64fb722dfe8769d4a92edb0133f1863383ecfd86 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:40:17 +0100 Subject: Draft text support in `iced_tiny_skia` --- tiny_skia/Cargo.toml | 15 ++ tiny_skia/src/backend.rs | 269 +++++++++++++++++++++--------------- tiny_skia/src/lib.rs | 1 + tiny_skia/src/text.rs | 350 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 523 insertions(+), 112 deletions(-) create mode 100644 tiny_skia/src/text.rs diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 70b1f5e7..55f7f5b7 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -12,6 +12,8 @@ raw-window-handle = "0.5" softbuffer = "0.2" tiny-skia = "0.8" bytemuck = "1" +rustc-hash = "1.1" +ouroboros = "0.15" [dependencies.iced_native] version = "0.9" @@ -20,3 +22,16 @@ path = "../native" [dependencies.iced_graphics] version = "0.7" path = "../graphics" + +[dependencies.cosmic-text] +features = ["std", "swash"] +git = "https://github.com/hecrj/cosmic-text" +rev = "dc83efbf00a2efb4118403538e8a47bfd69c3e5e" + +[dependencies.twox-hash] +version = "1.6" +default-features = false + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash] +version = "1.6.1" +features = ["std"] diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 9eea1a32..5e743479 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -9,6 +9,7 @@ use std::borrow::Cow; pub struct Backend { default_font: Font, default_text_size: f32, + text_pipeline: crate::text::Pipeline, } impl Backend { @@ -16,6 +17,7 @@ impl Backend { Self { default_font: settings.default_font, default_text_size: settings.default_text_size, + text_pipeline: crate::text::Pipeline::new(), } } @@ -32,123 +34,161 @@ impl Backend { let scale_factor = viewport.scale_factor() as f32; for primitive in primitives { - draw_primitive(primitive, pixels, None, scale_factor, Vector::ZERO); + self.draw_primitive( + primitive, + pixels, + None, + scale_factor, + Vector::ZERO, + ); } + + self.text_pipeline.end_frame(); } -} -fn draw_primitive( - primitive: &Primitive, - pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::ClipMask>, - scale_factor: f32, - translation: Vector, -) { - match primitive { - Primitive::None => {} - Primitive::Quad { - bounds, - background, - border_radius: _, // TODO - border_width, - border_color, - } => { - let transform = tiny_skia::Transform::from_translate( - translation.x, - translation.y, - ) - .post_scale(scale_factor, scale_factor); - - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, + fn draw_primitive( + &mut self, + primitive: &Primitive, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::ClipMask>, + scale_factor: f32, + translation: Vector, + ) { + match primitive { + Primitive::None => {} + Primitive::Quad { + bounds, + background, + border_radius: _, // TODO + border_width, + border_color, + } => { + let transform = tiny_skia::Transform::from_translate( + translation.x, + translation.y, ) - .expect("Create quad rectangle"), - ); + .post_scale(scale_factor, scale_factor); - pixels.fill_path( - &path, - &tiny_skia::Paint { - shader: match background { - Background::Color(color) => { - tiny_skia::Shader::SolidColor(into_color(*color)) - } - }, - anti_alias: true, - ..tiny_skia::Paint::default() - }, - tiny_skia::FillRule::EvenOdd, - transform, - clip_mask, - ); + let path = tiny_skia::PathBuilder::from_rect( + tiny_skia::Rect::from_xywh( + bounds.x, + bounds.y, + bounds.width, + bounds.height, + ) + .expect("Create quad rectangle"), + ); - if *border_width > 0.0 { - pixels.stroke_path( + pixels.fill_path( &path, &tiny_skia::Paint { - shader: tiny_skia::Shader::SolidColor(into_color( - *border_color, - )), + shader: match background { + Background::Color(color) => { + tiny_skia::Shader::SolidColor(into_color( + *color, + )) + } + }, anti_alias: true, ..tiny_skia::Paint::default() }, - &tiny_skia::Stroke { - width: *border_width, - ..tiny_skia::Stroke::default() - }, + tiny_skia::FillRule::EvenOdd, transform, clip_mask, ); + + if *border_width > 0.0 { + pixels.stroke_path( + &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, + ); + } } - } - Primitive::Text { .. } => { - // TODO - } - Primitive::Image { .. } => { - // TODO - } - Primitive::Svg { .. } => { - // TODO - } - Primitive::Group { primitives } => { - for primitive in primitives { - draw_primitive( - primitive, + Primitive::Text { + content, + bounds, + color, + size, + font, + horizontal_alignment, + vertical_alignment, + } => { + self.text_pipeline.draw( + content, + (*bounds + translation) * scale_factor, + *color, + *size * scale_factor, + *font, + *horizontal_alignment, + *vertical_alignment, + pixels, + clip_mask, + ); + } + Primitive::Image { .. } => { + // TODO + } + Primitive::Svg { .. } => { + // TODO + } + Primitive::Group { primitives } => { + for primitive in primitives { + self.draw_primitive( + primitive, + pixels, + clip_mask, + scale_factor, + translation, + ); + } + } + Primitive::Translate { + translation: offset, + content, + } => { + self.draw_primitive( + content, pixels, clip_mask, scale_factor, + translation + *offset, + ); + } + Primitive::Clip { bounds, content } => { + self.draw_primitive( + content, + pixels, + Some(&rectangular_clip_mask( + pixels, + *bounds * scale_factor, + )), + scale_factor, translation, ); } + Primitive::Cached { cache } => { + self.draw_primitive( + cache, + pixels, + clip_mask, + scale_factor, + translation, + ); + } + Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} } - Primitive::Translate { - translation: offset, - content, - } => { - draw_primitive( - content, - pixels, - clip_mask, - scale_factor, - translation + *offset, - ); - } - Primitive::Clip { bounds, content } => { - draw_primitive( - content, - pixels, - Some(&rectangular_clip_mask(pixels, *bounds * scale_factor)), - scale_factor, - translation, - ); - } - Primitive::Cached { cache } => { - draw_primitive(cache, pixels, clip_mask, scale_factor, translation); - } - Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} } } @@ -185,7 +225,7 @@ fn rectangular_clip_mask( impl iced_graphics::Backend for Backend { fn trim_measurements(&mut self) { - // TODO + self.text_pipeline.trim_measurement_cache(); } } @@ -204,30 +244,35 @@ impl backend::Text for Backend { fn measure( &self, - _contents: &str, - _size: f32, - _font: Font, - _bounds: Size, + contents: &str, + size: f32, + font: Font, + bounds: Size, ) -> (f32, f32) { - // TODO - (0.0, 0.0) + self.text_pipeline.measure(contents, size, font, bounds) } fn hit_test( &self, - _contents: &str, - _size: f32, - _font: Font, - _bounds: Size, - _point: iced_native::Point, - _nearest_only: bool, + contents: &str, + size: f32, + font: Font, + bounds: Size, + point: iced_native::Point, + nearest_only: bool, ) -> Option { - // TODO - None + self.text_pipeline.hit_test( + contents, + size, + font, + bounds, + point, + nearest_only, + ) } - fn load_font(&mut self, _font: Cow<'static, [u8]>) { - // TODO + fn load_font(&mut self, font: Cow<'static, [u8]>) { + self.text_pipeline.load_font(font); } } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index fce44e9e..420a1ffb 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -2,6 +2,7 @@ pub mod window; mod backend; mod settings; +mod text; pub use backend::Backend; pub use settings::Settings; diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs new file mode 100644 index 00000000..5bd6eff1 --- /dev/null +++ b/tiny_skia/src/text.rs @@ -0,0 +1,350 @@ +pub use iced_native::text::Hit; + +use iced_native::alignment; +use iced_native::{Color, Font, Rectangle, Size}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::hash_map; +use std::hash::{BuildHasher, Hash, Hasher}; +use std::sync::Arc; + +#[allow(missing_debug_implementations)] +pub struct Pipeline { + system: Option, +} + +#[ouroboros::self_referencing] +struct System { + fonts: cosmic_text::FontSystem, + + #[borrows(fonts)] + #[not_covariant] + measurement_cache: RefCell>, + + #[borrows(fonts)] + #[not_covariant] + render_cache: Cache<'this>, +} + +impl Pipeline { + pub fn new() -> Self { + Pipeline { + system: Some( + SystemBuilder { + fonts: cosmic_text::FontSystem::new_with_fonts( + [cosmic_text::fontdb::Source::Binary(Arc::new( + include_bytes!("../../wgpu/fonts/Iced-Icons.ttf") + .as_slice(), + ))] + .into_iter(), + ), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ), + } + } + + pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + let heads = self.system.take().unwrap().into_heads(); + + let (locale, mut db) = heads.fonts.into_locale_and_db(); + + db.load_font_source(cosmic_text::fontdb::Source::Binary(Arc::new( + bytes.into_owned(), + ))); + + self.system = Some( + SystemBuilder { + fonts: cosmic_text::FontSystem::new_with_locale_and_db( + locale, db, + ), + measurement_cache_builder: |_| RefCell::new(Cache::new()), + render_cache_builder: |_| Cache::new(), + } + .build(), + ); + } + + pub fn draw( + &mut self, + content: &str, + bounds: Rectangle, + color: Color, + size: f32, + font: Font, + _horizontal_alignment: alignment::Horizontal, // TODO + _vertical_alignment: alignment::Vertical, // TODO + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: Option<&tiny_skia::ClipMask>, + ) { + self.system.as_mut().unwrap().with_mut(|fields| { + let key = Key { + bounds: bounds.size(), + content, + font, + size, + }; + + let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + + let mut swash = cosmic_text::SwashCache::new(&fields.fonts); + + for run in buffer.layout_runs() { + for glyph in run.glyphs { + // TODO: Outline support + if let Some(image) = swash.get_image(glyph.cache_key) { + let glyph_size = image.placement.width as usize + * image.placement.height as usize; + + if glyph_size == 0 { + continue; + } + + // TODO: Cache glyph rasterization + let mut buffer = vec![0u32; glyph_size]; + + match image.content { + cosmic_text::SwashContent::Mask => { + let mut i = 0; + + // TODO: Blend alpha + let [r, g, b, _a] = color.into_rgba8(); + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + buffer[i] = + tiny_skia::ColorU8::from_rgba( + b, + g, + r, + image.data[i], + ) + .premultiply() + .get(); + + i += 1; + } + } + } + cosmic_text::SwashContent::Color => { + let mut i = 0; + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + // TODO: Blend alpha + buffer[i] = (image.data[i + 3] as u32) + << 24 + | (image.data[i + 2] as u32) << 16 + | (image.data[i + 1] as u32) << 8 + | image.data[i] as u32; + + i += 1; + } + } + } + cosmic_text::SwashContent::SubpixelMask => { + // TODO + } + } + + let pixmap = tiny_skia::PixmapRef::from_bytes( + bytemuck::cast_slice(&buffer), + image.placement.width, + image.placement.height, + ) + .expect("Create glyph pixel map"); + + pixels.draw_pixmap( + bounds.x as i32 + + glyph.x_int + + image.placement.left, + bounds.y as i32 - glyph.y_int - image.placement.top + + run.line_y as i32, + pixmap, + &tiny_skia::PixmapPaint::default(), + tiny_skia::Transform::identity(), + clip_mask, + ); + } + } + } + }); + } + + pub fn end_frame(&mut self) { + self.system + .as_mut() + .unwrap() + .with_render_cache_mut(|cache| cache.trim()); + } + + pub fn measure( + &self, + content: &str, + size: f32, + font: Font, + bounds: Size, + ) -> (f32, f32) { + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size, + font, + bounds, + }, + ); + + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + (max_width, size * 1.2 * total_lines as f32) + }) + } + + pub fn hit_test( + &self, + content: &str, + size: f32, + font: iced_native::Font, + bounds: iced_native::Size, + point: iced_native::Point, + _nearest_only: bool, + ) -> Option { + self.system.as_ref().unwrap().with(|fields| { + let mut measurement_cache = fields.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate( + fields.fonts, + Key { + content, + size, + font, + bounds, + }, + ); + + let cursor = paragraph.hit(point.x, point.y)?; + + Some(Hit::CharOffset(cursor.index)) + }) + } + + pub fn trim_measurement_cache(&mut self) { + self.system + .as_mut() + .unwrap() + .with_measurement_cache_mut(|cache| cache.borrow_mut().trim()); + } +} + +fn to_family(font: Font) -> cosmic_text::Family<'static> { + match font { + Font::Name(name) => cosmic_text::Family::Name(name), + Font::SansSerif => cosmic_text::Family::SansSerif, + Font::Serif => cosmic_text::Family::Serif, + Font::Cursive => cosmic_text::Family::Cursive, + Font::Fantasy => cosmic_text::Family::Fantasy, + Font::Monospace => cosmic_text::Family::Monospace, + } +} + +struct Cache<'a> { + entries: FxHashMap>, + recently_used: FxHashSet, + hasher: HashBuilder, + trim_count: usize, +} + +#[cfg(not(target_arch = "wasm32"))] +type HashBuilder = twox_hash::RandomXxHashBuilder64; + +#[cfg(target_arch = "wasm32")] +type HashBuilder = std::hash::BuildHasherDefault; + +impl<'a> Cache<'a> { + const TRIM_INTERVAL: usize = 300; + + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: HashBuilder::default(), + trim_count: 0, + } + } + + fn allocate( + &mut self, + fonts: &'a cosmic_text::FontSystem, + key: Key<'_>, + ) -> (KeyHash, &mut cosmic_text::Buffer<'a>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + key.size.to_bits().hash(&mut hasher); + key.font.hash(&mut hasher); + key.bounds.width.to_bits().hash(&mut hasher); + key.bounds.height.to_bits().hash(&mut hasher); + + hasher.finish() + }; + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) { + let metrics = cosmic_text::Metrics::new(key.size, key.size * 1.2); + let mut buffer = cosmic_text::Buffer::new(fonts, metrics); + + buffer.set_size( + key.bounds.width, + key.bounds.height.max(key.size * 1.2), + ); + buffer.set_text( + key.content, + cosmic_text::Attrs::new() + .family(to_family(key.font)) + .monospaced(matches!(key.font, Font::Monospace)), + ); + + let _ = entry.insert(buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + if self.trim_count >= Self::TRIM_INTERVAL { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + + self.trim_count = 0; + } else { + self.trim_count += 1; + } + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, +} + +type KeyHash = u64; -- cgit From 3386402f5a3e75cdacd230f5e76cd54f4868d87d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:44:50 +0100 Subject: Implement text alignment support in `iced_tiny_skia` --- tiny_skia/src/text.rs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 5bd6eff1..64f31aae 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -76,8 +76,8 @@ impl Pipeline { color: Color, size: f32, font: Font, - _horizontal_alignment: alignment::Horizontal, // TODO - _vertical_alignment: alignment::Vertical, // TODO + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, pixels: &mut tiny_skia::PixmapMut<'_>, clip_mask: Option<&tiny_skia::ClipMask>, ) { @@ -91,6 +91,27 @@ impl Pipeline { let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + let (total_lines, max_width) = buffer + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); + + let total_height = total_lines as f32 * size * 1.2; + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.x - max_width / 2.0, + alignment::Horizontal::Right => bounds.x - max_width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.y - total_height / 2.0, + alignment::Vertical::Bottom => bounds.y - total_height, + }; + let mut swash = cosmic_text::SwashCache::new(&fields.fonts); for run in buffer.layout_runs() { @@ -159,10 +180,8 @@ impl Pipeline { .expect("Create glyph pixel map"); pixels.draw_pixmap( - bounds.x as i32 - + glyph.x_int - + image.placement.left, - bounds.y as i32 - glyph.y_int - image.placement.top + x as i32 + glyph.x_int + image.placement.left, + y as i32 - glyph.y_int - image.placement.top + run.line_y as i32, pixmap, &tiny_skia::PixmapPaint::default(), -- cgit From 4067c427db19eb59c4ec6c8c6d6658a9643df580 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:49:58 +0100 Subject: Fix glyphs with color mask in `iced_tiny_skia` --- tiny_skia/src/text.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 64f31aae..fc26703b 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -157,13 +157,17 @@ impl Pipeline { for _y in 0..image.placement.height { for _x in 0..image.placement.width { // TODO: Blend alpha - buffer[i] = (image.data[i + 3] as u32) - << 24 - | (image.data[i + 2] as u32) << 16 - | (image.data[i + 1] as u32) << 8 - | image.data[i] as u32; + buffer[i >> 2] = + tiny_skia::ColorU8::from_rgba( + image.data[i + 2], + image.data[i + 1], + image.data[i], + image.data[i + 3], + ) + .premultiply() + .get(); - i += 1; + i += 4; } } } -- cgit From 53573cf7cfd48f9bfd97d9e8b82308a0290d2b9d Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sun, 26 Feb 2023 23:59:00 +0100 Subject: Draw debug overlay in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 5e743479..cefed71f 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,5 +1,6 @@ use crate::{Color, Font, Settings, Size, Viewport}; +use iced_graphics::alignment; use iced_graphics::backend; use iced_graphics::text; use iced_graphics::{Background, Primitive, Rectangle, Vector}; @@ -27,7 +28,7 @@ impl Backend { primitives: &[Primitive], viewport: &Viewport, background_color: Color, - _overlay: &[T], + overlay: &[T], ) { pixels.fill(into_color(background_color)); @@ -43,6 +44,31 @@ impl Backend { ); } + for (i, text) in overlay.iter().enumerate() { + const OVERLAY_TEXT_SIZE: f32 = 20.0; + + self.draw_primitive( + &Primitive::Text { + content: text.as_ref().to_owned(), + size: OVERLAY_TEXT_SIZE, + bounds: Rectangle { + x: 10.0, + y: 10.0 + i as f32 * OVERLAY_TEXT_SIZE * 1.2, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: Color::BLACK, + font: Font::Monospace, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + }, + pixels, + None, + scale_factor, + Vector::ZERO, + ); + } + self.text_pipeline.end_frame(); } -- cgit From fbb14bf9b879d3d154618fa8d6a81bac018fee69 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 00:47:53 +0100 Subject: Implement `border_radius` support for quads in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 80 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index cefed71f..b1dd6a46 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -85,7 +85,7 @@ impl Backend { Primitive::Quad { bounds, background, - border_radius: _, // TODO + border_radius, border_width, border_color, } => { @@ -95,15 +95,7 @@ impl Backend { ) .post_scale(scale_factor, scale_factor); - let path = tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh( - bounds.x, - bounds.y, - bounds.width, - bounds.height, - ) - .expect("Create quad rectangle"), - ); + let path = rounded_rectangle(*bounds, *border_radius); pixels.fill_path( &path, @@ -223,6 +215,74 @@ fn into_color(color: Color) -> tiny_skia::Color { .expect("Convert color from iced to tiny_skia") } +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 == 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 { + builder.quad_to( + bounds.x + bounds.width, + bounds.y, + bounds.x + bounds.width, + bounds.y + top_right, + ); + } + + builder.line_to( + bounds.x + bounds.width, + bounds.y + bounds.height - bottom_right, + ); + + if bottom_right > 0.0 { + builder.quad_to( + bounds.x + bounds.width, + bounds.y + bounds.height, + bounds.x + bounds.width - bottom_right, + bounds.y + bounds.height, + ); + } + + builder.line_to(bounds.x + bottom_left, bounds.y + bounds.height); + + if bottom_right > 0.0 { + builder.quad_to( + bounds.x, + bounds.y + bounds.height, + bounds.x, + bounds.y + bounds.height - bottom_left, + ); + } + + builder.line_to(bounds.x, bounds.y + top_left); + + if top_left > 0.0 { + builder.quad_to(bounds.x, bounds.y, bounds.x + top_left, bounds.y); + } + + builder.finish().expect("Build rounded rectangle path") +} + fn rectangular_clip_mask( pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, -- cgit From 4e615a65cab9dfc5fa4a17a72580c573c1c040d9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 01:12:06 +0100 Subject: Fix `clippy` lints --- renderer/src/backend.rs | 1 + tiny_skia/src/text.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index a46d6f9b..b0a409dc 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -5,6 +5,7 @@ use iced_graphics::text; use std::borrow::Cow; +#[allow(clippy::large_enum_variant)] pub enum Backend { Wgpu(iced_wgpu::Backend), TinySkia(iced_tiny_skia::Backend), diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index fc26703b..6d4cfe96 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -89,7 +89,7 @@ impl Pipeline { size, }; - let (_, buffer) = fields.render_cache.allocate(&fields.fonts, key); + let (_, buffer) = fields.render_cache.allocate(fields.fonts, key); let (total_lines, max_width) = buffer .layout_runs() @@ -112,7 +112,7 @@ impl Pipeline { alignment::Vertical::Bottom => bounds.y - total_height, }; - let mut swash = cosmic_text::SwashCache::new(&fields.fonts); + let mut swash = cosmic_text::SwashCache::new(fields.fonts); for run in buffer.layout_runs() { for glyph in run.glyphs { -- cgit From 37ce30f360ce7cba9ad05654e1faf26276a1dc17 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 02:58:02 +0100 Subject: Use `kurbo` to approximate arcs in `iced_tiny_skia` --- tiny_skia/Cargo.toml | 1 + tiny_skia/src/backend.rs | 80 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 55f7f5b7..7fee49cb 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -14,6 +14,7 @@ tiny-skia = "0.8" bytemuck = "1" rustc-hash = "1.1" ouroboros = "0.15" +kurbo = "0.9" [dependencies.iced_native] version = "0.9" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index b1dd6a46..38a6c51d 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -241,48 +241,108 @@ fn rounded_rectangle( builder.line_to(bounds.x + bounds.width - top_right, bounds.y); if top_right > 0.0 { - builder.quad_to( - bounds.x + bounds.width, + arc_to( + &mut builder, + bounds.x + bounds.width - top_right, bounds.y, bounds.x + bounds.width, bounds.y + top_right, + top_right, ); } - builder.line_to( + maybe_line_to( + &mut builder, bounds.x + bounds.width, bounds.y + bounds.height - bottom_right, ); if bottom_right > 0.0 { - builder.quad_to( + arc_to( + &mut builder, bounds.x + bounds.width, - bounds.y + bounds.height, + bounds.y + bounds.height - bottom_right, bounds.x + bounds.width - bottom_right, bounds.y + bounds.height, + bottom_right, ); } - builder.line_to(bounds.x + bottom_left, bounds.y + bounds.height); + maybe_line_to( + &mut builder, + bounds.x + bottom_left, + bounds.y + bounds.height, + ); if bottom_right > 0.0 { - builder.quad_to( - bounds.x, + arc_to( + &mut builder, + bounds.x + bottom_left, bounds.y + bounds.height, bounds.x, bounds.y + bounds.height - bottom_left, + bottom_left, ); } - builder.line_to(bounds.x, bounds.y + top_left); + maybe_line_to(&mut builder, bounds.x, bounds.y + top_left); if top_left > 0.0 { - builder.quad_to(bounds.x, bounds.y, bounds.x + top_left, bounds.y); + 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 as f32, y_to as f32); + } + } +} + fn rectangular_clip_mask( pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, -- cgit From 8750d83337041f1d0fac8c2c0f4c40fd3a051a2c Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 03:02:13 +0100 Subject: Short-circuit rectangle path building in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 38a6c51d..d0977462 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -221,6 +221,22 @@ fn rounded_rectangle( ) -> 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 -- cgit From 3105ad2e0036e101e66b5e6ab437b570f2d923a3 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 03:04:05 +0100 Subject: Remove useless `f32` conversion in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index d0977462..66d83221 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -354,7 +354,7 @@ fn arc_to( }); } None => { - path.line_to(x_to as f32, y_to as f32); + path.line_to(x_to, y_to); } } } -- cgit From 11b2c3bbe31a43e73a61b9bd9f022233f302ae27 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 03:40:58 +0100 Subject: Reuse text buffers independently of color in `iced_wgpu` --- wgpu/Cargo.toml | 2 +- wgpu/src/text.rs | 28 +++++++++++----------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 632873a3..9e80a76d 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -60,7 +60,7 @@ path = "../graphics" [dependencies.glyphon] version = "0.2" git = "https://github.com/hecrj/glyphon.git" -rev = "65b481d758f50fd13fc21af2cc5ef62ddee64955" +rev = "810bc979f9005e2bd343b72b980e57e46174283f" [dependencies.tracing] version = "0.1.6" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index dea6ab18..3839b31f 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,7 +2,7 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; use iced_native::alignment; -use iced_native::{Color, Font, Rectangle, Size}; +use iced_native::{Font, Rectangle, Size}; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; @@ -110,7 +110,6 @@ impl Pipeline { height: (section.bounds.height * scale_factor) .ceil(), }, - color: section.color, }, ); @@ -162,6 +161,16 @@ impl Pipeline { left: left as i32, top: top as i32, bounds, + default_color: { + let [r, g, b, a] = section.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }, } }); @@ -174,7 +183,6 @@ impl Pipeline { height: target_size.height, }, text_areas, - glyphon::Color::rgb(0, 0, 0), &mut glyphon::SwashCache::new(fields.fonts), ); @@ -249,7 +257,6 @@ impl Pipeline { size, font, bounds, - color: Color::BLACK, }, ); @@ -283,7 +290,6 @@ impl Pipeline { size, font, bounds, - color: Color::BLACK, }, ); @@ -354,7 +360,6 @@ impl<'a> Cache<'a> { key.font.hash(&mut hasher); key.bounds.width.to_bits().hash(&mut hasher); key.bounds.height.to_bits().hash(&mut hasher); - key.color.into_rgba8().hash(&mut hasher); hasher.finish() }; @@ -371,16 +376,6 @@ impl<'a> Cache<'a> { key.content, glyphon::Attrs::new() .family(to_family(key.font)) - .color({ - let [r, g, b, a] = key.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }) .monospaced(matches!(key.font, Font::Monospace)), ); @@ -412,7 +407,6 @@ struct Key<'a> { size: f32, font: Font, bounds: Size, - color: Color, } type KeyHash = u64; -- cgit From c1ff803b8f98beb2a73bb4252b34921110aa6cf0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 16:28:19 +0100 Subject: Implement basic glyph cache in `iced_tiny_skia` --- tiny_skia/Cargo.toml | 2 +- tiny_skia/src/text.rs | 183 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 120 insertions(+), 65 deletions(-) diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 7fee49cb..781e7d34 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -27,7 +27,7 @@ path = "../graphics" [dependencies.cosmic-text] features = ["std", "swash"] git = "https://github.com/hecrj/cosmic-text" -rev = "dc83efbf00a2efb4118403538e8a47bfd69c3e5e" +rev = "81080c1b9498933b43c1889601a7ea6a3d16e161" [dependencies.twox-hash] version = "1.6" diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 6d4cfe96..cd7eb1ed 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -13,6 +13,7 @@ use std::sync::Arc; #[allow(missing_debug_implementations)] pub struct Pipeline { system: Option, + glyph_cache: GlyphCache, } #[ouroboros::self_referencing] @@ -45,6 +46,7 @@ impl Pipeline { } .build(), ), + glyph_cache: GlyphCache::new(), } } @@ -116,76 +118,20 @@ impl Pipeline { for run in buffer.layout_runs() { for glyph in run.glyphs { - // TODO: Outline support - if let Some(image) = swash.get_image(glyph.cache_key) { - let glyph_size = image.placement.width as usize - * image.placement.height as usize; - - if glyph_size == 0 { - continue; - } - - // TODO: Cache glyph rasterization - let mut buffer = vec![0u32; glyph_size]; - - match image.content { - cosmic_text::SwashContent::Mask => { - let mut i = 0; - - // TODO: Blend alpha - let [r, g, b, _a] = color.into_rgba8(); - - for _y in 0..image.placement.height { - for _x in 0..image.placement.width { - buffer[i] = - tiny_skia::ColorU8::from_rgba( - b, - g, - r, - image.data[i], - ) - .premultiply() - .get(); - - i += 1; - } - } - } - cosmic_text::SwashContent::Color => { - let mut i = 0; - - for _y in 0..image.placement.height { - for _x in 0..image.placement.width { - // TODO: Blend alpha - buffer[i >> 2] = - tiny_skia::ColorU8::from_rgba( - image.data[i + 2], - image.data[i + 1], - image.data[i], - image.data[i + 3], - ) - .premultiply() - .get(); - - i += 4; - } - } - } - cosmic_text::SwashContent::SubpixelMask => { - // TODO - } - } - + if let Some((buffer, placement)) = self + .glyph_cache + .allocate(glyph.cache_key, color, &mut swash) + { let pixmap = tiny_skia::PixmapRef::from_bytes( bytemuck::cast_slice(&buffer), - image.placement.width, - image.placement.height, + placement.width, + placement.height, ) .expect("Create glyph pixel map"); pixels.draw_pixmap( - x as i32 + glyph.x_int + image.placement.left, - y as i32 - glyph.y_int - image.placement.top + x as i32 + glyph.x_int + placement.left, + y as i32 - glyph.y_int - placement.top + run.line_y as i32, pixmap, &tiny_skia::PixmapPaint::default(), @@ -203,6 +149,8 @@ impl Pipeline { .as_mut() .unwrap() .with_render_cache_mut(|cache| cache.trim()); + + self.glyph_cache.trim(); } pub fn measure( @@ -283,6 +231,113 @@ fn to_family(font: Font) -> cosmic_text::Family<'static> { } } +#[derive(Debug, Clone, Default)] +struct GlyphCache { + entries: FxHashMap< + (cosmic_text::CacheKey, [u8; 3]), + (Vec, cosmic_text::Placement), + >, + recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>, + trim_count: usize, +} + +impl GlyphCache { + fn new() -> Self { + GlyphCache::default() + } + + fn allocate( + &mut self, + cache_key: cosmic_text::CacheKey, + color: Color, + swash: &mut cosmic_text::SwashCache<'_>, + ) -> Option<(&[u8], cosmic_text::Placement)> { + let [r, g, b, _a] = color.into_rgba8(); + let key = (cache_key, [r, g, b]); + + if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { + // TODO: Outline support + let image = swash.get_image(cache_key).as_ref()?; + + let glyph_size = image.placement.width as usize + * image.placement.height as usize; + + if glyph_size == 0 { + return None; + } + + // TODO: Cache glyph rasterization + let mut buffer = vec![0u32; glyph_size]; + + match image.content { + cosmic_text::SwashContent::Mask => { + let mut i = 0; + + // TODO: Blend alpha + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + buffer[i] = tiny_skia::ColorU8::from_rgba( + b, + g, + r, + image.data[i], + ) + .premultiply() + .get(); + + i += 1; + } + } + } + cosmic_text::SwashContent::Color => { + let mut i = 0; + + for _y in 0..image.placement.height { + for _x in 0..image.placement.width { + // TODO: Blend alpha + buffer[i >> 2] = tiny_skia::ColorU8::from_rgba( + image.data[i + 2], + image.data[i + 1], + image.data[i], + image.data[i + 3], + ) + .premultiply() + .get(); + + i += 4; + } + } + } + cosmic_text::SwashContent::SubpixelMask => { + // TODO + } + } + + entry.insert((buffer, image.placement)); + } + + self.recently_used.insert(key); + + self.entries.get(&key).map(|(buffer, placement)| { + (bytemuck::cast_slice(buffer.as_slice()), *placement) + }) + } + + pub fn trim(&mut self) { + if self.trim_count > 300 { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + + self.trim_count = 0; + } else { + self.trim_count += 1; + } + } +} + struct Cache<'a> { entries: FxHashMap>, recently_used: FxHashSet, -- cgit From 151daf95b70d5a53007496e58f49fc618c1a22e4 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Mon, 27 Feb 2023 16:30:54 +0100 Subject: Remove unnecessary `cast_slice` in `iced_tiny_skia` --- tiny_skia/src/text.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index cd7eb1ed..37b0fe47 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -123,7 +123,7 @@ impl Pipeline { .allocate(glyph.cache_key, color, &mut swash) { let pixmap = tiny_skia::PixmapRef::from_bytes( - bytemuck::cast_slice(&buffer), + buffer, placement.width, placement.height, ) @@ -266,7 +266,6 @@ impl GlyphCache { return None; } - // TODO: Cache glyph rasterization let mut buffer = vec![0u32; glyph_size]; match image.content { -- cgit From fd06de5d9c5afa05c5b11cda730b0769aef92caa Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Feb 2023 03:48:34 +0100 Subject: Use `get_image_uncached` in `iced_tiny_skia` ... since we are not reusing the `SwashCache` --- tiny_skia/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index 37b0fe47..da3a06bf 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -257,7 +257,7 @@ impl GlyphCache { if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) { // TODO: Outline support - let image = swash.get_image(cache_key).as_ref()?; + let image = swash.get_image_uncached(cache_key)?; let glyph_size = image.placement.width as usize * image.placement.height as usize; -- cgit From 3f6e28fa9b1b8d911f765c9efb5249a9e0c942d5 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Tue, 28 Feb 2023 20:47:13 +0100 Subject: Use `iced_renderer` instead of `iced_graphics` in root crate --- Cargo.toml | 5 ++--- renderer/Cargo.toml | 2 ++ renderer/src/lib.rs | 1 + renderer/src/widget.rs | 5 +++++ src/error.rs | 2 +- src/widget.rs | 4 ++-- 6 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 renderer/src/widget.rs diff --git a/Cargo.toml b/Cargo.toml index b1f5cf66..942966e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ image = ["iced_renderer/image", "image_rs"] # Enables the `Svg` widget svg = ["iced_renderer/svg"] # Enables the `Canvas` widget -canvas = ["iced_graphics/canvas"] +canvas = ["iced_renderer/canvas"] # Enables the `QRCode` widget -qr_code = ["iced_graphics/qr_code"] +qr_code = ["iced_renderer/qr_code"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -60,7 +60,6 @@ members = [ iced_core = { version = "0.8", path = "core" } iced_futures = { version = "0.6", path = "futures" } iced_native = { version = "0.9", path = "native" } -iced_graphics = { version = "0.7", path = "graphics" } iced_renderer = { version = "0.1", path = "renderer" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } thiserror = "1.0" diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 5ba5d426..1f21f06b 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" [features] image = ["iced_wgpu/image", "iced_tiny_skia/image"] svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] +canvas = ["iced_graphics/canvas"] +qr_code = ["iced_graphics/qr_code"] tracing = ["iced_wgpu/tracing"] [dependencies] diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index d0ba6793..f9bfc373 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,3 +1,4 @@ +pub mod widget; pub mod window; mod backend; diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs new file mode 100644 index 00000000..417cc06f --- /dev/null +++ b/renderer/src/widget.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "qr_code")] +pub use iced_graphics::widget::qr_code; + +#[cfg(feature = "canvas")] +pub use iced_graphics::widget::canvas; diff --git a/src/error.rs b/src/error.rs index 0bfa3ff1..5326718f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,7 +13,7 @@ pub enum Error { /// The application graphics context could not be created. #[error("the application graphics context could not be created")] - GraphicsCreationFailed(iced_graphics::Error), + GraphicsCreationFailed(iced_renderer::Error), } impl From for Error { diff --git a/src/widget.rs b/src/widget.rs index e2b0537e..04cb0e50 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -165,7 +165,7 @@ pub use vertical_slider::VerticalSlider; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub use iced_graphics::widget::canvas; +pub use iced_renderer::widget::canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] @@ -192,7 +192,7 @@ pub mod image { #[cfg(feature = "qr_code")] #[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub use iced_graphics::widget::qr_code; +pub use iced_renderer::widget::qr_code; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] -- cgit From 5fd5d1cdf8e5354788dc40729c4565ef377d3bba Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:34:26 +0100 Subject: Implement `Canvas` support for `iced_tiny_skia` --- core/Cargo.toml | 1 + core/src/gradient.rs | 117 ++++++ core/src/gradient/linear.rs | 112 ++++++ core/src/lib.rs | 2 + examples/arc/src/main.rs | 9 +- examples/bezier_tool/src/main.rs | 25 +- examples/clock/src/main.rs | 9 +- examples/color_palette/src/main.rs | 9 +- examples/game_of_life/src/main.rs | 73 ++-- examples/geometry/src/main.rs | 6 +- examples/modern_art/src/main.rs | 5 +- examples/multitouch/src/main.rs | 7 +- examples/sierpinski_triangle/src/main.rs | 9 +- examples/solar_system/src/main.rs | 28 +- graphics/Cargo.toml | 13 +- graphics/src/backend.rs | 2 + graphics/src/gradient.rs | 117 ------ graphics/src/gradient/linear.rs | 112 ------ graphics/src/layer.rs | 270 ------------- graphics/src/layer/image.rs | 27 -- graphics/src/layer/mesh.rs | 93 ----- graphics/src/layer/quad.rs | 30 -- graphics/src/layer/text.rs | 26 -- graphics/src/lib.rs | 13 +- graphics/src/primitive.rs | 128 ++++-- graphics/src/renderer.rs | 43 +- graphics/src/triangle.rs | 32 -- graphics/src/widget.rs | 16 - graphics/src/widget/canvas.rs | 268 ------------- graphics/src/widget/canvas/cache.rs | 100 ----- graphics/src/widget/canvas/cursor.rs | 64 --- graphics/src/widget/canvas/event.rs | 21 - graphics/src/widget/canvas/fill.rs | 72 ---- graphics/src/widget/canvas/frame.rs | 530 ------------------------- graphics/src/widget/canvas/geometry.rs | 24 -- graphics/src/widget/canvas/path.rs | 109 ------ graphics/src/widget/canvas/path/arc.rs | 42 -- graphics/src/widget/canvas/path/builder.rs | 198 ---------- graphics/src/widget/canvas/program.rs | 102 ----- graphics/src/widget/canvas/stroke.rs | 126 ------ graphics/src/widget/canvas/style.rs | 23 -- graphics/src/widget/canvas/text.rs | 57 --- graphics/src/widget/qr_code.rs | 301 -------------- native/Cargo.toml | 6 + native/src/lib.rs | 4 +- native/src/widget.rs | 16 + native/src/widget/canvas.rs | 259 ++++++++++++ native/src/widget/canvas/cursor.rs | 64 +++ native/src/widget/canvas/event.rs | 21 + native/src/widget/canvas/fill.rs | 64 +++ native/src/widget/canvas/path.rs | 67 ++++ native/src/widget/canvas/path/arc.rs | 42 ++ native/src/widget/canvas/path/builder.rs | 192 +++++++++ native/src/widget/canvas/program.rs | 108 +++++ native/src/widget/canvas/stroke.rs | 106 +++++ native/src/widget/canvas/style.rs | 24 ++ native/src/widget/canvas/text.rs | 57 +++ renderer/Cargo.toml | 10 +- renderer/src/backend.rs | 4 +- renderer/src/lib.rs | 14 +- renderer/src/widget.rs | 12 +- renderer/src/widget/canvas.rs | 177 +++++++++ renderer/src/widget/canvas/cache.rs | 85 ++++ renderer/src/widget/qr_code.rs | 301 ++++++++++++++ src/widget.rs | 5 +- tiny_skia/Cargo.toml | 1 + tiny_skia/src/backend.rs | 48 ++- tiny_skia/src/canvas.rs | 276 +++++++++++++ tiny_skia/src/lib.rs | 10 +- tiny_skia/src/primitive.rs | 82 ++++ tiny_skia/src/window/compositor.rs | 3 +- wgpu/Cargo.toml | 15 +- wgpu/src/backend.rs | 7 +- wgpu/src/image.rs | 2 +- wgpu/src/layer.rs | 274 +++++++++++++ wgpu/src/layer/image.rs | 27 ++ wgpu/src/layer/mesh.rs | 93 +++++ wgpu/src/layer/quad.rs | 30 ++ wgpu/src/layer/text.rs | 26 ++ wgpu/src/lib.rs | 15 +- wgpu/src/quad.rs | 3 +- wgpu/src/text.rs | 3 +- wgpu/src/triangle.rs | 71 ++-- wgpu/src/widget.rs | 9 + wgpu/src/widget/canvas.rs | 16 + wgpu/src/widget/canvas/cache.rs | 93 +++++ wgpu/src/widget/canvas/frame.rs | 609 +++++++++++++++++++++++++++++ wgpu/src/widget/canvas/geometry.rs | 24 ++ 88 files changed, 3765 insertions(+), 2981 deletions(-) create mode 100644 core/src/gradient.rs create mode 100644 core/src/gradient/linear.rs delete mode 100644 graphics/src/gradient.rs delete mode 100644 graphics/src/gradient/linear.rs delete mode 100644 graphics/src/layer.rs delete mode 100644 graphics/src/layer/image.rs delete mode 100644 graphics/src/layer/mesh.rs delete mode 100644 graphics/src/layer/quad.rs delete mode 100644 graphics/src/layer/text.rs delete mode 100644 graphics/src/widget.rs delete mode 100644 graphics/src/widget/canvas.rs delete mode 100644 graphics/src/widget/canvas/cache.rs delete mode 100644 graphics/src/widget/canvas/cursor.rs delete mode 100644 graphics/src/widget/canvas/event.rs delete mode 100644 graphics/src/widget/canvas/fill.rs delete mode 100644 graphics/src/widget/canvas/frame.rs delete mode 100644 graphics/src/widget/canvas/geometry.rs delete mode 100644 graphics/src/widget/canvas/path.rs delete mode 100644 graphics/src/widget/canvas/path/arc.rs delete mode 100644 graphics/src/widget/canvas/path/builder.rs delete mode 100644 graphics/src/widget/canvas/program.rs delete mode 100644 graphics/src/widget/canvas/stroke.rs delete mode 100644 graphics/src/widget/canvas/style.rs delete mode 100644 graphics/src/widget/canvas/text.rs delete mode 100644 graphics/src/widget/qr_code.rs create mode 100644 native/src/widget/canvas.rs create mode 100644 native/src/widget/canvas/cursor.rs create mode 100644 native/src/widget/canvas/event.rs create mode 100644 native/src/widget/canvas/fill.rs create mode 100644 native/src/widget/canvas/path.rs create mode 100644 native/src/widget/canvas/path/arc.rs create mode 100644 native/src/widget/canvas/path/builder.rs create mode 100644 native/src/widget/canvas/program.rs create mode 100644 native/src/widget/canvas/stroke.rs create mode 100644 native/src/widget/canvas/style.rs create mode 100644 native/src/widget/canvas/text.rs create mode 100644 renderer/src/widget/canvas.rs create mode 100644 renderer/src/widget/canvas/cache.rs create mode 100644 renderer/src/widget/qr_code.rs create mode 100644 tiny_skia/src/canvas.rs create mode 100644 tiny_skia/src/primitive.rs create mode 100644 wgpu/src/layer.rs create mode 100644 wgpu/src/layer/image.rs create mode 100644 wgpu/src/layer/mesh.rs create mode 100644 wgpu/src/layer/quad.rs create mode 100644 wgpu/src/layer/text.rs create mode 100644 wgpu/src/widget.rs create mode 100644 wgpu/src/widget/canvas.rs create mode 100644 wgpu/src/widget/canvas/cache.rs create mode 100644 wgpu/src/widget/canvas/frame.rs create mode 100644 wgpu/src/widget/canvas/geometry.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 43865e4d..7ccb7b7a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/iced-rs/iced" [dependencies] bitflags = "1.2" +thiserror = "1" [dependencies.palette] version = "0.6" diff --git a/core/src/gradient.rs b/core/src/gradient.rs new file mode 100644 index 00000000..61e919d6 --- /dev/null +++ b/core/src/gradient.rs @@ -0,0 +1,117 @@ +//! For creating a Gradient. +pub mod linear; + +pub use linear::Linear; + +use crate::{Color, Point, Size}; + +#[derive(Debug, Clone, PartialEq)] +/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), +/// or conically (TBD). +pub enum Gradient { + /// A linear gradient interpolates colors along a direction from its `start` to its `end` + /// point. + Linear(Linear), +} + +impl Gradient { + /// Creates a new linear [`linear::Builder`]. + pub fn linear(position: impl Into) -> linear::Builder { + linear::Builder::new(position.into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// A point along the gradient vector where the specified [`color`] is unmixed. +/// +/// [`color`]: Self::color +pub struct ColorStop { + /// Offset along the gradient vector. + pub offset: f32, + + /// The color of the gradient at the specified [`offset`]. + /// + /// [`offset`]: Self::offset + pub color: Color, +} + +#[derive(Debug)] +/// The position of the gradient within its bounds. +pub enum Position { + /// The gradient will be positioned with respect to two points. + Absolute { + /// The starting point of the gradient. + start: Point, + /// The ending point of the gradient. + end: Point, + }, + /// The gradient will be positioned relative to the provided bounds. + Relative { + /// The top left position of the bounds. + top_left: Point, + /// The width & height of the bounds. + size: Size, + /// The start [Location] of the gradient. + start: Location, + /// The end [Location] of the gradient. + end: Location, + }, +} + +impl From<(Point, Point)> for Position { + fn from((start, end): (Point, Point)) -> Self { + Self::Absolute { start, end } + } +} + +#[derive(Debug, Clone, Copy)] +/// The location of a relatively-positioned gradient. +pub enum Location { + /// Top left. + TopLeft, + /// Top. + Top, + /// Top right. + TopRight, + /// Right. + Right, + /// Bottom right. + BottomRight, + /// Bottom. + Bottom, + /// Bottom left. + BottomLeft, + /// Left. + Left, +} + +impl Location { + fn to_absolute(self, top_left: Point, size: Size) -> Point { + match self { + Location::TopLeft => top_left, + Location::Top => { + Point::new(top_left.x + size.width / 2.0, top_left.y) + } + Location::TopRight => { + Point::new(top_left.x + size.width, top_left.y) + } + Location::Right => Point::new( + top_left.x + size.width, + top_left.y + size.height / 2.0, + ), + Location::BottomRight => { + Point::new(top_left.x + size.width, top_left.y + size.height) + } + Location::Bottom => Point::new( + top_left.x + size.width / 2.0, + top_left.y + size.height, + ), + Location::BottomLeft => { + Point::new(top_left.x, top_left.y + size.height) + } + Location::Left => { + Point::new(top_left.x, top_left.y + size.height / 2.0) + } + } + } +} diff --git a/core/src/gradient/linear.rs b/core/src/gradient/linear.rs new file mode 100644 index 00000000..c886db47 --- /dev/null +++ b/core/src/gradient/linear.rs @@ -0,0 +1,112 @@ +//! Linear gradient builder & definition. +use crate::gradient::{ColorStop, Gradient, Position}; +use crate::{Color, Point}; + +/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. +/// +/// [`Fill`]: crate::widget::canvas::Fill +/// [`Stroke`]: crate::widget::canvas::Stroke +#[derive(Debug, Clone, PartialEq)] +pub struct Linear { + /// The point where the linear gradient begins. + pub start: Point, + /// The point where the linear gradient ends. + pub end: Point, + /// [`ColorStop`]s along the linear gradient path. + pub color_stops: Vec, +} + +/// A [`Linear`] builder. +#[derive(Debug)] +pub struct Builder { + start: Point, + end: Point, + stops: Vec, + error: Option, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new(position: Position) -> Self { + let (start, end) = match position { + Position::Absolute { start, end } => (start, end), + Position::Relative { + top_left, + size, + start, + end, + } => ( + start.to_absolute(top_left, size), + end.to_absolute(top_left, size), + ), + }; + + Self { + start, + end, + stops: vec![], + error: None, + } + } + + /// Adds a new stop, defined by an offset and a color, to the gradient. + /// + /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built. + /// + /// Note: when using the [`glow`] backend, any color stop added after the 16th + /// will not be displayed. + /// + /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops). + /// + /// [`glow`]: https://docs.rs/iced_glow + /// [`wgpu`]: https://docs.rs/iced_wgpu + pub fn add_stop(mut self, offset: f32, color: Color) -> Self { + if offset.is_finite() && (0.0..=1.0).contains(&offset) { + match self.stops.binary_search_by(|stop| { + stop.offset.partial_cmp(&offset).unwrap() + }) { + Ok(_) => { + self.error = Some(BuilderError::DuplicateOffset(offset)) + } + Err(index) => { + self.stops.insert(index, ColorStop { offset, color }); + } + } + } else { + self.error = Some(BuilderError::InvalidOffset(offset)) + }; + + self + } + + /// Builds the linear [`Gradient`] of this [`Builder`]. + /// + /// Returns `BuilderError` if gradient in invalid. + pub fn build(self) -> Result { + if self.stops.is_empty() { + Err(BuilderError::MissingColorStop) + } else if let Some(error) = self.error { + Err(error) + } else { + Ok(Gradient::Linear(Linear { + start: self.start, + end: self.end, + color_stops: self.stops, + })) + } + } +} + +/// An error that happened when building a [`Linear`] gradient. +#[derive(Debug, thiserror::Error)] +pub enum BuilderError { + #[error("Gradients must contain at least one color stop.")] + /// Gradients must contain at least one color stop. + MissingColorStop, + #[error("Offset {0} must be a unique, finite number.")] + /// Offsets in a gradient must all be unique & finite. + DuplicateOffset(f32), + #[error("Offset {0} must be between 0.0..=1.0.")] + /// Offsets in a gradient must be between 0.0..=1.0. + InvalidOffset(f32), +} diff --git a/core/src/lib.rs b/core/src/lib.rs index d7314851..1e4f0411 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -26,6 +26,7 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; pub mod font; +pub mod gradient; pub mod keyboard; pub mod mouse; pub mod time; @@ -46,6 +47,7 @@ pub use background::Background; pub use color::Color; pub use content_fit::ContentFit; pub use font::Font; +pub use gradient::Gradient; pub use length::Length; pub use padding::Padding; pub use pixels::Pixels; diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 7b6ea0e1..d71ba6f6 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::canvas::{ self, stroke, Cache, Canvas, Cursor, Geometry, Path, Stroke, }; use iced::{ - Application, Command, Element, Length, Point, Rectangle, Settings, - Subscription, Theme, + Application, Command, Element, Length, Point, Rectangle, Renderer, + Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -69,17 +69,18 @@ impl Application for Arc { } } -impl canvas::Program for Arc { +impl canvas::Program for Arc { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let geometry = self.cache.draw(bounds.size(), |frame| { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { let palette = theme.palette(); let center = frame.center(); diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 7c3916d4..5bb463c3 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -64,7 +64,7 @@ mod bezier { use iced::widget::canvas::{ self, Canvas, Cursor, Frame, Geometry, Path, Stroke, }; - use iced::{Element, Length, Point, Rectangle, Theme}; + use iced::{Element, Length, Point, Rectangle, Renderer, Theme}; #[derive(Default)] pub struct State { @@ -92,7 +92,7 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl<'a> canvas::Program for Bezier<'a> { type State = Option; fn update( @@ -152,22 +152,26 @@ mod bezier { fn draw( &self, state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, cursor: Cursor, ) -> Vec { - let content = - self.state.cache.draw(bounds.size(), |frame: &mut Frame| { + let content = self.state.cache.draw( + renderer, + bounds.size(), + |frame: &mut Frame| { Curve::draw_all(self.curves, frame); frame.stroke( &Path::rectangle(Point::ORIGIN, frame.size()), Stroke::default().with_width(2.0), ); - }); + }, + ); if let Some(pending) = state { - let pending_curve = pending.draw(bounds, cursor); + let pending_curve = pending.draw(renderer, bounds, cursor); vec![content, pending_curve] } else { @@ -216,8 +220,13 @@ mod bezier { } impl Pending { - fn draw(&self, bounds: Rectangle, cursor: Cursor) -> Geometry { - let mut frame = Frame::new(bounds.size()); + fn draw( + &self, + renderer: &Renderer, + bounds: Rectangle, + cursor: Cursor, + ) -> Geometry { + let mut frame = Frame::new(renderer, bounds.size()); if let Some(cursor_position) = cursor.position_in(&bounds) { match *self { diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index a389c54f..6425e2da 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -4,8 +4,8 @@ use iced::widget::canvas::{ }; use iced::widget::{canvas, container}; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Settings, - Subscription, Theme, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Renderer, + Settings, Subscription, Theme, Vector, }; pub fn main() -> iced::Result { @@ -83,17 +83,18 @@ impl Application for Clock { } } -impl canvas::Program for Clock { +impl canvas::Program for Clock { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let clock = self.clock.draw(bounds.size(), |frame| { + let clock = self.clock.draw(renderer, bounds.size(), |frame| { let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index a2df36c2..1109a883 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -1,8 +1,8 @@ use iced::widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - alignment, Alignment, Color, Element, Length, Point, Rectangle, Sandbox, - Settings, Size, Vector, + alignment, Alignment, Color, Element, Length, Point, Rectangle, Renderer, + Sandbox, Settings, Size, Vector, }; use palette::{self, convert::FromColor, Hsl, Srgb}; use std::marker::PhantomData; @@ -237,17 +237,18 @@ impl Theme { } } -impl canvas::Program for Theme { +impl canvas::Program for Theme { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &iced::Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let theme = self.canvas_cache.draw(bounds.size(), |frame| { + let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| { self.draw(frame); }); diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index ed911160..494f71a6 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -211,8 +211,8 @@ mod grid { Cache, Canvas, Cursor, Frame, Geometry, Path, Text, }; use iced::{ - alignment, mouse, Color, Element, Length, Point, Rectangle, Size, - Theme, Vector, + alignment, mouse, Color, Element, Length, Point, Rectangle, Renderer, + Size, Theme, Vector, }; use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; @@ -393,7 +393,7 @@ mod grid { } } - impl canvas::Program for Grid { + impl canvas::Program for Grid { type State = Interaction; fn update( @@ -536,13 +536,14 @@ mod grid { fn draw( &self, _interaction: &Interaction, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, cursor: Cursor, ) -> Vec { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - let life = self.life_cache.draw(bounds.size(), |frame| { + let life = self.life_cache.draw(renderer, bounds.size(), |frame| { let background = Path::rectangle(Point::ORIGIN, frame.size()); frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); @@ -565,7 +566,7 @@ mod grid { }); let overlay = { - let mut frame = Frame::new(bounds.size()); + let mut frame = Frame::new(renderer, bounds.size()); let hovered_cell = cursor.position_in(&bounds).map(|position| { @@ -626,38 +627,40 @@ mod grid { if self.scaling < 0.2 || !self.show_lines { vec![life, overlay] } else { - let grid = self.grid_cache.draw(bounds.size(), |frame| { - frame.translate(center); - frame.scale(self.scaling); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); - - let region = self.visible_region(frame.size()); - let rows = region.rows(); - let columns = region.columns(); - let (total_rows, total_columns) = - (rows.clone().count(), columns.clone().count()); - let width = 2.0 / Cell::SIZE as f32; - let color = Color::from_rgb8(70, 74, 83); - - frame.translate(Vector::new(-width / 2.0, -width / 2.0)); + let grid = + self.grid_cache.draw(renderer, bounds.size(), |frame| { + frame.translate(center); + frame.scale(self.scaling); + frame.translate(self.translation); + frame.scale(Cell::SIZE as f32); - for row in region.rows() { - frame.fill_rectangle( - Point::new(*columns.start() as f32, row as f32), - Size::new(total_columns as f32, width), - color, - ); - } + let region = self.visible_region(frame.size()); + let rows = region.rows(); + let columns = region.columns(); + let (total_rows, total_columns) = + (rows.clone().count(), columns.clone().count()); + let width = 2.0 / Cell::SIZE as f32; + let color = Color::from_rgb8(70, 74, 83); + + frame + .translate(Vector::new(-width / 2.0, -width / 2.0)); + + for row in region.rows() { + frame.fill_rectangle( + Point::new(*columns.start() as f32, row as f32), + Size::new(total_columns as f32, width), + color, + ); + } - for column in region.columns() { - frame.fill_rectangle( - Point::new(column as f32, *rows.start() as f32), - Size::new(width, total_rows as f32), - color, - ); - } - }); + for column in region.columns() { + frame.fill_rectangle( + Point::new(column as f32, *rows.start() as f32), + Size::new(width, total_rows as f32), + color, + ); + } + }); vec![life, grid, overlay] } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 9bacce7f..a77772d5 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -10,9 +10,9 @@ mod rainbow { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. + use iced_graphics::primitive::{ColoredVertex2D, Primitive}; use iced_graphics::renderer::{self, Renderer}; - use iced_graphics::triangle::ColoredVertex2D; - use iced_graphics::{Backend, Primitive}; + use iced_graphics::Backend; use iced_native::layout; use iced_native::widget::{self, Widget}; @@ -59,7 +59,7 @@ mod rainbow { cursor_position: Point, _viewport: &Rectangle, ) { - use iced_graphics::triangle::Mesh2D; + use iced_graphics::primitive::Mesh2D; use iced_native::Renderer as _; let b = layout.bounds(); diff --git a/examples/modern_art/src/main.rs b/examples/modern_art/src/main.rs index 28ed3e21..a43a2b2b 100644 --- a/examples/modern_art/src/main.rs +++ b/examples/modern_art/src/main.rs @@ -55,17 +55,18 @@ impl Application for ModernArt { } } -impl canvas::Program for ModernArt { +impl canvas::Program for ModernArt { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let geometry = self.cache.draw(bounds.size(), |frame| { + let geometry = self.cache.draw(renderer, bounds.size(), |frame| { let num_squares = thread_rng().gen_range(0..1200); let mut i = 0; diff --git a/examples/multitouch/src/main.rs b/examples/multitouch/src/main.rs index f5faae0f..7df6c929 100644 --- a/examples/multitouch/src/main.rs +++ b/examples/multitouch/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{self, Canvas, Cursor, Geometry}; use iced::{ executor, touch, window, Application, Color, Command, Element, Length, - Point, Rectangle, Settings, Subscription, Theme, + Point, Rectangle, Renderer, Settings, Subscription, Theme, }; use std::collections::HashMap; @@ -95,7 +95,7 @@ impl Application for Multitouch { } } -impl canvas::Program for State { +impl canvas::Program for State { type State = (); fn update( @@ -125,11 +125,12 @@ impl canvas::Program for State { fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { - let fingerweb = self.cache.draw(bounds.size(), |frame| { + let fingerweb = self.cache.draw(renderer, bounds.size(), |frame| { if self.fingers.len() < 2 { return; } diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index 1d25d171..e85f8391 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -5,8 +5,8 @@ use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{self, Canvas}; use iced::widget::{column, row, slider, text}; use iced::{ - Application, Color, Command, Length, Point, Rectangle, Settings, Size, - Theme, + Application, Color, Command, Length, Point, Rectangle, Renderer, Settings, + Size, Theme, }; use rand::Rng; @@ -97,7 +97,7 @@ struct SierpinskiGraph { cache: canvas::Cache, } -impl canvas::Program for SierpinskiGraph { +impl canvas::Program for SierpinskiGraph { type State = (); fn update( @@ -134,11 +134,12 @@ impl canvas::Program for SierpinskiGraph { fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: canvas::Cursor, ) -> Vec { - let geom = self.cache.draw(bounds.size(), |frame| { + let geom = self.cache.draw(renderer, bounds.size(), |frame| { frame.stroke( &canvas::Path::rectangle(Point::ORIGIN, frame.size()), canvas::Stroke::default(), diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 9a4ee754..0023a69b 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -15,8 +15,8 @@ use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::{Cursor, Path}; use iced::window; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Settings, - Size, Subscription, Vector, + Application, Color, Command, Element, Length, Point, Rectangle, Renderer, + Settings, Size, Subscription, Vector, }; use std::time::Instant; @@ -150,30 +150,32 @@ impl State { } } -impl canvas::Program for State { +impl canvas::Program for State { type State = (); fn draw( &self, _state: &Self::State, + renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor, ) -> Vec { use std::f32::consts::PI; - let background = self.space_cache.draw(bounds.size(), |frame| { - let stars = Path::new(|path| { - for (p, size) in &self.stars { - path.rectangle(*p, Size::new(*size, *size)); - } - }); + let background = + self.space_cache.draw(renderer, bounds.size(), |frame| { + let stars = Path::new(|path| { + for (p, size) in &self.stars { + path.rectangle(*p, Size::new(*size, *size)); + } + }); - frame.translate(frame.center() - Point::ORIGIN); - frame.fill(&stars, Color::WHITE); - }); + frame.translate(frame.center() - Point::ORIGIN); + frame.fill(&stars, Color::WHITE); + }); - let system = self.system_cache.draw(bounds.size(), |frame| { + let system = self.system_cache.draw(renderer, bounds.size(), |frame| { let center = frame.center(); let sun = Path::circle(center, Self::SUN_RADIUS); diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 9795d31f..36d8a516 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -24,8 +24,7 @@ bmp = ["image_rs/bmp"] hdr = ["image_rs/hdr"] dds = ["image_rs/dds"] farbfeld = ["image_rs/farbfeld"] -canvas = ["lyon"] -qr_code = ["qrcode", "canvas"] +canvas = ["iced_native/canvas"] opengl = [] image_rs = ["kamadak-exif"] @@ -35,6 +34,7 @@ log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" bitflags = "1.2" +tiny-skia = "0.8" [dependencies.bytemuck] version = "1.4" @@ -48,15 +48,6 @@ path = "../native" version = "0.7" path = "../style" -[dependencies.lyon] -version = "1.0" -optional = true - -[dependencies.qrcode] -version = "0.12" -optional = true -default-features = false - [dependencies.image_rs] version = "0.24" package = "image" diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 8658cffe..c44372e8 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -10,6 +10,8 @@ use std::borrow::Cow; /// /// [`Renderer`]: crate::Renderer pub trait Backend { + type Geometry: Into; + /// Trims the measurements cache. /// /// This method is currently necessary to properly trim the text cache in diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs deleted file mode 100644 index 61e919d6..00000000 --- a/graphics/src/gradient.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! For creating a Gradient. -pub mod linear; - -pub use linear::Linear; - -use crate::{Color, Point, Size}; - -#[derive(Debug, Clone, PartialEq)] -/// A fill which transitions colors progressively along a direction, either linearly, radially (TBD), -/// or conically (TBD). -pub enum Gradient { - /// A linear gradient interpolates colors along a direction from its `start` to its `end` - /// point. - Linear(Linear), -} - -impl Gradient { - /// Creates a new linear [`linear::Builder`]. - pub fn linear(position: impl Into) -> linear::Builder { - linear::Builder::new(position.into()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -/// A point along the gradient vector where the specified [`color`] is unmixed. -/// -/// [`color`]: Self::color -pub struct ColorStop { - /// Offset along the gradient vector. - pub offset: f32, - - /// The color of the gradient at the specified [`offset`]. - /// - /// [`offset`]: Self::offset - pub color: Color, -} - -#[derive(Debug)] -/// The position of the gradient within its bounds. -pub enum Position { - /// The gradient will be positioned with respect to two points. - Absolute { - /// The starting point of the gradient. - start: Point, - /// The ending point of the gradient. - end: Point, - }, - /// The gradient will be positioned relative to the provided bounds. - Relative { - /// The top left position of the bounds. - top_left: Point, - /// The width & height of the bounds. - size: Size, - /// The start [Location] of the gradient. - start: Location, - /// The end [Location] of the gradient. - end: Location, - }, -} - -impl From<(Point, Point)> for Position { - fn from((start, end): (Point, Point)) -> Self { - Self::Absolute { start, end } - } -} - -#[derive(Debug, Clone, Copy)] -/// The location of a relatively-positioned gradient. -pub enum Location { - /// Top left. - TopLeft, - /// Top. - Top, - /// Top right. - TopRight, - /// Right. - Right, - /// Bottom right. - BottomRight, - /// Bottom. - Bottom, - /// Bottom left. - BottomLeft, - /// Left. - Left, -} - -impl Location { - fn to_absolute(self, top_left: Point, size: Size) -> Point { - match self { - Location::TopLeft => top_left, - Location::Top => { - Point::new(top_left.x + size.width / 2.0, top_left.y) - } - Location::TopRight => { - Point::new(top_left.x + size.width, top_left.y) - } - Location::Right => Point::new( - top_left.x + size.width, - top_left.y + size.height / 2.0, - ), - Location::BottomRight => { - Point::new(top_left.x + size.width, top_left.y + size.height) - } - Location::Bottom => Point::new( - top_left.x + size.width / 2.0, - top_left.y + size.height, - ), - Location::BottomLeft => { - Point::new(top_left.x, top_left.y + size.height) - } - Location::Left => { - Point::new(top_left.x, top_left.y + size.height / 2.0) - } - } - } -} diff --git a/graphics/src/gradient/linear.rs b/graphics/src/gradient/linear.rs deleted file mode 100644 index c886db47..00000000 --- a/graphics/src/gradient/linear.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Linear gradient builder & definition. -use crate::gradient::{ColorStop, Gradient, Position}; -use crate::{Color, Point}; - -/// A linear gradient that can be used in the style of [`Fill`] or [`Stroke`]. -/// -/// [`Fill`]: crate::widget::canvas::Fill -/// [`Stroke`]: crate::widget::canvas::Stroke -#[derive(Debug, Clone, PartialEq)] -pub struct Linear { - /// The point where the linear gradient begins. - pub start: Point, - /// The point where the linear gradient ends. - pub end: Point, - /// [`ColorStop`]s along the linear gradient path. - pub color_stops: Vec, -} - -/// A [`Linear`] builder. -#[derive(Debug)] -pub struct Builder { - start: Point, - end: Point, - stops: Vec, - error: Option, -} - -impl Builder { - /// Creates a new [`Builder`]. - pub fn new(position: Position) -> Self { - let (start, end) = match position { - Position::Absolute { start, end } => (start, end), - Position::Relative { - top_left, - size, - start, - end, - } => ( - start.to_absolute(top_left, size), - end.to_absolute(top_left, size), - ), - }; - - Self { - start, - end, - stops: vec![], - error: None, - } - } - - /// Adds a new stop, defined by an offset and a color, to the gradient. - /// - /// `offset` must be between `0.0` and `1.0` or the gradient cannot be built. - /// - /// Note: when using the [`glow`] backend, any color stop added after the 16th - /// will not be displayed. - /// - /// On the [`wgpu`] backend this limitation does not exist (technical limit is 524,288 stops). - /// - /// [`glow`]: https://docs.rs/iced_glow - /// [`wgpu`]: https://docs.rs/iced_wgpu - pub fn add_stop(mut self, offset: f32, color: Color) -> Self { - if offset.is_finite() && (0.0..=1.0).contains(&offset) { - match self.stops.binary_search_by(|stop| { - stop.offset.partial_cmp(&offset).unwrap() - }) { - Ok(_) => { - self.error = Some(BuilderError::DuplicateOffset(offset)) - } - Err(index) => { - self.stops.insert(index, ColorStop { offset, color }); - } - } - } else { - self.error = Some(BuilderError::InvalidOffset(offset)) - }; - - self - } - - /// Builds the linear [`Gradient`] of this [`Builder`]. - /// - /// Returns `BuilderError` if gradient in invalid. - pub fn build(self) -> Result { - if self.stops.is_empty() { - Err(BuilderError::MissingColorStop) - } else if let Some(error) = self.error { - Err(error) - } else { - Ok(Gradient::Linear(Linear { - start: self.start, - end: self.end, - color_stops: self.stops, - })) - } - } -} - -/// An error that happened when building a [`Linear`] gradient. -#[derive(Debug, thiserror::Error)] -pub enum BuilderError { - #[error("Gradients must contain at least one color stop.")] - /// Gradients must contain at least one color stop. - MissingColorStop, - #[error("Offset {0} must be a unique, finite number.")] - /// Offsets in a gradient must all be unique & finite. - DuplicateOffset(f32), - #[error("Offset {0} must be between 0.0..=1.0.")] - /// Offsets in a gradient must be between 0.0..=1.0. - InvalidOffset(f32), -} diff --git a/graphics/src/layer.rs b/graphics/src/layer.rs deleted file mode 100644 index f6eb2fdd..00000000 --- a/graphics/src/layer.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Organize rendering primitives into a flattened list of layers. -mod image; -mod quad; -mod text; - -pub mod mesh; - -pub use image::Image; -pub use mesh::Mesh; -pub use quad::Quad; -pub use text::Text; - -use crate::alignment; -use crate::{ - Background, Color, Font, Point, Primitive, Rectangle, Size, Vector, - Viewport, -}; - -/// A group of primitives that should be clipped together. -#[derive(Debug)] -pub struct Layer<'a> { - /// The clipping bounds of the [`Layer`]. - pub bounds: Rectangle, - - /// The quads of the [`Layer`]. - pub quads: Vec, - - /// The triangle meshes of the [`Layer`]. - pub meshes: Vec>, - - /// The text of the [`Layer`]. - pub text: Vec>, - - /// The images of the [`Layer`]. - pub images: Vec, -} - -impl<'a> Layer<'a> { - /// Creates a new [`Layer`] with the given clipping bounds. - pub fn new(bounds: Rectangle) -> Self { - Self { - bounds, - quads: Vec::new(), - meshes: Vec::new(), - text: Vec::new(), - images: Vec::new(), - } - } - - /// Creates a new [`Layer`] for the provided overlay text. - /// - /// This can be useful for displaying debug information. - pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { - let mut overlay = - Layer::new(Rectangle::with_size(viewport.logical_size())); - - for (i, line) in lines.iter().enumerate() { - let text = Text { - 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: 20.0, - font: Font::Monospace, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - }; - - overlay.text.push(text); - - overlay.text.push(Text { - bounds: text.bounds + Vector::new(-1.0, -1.0), - color: Color::BLACK, - ..text - }); - } - - overlay - } - - /// Distributes the given [`Primitive`] and generates a list of layers based - /// on its contents. - pub fn generate( - primitives: &'a [Primitive], - viewport: &Viewport, - ) -> Vec { - 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, - Vector::new(0.0, 0.0), - primitive, - 0, - ); - } - - layers - } - - fn process_primitive( - layers: &mut Vec, - translation: Vector, - primitive: &'a Primitive, - current_layer: usize, - ) { - match primitive { - Primitive::None => {} - Primitive::Group { primitives } => { - // TODO: Inspect a bit and regroup (?) - for primitive in primitives { - Self::process_primitive( - layers, - translation, - primitive, - current_layer, - ) - } - } - Primitive::Text { - content, - bounds, - size, - color, - font, - horizontal_alignment, - vertical_alignment, - } => { - let layer = &mut layers[current_layer]; - - layer.text.push(Text { - content, - bounds: *bounds + translation, - size: *size, - color: *color, - font: *font, - horizontal_alignment: *horizontal_alignment, - vertical_alignment: *vertical_alignment, - }); - } - Primitive::Quad { - bounds, - background, - border_radius, - border_width, - border_color, - } => { - let layer = &mut layers[current_layer]; - - // TODO: Move some of these computations to the GPU (?) - layer.quads.push(Quad { - position: [ - bounds.x + translation.x, - bounds.y + translation.y, - ], - size: [bounds.width, bounds.height], - color: match background { - Background::Color(color) => color.into_linear(), - }, - border_radius: *border_radius, - border_width: *border_width, - border_color: border_color.into_linear(), - }); - } - Primitive::SolidMesh { buffers, size } => { - let layer = &mut layers[current_layer]; - - let bounds = Rectangle::new( - Point::new(translation.x, translation.y), - *size, - ); - - // Only draw visible content - if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.meshes.push(Mesh::Solid { - origin: Point::new(translation.x, translation.y), - buffers, - clip_bounds, - }); - } - } - Primitive::GradientMesh { - buffers, - size, - gradient, - } => { - let layer = &mut layers[current_layer]; - - let bounds = Rectangle::new( - Point::new(translation.x, translation.y), - *size, - ); - - // Only draw visible content - if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { - layer.meshes.push(Mesh::Gradient { - origin: Point::new(translation.x, translation.y), - buffers, - clip_bounds, - gradient, - }); - } - } - Primitive::Clip { bounds, content } => { - let layer = &mut layers[current_layer]; - let translated_bounds = *bounds + translation; - - // 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, - translation, - content, - layers.len() - 1, - ); - } - } - Primitive::Translate { - translation: new_translation, - content, - } => { - Self::process_primitive( - layers, - translation + *new_translation, - content, - current_layer, - ); - } - Primitive::Cached { cache } => { - Self::process_primitive( - layers, - translation, - cache, - current_layer, - ); - } - Primitive::Image { handle, bounds } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Raster { - handle: handle.clone(), - bounds: *bounds + translation, - }); - } - Primitive::Svg { - handle, - color, - bounds, - } => { - let layer = &mut layers[current_layer]; - - layer.images.push(Image::Vector { - handle: handle.clone(), - color: *color, - bounds: *bounds + translation, - }); - } - } - } -} diff --git a/graphics/src/layer/image.rs b/graphics/src/layer/image.rs deleted file mode 100644 index 3eff2397..00000000 --- a/graphics/src/layer/image.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{Color, Rectangle}; - -use iced_native::{image, svg}; - -/// 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 bounds of the image. - bounds: Rectangle, - }, - /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option, - - /// The bounds of the image. - bounds: Rectangle, - }, -} diff --git a/graphics/src/layer/mesh.rs b/graphics/src/layer/mesh.rs deleted file mode 100644 index 7661c5c9..00000000 --- a/graphics/src/layer/mesh.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! A collection of triangle primitives. -use crate::triangle; -use crate::{Gradient, Point, Rectangle}; - -/// A mesh of triangles. -#[derive(Debug, Clone, Copy)] -pub enum Mesh<'a> { - /// A mesh of triangles with a solid color. - Solid { - /// The origin of the vertices of the [`Mesh`]. - origin: Point, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a triangle::Mesh2D, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle, - }, - /// A mesh of triangles with a gradient color. - Gradient { - /// The origin of the vertices of the [`Mesh`]. - origin: Point, - - /// The vertex and index buffers of the [`Mesh`]. - buffers: &'a triangle::Mesh2D, - - /// The clipping bounds of the [`Mesh`]. - clip_bounds: Rectangle, - - /// The gradient to apply to the [`Mesh`]. - gradient: &'a Gradient, - }, -} - -impl Mesh<'_> { - /// Returns the origin of the [`Mesh`]. - pub fn origin(&self) -> Point { - match self { - Self::Solid { origin, .. } | Self::Gradient { origin, .. } => { - *origin - } - } - } - - /// 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 { - 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 gradient vertices. - pub gradient_vertices: 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.solid_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - Mesh::Gradient { buffers, .. } => { - count.gradient_vertices += buffers.vertices.len(); - count.indices += buffers.indices.len(); - } - } - - count - }) -} diff --git a/graphics/src/layer/quad.rs b/graphics/src/layer/quad.rs deleted file mode 100644 index 0d8bde9d..00000000 --- a/graphics/src/layer/quad.rs +++ /dev/null @@ -1,30 +0,0 @@ -/// A colored rectangle with a border. -/// -/// This type can be directly uploaded to GPU memory. -#[derive(Debug, Clone, Copy)] -#[repr(C)] -pub struct Quad { - /// The position of the [`Quad`]. - pub position: [f32; 2], - - /// The size of the [`Quad`]. - pub size: [f32; 2], - - /// The color of the [`Quad`], in __linear RGB__. - pub color: [f32; 4], - - /// The border color of the [`Quad`], in __linear RGB__. - pub border_color: [f32; 4], - - /// The border radius of the [`Quad`]. - pub border_radius: [f32; 4], - - /// The border width of the [`Quad`]. - pub border_width: f32, -} - -#[allow(unsafe_code)] -unsafe impl bytemuck::Zeroable for Quad {} - -#[allow(unsafe_code)] -unsafe impl bytemuck::Pod for Quad {} diff --git a/graphics/src/layer/text.rs b/graphics/src/layer/text.rs deleted file mode 100644 index 38d62616..00000000 --- a/graphics/src/layer/text.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{alignment, Color, Font, Rectangle}; - -/// A paragraph of text. -#[derive(Debug, Clone, Copy)] -pub struct Text<'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`]. - pub size: f32, - - /// 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, -} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index bbbdfa0e..576b2d78 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -9,7 +9,7 @@ )] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unsafe_code, unused_results, clippy::extra_unused_lifetimes, @@ -23,25 +23,19 @@ #![cfg_attr(docsrs, feature(doc_cfg))] mod antialiasing; mod error; -mod primitive; mod transformation; mod viewport; pub mod backend; -pub mod gradient; pub mod image; -pub mod layer; pub mod overlay; +pub mod primitive; pub mod renderer; -pub mod triangle; -pub mod widget; pub mod window; pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; -pub use gradient::Gradient; -pub use layer::Layer; pub use primitive::Primitive; pub use renderer::Renderer; pub use transformation::Transformation; @@ -50,5 +44,6 @@ pub use viewport::Viewport; pub use iced_native::alignment; pub use iced_native::text; pub use iced_native::{ - Alignment, Background, Color, Font, Point, Rectangle, Size, Vector, + Alignment, Background, Color, Font, Gradient, Point, Rectangle, Size, + Vector, }; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 5a163a2f..e4826591 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,23 +1,15 @@ +use crate::alignment; + use iced_native::image; use iced_native::svg; -use iced_native::{Background, Color, Font, Rectangle, Size, Vector}; - -use crate::alignment; -use crate::gradient::Gradient; -use crate::triangle; +use iced_native::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; +use bytemuck::{Pod, Zeroable}; use std::sync::Arc; /// A rendering primitive. #[derive(Debug, Clone)] pub enum Primitive { - /// An empty primitive - None, - /// A group of primitives - Group { - /// The primitives of the group - primitives: Vec, - }, /// A text primitive Text { /// The contents of the text @@ -66,27 +58,12 @@ pub enum Primitive { /// The bounds of the viewport bounds: Rectangle, }, - /// A clip primitive - Clip { - /// The bounds of the clip - bounds: Rectangle, - /// The content of the clip - content: Box, - }, - /// A primitive that applies a translation - Translate { - /// The translation vector - translation: Vector, - - /// The primitive to translate - content: Box, - }, /// A low-level primitive to render a mesh of triangles with a solid color. /// /// It can be used to render many kinds of geometry freely. SolidMesh { /// The vertices and indices of the mesh. - buffers: triangle::Mesh2D, + buffers: Mesh2D, /// The size of the drawable region of the mesh. /// @@ -98,7 +75,7 @@ pub enum Primitive { /// It can be used to render many kinds of geometry freely. GradientMesh { /// The vertices and indices of the mesh. - buffers: triangle::Mesh2D, + buffers: Mesh2D, /// The size of the drawable region of the mesh. /// @@ -108,18 +85,101 @@ pub enum Primitive { /// The [`Gradient`] to apply to the mesh. gradient: Gradient, }, + Fill { + path: tiny_skia::Path, + paint: tiny_skia::Paint<'static>, + rule: tiny_skia::FillRule, + transform: tiny_skia::Transform, + }, + Stroke { + path: tiny_skia::Path, + paint: tiny_skia::Paint<'static>, + stroke: tiny_skia::Stroke, + transform: tiny_skia::Transform, + }, + /// A group of primitives + Group { + /// The primitives of the group + primitives: Vec, + }, + /// A clip primitive + Clip { + /// The bounds of the clip + bounds: Rectangle, + /// The content of the clip + content: Box, + }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box, + }, /// A cached primitive. /// /// This can be useful if you are implementing a widget where primitive /// generation is expensive. - Cached { + Cache { /// The cached primitive - cache: Arc, + content: Arc, }, } -impl Default for Primitive { - fn default() -> Primitive { - Primitive::None +impl Primitive { + pub fn group(primitives: Vec) -> Self { + Self::Group { primitives } + } + + pub fn clip(self, bounds: Rectangle) -> Self { + Self::Clip { + bounds, + content: Box::new(self), + } + } + + pub fn translate(self, translation: Vector) -> Self { + Self::Translate { + translation, + content: Box::new(self), + } + } +} + +/// A set of [`Vertex2D`] and indices representing a list of triangles. +#[derive(Clone, Debug)] +pub struct Mesh2D { + /// The vertices of the mesh + pub vertices: Vec, + + /// The list of vertex indices that defines the triangles of the mesh. + /// + /// Therefore, this list should always have a length that is a multiple of 3. + pub indices: Vec, +} + +/// A two-dimensional vertex. +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[repr(C)] +pub struct Vertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], +} + +/// A two-dimensional vertex with a color. +#[derive(Copy, Clone, Debug, Zeroable, Pod)] +#[repr(C)] +pub struct ColoredVertex2D { + /// The vertex position in 2D space. + pub position: [f32; 2], + + /// The color of the vertex in __linear__ RGBA. + pub color: [f32; 4], +} + +impl From<()> for Primitive { + fn from(_: ()) -> Self { + Self::Group { primitives: vec![] } } } diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 859ebc04..793ee7d7 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,6 +1,7 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; use crate::{Primitive, Vector}; + use iced_native::image; use iced_native::layout; use iced_native::renderer; @@ -70,19 +71,13 @@ where } fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) { - let current_primitives = std::mem::take(&mut self.primitives); + let current = std::mem::take(&mut self.primitives); f(self); - let layer_primitives = - std::mem::replace(&mut self.primitives, current_primitives); + let layer = std::mem::replace(&mut self.primitives, current); - self.primitives.push(Primitive::Clip { - bounds, - content: Box::new(Primitive::Group { - primitives: layer_primitives, - }), - }); + self.primitives.push(Primitive::group(layer).clip(bounds)); } fn with_translation( @@ -90,19 +85,14 @@ where translation: Vector, f: impl FnOnce(&mut Self), ) { - let current_primitives = std::mem::take(&mut self.primitives); + let current = std::mem::take(&mut self.primitives); f(self); - let layer_primitives = - std::mem::replace(&mut self.primitives, current_primitives); + let layer = std::mem::replace(&mut self.primitives, current); - self.primitives.push(Primitive::Translate { - translation, - content: Box::new(Primitive::Group { - primitives: layer_primitives, - }), - }); + self.primitives + .push(Primitive::group(layer).translate(translation)); } fn fill_quad( @@ -199,7 +189,7 @@ where } fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.draw_primitive(Primitive::Image { handle, bounds }) + self.primitives.push(Primitive::Image { handle, bounds }) } } @@ -217,10 +207,23 @@ where color: Option, bounds: Rectangle, ) { - self.draw_primitive(Primitive::Svg { + self.primitives.push(Primitive::Svg { handle, color, bounds, }) } } + +#[cfg(feature = "canvas")] +impl iced_native::widget::canvas::Renderer for Renderer +where + B: Backend, +{ + type Geometry = B::Geometry; + + fn draw(&mut self, layers: Vec) { + self.primitives + .extend(layers.into_iter().map(B::Geometry::into)); + } +} diff --git a/graphics/src/triangle.rs b/graphics/src/triangle.rs index f52b2339..09b61767 100644 --- a/graphics/src/triangle.rs +++ b/graphics/src/triangle.rs @@ -1,33 +1 @@ //! Draw geometry using meshes of triangles. -use bytemuck::{Pod, Zeroable}; - -/// A set of [`Vertex2D`] and indices representing a list of triangles. -#[derive(Clone, Debug)] -pub struct Mesh2D { - /// The vertices of the mesh - pub vertices: Vec, - - /// The list of vertex indices that defines the triangles of the mesh. - /// - /// Therefore, this list should always have a length that is a multiple of 3. - pub indices: Vec, -} - -/// A two-dimensional vertex. -#[derive(Copy, Clone, Debug, Zeroable, Pod)] -#[repr(C)] -pub struct Vertex2D { - /// The vertex position in 2D space. - pub position: [f32; 2], -} - -/// A two-dimensional vertex with a color. -#[derive(Copy, Clone, Debug, Zeroable, Pod)] -#[repr(C)] -pub struct ColoredVertex2D { - /// The vertex position in 2D space. - pub position: [f32; 2], - - /// The color of the vertex in __linear__ RGBA. - pub color: [f32; 4], -} diff --git a/graphics/src/widget.rs b/graphics/src/widget.rs deleted file mode 100644 index e7fab97c..00000000 --- a/graphics/src/widget.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Use the graphical widgets supported out-of-the-box. -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs deleted file mode 100644 index a8d050f5..00000000 --- a/graphics/src/widget/canvas.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub mod event; -pub mod fill; -pub mod path; -pub mod stroke; - -mod cache; -mod cursor; -mod frame; -mod geometry; -mod program; -mod style; -mod text; - -pub use crate::gradient::{self, Gradient}; -pub use cache::Cache; -pub use cursor::Cursor; -pub use event::Event; -pub use fill::{Fill, FillRule}; -pub use frame::Frame; -pub use geometry::Geometry; -pub use path::Path; -pub use program::Program; -pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; -pub use style::Style; -pub use text::Text; - -use crate::{Backend, Primitive, Renderer}; - -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::renderer; -use iced_native::widget::tree::{self, Tree}; -use iced_native::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, -}; - -use std::marker::PhantomData; - -/// A widget capable of drawing 2D graphics. -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub mod widget { -/// # pub use iced_graphics::widget::canvas; -/// # } -/// # pub use iced_native::{Color, Rectangle, Theme}; -/// # } -/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle, Theme}; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { -/// type State = (); -/// -/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ -/// // We prepare a new `Frame` -/// let mut frame = Frame::new(bounds.size()); -/// -/// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); -/// -/// // And fill it with some color -/// frame.fill(&circle, Color::BLACK); -/// -/// // Finally, we produce the geometry -/// vec![frame.into_geometry()] -/// } -/// } -/// -/// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); -/// ``` -#[derive(Debug)] -pub struct Canvas -where - P: Program, -{ - width: Length, - height: Length, - program: P, - message_: PhantomData, - theme_: PhantomData, -} - -impl Canvas -where - P: Program, -{ - const DEFAULT_SIZE: f32 = 100.0; - - /// Creates a new [`Canvas`]. - pub fn new(program: P) -> Self { - Canvas { - width: Length::Fixed(Self::DEFAULT_SIZE), - height: Length::Fixed(Self::DEFAULT_SIZE), - program, - message_: PhantomData, - theme_: PhantomData, - } - } - - /// Sets the width of the [`Canvas`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Canvas`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } -} - -impl Widget> for Canvas -where - P: Program, - B: Backend, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(P::State::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - - let canvas_event = match event { - iced_native::Event::Mouse(mouse_event) => { - Some(Event::Mouse(mouse_event)) - } - iced_native::Event::Touch(touch_event) => { - Some(Event::Touch(touch_event)) - } - iced_native::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - _ => None, - }; - - let cursor = Cursor::from_window_position(cursor_position); - - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::(); - - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); - - if let Some(message) = message { - shell.publish(message); - } - - return event_status; - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - self.program.mouse_interaction(state, bounds, cursor) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &T, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - use iced_native::Renderer as _; - - let bounds = layout.bounds(); - - if bounds.width < 1.0 || bounds.height < 1.0 { - return; - } - - let translation = Vector::new(bounds.x, bounds.y); - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(Primitive::Group { - primitives: self - .program - .draw(state, theme, bounds, cursor) - .into_iter() - .map(Geometry::into_primitive) - .collect(), - }); - }); - } -} - -impl<'a, Message, P, B, T> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - P: Program + 'a, - B: Backend, - T: 'a, -{ - fn from( - canvas: Canvas, - ) -> Element<'a, Message, Renderer> { - Element::new(canvas) - } -} diff --git a/graphics/src/widget/canvas/cache.rs b/graphics/src/widget/canvas/cache.rs deleted file mode 100644 index 52217bbb..00000000 --- a/graphics/src/widget/canvas/cache.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::widget::canvas::{Frame, Geometry}; -use crate::Primitive; - -use iced_native::Size; -use std::{cell::RefCell, sync::Arc}; - -enum State { - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -impl Default for State { - fn default() -> Self { - State::Empty - } -} -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: Default::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Geometry::from_primitive(Primitive::Cached { - cache: primitive.clone(), - }); - } - } - - let mut frame = Frame::new(bounds); - draw_fn(&mut frame); - - let primitive = { - let geometry = frame.into_geometry(); - - Arc::new(geometry.into_primitive()) - }; - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - Geometry::from_primitive(Primitive::Cached { cache: primitive }) - } -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::Empty => write!(f, "Empty"), - State::Filled { primitive, bounds } => f - .debug_struct("Filled") - .field("primitive", primitive) - .field("bounds", bounds) - .finish(), - } - } -} diff --git a/graphics/src/widget/canvas/cursor.rs b/graphics/src/widget/canvas/cursor.rs deleted file mode 100644 index 9588d129..00000000 --- a/graphics/src/widget/canvas/cursor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use iced_native::{Point, Rectangle}; - -/// The mouse cursor state. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Cursor { - /// The cursor has a defined position. - Available(Point), - - /// The cursor is currently unavailable (i.e. out of bounds or busy). - Unavailable, -} - -impl Cursor { - // TODO: Remove this once this type is used in `iced_native` to encode - // proper cursor availability - pub(crate) fn from_window_position(position: Point) -> Self { - if position.x < 0.0 || position.y < 0.0 { - Cursor::Unavailable - } else { - Cursor::Available(position) - } - } - - /// Returns the absolute position of the [`Cursor`], if available. - pub fn position(&self) -> Option { - match self { - Cursor::Available(position) => Some(*position), - Cursor::Unavailable => None, - } - } - - /// Returns the relative position of the [`Cursor`] inside the given bounds, - /// if available. - /// - /// If the [`Cursor`] is not over the provided bounds, this method will - /// return `None`. - pub fn position_in(&self, bounds: &Rectangle) -> Option { - if self.is_over(bounds) { - self.position_from(bounds.position()) - } else { - None - } - } - - /// Returns the relative position of the [`Cursor`] from the given origin, - /// if available. - pub fn position_from(&self, origin: Point) -> Option { - match self { - Cursor::Available(position) => { - Some(Point::new(position.x - origin.x, position.y - origin.y)) - } - Cursor::Unavailable => None, - } - } - - /// Returns whether the [`Cursor`] is currently over the provided bounds - /// or not. - pub fn is_over(&self, bounds: &Rectangle) -> bool { - match self { - Cursor::Available(position) => bounds.contains(*position), - Cursor::Unavailable => false, - } - } -} diff --git a/graphics/src/widget/canvas/event.rs b/graphics/src/widget/canvas/event.rs deleted file mode 100644 index 7c733a4d..00000000 --- a/graphics/src/widget/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use iced_native::keyboard; -use iced_native::mouse; -use iced_native::touch; - -pub use iced_native::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::widget::Canvas -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/graphics/src/widget/canvas/fill.rs b/graphics/src/widget/canvas/fill.rs deleted file mode 100644 index e954ebb5..00000000 --- a/graphics/src/widget/canvas/fill.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Fill [crate::widget::canvas::Geometry] with a certain style. -use crate::{Color, Gradient}; - -pub use crate::widget::canvas::Style; - -/// The style used to fill geometry. -#[derive(Debug, Clone)] -pub struct Fill { - /// The color or gradient of the fill. - /// - /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - - /// The fill rule defines how to determine what is inside and what is - /// outside of a shape. - /// - /// See the [SVG specification][1] for more details. - /// - /// By default, it is set to `NonZero`. - /// - /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty - pub rule: FillRule, -} - -impl Default for Fill { - fn default() -> Self { - Self { - style: Style::Solid(Color::BLACK), - rule: FillRule::NonZero, - } - } -} - -impl From for Fill { - fn from(color: Color) -> Fill { - Fill { - style: Style::Solid(color), - ..Fill::default() - } - } -} - -impl From for Fill { - fn from(gradient: Gradient) -> Self { - Fill { - style: Style::Gradient(gradient), - ..Default::default() - } - } -} - -/// The fill rule defines how to determine what is inside and what is outside of -/// a shape. -/// -/// See the [SVG specification][1]. -/// -/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum FillRule { - NonZero, - EvenOdd, -} - -impl From for lyon::tessellation::FillRule { - fn from(rule: FillRule) -> lyon::tessellation::FillRule { - match rule { - FillRule::NonZero => lyon::tessellation::FillRule::NonZero, - FillRule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, - } - } -} diff --git a/graphics/src/widget/canvas/frame.rs b/graphics/src/widget/canvas/frame.rs deleted file mode 100644 index d68548ae..00000000 --- a/graphics/src/widget/canvas/frame.rs +++ /dev/null @@ -1,530 +0,0 @@ -use crate::gradient::Gradient; -use crate::triangle; -use crate::widget::canvas::{path, Fill, Geometry, Path, Stroke, Style, Text}; -use crate::Primitive; - -use iced_native::{Point, Rectangle, Size, Vector}; - -use lyon::geom::euclid; -use lyon::tessellation; -use std::borrow::Cow; - -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - gradient.clone(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } - - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } - - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } -} - -#[derive(Debug)] -struct Transforms { - previous: Vec, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} - -impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { - let transformed = self - .raw - .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - let (start, end) = match &mut gradient { - Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), - }; - self.transform_point(start); - self.transform_point(end); - gradient - } -} - -impl Frame { - /// 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. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = - tessellation::FillOptions::default().with_fill_rule(rule.into()); - - if self.transforms.current.is_identity { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transformed(&self.transforms.current.raw); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); - - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = - tessellation::FillOptions::default().with_fill_rule(rule.into()); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = stroke.line_cap.into(); - options.end_cap = stroke.line_cap.into(); - options.line_join = stroke.line_join.into(); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(path::dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transformed(&self.transforms.current.raw); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); - - Point::new(transformed.x, transformed.y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - self.transforms.previous.push(self.transforms.current); - - f(self); - - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = Frame::new(region.size()); - - f(&mut frame); - - let primitives = frame.into_primitives(); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - let translation = Vector::new(region.x, region.y); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(region.size()), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale, scale); - self.transforms.current.is_identity = false; - } - - /// Produces the [`Geometry`] representing everything drawn on the [`Frame`]. - pub fn into_geometry(self) -> Geometry { - Geometry::from_primitive(Primitive::Group { - primitives: self.into_primitives(), - }) - } - - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: triangle::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) - } - } - Buffer::Gradient(buffer, gradient) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: triangle::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - gradient, - }) - } - } - } - } - - self.primitives - } -} - -struct Vertex2DBuilder; - -impl tessellation::FillVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> triangle::Vertex2D { - let position = vertex.position(); - - triangle::Vertex2D { - position: [position.x, position.y], - } - } -} - -impl tessellation::StrokeVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> triangle::Vertex2D { - let position = vertex.position(); - - triangle::Vertex2D { - position: [position.x, position.y], - } - } -} - -struct TriangleVertex2DBuilder([f32; 4]); - -impl tessellation::FillVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> triangle::ColoredVertex2D { - let position = vertex.position(); - - triangle::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -impl tessellation::StrokeVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> triangle::ColoredVertex2D { - let position = vertex.position(); - - triangle::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} diff --git a/graphics/src/widget/canvas/geometry.rs b/graphics/src/widget/canvas/geometry.rs deleted file mode 100644 index e8ac621d..00000000 --- a/graphics/src/widget/canvas/geometry.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::Primitive; - -/// A bunch of shapes that can be drawn. -/// -/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a -/// [`Cache`]. -/// -/// [`Frame`]: crate::widget::canvas::Frame -/// [`Cache`]: crate::widget::canvas::Cache -#[derive(Debug, Clone)] -pub struct Geometry(Primitive); - -impl Geometry { - pub(crate) fn from_primitive(primitive: Primitive) -> Self { - Self(primitive) - } - - /// Turns the [`Geometry`] into a [`Primitive`]. - /// - /// This can be useful if you are building a custom widget. - pub fn into_primitive(self) -> Primitive { - self.0 - } -} diff --git a/graphics/src/widget/canvas/path.rs b/graphics/src/widget/canvas/path.rs deleted file mode 100644 index aeb2589e..00000000 --- a/graphics/src/widget/canvas/path.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Build different kinds of 2D shapes. -pub mod arc; - -mod builder; - -#[doc(no_inline)] -pub use arc::Arc; -pub use builder::Builder; - -use crate::widget::canvas::LineDash; - -use iced_native::{Point, Size}; -use lyon::algorithms::walk::{walk_along_path, RepeatedPattern, WalkerEvent}; -use lyon::path::iterator::PathIterator; - -/// An immutable set of points that may or may not be connected. -/// -/// A single [`Path`] can represent different kinds of 2D shapes! -#[derive(Debug, Clone)] -pub struct Path { - raw: lyon::path::Path, -} - -impl Path { - /// Creates a new [`Path`] with the provided closure. - /// - /// Use the [`Builder`] to configure your [`Path`]. - pub fn new(f: impl FnOnce(&mut Builder)) -> Self { - let mut builder = Builder::new(); - - // TODO: Make it pure instead of side-effect-based (?) - f(&mut builder); - - builder.build() - } - - /// Creates a new [`Path`] representing a line segment given its starting - /// and end points. - pub fn line(from: Point, to: Point) -> Self { - Self::new(|p| { - p.move_to(from); - p.line_to(to); - }) - } - - /// Creates a new [`Path`] representing a rectangle given its top-left - /// corner coordinate and its `Size`. - pub fn rectangle(top_left: Point, size: Size) -> Self { - Self::new(|p| p.rectangle(top_left, size)) - } - - /// Creates a new [`Path`] representing a circle given its center - /// coordinate and its radius. - pub fn circle(center: Point, radius: f32) -> Self { - Self::new(|p| p.circle(center, radius)) - } - - #[inline] - pub(crate) fn raw(&self) -> &lyon::path::Path { - &self.raw - } - - #[inline] - pub(crate) fn transformed( - &self, - transform: &lyon::math::Transform, - ) -> Path { - Path { - raw: self.raw.clone().transformed(transform), - } - } -} - -pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { - Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1) - .then(|| [line_dash.segments, line_dash.segments].concat()); - - let mut draw_line = false; - - walk_along_path( - path.raw().iter().flattened(0.01), - 0.0, - lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, - &mut RepeatedPattern { - callback: |event: WalkerEvent<'_>| { - let point = Point { - x: event.position.x, - y: event.position.y, - }; - - if draw_line { - builder.line_to(point); - } else { - builder.move_to(point); - } - - draw_line = !draw_line; - - true - }, - index: line_dash.offset, - intervals: segments_odd - .as_deref() - .unwrap_or(line_dash.segments), - }, - ); - }) -} diff --git a/graphics/src/widget/canvas/path/arc.rs b/graphics/src/widget/canvas/path/arc.rs deleted file mode 100644 index b8e72daf..00000000 --- a/graphics/src/widget/canvas/path/arc.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Build and draw curves. -use iced_native::{Point, Vector}; - -/// A segment of a differentiable curve. -#[derive(Debug, Clone, Copy)] -pub struct Arc { - /// The center of the arc. - pub center: Point, - /// The radius of the arc. - pub radius: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -/// An elliptical [`Arc`]. -#[derive(Debug, Clone, Copy)] -pub struct Elliptical { - /// The center of the arc. - pub center: Point, - /// The radii of the arc's ellipse, defining its axes. - pub radii: Vector, - /// The rotation of the arc's ellipse. - pub rotation: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -impl From for Elliptical { - fn from(arc: Arc) -> Elliptical { - Elliptical { - center: arc.center, - radii: Vector::new(arc.radius, arc.radius), - rotation: 0.0, - start_angle: arc.start_angle, - end_angle: arc.end_angle, - } - } -} diff --git a/graphics/src/widget/canvas/path/builder.rs b/graphics/src/widget/canvas/path/builder.rs deleted file mode 100644 index 5121aa68..00000000 --- a/graphics/src/widget/canvas/path/builder.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::widget::canvas::path::{arc, Arc, Path}; - -use iced_native::{Point, Size}; -use lyon::path::builder::SvgPathBuilder; - -/// A [`Path`] builder. -/// -/// Once a [`Path`] is built, it can no longer be mutated. -#[allow(missing_debug_implementations)] -pub struct Builder { - raw: lyon::path::builder::WithSvg, -} - -impl Builder { - /// Creates a new [`Builder`]. - pub fn new() -> Builder { - Builder { - raw: lyon::path::Path::builder().with_svg(), - } - } - - /// Moves the starting point of a new sub-path to the given `Point`. - #[inline] - pub fn move_to(&mut self, point: Point) { - let _ = self.raw.move_to(lyon::math::Point::new(point.x, point.y)); - } - - /// Connects the last point in the [`Path`] to the given `Point` with a - /// straight line. - #[inline] - pub fn line_to(&mut self, point: Point) { - let _ = self.raw.line_to(lyon::math::Point::new(point.x, point.y)); - } - - /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in - /// a clockwise direction. - #[inline] - pub fn arc(&mut self, arc: Arc) { - self.ellipse(arc.into()); - } - - /// Adds a circular arc to the [`Path`] with the given control points and - /// radius. - /// - /// This essentially draws a straight line segment from the current - /// position to `a`, but fits a circular arc of `radius` tangent to that - /// segment and tangent to the line between `a` and `b`. - /// - /// With another `.line_to(b)`, the result will be a path connecting the - /// starting point and `b` with straight line segments towards `a` and a - /// circular arc smoothing out the corner at `a`. - /// - /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) - /// for more details and examples. - pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { - use lyon::{math, path}; - - let start = self.raw.current_position(); - let mid = math::Point::new(a.x, a.y); - let end = math::Point::new(b.x, b.y); - - if start == mid || mid == end || radius == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let double_area = start.x * (mid.y - end.y) - + mid.x * (end.y - start.y) - + end.x * (start.y - mid.y); - - if double_area == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let to_start = (start - mid).normalize(); - let to_end = (end - mid).normalize(); - - let inner_angle = to_start.dot(to_end).acos(); - - let origin_angle = inner_angle / 2.0; - - let origin_adjacent = radius / origin_angle.tan(); - - let arc_start = mid + to_start * origin_adjacent; - let arc_end = mid + to_end * origin_adjacent; - - let sweep = to_start.cross(to_end) < 0.0; - - let _ = self.raw.line_to(arc_start); - - self.raw.arc_to( - math::Vector::new(radius, radius), - math::Angle::radians(0.0), - path::ArcFlags { - large_arc: false, - sweep, - }, - arc_end, - ); - } - - /// Adds an ellipse to the [`Path`] using a clockwise direction. - pub fn ellipse(&mut self, arc: arc::Elliptical) { - use lyon::{geom, math}; - - let arc = geom::Arc { - center: math::Point::new(arc.center.x, arc.center.y), - radii: math::Vector::new(arc.radii.x, arc.radii.y), - x_rotation: math::Angle::radians(arc.rotation), - start_angle: math::Angle::radians(arc.start_angle), - sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), - }; - - let _ = self.raw.move_to(arc.sample(0.0)); - - arc.for_each_quadratic_bezier(&mut |curve| { - let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); - }); - } - - /// Adds a cubic Bézier curve to the [`Path`] given its two control points - /// and its end point. - #[inline] - pub fn bezier_curve_to( - &mut self, - control_a: Point, - control_b: Point, - to: Point, - ) { - use lyon::math; - - let _ = self.raw.cubic_bezier_to( - math::Point::new(control_a.x, control_a.y), - math::Point::new(control_b.x, control_b.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a quadratic Bézier curve to the [`Path`] given its control point - /// and its end point. - #[inline] - pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { - use lyon::math; - - let _ = self.raw.quadratic_bezier_to( - math::Point::new(control.x, control.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a rectangle to the [`Path`] given its top-left corner coordinate - /// and its `Size`. - #[inline] - pub fn rectangle(&mut self, top_left: Point, size: Size) { - self.move_to(top_left); - self.line_to(Point::new(top_left.x + size.width, top_left.y)); - self.line_to(Point::new( - top_left.x + size.width, - top_left.y + size.height, - )); - self.line_to(Point::new(top_left.x, top_left.y + size.height)); - self.close(); - } - - /// Adds a circle to the [`Path`] given its center coordinate and its - /// radius. - #[inline] - pub fn circle(&mut self, center: Point, radius: f32) { - self.arc(Arc { - center, - radius, - start_angle: 0.0, - end_angle: 2.0 * std::f32::consts::PI, - }); - } - - /// Closes the current sub-path in the [`Path`] with a straight line to - /// the starting point. - #[inline] - pub fn close(&mut self) { - self.raw.close() - } - - /// Builds the [`Path`] of this [`Builder`]. - #[inline] - pub fn build(self) -> Path { - Path { - raw: self.raw.build(), - } - } -} - -impl Default for Builder { - fn default() -> Self { - Self::new() - } -} diff --git a/graphics/src/widget/canvas/program.rs b/graphics/src/widget/canvas/program.rs deleted file mode 100644 index 656dbfa6..00000000 --- a/graphics/src/widget/canvas/program.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::widget::canvas::event::{self, Event}; -use crate::widget::canvas::mouse; -use crate::widget::canvas::{Cursor, Geometry}; -use crate::Rectangle; - -/// The state and logic of a [`Canvas`]. -/// -/// A [`Program`] can mutate internal state and produce messages for an -/// application. -/// -/// [`Canvas`]: crate::widget::Canvas -pub trait Program { - /// The internal state mutated by the [`Program`]. - type State: Default + 'static; - - /// Updates the [`State`](Self::State) of the [`Program`]. - /// - /// When a [`Program`] is used in a [`Canvas`], the runtime will call this - /// method for each [`Event`]. - /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. - /// - /// By default, this method does and returns nothing. - /// - /// [`Canvas`]: crate::widget::Canvas - fn update( - &self, - _state: &mut Self::State, - _event: Event, - _bounds: Rectangle, - _cursor: Cursor, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) - } - - /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. - /// - /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a - /// [`Cache`]. - /// - /// [`Frame`]: crate::widget::canvas::Frame - /// [`Cache`]: crate::widget::canvas::Cache - fn draw( - &self, - state: &Self::State, - theme: &Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec; - - /// Returns the current mouse interaction of the [`Program`]. - /// - /// The interaction returned will be in effect even if the cursor position - /// is out of bounds of the program's [`Canvas`]. - /// - /// [`Canvas`]: crate::widget::Canvas - fn mouse_interaction( - &self, - _state: &Self::State, - _bounds: Rectangle, - _cursor: Cursor, - ) -> mouse::Interaction { - mouse::Interaction::default() - } -} - -impl Program for &T -where - T: Program, -{ - type State = T::State; - - fn update( - &self, - state: &mut Self::State, - event: Event, - bounds: Rectangle, - cursor: Cursor, - ) -> (event::Status, Option) { - T::update(self, state, event, bounds, cursor) - } - - fn draw( - &self, - state: &Self::State, - theme: &Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec { - T::draw(self, state, theme, bounds, cursor) - } - - fn mouse_interaction( - &self, - state: &Self::State, - bounds: Rectangle, - cursor: Cursor, - ) -> mouse::Interaction { - T::mouse_interaction(self, state, bounds, cursor) - } -} diff --git a/graphics/src/widget/canvas/stroke.rs b/graphics/src/widget/canvas/stroke.rs deleted file mode 100644 index 4c19251d..00000000 --- a/graphics/src/widget/canvas/stroke.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. -pub use crate::widget::canvas::Style; - -use iced_native::Color; - -/// The style of a stroke. -#[derive(Debug, Clone)] -pub struct Stroke<'a> { - /// The color or gradient of the stroke. - /// - /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - /// The distance between the two edges of the stroke. - pub width: f32, - /// The shape to be used at the end of open subpaths when they are stroked. - pub line_cap: LineCap, - /// The shape to be used at the corners of paths or basic shapes when they - /// are stroked. - pub line_join: LineJoin, - /// The dash pattern used when stroking the line. - pub line_dash: LineDash<'a>, -} - -impl<'a> Stroke<'a> { - /// Sets the color of the [`Stroke`]. - pub fn with_color(self, color: Color) -> Self { - Stroke { - style: Style::Solid(color), - ..self - } - } - - /// Sets the width of the [`Stroke`]. - pub fn with_width(self, width: f32) -> Self { - Stroke { width, ..self } - } - - /// Sets the [`LineCap`] of the [`Stroke`]. - pub fn with_line_cap(self, line_cap: LineCap) -> Self { - Stroke { line_cap, ..self } - } - - /// Sets the [`LineJoin`] of the [`Stroke`]. - pub fn with_line_join(self, line_join: LineJoin) -> Self { - Stroke { line_join, ..self } - } -} - -impl<'a> Default for Stroke<'a> { - fn default() -> Self { - Stroke { - style: Style::Solid(Color::BLACK), - width: 1.0, - line_cap: LineCap::default(), - line_join: LineJoin::default(), - line_dash: LineDash::default(), - } - } -} - -/// The shape used at the end of open subpaths when they are stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineCap { - /// The stroke for each sub-path does not extend beyond its two endpoints. - Butt, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a square. - Square, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a semicircle. - Round, -} - -impl Default for LineCap { - fn default() -> LineCap { - LineCap::Butt - } -} - -impl From for lyon::tessellation::LineCap { - fn from(line_cap: LineCap) -> lyon::tessellation::LineCap { - match line_cap { - LineCap::Butt => lyon::tessellation::LineCap::Butt, - LineCap::Square => lyon::tessellation::LineCap::Square, - LineCap::Round => lyon::tessellation::LineCap::Round, - } - } -} - -/// The shape used at the corners of paths or basic shapes when they are -/// stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineJoin { - /// A sharp corner. - Miter, - /// A round corner. - Round, - /// A bevelled corner. - Bevel, -} - -impl Default for LineJoin { - fn default() -> LineJoin { - LineJoin::Miter - } -} - -impl From for lyon::tessellation::LineJoin { - fn from(line_join: LineJoin) -> lyon::tessellation::LineJoin { - match line_join { - LineJoin::Miter => lyon::tessellation::LineJoin::Miter, - LineJoin::Round => lyon::tessellation::LineJoin::Round, - LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, - } - } -} - -/// The dash pattern used when stroking the line. -#[derive(Debug, Clone, Copy, Default)] -pub struct LineDash<'a> { - /// The alternating lengths of lines and gaps which describe the pattern. - pub segments: &'a [f32], - - /// The offset of [`LineDash::segments`] to start the pattern. - pub offset: usize, -} diff --git a/graphics/src/widget/canvas/style.rs b/graphics/src/widget/canvas/style.rs deleted file mode 100644 index 6794f2e7..00000000 --- a/graphics/src/widget/canvas/style.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{Color, Gradient}; - -/// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] -pub enum Style { - /// A solid [`Color`]. - Solid(Color), - - /// A [`Gradient`] color. - Gradient(Gradient), -} - -impl From for Style { - fn from(color: Color) -> Self { - Self::Solid(color) - } -} - -impl From for Style { - fn from(gradient: Gradient) -> Self { - Self::Gradient(gradient) - } -} diff --git a/graphics/src/widget/canvas/text.rs b/graphics/src/widget/canvas/text.rs deleted file mode 100644 index 8c0b2dfb..00000000 --- a/graphics/src/widget/canvas/text.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::alignment; -use crate::{Color, Font, Point}; - -/// A bunch of text that can be drawn to a canvas -#[derive(Debug, Clone)] -pub struct Text { - /// The contents of the text - pub content: String, - /// The position of the text relative to the alignment properties. - /// By default, this position will be relative to the top-left corner coordinate meaning that - /// if the horizontal and vertical alignments are unchanged, this property will tell where the - /// top-left corner of the text should be placed. - /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to - /// change what part of text is placed at this positions. - /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the - /// center of the text will be placed at the given position NOT the top-left coordinate. - pub position: Point, - /// The color of the text - pub color: Color, - /// The size of the text - pub size: f32, - /// 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, -} - -impl Default for Text { - fn default() -> Text { - Text { - content: String::new(), - position: Point::ORIGIN, - color: Color::BLACK, - size: 16.0, - font: Font::SansSerif, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - } - } -} - -impl From for Text { - fn from(content: String) -> Text { - Text { - content, - ..Default::default() - } - } -} - -impl From<&str> for Text { - fn from(content: &str) -> Text { - String::from(content).into() - } -} diff --git a/graphics/src/widget/qr_code.rs b/graphics/src/widget/qr_code.rs deleted file mode 100644 index 12ce5b1f..00000000 --- a/graphics/src/widget/qr_code.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Encode and display information in a QR code. -use crate::renderer::{self, Renderer}; -use crate::widget::canvas; -use crate::Backend; - -use iced_native::layout; -use iced_native::widget::Tree; -use iced_native::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, -}; -use thiserror::Error; - -const DEFAULT_CELL_SIZE: u16 = 4; -const QUIET_ZONE: usize = 2; - -/// A type of matrix barcode consisting of squares arranged in a grid which -/// can be read by an imaging device, such as a camera. -#[derive(Debug)] -pub struct QRCode<'a> { - state: &'a State, - dark: Color, - light: Color, - cell_size: u16, -} - -impl<'a> QRCode<'a> { - /// Creates a new [`QRCode`] with the provided [`State`]. - pub fn new(state: &'a State) -> Self { - Self { - cell_size: DEFAULT_CELL_SIZE, - dark: Color::BLACK, - light: Color::WHITE, - state, - } - } - - /// Sets both the dark and light [`Color`]s of the [`QRCode`]. - pub fn color(mut self, dark: Color, light: Color) -> Self { - self.dark = dark; - self.light = light; - self - } - - /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: u16) -> Self { - self.cell_size = cell_size; - self - } -} - -impl<'a, Message, B, T> Widget> for QRCode<'a> -where - B: Backend, -{ - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 - * f32::from(self.cell_size); - - layout::Node::new(Size::new(side_length, side_length)) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - _theme: &T, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - use iced_native::Renderer as _; - - let bounds = layout.bounds(); - let side_length = self.state.width + 2 * QUIET_ZONE; - - // Reuse cache if possible - let geometry = self.state.cache.draw(bounds.size(), |frame| { - // Scale units to cell size - frame.scale(f32::from(self.cell_size)); - - // Draw background - frame.fill_rectangle( - Point::ORIGIN, - Size::new(side_length as f32, side_length as f32), - self.light, - ); - - // Avoid drawing on the quiet zone - frame.translate(Vector::new(QUIET_ZONE as f32, QUIET_ZONE as f32)); - - // Draw contents - self.state - .contents - .iter() - .enumerate() - .filter(|(_, value)| **value == qrcode::Color::Dark) - .for_each(|(index, _)| { - let row = index / self.state.width; - let column = index % self.state.width; - - frame.fill_rectangle( - Point::new(column as f32, row as f32), - Size::UNIT, - self.dark, - ); - }); - }); - - let translation = Vector::new(bounds.x, bounds.y); - - renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(geometry.into_primitive()); - }); - } -} - -impl<'a, Message, B, T> From> - for Element<'a, Message, Renderer> -where - B: Backend, -{ - fn from(qr_code: QRCode<'a>) -> Self { - Self::new(qr_code) - } -} - -/// The state of a [`QRCode`]. -/// -/// It stores the data that will be displayed. -#[derive(Debug)] -pub struct State { - contents: Vec, - width: usize, - cache: canvas::Cache, -} - -impl State { - /// Creates a new [`State`] with the provided data. - /// - /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest - /// size to display the data. - pub fn new(data: impl AsRef<[u8]>) -> Result { - let encoded = qrcode::QrCode::new(data)?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`ErrorCorrection`]. - pub fn with_error_correction( - data: impl AsRef<[u8]>, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_error_correction_level( - data, - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`Version`] and - /// [`ErrorCorrection`]. - pub fn with_version( - data: impl AsRef<[u8]>, - version: Version, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_version( - data, - version.into(), - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - fn build(encoded: qrcode::QrCode) -> Self { - let width = encoded.width(); - let contents = encoded.into_colors(); - - Self { - contents, - width, - cache: canvas::Cache::new(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// The size of a [`QRCode`]. -/// -/// The higher the version the larger the grid of cells, and therefore the more -/// information the [`QRCode`] can carry. -pub enum Version { - /// A normal QR code version. It should be between 1 and 40. - Normal(u8), - - /// A micro QR code version. It should be between 1 and 4. - Micro(u8), -} - -impl From for qrcode::Version { - fn from(version: Version) -> Self { - match version { - Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), - Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), - } - } -} - -/// The error correction level. -/// -/// It controls the amount of data that can be damaged while still being able -/// to recover the original information. -/// -/// A higher error correction level allows for more corrupted data. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ErrorCorrection { - /// Low error correction. 7% of the data can be restored. - Low, - /// Medium error correction. 15% of the data can be restored. - Medium, - /// Quartile error correction. 25% of the data can be restored. - Quartile, - /// High error correction. 30% of the data can be restored. - High, -} - -impl From for qrcode::EcLevel { - fn from(ec_level: ErrorCorrection) -> Self { - match ec_level { - ErrorCorrection::Low => qrcode::EcLevel::L, - ErrorCorrection::Medium => qrcode::EcLevel::M, - ErrorCorrection::Quartile => qrcode::EcLevel::Q, - ErrorCorrection::High => qrcode::EcLevel::H, - } - } -} - -/// An error that occurred when building a [`State`] for a [`QRCode`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] -pub enum Error { - /// The data is too long to encode in a QR code for the chosen [`Version`]. - #[error( - "The data is too long to encode in a QR code for the chosen version" - )] - DataTooLong, - - /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. - #[error( - "The chosen version and error correction level combination is invalid." - )] - InvalidVersion, - - /// One or more characters in the provided data are not supported by the - /// chosen [`Version`]. - #[error( - "One or more characters in the provided data are not supported by the \ - chosen version" - )] - UnsupportedCharacterSet, - - /// The chosen ECI designator is invalid. A valid designator should be - /// between 0 and 999999. - #[error( - "The chosen ECI designator is invalid. A valid designator should be \ - between 0 and 999999." - )] - InvalidEciDesignator, - - /// A character that does not belong to the character set was found. - #[error("A character that does not belong to the character set was found")] - InvalidCharacter, -} - -impl From for Error { - fn from(error: qrcode::types::QrError) -> Self { - use qrcode::types::QrError; - - match error { - QrError::DataTooLong => Error::DataTooLong, - QrError::InvalidVersion => Error::InvalidVersion, - QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, - QrError::InvalidEciDesignator => Error::InvalidEciDesignator, - QrError::InvalidCharacter => Error::InvalidCharacter, - } - } -} diff --git a/native/Cargo.toml b/native/Cargo.toml index 3f92783e..23533e33 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,12 +8,14 @@ license = "MIT" repository = "https://github.com/iced-rs/iced" [features] +canvas = ["lyon_path"] debug = [] [dependencies] twox-hash = { version = "1.5", default-features = false } unicode-segmentation = "1.6" num-traits = "0.2" +thiserror = "1" [dependencies.iced_core] version = "0.8" @@ -27,3 +29,7 @@ features = ["thread-pool"] [dependencies.iced_style] version = "0.7" path = "../style" + +[dependencies.lyon_path] +version = "1" +optional = true diff --git a/native/src/lib.rs b/native/src/lib.rs index 27b6fc0d..c98827e7 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -33,7 +33,7 @@ )] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unused_results, clippy::extra_unused_lifetimes, clippy::from_over_into, @@ -79,6 +79,7 @@ mod debug; mod debug; pub use iced_core::alignment; +pub use iced_core::gradient; pub use iced_core::time; pub use iced_core::{ color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, @@ -97,6 +98,7 @@ pub use debug::Debug; 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 overlay::Overlay; diff --git a/native/src/widget.rs b/native/src/widget.rs index 2b3ca7be..27330894 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -83,6 +83,22 @@ pub use tree::Tree; #[doc(no_inline)] pub use vertical_slider::VerticalSlider; +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +#[doc(no_inline)] +pub use qr_code::QRCode; + pub use action::Action; pub use id::Id; pub use operation::Operation; diff --git a/native/src/widget/canvas.rs b/native/src/widget/canvas.rs new file mode 100644 index 00000000..8a9addd2 --- /dev/null +++ b/native/src/widget/canvas.rs @@ -0,0 +1,259 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +pub mod event; +pub mod fill; +pub mod path; +pub mod stroke; + +mod cursor; +mod program; +mod style; +mod text; + +pub use crate::gradient::{self, Gradient}; +pub use cursor::Cursor; +pub use event::Event; +pub use fill::Fill; +pub use path::Path; +pub use program::Program; +pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; +pub use style::Style; +pub use text::Text; + +use crate::layout::{self, Layout}; +use crate::mouse; +use crate::renderer; +use crate::widget::tree::{self, Tree}; +use crate::{ + Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, +}; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// # mod iced { +/// # pub mod widget { +/// # pub use iced_graphics::widget::canvas; +/// # } +/// # pub use iced_native::{Color, Rectangle, Theme}; +/// # } +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle, Theme}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas +where + Renderer: self::Renderer, + P: Program, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData, + theme_: PhantomData, +} + +impl Canvas +where + Renderer: self::Renderer, + P: Program, +{ + const DEFAULT_SIZE: f32 = 100.0; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Fixed(Self::DEFAULT_SIZE), + height: Length::Fixed(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + theme_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } +} + +impl Widget + for Canvas +where + Renderer: self::Renderer, + P: Program, +{ + fn tag(&self) -> tree::Tag { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: crate::Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + crate::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), + crate::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), + crate::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( + self.program.draw(state, renderer, theme, bounds, cursor), + ); + }, + ); + } +} + +impl<'a, Message, Renderer, P> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + self::Renderer, + P: Program + 'a, +{ + fn from( + canvas: Canvas, + ) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} + +pub trait Renderer: crate::Renderer { + type Geometry; + + fn draw(&mut self, geometry: Vec); +} diff --git a/native/src/widget/canvas/cursor.rs b/native/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..ef6a7771 --- /dev/null +++ b/native/src/widget/canvas/cursor.rs @@ -0,0 +1,64 @@ +use crate::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + pub fn position(&self) -> Option { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + pub fn position_in(&self, bounds: &Rectangle) -> Option { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + pub fn position_from(&self, origin: Point) -> Option { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/native/src/widget/canvas/event.rs b/native/src/widget/canvas/event.rs new file mode 100644 index 00000000..1d726577 --- /dev/null +++ b/native/src/widget/canvas/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a canvas. +use crate::keyboard; +use crate::mouse; +use crate::touch; + +pub use crate::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/native/src/widget/canvas/fill.rs b/native/src/widget/canvas/fill.rs new file mode 100644 index 00000000..92b1e47e --- /dev/null +++ b/native/src/widget/canvas/fill.rs @@ -0,0 +1,64 @@ +//! Fill [crate::widget::canvas::Geometry] with a certain style. +use crate::widget::canvas::Gradient; +use crate::Color; + +pub use crate::widget::canvas::Style; + +/// The style used to fill geometry. +#[derive(Debug, Clone)] +pub struct Fill { + /// The color or gradient of the fill. + /// + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: Rule, +} + +impl Default for Fill { + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), + rule: Rule::NonZero, + } + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill { + style: Style::Solid(color), + ..Fill::default() + } + } +} + +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Rule { + NonZero, + EvenOdd, +} diff --git a/native/src/widget/canvas/path.rs b/native/src/widget/canvas/path.rs new file mode 100644 index 00000000..30c387c5 --- /dev/null +++ b/native/src/widget/canvas/path.rs @@ -0,0 +1,67 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +pub use lyon_path; + +use crate::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon_path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + pub fn rectangle(top_left: Point, size: Size) -> Self { + Self::new(|p| p.rectangle(top_left, size)) + } + + /// Creates a new [`Path`] representing a circle given its center + /// coordinate and its radius. + pub fn circle(center: Point, radius: f32) -> Self { + Self::new(|p| p.circle(center, radius)) + } + + #[inline] + pub fn raw(&self) -> &lyon_path::Path { + &self.raw + } + + #[inline] + pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { + Path { + raw: self.raw.clone().transformed(transform), + } + } +} diff --git a/native/src/widget/canvas/path/arc.rs b/native/src/widget/canvas/path/arc.rs new file mode 100644 index 00000000..e0747d3e --- /dev/null +++ b/native/src/widget/canvas/path/arc.rs @@ -0,0 +1,42 @@ +//! Build and draw curves. +use crate::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/native/src/widget/canvas/path/builder.rs b/native/src/widget/canvas/path/builder.rs new file mode 100644 index 00000000..84fda052 --- /dev/null +++ b/native/src/widget/canvas/path/builder.rs @@ -0,0 +1,192 @@ +use crate::widget::canvas::path::{arc, Arc, Path}; +use crate::{Point, Size}; + +use lyon_path::builder::{self, SvgPathBuilder}; +use lyon_path::geom; +use lyon_path::math; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: builder::WithSvg, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new() -> Builder { + Builder { + raw: lyon_path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); + + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + lyon_path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, + ); + } + + /// Adds an ellipse to the [`Path`] using a clockwise direction. + pub fn ellipse(&mut self, arc: arc::Elliptical) { + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/native/src/widget/canvas/program.rs b/native/src/widget/canvas/program.rs new file mode 100644 index 00000000..17a5a137 --- /dev/null +++ b/native/src/widget/canvas/program.rs @@ -0,0 +1,108 @@ +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::{Cursor, Renderer}; +use crate::Rectangle; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program +where + Renderer: self::Renderer, +{ + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> (event::Status, Option) { + (event::Status::Ignored, None) + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl Program for &T +where + Renderer: self::Renderer, + T: Program, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/native/src/widget/canvas/stroke.rs b/native/src/widget/canvas/stroke.rs new file mode 100644 index 00000000..ab4727b2 --- /dev/null +++ b/native/src/widget/canvas/stroke.rs @@ -0,0 +1,106 @@ +//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. +pub use crate::widget::canvas::Style; + +use crate::Color; + +/// The style of a stroke. +#[derive(Debug, Clone)] +pub struct Stroke<'a> { + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, + /// The dash pattern used when stroking the line. + pub line_dash: LineDash<'a>, +} + +impl<'a> Stroke<'a> { + /// Sets the color of the [`Stroke`]. + pub fn with_color(self, color: Color) -> Self { + Stroke { + style: Style::Solid(color), + ..self + } + } + + /// Sets the width of the [`Stroke`]. + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + pub fn with_line_cap(self, line_cap: LineCap) -> Self { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + pub fn with_line_join(self, line_join: LineJoin) -> Self { + Stroke { line_join, ..self } + } +} + +impl<'a> Default for Stroke<'a> { + fn default() -> Self { + Stroke { + style: Style::Solid(Color::BLACK), + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + line_dash: LineDash::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +/// The dash pattern used when stroking the line. +#[derive(Debug, Clone, Copy, Default)] +pub struct LineDash<'a> { + /// The alternating lengths of lines and gaps which describe the pattern. + pub segments: &'a [f32], + + /// The offset of [`LineDash::segments`] to start the pattern. + pub offset: usize, +} diff --git a/native/src/widget/canvas/style.rs b/native/src/widget/canvas/style.rs new file mode 100644 index 00000000..2642fdb8 --- /dev/null +++ b/native/src/widget/canvas/style.rs @@ -0,0 +1,24 @@ +use crate::widget::canvas::Gradient; +use crate::Color; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/native/src/widget/canvas/text.rs b/native/src/widget/canvas/text.rs new file mode 100644 index 00000000..8c0b2dfb --- /dev/null +++ b/native/src/widget/canvas/text.rs @@ -0,0 +1,57 @@ +use crate::alignment; +use crate::{Color, Font, Point}; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position of the text relative to the alignment properties. + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. + pub position: Point, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: f32, + /// 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, +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + color: Color::BLACK, + size: 16.0, + font: Font::SansSerif, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 1f21f06b..429b55c2 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -6,12 +6,13 @@ edition = "2021" [features] image = ["iced_wgpu/image", "iced_tiny_skia/image"] svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] -canvas = ["iced_graphics/canvas"] -qr_code = ["iced_graphics/qr_code"] +canvas = ["iced_wgpu/canvas", "iced_tiny_skia/canvas"] +qr_code = ["canvas", "qrcode"] tracing = ["iced_wgpu/tracing"] [dependencies] raw-window-handle = "0.5" +thiserror = "1" [dependencies.iced_native] version = "0.9" @@ -30,3 +31,8 @@ iced_wgpu = { version = "0.9", path = "../wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] } + +[dependencies.qrcode] +version = "0.12" +optional = true +default-features = false diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index b0a409dc..6c0b4e5c 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -1,4 +1,4 @@ -use crate::{Font, Point, Size}; +use crate::{Font, Geometry, Point, Size}; use iced_graphics::backend; use iced_graphics::text; @@ -12,6 +12,8 @@ pub enum Backend { } impl iced_graphics::Backend for Backend { + type Geometry = Geometry; + fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index f9bfc373..d9c85e82 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -4,11 +4,14 @@ pub mod window; mod backend; mod settings; +pub use iced_graphics::primitive; + pub use backend::Backend; +pub use primitive::Primitive; pub use settings::Settings; pub use iced_graphics::{ - Antialiasing, Color, Error, Font, Point, Size, Viewport, + Antialiasing, Color, Error, Font, Point, Rectangle, Size, Vector, Viewport, }; /// The default graphics renderer for [`iced`]. @@ -16,3 +19,12 @@ pub use iced_graphics::{ /// [`iced`]: https://github.com/iced-rs/iced pub type Renderer = iced_graphics::Renderer; + +#[derive(Debug, Clone)] +pub struct Geometry(pub(crate) Primitive); + +impl From for Primitive { + fn from(geometry: Geometry) -> Self { + geometry.0 + } +} diff --git a/renderer/src/widget.rs b/renderer/src/widget.rs index 417cc06f..6c0c2a83 100644 --- a/renderer/src/widget.rs +++ b/renderer/src/widget.rs @@ -1,5 +1,11 @@ -#[cfg(feature = "qr_code")] -pub use iced_graphics::widget::qr_code; +#[cfg(feature = "canvas")] +pub mod canvas; #[cfg(feature = "canvas")] -pub use iced_graphics::widget::canvas; +pub use canvas::Canvas; + +#[cfg(feature = "qr_code")] +pub mod qr_code; + +#[cfg(feature = "qr_code")] +pub use qr_code::QRCode; diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs new file mode 100644 index 00000000..f40a1097 --- /dev/null +++ b/renderer/src/widget/canvas.rs @@ -0,0 +1,177 @@ +mod cache; + +pub use cache::Cache; + +pub use iced_native::widget::canvas::event::{self, Event}; +pub use iced_native::widget::canvas::fill::{self, Fill}; +pub use iced_native::widget::canvas::gradient::{self, Gradient}; +pub use iced_native::widget::canvas::path::{self, Path}; +pub use iced_native::widget::canvas::stroke::{self, Stroke}; +pub use iced_native::widget::canvas::{ + Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, +}; + +use crate::{Backend, Point, Rectangle, Size, Vector}; + +pub use crate::Geometry; + +pub enum Frame { + Wgpu(iced_wgpu::widget::canvas::Frame), + TinySkia(iced_tiny_skia::canvas::Frame), +} + +macro_rules! delegate { + ($frame:expr, $name:ident => $body:expr) => { + match $frame { + Self::Wgpu($name) => $body, + Self::TinySkia($name) => $body, + } + }; +} + +impl Frame { + pub fn new(renderer: &crate::Renderer, size: Size) -> Self { + match renderer.backend() { + Backend::Wgpu(_) => { + Frame::Wgpu(iced_wgpu::widget::canvas::Frame::new(size)) + } + Backend::TinySkia(_) => { + Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) + } + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + delegate!(self, frame => frame.width()) + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + delegate!(self, frame => frame.height()) + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + delegate!(self, frame => frame.size()) + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + delegate!(self, frame => frame.center()) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + delegate!(self, frame => frame.fill(path, fill)); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + delegate!(self, frame => frame.stroke(path, stroke)); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + delegate!(self, frame => frame.fill_text(text)); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + delegate!(self, frame => frame.push_transform()); + + f(self); + + delegate!(self, frame => frame.pop_transform()); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = match self { + Self::Wgpu(_) => { + Self::Wgpu(iced_wgpu::widget::canvas::Frame::new(region.size())) + } + Self::TinySkia(_) => Self::TinySkia( + iced_tiny_skia::canvas::Frame::new(region.size()), + ), + }; + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + match (self, frame) { + (Self::Wgpu(target), Self::Wgpu(frame)) => { + target.clip(frame, translation); + } + (Self::TinySkia(target), Self::TinySkia(frame)) => { + target.clip(frame, translation); + } + _ => unreachable!(), + }; + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + delegate!(self, frame => frame.translate(translation)); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + delegate!(self, frame => frame.rotate(angle)); + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + delegate!(self, frame => frame.scale(scale)); + } + + pub fn into_geometry(self) -> Geometry { + Geometry(delegate!(self, frame => frame.into_primitive())) + } +} diff --git a/renderer/src/widget/canvas/cache.rs b/renderer/src/widget/canvas/cache.rs new file mode 100644 index 00000000..7d6b4811 --- /dev/null +++ b/renderer/src/widget/canvas/cache.rs @@ -0,0 +1,85 @@ +use crate::widget::canvas::{Frame, Geometry}; +use crate::{Primitive, Renderer, Size}; + +use std::cell::RefCell; +use std::sync::Arc; + +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +#[derive(Debug, Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.0) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry(Primitive::Cache { content: primitive }) + } +} diff --git a/renderer/src/widget/qr_code.rs b/renderer/src/widget/qr_code.rs new file mode 100644 index 00000000..aae4ec88 --- /dev/null +++ b/renderer/src/widget/qr_code.rs @@ -0,0 +1,301 @@ +//! Encode and display information in a QR code. +use crate::widget::canvas; +use crate::Renderer; + +use iced_graphics::renderer; + +use iced_native::layout; +use iced_native::widget::Tree; +use iced_native::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, +}; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, Theme> Widget> for QRCode<'a> { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new(side_length, side_length)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + use iced_native::Renderer as _; + + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = + self.state.cache.draw(renderer, bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new( + QUIET_ZONE as f32, + QUIET_ZONE as f32, + )); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + let translation = Vector::new(bounds.x, bounds.y); + + renderer.with_translation(translation, |renderer| { + renderer.draw_primitive(geometry.0); + }); + } +} + +impl<'a, Message, Theme> From> + for Element<'a, Message, Renderer> +{ + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} diff --git a/src/widget.rs b/src/widget.rs index 04cb0e50..f3a66101 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -170,9 +170,10 @@ pub use iced_renderer::widget::canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] /// Creates a new [`Canvas`]. -pub fn canvas(program: P) -> Canvas +pub fn canvas(program: P) -> Canvas where - P: canvas::Program, + Renderer: canvas::Renderer, + P: canvas::Program, { Canvas::new(program) } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 781e7d34..5f39fce2 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] image = [] svg = [] +canvas = ["iced_native/canvas"] [dependencies] raw-window-handle = "0.5" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 66d83221..e08cede7 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -1,9 +1,9 @@ -use crate::{Color, Font, Settings, Size, Viewport}; +use crate::{Color, Font, Primitive, Settings, Size, Viewport}; use iced_graphics::alignment; use iced_graphics::backend; use iced_graphics::text; -use iced_graphics::{Background, Primitive, Rectangle, Vector}; +use iced_graphics::{Background, Rectangle, Vector}; use std::borrow::Cow; @@ -81,7 +81,6 @@ impl Backend { translation: Vector, ) { match primitive { - Primitive::None => {} Primitive::Quad { bounds, background, @@ -161,6 +160,38 @@ impl Backend { Primitive::Svg { .. } => { // TODO } + Primitive::Fill { + path, + paint, + rule, + transform, + } => { + pixels.fill_path( + path, + paint, + *rule, + transform + .post_translate(translation.x, translation.y) + .post_scale(scale_factor, scale_factor), + clip_mask, + ); + } + Primitive::Stroke { + path, + paint, + stroke, + transform, + } => { + pixels.stroke_path( + path, + paint, + stroke, + transform + .post_translate(translation.x, translation.y) + .post_scale(scale_factor, scale_factor), + clip_mask, + ); + } Primitive::Group { primitives } => { for primitive in primitives { self.draw_primitive( @@ -196,16 +227,19 @@ impl Backend { translation, ); } - Primitive::Cached { cache } => { + Primitive::Cache { content } => { self.draw_primitive( - cache, + content, pixels, clip_mask, scale_factor, translation, ); } - Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => {} + Primitive::SolidMesh { .. } | Primitive::GradientMesh { .. } => { + // Not supported! + // TODO: Draw a placeholder (?) / Log it (?) + } } } } @@ -386,6 +420,8 @@ fn rectangular_clip_mask( } impl iced_graphics::Backend for Backend { + type Geometry = (); + fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache(); } diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs new file mode 100644 index 00000000..c3b8b316 --- /dev/null +++ b/tiny_skia/src/canvas.rs @@ -0,0 +1,276 @@ +use crate::{Point, Primitive, Rectangle, Size, Vector}; + +use iced_native::widget::canvas::fill::{self, Fill}; +use iced_native::widget::canvas::stroke::{self, Stroke}; +use iced_native::widget::canvas::{Path, Style, Text}; +use iced_native::Gradient; + +pub struct Frame { + size: Size, + transform: tiny_skia::Transform, + stack: Vec, + primitives: Vec, +} + +impl Frame { + pub fn new(size: Size) -> Self { + Self { + size, + transform: tiny_skia::Transform::identity(), + stack: Vec::new(), + primitives: Vec::new(), + } + } + + pub fn width(&self) -> f32 { + self.size.width + } + + pub fn height(&self) -> f32 { + self.size.height + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let path = convert_path(path); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: into_paint(fill.style), + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); + } + + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + self.fill(&Path::rectangle(top_left, size), fill); + } + + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let path = convert_path(path); + let stroke = stroke.into(); + let skia_stroke = into_stroke(&stroke); + + self.primitives.push(Primitive::Stroke { + path, + paint: into_paint(stroke.style), + stroke: skia_stroke, + transform: self.transform, + }); + } + + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transform.is_identity() { + text.position + } else { + let mut transformed = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut transformed); + + Point::new(transformed[0].x, transformed[0].y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + pub fn push_transform(&mut self) { + self.stack.push(self.transform); + } + + pub fn pop_transform(&mut self) { + self.transform = self.stack.pop().expect("Pop transform"); + } + + pub fn clip(&mut self, _frame: Self, _translation: Vector) {} + + pub fn translate(&mut self, translation: Vector) { + self.transform = + self.transform.pre_translate(translation.x, translation.y); + } + + pub fn rotate(&mut self, angle: f32) { + self.transform = self + .transform + .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); + } + + pub fn scale(&mut self, scale: f32) { + self.transform = self.transform.pre_scale(scale, scale); + } + + pub fn into_primitive(self) -> Primitive { + Primitive::Clip { + bounds: Rectangle::new(Point::ORIGIN, self.size), + content: Box::new(Primitive::Group { + primitives: self.primitives, + }), + } + } +} + +fn convert_path(path: &Path) -> tiny_skia::Path { + use iced_native::widget::canvas::path::lyon_path; + + let mut builder = tiny_skia::PathBuilder::new(); + let mut last_point = Default::default(); + + for event in path.raw().iter() { + match event { + lyon_path::Event::Begin { at } => { + builder.move_to(at.x, at.y); + + last_point = at; + } + lyon_path::Event::Line { from, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.line_to(to.x, to.y); + + last_point = to; + } + lyon_path::Event::Quadratic { from, ctrl, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::Cubic { + from, + ctrl1, + ctrl2, + to, + } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder + .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::End { close, .. } => { + if close { + builder.close(); + } + } + } + } + + builder + .finish() + .expect("Convert lyon path to tiny_skia path") +} + +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { + tiny_skia::Paint { + shader: match style { + Style::Solid(color) => tiny_skia::Shader::SolidColor( + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Create color"), + ), + Style::Gradient(gradient) => match gradient { + Gradient::Linear(linear) => tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: linear.start.x, + y: linear.start.y, + }, + tiny_skia::Point { + x: linear.end.x, + y: linear.end.y, + }, + linear + .color_stops + .into_iter() + .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::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient"), + }, + }, + anti_alias: true, + ..Default::default() + } +} + +pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { + match rule { + fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, + fill::Rule::NonZero => tiny_skia::FillRule::Winding, + } +} + +pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke { + tiny_skia::Stroke { + width: stroke.width, + line_cap: match stroke.line_cap { + stroke::LineCap::Butt => tiny_skia::LineCap::Butt, + stroke::LineCap::Square => tiny_skia::LineCap::Square, + stroke::LineCap::Round => tiny_skia::LineCap::Round, + }, + line_join: match stroke.line_join { + stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, + stroke::LineJoin::Round => tiny_skia::LineJoin::Round, + stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, + }, + dash: if stroke.line_dash.segments.is_empty() { + None + } else { + tiny_skia::StrokeDash::new( + stroke.line_dash.segments.into(), + stroke.line_dash.offset as f32, + ) + }, + ..Default::default() + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 420a1ffb..e66e6412 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -4,10 +4,18 @@ mod backend; mod settings; mod text; +#[cfg(feature = "canvas")] +pub mod canvas; + +pub use iced_graphics::primitive; + pub use backend::Backend; +pub use primitive::Primitive; pub use settings::Settings; -pub use iced_graphics::{Color, Error, Font, Point, Size, Vector, Viewport}; +pub use iced_graphics::{ + Color, Error, Font, Point, Rectangle, Size, Vector, Viewport, +}; /// A [`tiny-skia`] graphics renderer for [`iced`]. /// diff --git a/tiny_skia/src/primitive.rs b/tiny_skia/src/primitive.rs new file mode 100644 index 00000000..22daaedc --- /dev/null +++ b/tiny_skia/src/primitive.rs @@ -0,0 +1,82 @@ +use crate::{Rectangle, Vector}; + +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub enum Primitive { + /// A group of primitives + Group { + /// The primitives of the group + primitives: Vec, + }, + /// A clip primitive + Clip { + /// The bounds of the clip + bounds: Rectangle, + /// The content of the clip + content: Box, + }, + /// A primitive that applies a translation + Translate { + /// The translation vector + translation: Vector, + + /// The primitive to translate + content: Box, + }, + /// A cached primitive. + /// + /// This can be useful if you are implementing a widget where primitive + /// generation is expensive. + Cached { + /// The cached primitive + cache: Arc, + }, + /// A basic primitive. + Basic(iced_graphics::Primitive), +} + +impl iced_graphics::backend::Primitive for Primitive { + fn translate(self, translation: Vector) -> Self { + Self::Translate { + translation, + content: Box::new(self), + } + } + + fn clip(self, bounds: Rectangle) -> Self { + Self::Clip { + bounds, + content: Box::new(self), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct Recording(pub(crate) Vec); + +impl iced_graphics::backend::Recording for Recording { + type Primitive = Primitive; + + fn push(&mut self, primitive: Primitive) { + self.0.push(primitive); + } + + fn push_basic(&mut self, basic: iced_graphics::Primitive) { + self.0.push(Primitive::Basic(basic)); + } + + fn group(self) -> Self::Primitive { + Primitive::Group { primitives: self.0 } + } + + fn clear(&mut self) { + self.0.clear(); + } +} + +impl Recording { + pub fn primitives(&self) -> &[Primitive] { + &self.0 + } +} diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 2bd5831e..08159cd8 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -1,7 +1,6 @@ -use crate::{Backend, Color, Error, Renderer, Settings, Viewport}; +use crate::{Backend, Color, Error, Primitive, Renderer, Settings, Viewport}; use iced_graphics::window::compositor::{self, Information, SurfaceError}; -use iced_graphics::Primitive; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 632873a3..0bcef71c 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -21,8 +21,7 @@ bmp = ["iced_graphics/bmp"] hdr = ["iced_graphics/hdr"] dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas"] -qr_code = ["iced_graphics/qr_code"] +canvas = ["iced_graphics/canvas", "lyon"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] @@ -62,10 +61,6 @@ version = "0.2" git = "https://github.com/hecrj/glyphon.git" rev = "65b481d758f50fd13fc21af2cc5ef62ddee64955" -[dependencies.tracing] -version = "0.1.6" -optional = true - [dependencies.encase] version = "0.3.0" features = ["glam"] @@ -73,6 +68,14 @@ features = ["glam"] [dependencies.glam] version = "0.21.3" +[dependencies.lyon] +version = "1.0" +optional = true + +[dependencies.tracing] +version = "0.1.6" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index e650d9a5..10dc5b4f 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -1,11 +1,10 @@ use crate::quad; use crate::text; use crate::triangle; -use crate::{Settings, Transformation}; +use crate::{Layer, Primitive, Settings, Transformation}; use iced_graphics::backend; -use iced_graphics::layer::Layer; -use iced_graphics::{Color, Font, Primitive, Size, Viewport}; +use iced_graphics::{Color, Font, Size, Viewport}; #[cfg(feature = "tracing")] use tracing::info_span; @@ -330,6 +329,8 @@ impl Backend { } impl iced_graphics::Backend for Backend { + type Geometry = (); + fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache() } diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs index db05d2ff..2159a3ec 100644 --- a/wgpu/src/image.rs +++ b/wgpu/src/image.rs @@ -6,10 +6,10 @@ use iced_graphics::image::raster; #[cfg(feature = "svg")] use iced_graphics::image::vector; +use crate::layer; use crate::{Buffer, Transformation}; use atlas::Atlas; -use iced_graphics::layer; use iced_native::{Rectangle, Size}; use std::cell::RefCell; diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs new file mode 100644 index 00000000..0840555a --- /dev/null +++ b/wgpu/src/layer.rs @@ -0,0 +1,274 @@ +//! Organize rendering primitives into a flattened list of layers. +mod image; +mod quad; +mod text; + +pub mod mesh; + +pub use image::Image; +pub use mesh::Mesh; +pub use quad::Quad; +pub use text::Text; + +use crate::Primitive; + +use iced_graphics::alignment; +use iced_graphics::{ + Background, Color, Font, Point, Rectangle, Size, Vector, Viewport, +}; + +/// A group of primitives that should be clipped together. +#[derive(Debug)] +pub struct Layer<'a> { + /// The clipping bounds of the [`Layer`]. + pub bounds: Rectangle, + + /// The quads of the [`Layer`]. + pub quads: Vec, + + /// The triangle meshes of the [`Layer`]. + pub meshes: Vec>, + + /// The text of the [`Layer`]. + pub text: Vec>, + + /// The images of the [`Layer`]. + pub images: Vec, +} + +impl<'a> Layer<'a> { + /// Creates a new [`Layer`] with the given clipping bounds. + pub fn new(bounds: Rectangle) -> Self { + Self { + bounds, + quads: Vec::new(), + meshes: Vec::new(), + text: Vec::new(), + images: Vec::new(), + } + } + + /// Creates a new [`Layer`] for the provided overlay text. + /// + /// This can be useful for displaying debug information. + pub fn overlay(lines: &'a [impl AsRef], viewport: &Viewport) -> Self { + let mut overlay = + Layer::new(Rectangle::with_size(viewport.logical_size())); + + for (i, line) in lines.iter().enumerate() { + let text = Text { + 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: 20.0, + font: Font::Monospace, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + }; + + overlay.text.push(text); + + overlay.text.push(Text { + bounds: text.bounds + Vector::new(-1.0, -1.0), + color: Color::BLACK, + ..text + }); + } + + overlay + } + + /// Distributes the given [`Primitive`] and generates a list of layers based + /// on its contents. + pub fn generate( + primitives: &'a [Primitive], + viewport: &Viewport, + ) -> Vec { + 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, + Vector::new(0.0, 0.0), + primitive, + 0, + ); + } + + layers + } + + fn process_primitive( + layers: &mut Vec, + translation: Vector, + primitive: &'a Primitive, + current_layer: usize, + ) { + match primitive { + Primitive::Text { + content, + bounds, + size, + color, + font, + horizontal_alignment, + vertical_alignment, + } => { + let layer = &mut layers[current_layer]; + + layer.text.push(Text { + content, + bounds: *bounds + translation, + size: *size, + color: *color, + font: *font, + horizontal_alignment: *horizontal_alignment, + vertical_alignment: *vertical_alignment, + }); + } + Primitive::Quad { + bounds, + background, + border_radius, + border_width, + border_color, + } => { + let layer = &mut layers[current_layer]; + + // TODO: Move some of these computations to the GPU (?) + layer.quads.push(Quad { + position: [ + bounds.x + translation.x, + bounds.y + translation.y, + ], + size: [bounds.width, bounds.height], + color: match background { + Background::Color(color) => color.into_linear(), + }, + border_radius: *border_radius, + border_width: *border_width, + border_color: border_color.into_linear(), + }); + } + Primitive::Image { handle, bounds } => { + let layer = &mut layers[current_layer]; + + layer.images.push(Image::Raster { + handle: handle.clone(), + bounds: *bounds + translation, + }); + } + Primitive::Svg { + handle, + color, + bounds, + } => { + let layer = &mut layers[current_layer]; + + layer.images.push(Image::Vector { + handle: handle.clone(), + color: *color, + bounds: *bounds + translation, + }); + } + Primitive::SolidMesh { buffers, size } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh::Solid { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + }); + } + } + Primitive::GradientMesh { + buffers, + size, + gradient, + } => { + let layer = &mut layers[current_layer]; + + let bounds = Rectangle::new( + Point::new(translation.x, translation.y), + *size, + ); + + // Only draw visible content + if let Some(clip_bounds) = layer.bounds.intersection(&bounds) { + layer.meshes.push(Mesh::Gradient { + origin: Point::new(translation.x, translation.y), + buffers, + clip_bounds, + gradient, + }); + } + } + Primitive::Group { primitives } => { + // TODO: Inspect a bit and regroup (?) + for primitive in primitives { + Self::process_primitive( + layers, + translation, + primitive, + current_layer, + ) + } + } + Primitive::Clip { bounds, content } => { + let layer = &mut layers[current_layer]; + let translated_bounds = *bounds + translation; + + // 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, + translation, + content, + layers.len() - 1, + ); + } + } + Primitive::Translate { + translation: new_translation, + content, + } => { + Self::process_primitive( + layers, + translation + *new_translation, + content, + current_layer, + ); + } + Primitive::Cache { content } => { + Self::process_primitive( + layers, + translation, + content, + current_layer, + ); + } + Primitive::Fill { .. } | Primitive::Stroke { .. } => { + // Unsupported! + // TODO: Draw a placeholder (?) + } + } + } +} diff --git a/wgpu/src/layer/image.rs b/wgpu/src/layer/image.rs new file mode 100644 index 00000000..3eff2397 --- /dev/null +++ b/wgpu/src/layer/image.rs @@ -0,0 +1,27 @@ +use crate::{Color, Rectangle}; + +use iced_native::{image, svg}; + +/// 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 bounds of the image. + bounds: Rectangle, + }, + /// A vector image. + Vector { + /// The handle of a vector image. + handle: svg::Handle, + + /// The [`Color`] filter + color: Option, + + /// The bounds of the image. + bounds: Rectangle, + }, +} diff --git a/wgpu/src/layer/mesh.rs b/wgpu/src/layer/mesh.rs new file mode 100644 index 00000000..5c1e41ad --- /dev/null +++ b/wgpu/src/layer/mesh.rs @@ -0,0 +1,93 @@ +//! A collection of triangle primitives. +use crate::primitive; +use crate::{Gradient, Point, Rectangle}; + +/// A mesh of triangles. +#[derive(Debug, Clone, Copy)] +pub enum Mesh<'a> { + /// A mesh of triangles with a solid color. + Solid { + /// The origin of the vertices of the [`Mesh`]. + origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + buffers: &'a primitive::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + }, + /// A mesh of triangles with a gradient color. + Gradient { + /// The origin of the vertices of the [`Mesh`]. + origin: Point, + + /// The vertex and index buffers of the [`Mesh`]. + buffers: &'a primitive::Mesh2D, + + /// The clipping bounds of the [`Mesh`]. + clip_bounds: Rectangle, + + /// The gradient to apply to the [`Mesh`]. + gradient: &'a Gradient, + }, +} + +impl Mesh<'_> { + /// Returns the origin of the [`Mesh`]. + pub fn origin(&self) -> Point { + match self { + Self::Solid { origin, .. } | Self::Gradient { origin, .. } => { + *origin + } + } + } + + /// 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 { + 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 gradient vertices. + pub gradient_vertices: 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.solid_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + Mesh::Gradient { buffers, .. } => { + count.gradient_vertices += buffers.vertices.len(); + count.indices += buffers.indices.len(); + } + } + + count + }) +} diff --git a/wgpu/src/layer/quad.rs b/wgpu/src/layer/quad.rs new file mode 100644 index 00000000..0d8bde9d --- /dev/null +++ b/wgpu/src/layer/quad.rs @@ -0,0 +1,30 @@ +/// A colored rectangle with a border. +/// +/// This type can be directly uploaded to GPU memory. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Quad { + /// The position of the [`Quad`]. + pub position: [f32; 2], + + /// The size of the [`Quad`]. + pub size: [f32; 2], + + /// The color of the [`Quad`], in __linear RGB__. + pub color: [f32; 4], + + /// The border color of the [`Quad`], in __linear RGB__. + pub border_color: [f32; 4], + + /// The border radius of the [`Quad`]. + pub border_radius: [f32; 4], + + /// The border width of the [`Quad`]. + pub border_width: f32, +} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Zeroable for Quad {} + +#[allow(unsafe_code)] +unsafe impl bytemuck::Pod for Quad {} diff --git a/wgpu/src/layer/text.rs b/wgpu/src/layer/text.rs new file mode 100644 index 00000000..38d62616 --- /dev/null +++ b/wgpu/src/layer/text.rs @@ -0,0 +1,26 @@ +use crate::{alignment, Color, Font, Rectangle}; + +/// A paragraph of text. +#[derive(Debug, Clone, Copy)] +pub struct Text<'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`]. + pub size: f32, + + /// 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, +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9da40572..8be12602 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -25,7 +25,7 @@ )] #![deny( missing_debug_implementations, - missing_docs, + //missing_docs, unsafe_code, unused_results, clippy::extra_unused_lifetimes, @@ -38,7 +38,9 @@ #![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] +pub mod layer; pub mod settings; +pub mod widget; pub mod window; mod backend; @@ -47,16 +49,23 @@ mod quad; mod text; mod triangle; +pub use iced_graphics::primitive; pub use iced_graphics::{ - Antialiasing, Color, Error, Font, Primitive, Viewport, + Antialiasing, Color, Error, Font, Gradient, Point, Rectangle, Size, Vector, + Viewport, }; +pub use iced_native::alignment; + pub use iced_native::Theme; pub use wgpu; pub use backend::Backend; +pub use layer::Layer; +pub use primitive::Primitive; pub use settings::Settings; -use crate::buffer::Buffer; +use buffer::Buffer; + use iced_graphics::Transformation; #[cfg(any(feature = "image", feature = "svg"))] diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs index 246cc5e1..8a568968 100644 --- a/wgpu/src/quad.rs +++ b/wgpu/src/quad.rs @@ -1,5 +1,6 @@ +use crate::layer; use crate::{Buffer, Transformation}; -use iced_graphics::layer; + use iced_native::Rectangle; use bytemuck::{Pod, Zeroable}; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index dea6ab18..0dc8a64c 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -1,6 +1,7 @@ +use crate::layer::Text; + pub use iced_native::text::Hit; -use iced_graphics::layer::Text; use iced_native::alignment; use iced_native::{Color, Font, Rectangle, Size}; diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index 4b4fa16d..706e4282 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -2,12 +2,12 @@ mod msaa; use crate::buffer::r#static::Buffer; +use crate::layer::mesh::{self, Mesh}; use crate::settings; use crate::Transformation; -use iced_graphics::layer::mesh::{self, Mesh}; -use iced_graphics::triangle::ColoredVertex2D; use iced_graphics::Size; + #[cfg(feature = "tracing")] use tracing::info_span; @@ -468,6 +468,7 @@ mod solid { use crate::settings; use crate::triangle; use encase::ShaderType; + use iced_graphics::primitive; use iced_graphics::Transformation; #[derive(Debug)] @@ -478,7 +479,7 @@ mod solid { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer, + pub vertices: Buffer, pub uniforms: dynamic::Buffer, pub constants: wgpu::BindGroup, } @@ -596,7 +597,7 @@ mod solid { entry_point: "vs_main", buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< - triangle::ColoredVertex2D, + primitive::ColoredVertex2D, >() as u64, step_mode: wgpu::VertexStepMode::Vertex, @@ -637,7 +638,7 @@ mod gradient { use encase::ShaderType; use glam::{IVec4, Vec4}; - use iced_graphics::triangle::Vertex2D; + use iced_graphics::primitive; #[derive(Debug)] pub struct Pipeline { @@ -647,7 +648,7 @@ mod gradient { #[derive(Debug)] pub struct Layer { - pub vertices: Buffer, + pub vertices: Buffer, pub uniforms: dynamic::Buffer, pub storage: dynamic::Buffer, pub constants: wgpu::BindGroup, @@ -810,34 +811,38 @@ mod gradient { ), }); - let pipeline = device.create_render_pipeline( - &wgpu::RenderPipelineDescriptor { - label: Some("iced_wgpu::triangle::gradient pipeline"), - layout: Some(&layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: "vs_main", - buffers: &[wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() - as u64, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &wgpu::vertex_attr_array!( - // Position - 0 => Float32x2, - ), - }], + let pipeline = + device.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: Some("iced_wgpu::triangle::gradient pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::< + primitive::Vertex2D, + >( + ) + as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array!( + // Position + 0 => Float32x2, + ), + }], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[triangle::fragment_target(format)], + }), + primitive: triangle::primitive_state(), + depth_stencil: None, + multisample: triangle::multisample_state(antialiasing), + multiview: None, }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: "fs_main", - targets: &[triangle::fragment_target(format)], - }), - primitive: triangle::primitive_state(), - depth_stencil: None, - multisample: triangle::multisample_state(antialiasing), - multiview: None, - }, - ); + ); Self { pipeline, diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs new file mode 100644 index 00000000..8d05041e --- /dev/null +++ b/wgpu/src/widget.rs @@ -0,0 +1,9 @@ +//! Use the graphical widgets supported out-of-the-box. + +#[cfg(feature = "canvas")] +#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] +pub mod canvas; + +#[cfg(feature = "canvas")] +#[doc(no_inline)] +pub use canvas::Canvas; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs new file mode 100644 index 00000000..41444fcf --- /dev/null +++ b/wgpu/src/widget/canvas.rs @@ -0,0 +1,16 @@ +mod cache; +mod frame; +mod geometry; + +pub use cache::Cache; +pub use frame::Frame; +pub use geometry::Geometry; + +pub use iced_native::widget::canvas::event::{self, Event}; +pub use iced_native::widget::canvas::fill::{self, Fill}; +pub use iced_native::widget::canvas::gradient::{self, Gradient}; +pub use iced_native::widget::canvas::path::{self, Path}; +pub use iced_native::widget::canvas::stroke::{self, Stroke}; +pub use iced_native::widget::canvas::{ + Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, +}; diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs new file mode 100644 index 00000000..09b26b90 --- /dev/null +++ b/wgpu/src/widget/canvas/cache.rs @@ -0,0 +1,93 @@ +use crate::widget::canvas::{Frame, Geometry}; +use crate::Primitive; + +use iced_native::Size; +use std::{cell::RefCell, sync::Arc}; + +#[derive(Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + pub fn draw( + &self, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry::from_primitive(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(bounds); + draw_fn(&mut frame); + + let primitive = Arc::new(frame.into_primitive()); + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry::from_primitive(Primitive::Cache { content: primitive }) + } +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::Empty => write!(f, "Empty"), + State::Filled { primitive, bounds } => f + .debug_struct("Filled") + .field("primitive", primitive) + .field("bounds", bounds) + .finish(), + } + } +} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs new file mode 100644 index 00000000..987570ec --- /dev/null +++ b/wgpu/src/widget/canvas/frame.rs @@ -0,0 +1,609 @@ +use crate::primitive::{self, Primitive}; +use crate::widget::canvas::fill::{self, Fill}; +use crate::widget::canvas::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; + +use iced_native::{Gradient, Point, Rectangle, Size, Vector}; + +use lyon::geom::euclid; +use lyon::tessellation; +use std::borrow::Cow; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: crate::widget::Canvas +#[allow(missing_debug_implementations)] +pub struct Frame { + size: Size, + buffers: BufferStack, + primitives: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + +impl Frame { + /// 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. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.push_transform(); + + f(self); + + self.pop_transform(); + } + + pub fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + pub fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + self.clip(frame, translation); + } + + pub fn clip(&mut self, frame: Frame, translation: Vector) { + let size = frame.size(); + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(size), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. + pub fn into_primitive(self) -> Primitive { + Primitive::Group { + primitives: self.into_primitives(), + } + } + + fn into_primitives(mut self) -> Vec { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } + } + + self.primitives + } +} + +struct Vertex2DBuilder; + +impl tessellation::FillVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + walk_along_path, RepeatedPattern, WalkerEvent, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened(0.01), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs new file mode 100644 index 00000000..e8ac621d --- /dev/null +++ b/wgpu/src/widget/canvas/geometry.rs @@ -0,0 +1,24 @@ +use crate::Primitive; + +/// A bunch of shapes that can be drawn. +/// +/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a +/// [`Cache`]. +/// +/// [`Frame`]: crate::widget::canvas::Frame +/// [`Cache`]: crate::widget::canvas::Cache +#[derive(Debug, Clone)] +pub struct Geometry(Primitive); + +impl Geometry { + pub(crate) fn from_primitive(primitive: Primitive) -> Self { + Self(primitive) + } + + /// Turns the [`Geometry`] into a [`Primitive`]. + /// + /// This can be useful if you are building a custom widget. + pub fn into_primitive(self) -> Primitive { + self.0 + } +} -- cgit From 5c0427edbb4358896412c736af2f441c12601d1b Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:41:32 +0100 Subject: Fix `Clip` primitive translation in `iced_tiny_skia` --- examples/game_of_life/src/main.rs | 2 +- tiny_skia/src/backend.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 494f71a6..cdb33aca 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -145,7 +145,7 @@ impl Application for GameOfLife { self.grid .view() .map(move |message| Message::Grid(message, version)), - controls + controls, ]; container(content) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index e08cede7..838426f5 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -221,7 +221,7 @@ impl Backend { pixels, Some(&rectangular_clip_mask( pixels, - *bounds * scale_factor, + (*bounds + translation) * scale_factor, )), scale_factor, translation, -- cgit From 838fd96212b14f20fe2224c4844904a8995f2db7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:47:15 +0100 Subject: Disable `anti_alias` for `Frame::fill_rectangle` in `iced_tiny_skia` --- tiny_skia/src/canvas.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs index c3b8b316..59765f59 100644 --- a/tiny_skia/src/canvas.rs +++ b/tiny_skia/src/canvas.rs @@ -44,7 +44,7 @@ impl Frame { self.primitives.push(Primitive::Fill { path, - paint: into_paint(fill.style), + paint: into_paint(fill.style, true), rule: into_fill_rule(fill.rule), transform: self.transform, }); @@ -56,7 +56,15 @@ impl Frame { size: Size, fill: impl Into, ) { - self.fill(&Path::rectangle(top_left, size), fill); + let path = convert_path(&Path::rectangle(top_left, size)); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: into_paint(fill.style, false), + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); } pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { @@ -66,7 +74,7 @@ impl Frame { self.primitives.push(Primitive::Stroke { path, - paint: into_paint(stroke.style), + paint: into_paint(stroke.style, true), stroke: skia_stroke, transform: self.transform, }); @@ -199,7 +207,7 @@ fn convert_path(path: &Path) -> tiny_skia::Path { .expect("Convert lyon path to tiny_skia path") } -pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { +pub fn into_paint(style: Style, anti_alias: bool) -> tiny_skia::Paint<'static> { tiny_skia::Paint { shader: match style { Style::Solid(color) => tiny_skia::Shader::SolidColor( @@ -238,7 +246,7 @@ pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { .expect("Create linear gradient"), }, }, - anti_alias: true, + anti_alias, ..Default::default() } } -- cgit From 119cf2ecd10f70471199439acb1c4f9d96a57ced Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:48:27 +0100 Subject: Remove magic boolean in `into_paint` --- tiny_skia/src/canvas.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs index 59765f59..958063d2 100644 --- a/tiny_skia/src/canvas.rs +++ b/tiny_skia/src/canvas.rs @@ -44,7 +44,7 @@ impl Frame { self.primitives.push(Primitive::Fill { path, - paint: into_paint(fill.style, true), + paint: into_paint(fill.style), rule: into_fill_rule(fill.rule), transform: self.transform, }); @@ -61,7 +61,10 @@ impl Frame { self.primitives.push(Primitive::Fill { path, - paint: into_paint(fill.style, false), + paint: tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }, rule: into_fill_rule(fill.rule), transform: self.transform, }); @@ -74,7 +77,7 @@ impl Frame { self.primitives.push(Primitive::Stroke { path, - paint: into_paint(stroke.style, true), + paint: into_paint(stroke.style), stroke: skia_stroke, transform: self.transform, }); @@ -207,7 +210,7 @@ fn convert_path(path: &Path) -> tiny_skia::Path { .expect("Convert lyon path to tiny_skia path") } -pub fn into_paint(style: Style, anti_alias: bool) -> tiny_skia::Paint<'static> { +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { tiny_skia::Paint { shader: match style { Style::Solid(color) => tiny_skia::Shader::SolidColor( @@ -246,7 +249,7 @@ pub fn into_paint(style: Style, anti_alias: bool) -> tiny_skia::Paint<'static> { .expect("Create linear gradient"), }, }, - anti_alias, + anti_alias: true, ..Default::default() } } -- cgit From 350427e82c3a49367da65086c20a307e9b864a23 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Wed, 1 Mar 2023 21:52:12 +0100 Subject: Fix missing `qr_code` module in `iced_native` --- native/src/widget.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/native/src/widget.rs b/native/src/widget.rs index 27330894..f107cd69 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -91,14 +91,6 @@ pub mod canvas; #[doc(no_inline)] pub use canvas::Canvas; -#[cfg(feature = "qr_code")] -#[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub mod qr_code; - -#[cfg(feature = "qr_code")] -#[doc(no_inline)] -pub use qr_code::QRCode; - pub use action::Action; pub use id::Id; pub use operation::Operation; -- cgit From 868f79d22e2be82e98b06d66da3b4cbc6139d7c7 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 Mar 2023 00:40:36 +0100 Subject: Reuse `ClipMask` in `iced_tiny_skia` --- tiny_skia/src/backend.rs | 41 +++++++++++++++++++++++++------------- tiny_skia/src/window/compositor.rs | 7 +++++-- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 838426f5..2e4663ea 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -25,6 +25,7 @@ impl Backend { pub fn draw>( &mut self, pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::ClipMask, primitives: &[Primitive], viewport: &Viewport, background_color: Color, @@ -38,6 +39,7 @@ impl Backend { self.draw_primitive( primitive, pixels, + clip_mask, None, scale_factor, Vector::ZERO, @@ -63,6 +65,7 @@ impl Backend { vertical_alignment: alignment::Vertical::Top, }, pixels, + clip_mask, None, scale_factor, Vector::ZERO, @@ -76,7 +79,8 @@ impl Backend { &mut self, primitive: &Primitive, pixels: &mut tiny_skia::PixmapMut<'_>, - clip_mask: Option<&tiny_skia::ClipMask>, + clip_mask: &mut tiny_skia::ClipMask, + clip_bounds: Option, scale_factor: f32, translation: Vector, ) { @@ -95,6 +99,7 @@ impl Backend { .post_scale(scale_factor, scale_factor); let path = rounded_rectangle(*bounds, *border_radius); + let clip_mask = clip_bounds.map(|_| clip_mask as &_); pixels.fill_path( &path, @@ -151,7 +156,7 @@ impl Backend { *horizontal_alignment, *vertical_alignment, pixels, - clip_mask, + clip_bounds.map(|_| clip_mask as &_), ); } Primitive::Image { .. } => { @@ -173,7 +178,7 @@ impl Backend { transform .post_translate(translation.x, translation.y) .post_scale(scale_factor, scale_factor), - clip_mask, + clip_bounds.map(|_| clip_mask as &_), ); } Primitive::Stroke { @@ -189,7 +194,7 @@ impl Backend { transform .post_translate(translation.x, translation.y) .post_scale(scale_factor, scale_factor), - clip_mask, + clip_bounds.map(|_| clip_mask as &_), ); } Primitive::Group { primitives } => { @@ -198,6 +203,7 @@ impl Backend { primitive, pixels, clip_mask, + clip_bounds, scale_factor, translation, ); @@ -211,27 +217,37 @@ impl Backend { content, pixels, clip_mask, + clip_bounds, scale_factor, translation + *offset, ); } Primitive::Clip { bounds, content } => { + let bounds = (*bounds + translation) * scale_factor; + + adjust_clip_mask(clip_mask, pixels, bounds); + self.draw_primitive( content, pixels, - Some(&rectangular_clip_mask( - pixels, - (*bounds + translation) * scale_factor, - )), + clip_mask, + Some(bounds), scale_factor, translation, ); + + if let Some(bounds) = clip_bounds { + adjust_clip_mask(clip_mask, pixels, bounds); + } else { + clip_mask.clear(); + } } Primitive::Cache { content } => { self.draw_primitive( content, pixels, clip_mask, + clip_bounds, scale_factor, translation, ); @@ -393,12 +409,11 @@ fn arc_to( } } -fn rectangular_clip_mask( +fn adjust_clip_mask( + clip_mask: &mut tiny_skia::ClipMask, pixels: &tiny_skia::PixmapMut<'_>, bounds: Rectangle, -) -> tiny_skia::ClipMask { - let mut clip_mask = tiny_skia::ClipMask::new(); - +) { let path = { let mut builder = tiny_skia::PathBuilder::new(); builder.push_rect(bounds.x, bounds.y, bounds.width, bounds.height); @@ -415,8 +430,6 @@ fn rectangular_clip_mask( true, ) .expect("Set path of clipping area"); - - clip_mask } impl iced_graphics::Backend for Backend { diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 08159cd8..76f371e1 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -6,6 +6,7 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; use std::marker::PhantomData; pub struct Compositor { + clip_mask: tiny_skia::ClipMask, _theme: PhantomData, } @@ -83,9 +84,10 @@ impl iced_graphics::window::Compositor for Compositor { } pub fn new(settings: Settings) -> (Compositor, Backend) { - // TODO + // TOD ( Compositor { + clip_mask: tiny_skia::ClipMask::new(), _theme: PhantomData, }, Backend::new(settings), @@ -93,7 +95,7 @@ pub fn new(settings: Settings) -> (Compositor, Backend) { } pub fn present>( - _compositor: &mut Compositor, + compositor: &mut Compositor, backend: &mut Backend, surface: &mut Surface, primitives: &[Primitive], @@ -110,6 +112,7 @@ pub fn present>( physical_size.height, ) .expect("Create pixel map"), + &mut compositor.clip_mask, primitives, viewport, background_color, -- cgit From bbeaf10c04a922af5c1c3b898f0c4301d23feab0 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 03:55:07 +0100 Subject: Mark `Primitive` as `non-exhaustive` in `iced_graphics` --- graphics/Cargo.toml | 5 ++++- graphics/src/primitive.rs | 3 +++ tiny_skia/Cargo.toml | 1 + wgpu/src/layer.rs | 3 +-- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 36d8a516..62e67cf8 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -34,7 +34,6 @@ log = "0.4" raw-window-handle = "0.5" thiserror = "1.0" bitflags = "1.2" -tiny-skia = "0.8" [dependencies.bytemuck] version = "1.4" @@ -48,6 +47,10 @@ path = "../native" version = "0.7" path = "../style" +[dependencies.tiny-skia] +version = "0.8" +optional = true + [dependencies.image_rs] version = "0.24" package = "image" diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index e4826591..5a48639d 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -9,6 +9,7 @@ use std::sync::Arc; /// A rendering primitive. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum Primitive { /// A text primitive Text { @@ -85,12 +86,14 @@ pub enum Primitive { /// The [`Gradient`] to apply to the mesh. gradient: Gradient, }, + #[cfg(feature = "tiny_skia")] Fill { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, rule: tiny_skia::FillRule, transform: tiny_skia::Transform, }, + #[cfg(feature = "tiny_skia")] Stroke { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 5f39fce2..72181735 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -24,6 +24,7 @@ path = "../native" [dependencies.iced_graphics] version = "0.7" path = "../graphics" +features = ["tiny-skia"] [dependencies.cosmic-text] features = ["std", "swash"] diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 0840555a..69fcf899 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -265,9 +265,8 @@ impl<'a> Layer<'a> { current_layer, ); } - Primitive::Fill { .. } | Primitive::Stroke { .. } => { + _ => { // Unsupported! - // TODO: Draw a placeholder (?) } } } -- cgit From d13d19ba3569560edd67f20b48f37548d10ceee9 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 04:00:44 +0100 Subject: Rename `canvas::frame` to `canvas` in `iced_wgpu` --- graphics/src/primitive.rs | 4 +- renderer/src/widget/canvas.rs | 6 +- tiny_skia/src/backend.rs | 3 + wgpu/src/canvas.rs | 608 ++++++++++++++++++++++++++++++++++++ wgpu/src/lib.rs | 5 +- wgpu/src/widget.rs | 9 - wgpu/src/widget/canvas.rs | 16 - wgpu/src/widget/canvas/cache.rs | 93 ------ wgpu/src/widget/canvas/frame.rs | 609 ------------------------------------- wgpu/src/widget/canvas/geometry.rs | 24 -- 10 files changed, 619 insertions(+), 758 deletions(-) create mode 100644 wgpu/src/canvas.rs delete mode 100644 wgpu/src/widget.rs delete mode 100644 wgpu/src/widget/canvas.rs delete mode 100644 wgpu/src/widget/canvas/cache.rs delete mode 100644 wgpu/src/widget/canvas/frame.rs delete mode 100644 wgpu/src/widget/canvas/geometry.rs diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 5a48639d..f900b3fd 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -86,14 +86,14 @@ pub enum Primitive { /// The [`Gradient`] to apply to the mesh. gradient: Gradient, }, - #[cfg(feature = "tiny_skia")] + #[cfg(feature = "tiny-skia")] Fill { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, rule: tiny_skia::FillRule, transform: tiny_skia::Transform, }, - #[cfg(feature = "tiny_skia")] + #[cfg(feature = "tiny-skia")] Stroke { path: tiny_skia::Path, paint: tiny_skia::Paint<'static>, diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs index f40a1097..35c8fff9 100644 --- a/renderer/src/widget/canvas.rs +++ b/renderer/src/widget/canvas.rs @@ -16,7 +16,7 @@ use crate::{Backend, Point, Rectangle, Size, Vector}; pub use crate::Geometry; pub enum Frame { - Wgpu(iced_wgpu::widget::canvas::Frame), + Wgpu(iced_wgpu::canvas::Frame), TinySkia(iced_tiny_skia::canvas::Frame), } @@ -33,7 +33,7 @@ impl Frame { pub fn new(renderer: &crate::Renderer, size: Size) -> Self { match renderer.backend() { Backend::Wgpu(_) => { - Frame::Wgpu(iced_wgpu::widget::canvas::Frame::new(size)) + Frame::Wgpu(iced_wgpu::canvas::Frame::new(size)) } Backend::TinySkia(_) => { Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) @@ -131,7 +131,7 @@ impl Frame { pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { let mut frame = match self { Self::Wgpu(_) => { - Self::Wgpu(iced_wgpu::widget::canvas::Frame::new(region.size())) + Self::Wgpu(iced_wgpu::canvas::Frame::new(region.size())) } Self::TinySkia(_) => Self::TinySkia( iced_tiny_skia::canvas::Frame::new(region.size()), diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 2e4663ea..6883a953 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -256,6 +256,9 @@ impl Backend { // Not supported! // TODO: Draw a placeholder (?) / Log it (?) } + _ => { + // Not supported! + } } } } diff --git a/wgpu/src/canvas.rs b/wgpu/src/canvas.rs new file mode 100644 index 00000000..e8d540c3 --- /dev/null +++ b/wgpu/src/canvas.rs @@ -0,0 +1,608 @@ +use iced_graphics::primitive::{self, Primitive}; +use iced_native::widget::canvas::fill::{self, Fill}; +use iced_native::widget::canvas::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; +use iced_native::{Gradient, Point, Rectangle, Size, Vector}; + +use lyon::geom::euclid; +use lyon::tessellation; +use std::borrow::Cow; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: crate::widget::Canvas +#[allow(missing_debug_implementations)] +pub struct Frame { + size: Size, + buffers: BufferStack, + primitives: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + +impl Frame { + /// 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. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.push_transform(); + + f(self); + + self.pop_transform(); + } + + pub fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + pub fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + self.clip(frame, translation); + } + + pub fn clip(&mut self, frame: Frame, translation: Vector) { + let size = frame.size(); + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(size), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. + pub fn into_primitive(self) -> Primitive { + Primitive::Group { + primitives: self.into_primitives(), + } + } + + fn into_primitives(mut self) -> Vec { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } + } + + self.primitives + } +} + +struct Vertex2DBuilder; + +impl tessellation::FillVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + walk_along_path, RepeatedPattern, WalkerEvent, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened(0.01), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 8be12602..31db16a8 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -37,12 +37,13 @@ #![forbid(rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] - pub mod layer; pub mod settings; -pub mod widget; pub mod window; +#[cfg(feature = "canvas")] +pub mod canvas; + mod backend; mod buffer; mod quad; diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs deleted file mode 100644 index 8d05041e..00000000 --- a/wgpu/src/widget.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Use the graphical widgets supported out-of-the-box. - -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs deleted file mode 100644 index 41444fcf..00000000 --- a/wgpu/src/widget/canvas.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod cache; -mod frame; -mod geometry; - -pub use cache::Cache; -pub use frame::Frame; -pub use geometry::Geometry; - -pub use iced_native::widget::canvas::event::{self, Event}; -pub use iced_native::widget::canvas::fill::{self, Fill}; -pub use iced_native::widget::canvas::gradient::{self, Gradient}; -pub use iced_native::widget::canvas::path::{self, Path}; -pub use iced_native::widget::canvas::stroke::{self, Stroke}; -pub use iced_native::widget::canvas::{ - Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, -}; diff --git a/wgpu/src/widget/canvas/cache.rs b/wgpu/src/widget/canvas/cache.rs deleted file mode 100644 index 09b26b90..00000000 --- a/wgpu/src/widget/canvas/cache.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::widget::canvas::{Frame, Geometry}; -use crate::Primitive; - -use iced_native::Size; -use std::{cell::RefCell, sync::Arc}; - -#[derive(Default)] -enum State { - #[default] - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: Default::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Geometry::from_primitive(Primitive::Cache { - content: primitive.clone(), - }); - } - } - - let mut frame = Frame::new(bounds); - draw_fn(&mut frame); - - let primitive = Arc::new(frame.into_primitive()); - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - Geometry::from_primitive(Primitive::Cache { content: primitive }) - } -} - -impl std::fmt::Debug for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::Empty => write!(f, "Empty"), - State::Filled { primitive, bounds } => f - .debug_struct("Filled") - .field("primitive", primitive) - .field("bounds", bounds) - .finish(), - } - } -} diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs deleted file mode 100644 index 987570ec..00000000 --- a/wgpu/src/widget/canvas/frame.rs +++ /dev/null @@ -1,609 +0,0 @@ -use crate::primitive::{self, Primitive}; -use crate::widget::canvas::fill::{self, Fill}; -use crate::widget::canvas::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, -}; - -use iced_native::{Gradient, Point, Rectangle, Size, Vector}; - -use lyon::geom::euclid; -use lyon::tessellation; -use std::borrow::Cow; - -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - gradient.clone(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } - - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } - - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } -} - -#[derive(Debug)] -struct Transforms { - previous: Vec, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} - -impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { - let transformed = self - .raw - .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - let (start, end) = match &mut gradient { - Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), - }; - self.transform_point(start); - self.transform_point(end); - gradient - } -} - -impl Frame { - /// 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. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - if self.transforms.current.is_identity { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); - - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = into_line_cap(stroke.line_cap); - options.end_cap = into_line_cap(stroke.line_cap); - options.line_join = into_line_join(stroke.line_join); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); - - Point::new(transformed.x, transformed.y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - self.push_transform(); - - f(self); - - self.pop_transform(); - } - - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); - } - - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = Frame::new(region.size()); - - f(&mut frame); - - let translation = Vector::new(region.x, region.y); - - self.clip(frame, translation); - } - - pub fn clip(&mut self, frame: Frame, translation: Vector) { - let size = frame.size(); - let primitives = frame.into_primitives(); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale, scale); - self.transforms.current.is_identity = false; - } - - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), - } - } - - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) - } - } - Buffer::Gradient(buffer, gradient) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - gradient, - }) - } - } - } - } - - self.primitives - } -} - -struct Vertex2DBuilder; - -impl tessellation::FillVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -impl tessellation::StrokeVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -struct TriangleVertex2DBuilder([f32; 4]); - -impl tessellation::FillVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -impl tessellation::StrokeVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { - match line_join { - LineJoin::Miter => lyon::tessellation::LineJoin::Miter, - LineJoin::Round => lyon::tessellation::LineJoin::Round, - LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, - } -} - -fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { - match line_cap { - LineCap::Butt => lyon::tessellation::LineCap::Butt, - LineCap::Square => lyon::tessellation::LineCap::Square, - LineCap::Round => lyon::tessellation::LineCap::Round, - } -} - -fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { - match rule { - fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, - fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, - } -} - -pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { - use lyon::algorithms::walk::{ - walk_along_path, RepeatedPattern, WalkerEvent, - }; - use lyon::path::iterator::PathIterator; - - Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1) - .then(|| [line_dash.segments, line_dash.segments].concat()); - - let mut draw_line = false; - - walk_along_path( - path.raw().iter().flattened(0.01), - 0.0, - lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, - &mut RepeatedPattern { - callback: |event: WalkerEvent<'_>| { - let point = Point { - x: event.position.x, - y: event.position.y, - }; - - if draw_line { - builder.line_to(point); - } else { - builder.move_to(point); - } - - draw_line = !draw_line; - - true - }, - index: line_dash.offset, - intervals: segments_odd - .as_deref() - .unwrap_or(line_dash.segments), - }, - ); - }) -} diff --git a/wgpu/src/widget/canvas/geometry.rs b/wgpu/src/widget/canvas/geometry.rs deleted file mode 100644 index e8ac621d..00000000 --- a/wgpu/src/widget/canvas/geometry.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::Primitive; - -/// A bunch of shapes that can be drawn. -/// -/// [`Geometry`] can be easily generated with a [`Frame`] or stored in a -/// [`Cache`]. -/// -/// [`Frame`]: crate::widget::canvas::Frame -/// [`Cache`]: crate::widget::canvas::Cache -#[derive(Debug, Clone)] -pub struct Geometry(Primitive); - -impl Geometry { - pub(crate) fn from_primitive(primitive: Primitive) -> Self { - Self(primitive) - } - - /// Turns the [`Geometry`] into a [`Primitive`]. - /// - /// This can be useful if you are building a custom widget. - pub fn into_primitive(self) -> Primitive { - self.0 - } -} -- cgit From 6cc48b5c62bac287b8f9f1c79c1fb7486c51b18f Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 04:57:55 +0100 Subject: Move `Canvas` and `QRCode` to `iced` crate Rename `canvas` modules to `geometry` in graphics subcrates --- Cargo.toml | 9 +- examples/arc/src/main.rs | 2 +- examples/bezier_tool/src/main.rs | 2 +- examples/color_palette/src/main.rs | 2 +- examples/game_of_life/src/main.rs | 2 +- examples/sierpinski_triangle/src/main.rs | 2 +- examples/solar_system/src/main.rs | 2 +- graphics/Cargo.toml | 6 +- graphics/src/backend.rs | 2 - graphics/src/geometry.rs | 36 ++ graphics/src/geometry/fill.rs | 63 ++++ graphics/src/geometry/path.rs | 67 ++++ graphics/src/geometry/path/arc.rs | 42 +++ graphics/src/geometry/path/builder.rs | 192 ++++++++++ graphics/src/geometry/stroke.rs | 106 ++++++ graphics/src/geometry/style.rs | 23 ++ graphics/src/geometry/text.rs | 57 +++ graphics/src/lib.rs | 6 + graphics/src/renderer.rs | 8 +- native/Cargo.toml | 5 - native/src/widget/canvas.rs | 259 ------------- native/src/widget/canvas/cursor.rs | 64 ---- native/src/widget/canvas/event.rs | 21 -- native/src/widget/canvas/fill.rs | 64 ---- native/src/widget/canvas/path.rs | 67 ---- native/src/widget/canvas/path/arc.rs | 42 --- native/src/widget/canvas/path/builder.rs | 192 ---------- native/src/widget/canvas/program.rs | 108 ------ native/src/widget/canvas/stroke.rs | 106 ------ native/src/widget/canvas/style.rs | 24 -- native/src/widget/canvas/text.rs | 57 --- renderer/Cargo.toml | 8 +- renderer/src/backend.rs | 4 +- renderer/src/geometry.rs | 168 +++++++++ renderer/src/geometry/cache.rs | 85 +++++ renderer/src/lib.rs | 13 +- renderer/src/widget/canvas.rs | 177 --------- renderer/src/widget/canvas/cache.rs | 85 ----- renderer/src/widget/qr_code.rs | 301 --------------- src/widget.rs | 8 +- src/widget/canvas.rs | 238 ++++++++++++ src/widget/canvas/cursor.rs | 64 ++++ src/widget/canvas/event.rs | 21 ++ src/widget/canvas/program.rs | 108 ++++++ src/widget/qr_code.rs | 300 +++++++++++++++ tiny_skia/Cargo.toml | 2 +- tiny_skia/src/backend.rs | 2 - tiny_skia/src/canvas.rs | 287 --------------- tiny_skia/src/geometry.rs | 287 +++++++++++++++ tiny_skia/src/lib.rs | 4 +- wgpu/Cargo.toml | 2 +- wgpu/src/backend.rs | 2 - wgpu/src/canvas.rs | 608 ------------------------------- wgpu/src/geometry.rs | 608 +++++++++++++++++++++++++++++++ wgpu/src/lib.rs | 4 +- 55 files changed, 2508 insertions(+), 2516 deletions(-) create mode 100644 graphics/src/geometry.rs create mode 100644 graphics/src/geometry/fill.rs create mode 100644 graphics/src/geometry/path.rs create mode 100644 graphics/src/geometry/path/arc.rs create mode 100644 graphics/src/geometry/path/builder.rs create mode 100644 graphics/src/geometry/stroke.rs create mode 100644 graphics/src/geometry/style.rs create mode 100644 graphics/src/geometry/text.rs delete mode 100644 native/src/widget/canvas.rs delete mode 100644 native/src/widget/canvas/cursor.rs delete mode 100644 native/src/widget/canvas/event.rs delete mode 100644 native/src/widget/canvas/fill.rs delete mode 100644 native/src/widget/canvas/path.rs delete mode 100644 native/src/widget/canvas/path/arc.rs delete mode 100644 native/src/widget/canvas/path/builder.rs delete mode 100644 native/src/widget/canvas/program.rs delete mode 100644 native/src/widget/canvas/stroke.rs delete mode 100644 native/src/widget/canvas/style.rs delete mode 100644 native/src/widget/canvas/text.rs create mode 100644 renderer/src/geometry.rs create mode 100644 renderer/src/geometry/cache.rs delete mode 100644 renderer/src/widget/canvas.rs delete mode 100644 renderer/src/widget/canvas/cache.rs delete mode 100644 renderer/src/widget/qr_code.rs create mode 100644 src/widget/canvas.rs create mode 100644 src/widget/canvas/cursor.rs create mode 100644 src/widget/canvas/event.rs create mode 100644 src/widget/canvas/program.rs create mode 100644 src/widget/qr_code.rs delete mode 100644 tiny_skia/src/canvas.rs create mode 100644 tiny_skia/src/geometry.rs delete mode 100644 wgpu/src/canvas.rs create mode 100644 wgpu/src/geometry.rs diff --git a/Cargo.toml b/Cargo.toml index 942966e5..28938df9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ image = ["iced_renderer/image", "image_rs"] # Enables the `Svg` widget svg = ["iced_renderer/svg"] # Enables the `Canvas` widget -canvas = ["iced_renderer/canvas"] +canvas = ["iced_renderer/geometry"] # Enables the `QRCode` widget -qr_code = ["iced_renderer/qr_code"] +qr_code = ["canvas", "qrcode"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -69,6 +69,11 @@ version = "0.24" package = "image" optional = true +[dependencies.qrcode] +version = "0.12" +optional = true +default-features = false + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index d71ba6f6..80ad0b5b 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -69,7 +69,7 @@ impl Application for Arc { } } -impl canvas::Program for Arc { +impl canvas::Program for Arc { type State = (); fn draw( diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 5bb463c3..f1c83a16 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -92,7 +92,7 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl<'a> canvas::Program for Bezier<'a> { type State = Option; fn update( diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index 1109a883..5c4304ee 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -237,7 +237,7 @@ impl Theme { } } -impl canvas::Program for Theme { +impl canvas::Program for Theme { type State = (); fn draw( diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index cdb33aca..eab8908b 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -393,7 +393,7 @@ mod grid { } } - impl canvas::Program for Grid { + impl canvas::Program for Grid { type State = Interaction; fn update( diff --git a/examples/sierpinski_triangle/src/main.rs b/examples/sierpinski_triangle/src/main.rs index e85f8391..4faac6d6 100644 --- a/examples/sierpinski_triangle/src/main.rs +++ b/examples/sierpinski_triangle/src/main.rs @@ -97,7 +97,7 @@ struct SierpinskiGraph { cache: canvas::Cache, } -impl canvas::Program for SierpinskiGraph { +impl canvas::Program for SierpinskiGraph { type State = (); fn update( diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index 0023a69b..f2606feb 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -150,7 +150,7 @@ impl State { } } -impl canvas::Program for State { +impl canvas::Program for State { type State = (); fn draw( diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 62e67cf8..98e6f474 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -24,7 +24,7 @@ bmp = ["image_rs/bmp"] hdr = ["image_rs/hdr"] dds = ["image_rs/dds"] farbfeld = ["image_rs/farbfeld"] -canvas = ["iced_native/canvas"] +geometry = ["lyon_path"] opengl = [] image_rs = ["kamadak-exif"] @@ -65,6 +65,10 @@ optional = true version = "0.5" optional = true +[dependencies.lyon_path] +version = "1" +optional = true + [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] all-features = true diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index c44372e8..8658cffe 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -10,8 +10,6 @@ use std::borrow::Cow; /// /// [`Renderer`]: crate::Renderer pub trait Backend { - type Geometry: Into; - /// Trims the measurements cache. /// /// This method is currently necessary to properly trim the text cache in diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs new file mode 100644 index 00000000..29ac84d6 --- /dev/null +++ b/graphics/src/geometry.rs @@ -0,0 +1,36 @@ +//! Draw 2D graphics for your users. +//! +//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a +//! [`Frame`]. It can be used for animation, data visualization, game graphics, +//! and more! +pub mod fill; +pub mod path; +pub mod stroke; + +mod style; +mod text; + +pub use fill::Fill; +pub use path::Path; +pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; +pub use style::Style; +pub use text::Text; + +pub use iced_native::gradient::{self, Gradient}; + +use crate::Primitive; + +#[derive(Debug, Clone)] +pub struct Geometry(pub Primitive); + +impl From for Primitive { + fn from(geometry: Geometry) -> Self { + geometry.0 + } +} + +pub trait Renderer: iced_native::Renderer { + type Geometry; + + fn draw(&mut self, geometry: Vec); +} diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs new file mode 100644 index 00000000..109d5e99 --- /dev/null +++ b/graphics/src/geometry/fill.rs @@ -0,0 +1,63 @@ +//! Fill [crate::widget::canvas::Geometry] with a certain style. +use crate::{Color, Gradient}; + +pub use crate::geometry::Style; + +/// The style used to fill geometry. +#[derive(Debug, Clone)] +pub struct Fill { + /// The color or gradient of the fill. + /// + /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + + /// The fill rule defines how to determine what is inside and what is + /// outside of a shape. + /// + /// See the [SVG specification][1] for more details. + /// + /// By default, it is set to `NonZero`. + /// + /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty + pub rule: Rule, +} + +impl Default for Fill { + fn default() -> Self { + Self { + style: Style::Solid(Color::BLACK), + rule: Rule::NonZero, + } + } +} + +impl From for Fill { + fn from(color: Color) -> Fill { + Fill { + style: Style::Solid(color), + ..Fill::default() + } + } +} + +impl From for Fill { + fn from(gradient: Gradient) -> Self { + Fill { + style: Style::Gradient(gradient), + ..Default::default() + } + } +} + +/// The fill rule defines how to determine what is inside and what is outside of +/// a shape. +/// +/// See the [SVG specification][1]. +/// +/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum Rule { + NonZero, + EvenOdd, +} diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs new file mode 100644 index 00000000..30c387c5 --- /dev/null +++ b/graphics/src/geometry/path.rs @@ -0,0 +1,67 @@ +//! Build different kinds of 2D shapes. +pub mod arc; + +mod builder; + +#[doc(no_inline)] +pub use arc::Arc; +pub use builder::Builder; + +pub use lyon_path; + +use crate::{Point, Size}; + +/// An immutable set of points that may or may not be connected. +/// +/// A single [`Path`] can represent different kinds of 2D shapes! +#[derive(Debug, Clone)] +pub struct Path { + raw: lyon_path::Path, +} + +impl Path { + /// Creates a new [`Path`] with the provided closure. + /// + /// Use the [`Builder`] to configure your [`Path`]. + pub fn new(f: impl FnOnce(&mut Builder)) -> Self { + let mut builder = Builder::new(); + + // TODO: Make it pure instead of side-effect-based (?) + f(&mut builder); + + builder.build() + } + + /// Creates a new [`Path`] representing a line segment given its starting + /// and end points. + pub fn line(from: Point, to: Point) -> Self { + Self::new(|p| { + p.move_to(from); + p.line_to(to); + }) + } + + /// Creates a new [`Path`] representing a rectangle given its top-left + /// corner coordinate and its `Size`. + pub fn rectangle(top_left: Point, size: Size) -> Self { + Self::new(|p| p.rectangle(top_left, size)) + } + + /// Creates a new [`Path`] representing a circle given its center + /// coordinate and its radius. + pub fn circle(center: Point, radius: f32) -> Self { + Self::new(|p| p.circle(center, radius)) + } + + #[inline] + pub fn raw(&self) -> &lyon_path::Path { + &self.raw + } + + #[inline] + pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { + Path { + raw: self.raw.clone().transformed(transform), + } + } +} diff --git a/graphics/src/geometry/path/arc.rs b/graphics/src/geometry/path/arc.rs new file mode 100644 index 00000000..e0747d3e --- /dev/null +++ b/graphics/src/geometry/path/arc.rs @@ -0,0 +1,42 @@ +//! Build and draw curves. +use crate::{Point, Vector}; + +/// A segment of a differentiable curve. +#[derive(Debug, Clone, Copy)] +pub struct Arc { + /// The center of the arc. + pub center: Point, + /// The radius of the arc. + pub radius: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +/// An elliptical [`Arc`]. +#[derive(Debug, Clone, Copy)] +pub struct Elliptical { + /// The center of the arc. + pub center: Point, + /// The radii of the arc's ellipse, defining its axes. + pub radii: Vector, + /// The rotation of the arc's ellipse. + pub rotation: f32, + /// The start of the segment's angle, clockwise rotation. + pub start_angle: f32, + /// The end of the segment's angle, clockwise rotation. + pub end_angle: f32, +} + +impl From for Elliptical { + fn from(arc: Arc) -> Elliptical { + Elliptical { + center: arc.center, + radii: Vector::new(arc.radius, arc.radius), + rotation: 0.0, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + } + } +} diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs new file mode 100644 index 00000000..4a9c5e36 --- /dev/null +++ b/graphics/src/geometry/path/builder.rs @@ -0,0 +1,192 @@ +use crate::geometry::path::{arc, Arc, Path}; +use crate::{Point, Size}; + +use lyon_path::builder::{self, SvgPathBuilder}; +use lyon_path::geom; +use lyon_path::math; + +/// A [`Path`] builder. +/// +/// Once a [`Path`] is built, it can no longer be mutated. +#[allow(missing_debug_implementations)] +pub struct Builder { + raw: builder::WithSvg, +} + +impl Builder { + /// Creates a new [`Builder`]. + pub fn new() -> Builder { + Builder { + raw: lyon_path::Path::builder().with_svg(), + } + } + + /// Moves the starting point of a new sub-path to the given `Point`. + #[inline] + pub fn move_to(&mut self, point: Point) { + let _ = self.raw.move_to(math::Point::new(point.x, point.y)); + } + + /// Connects the last point in the [`Path`] to the given `Point` with a + /// straight line. + #[inline] + pub fn line_to(&mut self, point: Point) { + let _ = self.raw.line_to(math::Point::new(point.x, point.y)); + } + + /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in + /// a clockwise direction. + #[inline] + pub fn arc(&mut self, arc: Arc) { + self.ellipse(arc.into()); + } + + /// Adds a circular arc to the [`Path`] with the given control points and + /// radius. + /// + /// This essentially draws a straight line segment from the current + /// position to `a`, but fits a circular arc of `radius` tangent to that + /// segment and tangent to the line between `a` and `b`. + /// + /// With another `.line_to(b)`, the result will be a path connecting the + /// starting point and `b` with straight line segments towards `a` and a + /// circular arc smoothing out the corner at `a`. + /// + /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) + /// for more details and examples. + pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { + let start = self.raw.current_position(); + let mid = math::Point::new(a.x, a.y); + let end = math::Point::new(b.x, b.y); + + if start == mid || mid == end || radius == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let double_area = start.x * (mid.y - end.y) + + mid.x * (end.y - start.y) + + end.x * (start.y - mid.y); + + if double_area == 0.0 { + let _ = self.raw.line_to(mid); + return; + } + + let to_start = (start - mid).normalize(); + let to_end = (end - mid).normalize(); + + let inner_angle = to_start.dot(to_end).acos(); + + let origin_angle = inner_angle / 2.0; + + let origin_adjacent = radius / origin_angle.tan(); + + let arc_start = mid + to_start * origin_adjacent; + let arc_end = mid + to_end * origin_adjacent; + + let sweep = to_start.cross(to_end) < 0.0; + + let _ = self.raw.line_to(arc_start); + + self.raw.arc_to( + math::Vector::new(radius, radius), + math::Angle::radians(0.0), + lyon_path::ArcFlags { + large_arc: false, + sweep, + }, + arc_end, + ); + } + + /// Adds an ellipse to the [`Path`] using a clockwise direction. + pub fn ellipse(&mut self, arc: arc::Elliptical) { + let arc = geom::Arc { + center: math::Point::new(arc.center.x, arc.center.y), + radii: math::Vector::new(arc.radii.x, arc.radii.y), + x_rotation: math::Angle::radians(arc.rotation), + start_angle: math::Angle::radians(arc.start_angle), + sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), + }; + + let _ = self.raw.move_to(arc.sample(0.0)); + + arc.for_each_quadratic_bezier(&mut |curve| { + let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); + }); + } + + /// Adds a cubic Bézier curve to the [`Path`] given its two control points + /// and its end point. + #[inline] + pub fn bezier_curve_to( + &mut self, + control_a: Point, + control_b: Point, + to: Point, + ) { + let _ = self.raw.cubic_bezier_to( + math::Point::new(control_a.x, control_a.y), + math::Point::new(control_b.x, control_b.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a quadratic Bézier curve to the [`Path`] given its control point + /// and its end point. + #[inline] + pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { + let _ = self.raw.quadratic_bezier_to( + math::Point::new(control.x, control.y), + math::Point::new(to.x, to.y), + ); + } + + /// Adds a rectangle to the [`Path`] given its top-left corner coordinate + /// and its `Size`. + #[inline] + pub fn rectangle(&mut self, top_left: Point, size: Size) { + self.move_to(top_left); + self.line_to(Point::new(top_left.x + size.width, top_left.y)); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height, + )); + self.line_to(Point::new(top_left.x, top_left.y + size.height)); + self.close(); + } + + /// Adds a circle to the [`Path`] given its center coordinate and its + /// radius. + #[inline] + pub fn circle(&mut self, center: Point, radius: f32) { + self.arc(Arc { + center, + radius, + start_angle: 0.0, + end_angle: 2.0 * std::f32::consts::PI, + }); + } + + /// Closes the current sub-path in the [`Path`] with a straight line to + /// the starting point. + #[inline] + pub fn close(&mut self) { + self.raw.close() + } + + /// Builds the [`Path`] of this [`Builder`]. + #[inline] + pub fn build(self) -> Path { + Path { + raw: self.raw.build(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs new file mode 100644 index 00000000..b551a9c9 --- /dev/null +++ b/graphics/src/geometry/stroke.rs @@ -0,0 +1,106 @@ +//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. +pub use crate::geometry::Style; + +use crate::Color; + +/// The style of a stroke. +#[derive(Debug, Clone)] +pub struct Stroke<'a> { + /// The color or gradient of the stroke. + /// + /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. + pub style: Style, + /// The distance between the two edges of the stroke. + pub width: f32, + /// The shape to be used at the end of open subpaths when they are stroked. + pub line_cap: LineCap, + /// The shape to be used at the corners of paths or basic shapes when they + /// are stroked. + pub line_join: LineJoin, + /// The dash pattern used when stroking the line. + pub line_dash: LineDash<'a>, +} + +impl<'a> Stroke<'a> { + /// Sets the color of the [`Stroke`]. + pub fn with_color(self, color: Color) -> Self { + Stroke { + style: Style::Solid(color), + ..self + } + } + + /// Sets the width of the [`Stroke`]. + pub fn with_width(self, width: f32) -> Self { + Stroke { width, ..self } + } + + /// Sets the [`LineCap`] of the [`Stroke`]. + pub fn with_line_cap(self, line_cap: LineCap) -> Self { + Stroke { line_cap, ..self } + } + + /// Sets the [`LineJoin`] of the [`Stroke`]. + pub fn with_line_join(self, line_join: LineJoin) -> Self { + Stroke { line_join, ..self } + } +} + +impl<'a> Default for Stroke<'a> { + fn default() -> Self { + Stroke { + style: Style::Solid(Color::BLACK), + width: 1.0, + line_cap: LineCap::default(), + line_join: LineJoin::default(), + line_dash: LineDash::default(), + } + } +} + +/// The shape used at the end of open subpaths when they are stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineCap { + /// The stroke for each sub-path does not extend beyond its two endpoints. + Butt, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a square. + Square, + /// At the end of each sub-path, the shape representing the stroke will be + /// extended by a semicircle. + Round, +} + +impl Default for LineCap { + fn default() -> LineCap { + LineCap::Butt + } +} + +/// The shape used at the corners of paths or basic shapes when they are +/// stroked. +#[derive(Debug, Clone, Copy)] +pub enum LineJoin { + /// A sharp corner. + Miter, + /// A round corner. + Round, + /// A bevelled corner. + Bevel, +} + +impl Default for LineJoin { + fn default() -> LineJoin { + LineJoin::Miter + } +} + +/// The dash pattern used when stroking the line. +#[derive(Debug, Clone, Copy, Default)] +pub struct LineDash<'a> { + /// The alternating lengths of lines and gaps which describe the pattern. + pub segments: &'a [f32], + + /// The offset of [`LineDash::segments`] to start the pattern. + pub offset: usize, +} diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs new file mode 100644 index 00000000..6794f2e7 --- /dev/null +++ b/graphics/src/geometry/style.rs @@ -0,0 +1,23 @@ +use crate::{Color, Gradient}; + +/// The coloring style of some drawing. +#[derive(Debug, Clone, PartialEq)] +pub enum Style { + /// A solid [`Color`]. + Solid(Color), + + /// A [`Gradient`] color. + Gradient(Gradient), +} + +impl From for Style { + fn from(color: Color) -> Self { + Self::Solid(color) + } +} + +impl From for Style { + fn from(gradient: Gradient) -> Self { + Self::Gradient(gradient) + } +} diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs new file mode 100644 index 00000000..8c0b2dfb --- /dev/null +++ b/graphics/src/geometry/text.rs @@ -0,0 +1,57 @@ +use crate::alignment; +use crate::{Color, Font, Point}; + +/// A bunch of text that can be drawn to a canvas +#[derive(Debug, Clone)] +pub struct Text { + /// The contents of the text + pub content: String, + /// The position of the text relative to the alignment properties. + /// By default, this position will be relative to the top-left corner coordinate meaning that + /// if the horizontal and vertical alignments are unchanged, this property will tell where the + /// top-left corner of the text should be placed. + /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to + /// change what part of text is placed at this positions. + /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the + /// center of the text will be placed at the given position NOT the top-left coordinate. + pub position: Point, + /// The color of the text + pub color: Color, + /// The size of the text + pub size: f32, + /// 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, +} + +impl Default for Text { + fn default() -> Text { + Text { + content: String::new(), + position: Point::ORIGIN, + color: Color::BLACK, + size: 16.0, + font: Font::SansSerif, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + } + } +} + +impl From for Text { + fn from(content: String) -> Text { + Text { + content, + ..Default::default() + } + } +} + +impl From<&str> for Text { + fn from(content: &str) -> Text { + String::from(content).into() + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index 576b2d78..e56f8ad8 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -33,6 +33,9 @@ pub mod primitive; pub mod renderer; pub mod window; +#[cfg(feature = "geometry")] +pub mod geometry; + pub use antialiasing::Antialiasing; pub use backend::Backend; pub use error::Error; @@ -41,6 +44,9 @@ pub use renderer::Renderer; pub use transformation::Transformation; pub use viewport::Viewport; +#[cfg(feature = "geometry")] +pub use geometry::Geometry; + pub use iced_native::alignment; pub use iced_native::text; pub use iced_native::{ diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 793ee7d7..cb57f429 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -215,15 +215,15 @@ where } } -#[cfg(feature = "canvas")] -impl iced_native::widget::canvas::Renderer for Renderer +#[cfg(feature = "geometry")] +impl crate::geometry::Renderer for Renderer where B: Backend, { - type Geometry = B::Geometry; + type Geometry = crate::Geometry; fn draw(&mut self, layers: Vec) { self.primitives - .extend(layers.into_iter().map(B::Geometry::into)); + .extend(layers.into_iter().map(crate::Geometry::into)); } } diff --git a/native/Cargo.toml b/native/Cargo.toml index 23533e33..1eedf0da 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT" repository = "https://github.com/iced-rs/iced" [features] -canvas = ["lyon_path"] debug = [] [dependencies] @@ -29,7 +28,3 @@ features = ["thread-pool"] [dependencies.iced_style] version = "0.7" path = "../style" - -[dependencies.lyon_path] -version = "1" -optional = true diff --git a/native/src/widget/canvas.rs b/native/src/widget/canvas.rs deleted file mode 100644 index 8a9addd2..00000000 --- a/native/src/widget/canvas.rs +++ /dev/null @@ -1,259 +0,0 @@ -//! Draw 2D graphics for your users. -//! -//! A [`Canvas`] widget can be used to draw different kinds of 2D shapes in a -//! [`Frame`]. It can be used for animation, data visualization, game graphics, -//! and more! -pub mod event; -pub mod fill; -pub mod path; -pub mod stroke; - -mod cursor; -mod program; -mod style; -mod text; - -pub use crate::gradient::{self, Gradient}; -pub use cursor::Cursor; -pub use event::Event; -pub use fill::Fill; -pub use path::Path; -pub use program::Program; -pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; -pub use style::Style; -pub use text::Text; - -use crate::layout::{self, Layout}; -use crate::mouse; -use crate::renderer; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, Widget, -}; - -use std::marker::PhantomData; - -/// A widget capable of drawing 2D graphics. -/// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// -/// ```no_run -/// # mod iced { -/// # pub mod widget { -/// # pub use iced_graphics::widget::canvas; -/// # } -/// # pub use iced_native::{Color, Rectangle, Theme}; -/// # } -/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; -/// use iced::{Color, Rectangle, Theme}; -/// -/// // First, we define the data we need for drawing -/// #[derive(Debug)] -/// struct Circle { -/// radius: f32, -/// } -/// -/// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { -/// type State = (); -/// -/// fn draw(&self, _state: &(), _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ -/// // We prepare a new `Frame` -/// let mut frame = Frame::new(bounds.size()); -/// -/// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); -/// -/// // And fill it with some color -/// frame.fill(&circle, Color::BLACK); -/// -/// // Finally, we produce the geometry -/// vec![frame.into_geometry()] -/// } -/// } -/// -/// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); -/// ``` -#[derive(Debug)] -pub struct Canvas -where - Renderer: self::Renderer, - P: Program, -{ - width: Length, - height: Length, - program: P, - message_: PhantomData, - theme_: PhantomData, -} - -impl Canvas -where - Renderer: self::Renderer, - P: Program, -{ - const DEFAULT_SIZE: f32 = 100.0; - - /// Creates a new [`Canvas`]. - pub fn new(program: P) -> Self { - Canvas { - width: Length::Fixed(Self::DEFAULT_SIZE), - height: Length::Fixed(Self::DEFAULT_SIZE), - program, - message_: PhantomData, - theme_: PhantomData, - } - } - - /// Sets the width of the [`Canvas`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Canvas`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } -} - -impl Widget - for Canvas -where - Renderer: self::Renderer, - P: Program, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(P::State::default()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: crate::Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - - let canvas_event = match event { - crate::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - crate::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - crate::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - _ => None, - }; - - let cursor = Cursor::from_window_position(cursor_position); - - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::(); - - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); - - if let Some(message) = message { - shell.publish(message); - } - - return event_status; - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let bounds = layout.bounds(); - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - self.program.mouse_interaction(state, bounds, cursor) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - - if bounds.width < 1.0 || bounds.height < 1.0 { - return; - } - - let cursor = Cursor::from_window_position(cursor_position); - let state = tree.state.downcast_ref::(); - - renderer.with_translation( - Vector::new(bounds.x, bounds.y), - |renderer| { - renderer.draw( - self.program.draw(state, renderer, theme, bounds, cursor), - ); - }, - ); - } -} - -impl<'a, Message, Renderer, P> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + self::Renderer, - P: Program + 'a, -{ - fn from( - canvas: Canvas, - ) -> Element<'a, Message, Renderer> { - Element::new(canvas) - } -} - -pub trait Renderer: crate::Renderer { - type Geometry; - - fn draw(&mut self, geometry: Vec); -} diff --git a/native/src/widget/canvas/cursor.rs b/native/src/widget/canvas/cursor.rs deleted file mode 100644 index ef6a7771..00000000 --- a/native/src/widget/canvas/cursor.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::{Point, Rectangle}; - -/// The mouse cursor state. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Cursor { - /// The cursor has a defined position. - Available(Point), - - /// The cursor is currently unavailable (i.e. out of bounds or busy). - Unavailable, -} - -impl Cursor { - // TODO: Remove this once this type is used in `iced_native` to encode - // proper cursor availability - pub(crate) fn from_window_position(position: Point) -> Self { - if position.x < 0.0 || position.y < 0.0 { - Cursor::Unavailable - } else { - Cursor::Available(position) - } - } - - /// Returns the absolute position of the [`Cursor`], if available. - pub fn position(&self) -> Option { - match self { - Cursor::Available(position) => Some(*position), - Cursor::Unavailable => None, - } - } - - /// Returns the relative position of the [`Cursor`] inside the given bounds, - /// if available. - /// - /// If the [`Cursor`] is not over the provided bounds, this method will - /// return `None`. - pub fn position_in(&self, bounds: &Rectangle) -> Option { - if self.is_over(bounds) { - self.position_from(bounds.position()) - } else { - None - } - } - - /// Returns the relative position of the [`Cursor`] from the given origin, - /// if available. - pub fn position_from(&self, origin: Point) -> Option { - match self { - Cursor::Available(position) => { - Some(Point::new(position.x - origin.x, position.y - origin.y)) - } - Cursor::Unavailable => None, - } - } - - /// Returns whether the [`Cursor`] is currently over the provided bounds - /// or not. - pub fn is_over(&self, bounds: &Rectangle) -> bool { - match self { - Cursor::Available(position) => bounds.contains(*position), - Cursor::Unavailable => false, - } - } -} diff --git a/native/src/widget/canvas/event.rs b/native/src/widget/canvas/event.rs deleted file mode 100644 index 1d726577..00000000 --- a/native/src/widget/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use crate::keyboard; -use crate::mouse; -use crate::touch; - -pub use crate::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::widget::Canvas -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/native/src/widget/canvas/fill.rs b/native/src/widget/canvas/fill.rs deleted file mode 100644 index 92b1e47e..00000000 --- a/native/src/widget/canvas/fill.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Fill [crate::widget::canvas::Geometry] with a certain style. -use crate::widget::canvas::Gradient; -use crate::Color; - -pub use crate::widget::canvas::Style; - -/// The style used to fill geometry. -#[derive(Debug, Clone)] -pub struct Fill { - /// The color or gradient of the fill. - /// - /// By default, it is set to [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - - /// The fill rule defines how to determine what is inside and what is - /// outside of a shape. - /// - /// See the [SVG specification][1] for more details. - /// - /// By default, it is set to `NonZero`. - /// - /// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty - pub rule: Rule, -} - -impl Default for Fill { - fn default() -> Self { - Self { - style: Style::Solid(Color::BLACK), - rule: Rule::NonZero, - } - } -} - -impl From for Fill { - fn from(color: Color) -> Fill { - Fill { - style: Style::Solid(color), - ..Fill::default() - } - } -} - -impl From for Fill { - fn from(gradient: Gradient) -> Self { - Fill { - style: Style::Gradient(gradient), - ..Default::default() - } - } -} - -/// The fill rule defines how to determine what is inside and what is outside of -/// a shape. -/// -/// See the [SVG specification][1]. -/// -/// [1]: https://www.w3.org/TR/SVG/painting.html#FillRuleProperty -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum Rule { - NonZero, - EvenOdd, -} diff --git a/native/src/widget/canvas/path.rs b/native/src/widget/canvas/path.rs deleted file mode 100644 index 30c387c5..00000000 --- a/native/src/widget/canvas/path.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Build different kinds of 2D shapes. -pub mod arc; - -mod builder; - -#[doc(no_inline)] -pub use arc::Arc; -pub use builder::Builder; - -pub use lyon_path; - -use crate::{Point, Size}; - -/// An immutable set of points that may or may not be connected. -/// -/// A single [`Path`] can represent different kinds of 2D shapes! -#[derive(Debug, Clone)] -pub struct Path { - raw: lyon_path::Path, -} - -impl Path { - /// Creates a new [`Path`] with the provided closure. - /// - /// Use the [`Builder`] to configure your [`Path`]. - pub fn new(f: impl FnOnce(&mut Builder)) -> Self { - let mut builder = Builder::new(); - - // TODO: Make it pure instead of side-effect-based (?) - f(&mut builder); - - builder.build() - } - - /// Creates a new [`Path`] representing a line segment given its starting - /// and end points. - pub fn line(from: Point, to: Point) -> Self { - Self::new(|p| { - p.move_to(from); - p.line_to(to); - }) - } - - /// Creates a new [`Path`] representing a rectangle given its top-left - /// corner coordinate and its `Size`. - pub fn rectangle(top_left: Point, size: Size) -> Self { - Self::new(|p| p.rectangle(top_left, size)) - } - - /// Creates a new [`Path`] representing a circle given its center - /// coordinate and its radius. - pub fn circle(center: Point, radius: f32) -> Self { - Self::new(|p| p.circle(center, radius)) - } - - #[inline] - pub fn raw(&self) -> &lyon_path::Path { - &self.raw - } - - #[inline] - pub fn transform(&self, transform: &lyon_path::math::Transform) -> Path { - Path { - raw: self.raw.clone().transformed(transform), - } - } -} diff --git a/native/src/widget/canvas/path/arc.rs b/native/src/widget/canvas/path/arc.rs deleted file mode 100644 index e0747d3e..00000000 --- a/native/src/widget/canvas/path/arc.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Build and draw curves. -use crate::{Point, Vector}; - -/// A segment of a differentiable curve. -#[derive(Debug, Clone, Copy)] -pub struct Arc { - /// The center of the arc. - pub center: Point, - /// The radius of the arc. - pub radius: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -/// An elliptical [`Arc`]. -#[derive(Debug, Clone, Copy)] -pub struct Elliptical { - /// The center of the arc. - pub center: Point, - /// The radii of the arc's ellipse, defining its axes. - pub radii: Vector, - /// The rotation of the arc's ellipse. - pub rotation: f32, - /// The start of the segment's angle, clockwise rotation. - pub start_angle: f32, - /// The end of the segment's angle, clockwise rotation. - pub end_angle: f32, -} - -impl From for Elliptical { - fn from(arc: Arc) -> Elliptical { - Elliptical { - center: arc.center, - radii: Vector::new(arc.radius, arc.radius), - rotation: 0.0, - start_angle: arc.start_angle, - end_angle: arc.end_angle, - } - } -} diff --git a/native/src/widget/canvas/path/builder.rs b/native/src/widget/canvas/path/builder.rs deleted file mode 100644 index 84fda052..00000000 --- a/native/src/widget/canvas/path/builder.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::widget::canvas::path::{arc, Arc, Path}; -use crate::{Point, Size}; - -use lyon_path::builder::{self, SvgPathBuilder}; -use lyon_path::geom; -use lyon_path::math; - -/// A [`Path`] builder. -/// -/// Once a [`Path`] is built, it can no longer be mutated. -#[allow(missing_debug_implementations)] -pub struct Builder { - raw: builder::WithSvg, -} - -impl Builder { - /// Creates a new [`Builder`]. - pub fn new() -> Builder { - Builder { - raw: lyon_path::Path::builder().with_svg(), - } - } - - /// Moves the starting point of a new sub-path to the given `Point`. - #[inline] - pub fn move_to(&mut self, point: Point) { - let _ = self.raw.move_to(math::Point::new(point.x, point.y)); - } - - /// Connects the last point in the [`Path`] to the given `Point` with a - /// straight line. - #[inline] - pub fn line_to(&mut self, point: Point) { - let _ = self.raw.line_to(math::Point::new(point.x, point.y)); - } - - /// Adds an [`Arc`] to the [`Path`] from `start_angle` to `end_angle` in - /// a clockwise direction. - #[inline] - pub fn arc(&mut self, arc: Arc) { - self.ellipse(arc.into()); - } - - /// Adds a circular arc to the [`Path`] with the given control points and - /// radius. - /// - /// This essentially draws a straight line segment from the current - /// position to `a`, but fits a circular arc of `radius` tangent to that - /// segment and tangent to the line between `a` and `b`. - /// - /// With another `.line_to(b)`, the result will be a path connecting the - /// starting point and `b` with straight line segments towards `a` and a - /// circular arc smoothing out the corner at `a`. - /// - /// See [the HTML5 specification of `arcTo`](https://html.spec.whatwg.org/multipage/canvas.html#building-paths:dom-context-2d-arcto) - /// for more details and examples. - pub fn arc_to(&mut self, a: Point, b: Point, radius: f32) { - let start = self.raw.current_position(); - let mid = math::Point::new(a.x, a.y); - let end = math::Point::new(b.x, b.y); - - if start == mid || mid == end || radius == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let double_area = start.x * (mid.y - end.y) - + mid.x * (end.y - start.y) - + end.x * (start.y - mid.y); - - if double_area == 0.0 { - let _ = self.raw.line_to(mid); - return; - } - - let to_start = (start - mid).normalize(); - let to_end = (end - mid).normalize(); - - let inner_angle = to_start.dot(to_end).acos(); - - let origin_angle = inner_angle / 2.0; - - let origin_adjacent = radius / origin_angle.tan(); - - let arc_start = mid + to_start * origin_adjacent; - let arc_end = mid + to_end * origin_adjacent; - - let sweep = to_start.cross(to_end) < 0.0; - - let _ = self.raw.line_to(arc_start); - - self.raw.arc_to( - math::Vector::new(radius, radius), - math::Angle::radians(0.0), - lyon_path::ArcFlags { - large_arc: false, - sweep, - }, - arc_end, - ); - } - - /// Adds an ellipse to the [`Path`] using a clockwise direction. - pub fn ellipse(&mut self, arc: arc::Elliptical) { - let arc = geom::Arc { - center: math::Point::new(arc.center.x, arc.center.y), - radii: math::Vector::new(arc.radii.x, arc.radii.y), - x_rotation: math::Angle::radians(arc.rotation), - start_angle: math::Angle::radians(arc.start_angle), - sweep_angle: math::Angle::radians(arc.end_angle - arc.start_angle), - }; - - let _ = self.raw.move_to(arc.sample(0.0)); - - arc.for_each_quadratic_bezier(&mut |curve| { - let _ = self.raw.quadratic_bezier_to(curve.ctrl, curve.to); - }); - } - - /// Adds a cubic Bézier curve to the [`Path`] given its two control points - /// and its end point. - #[inline] - pub fn bezier_curve_to( - &mut self, - control_a: Point, - control_b: Point, - to: Point, - ) { - let _ = self.raw.cubic_bezier_to( - math::Point::new(control_a.x, control_a.y), - math::Point::new(control_b.x, control_b.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a quadratic Bézier curve to the [`Path`] given its control point - /// and its end point. - #[inline] - pub fn quadratic_curve_to(&mut self, control: Point, to: Point) { - let _ = self.raw.quadratic_bezier_to( - math::Point::new(control.x, control.y), - math::Point::new(to.x, to.y), - ); - } - - /// Adds a rectangle to the [`Path`] given its top-left corner coordinate - /// and its `Size`. - #[inline] - pub fn rectangle(&mut self, top_left: Point, size: Size) { - self.move_to(top_left); - self.line_to(Point::new(top_left.x + size.width, top_left.y)); - self.line_to(Point::new( - top_left.x + size.width, - top_left.y + size.height, - )); - self.line_to(Point::new(top_left.x, top_left.y + size.height)); - self.close(); - } - - /// Adds a circle to the [`Path`] given its center coordinate and its - /// radius. - #[inline] - pub fn circle(&mut self, center: Point, radius: f32) { - self.arc(Arc { - center, - radius, - start_angle: 0.0, - end_angle: 2.0 * std::f32::consts::PI, - }); - } - - /// Closes the current sub-path in the [`Path`] with a straight line to - /// the starting point. - #[inline] - pub fn close(&mut self) { - self.raw.close() - } - - /// Builds the [`Path`] of this [`Builder`]. - #[inline] - pub fn build(self) -> Path { - Path { - raw: self.raw.build(), - } - } -} - -impl Default for Builder { - fn default() -> Self { - Self::new() - } -} diff --git a/native/src/widget/canvas/program.rs b/native/src/widget/canvas/program.rs deleted file mode 100644 index 17a5a137..00000000 --- a/native/src/widget/canvas/program.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::widget::canvas::event::{self, Event}; -use crate::widget::canvas::mouse; -use crate::widget::canvas::{Cursor, Renderer}; -use crate::Rectangle; - -/// The state and logic of a [`Canvas`]. -/// -/// A [`Program`] can mutate internal state and produce messages for an -/// application. -/// -/// [`Canvas`]: crate::widget::Canvas -pub trait Program -where - Renderer: self::Renderer, -{ - /// The internal state mutated by the [`Program`]. - type State: Default + 'static; - - /// Updates the [`State`](Self::State) of the [`Program`]. - /// - /// When a [`Program`] is used in a [`Canvas`], the runtime will call this - /// method for each [`Event`]. - /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. - /// - /// By default, this method does and returns nothing. - /// - /// [`Canvas`]: crate::widget::Canvas - fn update( - &self, - _state: &mut Self::State, - _event: Event, - _bounds: Rectangle, - _cursor: Cursor, - ) -> (event::Status, Option) { - (event::Status::Ignored, None) - } - - /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. - /// - /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a - /// [`Cache`]. - /// - /// [`Frame`]: crate::widget::canvas::Frame - /// [`Cache`]: crate::widget::canvas::Cache - fn draw( - &self, - state: &Self::State, - renderer: &Renderer, - theme: &Renderer::Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec; - - /// Returns the current mouse interaction of the [`Program`]. - /// - /// The interaction returned will be in effect even if the cursor position - /// is out of bounds of the program's [`Canvas`]. - /// - /// [`Canvas`]: crate::widget::Canvas - fn mouse_interaction( - &self, - _state: &Self::State, - _bounds: Rectangle, - _cursor: Cursor, - ) -> mouse::Interaction { - mouse::Interaction::default() - } -} - -impl Program for &T -where - Renderer: self::Renderer, - T: Program, -{ - type State = T::State; - - fn update( - &self, - state: &mut Self::State, - event: Event, - bounds: Rectangle, - cursor: Cursor, - ) -> (event::Status, Option) { - T::update(self, state, event, bounds, cursor) - } - - fn draw( - &self, - state: &Self::State, - renderer: &Renderer, - theme: &Renderer::Theme, - bounds: Rectangle, - cursor: Cursor, - ) -> Vec { - T::draw(self, state, renderer, theme, bounds, cursor) - } - - fn mouse_interaction( - &self, - state: &Self::State, - bounds: Rectangle, - cursor: Cursor, - ) -> mouse::Interaction { - T::mouse_interaction(self, state, bounds, cursor) - } -} diff --git a/native/src/widget/canvas/stroke.rs b/native/src/widget/canvas/stroke.rs deleted file mode 100644 index ab4727b2..00000000 --- a/native/src/widget/canvas/stroke.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. -pub use crate::widget::canvas::Style; - -use crate::Color; - -/// The style of a stroke. -#[derive(Debug, Clone)] -pub struct Stroke<'a> { - /// The color or gradient of the stroke. - /// - /// By default, it is set to a [`Style::Solid`] with [`Color::BLACK`]. - pub style: Style, - /// The distance between the two edges of the stroke. - pub width: f32, - /// The shape to be used at the end of open subpaths when they are stroked. - pub line_cap: LineCap, - /// The shape to be used at the corners of paths or basic shapes when they - /// are stroked. - pub line_join: LineJoin, - /// The dash pattern used when stroking the line. - pub line_dash: LineDash<'a>, -} - -impl<'a> Stroke<'a> { - /// Sets the color of the [`Stroke`]. - pub fn with_color(self, color: Color) -> Self { - Stroke { - style: Style::Solid(color), - ..self - } - } - - /// Sets the width of the [`Stroke`]. - pub fn with_width(self, width: f32) -> Self { - Stroke { width, ..self } - } - - /// Sets the [`LineCap`] of the [`Stroke`]. - pub fn with_line_cap(self, line_cap: LineCap) -> Self { - Stroke { line_cap, ..self } - } - - /// Sets the [`LineJoin`] of the [`Stroke`]. - pub fn with_line_join(self, line_join: LineJoin) -> Self { - Stroke { line_join, ..self } - } -} - -impl<'a> Default for Stroke<'a> { - fn default() -> Self { - Stroke { - style: Style::Solid(Color::BLACK), - width: 1.0, - line_cap: LineCap::default(), - line_join: LineJoin::default(), - line_dash: LineDash::default(), - } - } -} - -/// The shape used at the end of open subpaths when they are stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineCap { - /// The stroke for each sub-path does not extend beyond its two endpoints. - Butt, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a square. - Square, - /// At the end of each sub-path, the shape representing the stroke will be - /// extended by a semicircle. - Round, -} - -impl Default for LineCap { - fn default() -> LineCap { - LineCap::Butt - } -} - -/// The shape used at the corners of paths or basic shapes when they are -/// stroked. -#[derive(Debug, Clone, Copy)] -pub enum LineJoin { - /// A sharp corner. - Miter, - /// A round corner. - Round, - /// A bevelled corner. - Bevel, -} - -impl Default for LineJoin { - fn default() -> LineJoin { - LineJoin::Miter - } -} - -/// The dash pattern used when stroking the line. -#[derive(Debug, Clone, Copy, Default)] -pub struct LineDash<'a> { - /// The alternating lengths of lines and gaps which describe the pattern. - pub segments: &'a [f32], - - /// The offset of [`LineDash::segments`] to start the pattern. - pub offset: usize, -} diff --git a/native/src/widget/canvas/style.rs b/native/src/widget/canvas/style.rs deleted file mode 100644 index 2642fdb8..00000000 --- a/native/src/widget/canvas/style.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::widget::canvas::Gradient; -use crate::Color; - -/// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] -pub enum Style { - /// A solid [`Color`]. - Solid(Color), - - /// A [`Gradient`] color. - Gradient(Gradient), -} - -impl From for Style { - fn from(color: Color) -> Self { - Self::Solid(color) - } -} - -impl From for Style { - fn from(gradient: Gradient) -> Self { - Self::Gradient(gradient) - } -} diff --git a/native/src/widget/canvas/text.rs b/native/src/widget/canvas/text.rs deleted file mode 100644 index 8c0b2dfb..00000000 --- a/native/src/widget/canvas/text.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::alignment; -use crate::{Color, Font, Point}; - -/// A bunch of text that can be drawn to a canvas -#[derive(Debug, Clone)] -pub struct Text { - /// The contents of the text - pub content: String, - /// The position of the text relative to the alignment properties. - /// By default, this position will be relative to the top-left corner coordinate meaning that - /// if the horizontal and vertical alignments are unchanged, this property will tell where the - /// top-left corner of the text should be placed. - /// By changing the horizontal_alignment and vertical_alignment properties, you are are able to - /// change what part of text is placed at this positions. - /// For example, when the horizontal_alignment and vertical_alignment are set to Center, the - /// center of the text will be placed at the given position NOT the top-left coordinate. - pub position: Point, - /// The color of the text - pub color: Color, - /// The size of the text - pub size: f32, - /// 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, -} - -impl Default for Text { - fn default() -> Text { - Text { - content: String::new(), - position: Point::ORIGIN, - color: Color::BLACK, - size: 16.0, - font: Font::SansSerif, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - } - } -} - -impl From for Text { - fn from(content: String) -> Text { - Text { - content, - ..Default::default() - } - } -} - -impl From<&str> for Text { - fn from(content: &str) -> Text { - String::from(content).into() - } -} diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 429b55c2..189f5309 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -6,8 +6,7 @@ edition = "2021" [features] image = ["iced_wgpu/image", "iced_tiny_skia/image"] svg = ["iced_wgpu/svg", "iced_tiny_skia/svg"] -canvas = ["iced_wgpu/canvas", "iced_tiny_skia/canvas"] -qr_code = ["canvas", "qrcode"] +geometry = ["iced_wgpu/geometry", "iced_tiny_skia/geometry"] tracing = ["iced_wgpu/tracing"] [dependencies] @@ -31,8 +30,3 @@ iced_wgpu = { version = "0.9", path = "../wgpu" } [target.'cfg(target_arch = "wasm32")'.dependencies] iced_wgpu = { version = "0.9", path = "../wgpu", features = ["webgl"] } - -[dependencies.qrcode] -version = "0.12" -optional = true -default-features = false diff --git a/renderer/src/backend.rs b/renderer/src/backend.rs index 6c0b4e5c..b0a409dc 100644 --- a/renderer/src/backend.rs +++ b/renderer/src/backend.rs @@ -1,4 +1,4 @@ -use crate::{Font, Geometry, Point, Size}; +use crate::{Font, Point, Size}; use iced_graphics::backend; use iced_graphics::text; @@ -12,8 +12,6 @@ pub enum Backend { } impl iced_graphics::Backend for Backend { - type Geometry = Geometry; - fn trim_measurements(&mut self) { match self { Self::Wgpu(backend) => backend.trim_measurements(), diff --git a/renderer/src/geometry.rs b/renderer/src/geometry.rs new file mode 100644 index 00000000..e491ea73 --- /dev/null +++ b/renderer/src/geometry.rs @@ -0,0 +1,168 @@ +mod cache; + +pub use cache::Cache; + +pub use iced_graphics::geometry::*; + +use crate::{Backend, Point, Rectangle, Size, Vector}; + +pub enum Frame { + Wgpu(iced_wgpu::geometry::Frame), + TinySkia(iced_tiny_skia::geometry::Frame), +} + +macro_rules! delegate { + ($frame:expr, $name:ident => $body:expr) => { + match $frame { + Self::Wgpu($name) => $body, + Self::TinySkia($name) => $body, + } + }; +} + +impl Frame { + pub fn new(renderer: &crate::Renderer, size: Size) -> Self { + match renderer.backend() { + Backend::Wgpu(_) => { + Frame::Wgpu(iced_wgpu::geometry::Frame::new(size)) + } + Backend::TinySkia(_) => { + Frame::TinySkia(iced_tiny_skia::geometry::Frame::new(size)) + } + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + delegate!(self, frame => frame.width()) + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + delegate!(self, frame => frame.height()) + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + delegate!(self, frame => frame.size()) + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + delegate!(self, frame => frame.center()) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + delegate!(self, frame => frame.fill(path, fill)); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + delegate!(self, frame => frame.stroke(path, stroke)); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + delegate!(self, frame => frame.fill_text(text)); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + delegate!(self, frame => frame.push_transform()); + + f(self); + + delegate!(self, frame => frame.pop_transform()); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = match self { + Self::Wgpu(_) => { + Self::Wgpu(iced_wgpu::geometry::Frame::new(region.size())) + } + Self::TinySkia(_) => Self::TinySkia( + iced_tiny_skia::geometry::Frame::new(region.size()), + ), + }; + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + match (self, frame) { + (Self::Wgpu(target), Self::Wgpu(frame)) => { + target.clip(frame, translation); + } + (Self::TinySkia(target), Self::TinySkia(frame)) => { + target.clip(frame, translation); + } + _ => unreachable!(), + }; + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + delegate!(self, frame => frame.translate(translation)); + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + delegate!(self, frame => frame.rotate(angle)); + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + delegate!(self, frame => frame.scale(scale)); + } + + pub fn into_geometry(self) -> Geometry { + Geometry(delegate!(self, frame => frame.into_primitive())) + } +} diff --git a/renderer/src/geometry/cache.rs b/renderer/src/geometry/cache.rs new file mode 100644 index 00000000..1f1febdd --- /dev/null +++ b/renderer/src/geometry/cache.rs @@ -0,0 +1,85 @@ +use crate::geometry::{Frame, Geometry}; +use crate::{Primitive, Renderer, Size}; + +use std::cell::RefCell; +use std::sync::Arc; + +/// A simple cache that stores generated [`Geometry`] to avoid recomputation. +/// +/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer +/// change or it is explicitly cleared. +#[derive(Debug, Default)] +pub struct Cache { + state: RefCell, +} + +#[derive(Debug, Default)] +enum State { + #[default] + Empty, + Filled { + bounds: Size, + primitive: Arc, + }, +} + +impl Cache { + /// Creates a new empty [`Cache`]. + pub fn new() -> Self { + Cache { + state: Default::default(), + } + } + + /// Clears the [`Cache`], forcing a redraw the next time it is used. + pub fn clear(&self) { + *self.state.borrow_mut() = State::Empty; + } + + /// Draws [`Geometry`] using the provided closure and stores it in the + /// [`Cache`]. + /// + /// The closure will only be called when + /// - the bounds have changed since the previous draw call. + /// - the [`Cache`] is empty or has been explicitly cleared. + /// + /// Otherwise, the previously stored [`Geometry`] will be returned. The + /// [`Cache`] is not cleared in this case. In other words, it will keep + /// returning the stored [`Geometry`] if needed. + pub fn draw( + &self, + renderer: &Renderer, + bounds: Size, + draw_fn: impl FnOnce(&mut Frame), + ) -> Geometry { + use std::ops::Deref; + + if let State::Filled { + bounds: cached_bounds, + primitive, + } = self.state.borrow().deref() + { + if *cached_bounds == bounds { + return Geometry(Primitive::Cache { + content: primitive.clone(), + }); + } + } + + let mut frame = Frame::new(renderer, bounds); + draw_fn(&mut frame); + + let primitive = { + let geometry = frame.into_geometry(); + + Arc::new(geometry.0) + }; + + *self.state.borrow_mut() = State::Filled { + bounds, + primitive: primitive.clone(), + }; + + Geometry(Primitive::Cache { content: primitive }) + } +} diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index d9c85e82..aae3322d 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -1,6 +1,8 @@ -pub mod widget; pub mod window; +#[cfg(feature = "geometry")] +pub mod geometry; + mod backend; mod settings; @@ -19,12 +21,3 @@ pub use iced_graphics::{ /// [`iced`]: https://github.com/iced-rs/iced pub type Renderer = iced_graphics::Renderer; - -#[derive(Debug, Clone)] -pub struct Geometry(pub(crate) Primitive); - -impl From for Primitive { - fn from(geometry: Geometry) -> Self { - geometry.0 - } -} diff --git a/renderer/src/widget/canvas.rs b/renderer/src/widget/canvas.rs deleted file mode 100644 index 35c8fff9..00000000 --- a/renderer/src/widget/canvas.rs +++ /dev/null @@ -1,177 +0,0 @@ -mod cache; - -pub use cache::Cache; - -pub use iced_native::widget::canvas::event::{self, Event}; -pub use iced_native::widget::canvas::fill::{self, Fill}; -pub use iced_native::widget::canvas::gradient::{self, Gradient}; -pub use iced_native::widget::canvas::path::{self, Path}; -pub use iced_native::widget::canvas::stroke::{self, Stroke}; -pub use iced_native::widget::canvas::{ - Canvas, Cursor, LineCap, LineDash, LineJoin, Program, Renderer, Style, Text, -}; - -use crate::{Backend, Point, Rectangle, Size, Vector}; - -pub use crate::Geometry; - -pub enum Frame { - Wgpu(iced_wgpu::canvas::Frame), - TinySkia(iced_tiny_skia::canvas::Frame), -} - -macro_rules! delegate { - ($frame:expr, $name:ident => $body:expr) => { - match $frame { - Self::Wgpu($name) => $body, - Self::TinySkia($name) => $body, - } - }; -} - -impl Frame { - pub fn new(renderer: &crate::Renderer, size: Size) -> Self { - match renderer.backend() { - Backend::Wgpu(_) => { - Frame::Wgpu(iced_wgpu::canvas::Frame::new(size)) - } - Backend::TinySkia(_) => { - Frame::TinySkia(iced_tiny_skia::canvas::Frame::new(size)) - } - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - delegate!(self, frame => frame.width()) - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - delegate!(self, frame => frame.height()) - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - delegate!(self, frame => frame.size()) - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - delegate!(self, frame => frame.center()) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - delegate!(self, frame => frame.fill(path, fill)); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - delegate!(self, frame => frame.fill_rectangle(top_left, size, fill)); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - delegate!(self, frame => frame.stroke(path, stroke)); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - delegate!(self, frame => frame.fill_text(text)); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - delegate!(self, frame => frame.push_transform()); - - f(self); - - delegate!(self, frame => frame.pop_transform()); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = match self { - Self::Wgpu(_) => { - Self::Wgpu(iced_wgpu::canvas::Frame::new(region.size())) - } - Self::TinySkia(_) => Self::TinySkia( - iced_tiny_skia::canvas::Frame::new(region.size()), - ), - }; - - f(&mut frame); - - let translation = Vector::new(region.x, region.y); - - match (self, frame) { - (Self::Wgpu(target), Self::Wgpu(frame)) => { - target.clip(frame, translation); - } - (Self::TinySkia(target), Self::TinySkia(frame)) => { - target.clip(frame, translation); - } - _ => unreachable!(), - }; - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - delegate!(self, frame => frame.translate(translation)); - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - delegate!(self, frame => frame.rotate(angle)); - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - delegate!(self, frame => frame.scale(scale)); - } - - pub fn into_geometry(self) -> Geometry { - Geometry(delegate!(self, frame => frame.into_primitive())) - } -} diff --git a/renderer/src/widget/canvas/cache.rs b/renderer/src/widget/canvas/cache.rs deleted file mode 100644 index 7d6b4811..00000000 --- a/renderer/src/widget/canvas/cache.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::widget::canvas::{Frame, Geometry}; -use crate::{Primitive, Renderer, Size}; - -use std::cell::RefCell; -use std::sync::Arc; - -/// A simple cache that stores generated [`Geometry`] to avoid recomputation. -/// -/// A [`Cache`] will not redraw its geometry unless the dimensions of its layer -/// change or it is explicitly cleared. -#[derive(Debug, Default)] -pub struct Cache { - state: RefCell, -} - -#[derive(Debug, Default)] -enum State { - #[default] - Empty, - Filled { - bounds: Size, - primitive: Arc, - }, -} - -impl Cache { - /// Creates a new empty [`Cache`]. - pub fn new() -> Self { - Cache { - state: Default::default(), - } - } - - /// Clears the [`Cache`], forcing a redraw the next time it is used. - pub fn clear(&self) { - *self.state.borrow_mut() = State::Empty; - } - - /// Draws [`Geometry`] using the provided closure and stores it in the - /// [`Cache`]. - /// - /// The closure will only be called when - /// - the bounds have changed since the previous draw call. - /// - the [`Cache`] is empty or has been explicitly cleared. - /// - /// Otherwise, the previously stored [`Geometry`] will be returned. The - /// [`Cache`] is not cleared in this case. In other words, it will keep - /// returning the stored [`Geometry`] if needed. - pub fn draw( - &self, - renderer: &Renderer, - bounds: Size, - draw_fn: impl FnOnce(&mut Frame), - ) -> Geometry { - use std::ops::Deref; - - if let State::Filled { - bounds: cached_bounds, - primitive, - } = self.state.borrow().deref() - { - if *cached_bounds == bounds { - return Geometry(Primitive::Cache { - content: primitive.clone(), - }); - } - } - - let mut frame = Frame::new(renderer, bounds); - draw_fn(&mut frame); - - let primitive = { - let geometry = frame.into_geometry(); - - Arc::new(geometry.0) - }; - - *self.state.borrow_mut() = State::Filled { - bounds, - primitive: primitive.clone(), - }; - - Geometry(Primitive::Cache { content: primitive }) - } -} diff --git a/renderer/src/widget/qr_code.rs b/renderer/src/widget/qr_code.rs deleted file mode 100644 index aae4ec88..00000000 --- a/renderer/src/widget/qr_code.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Encode and display information in a QR code. -use crate::widget::canvas; -use crate::Renderer; - -use iced_graphics::renderer; - -use iced_native::layout; -use iced_native::widget::Tree; -use iced_native::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, -}; -use thiserror::Error; - -const DEFAULT_CELL_SIZE: u16 = 4; -const QUIET_ZONE: usize = 2; - -/// A type of matrix barcode consisting of squares arranged in a grid which -/// can be read by an imaging device, such as a camera. -#[derive(Debug)] -pub struct QRCode<'a> { - state: &'a State, - dark: Color, - light: Color, - cell_size: u16, -} - -impl<'a> QRCode<'a> { - /// Creates a new [`QRCode`] with the provided [`State`]. - pub fn new(state: &'a State) -> Self { - Self { - cell_size: DEFAULT_CELL_SIZE, - dark: Color::BLACK, - light: Color::WHITE, - state, - } - } - - /// Sets both the dark and light [`Color`]s of the [`QRCode`]. - pub fn color(mut self, dark: Color, light: Color) -> Self { - self.dark = dark; - self.light = light; - self - } - - /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: u16) -> Self { - self.cell_size = cell_size; - self - } -} - -impl<'a, Message, Theme> Widget> for QRCode<'a> { - fn width(&self) -> Length { - Length::Shrink - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - _limits: &layout::Limits, - ) -> layout::Node { - let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 - * f32::from(self.cell_size); - - layout::Node::new(Size::new(side_length, side_length)) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - _theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - use iced_native::Renderer as _; - - let bounds = layout.bounds(); - let side_length = self.state.width + 2 * QUIET_ZONE; - - // Reuse cache if possible - let geometry = - self.state.cache.draw(renderer, bounds.size(), |frame| { - // Scale units to cell size - frame.scale(f32::from(self.cell_size)); - - // Draw background - frame.fill_rectangle( - Point::ORIGIN, - Size::new(side_length as f32, side_length as f32), - self.light, - ); - - // Avoid drawing on the quiet zone - frame.translate(Vector::new( - QUIET_ZONE as f32, - QUIET_ZONE as f32, - )); - - // Draw contents - self.state - .contents - .iter() - .enumerate() - .filter(|(_, value)| **value == qrcode::Color::Dark) - .for_each(|(index, _)| { - let row = index / self.state.width; - let column = index % self.state.width; - - frame.fill_rectangle( - Point::new(column as f32, row as f32), - Size::UNIT, - self.dark, - ); - }); - }); - - let translation = Vector::new(bounds.x, bounds.y); - - renderer.with_translation(translation, |renderer| { - renderer.draw_primitive(geometry.0); - }); - } -} - -impl<'a, Message, Theme> From> - for Element<'a, Message, Renderer> -{ - fn from(qr_code: QRCode<'a>) -> Self { - Self::new(qr_code) - } -} - -/// The state of a [`QRCode`]. -/// -/// It stores the data that will be displayed. -#[derive(Debug)] -pub struct State { - contents: Vec, - width: usize, - cache: canvas::Cache, -} - -impl State { - /// Creates a new [`State`] with the provided data. - /// - /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest - /// size to display the data. - pub fn new(data: impl AsRef<[u8]>) -> Result { - let encoded = qrcode::QrCode::new(data)?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`ErrorCorrection`]. - pub fn with_error_correction( - data: impl AsRef<[u8]>, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_error_correction_level( - data, - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - /// Creates a new [`State`] with the provided [`Version`] and - /// [`ErrorCorrection`]. - pub fn with_version( - data: impl AsRef<[u8]>, - version: Version, - error_correction: ErrorCorrection, - ) -> Result { - let encoded = qrcode::QrCode::with_version( - data, - version.into(), - error_correction.into(), - )?; - - Ok(Self::build(encoded)) - } - - fn build(encoded: qrcode::QrCode) -> Self { - let width = encoded.width(); - let contents = encoded.into_colors(); - - Self { - contents, - width, - cache: canvas::Cache::new(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// The size of a [`QRCode`]. -/// -/// The higher the version the larger the grid of cells, and therefore the more -/// information the [`QRCode`] can carry. -pub enum Version { - /// A normal QR code version. It should be between 1 and 40. - Normal(u8), - - /// A micro QR code version. It should be between 1 and 4. - Micro(u8), -} - -impl From for qrcode::Version { - fn from(version: Version) -> Self { - match version { - Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), - Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), - } - } -} - -/// The error correction level. -/// -/// It controls the amount of data that can be damaged while still being able -/// to recover the original information. -/// -/// A higher error correction level allows for more corrupted data. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ErrorCorrection { - /// Low error correction. 7% of the data can be restored. - Low, - /// Medium error correction. 15% of the data can be restored. - Medium, - /// Quartile error correction. 25% of the data can be restored. - Quartile, - /// High error correction. 30% of the data can be restored. - High, -} - -impl From for qrcode::EcLevel { - fn from(ec_level: ErrorCorrection) -> Self { - match ec_level { - ErrorCorrection::Low => qrcode::EcLevel::L, - ErrorCorrection::Medium => qrcode::EcLevel::M, - ErrorCorrection::Quartile => qrcode::EcLevel::Q, - ErrorCorrection::High => qrcode::EcLevel::H, - } - } -} - -/// An error that occurred when building a [`State`] for a [`QRCode`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] -pub enum Error { - /// The data is too long to encode in a QR code for the chosen [`Version`]. - #[error( - "The data is too long to encode in a QR code for the chosen version" - )] - DataTooLong, - - /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. - #[error( - "The chosen version and error correction level combination is invalid." - )] - InvalidVersion, - - /// One or more characters in the provided data are not supported by the - /// chosen [`Version`]. - #[error( - "One or more characters in the provided data are not supported by the \ - chosen version" - )] - UnsupportedCharacterSet, - - /// The chosen ECI designator is invalid. A valid designator should be - /// between 0 and 999999. - #[error( - "The chosen ECI designator is invalid. A valid designator should be \ - between 0 and 999999." - )] - InvalidEciDesignator, - - /// A character that does not belong to the character set was found. - #[error("A character that does not belong to the character set was found")] - InvalidCharacter, -} - -impl From for Error { - fn from(error: qrcode::types::QrError) -> Self { - use qrcode::types::QrError; - - match error { - QrError::DataTooLong => Error::DataTooLong, - QrError::InvalidVersion => Error::InvalidVersion, - QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, - QrError::InvalidEciDesignator => Error::InvalidEciDesignator, - QrError::InvalidCharacter => Error::InvalidCharacter, - } - } -} diff --git a/src/widget.rs b/src/widget.rs index f3a66101..19434e84 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -165,14 +165,14 @@ pub use vertical_slider::VerticalSlider; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub use iced_renderer::widget::canvas; +pub mod canvas; #[cfg(feature = "canvas")] #[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] /// Creates a new [`Canvas`]. -pub fn canvas(program: P) -> Canvas +pub fn canvas(program: P) -> Canvas where - Renderer: canvas::Renderer, + Renderer: iced_renderer::geometry::Renderer, P: canvas::Program, { Canvas::new(program) @@ -193,7 +193,7 @@ pub mod image { #[cfg(feature = "qr_code")] #[cfg_attr(docsrs, doc(cfg(feature = "qr_code")))] -pub use iced_renderer::widget::qr_code; +pub mod qr_code; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs new file mode 100644 index 00000000..bc5995c6 --- /dev/null +++ b/src/widget/canvas.rs @@ -0,0 +1,238 @@ +//! Draw 2D graphics for your users. +pub mod event; + +mod cursor; +mod program; + +pub use cursor::Cursor; +pub use event::Event; +pub use program::Program; + +pub use iced_renderer::geometry::*; + +use crate::{Length, Point, Rectangle, Size, Vector}; + +use iced_native::layout::{self, Layout}; +use iced_native::mouse; +use iced_native::renderer; +use iced_native::widget::tree::{self, Tree}; +use iced_native::{Clipboard, Element, Shell, Widget}; + +use std::marker::PhantomData; + +/// A widget capable of drawing 2D graphics. +/// +/// ## Drawing a simple circle +/// If you want to get a quick overview, here's how we can draw a simple circle: +/// +/// ```no_run +/// use iced::widget::canvas::{self, Canvas, Cursor, Fill, Frame, Geometry, Path, Program}; +/// use iced::{Color, Rectangle, Theme, Renderer}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl Program<()> for Circle { +/// type State = (); +/// +/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: Cursor) -> Vec{ +/// // We prepare a new `Frame` +/// let mut frame = Frame::new(renderer, bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Finally, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// ``` +#[derive(Debug)] +pub struct Canvas +where + Renderer: iced_renderer::geometry::Renderer, + P: Program, +{ + width: Length, + height: Length, + program: P, + message_: PhantomData, + theme_: PhantomData, +} + +impl Canvas +where + Renderer: iced_renderer::geometry::Renderer, + P: Program, +{ + const DEFAULT_SIZE: f32 = 100.0; + + /// Creates a new [`Canvas`]. + pub fn new(program: P) -> Self { + Canvas { + width: Length::Fixed(Self::DEFAULT_SIZE), + height: Length::Fixed(Self::DEFAULT_SIZE), + program, + message_: PhantomData, + theme_: PhantomData, + } + } + + /// Sets the width of the [`Canvas`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Canvas`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } +} + +impl Widget + for Canvas +where + Renderer: iced_renderer::geometry::Renderer, + P: Program, +{ + fn tag(&self) -> tree::Tag { + struct Tag(T); + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(P::State::default()) + } + + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + let size = limits.resolve(Size::ZERO); + + layout::Node::new(size) + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: iced_native::Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let bounds = layout.bounds(); + + let canvas_event = match event { + iced_native::Event::Mouse(mouse_event) => { + Some(Event::Mouse(mouse_event)) + } + iced_native::Event::Touch(touch_event) => { + Some(Event::Touch(touch_event)) + } + iced_native::Event::Keyboard(keyboard_event) => { + Some(Event::Keyboard(keyboard_event)) + } + _ => None, + }; + + let cursor = Cursor::from_window_position(cursor_position); + + if let Some(canvas_event) = canvas_event { + let state = tree.state.downcast_mut::(); + + let (event_status, message) = + self.program.update(state, canvas_event, bounds, cursor); + + if let Some(message) = message { + shell.publish(message); + } + + return event_status; + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + let bounds = layout.bounds(); + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + self.program.mouse_interaction(state, bounds, cursor) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if bounds.width < 1.0 || bounds.height < 1.0 { + return; + } + + let cursor = Cursor::from_window_position(cursor_position); + let state = tree.state.downcast_ref::(); + + renderer.with_translation( + Vector::new(bounds.x, bounds.y), + |renderer| { + renderer.draw( + self.program.draw(state, renderer, theme, bounds, cursor), + ); + }, + ); + } +} + +impl<'a, P, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Message: 'a, + Renderer: 'a + iced_renderer::geometry::Renderer, + P: Program + 'a, +{ + fn from( + canvas: Canvas, + ) -> Element<'a, Message, Renderer> { + Element::new(canvas) + } +} diff --git a/src/widget/canvas/cursor.rs b/src/widget/canvas/cursor.rs new file mode 100644 index 00000000..ef6a7771 --- /dev/null +++ b/src/widget/canvas/cursor.rs @@ -0,0 +1,64 @@ +use crate::{Point, Rectangle}; + +/// The mouse cursor state. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Cursor { + /// The cursor has a defined position. + Available(Point), + + /// The cursor is currently unavailable (i.e. out of bounds or busy). + Unavailable, +} + +impl Cursor { + // TODO: Remove this once this type is used in `iced_native` to encode + // proper cursor availability + pub(crate) fn from_window_position(position: Point) -> Self { + if position.x < 0.0 || position.y < 0.0 { + Cursor::Unavailable + } else { + Cursor::Available(position) + } + } + + /// Returns the absolute position of the [`Cursor`], if available. + pub fn position(&self) -> Option { + match self { + Cursor::Available(position) => Some(*position), + Cursor::Unavailable => None, + } + } + + /// Returns the relative position of the [`Cursor`] inside the given bounds, + /// if available. + /// + /// If the [`Cursor`] is not over the provided bounds, this method will + /// return `None`. + pub fn position_in(&self, bounds: &Rectangle) -> Option { + if self.is_over(bounds) { + self.position_from(bounds.position()) + } else { + None + } + } + + /// Returns the relative position of the [`Cursor`] from the given origin, + /// if available. + pub fn position_from(&self, origin: Point) -> Option { + match self { + Cursor::Available(position) => { + Some(Point::new(position.x - origin.x, position.y - origin.y)) + } + Cursor::Unavailable => None, + } + } + + /// Returns whether the [`Cursor`] is currently over the provided bounds + /// or not. + pub fn is_over(&self, bounds: &Rectangle) -> bool { + match self { + Cursor::Available(position) => bounds.contains(*position), + Cursor::Unavailable => false, + } + } +} diff --git a/src/widget/canvas/event.rs b/src/widget/canvas/event.rs new file mode 100644 index 00000000..7c733a4d --- /dev/null +++ b/src/widget/canvas/event.rs @@ -0,0 +1,21 @@ +//! Handle events of a canvas. +use iced_native::keyboard; +use iced_native::mouse; +use iced_native::touch; + +pub use iced_native::event::Status; + +/// A [`Canvas`] event. +/// +/// [`Canvas`]: crate::widget::Canvas +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Event { + /// A mouse event. + Mouse(mouse::Event), + + /// A touch event. + Touch(touch::Event), + + /// A keyboard event. + Keyboard(keyboard::Event), +} diff --git a/src/widget/canvas/program.rs b/src/widget/canvas/program.rs new file mode 100644 index 00000000..fd15663a --- /dev/null +++ b/src/widget/canvas/program.rs @@ -0,0 +1,108 @@ +use crate::widget::canvas::event::{self, Event}; +use crate::widget::canvas::mouse; +use crate::widget::canvas::Cursor; +use crate::Rectangle; + +/// The state and logic of a [`Canvas`]. +/// +/// A [`Program`] can mutate internal state and produce messages for an +/// application. +/// +/// [`Canvas`]: crate::widget::Canvas +pub trait Program +where + Renderer: iced_renderer::geometry::Renderer, +{ + /// The internal state mutated by the [`Program`]. + type State: Default + 'static; + + /// Updates the [`State`](Self::State) of the [`Program`]. + /// + /// When a [`Program`] is used in a [`Canvas`], the runtime will call this + /// method for each [`Event`]. + /// + /// This method can optionally return a `Message` to notify an application + /// of any meaningful interactions. + /// + /// By default, this method does and returns nothing. + /// + /// [`Canvas`]: crate::widget::Canvas + fn update( + &self, + _state: &mut Self::State, + _event: Event, + _bounds: Rectangle, + _cursor: Cursor, + ) -> (event::Status, Option) { + (event::Status::Ignored, None) + } + + /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. + /// + /// [`Geometry`] can be easily generated with a [`Frame`] or stored in a + /// [`Cache`]. + /// + /// [`Frame`]: crate::widget::canvas::Frame + /// [`Cache`]: crate::widget::canvas::Cache + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec; + + /// Returns the current mouse interaction of the [`Program`]. + /// + /// The interaction returned will be in effect even if the cursor position + /// is out of bounds of the program's [`Canvas`]. + /// + /// [`Canvas`]: crate::widget::Canvas + fn mouse_interaction( + &self, + _state: &Self::State, + _bounds: Rectangle, + _cursor: Cursor, + ) -> mouse::Interaction { + mouse::Interaction::default() + } +} + +impl Program for &T +where + Renderer: iced_renderer::geometry::Renderer, + T: Program, +{ + type State = T::State; + + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (event::Status, Option) { + T::update(self, state, event, bounds, cursor) + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Renderer::Theme, + bounds: Rectangle, + cursor: Cursor, + ) -> Vec { + T::draw(self, state, renderer, theme, bounds, cursor) + } + + fn mouse_interaction( + &self, + state: &Self::State, + bounds: Rectangle, + cursor: Cursor, + ) -> mouse::Interaction { + T::mouse_interaction(self, state, bounds, cursor) + } +} diff --git a/src/widget/qr_code.rs b/src/widget/qr_code.rs new file mode 100644 index 00000000..66442d5d --- /dev/null +++ b/src/widget/qr_code.rs @@ -0,0 +1,300 @@ +//! Encode and display information in a QR code. +use crate::widget::canvas; +use crate::Renderer; + +use iced_native::layout; +use iced_native::renderer; +use iced_native::widget::Tree; +use iced_native::{ + Color, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, +}; +use thiserror::Error; + +const DEFAULT_CELL_SIZE: u16 = 4; +const QUIET_ZONE: usize = 2; + +/// A type of matrix barcode consisting of squares arranged in a grid which +/// can be read by an imaging device, such as a camera. +#[derive(Debug)] +pub struct QRCode<'a> { + state: &'a State, + dark: Color, + light: Color, + cell_size: u16, +} + +impl<'a> QRCode<'a> { + /// Creates a new [`QRCode`] with the provided [`State`]. + pub fn new(state: &'a State) -> Self { + Self { + cell_size: DEFAULT_CELL_SIZE, + dark: Color::BLACK, + light: Color::WHITE, + state, + } + } + + /// Sets both the dark and light [`Color`]s of the [`QRCode`]. + pub fn color(mut self, dark: Color, light: Color) -> Self { + self.dark = dark; + self.light = light; + self + } + + /// Sets the size of the squares of the grid cell of the [`QRCode`]. + pub fn cell_size(mut self, cell_size: u16) -> Self { + self.cell_size = cell_size; + self + } +} + +impl<'a, Message, Theme> Widget> for QRCode<'a> { + fn width(&self) -> Length { + Length::Shrink + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + _renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + let side_length = (self.state.width + 2 * QUIET_ZONE) as f32 + * f32::from(self.cell_size); + + layout::Node::new(Size::new(side_length, side_length)) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + _theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + use iced_native::Renderer as _; + + let bounds = layout.bounds(); + let side_length = self.state.width + 2 * QUIET_ZONE; + + // Reuse cache if possible + let geometry = + self.state.cache.draw(renderer, bounds.size(), |frame| { + // Scale units to cell size + frame.scale(f32::from(self.cell_size)); + + // Draw background + frame.fill_rectangle( + Point::ORIGIN, + Size::new(side_length as f32, side_length as f32), + self.light, + ); + + // Avoid drawing on the quiet zone + frame.translate(Vector::new( + QUIET_ZONE as f32, + QUIET_ZONE as f32, + )); + + // Draw contents + self.state + .contents + .iter() + .enumerate() + .filter(|(_, value)| **value == qrcode::Color::Dark) + .for_each(|(index, _)| { + let row = index / self.state.width; + let column = index % self.state.width; + + frame.fill_rectangle( + Point::new(column as f32, row as f32), + Size::UNIT, + self.dark, + ); + }); + }); + + let translation = Vector::new(bounds.x, bounds.y); + + renderer.with_translation(translation, |renderer| { + renderer.draw_primitive(geometry.0); + }); + } +} + +impl<'a, Message, Theme> From> + for Element<'a, Message, Renderer> +{ + fn from(qr_code: QRCode<'a>) -> Self { + Self::new(qr_code) + } +} + +/// The state of a [`QRCode`]. +/// +/// It stores the data that will be displayed. +#[derive(Debug)] +pub struct State { + contents: Vec, + width: usize, + cache: canvas::Cache, +} + +impl State { + /// Creates a new [`State`] with the provided data. + /// + /// This method uses an [`ErrorCorrection::Medium`] and chooses the smallest + /// size to display the data. + pub fn new(data: impl AsRef<[u8]>) -> Result { + let encoded = qrcode::QrCode::new(data)?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`ErrorCorrection`]. + pub fn with_error_correction( + data: impl AsRef<[u8]>, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_error_correction_level( + data, + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + /// Creates a new [`State`] with the provided [`Version`] and + /// [`ErrorCorrection`]. + pub fn with_version( + data: impl AsRef<[u8]>, + version: Version, + error_correction: ErrorCorrection, + ) -> Result { + let encoded = qrcode::QrCode::with_version( + data, + version.into(), + error_correction.into(), + )?; + + Ok(Self::build(encoded)) + } + + fn build(encoded: qrcode::QrCode) -> Self { + let width = encoded.width(); + let contents = encoded.into_colors(); + + Self { + contents, + width, + cache: canvas::Cache::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The size of a [`QRCode`]. +/// +/// The higher the version the larger the grid of cells, and therefore the more +/// information the [`QRCode`] can carry. +pub enum Version { + /// A normal QR code version. It should be between 1 and 40. + Normal(u8), + + /// A micro QR code version. It should be between 1 and 4. + Micro(u8), +} + +impl From for qrcode::Version { + fn from(version: Version) -> Self { + match version { + Version::Normal(v) => qrcode::Version::Normal(i16::from(v)), + Version::Micro(v) => qrcode::Version::Micro(i16::from(v)), + } + } +} + +/// The error correction level. +/// +/// It controls the amount of data that can be damaged while still being able +/// to recover the original information. +/// +/// A higher error correction level allows for more corrupted data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCorrection { + /// Low error correction. 7% of the data can be restored. + Low, + /// Medium error correction. 15% of the data can be restored. + Medium, + /// Quartile error correction. 25% of the data can be restored. + Quartile, + /// High error correction. 30% of the data can be restored. + High, +} + +impl From for qrcode::EcLevel { + fn from(ec_level: ErrorCorrection) -> Self { + match ec_level { + ErrorCorrection::Low => qrcode::EcLevel::L, + ErrorCorrection::Medium => qrcode::EcLevel::M, + ErrorCorrection::Quartile => qrcode::EcLevel::Q, + ErrorCorrection::High => qrcode::EcLevel::H, + } + } +} + +/// An error that occurred when building a [`State`] for a [`QRCode`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum Error { + /// The data is too long to encode in a QR code for the chosen [`Version`]. + #[error( + "The data is too long to encode in a QR code for the chosen version" + )] + DataTooLong, + + /// The chosen [`Version`] and [`ErrorCorrection`] combination is invalid. + #[error( + "The chosen version and error correction level combination is invalid." + )] + InvalidVersion, + + /// One or more characters in the provided data are not supported by the + /// chosen [`Version`]. + #[error( + "One or more characters in the provided data are not supported by the \ + chosen version" + )] + UnsupportedCharacterSet, + + /// The chosen ECI designator is invalid. A valid designator should be + /// between 0 and 999999. + #[error( + "The chosen ECI designator is invalid. A valid designator should be \ + between 0 and 999999." + )] + InvalidEciDesignator, + + /// A character that does not belong to the character set was found. + #[error("A character that does not belong to the character set was found")] + InvalidCharacter, +} + +impl From for Error { + fn from(error: qrcode::types::QrError) -> Self { + use qrcode::types::QrError; + + match error { + QrError::DataTooLong => Error::DataTooLong, + QrError::InvalidVersion => Error::InvalidVersion, + QrError::UnsupportedCharacterSet => Error::UnsupportedCharacterSet, + QrError::InvalidEciDesignator => Error::InvalidEciDesignator, + QrError::InvalidCharacter => Error::InvalidCharacter, + } + } +} diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 72181735..c4c36aba 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [features] image = [] svg = [] -canvas = ["iced_native/canvas"] +geometry = ["iced_graphics/geometry"] [dependencies] raw-window-handle = "0.5" diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 6883a953..050c6c75 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -436,8 +436,6 @@ fn adjust_clip_mask( } impl iced_graphics::Backend for Backend { - type Geometry = (); - fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache(); } diff --git a/tiny_skia/src/canvas.rs b/tiny_skia/src/canvas.rs deleted file mode 100644 index 958063d2..00000000 --- a/tiny_skia/src/canvas.rs +++ /dev/null @@ -1,287 +0,0 @@ -use crate::{Point, Primitive, Rectangle, Size, Vector}; - -use iced_native::widget::canvas::fill::{self, Fill}; -use iced_native::widget::canvas::stroke::{self, Stroke}; -use iced_native::widget::canvas::{Path, Style, Text}; -use iced_native::Gradient; - -pub struct Frame { - size: Size, - transform: tiny_skia::Transform, - stack: Vec, - primitives: Vec, -} - -impl Frame { - pub fn new(size: Size) -> Self { - Self { - size, - transform: tiny_skia::Transform::identity(), - stack: Vec::new(), - primitives: Vec::new(), - } - } - - pub fn width(&self) -> f32 { - self.size.width - } - - pub fn height(&self) -> f32 { - self.size.height - } - - pub fn size(&self) -> Size { - self.size - } - - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let path = convert_path(path); - let fill = fill.into(); - - self.primitives.push(Primitive::Fill { - path, - paint: into_paint(fill.style), - rule: into_fill_rule(fill.rule), - transform: self.transform, - }); - } - - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let path = convert_path(&Path::rectangle(top_left, size)); - let fill = fill.into(); - - self.primitives.push(Primitive::Fill { - path, - paint: tiny_skia::Paint { - anti_alias: false, - ..into_paint(fill.style) - }, - rule: into_fill_rule(fill.rule), - transform: self.transform, - }); - } - - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let path = convert_path(path); - let stroke = stroke.into(); - let skia_stroke = into_stroke(&stroke); - - self.primitives.push(Primitive::Stroke { - path, - paint: into_paint(stroke.style), - stroke: skia_stroke, - transform: self.transform, - }); - } - - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transform.is_identity() { - text.position - } else { - let mut transformed = [tiny_skia::Point { - x: text.position.x, - y: text.position.y, - }]; - - self.transform.map_points(&mut transformed); - - Point::new(transformed[0].x, transformed[0].y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - pub fn push_transform(&mut self) { - self.stack.push(self.transform); - } - - pub fn pop_transform(&mut self) { - self.transform = self.stack.pop().expect("Pop transform"); - } - - pub fn clip(&mut self, _frame: Self, _translation: Vector) {} - - pub fn translate(&mut self, translation: Vector) { - self.transform = - self.transform.pre_translate(translation.x, translation.y); - } - - pub fn rotate(&mut self, angle: f32) { - self.transform = self - .transform - .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); - } - - pub fn scale(&mut self, scale: f32) { - self.transform = self.transform.pre_scale(scale, scale); - } - - pub fn into_primitive(self) -> Primitive { - Primitive::Clip { - bounds: Rectangle::new(Point::ORIGIN, self.size), - content: Box::new(Primitive::Group { - primitives: self.primitives, - }), - } - } -} - -fn convert_path(path: &Path) -> tiny_skia::Path { - use iced_native::widget::canvas::path::lyon_path; - - let mut builder = tiny_skia::PathBuilder::new(); - let mut last_point = Default::default(); - - for event in path.raw().iter() { - match event { - lyon_path::Event::Begin { at } => { - builder.move_to(at.x, at.y); - - last_point = at; - } - lyon_path::Event::Line { from, to } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.line_to(to.x, to.y); - - last_point = to; - } - lyon_path::Event::Quadratic { from, ctrl, to } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); - - last_point = to; - } - lyon_path::Event::Cubic { - from, - ctrl1, - ctrl2, - to, - } => { - if last_point != from { - builder.move_to(from.x, from.y); - } - - builder - .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); - - last_point = to; - } - lyon_path::Event::End { close, .. } => { - if close { - builder.close(); - } - } - } - } - - builder - .finish() - .expect("Convert lyon path to tiny_skia path") -} - -pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { - tiny_skia::Paint { - shader: match style { - Style::Solid(color) => tiny_skia::Shader::SolidColor( - tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) - .expect("Create color"), - ), - Style::Gradient(gradient) => match gradient { - Gradient::Linear(linear) => tiny_skia::LinearGradient::new( - tiny_skia::Point { - x: linear.start.x, - y: linear.start.y, - }, - tiny_skia::Point { - x: linear.end.x, - y: linear.end.y, - }, - linear - .color_stops - .into_iter() - .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::SpreadMode::Pad, - tiny_skia::Transform::identity(), - ) - .expect("Create linear gradient"), - }, - }, - anti_alias: true, - ..Default::default() - } -} - -pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { - match rule { - fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, - fill::Rule::NonZero => tiny_skia::FillRule::Winding, - } -} - -pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke { - tiny_skia::Stroke { - width: stroke.width, - line_cap: match stroke.line_cap { - stroke::LineCap::Butt => tiny_skia::LineCap::Butt, - stroke::LineCap::Square => tiny_skia::LineCap::Square, - stroke::LineCap::Round => tiny_skia::LineCap::Round, - }, - line_join: match stroke.line_join { - stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, - stroke::LineJoin::Round => tiny_skia::LineJoin::Round, - stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, - }, - dash: if stroke.line_dash.segments.is_empty() { - None - } else { - tiny_skia::StrokeDash::new( - stroke.line_dash.segments.into(), - stroke.line_dash.offset as f32, - ) - }, - ..Default::default() - } -} diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs new file mode 100644 index 00000000..73fc1ebd --- /dev/null +++ b/tiny_skia/src/geometry.rs @@ -0,0 +1,287 @@ +use crate::{Point, Primitive, Rectangle, Size, Vector}; + +use iced_graphics::geometry::fill::{self, Fill}; +use iced_graphics::geometry::stroke::{self, Stroke}; +use iced_graphics::geometry::{Path, Style, Text}; +use iced_graphics::Gradient; + +pub struct Frame { + size: Size, + transform: tiny_skia::Transform, + stack: Vec, + primitives: Vec, +} + +impl Frame { + pub fn new(size: Size) -> Self { + Self { + size, + transform: tiny_skia::Transform::identity(), + stack: Vec::new(), + primitives: Vec::new(), + } + } + + pub fn width(&self) -> f32 { + self.size.width + } + + pub fn height(&self) -> f32 { + self.size.height + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let path = convert_path(path); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: into_paint(fill.style), + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); + } + + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let path = convert_path(&Path::rectangle(top_left, size)); + let fill = fill.into(); + + self.primitives.push(Primitive::Fill { + path, + paint: tiny_skia::Paint { + anti_alias: false, + ..into_paint(fill.style) + }, + rule: into_fill_rule(fill.rule), + transform: self.transform, + }); + } + + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let path = convert_path(path); + let stroke = stroke.into(); + let skia_stroke = into_stroke(&stroke); + + self.primitives.push(Primitive::Stroke { + path, + paint: into_paint(stroke.style), + stroke: skia_stroke, + transform: self.transform, + }); + } + + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transform.is_identity() { + text.position + } else { + let mut transformed = [tiny_skia::Point { + x: text.position.x, + y: text.position.y, + }]; + + self.transform.map_points(&mut transformed); + + Point::new(transformed[0].x, transformed[0].y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + pub fn push_transform(&mut self) { + self.stack.push(self.transform); + } + + pub fn pop_transform(&mut self) { + self.transform = self.stack.pop().expect("Pop transform"); + } + + pub fn clip(&mut self, _frame: Self, _translation: Vector) {} + + pub fn translate(&mut self, translation: Vector) { + self.transform = + self.transform.pre_translate(translation.x, translation.y); + } + + pub fn rotate(&mut self, angle: f32) { + self.transform = self + .transform + .pre_concat(tiny_skia::Transform::from_rotate(angle.to_degrees())); + } + + pub fn scale(&mut self, scale: f32) { + self.transform = self.transform.pre_scale(scale, scale); + } + + pub fn into_primitive(self) -> Primitive { + Primitive::Clip { + bounds: Rectangle::new(Point::ORIGIN, self.size), + content: Box::new(Primitive::Group { + primitives: self.primitives, + }), + } + } +} + +fn convert_path(path: &Path) -> tiny_skia::Path { + use iced_graphics::geometry::path::lyon_path; + + let mut builder = tiny_skia::PathBuilder::new(); + let mut last_point = Default::default(); + + for event in path.raw().iter() { + match event { + lyon_path::Event::Begin { at } => { + builder.move_to(at.x, at.y); + + last_point = at; + } + lyon_path::Event::Line { from, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.line_to(to.x, to.y); + + last_point = to; + } + lyon_path::Event::Quadratic { from, ctrl, to } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder.quad_to(ctrl.x, ctrl.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::Cubic { + from, + ctrl1, + ctrl2, + to, + } => { + if last_point != from { + builder.move_to(from.x, from.y); + } + + builder + .cubic_to(ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y); + + last_point = to; + } + lyon_path::Event::End { close, .. } => { + if close { + builder.close(); + } + } + } + } + + builder + .finish() + .expect("Convert lyon path to tiny_skia path") +} + +pub fn into_paint(style: Style) -> tiny_skia::Paint<'static> { + tiny_skia::Paint { + shader: match style { + Style::Solid(color) => tiny_skia::Shader::SolidColor( + tiny_skia::Color::from_rgba(color.b, color.g, color.r, color.a) + .expect("Create color"), + ), + Style::Gradient(gradient) => match gradient { + Gradient::Linear(linear) => tiny_skia::LinearGradient::new( + tiny_skia::Point { + x: linear.start.x, + y: linear.start.y, + }, + tiny_skia::Point { + x: linear.end.x, + y: linear.end.y, + }, + linear + .color_stops + .into_iter() + .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::SpreadMode::Pad, + tiny_skia::Transform::identity(), + ) + .expect("Create linear gradient"), + }, + }, + anti_alias: true, + ..Default::default() + } +} + +pub fn into_fill_rule(rule: fill::Rule) -> tiny_skia::FillRule { + match rule { + fill::Rule::EvenOdd => tiny_skia::FillRule::EvenOdd, + fill::Rule::NonZero => tiny_skia::FillRule::Winding, + } +} + +pub fn into_stroke(stroke: &Stroke) -> tiny_skia::Stroke { + tiny_skia::Stroke { + width: stroke.width, + line_cap: match stroke.line_cap { + stroke::LineCap::Butt => tiny_skia::LineCap::Butt, + stroke::LineCap::Square => tiny_skia::LineCap::Square, + stroke::LineCap::Round => tiny_skia::LineCap::Round, + }, + line_join: match stroke.line_join { + stroke::LineJoin::Miter => tiny_skia::LineJoin::Miter, + stroke::LineJoin::Round => tiny_skia::LineJoin::Round, + stroke::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, + }, + dash: if stroke.line_dash.segments.is_empty() { + None + } else { + tiny_skia::StrokeDash::new( + stroke.line_dash.segments.into(), + stroke.line_dash.offset as f32, + ) + }, + ..Default::default() + } +} diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index e66e6412..ef5c6b1d 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -4,8 +4,8 @@ mod backend; mod settings; mod text; -#[cfg(feature = "canvas")] -pub mod canvas; +#[cfg(feature = "geometry")] +pub mod geometry; pub use iced_graphics::primitive; diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 0bcef71c..2e39a9e7 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -21,7 +21,7 @@ bmp = ["iced_graphics/bmp"] hdr = ["iced_graphics/hdr"] dds = ["iced_graphics/dds"] farbfeld = ["iced_graphics/farbfeld"] -canvas = ["iced_graphics/canvas", "lyon"] +geometry = ["iced_graphics/geometry", "lyon"] spirv = ["wgpu/spirv"] webgl = ["wgpu/webgl"] diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 10dc5b4f..6f39a5fe 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -329,8 +329,6 @@ impl Backend { } impl iced_graphics::Backend for Backend { - type Geometry = (); - fn trim_measurements(&mut self) { self.text_pipeline.trim_measurement_cache() } diff --git a/wgpu/src/canvas.rs b/wgpu/src/canvas.rs deleted file mode 100644 index e8d540c3..00000000 --- a/wgpu/src/canvas.rs +++ /dev/null @@ -1,608 +0,0 @@ -use iced_graphics::primitive::{self, Primitive}; -use iced_native::widget::canvas::fill::{self, Fill}; -use iced_native::widget::canvas::{ - LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, -}; -use iced_native::{Gradient, Point, Rectangle, Size, Vector}; - -use lyon::geom::euclid; -use lyon::tessellation; -use std::borrow::Cow; - -/// The frame of a [`Canvas`]. -/// -/// [`Canvas`]: crate::widget::Canvas -#[allow(missing_debug_implementations)] -pub struct Frame { - size: Size, - buffers: BufferStack, - primitives: Vec, - transforms: Transforms, - fill_tessellator: tessellation::FillTessellator, - stroke_tessellator: tessellation::StrokeTessellator, -} - -enum Buffer { - Solid(tessellation::VertexBuffers), - Gradient( - tessellation::VertexBuffers, - Gradient, - ), -} - -struct BufferStack { - stack: Vec, -} - -impl BufferStack { - fn new() -> Self { - Self { stack: Vec::new() } - } - - fn get_mut(&mut self, style: &Style) -> &mut Buffer { - match style { - Style::Solid(_) => match self.stack.last() { - Some(Buffer::Solid(_)) => {} - _ => { - self.stack.push(Buffer::Solid( - tessellation::VertexBuffers::new(), - )); - } - }, - Style::Gradient(gradient) => match self.stack.last() { - Some(Buffer::Gradient(_, last)) if gradient == last => {} - _ => { - self.stack.push(Buffer::Gradient( - tessellation::VertexBuffers::new(), - gradient.clone(), - )); - } - }, - } - - self.stack.last_mut().unwrap() - } - - fn get_fill<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } - - fn get_stroke<'a>( - &'a mut self, - style: &Style, - ) -> Box { - match (style, self.get_mut(style)) { - (Style::Solid(color), Buffer::Solid(buffer)) => { - Box::new(tessellation::BuffersBuilder::new( - buffer, - TriangleVertex2DBuilder(color.into_linear()), - )) - } - (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( - tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), - ), - _ => unreachable!(), - } - } -} - -#[derive(Debug)] -struct Transforms { - previous: Vec, - current: Transform, -} - -#[derive(Debug, Clone, Copy)] -struct Transform { - raw: lyon::math::Transform, - is_identity: bool, -} - -impl Transform { - /// Transforms the given [Point] by the transformation matrix. - fn transform_point(&self, point: &mut Point) { - let transformed = self - .raw - .transform_point(euclid::Point2D::new(point.x, point.y)); - point.x = transformed.x; - point.y = transformed.y; - } - - fn transform_style(&self, style: Style) -> Style { - match style { - Style::Solid(color) => Style::Solid(color), - Style::Gradient(gradient) => { - Style::Gradient(self.transform_gradient(gradient)) - } - } - } - - fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { - let (start, end) = match &mut gradient { - Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), - }; - self.transform_point(start); - self.transform_point(end); - gradient - } -} - -impl Frame { - /// 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. - pub fn new(size: Size) -> Frame { - Frame { - size, - buffers: BufferStack::new(), - primitives: Vec::new(), - transforms: Transforms { - previous: Vec::new(), - current: Transform { - raw: lyon::math::Transform::identity(), - is_identity: true, - }, - }, - fill_tessellator: tessellation::FillTessellator::new(), - stroke_tessellator: tessellation::StrokeTessellator::new(), - } - } - - /// Returns the width of the [`Frame`]. - #[inline] - pub fn width(&self) -> f32 { - self.size.width - } - - /// Returns the height of the [`Frame`]. - #[inline] - pub fn height(&self) -> f32 { - self.size.height - } - - /// Returns the dimensions of the [`Frame`]. - #[inline] - pub fn size(&self) -> Size { - self.size - } - - /// Returns the coordinate of the center of the [`Frame`]. - #[inline] - pub fn center(&self) -> Point { - Point::new(self.size.width / 2.0, self.size.height / 2.0) - } - - /// Draws the given [`Path`] on the [`Frame`] by filling it with the - /// provided style. - pub fn fill(&mut self, path: &Path, fill: impl Into) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - if self.transforms.current.is_identity { - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.fill_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Tessellate path."); - } - - /// Draws an axis-aligned rectangle given its top-left corner coordinate and - /// its `Size` on the [`Frame`] by filling it with the provided style. - pub fn fill_rectangle( - &mut self, - top_left: Point, - size: Size, - fill: impl Into, - ) { - let Fill { style, rule } = fill.into(); - - let mut buffer = self - .buffers - .get_fill(&self.transforms.current.transform_style(style)); - - let top_left = - self.transforms.current.raw.transform_point( - lyon::math::Point::new(top_left.x, top_left.y), - ); - - let size = - self.transforms.current.raw.transform_vector( - lyon::math::Vector::new(size.width, size.height), - ); - - let options = tessellation::FillOptions::default() - .with_fill_rule(into_fill_rule(rule)); - - self.fill_tessellator - .tessellate_rectangle( - &lyon::math::Box2D::new(top_left, top_left + size), - &options, - buffer.as_mut(), - ) - .expect("Fill rectangle"); - } - - /// Draws the stroke of the given [`Path`] on the [`Frame`] with the - /// provided style. - pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { - let stroke = stroke.into(); - - let mut buffer = self - .buffers - .get_stroke(&self.transforms.current.transform_style(stroke.style)); - - let mut options = tessellation::StrokeOptions::default(); - options.line_width = stroke.width; - options.start_cap = into_line_cap(stroke.line_cap); - options.end_cap = into_line_cap(stroke.line_cap); - options.line_join = into_line_join(stroke.line_join); - - let path = if stroke.line_dash.segments.is_empty() { - Cow::Borrowed(path) - } else { - Cow::Owned(dashed(path, stroke.line_dash)) - }; - - if self.transforms.current.is_identity { - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } else { - let path = path.transform(&self.transforms.current.raw); - - self.stroke_tessellator.tessellate_path( - path.raw(), - &options, - buffer.as_mut(), - ) - } - .expect("Stroke path"); - } - - /// Draws the characters of the given [`Text`] on the [`Frame`], filling - /// them with the given color. - /// - /// __Warning:__ Text currently does not work well with rotations and scale - /// transforms! The position will be correctly transformed, but the - /// resulting glyphs will not be rotated or scaled properly. - /// - /// Additionally, all text will be rendered on top of all the layers of - /// a [`Canvas`]. Therefore, it is currently only meant to be used for - /// overlays, which is the most common use case. - /// - /// Support for vectorial text is planned, and should address all these - /// limitations. - /// - /// [`Canvas`]: crate::widget::Canvas - pub fn fill_text(&mut self, text: impl Into) { - let text = text.into(); - - let position = if self.transforms.current.is_identity { - text.position - } else { - let transformed = self.transforms.current.raw.transform_point( - lyon::math::Point::new(text.position.x, text.position.y), - ); - - Point::new(transformed.x, transformed.y) - }; - - // TODO: Use vectorial text instead of primitive - self.primitives.push(Primitive::Text { - content: text.content, - bounds: Rectangle { - x: position.x, - y: position.y, - width: f32::INFINITY, - height: f32::INFINITY, - }, - color: text.color, - size: text.size, - font: text.font, - horizontal_alignment: text.horizontal_alignment, - vertical_alignment: text.vertical_alignment, - }); - } - - /// Stores the current transform of the [`Frame`] and executes the given - /// drawing operations, restoring the transform afterwards. - /// - /// This method is useful to compose transforms and perform drawing - /// operations in different coordinate systems. - #[inline] - pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { - self.push_transform(); - - f(self); - - self.pop_transform(); - } - - pub fn push_transform(&mut self) { - self.transforms.previous.push(self.transforms.current); - } - - pub fn pop_transform(&mut self) { - self.transforms.current = self.transforms.previous.pop().unwrap(); - } - - /// Executes the given drawing operations within a [`Rectangle`] region, - /// clipping any geometry that overflows its bounds. Any transformations - /// performed are local to the provided closure. - /// - /// This method is useful to perform drawing operations that need to be - /// clipped. - #[inline] - pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { - let mut frame = Frame::new(region.size()); - - f(&mut frame); - - let translation = Vector::new(region.x, region.y); - - self.clip(frame, translation); - } - - pub fn clip(&mut self, frame: Frame, translation: Vector) { - let size = frame.size(); - let primitives = frame.into_primitives(); - - let (text, meshes) = primitives - .into_iter() - .partition(|primitive| matches!(primitive, Primitive::Text { .. })); - - self.primitives.push(Primitive::Group { - primitives: vec![ - Primitive::Translate { - translation, - content: Box::new(Primitive::Group { primitives: meshes }), - }, - Primitive::Translate { - translation, - content: Box::new(Primitive::Clip { - bounds: Rectangle::with_size(size), - content: Box::new(Primitive::Group { - primitives: text, - }), - }), - }, - ], - }); - } - - /// Applies a translation to the current transform of the [`Frame`]. - #[inline] - pub fn translate(&mut self, translation: Vector) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_translate(lyon::math::Vector::new( - translation.x, - translation.y, - )); - self.transforms.current.is_identity = false; - } - - /// Applies a rotation in radians to the current transform of the [`Frame`]. - #[inline] - pub fn rotate(&mut self, angle: f32) { - self.transforms.current.raw = self - .transforms - .current - .raw - .pre_rotate(lyon::math::Angle::radians(angle)); - self.transforms.current.is_identity = false; - } - - /// Applies a scaling to the current transform of the [`Frame`]. - #[inline] - pub fn scale(&mut self, scale: f32) { - self.transforms.current.raw = - self.transforms.current.raw.pre_scale(scale, scale); - self.transforms.current.is_identity = false; - } - - /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. - pub fn into_primitive(self) -> Primitive { - Primitive::Group { - primitives: self.into_primitives(), - } - } - - fn into_primitives(mut self) -> Vec { - for buffer in self.buffers.stack { - match buffer { - Buffer::Solid(buffer) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::SolidMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - }) - } - } - Buffer::Gradient(buffer, gradient) => { - if !buffer.indices.is_empty() { - self.primitives.push(Primitive::GradientMesh { - buffers: primitive::Mesh2D { - vertices: buffer.vertices, - indices: buffer.indices, - }, - size: self.size, - gradient, - }) - } - } - } - } - - self.primitives - } -} - -struct Vertex2DBuilder; - -impl tessellation::FillVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -impl tessellation::StrokeVertexConstructor - for Vertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::Vertex2D { - let position = vertex.position(); - - primitive::Vertex2D { - position: [position.x, position.y], - } - } -} - -struct TriangleVertex2DBuilder([f32; 4]); - -impl tessellation::FillVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::FillVertex<'_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -impl tessellation::StrokeVertexConstructor - for TriangleVertex2DBuilder -{ - fn new_vertex( - &mut self, - vertex: tessellation::StrokeVertex<'_, '_>, - ) -> primitive::ColoredVertex2D { - let position = vertex.position(); - - primitive::ColoredVertex2D { - position: [position.x, position.y], - color: self.0, - } - } -} - -fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { - match line_join { - LineJoin::Miter => lyon::tessellation::LineJoin::Miter, - LineJoin::Round => lyon::tessellation::LineJoin::Round, - LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, - } -} - -fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { - match line_cap { - LineCap::Butt => lyon::tessellation::LineCap::Butt, - LineCap::Square => lyon::tessellation::LineCap::Square, - LineCap::Round => lyon::tessellation::LineCap::Round, - } -} - -fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { - match rule { - fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, - fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, - } -} - -pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { - use lyon::algorithms::walk::{ - walk_along_path, RepeatedPattern, WalkerEvent, - }; - use lyon::path::iterator::PathIterator; - - Path::new(|builder| { - let segments_odd = (line_dash.segments.len() % 2 == 1) - .then(|| [line_dash.segments, line_dash.segments].concat()); - - let mut draw_line = false; - - walk_along_path( - path.raw().iter().flattened(0.01), - 0.0, - lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, - &mut RepeatedPattern { - callback: |event: WalkerEvent<'_>| { - let point = Point { - x: event.position.x, - y: event.position.y, - }; - - if draw_line { - builder.line_to(point); - } else { - builder.move_to(point); - } - - draw_line = !draw_line; - - true - }, - index: line_dash.offset, - intervals: segments_odd - .as_deref() - .unwrap_or(line_dash.segments), - }, - ); - }) -} diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs new file mode 100644 index 00000000..11e8126f --- /dev/null +++ b/wgpu/src/geometry.rs @@ -0,0 +1,608 @@ +use iced_graphics::geometry::fill::{self, Fill}; +use iced_graphics::geometry::{ + LineCap, LineDash, LineJoin, Path, Stroke, Style, Text, +}; +use iced_graphics::primitive::{self, Primitive}; +use iced_graphics::{Gradient, Point, Rectangle, Size, Vector}; + +use lyon::geom::euclid; +use lyon::tessellation; +use std::borrow::Cow; + +/// The frame of a [`Canvas`]. +/// +/// [`Canvas`]: crate::widget::Canvas +#[allow(missing_debug_implementations)] +pub struct Frame { + size: Size, + buffers: BufferStack, + primitives: Vec, + transforms: Transforms, + fill_tessellator: tessellation::FillTessellator, + stroke_tessellator: tessellation::StrokeTessellator, +} + +enum Buffer { + Solid(tessellation::VertexBuffers), + Gradient( + tessellation::VertexBuffers, + Gradient, + ), +} + +struct BufferStack { + stack: Vec, +} + +impl BufferStack { + fn new() -> Self { + Self { stack: Vec::new() } + } + + fn get_mut(&mut self, style: &Style) -> &mut Buffer { + match style { + Style::Solid(_) => match self.stack.last() { + Some(Buffer::Solid(_)) => {} + _ => { + self.stack.push(Buffer::Solid( + tessellation::VertexBuffers::new(), + )); + } + }, + Style::Gradient(gradient) => match self.stack.last() { + Some(Buffer::Gradient(_, last)) if gradient == last => {} + _ => { + self.stack.push(Buffer::Gradient( + tessellation::VertexBuffers::new(), + gradient.clone(), + )); + } + }, + } + + self.stack.last_mut().unwrap() + } + + fn get_fill<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } + + fn get_stroke<'a>( + &'a mut self, + style: &Style, + ) -> Box { + match (style, self.get_mut(style)) { + (Style::Solid(color), Buffer::Solid(buffer)) => { + Box::new(tessellation::BuffersBuilder::new( + buffer, + TriangleVertex2DBuilder(color.into_linear()), + )) + } + (Style::Gradient(_), Buffer::Gradient(buffer, _)) => Box::new( + tessellation::BuffersBuilder::new(buffer, Vertex2DBuilder), + ), + _ => unreachable!(), + } + } +} + +#[derive(Debug)] +struct Transforms { + previous: Vec, + current: Transform, +} + +#[derive(Debug, Clone, Copy)] +struct Transform { + raw: lyon::math::Transform, + is_identity: bool, +} + +impl Transform { + /// Transforms the given [Point] by the transformation matrix. + fn transform_point(&self, point: &mut Point) { + let transformed = self + .raw + .transform_point(euclid::Point2D::new(point.x, point.y)); + point.x = transformed.x; + point.y = transformed.y; + } + + fn transform_style(&self, style: Style) -> Style { + match style { + Style::Solid(color) => Style::Solid(color), + Style::Gradient(gradient) => { + Style::Gradient(self.transform_gradient(gradient)) + } + } + } + + fn transform_gradient(&self, mut gradient: Gradient) -> Gradient { + let (start, end) = match &mut gradient { + Gradient::Linear(linear) => (&mut linear.start, &mut linear.end), + }; + self.transform_point(start); + self.transform_point(end); + gradient + } +} + +impl Frame { + /// 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. + pub fn new(size: Size) -> Frame { + Frame { + size, + buffers: BufferStack::new(), + primitives: Vec::new(), + transforms: Transforms { + previous: Vec::new(), + current: Transform { + raw: lyon::math::Transform::identity(), + is_identity: true, + }, + }, + fill_tessellator: tessellation::FillTessellator::new(), + stroke_tessellator: tessellation::StrokeTessellator::new(), + } + } + + /// Returns the width of the [`Frame`]. + #[inline] + pub fn width(&self) -> f32 { + self.size.width + } + + /// Returns the height of the [`Frame`]. + #[inline] + pub fn height(&self) -> f32 { + self.size.height + } + + /// Returns the dimensions of the [`Frame`]. + #[inline] + pub fn size(&self) -> Size { + self.size + } + + /// Returns the coordinate of the center of the [`Frame`]. + #[inline] + pub fn center(&self) -> Point { + Point::new(self.size.width / 2.0, self.size.height / 2.0) + } + + /// Draws the given [`Path`] on the [`Frame`] by filling it with the + /// provided style. + pub fn fill(&mut self, path: &Path, fill: impl Into) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + if self.transforms.current.is_identity { + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.fill_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Tessellate path."); + } + + /// Draws an axis-aligned rectangle given its top-left corner coordinate and + /// its `Size` on the [`Frame`] by filling it with the provided style. + pub fn fill_rectangle( + &mut self, + top_left: Point, + size: Size, + fill: impl Into, + ) { + let Fill { style, rule } = fill.into(); + + let mut buffer = self + .buffers + .get_fill(&self.transforms.current.transform_style(style)); + + let top_left = + self.transforms.current.raw.transform_point( + lyon::math::Point::new(top_left.x, top_left.y), + ); + + let size = + self.transforms.current.raw.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let options = tessellation::FillOptions::default() + .with_fill_rule(into_fill_rule(rule)); + + self.fill_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Fill rectangle"); + } + + /// Draws the stroke of the given [`Path`] on the [`Frame`] with the + /// provided style. + pub fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + let path = if stroke.line_dash.segments.is_empty() { + Cow::Borrowed(path) + } else { + Cow::Owned(dashed(path, stroke.line_dash)) + }; + + if self.transforms.current.is_identity { + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } else { + let path = path.transform(&self.transforms.current.raw); + + self.stroke_tessellator.tessellate_path( + path.raw(), + &options, + buffer.as_mut(), + ) + } + .expect("Stroke path"); + } + + /// Draws the characters of the given [`Text`] on the [`Frame`], filling + /// them with the given color. + /// + /// __Warning:__ Text currently does not work well with rotations and scale + /// transforms! The position will be correctly transformed, but the + /// resulting glyphs will not be rotated or scaled properly. + /// + /// Additionally, all text will be rendered on top of all the layers of + /// a [`Canvas`]. Therefore, it is currently only meant to be used for + /// overlays, which is the most common use case. + /// + /// Support for vectorial text is planned, and should address all these + /// limitations. + /// + /// [`Canvas`]: crate::widget::Canvas + pub fn fill_text(&mut self, text: impl Into) { + let text = text.into(); + + let position = if self.transforms.current.is_identity { + text.position + } else { + let transformed = self.transforms.current.raw.transform_point( + lyon::math::Point::new(text.position.x, text.position.y), + ); + + Point::new(transformed.x, transformed.y) + }; + + // TODO: Use vectorial text instead of primitive + self.primitives.push(Primitive::Text { + content: text.content, + bounds: Rectangle { + x: position.x, + y: position.y, + width: f32::INFINITY, + height: f32::INFINITY, + }, + color: text.color, + size: text.size, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + }); + } + + /// Stores the current transform of the [`Frame`] and executes the given + /// drawing operations, restoring the transform afterwards. + /// + /// This method is useful to compose transforms and perform drawing + /// operations in different coordinate systems. + #[inline] + pub fn with_save(&mut self, f: impl FnOnce(&mut Frame)) { + self.push_transform(); + + f(self); + + self.pop_transform(); + } + + pub fn push_transform(&mut self) { + self.transforms.previous.push(self.transforms.current); + } + + pub fn pop_transform(&mut self) { + self.transforms.current = self.transforms.previous.pop().unwrap(); + } + + /// Executes the given drawing operations within a [`Rectangle`] region, + /// clipping any geometry that overflows its bounds. Any transformations + /// performed are local to the provided closure. + /// + /// This method is useful to perform drawing operations that need to be + /// clipped. + #[inline] + pub fn with_clip(&mut self, region: Rectangle, f: impl FnOnce(&mut Frame)) { + let mut frame = Frame::new(region.size()); + + f(&mut frame); + + let translation = Vector::new(region.x, region.y); + + self.clip(frame, translation); + } + + pub fn clip(&mut self, frame: Frame, translation: Vector) { + let size = frame.size(); + let primitives = frame.into_primitives(); + + let (text, meshes) = primitives + .into_iter() + .partition(|primitive| matches!(primitive, Primitive::Text { .. })); + + self.primitives.push(Primitive::Group { + primitives: vec![ + Primitive::Translate { + translation, + content: Box::new(Primitive::Group { primitives: meshes }), + }, + Primitive::Translate { + translation, + content: Box::new(Primitive::Clip { + bounds: Rectangle::with_size(size), + content: Box::new(Primitive::Group { + primitives: text, + }), + }), + }, + ], + }); + } + + /// Applies a translation to the current transform of the [`Frame`]. + #[inline] + pub fn translate(&mut self, translation: Vector) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_translate(lyon::math::Vector::new( + translation.x, + translation.y, + )); + self.transforms.current.is_identity = false; + } + + /// Applies a rotation in radians to the current transform of the [`Frame`]. + #[inline] + pub fn rotate(&mut self, angle: f32) { + self.transforms.current.raw = self + .transforms + .current + .raw + .pre_rotate(lyon::math::Angle::radians(angle)); + self.transforms.current.is_identity = false; + } + + /// Applies a scaling to the current transform of the [`Frame`]. + #[inline] + pub fn scale(&mut self, scale: f32) { + self.transforms.current.raw = + self.transforms.current.raw.pre_scale(scale, scale); + self.transforms.current.is_identity = false; + } + + /// Produces the [`Primitive`] representing everything drawn on the [`Frame`]. + pub fn into_primitive(self) -> Primitive { + Primitive::Group { + primitives: self.into_primitives(), + } + } + + fn into_primitives(mut self) -> Vec { + for buffer in self.buffers.stack { + match buffer { + Buffer::Solid(buffer) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::SolidMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + }) + } + } + Buffer::Gradient(buffer, gradient) => { + if !buffer.indices.is_empty() { + self.primitives.push(Primitive::GradientMesh { + buffers: primitive::Mesh2D { + vertices: buffer.vertices, + indices: buffer.indices, + }, + size: self.size, + gradient, + }) + } + } + } + } + + self.primitives + } +} + +struct Vertex2DBuilder; + +impl tessellation::FillVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +impl tessellation::StrokeVertexConstructor + for Vertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::Vertex2D { + let position = vertex.position(); + + primitive::Vertex2D { + position: [position.x, position.y], + } + } +} + +struct TriangleVertex2DBuilder([f32; 4]); + +impl tessellation::FillVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::FillVertex<'_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +impl tessellation::StrokeVertexConstructor + for TriangleVertex2DBuilder +{ + fn new_vertex( + &mut self, + vertex: tessellation::StrokeVertex<'_, '_>, + ) -> primitive::ColoredVertex2D { + let position = vertex.position(); + + primitive::ColoredVertex2D { + position: [position.x, position.y], + color: self.0, + } + } +} + +fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin { + match line_join { + LineJoin::Miter => lyon::tessellation::LineJoin::Miter, + LineJoin::Round => lyon::tessellation::LineJoin::Round, + LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel, + } +} + +fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap { + match line_cap { + LineCap::Butt => lyon::tessellation::LineCap::Butt, + LineCap::Square => lyon::tessellation::LineCap::Square, + LineCap::Round => lyon::tessellation::LineCap::Round, + } +} + +fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { + match rule { + fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero, + fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd, + } +} + +pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { + use lyon::algorithms::walk::{ + walk_along_path, RepeatedPattern, WalkerEvent, + }; + use lyon::path::iterator::PathIterator; + + Path::new(|builder| { + let segments_odd = (line_dash.segments.len() % 2 == 1) + .then(|| [line_dash.segments, line_dash.segments].concat()); + + let mut draw_line = false; + + walk_along_path( + path.raw().iter().flattened(0.01), + 0.0, + lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE, + &mut RepeatedPattern { + callback: |event: WalkerEvent<'_>| { + let point = Point { + x: event.position.x, + y: event.position.y, + }; + + if draw_line { + builder.line_to(point); + } else { + builder.move_to(point); + } + + draw_line = !draw_line; + + true + }, + index: line_dash.offset, + intervals: segments_odd + .as_deref() + .unwrap_or(line_dash.segments), + }, + ); + }) +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 31db16a8..4439b185 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -41,8 +41,8 @@ pub mod layer; pub mod settings; pub mod window; -#[cfg(feature = "canvas")] -pub mod canvas; +#[cfg(feature = "geometry")] +pub mod geometry; mod backend; mod buffer; -- cgit From c54409d1711e1f615c7ea4b02c082954e340632a Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Fri, 3 Mar 2023 17:31:44 +0100 Subject: Remove `canvas` leftovers in `iced_native` --- native/src/widget.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/native/src/widget.rs b/native/src/widget.rs index f107cd69..2b3ca7be 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -83,14 +83,6 @@ pub use tree::Tree; #[doc(no_inline)] pub use vertical_slider::VerticalSlider; -#[cfg(feature = "canvas")] -#[cfg_attr(docsrs, doc(cfg(feature = "canvas")))] -pub mod canvas; - -#[cfg(feature = "canvas")] -#[doc(no_inline)] -pub use canvas::Canvas; - pub use action::Action; pub use id::Id; pub use operation::Operation; -- cgit From 3a0d34c0240f4421737a6a08761f99d6f8140d02 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 4 Mar 2023 05:37:11 +0100 Subject: Create `iced_widget` subcrate and re-organize the whole codebase --- Cargo.toml | 29 +- core/Cargo.toml | 1 + core/src/clipboard.rs | 23 + core/src/element.rs | 608 ++++++++++++ core/src/event.rs | 78 ++ core/src/hasher.rs | 13 + core/src/image.rs | 174 ++++ core/src/layout.rs | 65 ++ core/src/layout/DRUID_LICENSE | 202 ++++ core/src/layout/flex.rs | 232 +++++ core/src/layout/limits.rs | 163 ++++ core/src/layout/node.rs | 91 ++ core/src/lib.rs | 24 + core/src/mouse.rs | 3 + core/src/mouse/click.rs | 76 ++ core/src/overlay.rs | 122 +++ core/src/overlay/element.rs | 270 ++++++ core/src/overlay/group.rs | 172 ++++ core/src/renderer.rs | 99 ++ core/src/renderer/null.rs | 82 ++ core/src/shell.rs | 108 +++ core/src/svg.rs | 89 ++ core/src/text.rs | 111 +++ core/src/touch.rs | 23 + core/src/widget.rs | 145 +++ core/src/widget/id.rs | 43 + core/src/widget/operation.rs | 112 +++ core/src/widget/operation/focusable.rs | 203 ++++ core/src/widget/operation/scrollable.rs | 54 ++ core/src/widget/operation/text_input.rs | 131 +++ core/src/widget/text.rs | 277 ++++++ core/src/widget/tree.rs | 187 ++++ core/src/window.rs | 10 + core/src/window/event.rs | 58 ++ core/src/window/mode.rs | 12 + core/src/window/redraw_request.rs | 38 + core/src/window/user_attention.rs | 21 + examples/component/Cargo.toml | 4 +- examples/component/src/main.rs | 22 +- examples/custom_quad/Cargo.toml | 3 +- examples/custom_quad/src/main.rs | 8 +- examples/custom_widget/Cargo.toml | 3 +- examples/custom_widget/src/main.rs | 8 +- examples/events/Cargo.toml | 1 - examples/events/src/main.rs | 9 +- examples/geometry/Cargo.toml | 3 +- examples/geometry/src/main.rs | 38 +- examples/integration/Cargo.toml | 1 + examples/integration/src/controls.rs | 10 +- examples/integration/src/main.rs | 91 +- examples/integration/src/scene.rs | 2 +- examples/lazy/Cargo.toml | 3 +- examples/lazy/src/main.rs | 3 +- examples/modal/Cargo.toml | 3 +- examples/modal/src/main.rs | 44 +- examples/pane_grid/Cargo.toml | 4 +- examples/pane_grid/src/main.rs | 8 +- examples/toast/Cargo.toml | 3 +- examples/toast/src/main.rs | 24 +- examples/url_handler/Cargo.toml | 1 - examples/url_handler/src/main.rs | 10 +- graphics/Cargo.toml | 10 +- graphics/src/backend.rs | 8 +- graphics/src/compositor.rs | 92 ++ graphics/src/geometry.rs | 4 +- graphics/src/geometry/fill.rs | 2 +- graphics/src/geometry/path.rs | 2 +- graphics/src/geometry/path/arc.rs | 2 +- graphics/src/geometry/path/builder.rs | 3 +- graphics/src/geometry/stroke.rs | 2 +- graphics/src/geometry/style.rs | 2 +- graphics/src/geometry/text.rs | 4 +- graphics/src/image/raster.rs | 4 +- graphics/src/image/storage.rs | 2 +- graphics/src/image/vector.rs | 5 +- graphics/src/lib.rs | 11 +- graphics/src/overlay.rs | 2 - graphics/src/overlay/menu.rs | 3 - graphics/src/primitive.rs | 9 +- graphics/src/renderer.rs | 20 +- graphics/src/viewport.rs | 4 +- graphics/src/window.rs | 10 - graphics/src/window/compositor.rs | 90 -- graphics/src/window/gl_compositor.rs | 71 -- lazy/Cargo.toml | 18 - lazy/src/cache.rs | 13 - lazy/src/component.rs | 575 ----------- lazy/src/lazy.rs | 396 -------- lazy/src/lib.rs | 66 -- lazy/src/responsive.rs | 425 --------- native/Cargo.toml | 7 - native/src/clipboard.rs | 22 - native/src/command.rs | 4 +- native/src/debug/basic.rs | 2 +- native/src/element.rs | 583 ----------- native/src/event.rs | 78 -- native/src/hasher.rs | 13 - native/src/image.rs | 174 ---- native/src/layout.rs | 65 -- native/src/layout/DRUID_LICENSE | 202 ---- native/src/layout/flex.rs | 232 ----- native/src/layout/limits.rs | 163 ---- native/src/layout/node.rs | 91 -- native/src/lib.rs | 39 +- native/src/mouse.rs | 6 - native/src/mouse/click.rs | 76 -- native/src/overlay.rs | 125 --- native/src/overlay/element.rs | 270 ------ native/src/overlay/group.rs | 174 ---- native/src/overlay/menu.rs | 519 ---------- native/src/program.rs | 6 +- native/src/program/state.rs | 18 +- native/src/renderer.rs | 98 -- native/src/renderer/null.rs | 82 -- native/src/runtime.rs | 4 +- native/src/shell.rs | 108 --- native/src/subscription.rs | 17 +- native/src/svg.rs | 89 -- native/src/text.rs | 111 --- native/src/touch.rs | 23 - native/src/user_interface.rs | 82 +- native/src/widget.rs | 206 ---- native/src/widget/action.rs | 5 +- native/src/widget/button.rs | 455 --------- native/src/widget/checkbox.rs | 321 ------- native/src/widget/column.rs | 264 ----- native/src/widget/container.rs | 368 ------- native/src/widget/helpers.rs | 317 ------ native/src/widget/id.rs | 43 - native/src/widget/image.rs | 204 ---- native/src/widget/image/viewer.rs | 428 --------- native/src/widget/operation.rs | 112 --- native/src/widget/operation/focusable.rs | 203 ---- native/src/widget/operation/scrollable.rs | 54 -- native/src/widget/operation/text_input.rs | 131 --- native/src/widget/pane_grid.rs | 991 ------------------- native/src/widget/pane_grid/axis.rs | 241 ----- native/src/widget/pane_grid/configuration.rs | 26 - native/src/widget/pane_grid/content.rs | 373 -------- native/src/widget/pane_grid/direction.rs | 12 - native/src/widget/pane_grid/draggable.rs | 12 - native/src/widget/pane_grid/node.rs | 250 ----- native/src/widget/pane_grid/pane.rs | 5 - native/src/widget/pane_grid/split.rs | 5 - native/src/widget/pane_grid/state.rs | 350 ------- native/src/widget/pane_grid/title_bar.rs | 432 --------- native/src/widget/pick_list.rs | 657 ------------- native/src/widget/progress_bar.rs | 168 ---- native/src/widget/radio.rs | 299 ------ native/src/widget/row.rs | 253 ----- native/src/widget/rule.rs | 147 --- native/src/widget/scrollable.rs | 1327 -------------------------- native/src/widget/slider.rs | 473 --------- native/src/widget/space.rs | 85 -- native/src/widget/svg.rs | 195 ---- native/src/widget/text.rs | 263 ----- native/src/widget/text_input.rs | 1218 ----------------------- native/src/widget/text_input/cursor.rs | 189 ---- native/src/widget/text_input/editor.rs | 70 -- native/src/widget/text_input/value.rs | 133 --- native/src/widget/toggler.rs | 324 ------- native/src/widget/tooltip.rs | 387 -------- native/src/widget/tree.rs | 187 ---- native/src/widget/vertical_slider.rs | 468 --------- native/src/window.rs | 13 +- native/src/window/action.rs | 4 +- native/src/window/event.rs | 58 -- native/src/window/mode.rs | 12 - native/src/window/redraw_request.rs | 38 - native/src/window/user_attention.rs | 21 - renderer/Cargo.toml | 4 - renderer/src/backend.rs | 11 +- renderer/src/compositor.rs | 133 +++ renderer/src/geometry.rs | 6 +- renderer/src/geometry/cache.rs | 4 +- renderer/src/lib.rs | 14 +- renderer/src/settings.rs | 3 +- renderer/src/window.rs | 3 - renderer/src/window/compositor.rs | 132 --- src/advanced.rs | 9 + src/application.rs | 12 +- src/clipboard.rs | 3 - src/element.rs | 5 - src/error.rs | 16 +- src/executor.rs | 14 - src/keyboard.rs | 2 - src/lib.rs | 134 ++- src/mouse.rs | 2 - src/overlay.rs | 18 - src/result.rs | 6 - src/touch.rs | 2 +- src/widget.rs | 239 ----- src/widget/canvas.rs | 238 ----- src/widget/canvas/cursor.rs | 64 -- src/widget/canvas/event.rs | 21 - src/widget/canvas/program.rs | 108 --- src/widget/qr_code.rs | 300 ------ src/window.rs | 5 +- style/src/lib.rs | 3 +- style/src/text.rs | 2 +- style/src/theme.rs | 2 +- tiny_skia/src/backend.rs | 18 +- tiny_skia/src/geometry.rs | 12 +- tiny_skia/src/lib.rs | 11 +- tiny_skia/src/settings.rs | 2 +- tiny_skia/src/text.rs | 11 +- tiny_skia/src/window/compositor.rs | 9 +- wgpu/Cargo.toml | 4 - wgpu/src/backend.rs | 20 +- wgpu/src/geometry.rs | 8 +- wgpu/src/image.rs | 21 +- wgpu/src/image/atlas.rs | 4 +- wgpu/src/image/atlas/allocation.rs | 3 +- wgpu/src/image/atlas/allocator.rs | 4 +- wgpu/src/image/atlas/entry.rs | 5 +- wgpu/src/layer.rs | 9 +- wgpu/src/layer/image.rs | 6 +- wgpu/src/layer/mesh.rs | 4 +- wgpu/src/layer/text.rs | 3 +- wgpu/src/lib.rs | 15 +- wgpu/src/quad.rs | 6 +- wgpu/src/settings.rs | 5 +- wgpu/src/text.rs | 14 +- wgpu/src/triangle.rs | 24 +- wgpu/src/triangle/msaa.rs | 4 +- wgpu/src/window/compositor.rs | 10 +- widget/Cargo.toml | 37 + widget/src/button.rs | 455 +++++++++ widget/src/canvas.rs | 238 +++++ widget/src/canvas/cursor.rs | 64 ++ widget/src/canvas/event.rs | 21 + widget/src/canvas/program.rs | 109 +++ widget/src/checkbox.rs | 323 +++++++ widget/src/column.rs | 264 +++++ widget/src/container.rs | 368 +++++++ widget/src/helpers.rs | 362 +++++++ widget/src/image.rs | 205 ++++ widget/src/image/viewer.rs | 428 +++++++++ widget/src/lazy.rs | 409 ++++++++ widget/src/lazy/cache.rs | 13 + widget/src/lazy/component.rs | 575 +++++++++++ widget/src/lazy/helpers.rs | 39 + widget/src/lazy/responsive.rs | 427 +++++++++ widget/src/lib.rs | 122 +++ widget/src/overlay.rs | 1 + widget/src/overlay/menu.rs | 519 ++++++++++ widget/src/pane_grid.rs | 991 +++++++++++++++++++ widget/src/pane_grid/axis.rs | 241 +++++ widget/src/pane_grid/configuration.rs | 26 + widget/src/pane_grid/content.rs | 373 ++++++++ widget/src/pane_grid/direction.rs | 12 + widget/src/pane_grid/draggable.rs | 12 + widget/src/pane_grid/node.rs | 250 +++++ widget/src/pane_grid/pane.rs | 5 + widget/src/pane_grid/split.rs | 5 + widget/src/pane_grid/state.rs | 348 +++++++ widget/src/pane_grid/title_bar.rs | 432 +++++++++ widget/src/pick_list.rs | 658 +++++++++++++ widget/src/progress_bar.rs | 172 ++++ widget/src/qr_code.rs | 297 ++++++ widget/src/radio.rs | 300 ++++++ widget/src/row.rs | 253 +++++ widget/src/rule.rs | 147 +++ widget/src/scrollable.rs | 1325 +++++++++++++++++++++++++ widget/src/slider.rs | 471 +++++++++ widget/src/space.rs | 86 ++ widget/src/svg.rs | 195 ++++ widget/src/text.rs | 4 + widget/src/text_input.rs | 1221 ++++++++++++++++++++++++ widget/src/text_input/cursor.rs | 189 ++++ widget/src/text_input/editor.rs | 70 ++ widget/src/text_input/value.rs | 133 +++ widget/src/toggler.rs | 326 +++++++ widget/src/tooltip.rs | 388 ++++++++ widget/src/vertical_slider.rs | 471 +++++++++ winit/Cargo.toml | 6 +- winit/src/application.rs | 77 +- winit/src/application/state.rs | 14 +- winit/src/clipboard.rs | 7 +- winit/src/conversion.rs | 11 +- winit/src/error.rs | 7 +- winit/src/lib.rs | 8 +- winit/src/proxy.rs | 2 +- winit/src/system.rs | 7 +- winit/src/window.rs | 52 +- 285 files changed, 19342 insertions(+), 19566 deletions(-) create mode 100644 core/src/clipboard.rs create mode 100644 core/src/element.rs create mode 100644 core/src/event.rs create mode 100644 core/src/hasher.rs create mode 100644 core/src/image.rs create mode 100644 core/src/layout.rs create mode 100644 core/src/layout/DRUID_LICENSE create mode 100644 core/src/layout/flex.rs create mode 100644 core/src/layout/limits.rs create mode 100644 core/src/layout/node.rs create mode 100644 core/src/mouse/click.rs create mode 100644 core/src/overlay.rs create mode 100644 core/src/overlay/element.rs create mode 100644 core/src/overlay/group.rs create mode 100644 core/src/renderer.rs create mode 100644 core/src/renderer/null.rs create mode 100644 core/src/shell.rs create mode 100644 core/src/svg.rs create mode 100644 core/src/text.rs create mode 100644 core/src/touch.rs create mode 100644 core/src/widget.rs create mode 100644 core/src/widget/id.rs create mode 100644 core/src/widget/operation.rs create mode 100644 core/src/widget/operation/focusable.rs create mode 100644 core/src/widget/operation/scrollable.rs create mode 100644 core/src/widget/operation/text_input.rs create mode 100644 core/src/widget/text.rs create mode 100644 core/src/widget/tree.rs create mode 100644 core/src/window.rs create mode 100644 core/src/window/event.rs create mode 100644 core/src/window/mode.rs create mode 100644 core/src/window/redraw_request.rs create mode 100644 core/src/window/user_attention.rs create mode 100644 graphics/src/compositor.rs delete mode 100644 graphics/src/overlay.rs delete mode 100644 graphics/src/overlay/menu.rs delete mode 100644 graphics/src/window.rs delete mode 100644 graphics/src/window/compositor.rs delete mode 100644 graphics/src/window/gl_compositor.rs delete mode 100644 lazy/Cargo.toml delete mode 100644 lazy/src/cache.rs delete mode 100644 lazy/src/component.rs delete mode 100644 lazy/src/lazy.rs delete mode 100644 lazy/src/lib.rs delete mode 100644 lazy/src/responsive.rs delete mode 100644 native/src/element.rs delete mode 100644 native/src/event.rs delete mode 100644 native/src/hasher.rs delete mode 100644 native/src/image.rs delete mode 100644 native/src/layout.rs delete mode 100644 native/src/layout/DRUID_LICENSE delete mode 100644 native/src/layout/flex.rs delete mode 100644 native/src/layout/limits.rs delete mode 100644 native/src/layout/node.rs delete mode 100644 native/src/mouse.rs delete mode 100644 native/src/mouse/click.rs delete mode 100644 native/src/overlay.rs delete mode 100644 native/src/overlay/element.rs delete mode 100644 native/src/overlay/group.rs delete mode 100644 native/src/overlay/menu.rs delete mode 100644 native/src/renderer.rs delete mode 100644 native/src/renderer/null.rs delete mode 100644 native/src/shell.rs delete mode 100644 native/src/svg.rs delete mode 100644 native/src/text.rs delete mode 100644 native/src/touch.rs delete mode 100644 native/src/widget/button.rs delete mode 100644 native/src/widget/checkbox.rs delete mode 100644 native/src/widget/column.rs delete mode 100644 native/src/widget/container.rs delete mode 100644 native/src/widget/helpers.rs delete mode 100644 native/src/widget/id.rs delete mode 100644 native/src/widget/image.rs delete mode 100644 native/src/widget/image/viewer.rs delete mode 100644 native/src/widget/operation.rs delete mode 100644 native/src/widget/operation/focusable.rs delete mode 100644 native/src/widget/operation/scrollable.rs delete mode 100644 native/src/widget/operation/text_input.rs delete mode 100644 native/src/widget/pane_grid.rs delete mode 100644 native/src/widget/pane_grid/axis.rs delete mode 100644 native/src/widget/pane_grid/configuration.rs delete mode 100644 native/src/widget/pane_grid/content.rs delete mode 100644 native/src/widget/pane_grid/direction.rs delete mode 100644 native/src/widget/pane_grid/draggable.rs delete mode 100644 native/src/widget/pane_grid/node.rs delete mode 100644 native/src/widget/pane_grid/pane.rs delete mode 100644 native/src/widget/pane_grid/split.rs delete mode 100644 native/src/widget/pane_grid/state.rs delete mode 100644 native/src/widget/pane_grid/title_bar.rs delete mode 100644 native/src/widget/pick_list.rs delete mode 100644 native/src/widget/progress_bar.rs delete mode 100644 native/src/widget/radio.rs delete mode 100644 native/src/widget/row.rs delete mode 100644 native/src/widget/rule.rs delete mode 100644 native/src/widget/scrollable.rs delete mode 100644 native/src/widget/slider.rs delete mode 100644 native/src/widget/space.rs delete mode 100644 native/src/widget/svg.rs delete mode 100644 native/src/widget/text.rs delete mode 100644 native/src/widget/text_input.rs delete mode 100644 native/src/widget/text_input/cursor.rs delete mode 100644 native/src/widget/text_input/editor.rs delete mode 100644 native/src/widget/text_input/value.rs delete mode 100644 native/src/widget/toggler.rs delete mode 100644 native/src/widget/tooltip.rs delete mode 100644 native/src/widget/tree.rs delete mode 100644 native/src/widget/vertical_slider.rs delete mode 100644 native/src/window/event.rs delete mode 100644 native/src/window/mode.rs delete mode 100644 native/src/window/redraw_request.rs delete mode 100644 native/src/window/user_attention.rs create mode 100644 renderer/src/compositor.rs delete mode 100644 renderer/src/window.rs delete mode 100644 renderer/src/window/compositor.rs create mode 100644 src/advanced.rs delete mode 100644 src/clipboard.rs delete mode 100644 src/element.rs delete mode 100644 src/executor.rs delete mode 100644 src/keyboard.rs delete mode 100644 src/mouse.rs delete mode 100644 src/overlay.rs delete mode 100644 src/result.rs delete mode 100644 src/widget.rs delete mode 100644 src/widget/canvas.rs delete mode 100644 src/widget/canvas/cursor.rs delete mode 100644 src/widget/canvas/event.rs delete mode 100644 src/widget/canvas/program.rs delete mode 100644 src/widget/qr_code.rs create mode 100644 widget/Cargo.toml create mode 100644 widget/src/button.rs create mode 100644 widget/src/canvas.rs create mode 100644 widget/src/canvas/cursor.rs create mode 100644 widget/src/canvas/event.rs create mode 100644 widget/src/canvas/program.rs create mode 100644 widget/src/checkbox.rs create mode 100644 widget/src/column.rs create mode 100644 widget/src/container.rs create mode 100644 widget/src/helpers.rs create mode 100644 widget/src/image.rs create mode 100644 widget/src/image/viewer.rs create mode 100644 widget/src/lazy.rs create mode 100644 widget/src/lazy/cache.rs create mode 100644 widget/src/lazy/component.rs create mode 100644 widget/src/lazy/helpers.rs create mode 100644 widget/src/lazy/responsive.rs create mode 100644 widget/src/lib.rs create mode 100644 widget/src/overlay.rs create mode 100644 widget/src/overlay/menu.rs create mode 100644 widget/src/pane_grid.rs create mode 100644 widget/src/pane_grid/axis.rs create mode 100644 widget/src/pane_grid/configuration.rs create mode 100644 widget/src/pane_grid/content.rs create mode 100644 widget/src/pane_grid/direction.rs create mode 100644 widget/src/pane_grid/draggable.rs create mode 100644 widget/src/pane_grid/node.rs create mode 100644 widget/src/pane_grid/pane.rs create mode 100644 widget/src/pane_grid/split.rs create mode 100644 widget/src/pane_grid/state.rs create mode 100644 widget/src/pane_grid/title_bar.rs create mode 100644 widget/src/pick_list.rs create mode 100644 widget/src/progress_bar.rs create mode 100644 widget/src/qr_code.rs create mode 100644 widget/src/radio.rs create mode 100644 widget/src/row.rs create mode 100644 widget/src/rule.rs create mode 100644 widget/src/scrollable.rs create mode 100644 widget/src/slider.rs create mode 100644 widget/src/space.rs create mode 100644 widget/src/svg.rs create mode 100644 widget/src/text.rs create mode 100644 widget/src/text_input.rs create mode 100644 widget/src/text_input/cursor.rs create mode 100644 widget/src/text_input/editor.rs create mode 100644 widget/src/text_input/value.rs create mode 100644 widget/src/toggler.rs create mode 100644 widget/src/tooltip.rs create mode 100644 widget/src/vertical_slider.rs diff --git a/Cargo.toml b/Cargo.toml index 28938df9..49a52311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,15 @@ categories = ["gui"] [features] # Enables the `Image` widget -image = ["iced_renderer/image", "image_rs"] +image = ["iced_widget/image", "image_rs"] # Enables the `Svg` widget -svg = ["iced_renderer/svg"] +svg = ["iced_widget/svg"] # Enables the `Canvas` widget -canvas = ["iced_renderer/geometry"] +canvas = ["iced_widget/canvas"] # Enables the `QRCode` widget -qr_code = ["canvas", "qrcode"] +qr_code = ["iced_widget/qr_code"] +# Enables lazy widgets +lazy = ["iced_widget/lazy"] # Enables a debug view in native platforms (press F12) debug = ["iced_winit/debug"] # Enables `tokio` as the `executor::Default` on native platforms @@ -32,11 +34,8 @@ smol = ["iced_futures/smol"] palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] -# Enables chrome traces -chrome-trace = [ - "iced_winit/chrome-trace", - "iced_renderer/tracing", -] +# Enables the advanced module +advanced = [] [badges] maintenance = { status = "actively-developed" } @@ -46,12 +45,12 @@ members = [ "core", "futures", "graphics", - "lazy", "native", "renderer", "style", "tiny_skia", "wgpu", + "widget", "winit", "examples/*", ] @@ -59,21 +58,15 @@ members = [ [dependencies] iced_core = { version = "0.8", path = "core" } iced_futures = { version = "0.6", path = "futures" } -iced_native = { version = "0.9", path = "native" } -iced_renderer = { version = "0.1", path = "renderer" } +iced_widget = { version = "0.1", path = "widget" } iced_winit = { version = "0.8", path = "winit", features = ["application"] } -thiserror = "1.0" +thiserror = "1" [dependencies.image_rs] version = "0.24" package = "image" optional = true -[dependencies.qrcode] -version = "0.12" -optional = true -default-features = false - [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] features = ["image", "svg", "canvas", "qr_code"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 7ccb7b7a..9edc20f6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/iced-rs/iced" [dependencies] bitflags = "1.2" thiserror = "1" +twox-hash = { version = "1.5", default-features = false } [dependencies.palette] version = "0.6" diff --git a/core/src/clipboard.rs b/core/src/clipboard.rs new file mode 100644 index 00000000..081b4004 --- /dev/null +++ b/core/src/clipboard.rs @@ -0,0 +1,23 @@ +//! Access the clipboard. + +/// A buffer for short-term storage and transfer within and between +/// applications. +pub trait Clipboard { + /// Reads the current content of the [`Clipboard`] as text. + fn read(&self) -> Option; + + /// Writes the given text contents to the [`Clipboard`]. + fn write(&mut self, contents: String); +} + +/// A null implementation of the [`Clipboard`] trait. +#[derive(Debug, Clone, Copy)] +pub struct Null; + +impl Clipboard for Null { + fn read(&self) -> Option { + None + } + + fn write(&mut self, _contents: String) {} +} diff --git a/core/src/element.rs b/core/src/element.rs new file mode 100644 index 00000000..98c53737 --- /dev/null +++ b/core/src/element.rs @@ -0,0 +1,608 @@ +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::widget::tree::{self, Tree}; +use crate::{ + Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, +}; + +use std::any::Any; +use std::borrow::Borrow; + +/// A generic [`Widget`]. +/// +/// It is useful to build composable user interfaces that do not leak +/// implementation details in their __view logic__. +/// +/// If you have a [built-in widget], you should be able to use `Into` +/// to turn it into an [`Element`]. +/// +/// [built-in widget]: crate::widget +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + widget: Box + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> { + /// Creates a new [`Element`] containing the given [`Widget`]. + pub fn new(widget: impl Widget + 'a) -> Self + where + Renderer: crate::Renderer, + { + Self { + widget: Box::new(widget), + } + } + + /// Returns a reference to the [`Widget`] of the [`Element`], + pub fn as_widget(&self) -> &dyn Widget { + self.widget.as_ref() + } + + /// Returns a mutable reference to the [`Widget`] of the [`Element`], + pub fn as_widget_mut(&mut self) -> &mut dyn Widget { + self.widget.as_mut() + } + + /// Applies a transformation to the produced message of the [`Element`]. + /// + /// This method is useful when you want to decouple different parts of your + /// UI and make them __composable__. + /// + /// # Example + /// Imagine we want to use [our counter](index.html#usage). But instead of + /// showing a single counter, we want to display many of them. We can reuse + /// the `Counter` type as it is! + /// + /// We use composition to model the __state__ of our new application: + /// + /// ``` + /// # mod counter { + /// # pub struct Counter; + /// # } + /// use counter::Counter; + /// + /// struct ManyCounters { + /// counters: Vec, + /// } + /// ``` + /// + /// We can store the state of multiple counters now. However, the + /// __messages__ we implemented before describe the user interactions + /// of a __single__ counter. Right now, we need to also identify which + /// counter is receiving user interactions. Can we use composition again? + /// Yes. + /// + /// ``` + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # } + /// #[derive(Debug, Clone, Copy)] + /// pub enum Message { + /// Counter(usize, counter::Message) + /// } + /// ``` + /// + /// We compose the previous __messages__ with the index of the counter + /// producing them. Let's implement our __view logic__ now: + /// + /// ```no_run + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn view( + /// # &self, + /// # ) -> iced_core::Element { + /// # unimplemented!() + /// # } + /// # } + /// # } + /// # + /// # mod iced { + /// # pub use iced_core::renderer::Null as Renderer; + /// # pub use iced_core::Element; + /// # + /// # pub mod widget { + /// # pub struct Row { + /// # _t: std::marker::PhantomData, + /// # } + /// # + /// # impl Row { + /// # pub fn new() -> Self { + /// # unimplemented!() + /// # } + /// # + /// # pub fn spacing(mut self, _: u32) -> Self { + /// # unimplemented!() + /// # } + /// # + /// # pub fn push( + /// # mut self, + /// # _: iced_core::Element, + /// # ) -> Self { + /// # unimplemented!() + /// # } + /// # } + /// # } + /// # } + /// # + /// use counter::Counter; + /// + /// use iced::widget::Row; + /// use iced::{Element, Renderer}; + /// + /// struct ManyCounters { + /// counters: Vec, + /// } + /// + /// #[derive(Debug, Clone, Copy)] + /// pub enum Message { + /// Counter(usize, counter::Message), + /// } + /// + /// impl ManyCounters { + /// pub fn view(&mut self) -> Row { + /// // We can quickly populate a `Row` by folding over our counters + /// self.counters.iter_mut().enumerate().fold( + /// Row::new().spacing(20), + /// |row, (index, counter)| { + /// // We display the counter + /// let element: Element = + /// counter.view().into(); + /// + /// row.push( + /// // Here we turn our `Element` into + /// // an `Element` by combining the `index` and the + /// // message of the `element`. + /// element + /// .map(move |message| Message::Counter(index, message)), + /// ) + /// }, + /// ) + /// } + /// } + /// ``` + /// + /// Finally, our __update logic__ is pretty straightforward: simple + /// delegation. + /// + /// ``` + /// # mod counter { + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message {} + /// # pub struct Counter; + /// # + /// # impl Counter { + /// # pub fn update(&mut self, _message: Message) {} + /// # } + /// # } + /// # + /// # use counter::Counter; + /// # + /// # struct ManyCounters { + /// # counters: Vec, + /// # } + /// # + /// # #[derive(Debug, Clone, Copy)] + /// # pub enum Message { + /// # Counter(usize, counter::Message) + /// # } + /// impl ManyCounters { + /// pub fn update(&mut self, message: Message) { + /// match message { + /// Message::Counter(index, counter_msg) => { + /// if let Some(counter) = self.counters.get_mut(index) { + /// counter.update(counter_msg); + /// } + /// } + /// } + /// } + /// } + /// ``` + pub fn map( + self, + f: impl Fn(Message) -> B + 'a, + ) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: crate::Renderer + 'a, + B: 'a, + { + Element::new(Map::new(self.widget, f)) + } + + /// Marks the [`Element`] as _to-be-explained_. + /// + /// The [`Renderer`] will explain the layout of the [`Element`] graphically. + /// This can be very useful for debugging your layout! + /// + /// [`Renderer`]: crate::Renderer + pub fn explain>( + self, + color: C, + ) -> Element<'a, Message, Renderer> + where + Message: 'static, + Renderer: crate::Renderer + 'a, + { + Element { + widget: Box::new(Explain::new(self, color.into())), + } + } +} + +impl<'a, Message, Renderer> Borrow + 'a> + for Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +impl<'a, Message, Renderer> Borrow + 'a> + for &Element<'a, Message, Renderer> +{ + fn borrow(&self) -> &(dyn Widget + 'a) { + self.widget.borrow() + } +} + +struct Map<'a, A, B, Renderer> { + widget: Box + 'a>, + mapper: Box B + 'a>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + widget: Box + 'a>, + mapper: F, + ) -> Map<'a, A, B, Renderer> + where + F: 'a + Fn(A) -> B, + { + Map { + widget, + mapper: Box::new(mapper), + } + } +} + +impl<'a, A, B, Renderer> Widget for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer + 'a, + A: 'a, + B: 'a, +{ + fn tag(&self) -> tree::Tag { + self.widget.tag() + } + + fn state(&self) -> tree::State { + self.widget.state() + } + + fn children(&self) -> Vec { + self.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.widget.diff(tree) + } + + fn width(&self) -> Length { + self.widget.width() + } + + fn height(&self) -> Length { + self.widget.height() + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.widget.layout(renderer, limits) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id); + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { + self.operation.custom(state, id); + } + } + + self.widget.operate( + tree, + layout, + renderer, + &mut MapOperation { operation }, + ); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, B>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let status = self.widget.on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ); + + shell.merge(local_shell, &self.mapper); + + status + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.widget.draw( + tree, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.widget.mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let mapper = &self.mapper; + + self.widget + .overlay(tree, layout, renderer) + .map(move |overlay| overlay.map(mapper)) + } +} + +struct Explain<'a, Message, Renderer: crate::Renderer> { + element: Element<'a, Message, Renderer>, + color: Color, +} + +impl<'a, Message, Renderer> Explain<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self { + Explain { element, color } + } +} + +impl<'a, Message, Renderer> Widget + for Explain<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + self.element.widget.width() + } + + fn height(&self) -> Length { + self.element.widget.height() + } + + fn tag(&self) -> tree::Tag { + self.element.widget.tag() + } + + fn state(&self) -> tree::State { + self.element.widget.state() + } + + fn children(&self) -> Vec { + self.element.widget.children() + } + + fn diff(&self, tree: &mut Tree) { + self.element.widget.diff(tree); + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.element.widget.layout(renderer, limits) + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.element + .widget + .operate(state, layout, renderer, operation) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.element.widget.on_event( + state, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + fn explain_layout( + renderer: &mut Renderer, + color: Color, + layout: Layout<'_>, + ) { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border_color: color, + border_width: 1.0, + border_radius: 0.0.into(), + }, + Color::TRANSPARENT, + ); + + for child in layout.children() { + explain_layout(renderer, color, child); + } + } + + self.element.widget.draw( + state, + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + + explain_layout(renderer, self.color, layout); + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.element.widget.mouse_interaction( + state, + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + self.element.widget.overlay(state, layout, renderer) + } +} diff --git a/core/src/event.rs b/core/src/event.rs new file mode 100644 index 00000000..953cd73f --- /dev/null +++ b/core/src/event.rs @@ -0,0 +1,78 @@ +//! Handle events of a user interface. +use crate::keyboard; +use crate::mouse; +use crate::touch; +use crate::window; + +/// A user interface event. +/// +/// _**Note:** This type is largely incomplete! If you need to track +/// additional events, feel free to [open an issue] and share your use case!_ +/// +/// [open an issue]: https://github.com/iced-rs/iced/issues +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// A keyboard event + Keyboard(keyboard::Event), + + /// A mouse event + Mouse(mouse::Event), + + /// A window event + Window(window::Event), + + /// A touch event + Touch(touch::Event), + + /// A platform specific event + PlatformSpecific(PlatformSpecific), +} + +/// A platform specific event +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlatformSpecific { + /// A MacOS specific event + MacOS(MacOS), +} + +/// Describes an event specific to MacOS +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacOS { + /// Triggered when the app receives an URL from the system + /// + /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ + /// + /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 + ReceivedUrl(String), +} + +/// The status of an [`Event`] after being processed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Event`] was **NOT** handled by any widget. + Ignored, + + /// The [`Event`] was handled and processed by a widget. + Captured, +} + +impl Status { + /// Merges two [`Status`] into one. + /// + /// `Captured` takes precedence over `Ignored`: + /// + /// ``` + /// use iced_core::event::Status; + /// + /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); + /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); + /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); + /// ``` + pub fn merge(self, b: Self) -> Self { + match self { + Status::Ignored => b, + Status::Captured => Status::Captured, + } + } +} diff --git a/core/src/hasher.rs b/core/src/hasher.rs new file mode 100644 index 00000000..fa52f16d --- /dev/null +++ b/core/src/hasher.rs @@ -0,0 +1,13 @@ +/// The hasher used to compare layouts. +#[derive(Debug, Default)] +pub struct Hasher(twox_hash::XxHash64); + +impl core::hash::Hasher for Hasher { + fn write(&mut self, bytes: &[u8]) { + self.0.write(bytes) + } + + fn finish(&self) -> u64 { + self.0.finish() + } +} diff --git a/core/src/image.rs b/core/src/image.rs new file mode 100644 index 00000000..70fbade0 --- /dev/null +++ b/core/src/image.rs @@ -0,0 +1,174 @@ +//! Load and draw raster graphics. +use crate::{Hasher, Rectangle, Size}; + +use std::hash::{Hash, Hasher as _}; +use std::path::PathBuf; +use std::sync::Arc; + +/// A handle of some image data. +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Data, +} + +impl Handle { + /// Creates an image [`Handle`] pointing to the image of the given path. + /// + /// Makes an educated guess about the image format by examining the data in the file. + pub fn from_path>(path: T) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + /// Creates an image [`Handle`] containing the image pixels directly. This + /// function expects the input data to be provided as a `Vec` of RGBA + /// pixels. + /// + /// This is useful if you have already decoded your image. + pub fn from_pixels( + width: u32, + height: u32, + pixels: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Handle { + Self::from_data(Data::Rgba { + width, + height, + pixels: Bytes::new(pixels), + }) + } + + /// Creates an image [`Handle`] containing the image data directly. + /// + /// Makes an educated guess about the image format by examining the given data. + /// + /// This is useful if you already have your image loaded in-memory, maybe + /// because you downloaded or generated it procedurally. + pub fn from_memory( + bytes: impl AsRef<[u8]> + Send + Sync + 'static, + ) -> Handle { + Self::from_data(Data::Bytes(Bytes::new(bytes))) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data, + } + } + + /// Returns the unique identifier of the [`Handle`]. + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the image [`Data`]. + pub fn data(&self) -> &Data { + &self.data + } +} + +impl From for Handle +where + T: Into, +{ + fn from(path: T) -> Handle { + Handle::from_path(path.into()) + } +} + +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// A wrapper around raw image data. +/// +/// It behaves like a `&[u8]`. +#[derive(Clone)] +pub struct Bytes(Arc + Send + Sync + 'static>); + +impl Bytes { + /// Creates new [`Bytes`] around `data`. + pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { + Self(Arc::new(data)) + } +} + +impl std::fmt::Debug for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.as_ref().as_ref().fmt(f) + } +} + +impl std::hash::Hash for Bytes { + fn hash(&self, state: &mut H) { + self.0.as_ref().as_ref().hash(state); + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref().as_ref() + } +} + +impl std::ops::Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + self.0.as_ref().as_ref() + } +} + +/// The data of a raster image. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + Bytes(Bytes), + + /// Decoded image pixels in RGBA format. + Rgba { + /// The width of the image. + width: u32, + /// The height of the image. + height: u32, + /// The pixels. + pixels: Bytes, + }, +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({path:?})"), + Data::Bytes(_) => write!(f, "Bytes(...)"), + Data::Rgba { width, height, .. } => { + write!(f, "Pixels({width} * {height})") + } + } + } +} + +/// A [`Renderer`] that can render raster graphics. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer { + /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] + /// + /// [`Handle`]: Self::Handle + type Handle: Clone + Hash; + + /// Returns the dimensions of an image for the given [`Handle`]. + fn dimensions(&self, handle: &Self::Handle) -> Size; + + /// Draws an image with the given [`Handle`] and inside the provided + /// `bounds`. + fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); +} diff --git a/core/src/layout.rs b/core/src/layout.rs new file mode 100644 index 00000000..04954fb9 --- /dev/null +++ b/core/src/layout.rs @@ -0,0 +1,65 @@ +//! Position your widgets properly. +mod limits; +mod node; + +pub mod flex; + +pub use limits::Limits; +pub use node::Node; + +use crate::{Point, Rectangle, Vector}; + +/// The bounds of a [`Node`] and its children, using absolute coordinates. +#[derive(Debug, Clone, Copy)] +pub struct Layout<'a> { + position: Point, + node: &'a Node, +} + +impl<'a> Layout<'a> { + /// Creates a new [`Layout`] for the given [`Node`] at the origin. + pub fn new(node: &'a Node) -> Self { + Self::with_offset(Vector::new(0.0, 0.0), node) + } + + /// Creates a new [`Layout`] for the given [`Node`] with the provided offset + /// from the origin. + pub fn with_offset(offset: Vector, node: &'a Node) -> Self { + let bounds = node.bounds(); + + Self { + position: Point::new(bounds.x, bounds.y) + offset, + node, + } + } + + /// Returns the position of the [`Layout`]. + pub fn position(&self) -> Point { + self.position + } + + /// Returns the bounds of the [`Layout`]. + /// + /// The returned [`Rectangle`] describes the position and size of a + /// [`Node`]. + pub fn bounds(&self) -> Rectangle { + let bounds = self.node.bounds(); + + Rectangle { + x: self.position.x, + y: self.position.y, + width: bounds.width, + height: bounds.height, + } + } + + /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. + pub fn children(self) -> impl Iterator> { + self.node.children().iter().map(move |node| { + Layout::with_offset( + Vector::new(self.position.x, self.position.y), + node, + ) + }) + } +} diff --git a/core/src/layout/DRUID_LICENSE b/core/src/layout/DRUID_LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/core/src/layout/DRUID_LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs new file mode 100644 index 00000000..5d70c2fc --- /dev/null +++ b/core/src/layout/flex.rs @@ -0,0 +1,232 @@ +//! Distribute elements using a flex-based layout. +// This code is heavily inspired by the [`druid`] codebase. +// +// [`druid`]: https://github.com/xi-editor/druid +// +// Copyright 2018 The xi-editor Authors, Héctor Ramón +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::Element; + +use crate::layout::{Limits, Node}; +use crate::{Alignment, Padding, Point, Size}; + +/// The main axis of a flex layout. +#[derive(Debug)] +pub enum Axis { + /// The horizontal axis + Horizontal, + + /// The vertical axis + Vertical, +} + +impl Axis { + fn main(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.width, + Axis::Vertical => size.height, + } + } + + fn cross(&self, size: Size) -> f32 { + match self { + Axis::Horizontal => size.height, + Axis::Vertical => size.width, + } + } + + fn pack(&self, main: f32, cross: f32) -> (f32, f32) { + match self { + Axis::Horizontal => (main, cross), + Axis::Vertical => (cross, main), + } + } +} + +/// Computes the flex layout with the given axis and limits, applying spacing, +/// padding and alignment to the items as needed. +/// +/// It returns a new layout [`Node`]. +pub fn resolve( + axis: Axis, + renderer: &Renderer, + limits: &Limits, + padding: Padding, + spacing: f32, + align_items: Alignment, + items: &[Element<'_, Message, Renderer>], +) -> Node +where + Renderer: crate::Renderer, +{ + let limits = limits.pad(padding); + let total_spacing = spacing * items.len().saturating_sub(1) as f32; + let max_cross = axis.cross(limits.max()); + + let mut fill_sum = 0; + let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); + let mut available = axis.main(limits.max()) - total_spacing; + + let mut nodes: Vec = Vec::with_capacity(items.len()); + nodes.resize(items.len(), Node::default()); + + if align_items == Alignment::Fill { + let mut fill_cross = axis.cross(limits.min()); + + items.iter().for_each(|child| { + let cross_fill_factor = match axis { + Axis::Horizontal => child.as_widget().height(), + Axis::Vertical => child.as_widget().width(), + } + .fill_factor(); + + if cross_fill_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = + Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = child.as_widget().layout(renderer, &child_limits); + let size = layout.size(); + + fill_cross = fill_cross.max(axis.cross(size)); + } + }); + + cross = fill_cross; + } + + for (i, child) in items.iter().enumerate() { + let fill_factor = match axis { + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), + } + .fill_factor(); + + if fill_factor == 0 { + let (min_width, min_height) = if align_items == Alignment::Fill { + axis.pack(0.0, cross) + } else { + axis.pack(0.0, 0.0) + }; + + let (max_width, max_height) = if align_items == Alignment::Fill { + axis.pack(available, cross) + } else { + axis.pack(available, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.as_widget().layout(renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + + if align_items != Alignment::Fill { + cross = cross.max(axis.cross(size)); + } + + nodes[i] = layout; + } else { + fill_sum += fill_factor; + } + } + + let remaining = available.max(0.0); + + for (i, child) in items.iter().enumerate() { + let fill_factor = match axis { + Axis::Horizontal => child.as_widget().width(), + Axis::Vertical => child.as_widget().height(), + } + .fill_factor(); + + if fill_factor != 0 { + let max_main = remaining * fill_factor as f32 / fill_sum as f32; + let min_main = if max_main.is_infinite() { + 0.0 + } else { + max_main + }; + + let (min_width, min_height) = if align_items == Alignment::Fill { + axis.pack(min_main, cross) + } else { + axis.pack(min_main, axis.cross(limits.min())) + }; + + let (max_width, max_height) = if align_items == Alignment::Fill { + axis.pack(max_main, cross) + } else { + axis.pack(max_main, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.as_widget().layout(renderer, &child_limits); + + if align_items != Alignment::Fill { + cross = cross.max(axis.cross(layout.size())); + } + + nodes[i] = layout; + } + } + + let pad = axis.pack(padding.left, padding.top); + let mut main = pad.0; + + for (i, node) in nodes.iter_mut().enumerate() { + if i > 0 { + main += spacing; + } + + let (x, y) = axis.pack(main, pad.1); + + node.move_to(Point::new(x, y)); + + match axis { + Axis::Horizontal => { + node.align( + Alignment::Start, + align_items, + Size::new(0.0, cross), + ); + } + Axis::Vertical => { + node.align( + align_items, + Alignment::Start, + Size::new(cross, 0.0), + ); + } + } + + let size = node.size(); + + main += axis.main(size); + } + + let (width, height) = axis.pack(main - pad.0, cross); + let size = limits.resolve(Size::new(width, height)); + + Node::with_children(size.pad(padding), nodes) +} diff --git a/core/src/layout/limits.rs b/core/src/layout/limits.rs new file mode 100644 index 00000000..5d3c1556 --- /dev/null +++ b/core/src/layout/limits.rs @@ -0,0 +1,163 @@ +#![allow(clippy::manual_clamp)] +use crate::{Length, Padding, Size}; + +/// A set of size constraints for layouting. +#[derive(Debug, Clone, Copy)] +pub struct Limits { + min: Size, + max: Size, + fill: Size, +} + +impl Limits { + /// No limits + pub const NONE: Limits = Limits { + min: Size::ZERO, + max: Size::INFINITY, + fill: Size::INFINITY, + }; + + /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. + pub const fn new(min: Size, max: Size) -> Limits { + Limits { + min, + max, + fill: Size::INFINITY, + } + } + + /// Returns the minimum [`Size`] of the [`Limits`]. + pub fn min(&self) -> Size { + self.min + } + + /// Returns the maximum [`Size`] of the [`Limits`]. + pub fn max(&self) -> Size { + self.max + } + + /// Returns the fill [`Size`] of the [`Limits`]. + pub fn fill(&self) -> Size { + self.fill + } + + /// Applies a width constraint to the current [`Limits`]. + pub fn width(mut self, width: impl Into) -> Limits { + match width.into() { + Length::Shrink => { + self.fill.width = self.min.width; + } + Length::Fill | Length::FillPortion(_) => { + self.fill.width = self.fill.width.min(self.max.width); + } + Length::Fixed(amount) => { + let new_width = amount.min(self.max.width).max(self.min.width); + + self.min.width = new_width; + self.max.width = new_width; + self.fill.width = new_width; + } + } + + self + } + + /// Applies a height constraint to the current [`Limits`]. + pub fn height(mut self, height: impl Into) -> Limits { + match height.into() { + Length::Shrink => { + self.fill.height = self.min.height; + } + Length::Fill | Length::FillPortion(_) => { + self.fill.height = self.fill.height.min(self.max.height); + } + Length::Fixed(amount) => { + let new_height = + amount.min(self.max.height).max(self.min.height); + + self.min.height = new_height; + self.max.height = new_height; + self.fill.height = new_height; + } + } + + self + } + + /// Applies a minimum width constraint to the current [`Limits`]. + pub fn min_width(mut self, min_width: f32) -> Limits { + self.min.width = self.min.width.max(min_width).min(self.max.width); + + self + } + + /// Applies a maximum width constraint to the current [`Limits`]. + pub fn max_width(mut self, max_width: f32) -> Limits { + self.max.width = self.max.width.min(max_width).max(self.min.width); + + self + } + + /// Applies a minimum height constraint to the current [`Limits`]. + pub fn min_height(mut self, min_height: f32) -> Limits { + self.min.height = self.min.height.max(min_height).min(self.max.height); + + self + } + + /// Applies a maximum height constraint to the current [`Limits`]. + pub fn max_height(mut self, max_height: f32) -> Limits { + self.max.height = self.max.height.min(max_height).max(self.min.height); + + self + } + + /// Shrinks the current [`Limits`] to account for the given padding. + pub fn pad(&self, padding: Padding) -> Limits { + self.shrink(Size::new(padding.horizontal(), padding.vertical())) + } + + /// Shrinks the current [`Limits`] by the given [`Size`]. + pub fn shrink(&self, size: Size) -> Limits { + let min = Size::new( + (self.min().width - size.width).max(0.0), + (self.min().height - size.height).max(0.0), + ); + + let max = Size::new( + (self.max().width - size.width).max(0.0), + (self.max().height - size.height).max(0.0), + ); + + let fill = Size::new( + (self.fill.width - size.width).max(0.0), + (self.fill.height - size.height).max(0.0), + ); + + Limits { min, max, fill } + } + + /// Removes the minimum width constraint for the current [`Limits`]. + pub fn loose(&self) -> Limits { + Limits { + min: Size::ZERO, + max: self.max, + fill: self.fill, + } + } + + /// Computes the resulting [`Size`] that fits the [`Limits`] given the + /// intrinsic size of some content. + pub fn resolve(&self, intrinsic_size: Size) -> Size { + Size::new( + intrinsic_size + .width + .min(self.max.width) + .max(self.fill.width), + intrinsic_size + .height + .min(self.max.height) + .max(self.fill.height), + ) + } +} diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs new file mode 100644 index 00000000..e0c7dcb2 --- /dev/null +++ b/core/src/layout/node.rs @@ -0,0 +1,91 @@ +use crate::{Alignment, Point, Rectangle, Size, Vector}; + +/// The bounds of an element and its children. +#[derive(Debug, Clone, Default)] +pub struct Node { + bounds: Rectangle, + children: Vec, +} + +impl Node { + /// Creates a new [`Node`] with the given [`Size`]. + pub const fn new(size: Size) -> Self { + Self::with_children(size, Vec::new()) + } + + /// Creates a new [`Node`] with the given [`Size`] and children. + pub const fn with_children(size: Size, children: Vec) -> Self { + Node { + bounds: Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }, + children, + } + } + + /// Returns the [`Size`] of the [`Node`]. + pub fn size(&self) -> Size { + Size::new(self.bounds.width, self.bounds.height) + } + + /// Returns the bounds of the [`Node`]. + pub fn bounds(&self) -> Rectangle { + self.bounds + } + + /// Returns the children of the [`Node`]. + pub fn children(&self) -> &[Node] { + &self.children + } + + /// Aligns the [`Node`] in the given space. + pub fn align( + &mut self, + horizontal_alignment: Alignment, + vertical_alignment: Alignment, + space: Size, + ) { + match horizontal_alignment { + Alignment::Start => {} + Alignment::Center => { + self.bounds.x += (space.width - self.bounds.width) / 2.0; + } + Alignment::End => { + self.bounds.x += space.width - self.bounds.width; + } + Alignment::Fill => { + self.bounds.width = space.width; + } + } + + match vertical_alignment { + Alignment::Start => {} + Alignment::Center => { + self.bounds.y += (space.height - self.bounds.height) / 2.0; + } + Alignment::End => { + self.bounds.y += space.height - self.bounds.height; + } + Alignment::Fill => { + self.bounds.height = space.height; + } + } + } + + /// Moves the [`Node`] to the given position. + pub fn move_to(&mut self, position: Point) { + self.bounds.x = position.x; + self.bounds.y = position.y; + } + + /// Translates the [`Node`] by the given translation. + pub fn translate(self, translation: Vector) -> Self { + Self { + bounds: self.bounds + translation, + ..self + } + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 1e4f0411..5bdcee6a 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,33 +25,57 @@ #![forbid(unsafe_code, rust_2018_idioms)] #![allow(clippy::inherent_to_string, clippy::type_complexity)] pub mod alignment; +pub mod clipboard; +pub mod event; pub mod font; pub mod gradient; +pub mod image; pub mod keyboard; +pub mod layout; pub mod mouse; +pub mod overlay; +pub mod renderer; +pub mod svg; +pub mod text; pub mod time; +pub mod touch; +pub mod widget; +pub mod window; mod background; mod color; mod content_fit; +mod element; +mod hasher; mod length; mod padding; mod pixels; mod point; mod rectangle; +mod shell; mod size; mod vector; pub use alignment::Alignment; pub use background::Background; +pub use clipboard::Clipboard; pub use color::Color; pub use content_fit::ContentFit; +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; pub use padding::Padding; pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; +pub use renderer::Renderer; +pub use shell::Shell; pub use size::Size; +pub use text::Text; pub use vector::Vector; +pub use widget::Widget; diff --git a/core/src/mouse.rs b/core/src/mouse.rs index 48214f65..0c405ce6 100644 --- a/core/src/mouse.rs +++ b/core/src/mouse.rs @@ -1,8 +1,11 @@ //! Handle mouse events. +pub mod click; + mod button; mod event; mod interaction; pub use button::Button; +pub use click::Click; pub use event::{Event, ScrollDelta}; pub use interaction::Interaction; diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs new file mode 100644 index 00000000..4a7d796c --- /dev/null +++ b/core/src/mouse/click.rs @@ -0,0 +1,76 @@ +//! Track mouse clicks. +use crate::time::Instant; +use crate::Point; + +/// A mouse click. +#[derive(Debug, Clone, Copy)] +pub struct Click { + kind: Kind, + position: Point, + time: Instant, +} + +/// The kind of mouse click. +#[derive(Debug, Clone, Copy)] +pub enum Kind { + /// A single click + Single, + + /// A double click + Double, + + /// A triple click + Triple, +} + +impl Kind { + fn next(&self) -> Kind { + match self { + Kind::Single => Kind::Double, + Kind::Double => Kind::Triple, + Kind::Triple => Kind::Double, + } + } +} + +impl Click { + /// Creates a new [`Click`] with the given position and previous last + /// [`Click`]. + pub fn new(position: Point, previous: Option) -> Click { + let time = Instant::now(); + + let kind = if let Some(previous) = previous { + if previous.is_consecutive(position, time) { + previous.kind.next() + } else { + Kind::Single + } + } else { + Kind::Single + }; + + Click { + kind, + position, + time, + } + } + + /// Returns the [`Kind`] of [`Click`]. + pub fn kind(&self) -> Kind { + self.kind + } + + fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { + let duration = if time > self.time { + Some(time - self.time) + } else { + None + }; + + self.position == new_position + && duration + .map(|duration| duration.as_millis() <= 300) + .unwrap_or(false) + } +} diff --git a/core/src/overlay.rs b/core/src/overlay.rs new file mode 100644 index 00000000..b9f3c735 --- /dev/null +++ b/core/src/overlay.rs @@ -0,0 +1,122 @@ +//! Display interactive elements on top of other widgets. +mod element; +mod group; + +pub use element::Element; +pub use group::Group; + +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::renderer; +use crate::widget; +use crate::widget::Tree; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; + +/// An interactive component that can be displayed on top of other widgets. +pub trait Overlay +where + Renderer: crate::Renderer, +{ + /// Returns the layout [`Node`] of the [`Overlay`]. + /// + /// This [`Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + /// + /// [`Node`]: layout::Node + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node; + + /// Draws the [`Overlay`] using the associated `Renderer`. + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ); + + /// Applies a [`widget::Operation`] to the [`Overlay`]. + fn operate( + &mut self, + _layout: Layout<'_>, + _renderer: &Renderer, + _operation: &mut dyn widget::Operation, + ) { + } + + /// Processes a runtime [`Event`]. + /// + /// It receives: + /// * an [`Event`] describing user interaction + /// * the computed [`Layout`] of the [`Overlay`] + /// * the current cursor position + /// * a mutable `Message` list, allowing the [`Overlay`] to produce + /// new messages based on user interaction. + /// * the `Renderer` + /// * a [`Clipboard`], if available + /// + /// By default, it does nothing. + fn on_event( + &mut self, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + event::Status::Ignored + } + + /// Returns the current [`mouse::Interaction`] of the [`Overlay`]. + /// + /// By default, it returns [`mouse::Interaction::Idle`]. + fn mouse_interaction( + &self, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } + + /// Returns true if the cursor is over the [`Overlay`]. + /// + /// By default, it returns true if the bounds of the `layout` contain + /// the `cursor_position`. + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + layout.bounds().contains(cursor_position) + } +} + +/// Returns a [`Group`] of overlay [`Element`] children. +/// +/// This method will generally only be used by advanced users that are +/// implementing the [`Widget`](crate::Widget) trait. +pub fn from_children<'a, Message, Renderer>( + children: &'a mut [crate::Element<'_, Message, Renderer>], + tree: &'a mut Tree, + layout: Layout<'_>, + renderer: &Renderer, +) -> Option> +where + Renderer: crate::Renderer, +{ + let children = children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .filter_map(|((child, state), layout)| { + child.as_widget_mut().overlay(state, layout, renderer) + }) + .collect::>(); + + (!children.is_empty()).then(|| Group::with_children(children).overlay()) +} diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs new file mode 100644 index 00000000..237d25d1 --- /dev/null +++ b/core/src/overlay/element.rs @@ -0,0 +1,270 @@ +pub use crate::Overlay; + +use crate::event::{self, Event}; +use crate::layout; +use crate::mouse; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; + +use std::any::Any; + +/// A generic [`Overlay`]. +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { + position: Point, + overlay: Box + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + /// Creates a new [`Element`] containing the given [`Overlay`]. + pub fn new( + position: Point, + overlay: Box + 'a>, + ) -> Self { + Self { position, overlay } + } + + /// Returns the position of the [`Element`]. + pub fn position(&self) -> Point { + self.position + } + + /// Translates the [`Element`]. + pub fn translate(mut self, translation: Vector) -> Self { + self.position = self.position + translation; + self + } + + /// Applies a transformation to the produced message of the [`Element`]. + pub fn map(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> + where + Message: 'a, + Renderer: 'a, + B: 'a, + { + Element { + position: self.position, + overlay: Box::new(Map::new(self.overlay, f)), + } + } + + /// Computes the layout of the [`Element`] in the given bounds. + pub fn layout( + &self, + renderer: &Renderer, + bounds: Size, + translation: Vector, + ) -> layout::Node { + self.overlay + .layout(renderer, bounds, self.position + translation) + } + + /// Processes a runtime [`Event`]. + pub fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.overlay.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + /// Returns the current [`mouse::Interaction`] of the [`Element`]. + pub fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.overlay.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + } + + /// Draws the [`Element`] and its children using the given [`Layout`]. + pub fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.overlay + .draw(renderer, theme, style, layout, cursor_position) + } + + /// Applies a [`widget::Operation`] to the [`Element`]. + pub fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.overlay.operate(layout, renderer, operation); + } + + /// Returns true if the cursor is over the [`Element`]. + pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.overlay.is_over(layout, cursor_position) + } +} + +struct Map<'a, A, B, Renderer> { + content: Box + 'a>, + mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { + pub fn new( + content: Box + 'a>, + mapper: &'a dyn Fn(A) -> B, + ) -> Map<'a, A, B, Renderer> { + Map { content, mapper } + } +} + +impl<'a, A, B, Renderer> Overlay for Map<'a, A, B, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + self.content.layout(renderer, bounds, position) + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + struct MapOperation<'a, B> { + operation: &'a mut dyn widget::Operation, + } + + impl<'a, T, B> widget::Operation for MapOperation<'a, B> { + fn container( + &mut self, + id: Option<&widget::Id>, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation, + ), + ) { + self.operation.container(id, &mut |operation| { + operate_on_children(&mut MapOperation { operation }); + }); + } + + fn focusable( + &mut self, + state: &mut dyn widget::operation::Focusable, + id: Option<&widget::Id>, + ) { + self.operation.focusable(state, id); + } + + fn scrollable( + &mut self, + state: &mut dyn widget::operation::Scrollable, + id: Option<&widget::Id>, + ) { + self.operation.scrollable(state, id); + } + + fn text_input( + &mut self, + state: &mut dyn widget::operation::TextInput, + id: Option<&widget::Id>, + ) { + self.operation.text_input(state, id) + } + + fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { + self.operation.custom(state, id); + } + } + + self.content + .operate(layout, renderer, &mut MapOperation { operation }); + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, B>, + ) -> event::Status { + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + let event_status = self.content.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + &mut local_shell, + ); + + shell.merge(local_shell, self.mapper); + + event_status + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + self.content + .draw(renderer, theme, style, layout, cursor_position) + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.content.is_over(layout, cursor_position) + } +} diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs new file mode 100644 index 00000000..0c48df34 --- /dev/null +++ b/core/src/overlay/group.rs @@ -0,0 +1,172 @@ +use crate::event; +use crate::layout; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::widget; +use crate::{Clipboard, Event, Layout, Overlay, Point, Rectangle, Shell, Size}; + +/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] +/// children. +#[allow(missing_debug_implementations)] +pub struct Group<'a, Message, Renderer> { + children: Vec>, +} + +impl<'a, Message, Renderer> Group<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + /// Creates an empty [`Group`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates a [`Group`] with the given elements. + pub fn with_children( + children: Vec>, + ) -> Self { + Group { children } + } + + /// Adds an [`overlay::Element`] to the [`Group`]. + pub fn push( + mut self, + child: impl Into>, + ) -> Self { + self.children.push(child.into()); + self + } + + /// Turns the [`Group`] into an overlay [`overlay::Element`]. + pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> { + overlay::Element::new(Point::ORIGIN, Box::new(self)) + } +} + +impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + fn default() -> Self { + Self::with_children(Vec::new()) + } +} + +impl<'a, Message, Renderer> Overlay + for Group<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn layout( + &self, + renderer: &Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { + let translation = position - Point::ORIGIN; + + layout::Node::with_children( + bounds, + self.children + .iter() + .map(|child| child.layout(renderer, bounds, translation)) + .collect(), + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.children + .iter_mut() + .zip(layout.children()) + .map(|(child, layout)| { + child.on_event( + event.clone(), + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + for (child, layout) in self.children.iter().zip(layout.children()) { + child.draw(renderer, theme, style, layout, cursor_position); + } + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(layout.children()) + .map(|(child, layout)| { + child.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + operation.container(None, &mut |operation| { + self.children.iter_mut().zip(layout.children()).for_each( + |(child, layout)| { + child.operate(layout, renderer, operation); + }, + ) + }); + } + + fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { + self.children + .iter() + .zip(layout.children()) + .any(|(child, layout)| child.is_over(layout, cursor_position)) + } +} + +impl<'a, Message, Renderer> From> + for overlay::Element<'a, Message, Renderer> +where + Renderer: 'a + crate::Renderer, + Message: 'a, +{ + fn from(group: Group<'a, Message, Renderer>) -> Self { + group.overlay() + } +} diff --git a/core/src/renderer.rs b/core/src/renderer.rs new file mode 100644 index 00000000..d6247e39 --- /dev/null +++ b/core/src/renderer.rs @@ -0,0 +1,99 @@ +//! Write your own renderer. +#[cfg(debug_assertions)] +mod null; + +#[cfg(debug_assertions)] +pub use null::Null; + +use crate::layout; +use crate::{Background, Color, Element, Rectangle, Vector}; + +/// A component that can be used by widgets to draw themselves on a screen. +pub trait Renderer: Sized { + /// The supported theme of the [`Renderer`]. + type Theme; + + /// Lays out the elements of a user interface. + /// + /// You should override this if you need to perform any operations before or + /// after layouting. For instance, trimming the measurements cache. + fn layout( + &mut self, + element: &Element<'_, Message, Self>, + limits: &layout::Limits, + ) -> layout::Node { + element.as_widget().layout(self, limits) + } + + /// 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)); + + /// Applies a `translation` to the primitives recorded in the given closure. + fn with_translation( + &mut self, + translation: Vector, + f: impl FnOnce(&mut Self), + ); + + /// Fills a [`Quad`] with the provided [`Background`]. + fn fill_quad(&mut self, quad: Quad, background: impl Into); + + /// Clears all of the recorded primitives in the [`Renderer`]. + fn clear(&mut self); +} + +/// A polygon with four sides. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Quad { + /// The bounds of the [`Quad`]. + pub bounds: Rectangle, + + /// The border radius of the [`Quad`]. + pub border_radius: BorderRadius, + + /// The border width of the [`Quad`]. + pub border_width: f32, + + /// The border color of the [`Quad`]. + pub border_color: Color, +} + +/// The border radi for the corners of a graphics primitive in the order: +/// top-left, top-right, bottom-right, bottom-left. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct BorderRadius([f32; 4]); + +impl From for BorderRadius { + fn from(w: f32) -> Self { + Self([w; 4]) + } +} + +impl From<[f32; 4]> for BorderRadius { + fn from(radi: [f32; 4]) -> Self { + Self(radi) + } +} + +impl From for [f32; 4] { + fn from(radi: BorderRadius) -> Self { + radi.0 + } +} + +/// The styling attributes of a [`Renderer`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// The text color + pub text_color: Color, +} + +impl Default for Style { + fn default() -> Self { + Style { + text_color: Color::BLACK, + } + } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs new file mode 100644 index 00000000..d93338ae --- /dev/null +++ b/core/src/renderer/null.rs @@ -0,0 +1,82 @@ +use crate::renderer::{self, Renderer}; +use crate::text::{self, Text}; +use crate::{Background, Font, Point, Rectangle, Size, Vector}; + +use std::borrow::Cow; + +/// A renderer that does nothing. +/// +/// It can be useful if you are writing tests! +#[derive(Debug, Clone, Copy, Default)] +pub struct Null; + +impl Null { + /// Creates a new [`Null`] renderer. + pub fn new() -> Null { + Null + } +} + +impl Renderer for Null { + type Theme = (); + + fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} + + fn with_translation( + &mut self, + _translation: Vector, + _f: impl FnOnce(&mut Self), + ) { + } + + fn clear(&mut self) {} + + fn fill_quad( + &mut self, + _quad: renderer::Quad, + _background: impl Into, + ) { + } +} + +impl text::Renderer for Null { + type Font = Font; + + const ICON_FONT: Font = Font::SansSerif; + const CHECKMARK_ICON: char = '0'; + const ARROW_DOWN_ICON: char = '0'; + + fn default_font(&self) -> Self::Font { + Font::SansSerif + } + + fn default_size(&self) -> f32 { + 16.0 + } + + fn load_font(&mut self, _font: Cow<'static, [u8]>) {} + + fn measure( + &self, + _content: &str, + _size: f32, + _font: Font, + _bounds: Size, + ) -> (f32, f32) { + (0.0, 20.0) + } + + fn hit_test( + &self, + _contents: &str, + _size: f32, + _font: Self::Font, + _bounds: Size, + _point: Point, + _nearest_only: bool, + ) -> Option { + None + } + + fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} +} diff --git a/core/src/shell.rs b/core/src/shell.rs new file mode 100644 index 00000000..74a5c616 --- /dev/null +++ b/core/src/shell.rs @@ -0,0 +1,108 @@ +use crate::window; + +/// A connection to the state of a shell. +/// +/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application, +/// like publishing messages or invalidating the current layout. +/// +/// [`Widget`]: crate::Widget +#[derive(Debug)] +pub struct Shell<'a, Message> { + messages: &'a mut Vec, + redraw_request: Option, + is_layout_invalid: bool, + are_widgets_invalid: bool, +} + +impl<'a, Message> Shell<'a, Message> { + /// Creates a new [`Shell`] with the provided buffer of messages. + pub fn new(messages: &'a mut Vec) -> Self { + Self { + messages, + redraw_request: None, + is_layout_invalid: false, + are_widgets_invalid: false, + } + } + + /// Returns true if the [`Shell`] contains no published messages + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + /// Publish the given `Message` for an application to process it. + pub fn publish(&mut self, message: Message) { + self.messages.push(message); + } + + /// Requests a new frame to be drawn at the given [`Instant`]. + pub fn request_redraw(&mut self, request: window::RedrawRequest) { + match self.redraw_request { + None => { + self.redraw_request = Some(request); + } + Some(current) if request < current => { + self.redraw_request = Some(request); + } + _ => {} + } + } + + /// Returns the requested [`Instant`] a redraw should happen, if any. + pub fn redraw_request(&self) -> Option { + self.redraw_request + } + + /// Returns whether the current layout is invalid or not. + pub fn is_layout_invalid(&self) -> bool { + self.is_layout_invalid + } + + /// Invalidates the current application layout. + /// + /// The shell will relayout the application widgets. + pub fn invalidate_layout(&mut self) { + self.is_layout_invalid = true; + } + + /// Triggers the given function if the layout is invalid, cleaning it in the + /// process. + pub fn revalidate_layout(&mut self, f: impl FnOnce()) { + if self.is_layout_invalid { + self.is_layout_invalid = false; + + f() + } + } + + /// Returns whether the widgets of the current application have been + /// invalidated. + pub fn are_widgets_invalid(&self) -> bool { + self.are_widgets_invalid + } + + /// Invalidates the current application widgets. + /// + /// The shell will rebuild and relayout the widget tree. + pub fn invalidate_widgets(&mut self) { + self.are_widgets_invalid = true; + } + + /// Merges the current [`Shell`] with another one by applying the given + /// function to the messages of the latter. + /// + /// This method is useful for composition. + pub fn merge(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) { + self.messages.extend(other.messages.drain(..).map(f)); + + if let Some(at) = other.redraw_request { + self.request_redraw(at); + } + + self.is_layout_invalid = + self.is_layout_invalid || other.is_layout_invalid; + + self.are_widgets_invalid = + self.are_widgets_invalid || other.are_widgets_invalid; + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs new file mode 100644 index 00000000..9b98877a --- /dev/null +++ b/core/src/svg.rs @@ -0,0 +1,89 @@ +//! Load and draw vector graphics. +use crate::{Color, Hasher, Rectangle, Size}; + +use std::borrow::Cow; +use std::hash::{Hash, Hasher as _}; +use std::path::PathBuf; +use std::sync::Arc; + +/// A handle of Svg data. +#[derive(Debug, Clone)] +pub struct Handle { + id: u64, + data: Arc, +} + +impl Handle { + /// Creates an SVG [`Handle`] pointing to the vector image of the given + /// path. + pub fn from_path(path: impl Into) -> Handle { + Self::from_data(Data::Path(path.into())) + } + + /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string + /// or gzip compressed data. + /// + /// This is useful if you already have your SVG data in-memory, maybe + /// because you downloaded or generated it procedurally. + pub fn from_memory(bytes: impl Into>) -> Handle { + Self::from_data(Data::Bytes(bytes.into())) + } + + fn from_data(data: Data) -> Handle { + let mut hasher = Hasher::default(); + data.hash(&mut hasher); + + Handle { + id: hasher.finish(), + data: Arc::new(data), + } + } + + /// Returns the unique identifier of the [`Handle`]. + pub fn id(&self) -> u64 { + self.id + } + + /// Returns a reference to the SVG [`Data`]. + pub fn data(&self) -> &Data { + &self.data + } +} + +impl Hash for Handle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// The data of a vectorial image. +#[derive(Clone, Hash)] +pub enum Data { + /// File data + Path(PathBuf), + + /// In-memory data + /// + /// Can contain an SVG string or a gzip compressed data. + Bytes(Cow<'static, [u8]>), +} + +impl std::fmt::Debug for Data { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Data::Path(path) => write!(f, "Path({path:?})"), + Data::Bytes(_) => write!(f, "Bytes(...)"), + } + } +} + +/// A [`Renderer`] that can render vector graphics. +/// +/// [renderer]: crate::renderer +pub trait Renderer: crate::Renderer { + /// Returns the default dimensions of an SVG for the given [`Handle`]. + fn dimensions(&self, handle: &Handle) -> Size; + + /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. + fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); +} diff --git a/core/src/text.rs b/core/src/text.rs new file mode 100644 index 00000000..4c72abc3 --- /dev/null +++ b/core/src/text.rs @@ -0,0 +1,111 @@ +//! Draw and interact with text. +use crate::alignment; +use crate::{Color, Point, Rectangle, Size}; + +use std::borrow::Cow; + +/// A paragraph. +#[derive(Debug, Clone, Copy)] +pub struct Text<'a, Font> { + /// The content of the paragraph. + pub content: &'a str, + + /// The bounds of the paragraph. + pub bounds: Rectangle, + + /// The size of the [`Text`]. + pub size: f32, + + /// The color of the [`Text`]. + pub color: Color, + + /// 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 result of hit testing on text. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Hit { + /// The point was within the bounds of the returned character index. + CharOffset(usize), +} + +impl Hit { + /// Computes the cursor position of the [`Hit`] . + pub fn cursor(self) -> usize { + match self { + Self::CharOffset(i) => i, + } + } +} + +/// A renderer capable of measuring and drawing [`Text`]. +pub trait Renderer: crate::Renderer { + /// The font type used. + type Font: Copy; + + /// The icon font of the backend. + const ICON_FONT: Self::Font; + + /// The `char` representing a ✔ icon in the [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: Self::ICON_FONT + const CHECKMARK_ICON: char; + + /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`]. + /// + /// [`ICON_FONT`]: Self::ICON_FONT + const ARROW_DOWN_ICON: char; + + /// Returns the default [`Self::Font`]. + fn default_font(&self) -> Self::Font; + + /// Returns the default size of [`Text`]. + fn default_size(&self) -> f32; + + /// Measures the text in the given bounds and returns the minimum boundaries + /// that can fit the contents. + fn measure( + &self, + content: &str, + size: f32, + font: Self::Font, + bounds: Size, + ) -> (f32, f32); + + /// Measures the width of the text as if it were laid out in a single line. + fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 { + let (width, _) = self.measure(content, size, font, Size::INFINITY); + + width + } + + /// Tests whether the provided point is within the boundaries of text + /// laid out with the given parameters, returning information about + /// the nearest character. + /// + /// If `nearest_only` is true, the hit test does not consider whether the + /// the point is interior to any glyph bounds, returning only the character + /// with the nearest centeroid. + fn hit_test( + &self, + contents: &str, + size: f32, + font: Self::Font, + bounds: Size, + point: Point, + nearest_only: bool, + ) -> Option; + + /// Loads a [`Self::Font`] from its bytes. + fn load_font(&mut self, font: Cow<'static, [u8]>); + + /// Draws the given [`Text`]. + fn fill_text(&mut self, text: Text<'_, Self::Font>); +} diff --git a/core/src/touch.rs b/core/src/touch.rs new file mode 100644 index 00000000..18120644 --- /dev/null +++ b/core/src/touch.rs @@ -0,0 +1,23 @@ +//! Build touch events. +use crate::Point; + +/// A touch interaction. +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(missing_docs)] +pub enum Event { + /// A touch interaction was started. + FingerPressed { id: Finger, position: Point }, + + /// An on-going touch interaction was moved. + FingerMoved { id: Finger, position: Point }, + + /// A touch interaction was ended. + FingerLifted { id: Finger, position: Point }, + + /// A touch interaction was canceled. + FingerLost { id: Finger, position: Point }, +} + +/// A unique identifier representing a finger on a touch interaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Finger(pub u64); diff --git a/core/src/widget.rs b/core/src/widget.rs new file mode 100644 index 00000000..70e2c2d9 --- /dev/null +++ b/core/src/widget.rs @@ -0,0 +1,145 @@ +//! Create custom widgets and operate on them. +pub mod operation; +pub mod text; +pub mod tree; + +mod id; + +pub use id::Id; +pub use operation::Operation; +pub use text::Text; +pub use tree::Tree; + +use crate::event::{self, Event}; +use crate::layout::{self, Layout}; +use crate::mouse; +use crate::overlay; +use crate::renderer; +use crate::{Clipboard, Length, Point, Rectangle, Shell}; + +/// A component that displays information and allows interaction. +/// +/// If you want to build your own widgets, you will need to implement this +/// trait. +/// +/// # Examples +/// The repository has some [examples] showcasing how to implement a custom +/// widget: +/// +/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using +/// [`lyon`]. +/// - [`custom_widget`], a demonstration of how to build a custom widget that +/// draws a circle. +/// - [`geometry`], a custom widget showcasing how to draw geometry with the +/// `Mesh2D` primitive in [`iced_wgpu`]. +/// +/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples +/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool +/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget +/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry +/// [`lyon`]: https://github.com/nical/lyon +/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu +pub trait Widget +where + Renderer: crate::Renderer, +{ + /// Returns the width of the [`Widget`]. + fn width(&self) -> Length; + + /// Returns the height of the [`Widget`]. + fn height(&self) -> Length; + + /// Returns the [`layout::Node`] of the [`Widget`]. + /// + /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the + /// user interface. + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node; + + /// Draws the [`Widget`] using the associated `Renderer`. + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ); + + /// Returns the [`Tag`] of the [`Widget`]. + /// + /// [`Tag`]: tree::Tag + fn tag(&self) -> tree::Tag { + tree::Tag::stateless() + } + + /// Returns the [`State`] of the [`Widget`]. + /// + /// [`State`]: tree::State + fn state(&self) -> tree::State { + tree::State::None + } + + /// Returns the state [`Tree`] of the children of the [`Widget`]. + fn children(&self) -> Vec { + Vec::new() + } + + /// Reconciliates the [`Widget`] with the provided [`Tree`]. + fn diff(&self, _tree: &mut Tree) {} + + /// Applies an [`Operation`] to the [`Widget`]. + fn operate( + &self, + _state: &mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + _operation: &mut dyn Operation, + ) { + } + + /// Processes a runtime [`Event`]. + /// + /// By default, it does nothing. + fn on_event( + &mut self, + _state: &mut Tree, + _event: Event, + _layout: Layout<'_>, + _cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + _shell: &mut Shell<'_, Message>, + ) -> event::Status { + event::Status::Ignored + } + + /// Returns the current [`mouse::Interaction`] of the [`Widget`]. + /// + /// By default, it returns [`mouse::Interaction::Idle`]. + fn mouse_interaction( + &self, + _state: &Tree, + _layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + mouse::Interaction::Idle + } + + /// Returns the overlay of the [`Widget`], if there is any. + fn overlay<'a>( + &'a mut self, + _state: &'a mut Tree, + _layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + None + } +} diff --git a/core/src/widget/id.rs b/core/src/widget/id.rs new file mode 100644 index 00000000..ae739bb7 --- /dev/null +++ b/core/src/widget/id.rs @@ -0,0 +1,43 @@ +use std::borrow; +use std::sync::atomic::{self, AtomicUsize}; + +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + +/// The identifier of a generic widget. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(Internal); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(Internal::Custom(id.into())) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + pub fn unique() -> Self { + let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); + + Self(Internal::Unique(id)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Internal { + Unique(usize), + Custom(borrow::Cow<'static, str>), +} + +#[cfg(test)] +mod tests { + use super::Id; + + #[test] + fn unique_generates_different_ids() { + let a = Id::unique(); + let b = Id::unique(); + + assert_ne!(a, b); + } +} diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs new file mode 100644 index 00000000..53688a21 --- /dev/null +++ b/core/src/widget/operation.rs @@ -0,0 +1,112 @@ +//! Query or update internal widget state. +pub mod focusable; +pub mod scrollable; +pub mod text_input; + +pub use focusable::Focusable; +pub use scrollable::Scrollable; +pub use text_input::TextInput; + +use crate::widget::Id; + +use std::any::Any; +use std::fmt; + +/// A piece of logic that can traverse the widget tree of an application in +/// order to query or update some widget state. +pub trait Operation { + /// Operates on a widget that contains other widgets. + /// + /// The `operate_on_children` function can be called to return control to + /// the widget tree and keep traversing it. + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ); + + /// Operates on a widget that can be focused. + fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} + + /// Operates on a widget that can be scrolled. + fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} + + /// Operates on a widget that has text input. + fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} + + /// Operates on a custom widget with some state. + fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} + + /// Finishes the [`Operation`] and returns its [`Outcome`]. + fn finish(&self) -> Outcome { + Outcome::None + } +} + +/// The result of an [`Operation`]. +pub enum Outcome { + /// The [`Operation`] produced no result. + None, + + /// The [`Operation`] produced some result. + Some(T), + + /// The [`Operation`] needs to be followed by another [`Operation`]. + Chain(Box>), +} + +impl fmt::Debug for Outcome +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "Outcome::None"), + Self::Some(output) => write!(f, "Outcome::Some({output:?})"), + Self::Chain(_) => write!(f, "Outcome::Chain(...)"), + } + } +} + +/// Produces an [`Operation`] that applies the given [`Operation`] to the +/// children of a container with the given [`Id`]. +pub fn scoped( + target: Id, + operation: impl Operation + 'static, +) -> impl Operation { + struct ScopedOperation { + target: Id, + operation: Box>, + } + + impl Operation for ScopedOperation { + fn container( + &mut self, + id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + if id == Some(&self.target) { + operate_on_children(self.operation.as_mut()); + } else { + operate_on_children(self); + } + } + + fn finish(&self) -> Outcome { + match self.operation.finish() { + Outcome::Chain(next) => { + Outcome::Chain(Box::new(ScopedOperation { + target: self.target.clone(), + operation: next, + })) + } + outcome => outcome, + } + } + } + + ScopedOperation { + target, + operation: Box::new(operation), + } +} diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs new file mode 100644 index 00000000..312e4894 --- /dev/null +++ b/core/src/widget/operation/focusable.rs @@ -0,0 +1,203 @@ +//! Operate on widgets that can be focused. +use crate::widget::operation::{Operation, Outcome}; +use crate::widget::Id; + +/// The internal state of a widget that can be focused. +pub trait Focusable { + /// Returns whether the widget is focused or not. + fn is_focused(&self) -> bool; + + /// Focuses the widget. + fn focus(&mut self); + + /// Unfocuses the widget. + fn unfocus(&mut self); +} + +/// A summary of the focusable widgets present on a widget tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Count { + /// The index of the current focused widget, if any. + pub focused: Option, + + /// The total amount of focusable widgets. + pub total: usize, +} + +/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. +pub fn focus(target: Id) -> impl Operation { + struct Focus { + target: Id, + } + + impl Operation for Focus { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.focus(); + } + _ => { + state.unfocus(); + } + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + Focus { target } +} + +/// Produces an [`Operation`] that generates a [`Count`] and chains it with the +/// provided function to build a new [`Operation`]. +pub fn count(f: fn(Count) -> O) -> impl Operation +where + O: Operation + 'static, +{ + struct CountFocusable { + count: Count, + next: fn(Count) -> O, + } + + impl Operation for CountFocusable + where + O: Operation + 'static, + { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if state.is_focused() { + self.count.focused = Some(self.count.total); + } + + self.count.total += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + Outcome::Chain(Box::new((self.next)(self.count))) + } + } + + CountFocusable { + count: Count::default(), + next: f, + } +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the previous focusable widget. +/// - if not found, focuses the last focusable widget. +pub fn focus_previous() -> impl Operation { + struct FocusPrevious { + count: Count, + current: usize, + } + + impl Operation for FocusPrevious { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + if self.count.total == 0 { + return; + } + + match self.count.focused { + None if self.current == self.count.total - 1 => state.focus(), + Some(0) if self.current == 0 => state.unfocus(), + Some(0) => {} + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused - 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + count(|count| FocusPrevious { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget, and +/// - if found, focuses the next focusable widget. +/// - if not found, focuses the first focusable widget. +pub fn focus_next() -> impl Operation { + struct FocusNext { + count: Count, + current: usize, + } + + impl Operation for FocusNext { + fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + match self.count.focused { + None if self.current == 0 => state.focus(), + Some(focused) if focused == self.current => state.unfocus(), + Some(focused) if focused + 1 == self.current => state.focus(), + _ => {} + } + + self.current += 1; + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + count(|count| FocusNext { count, current: 0 }) +} + +/// Produces an [`Operation`] that searches for the current focused widget +/// and stores its ID. This ignores widgets that do not have an ID. +pub fn find_focused() -> impl Operation { + struct FindFocused { + focused: Option, + } + + impl Operation for FindFocused { + fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { + if state.is_focused() && id.is_some() { + self.focused = id.cloned(); + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn finish(&self) -> Outcome { + if let Some(id) = &self.focused { + Outcome::Some(id.clone()) + } else { + Outcome::None + } + } + } + + FindFocused { focused: None } +} diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs new file mode 100644 index 00000000..3b20631f --- /dev/null +++ b/core/src/widget/operation/scrollable.rs @@ -0,0 +1,54 @@ +//! Operate on widgets that can be scrolled. +use crate::widget::{Id, Operation}; + +/// The internal state of a widget that can be scrolled. +pub trait Scrollable { + /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. + fn snap_to(&mut self, offset: RelativeOffset); +} + +/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to +/// the provided `percentage`. +pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { + struct SnapTo { + target: Id, + offset: RelativeOffset, + } + + impl Operation for SnapTo { + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + + fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { + if Some(&self.target) == id { + state.snap_to(self.offset); + } + } + } + + SnapTo { target, offset } +} + +/// The amount of offset in each direction of a [`Scrollable`]. +/// +/// A value of `0.0` means start, while `1.0` means end. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct RelativeOffset { + /// The amount of horizontal offset + pub x: f32, + /// The amount of vertical offset + pub y: f32, +} + +impl RelativeOffset { + /// A relative offset that points to the top-left of a [`Scrollable`]. + pub const START: Self = Self { x: 0.0, y: 0.0 }; + + /// A relative offset that points to the bottom-right of a [`Scrollable`]. + pub const END: Self = Self { x: 1.0, y: 1.0 }; +} diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs new file mode 100644 index 00000000..4c773e99 --- /dev/null +++ b/core/src/widget/operation/text_input.rs @@ -0,0 +1,131 @@ +//! Operate on widgets that have text input. +use crate::widget::operation::Operation; +use crate::widget::Id; + +/// The internal state of a widget that has text input. +pub trait TextInput { + /// Moves the cursor of the text input to the front of the input text. + fn move_cursor_to_front(&mut self); + /// Moves the cursor of the text input to the end of the input text. + fn move_cursor_to_end(&mut self); + /// Moves the cursor of the text input to an arbitrary location. + fn move_cursor_to(&mut self, position: usize); + /// Selects all the content of the text input. + fn select_all(&mut self); +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// front. +pub fn move_cursor_to_front(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to_front(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// end. +pub fn move_cursor_to_end(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to_end(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} + +/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the +/// provided position. +pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { + struct MoveCursor { + target: Id, + position: usize, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.move_cursor_to(self.position); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target, position } +} + +/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`]. +pub fn select_all(target: Id) -> impl Operation { + struct MoveCursor { + target: Id, + } + + impl Operation for MoveCursor { + fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { + match id { + Some(id) if id == &self.target => { + state.select_all(); + } + _ => {} + } + } + + fn container( + &mut self, + _id: Option<&Id>, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + MoveCursor { target } +} diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs new file mode 100644 index 00000000..485bb542 --- /dev/null +++ b/core/src/widget/text.rs @@ -0,0 +1,277 @@ +//! Write some text for your users to read. +use crate::alignment; +use crate::layout; +use crate::renderer; +use crate::text; +use crate::widget::Tree; +use crate::{ + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, +}; + +use std::borrow::Cow; + +/// A paragraph of text. +#[allow(missing_debug_implementations)] +pub struct Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + content: Cow<'a, str>, + size: Option, + width: Length, + height: Length, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + font: Option, + style: ::Style, +} + +impl<'a, Renderer> Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + /// Create a new fragment of [`Text`] with the given contents. + pub fn new(content: impl Into>) -> Self { + Text { + content: content.into(), + size: None, + font: None, + width: Length::Shrink, + height: Length::Shrink, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + style: Default::default(), + } + } + + /// Sets the size of the [`Text`]. + pub fn size(mut self, size: impl Into) -> Self { + self.size = Some(size.into().0); + self + } + + /// Sets the [`Font`] of the [`Text`]. + /// + /// [`Font`]: crate::text::Renderer::Font + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the style of the [`Text`]. + pub fn style( + mut self, + style: impl Into<::Style>, + ) -> Self { + self.style = style.into(); + self + } + + /// Sets the width of the [`Text`] boundaries. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Text`] boundaries. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`alignment::Horizontal`] of the [`Text`]. + pub fn horizontal_alignment( + mut self, + alignment: alignment::Horizontal, + ) -> Self { + self.horizontal_alignment = alignment; + self + } + + /// Sets the [`alignment::Vertical`] of the [`Text`]. + pub fn vertical_alignment( + mut self, + alignment: alignment::Vertical, + ) -> Self { + self.vertical_alignment = alignment; + self + } +} + +impl<'a, Message, Renderer> Widget for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + let size = self.size.unwrap_or_else(|| renderer.default_size()); + + let bounds = limits.max(); + + let (width, height) = renderer.measure( + &self.content, + size, + self.font.unwrap_or_else(|| renderer.default_font()), + bounds, + ); + + let size = limits.resolve(Size::new(width, height)); + + layout::Node::new(size) + } + + fn draw( + &self, + _state: &Tree, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + _cursor_position: Point, + _viewport: &Rectangle, + ) { + draw( + renderer, + style, + layout, + &self.content, + self.size, + self.font, + theme.appearance(self.style), + self.horizontal_alignment, + self.vertical_alignment, + ); + } +} + +/// Draws text using the same logic as the [`Text`] widget. +/// +/// Specifically: +/// +/// * If no `size` is provided, the default text size of the `Renderer` will be +/// used. +/// * If no `color` is provided, the [`renderer::Style::text_color`] will be +/// used. +/// * The alignment attributes do not affect the position of the bounds of the +/// [`Layout`]. +pub fn draw( + renderer: &mut Renderer, + style: &renderer::Style, + layout: Layout<'_>, + content: &str, + size: Option, + font: Option, + appearance: Appearance, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, +) where + Renderer: text::Renderer, +{ + let bounds = layout.bounds(); + + let x = match horizontal_alignment { + alignment::Horizontal::Left => bounds.x, + alignment::Horizontal::Center => bounds.center_x(), + alignment::Horizontal::Right => bounds.x + bounds.width, + }; + + let y = match vertical_alignment { + alignment::Vertical::Top => bounds.y, + alignment::Vertical::Center => bounds.center_y(), + alignment::Vertical::Bottom => bounds.y + bounds.height, + }; + + renderer.fill_text(crate::Text { + content, + size: size.unwrap_or_else(|| renderer.default_size()), + bounds: Rectangle { x, y, ..bounds }, + color: appearance.color.unwrap_or(style.text_color), + font: font.unwrap_or_else(|| renderer.default_font()), + horizontal_alignment, + vertical_alignment, + }); +} + +impl<'a, Message, Renderer> From> + for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(text: Text<'a, Renderer>) -> Element<'a, Message, Renderer> { + Element::new(text) + } +} + +impl<'a, Renderer> Clone for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn clone(&self) -> Self { + Self { + content: self.content.clone(), + size: self.size, + width: self.width, + height: self.height, + horizontal_alignment: self.horizontal_alignment, + vertical_alignment: self.vertical_alignment, + font: self.font, + style: self.style, + } + } +} + +impl<'a, Renderer> From<&'a str> for Text<'a, Renderer> +where + Renderer: text::Renderer, + Renderer::Theme: StyleSheet, +{ + fn from(content: &'a str) -> Self { + Self::new(content) + } +} + +impl<'a, Message, Renderer> From<&'a str> for Element<'a, Message, Renderer> +where + Renderer: text::Renderer + 'a, + Renderer::Theme: StyleSheet, +{ + fn from(content: &'a str) -> Self { + Text::from(content).into() + } +} + +/// The style sheet of some text. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default + Copy; + + /// Produces the [`Appearance`] of some text. + fn appearance(&self, style: Self::Style) -> Appearance; +} + +/// The apperance of some text. +#[derive(Debug, Clone, Copy, Default)] +pub struct Appearance { + /// The [`Color`] of the text. + /// + /// The default, `None`, means using the inherited color. + pub color: Option, +} diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs new file mode 100644 index 00000000..0af40c33 --- /dev/null +++ b/core/src/widget/tree.rs @@ -0,0 +1,187 @@ +//! Store internal widget state in a state tree to ensure continuity. +use crate::Widget; + +use std::any::{self, Any}; +use std::borrow::Borrow; +use std::fmt; + +/// A persistent state widget tree. +/// +/// A [`Tree`] is normally associated with a specific widget in the widget tree. +#[derive(Debug)] +pub struct Tree { + /// The tag of the [`Tree`]. + pub tag: Tag, + + /// The [`State`] of the [`Tree`]. + pub state: State, + + /// The children of the root widget of the [`Tree`]. + pub children: Vec, +} + +impl Tree { + /// Creates an empty, stateless [`Tree`] with no children. + pub fn empty() -> Self { + Self { + tag: Tag::stateless(), + state: State::None, + children: Vec::new(), + } + } + + /// Creates a new [`Tree`] for the provided [`Widget`]. + pub fn new<'a, Message, Renderer>( + widget: impl Borrow + 'a>, + ) -> Self + where + Renderer: crate::Renderer, + { + let widget = widget.borrow(); + + Self { + tag: widget.tag(), + state: widget.state(), + children: widget.children(), + } + } + + /// Reconciliates the current tree with the provided [`Widget`]. + /// + /// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the + /// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called). + /// + /// Otherwise, the whole [`Tree`] is recreated. + /// + /// [`Widget::diff`]: crate::Widget::diff + pub fn diff<'a, Message, Renderer>( + &mut self, + new: impl Borrow + 'a>, + ) where + Renderer: crate::Renderer, + { + if self.tag == new.borrow().tag() { + new.borrow().diff(self) + } else { + *self = Self::new(new); + } + } + + /// Reconciliates the children of the tree with the provided list of widgets. + pub fn diff_children<'a, Message, Renderer>( + &mut self, + new_children: &[impl Borrow + 'a>], + ) where + Renderer: crate::Renderer, + { + self.diff_children_custom( + new_children, + |tree, widget| tree.diff(widget.borrow()), + |widget| Self::new(widget.borrow()), + ) + } + + /// Reconciliates the children of the tree with the provided list of widgets using custom + /// logic both for diffing and creating new widget state. + pub fn diff_children_custom( + &mut self, + new_children: &[T], + diff: impl Fn(&mut Tree, &T), + new_state: impl Fn(&T) -> Self, + ) { + if self.children.len() > new_children.len() { + self.children.truncate(new_children.len()); + } + + for (child_state, new) in + self.children.iter_mut().zip(new_children.iter()) + { + diff(child_state, new); + } + + if self.children.len() < new_children.len() { + self.children.extend( + new_children[self.children.len()..].iter().map(new_state), + ); + } + } +} + +/// The identifier of some widget state. +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] +pub struct Tag(any::TypeId); + +impl Tag { + /// Creates a [`Tag`] for a state of type `T`. + pub fn of() -> Self + where + T: 'static, + { + Self(any::TypeId::of::()) + } + + /// Creates a [`Tag`] for a stateless widget. + pub fn stateless() -> Self { + Self::of::<()>() + } +} + +/// The internal [`State`] of a widget. +pub enum State { + /// No meaningful internal state. + None, + + /// Some meaningful internal state. + Some(Box), +} + +impl State { + /// Creates a new [`State`]. + pub fn new(state: T) -> Self + where + T: 'static, + { + State::Some(Box::new(state)) + } + + /// Downcasts the [`State`] to `T` and returns a reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_ref(&self) -> &T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_ref().expect("Downcast widget state") + } + } + } + + /// Downcasts the [`State`] to `T` and returns a mutable reference to it. + /// + /// # Panics + /// This method will panic if the downcast fails or the [`State`] is [`State::None`]. + pub fn downcast_mut(&mut self) -> &mut T + where + T: 'static, + { + match self { + State::None => panic!("Downcast on stateless state"), + State::Some(state) => { + state.downcast_mut().expect("Downcast widget state") + } + } + } +} + +impl fmt::Debug for State { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "State::None"), + Self::Some(_) => write!(f, "State::Some"), + } + } +} diff --git a/core/src/window.rs b/core/src/window.rs new file mode 100644 index 00000000..d829a4b4 --- /dev/null +++ b/core/src/window.rs @@ -0,0 +1,10 @@ +//! Build window-based GUI applications. +mod event; +mod mode; +mod redraw_request; +mod user_attention; + +pub use event::Event; +pub use mode::Mode; +pub use redraw_request::RedrawRequest; +pub use user_attention::UserAttention; diff --git a/core/src/window/event.rs b/core/src/window/event.rs new file mode 100644 index 00000000..e2fb5e66 --- /dev/null +++ b/core/src/window/event.rs @@ -0,0 +1,58 @@ +use crate::time::Instant; + +use std::path::PathBuf; + +/// A window-related event. +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum Event { + /// A window was moved. + Moved { + /// The new logical x location of the window + x: i32, + /// The new logical y location of the window + y: i32, + }, + + /// A window was resized. + Resized { + /// The new logical width of the window + width: u32, + /// The new logical height of the window + height: u32, + }, + + /// A window redraw was requested. + /// + /// The [`Instant`] contains the current time. + RedrawRequested(Instant), + + /// The user has requested for the window to close. + /// + /// Usually, you will want to terminate the execution whenever this event + /// occurs. + CloseRequested, + + /// A window was focused. + Focused, + + /// A window was unfocused. + Unfocused, + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted + /// for each file separately. + FileHovered(PathBuf), + + /// A file has beend dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted + /// for each file separately. + FileDropped(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `FilesHoveredLeft` event triggered even if + /// multiple files were hovered. + FilesHoveredLeft, +} diff --git a/core/src/window/mode.rs b/core/src/window/mode.rs new file mode 100644 index 00000000..fdce8e23 --- /dev/null +++ b/core/src/window/mode.rs @@ -0,0 +1,12 @@ +/// The mode of a window-based application. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// The application appears in its own window. + Windowed, + + /// The application takes the whole screen of its current monitor. + Fullscreen, + + /// The application is hidden + Hidden, +} diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs new file mode 100644 index 00000000..3b4f0fd3 --- /dev/null +++ b/core/src/window/redraw_request.rs @@ -0,0 +1,38 @@ +use crate::time::Instant; + +/// A request to redraw a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum RedrawRequest { + /// Redraw the next frame. + NextFrame, + + /// Redraw at the given time. + At(Instant), +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + #[test] + fn ordering() { + let now = Instant::now(); + let later = now + Duration::from_millis(10); + + assert_eq!(RedrawRequest::NextFrame, RedrawRequest::NextFrame); + assert_eq!(RedrawRequest::At(now), RedrawRequest::At(now)); + + assert!(RedrawRequest::NextFrame < RedrawRequest::At(now)); + assert!(RedrawRequest::At(now) > RedrawRequest::NextFrame); + assert!(RedrawRequest::At(now) < RedrawRequest::At(later)); + assert!(RedrawRequest::At(later) > RedrawRequest::At(now)); + + assert!(RedrawRequest::NextFrame <= RedrawRequest::NextFrame); + assert!(RedrawRequest::NextFrame <= RedrawRequest::At(now)); + assert!(RedrawRequest::At(now) >= RedrawRequest::NextFrame); + assert!(RedrawRequest::At(now) <= RedrawRequest::At(now)); + assert!(RedrawRequest::At(now) <= RedrawRequest::At(later)); + assert!(RedrawRequest::At(later) >= RedrawRequest::At(now)); + } +} diff --git a/core/src/window/user_attention.rs b/core/src/window/user_attention.rs new file mode 100644 index 00000000..b03dfeef --- /dev/null +++ b/core/src/window/user_attention.rs @@ -0,0 +1,21 @@ +/// The type of user attention to request. +/// +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy)] +pub enum UserAttention { + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in focus. + Critical, + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + Informational, +} diff --git a/examples/component/Cargo.toml b/examples/component/Cargo.toml index dd435201..9db1e6b4 100644 --- a/examples/component/Cargo.toml +++ b/examples/component/Cargo.toml @@ -6,6 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } -iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "lazy"] } diff --git a/examples/component/src/main.rs b/examples/component/src/main.rs index c407bb06..e59588b1 100644 --- a/examples/component/src/main.rs +++ b/examples/component/src/main.rs @@ -47,9 +47,8 @@ impl Sandbox for Component { mod numeric_input { use iced::alignment::{self, Alignment}; - use iced::widget::{self, button, row, text, text_input}; - use iced::{Element, Length}; - use iced_lazy::{self, Component}; + use iced::widget::{button, component, row, text, text_input, Component}; + use iced::{Element, Length, Renderer}; pub struct NumericInput { value: Option, @@ -82,13 +81,7 @@ mod numeric_input { } } - impl Component for NumericInput - where - Renderer: iced_native::text::Renderer + 'static, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, - { + impl Component for NumericInput { type State = (); type Event = Event; @@ -151,17 +144,12 @@ mod numeric_input { } } - impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> + impl<'a, Message> From> for Element<'a, Message, Renderer> where Message: 'a, - Renderer: 'static + iced_native::text::Renderer, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, { fn from(numeric_input: NumericInput) -> Self { - iced_lazy::component(numeric_input) + component(numeric_input) } } } diff --git a/examples/custom_quad/Cargo.toml b/examples/custom_quad/Cargo.toml index 39154786..f097c2dd 100644 --- a/examples/custom_quad/Cargo.toml +++ b/examples/custom_quad/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/custom_quad/src/main.rs b/examples/custom_quad/src/main.rs index 6509887c..b07f42ce 100644 --- a/examples/custom_quad/src/main.rs +++ b/examples/custom_quad/src/main.rs @@ -1,9 +1,9 @@ //! This example showcases a drawing a quad. mod quad { - use iced_native::layout::{self, Layout}; - use iced_native::renderer; - use iced_native::widget::{self, Widget}; - use iced_native::{Color, Element, Length, Point, Rectangle, Size}; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::{Color, Element, Length, Point, Rectangle, Size}; pub struct CustomQuad { size: f32, diff --git a/examples/custom_widget/Cargo.toml b/examples/custom_widget/Cargo.toml index 067aab4a..dda0efe8 100644 --- a/examples/custom_widget/Cargo.toml +++ b/examples/custom_widget/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index f6bb3b1e..7854548c 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -9,10 +9,10 @@ mod circle { // Of course, you can choose to make the implementation renderer-agnostic, // if you wish to, by creating your own `Renderer` trait, which could be // implemented by `iced_wgpu` and other renderers. - use iced_native::layout::{self, Layout}; - use iced_native::renderer; - use iced_native::widget::{self, Widget}; - use iced_native::{Color, Element, Length, Point, Rectangle, Size}; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::{Color, Element, Length, Point, Rectangle, Size}; pub struct Circle { radius: f32, diff --git a/examples/events/Cargo.toml b/examples/events/Cargo.toml index 8c56e471..15ffc0af 100644 --- a/examples/events/Cargo.toml +++ b/examples/events/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] iced = { path = "../..", features = ["debug"] } -iced_native = { path = "../../native" } diff --git a/examples/events/src/main.rs b/examples/events/src/main.rs index 1b97018e..7f3a5e1d 100644 --- a/examples/events/src/main.rs +++ b/examples/events/src/main.rs @@ -1,12 +1,13 @@ use iced::alignment; use iced::executor; +use iced::subscription; use iced::widget::{button, checkbox, container, text, Column}; use iced::window; +use iced::Event; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, Theme, }; -use iced_native::Event; pub fn main() -> iced::Result { Events::run(Settings { @@ -17,13 +18,13 @@ pub fn main() -> iced::Result { #[derive(Debug, Default)] struct Events { - last: Vec, + last: Vec, enabled: bool, } #[derive(Debug, Clone)] enum Message { - EventOccurred(iced_native::Event), + EventOccurred(Event), Toggled(bool), Exit, } @@ -70,7 +71,7 @@ impl Application for Events { } fn subscription(&self) -> Subscription { - iced_native::subscription::events().map(Message::EventOccurred) + subscription::events().map(Message::EventOccurred) } fn view(&self) -> Element { diff --git a/examples/geometry/Cargo.toml b/examples/geometry/Cargo.toml index 22ede0e0..79fe52d5 100644 --- a/examples/geometry/Cargo.toml +++ b/examples/geometry/Cargo.toml @@ -6,6 +6,5 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../.." } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } iced_graphics = { path = "../../graphics" } diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index a77772d5..5cb41184 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -1,23 +1,13 @@ //! This example showcases a simple native custom widget that renders using //! arbitrary low-level geometry. mod rainbow { - // For now, to implement a custom native widget you will need to add - // `iced_native` and `iced_wgpu` to your dependencies. - // - // Then, you simply need to define your widget type and implement the - // `iced_native::Widget` trait with the `iced_wgpu::Renderer`. - // - // Of course, you can choose to make the implementation renderer-agnostic, - // if you wish to, by creating your own `Renderer` trait, which could be - // implemented by `iced_wgpu` and other renderers. use iced_graphics::primitive::{ColoredVertex2D, Primitive}; - use iced_graphics::renderer::{self, Renderer}; - use iced_graphics::Backend; - use iced_native::layout; - use iced_native::widget::{self, Widget}; - use iced_native::{ - Element, Layout, Length, Point, Rectangle, Size, Vector, + use iced::advanced::layout::{self, Layout}; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::{ + Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, }; #[derive(Debug, Clone, Copy, Default)] @@ -27,10 +17,7 @@ mod rainbow { Rainbow } - impl Widget> for Rainbow - where - B: Backend, - { + impl Widget for Rainbow { fn width(&self) -> Length { Length::Fill } @@ -41,7 +28,7 @@ mod rainbow { fn layout( &self, - _renderer: &Renderer, + _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let size = limits.width(Length::Fill).resolve(Size::ZERO); @@ -52,15 +39,15 @@ mod rainbow { fn draw( &self, _tree: &widget::Tree, - renderer: &mut Renderer, - _theme: &T, + renderer: &mut Renderer, + _theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, ) { + use iced::advanced::Renderer as _; use iced_graphics::primitive::Mesh2D; - use iced_native::Renderer as _; let b = layout.bounds(); @@ -151,10 +138,7 @@ mod rainbow { } } - impl<'a, Message, B, T> From for Element<'a, Message, Renderer> - where - B: Backend, - { + impl<'a, Message> From for Element<'a, Message, Renderer> { fn from(rainbow: Rainbow) -> Self { Self::new(rainbow) } diff --git a/examples/integration/Cargo.toml b/examples/integration/Cargo.toml index 200306aa..60390242 100644 --- a/examples/integration/Cargo.toml +++ b/examples/integration/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] iced_winit = { path = "../../winit" } iced_wgpu = { path = "../../wgpu", features = ["webgl"] } +iced_widget = { path = "../../widget" } env_logger = "0.8" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index 533cb6e2..16e21709 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,6 +1,8 @@ use iced_wgpu::Renderer; -use iced_winit::widget::{slider, text_input, Column, Row, Text}; -use iced_winit::{Alignment, Color, Command, Element, Length, Program}; +use iced_widget::{slider, text_input, Column, Row, Text}; +use iced_winit::core::{Alignment, Color, Element, Length}; +use iced_winit::native::{Command, Program}; +use iced_winit::style::Theme; pub struct Controls { background_color: Color, @@ -27,7 +29,7 @@ impl Controls { } impl Program for Controls { - type Renderer = Renderer; + type Renderer = Renderer; type Message = Message; fn update(&mut self, message: Message) -> Command { @@ -43,7 +45,7 @@ impl Program for Controls { Command::none() } - fn view(&self) -> Element { + fn view(&self) -> Element> { let background_color = self.background_color; let text = &self.text; diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index a7a90ced..c1f1f076 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -4,11 +4,14 @@ mod scene; use controls::Controls; use scene::Scene; -use iced_wgpu::{wgpu, Backend, Renderer, Settings, Viewport}; -use iced_winit::{ - conversion, futures, program, renderer, winit, Clipboard, Color, Debug, - Size, -}; +use iced_wgpu::graphics::Viewport; +use iced_wgpu::{wgpu, Backend, Renderer, Settings}; +use iced_winit::core::renderer; +use iced_winit::core::{Color, Size}; +use iced_winit::native::program; +use iced_winit::native::Debug; +use iced_winit::style::Theme; +use iced_winit::{conversion, futures, winit, Clipboard}; use winit::{ dpi::PhysicalPosition, @@ -73,43 +76,45 @@ pub fn main() { let instance = wgpu::Instance::new(backend); let surface = unsafe { instance.create_surface(&window) }; - let (format, (device, queue)) = futures::executor::block_on(async { - let adapter = wgpu::util::initialize_adapter_from_env_or_default( - &instance, - backend, - Some(&surface), - ) - .await - .expect("No suitable GPU adapters found on the system!"); - - let adapter_features = adapter.features(); - - #[cfg(target_arch = "wasm32")] - let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()); - - #[cfg(not(target_arch = "wasm32"))] - let needed_limits = wgpu::Limits::default(); - - ( - surface - .get_supported_formats(&adapter) - .first() - .copied() - .expect("Get preferred format"), - adapter - .request_device( - &wgpu::DeviceDescriptor { - label: None, - features: adapter_features & wgpu::Features::default(), - limits: needed_limits, - }, - None, - ) - .await - .expect("Request device"), - ) - }); + let (format, (device, queue)) = + futures::futures::executor::block_on(async { + let adapter = wgpu::util::initialize_adapter_from_env_or_default( + &instance, + backend, + Some(&surface), + ) + .await + .expect("No suitable GPU adapters found on the system!"); + + let adapter_features = adapter.features(); + + #[cfg(target_arch = "wasm32")] + let needed_limits = wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()); + + #[cfg(not(target_arch = "wasm32"))] + let needed_limits = wgpu::Limits::default(); + + ( + surface + .get_supported_formats(&adapter) + .first() + .copied() + .expect("Get preferred format"), + adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: adapter_features + & wgpu::Features::default(), + limits: needed_limits, + }, + None, + ) + .await + .expect("Request device"), + ) + }); surface.configure( &device, @@ -188,7 +193,7 @@ pub fn main() { viewport.scale_factor(), ), &mut renderer, - &iced_wgpu::Theme::Dark, + &Theme::Dark, &renderer::Style { text_color: Color::WHITE }, &mut clipboard, &mut debug, diff --git a/examples/integration/src/scene.rs b/examples/integration/src/scene.rs index 3e41fbda..90c7efbf 100644 --- a/examples/integration/src/scene.rs +++ b/examples/integration/src/scene.rs @@ -1,5 +1,5 @@ use iced_wgpu::wgpu; -use iced_winit::Color; +use iced_winit::core::Color; pub struct Scene { pipeline: wgpu::RenderPipeline, diff --git a/examples/lazy/Cargo.toml b/examples/lazy/Cargo.toml index 79255c25..e03e89a9 100644 --- a/examples/lazy/Cargo.toml +++ b/examples/lazy/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "lazy"] } diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 6512106f..e1cdaefe 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,10 +1,9 @@ use iced::theme; use iced::widget::{ - button, column, horizontal_space, pick_list, row, scrollable, text, + button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, }; use iced::{Element, Length, Sandbox, Settings}; -use iced_lazy::lazy; use std::collections::HashSet; use std::hash::Hash; diff --git a/examples/modal/Cargo.toml b/examples/modal/Cargo.toml index 8770acac..3ac61e6a 100644 --- a/examples/modal/Cargo.toml +++ b/examples/modal/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = [] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 54555684..214ec97e 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -178,12 +178,15 @@ impl App { } mod modal { - use iced_native::alignment::Alignment; - use iced_native::widget::{self, Tree}; - use iced_native::{ - event, layout, mouse, overlay, renderer, Clipboard, Color, Element, - Event, Layout, Length, Point, Rectangle, Shell, Size, Widget, - }; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::overlay; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Widget}; + use iced::advanced::{self, Clipboard, Shell}; + use iced::alignment::Alignment; + use iced::event; + use iced::mouse; + use iced::{Color, Element, Event, Length, Point, Rectangle, Size}; /// A widget that centers a modal element over some base element pub struct Modal<'a, Message, Renderer> { @@ -218,14 +221,17 @@ mod modal { impl<'a, Message, Renderer> Widget for Modal<'a, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: advanced::Renderer, Message: Clone, { - fn children(&self) -> Vec { - vec![Tree::new(&self.base), Tree::new(&self.modal)] + fn children(&self) -> Vec { + vec![ + widget::Tree::new(&self.base), + widget::Tree::new(&self.modal), + ] } - fn diff(&self, tree: &mut Tree) { + fn diff(&self, tree: &mut widget::Tree) { tree.diff_children(&[&self.base, &self.modal]); } @@ -247,7 +253,7 @@ mod modal { fn on_event( &mut self, - state: &mut Tree, + state: &mut widget::Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -268,9 +274,9 @@ mod modal { fn draw( &self, - state: &Tree, + state: &widget::Tree, renderer: &mut Renderer, - theme: &::Theme, + theme: &::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -289,7 +295,7 @@ mod modal { fn overlay<'b>( &'b mut self, - state: &'b mut Tree, + state: &'b mut widget::Tree, layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { @@ -306,7 +312,7 @@ mod modal { fn mouse_interaction( &self, - state: &Tree, + state: &widget::Tree, layout: Layout<'_>, cursor_position: Point, viewport: &Rectangle, @@ -323,7 +329,7 @@ mod modal { fn operate( &self, - state: &mut Tree, + state: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, @@ -339,7 +345,7 @@ mod modal { struct Overlay<'a, 'b, Message, Renderer> { content: &'b mut Element<'a, Message, Renderer>, - tree: &'b mut Tree, + tree: &'b mut widget::Tree, size: Size, on_blur: Option, } @@ -347,7 +353,7 @@ mod modal { impl<'a, 'b, Message, Renderer> overlay::Overlay for Overlay<'a, 'b, Message, Renderer> where - Renderer: iced_native::Renderer, + Renderer: advanced::Renderer, Message: Clone, { fn layout( @@ -469,7 +475,7 @@ mod modal { impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where - Renderer: 'a + iced_native::Renderer, + Renderer: 'a + advanced::Renderer, Message: 'a + Clone, { fn from(modal: Modal<'a, Message, Renderer>) -> Self { diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml index dfd6dfa9..4c0bf072 100644 --- a/examples/pane_grid/Cargo.toml +++ b/examples/pane_grid/Cargo.toml @@ -6,6 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = ["debug"] } -iced_native = { path = "../../native" } -iced_lazy = { path = "../../lazy" } +iced = { path = "../..", features = ["debug", "lazy"] } diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index c9f1376c..dfb80853 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,14 +1,16 @@ use iced::alignment::{self, Alignment}; +use iced::event::{self, Event}; use iced::executor; use iced::keyboard; +use iced::subscription; use iced::theme::{self, Theme}; use iced::widget::pane_grid::{self, PaneGrid}; -use iced::widget::{button, column, container, row, scrollable, text}; +use iced::widget::{ + button, column, container, responsive, row, scrollable, text, +}; use iced::{ Application, Color, Command, Element, Length, Settings, Size, Subscription, }; -use iced_lazy::responsive; -use iced_native::{event, subscription, Event}; pub fn main() -> iced::Result { Example::run(Settings::default()) diff --git a/examples/toast/Cargo.toml b/examples/toast/Cargo.toml index f1f986aa..f703572c 100644 --- a/examples/toast/Cargo.toml +++ b/examples/toast/Cargo.toml @@ -6,5 +6,4 @@ edition = "2021" publish = false [dependencies] -iced = { path = "../..", features = [] } -iced_native = { path = "../../native" } +iced = { path = "../..", features = ["advanced"] } diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index e74b3ee6..78fb9de1 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -176,17 +176,23 @@ mod toast { use std::fmt; use std::time::{Duration, Instant}; + use iced::advanced; + use iced::advanced::layout::{self, Layout}; + use iced::advanced::overlay; + use iced::advanced::renderer; + use iced::advanced::widget::{self, Operation, Tree}; + use iced::advanced::{Clipboard, Shell, Widget}; + use iced::event::{self, Event}; + use iced::mouse; use iced::theme; use iced::widget::{ button, column, container, horizontal_rule, horizontal_space, row, text, }; + use iced::window; use iced::{ Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector, }; - use iced_native::widget::{tree, Operation, Tree}; - use iced_native::{event, layout, mouse, overlay, renderer, window}; - use iced_native::{Clipboard, Event, Layout, Shell, Widget}; pub const DEFAULT_TIMEOUT: u64 = 5; @@ -324,13 +330,13 @@ mod toast { self.content.as_widget().layout(renderer, limits) } - fn tag(&self) -> tree::Tag { + fn tag(&self) -> widget::tree::Tag { struct Marker(Vec); - iced_native::widget::tree::Tag::of::() + widget::tree::Tag::of::() } - fn state(&self) -> tree::State { - iced_native::widget::tree::State::new(Vec::>::new()) + fn state(&self) -> widget::tree::State { + widget::tree::State::new(Vec::>::new()) } fn children(&self) -> Vec { @@ -584,7 +590,7 @@ mod toast { fn draw( &self, renderer: &mut Renderer, - theme: &::Theme, + theme: &::Theme, style: &renderer::Style, layout: Layout<'_>, cursor_position: Point, @@ -613,7 +619,7 @@ mod toast { &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_native::widget::Operation, + operation: &mut dyn widget::Operation, ) { operation.container(None, &mut |operation| { self.toasts diff --git a/examples/url_handler/Cargo.toml b/examples/url_handler/Cargo.toml index 63c7ec27..4dcff92d 100644 --- a/examples/url_handler/Cargo.toml +++ b/examples/url_handler/Cargo.toml @@ -7,4 +7,3 @@ publish = false [dependencies] iced = { path = "../.." } -iced_native = { path = "../../native" } diff --git a/examples/url_handler/src/main.rs b/examples/url_handler/src/main.rs index 3257b519..f63fa06a 100644 --- a/examples/url_handler/src/main.rs +++ b/examples/url_handler/src/main.rs @@ -1,12 +1,10 @@ +use iced::event::{Event, MacOS, PlatformSpecific}; use iced::executor; +use iced::subscription; use iced::widget::{container, text}; use iced::{ Application, Command, Element, Length, Settings, Subscription, Theme, }; -use iced_native::{ - event::{MacOS, PlatformSpecific}, - Event, -}; pub fn main() -> iced::Result { App::run(Settings::default()) @@ -19,7 +17,7 @@ struct App { #[derive(Debug, Clone)] enum Message { - EventOccurred(iced_native::Event), + EventOccurred(Event), } impl Application for App { @@ -52,7 +50,7 @@ impl Application for App { } fn subscription(&self) -> Subscription { - iced_native::subscription::events().map(Message::EventOccurred) + subscription::events().map(Message::EventOccurred) } fn view(&self) -> Element { diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index 98e6f474..4bb22b6c 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -39,13 +39,9 @@ bitflags = "1.2" version = "1.4" features = ["derive"] -[dependencies.iced_native] -version = "0.9" -path = "../native" - -[dependencies.iced_style] -version = "0.7" -path = "../style" +[dependencies.iced_core] +version = "0.8" +path = "../core" [dependencies.tiny-skia] version = "0.8" diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 8658cffe..dd2888ab 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -1,8 +1,8 @@ //! Write a graphics backend. -use iced_native::image; -use iced_native::svg; -use iced_native::text; -use iced_native::{Font, Point, Size}; +use iced_core::image; +use iced_core::svg; +use iced_core::text; +use iced_core::{Font, Point, Size}; use std::borrow::Cow; diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs new file mode 100644 index 00000000..d55e801a --- /dev/null +++ b/graphics/src/compositor.rs @@ -0,0 +1,92 @@ +//! A compositor is responsible for initializing a renderer and managing window +//! surfaces. +use crate::{Error, Viewport}; + +use iced_core::Color; + +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use thiserror::Error; + +/// A graphics compositor that can draw to windows. +pub trait Compositor: Sized { + /// The settings of the backend. + type Settings: Default; + + /// The iced renderer of the backend. + type Renderer: iced_core::Renderer; + + /// The surface of the backend. + type Surface; + + /// Creates a new [`Compositor`]. + fn new( + settings: Self::Settings, + compatible_window: Option<&W>, + ) -> Result<(Self, Self::Renderer), Error>; + + /// Crates a new [`Surface`] for the given window. + /// + /// [`Surface`]: Self::Surface + fn create_surface( + &mut self, + window: &W, + width: u32, + height: u32, + ) -> Self::Surface; + + /// Configures a new [`Surface`] with the given dimensions. + /// + /// [`Surface`]: Self::Surface + fn configure_surface( + &mut self, + surface: &mut Self::Surface, + width: u32, + height: u32, + ); + + /// Returns [`Information`] used by this [`Compositor`]. + fn fetch_information(&self) -> Information; + + /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. + /// + /// [`Renderer`]: Self::Renderer + /// [`Surface`]: Self::Surface + fn present>( + &mut self, + renderer: &mut Self::Renderer, + surface: &mut Self::Surface, + viewport: &Viewport, + background_color: Color, + overlay: &[T], + ) -> Result<(), SurfaceError>; +} + +/// Result of an unsuccessful call to [`Compositor::present`]. +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum SurfaceError { + /// A timeout was encountered while trying to acquire the next frame. + #[error( + "A timeout was encountered while trying to acquire the next frame" + )] + Timeout, + /// The underlying surface has changed, and therefore the surface must be updated. + #[error( + "The underlying surface has changed, and therefore the surface must be updated." + )] + Outdated, + /// The swap chain has been lost and needs to be recreated. + #[error("The surface has been lost and needs to be recreated")] + Lost, + /// There is no more memory left to allocate a new frame. + #[error("There is no more memory left to allocate a new frame")] + OutOfMemory, +} + +/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). +#[derive(Debug)] +pub struct Information { + /// Contains the graphics adapter. + pub adapter: String, + /// Contains the graphics backend. + pub backend: String, +} diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index 29ac84d6..8db1594a 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,7 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; -pub use iced_native::gradient::{self, Gradient}; +pub use iced_core::gradient::{self, Gradient}; use crate::Primitive; @@ -29,7 +29,7 @@ impl From for Primitive { } } -pub trait Renderer: iced_native::Renderer { +pub trait Renderer: iced_core::Renderer { type Geometry; fn draw(&mut self, geometry: Vec); diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 109d5e99..2e8c1669 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -1,5 +1,5 @@ //! Fill [crate::widget::canvas::Geometry] with a certain style. -use crate::{Color, Gradient}; +use iced_core::{Color, Gradient}; pub use crate::geometry::Style; diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs index 30c387c5..c3127bdf 100644 --- a/graphics/src/geometry/path.rs +++ b/graphics/src/geometry/path.rs @@ -9,7 +9,7 @@ pub use builder::Builder; pub use lyon_path; -use crate::{Point, Size}; +use iced_core::{Point, Size}; /// An immutable set of points that may or may not be connected. /// diff --git a/graphics/src/geometry/path/arc.rs b/graphics/src/geometry/path/arc.rs index e0747d3e..2cdebb66 100644 --- a/graphics/src/geometry/path/arc.rs +++ b/graphics/src/geometry/path/arc.rs @@ -1,5 +1,5 @@ //! Build and draw curves. -use crate::{Point, Vector}; +use iced_core::{Point, Vector}; /// A segment of a differentiable curve. #[derive(Debug, Clone, Copy)] diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs index 4a9c5e36..794dd3bc 100644 --- a/graphics/src/geometry/path/builder.rs +++ b/graphics/src/geometry/path/builder.rs @@ -1,5 +1,6 @@ use crate::geometry::path::{arc, Arc, Path}; -use crate::{Point, Size}; + +use iced_core::{Point, Size}; use lyon_path::builder::{self, SvgPathBuilder}; use lyon_path::geom; diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index b551a9c9..2d760a6c 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -1,7 +1,7 @@ //! Create lines from a [crate::widget::canvas::Path] and assigns them various attributes/styles. pub use crate::geometry::Style; -use crate::Color; +use iced_core::Color; /// The style of a stroke. #[derive(Debug, Clone)] diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index 6794f2e7..be9ee376 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -1,4 +1,4 @@ -use crate::{Color, Gradient}; +use iced_core::{Color, Gradient}; /// The coloring style of some drawing. #[derive(Debug, Clone, PartialEq)] diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index 8c0b2dfb..06e0b4d0 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -1,5 +1,5 @@ -use crate::alignment; -use crate::{Color, Font, Point}; +use iced_core::alignment; +use iced_core::{Color, Font, Point}; /// A bunch of text that can be drawn to a canvas #[derive(Debug, Clone)] diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs index da46c30f..03211160 100644 --- a/graphics/src/image/raster.rs +++ b/graphics/src/image/raster.rs @@ -1,8 +1,8 @@ //! Raster image loading and caching. use crate::image::Storage; -use crate::Size; -use iced_native::image; +use iced_core::image; +use iced_core::Size; use bitflags::bitflags; use std::collections::{HashMap, HashSet}; diff --git a/graphics/src/image/storage.rs b/graphics/src/image/storage.rs index 1b5b5c35..4caa6141 100644 --- a/graphics/src/image/storage.rs +++ b/graphics/src/image/storage.rs @@ -1,5 +1,5 @@ //! Store images. -use crate::Size; +use iced_core::Size; use std::fmt::Debug; diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs index c950ccd6..32729acd 100644 --- a/graphics/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -1,9 +1,8 @@ //! Vector image loading and caching use crate::image::Storage; -use crate::Color; -use iced_native::svg; -use iced_native::Size; +use iced_core::svg; +use iced_core::{Color, Size}; use resvg::tiny_skia; use resvg::usvg; diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index e56f8ad8..c6f9cf57 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -27,17 +27,17 @@ mod transformation; mod viewport; pub mod backend; +pub mod compositor; pub mod image; -pub mod overlay; pub mod primitive; pub mod renderer; -pub mod window; #[cfg(feature = "geometry")] pub mod geometry; pub use antialiasing::Antialiasing; pub use backend::Backend; +pub use compositor::Compositor; pub use error::Error; pub use primitive::Primitive; pub use renderer::Renderer; @@ -47,9 +47,4 @@ pub use viewport::Viewport; #[cfg(feature = "geometry")] pub use geometry::Geometry; -pub use iced_native::alignment; -pub use iced_native::text; -pub use iced_native::{ - Alignment, Background, Color, Font, Gradient, Point, Rectangle, Size, - Vector, -}; +pub use iced_core as core; diff --git a/graphics/src/overlay.rs b/graphics/src/overlay.rs deleted file mode 100644 index bc0ed744..00000000 --- a/graphics/src/overlay.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Display interactive elements on top of other widgets. -pub mod menu; diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs deleted file mode 100644 index 8b489e5e..00000000 --- a/graphics/src/overlay/menu.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Build and show dropdown menus. - -pub use iced_style::menu::{Appearance, StyleSheet}; diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index f900b3fd..195b62da 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -1,8 +1,7 @@ -use crate::alignment; - -use iced_native::image; -use iced_native::svg; -use iced_native::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; +use iced_core::alignment; +use iced_core::image; +use iced_core::svg; +use iced_core::{Background, Color, Font, Gradient, Rectangle, Size, Vector}; use bytemuck::{Pod, Zeroable}; use std::sync::Arc; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index cb57f429..7bc462ef 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -1,15 +1,15 @@ //! Create a renderer from a [`Backend`]. use crate::backend::{self, Backend}; -use crate::{Primitive, Vector}; +use crate::Primitive; -use iced_native::image; -use iced_native::layout; -use iced_native::renderer; -use iced_native::svg; -use iced_native::text::{self, Text}; -use iced_native::{Background, Color, Element, Font, Point, Rectangle, Size}; - -pub use iced_native::renderer::Style; +use iced_core::image; +use iced_core::layout; +use iced_core::renderer; +use iced_core::svg; +use iced_core::text::{self, Text}; +use iced_core::{ + Background, Color, Element, Font, Point, Rectangle, Size, Vector, +}; use std::borrow::Cow; use std::marker::PhantomData; @@ -52,7 +52,7 @@ impl Renderer { } } -impl iced_native::Renderer for Renderer +impl iced_core::Renderer for Renderer where B: Backend, { diff --git a/graphics/src/viewport.rs b/graphics/src/viewport.rs index 2c0b541a..5792555d 100644 --- a/graphics/src/viewport.rs +++ b/graphics/src/viewport.rs @@ -1,4 +1,6 @@ -use crate::{Size, Transformation}; +use crate::Transformation; + +use iced_core::Size; /// A viewing region for displaying computer graphics. #[derive(Debug, Clone)] diff --git a/graphics/src/window.rs b/graphics/src/window.rs deleted file mode 100644 index a38b81f3..00000000 --- a/graphics/src/window.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Draw graphics to window surfaces. -pub mod compositor; - -#[cfg(feature = "opengl")] -pub mod gl_compositor; - -pub use compositor::Compositor; - -#[cfg(feature = "opengl")] -pub use gl_compositor::GLCompositor; diff --git a/graphics/src/window/compositor.rs b/graphics/src/window/compositor.rs deleted file mode 100644 index 15f8dab5..00000000 --- a/graphics/src/window/compositor.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! A compositor is responsible for initializing a renderer and managing window -//! surfaces. -use crate::{Color, Error, Viewport}; - -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; -use thiserror::Error; - -/// A graphics compositor that can draw to windows. -pub trait Compositor: Sized { - /// The settings of the backend. - type Settings: Default; - - /// The iced renderer of the backend. - type Renderer: iced_native::Renderer; - - /// The surface of the backend. - type Surface; - - /// Creates a new [`Compositor`]. - fn new( - settings: Self::Settings, - compatible_window: Option<&W>, - ) -> Result<(Self, Self::Renderer), Error>; - - /// Crates a new [`Surface`] for the given window. - /// - /// [`Surface`]: Self::Surface - fn create_surface( - &mut self, - window: &W, - width: u32, - height: u32, - ) -> Self::Surface; - - /// Configures a new [`Surface`] with the given dimensions. - /// - /// [`Surface`]: Self::Surface - fn configure_surface( - &mut self, - surface: &mut Self::Surface, - width: u32, - height: u32, - ); - - /// Returns [`Information`] used by this [`Compositor`]. - fn fetch_information(&self) -> Information; - - /// Presents the [`Renderer`] primitives to the next frame of the given [`Surface`]. - /// - /// [`Renderer`]: Self::Renderer - /// [`Surface`]: Self::Surface - fn present>( - &mut self, - renderer: &mut Self::Renderer, - surface: &mut Self::Surface, - viewport: &Viewport, - background_color: Color, - overlay: &[T], - ) -> Result<(), SurfaceError>; -} - -/// Result of an unsuccessful call to [`Compositor::present`]. -#[derive(Clone, PartialEq, Eq, Debug, Error)] -pub enum SurfaceError { - /// A timeout was encountered while trying to acquire the next frame. - #[error( - "A timeout was encountered while trying to acquire the next frame" - )] - Timeout, - /// The underlying surface has changed, and therefore the surface must be updated. - #[error( - "The underlying surface has changed, and therefore the surface must be updated." - )] - Outdated, - /// The swap chain has been lost and needs to be recreated. - #[error("The surface has been lost and needs to be recreated")] - Lost, - /// There is no more memory left to allocate a new frame. - #[error("There is no more memory left to allocate a new frame")] - OutOfMemory, -} - -/// Contains informations about the graphics (e.g. graphics adapter, graphics backend). -#[derive(Debug)] -pub struct Information { - /// Contains the graphics adapter. - pub adapter: String, - /// Contains the graphics backend. - pub backend: String, -} diff --git a/graphics/src/window/gl_compositor.rs b/graphics/src/window/gl_compositor.rs deleted file mode 100644 index 3e6dfd9e..00000000 --- a/graphics/src/window/gl_compositor.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! A compositor is responsible for initializing a renderer and managing window -//! surfaces. -use crate::window::compositor::Information; -use crate::{Color, Error, Size, Viewport}; - -use core::ffi::c_void; - -/// A basic OpenGL compositor. -/// -/// A compositor is responsible for initializing a renderer and managing window -/// surfaces. -/// -/// For now, this compositor only deals with a single global surface -/// for drawing. However, the trait will most likely change in the near future -/// to handle multiple surfaces at once. -/// -/// If you implement an OpenGL renderer, you can implement this trait to ease -/// integration with existing windowing shells, like `iced_glutin`. -pub trait GLCompositor: Sized { - /// The renderer of the [`GLCompositor`]. - /// - /// This should point to your renderer type, which could be a type alias - /// of the [`Renderer`] provided in this crate with with a specific - /// [`Backend`]. - /// - /// [`Renderer`]: crate::Renderer - /// [`Backend`]: crate::Backend - type Renderer: iced_native::Renderer; - - /// The settings of the [`GLCompositor`]. - /// - /// It's up to you to decide the configuration supported by your renderer! - type Settings: Default; - - /// Creates a new [`GLCompositor`] and [`Renderer`] with the given - /// [`Settings`] and an OpenGL address loader function. - /// - /// # Safety - /// The `loader_function` should resolve to valid OpenGL bindings. - /// - /// [`Renderer`]: crate::Renderer - /// [`Backend`]: crate::Backend - /// [`Settings`]: Self::Settings - #[allow(unsafe_code)] - unsafe fn new( - settings: Self::Settings, - loader_function: impl FnMut(&str) -> *const c_void, - ) -> Result<(Self, Self::Renderer), Error>; - - /// Returns the amount of samples that should be used when configuring - /// an OpenGL context for this [`GLCompositor`]. - fn sample_count(settings: &Self::Settings) -> u32; - - /// Resizes the viewport of the [`GLCompositor`]. - fn resize_viewport(&mut self, physical_size: Size); - - /// Returns [`Information`] used by this [`GLCompositor`]. - fn fetch_information(&self) -> Information; - - /// Presents the primitives of the [`Renderer`] to the next frame of the - /// [`GLCompositor`]. - /// - /// [`Renderer`]: crate::Renderer - fn present>( - &mut self, - renderer: &mut Self::Renderer, - viewport: &Viewport, - background_color: Color, - overlay: &[T], - ); -} diff --git a/lazy/Cargo.toml b/lazy/Cargo.toml deleted file mode 100644 index c739b312..00000000 --- a/lazy/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "iced_lazy" -version = "0.5.0" -authors = ["Héctor Ramón Jiménez "] -edition = "2021" -description = "Lazy widgets for Iced" -license = "MIT" -repository = "https://github.com/iced-rs/iced" -documentation = "https://docs.rs/iced_lazy" -keywords = ["gui", "ui", "graphics", "interface", "widgets"] -categories = ["gui"] - -[dependencies] -ouroboros = "0.13" - -[dependencies.iced_native] -version = "0.9" -path = "../native" diff --git a/lazy/src/cache.rs b/lazy/src/cache.rs deleted file mode 100644 index 5b4a39f6..00000000 --- a/lazy/src/cache.rs +++ /dev/null @@ -1,13 +0,0 @@ -use iced_native::overlay; -use iced_native::Element; - -use ouroboros::self_referencing; - -#[self_referencing(pub_extras)] -pub struct Cache<'a, Message: 'a, Renderer: 'a> { - pub element: Element<'a, Message, Renderer>, - - #[borrows(mut element)] - #[covariant] - overlay: Option>, -} diff --git a/lazy/src/component.rs b/lazy/src/component.rs deleted file mode 100644 index b23da9f7..00000000 --- a/lazy/src/component.rs +++ /dev/null @@ -1,575 +0,0 @@ -//! Build and reuse custom widgets using The Elm Architecture. -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget; -use iced_native::widget::tree::{self, Tree}; -use iced_native::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, -}; - -use ouroboros::self_referencing; -use std::cell::RefCell; -use std::marker::PhantomData; - -/// A reusable, custom widget that uses The Elm Architecture. -/// -/// A [`Component`] allows you to implement custom widgets as if they were -/// `iced` applications with encapsulated state. -/// -/// In other words, a [`Component`] allows you to turn `iced` applications into -/// custom widgets and embed them without cumbersome wiring. -/// -/// A [`Component`] produces widgets that may fire an [`Event`](Component::Event) -/// and update the internal state of the [`Component`]. -/// -/// Additionally, a [`Component`] is capable of producing a `Message` to notify -/// the parent application of any relevant interactions. -pub trait Component { - /// The internal state of this [`Component`]. - type State: Default; - - /// The type of event this [`Component`] handles internally. - type Event; - - /// Processes an [`Event`](Component::Event) and updates the [`Component`] state accordingly. - /// - /// It can produce a `Message` for the parent application. - fn update( - &mut self, - state: &mut Self::State, - event: Self::Event, - ) -> Option; - - /// Produces the widgets of the [`Component`], which may trigger an [`Event`](Component::Event) - /// on user interaction. - fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer>; - - /// Update the [`Component`] state based on the provided [`Operation`](widget::Operation) - /// - /// By default, it does nothing. - fn operate( - &self, - _state: &mut Self::State, - _operation: &mut dyn widget::Operation, - ) { - } -} - -/// Turns an implementor of [`Component`] into an [`Element`] that can be -/// embedded in any application. -pub fn view<'a, C, Message, Renderer>( - component: C, -) -> Element<'a, Message, Renderer> -where - C: Component + 'a, - C::State: 'static, - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - Element::new(Instance { - state: RefCell::new(Some( - StateBuilder { - component: Box::new(component), - message: PhantomData, - state: PhantomData, - element_builder: |_| None, - } - .build(), - )), - }) -} - -struct Instance<'a, Message, Renderer, Event, S> { - state: RefCell>>, -} - -#[self_referencing] -struct State<'a, Message: 'a, Renderer: 'a, Event: 'a, S: 'a> { - component: - Box + 'a>, - message: PhantomData, - state: PhantomData, - - #[borrows(component)] - #[covariant] - element: Option>, -} - -impl<'a, Message, Renderer, Event, S> Instance<'a, Message, Renderer, Event, S> -where - S: Default, -{ - fn rebuild_element(&self, state: &S) { - let heads = self.state.borrow_mut().take().unwrap().into_heads(); - - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| Some(component.view(state)), - } - .build(), - ); - } - - fn rebuild_element_with_operation( - &self, - state: &mut S, - operation: &mut dyn widget::Operation, - ) { - let heads = self.state.borrow_mut().take().unwrap().into_heads(); - - heads.component.operate(state, operation); - - *self.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |component| Some(component.view(state)), - } - .build(), - ); - } - - fn with_element( - &self, - f: impl FnOnce(&Element<'_, Event, Renderer>) -> T, - ) -> T { - self.with_element_mut(|element| f(element)) - } - - fn with_element_mut( - &self, - f: impl FnOnce(&mut Element<'_, Event, Renderer>) -> T, - ) -> T { - self.state - .borrow_mut() - .as_mut() - .unwrap() - .with_element_mut(|element| f(element.as_mut().unwrap())) - } -} - -impl<'a, Message, Renderer, Event, S> Widget - for Instance<'a, Message, Renderer, Event, S> -where - S: 'static + Default, - Renderer: iced_native::Renderer, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(S::default()) - } - - fn children(&self) -> Vec { - self.rebuild_element(&S::default()); - self.with_element(|element| vec![Tree::new(element)]) - } - - fn diff(&self, tree: &mut Tree) { - self.rebuild_element(tree.state.downcast_ref()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }) - } - - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.with_element(|element| { - element.as_widget().layout(renderer, limits) - }) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self.with_element_mut(|element| { - element.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ) - }); - - local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if let Some(redraw_request) = local_shell.redraw_request() { - shell.request_redraw(redraw_request); - } - - if !local_messages.is_empty() { - let mut heads = self.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(tree.state.downcast_mut::(), message) - }) { - shell.publish(message); - } - - self.state = RefCell::new(Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |state| { - Some(state.view(tree.state.downcast_ref::())) - }, - } - .build(), - )); - - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)) - }); - - shell.invalidate_layout(); - } - - event_status - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.rebuild_element_with_operation( - tree.state.downcast_mut(), - operation, - ); - - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - } - - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element)); - - element.as_widget().operate( - &mut tree.children[0], - layout, - renderer, - &mut MapOperation { operation }, - ); - }); - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.with_element(|element| { - element.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - }); - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_element(|element| { - element.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let overlay = OverlayBuilder { - instance: self, - tree, - types: PhantomData, - overlay_builder: |instance, tree| { - instance.state.get_mut().as_mut().unwrap().with_element_mut( - move |element| { - element.as_mut().unwrap().as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - ) - }, - ) - }, - } - .build(); - - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); - - has_overlay.map(|position| { - overlay::Element::new( - position, - Box::new(OverlayInstance { - overlay: Some(overlay), - }), - ) - }) - } -} - -#[self_referencing] -struct Overlay<'a, 'b, Message, Renderer, Event, S> { - instance: &'a mut Instance<'b, Message, Renderer, Event, S>, - tree: &'a mut Tree, - types: PhantomData<(Message, Event, S)>, - - #[borrows(mut instance, mut tree)] - #[covariant] - overlay: Option>, -} - -struct OverlayInstance<'a, 'b, Message, Renderer, Event, S> { - overlay: Option>, -} - -impl<'a, 'b, Message, Renderer, Event, S> - OverlayInstance<'a, 'b, Message, Renderer, Event, S> -{ - fn with_overlay_maybe( - &self, - f: impl FnOnce(&overlay::Element<'_, Event, Renderer>) -> T, - ) -> Option { - self.overlay - .as_ref() - .unwrap() - .borrow_overlay() - .as_ref() - .map(f) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Event, Renderer>) -> T, - ) -> Option { - self.overlay - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) - } -} - -impl<'a, 'b, Message, Renderer, Event, S> overlay::Overlay - for OverlayInstance<'a, 'b, Message, Renderer, Event, S> -where - Renderer: iced_native::Renderer, - S: 'static + Default, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) - }) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> iced_native::event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self - .with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ) - }) - .unwrap_or(iced_native::event::Status::Ignored); - - local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if !local_messages.is_empty() { - let overlay = self.overlay.take().unwrap().into_heads(); - let mut heads = overlay.instance.state.take().unwrap().into_heads(); - - for message in local_messages.into_iter().filter_map(|message| { - heads - .component - .update(overlay.tree.state.downcast_mut::(), message) - }) { - shell.publish(message); - } - - *overlay.instance.state.borrow_mut() = Some( - StateBuilder { - component: heads.component, - message: PhantomData, - state: PhantomData, - element_builder: |state| { - Some(state.view(overlay.tree.state.downcast_ref::())) - }, - } - .build(), - ); - - overlay.instance.with_element(|element| { - overlay.tree.diff_children(std::slice::from_ref(&element)) - }); - - self.overlay = Some( - OverlayBuilder { - instance: overlay.instance, - tree: overlay.tree, - types: PhantomData, - overlay_builder: |_, _| None, - } - .build(), - ); - - shell.invalidate_layout(); - } - - event_status - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) - }) - .unwrap_or_default() - } -} diff --git a/lazy/src/lazy.rs b/lazy/src/lazy.rs deleted file mode 100644 index 5e909a49..00000000 --- a/lazy/src/lazy.rs +++ /dev/null @@ -1,396 +0,0 @@ -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::tree::{self, Tree}; -use iced_native::widget::{self, Widget}; -use iced_native::Element; -use iced_native::{Clipboard, Hasher, Length, Point, Rectangle, Shell, Size}; - -use ouroboros::self_referencing; -use std::cell::RefCell; -use std::hash::{Hash, Hasher as H}; -use std::rc::Rc; - -#[allow(missing_debug_implementations)] -pub struct Lazy<'a, Message, Renderer, Dependency, View> { - dependency: Dependency, - view: Box View + 'a>, - element: RefCell< - Option>>>>, - >, -} - -impl<'a, Message, Renderer, Dependency, View> - Lazy<'a, Message, Renderer, Dependency, View> -where - Dependency: Hash + 'a, - View: Into>, -{ - pub fn new( - dependency: Dependency, - view: impl Fn(&Dependency) -> View + 'a, - ) -> Self { - Self { - dependency, - view: Box::new(view), - element: RefCell::new(None), - } - } - - fn with_element( - &self, - f: impl FnOnce(&Element) -> T, - ) -> T { - f(self - .element - .borrow() - .as_ref() - .unwrap() - .borrow() - .as_ref() - .unwrap()) - } - - fn with_element_mut( - &self, - f: impl FnOnce(&mut Element) -> T, - ) -> T { - f(self - .element - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .as_mut() - .unwrap()) - } -} - -struct Internal { - element: Rc>>>, - hash: u64, -} - -impl<'a, Message, Renderer, Dependency, View> Widget - for Lazy<'a, Message, Renderer, Dependency, View> -where - View: Into> + 'static, - Dependency: Hash + 'a, - Message: 'static, - Renderer: iced_native::Renderer + 'static, -{ - fn tag(&self) -> tree::Tag { - struct Tag(T); - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let hash = hasher.finish(); - - let element = - Rc::new(RefCell::new(Some((self.view)(&self.dependency).into()))); - - (*self.element.borrow_mut()) = Some(element.clone()); - - tree::State::new(Internal { element, hash }) - } - - fn children(&self) -> Vec { - self.with_element(|element| vec![Tree::new(element.as_widget())]) - } - - fn diff(&self, tree: &mut Tree) { - let current = tree.state.downcast_mut::>(); - - let mut hasher = Hasher::default(); - self.dependency.hash(&mut hasher); - let new_hash = hasher.finish(); - - if current.hash != new_hash { - current.hash = new_hash; - - let element = (self.view)(&self.dependency).into(); - current.element = Rc::new(RefCell::new(Some(element))); - - (*self.element.borrow_mut()) = Some(current.element.clone()); - self.with_element(|element| { - tree.diff_children(std::slice::from_ref(&element.as_widget())) - }); - } else { - (*self.element.borrow_mut()) = Some(current.element.clone()); - } - } - - fn width(&self) -> Length { - self.with_element(|element| element.as_widget().width()) - } - - fn height(&self) -> Length { - self.with_element(|element| element.as_widget().height()) - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.with_element(|element| { - element.as_widget().layout(renderer, limits) - }) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.with_element(|element| { - element.as_widget().operate( - &mut tree.children[0], - layout, - renderer, - operation, - ); - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_element_mut(|element| { - element.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_element(|element| { - element.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.with_element(|element| { - element.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let overlay = Overlay(Some( - InnerBuilder { - cell: self.element.borrow().as_ref().unwrap().clone(), - element: self - .element - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .take() - .unwrap(), - tree: &mut tree.children[0], - overlay_builder: |element, tree| { - element.as_widget_mut().overlay(tree, layout, renderer) - }, - } - .build(), - )); - - let has_overlay = overlay - .with_overlay_maybe(|overlay| overlay::Element::position(overlay)); - - has_overlay - .map(|position| overlay::Element::new(position, Box::new(overlay))) - } -} - -#[self_referencing] -struct Inner<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a, -{ - cell: Rc>>>, - element: Element<'static, Message, Renderer>, - tree: &'a mut Tree, - - #[borrows(mut element, mut tree)] - #[covariant] - overlay: Option>, -} - -struct Overlay<'a, Message, Renderer>(Option>); - -impl<'a, Message, Renderer> Drop for Overlay<'a, Message, Renderer> { - fn drop(&mut self) { - let heads = self.0.take().unwrap().into_heads(); - (*heads.cell.borrow_mut()) = Some(heads.element); - } -} - -impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> { - fn with_overlay_maybe( - &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.0.as_ref().unwrap().borrow_overlay().as_ref().map(f) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.0 - .as_mut() - .unwrap() - .with_overlay_mut(|overlay| overlay.as_mut().map(f)) - } -} - -impl<'a, Message, Renderer> overlay::Overlay - for Overlay<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) - }) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .unwrap_or(iced_native::event::Status::Ignored) - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) - }) - .unwrap_or_default() - } -} - -impl<'a, Message, Renderer, Dependency, View> - From> - for Element<'a, Message, Renderer> -where - View: Into> + 'static, - Renderer: iced_native::Renderer + 'static, - Message: 'static, - Dependency: Hash + 'a, -{ - fn from(lazy: Lazy<'a, Message, Renderer, Dependency, View>) -> Self { - Self::new(lazy) - } -} diff --git a/lazy/src/lib.rs b/lazy/src/lib.rs deleted file mode 100644 index 41a28773..00000000 --- a/lazy/src/lib.rs +++ /dev/null @@ -1,66 +0,0 @@ -#![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" -)] -#![deny( - missing_debug_implementations, - unused_results, - clippy::extra_unused_lifetimes, - clippy::from_over_into, - clippy::needless_borrow, - clippy::new_without_default, - clippy::useless_conversion -)] -#![forbid(unsafe_code)] -#![allow( - clippy::await_holding_refcell_ref, - clippy::inherent_to_string, - clippy::type_complexity -)] -#![cfg_attr(docsrs, feature(doc_cfg))] -mod lazy; - -pub mod component; -pub mod responsive; - -pub use component::Component; -pub use lazy::Lazy; -pub use responsive::Responsive; - -mod cache; - -use iced_native::{Element, Size}; -use std::hash::Hash; - -pub fn lazy<'a, Message, Renderer, Dependency, View>( - dependency: Dependency, - view: impl Fn(&Dependency) -> View + 'a, -) -> Lazy<'a, Message, Renderer, Dependency, View> -where - Dependency: Hash + 'a, - View: Into>, -{ - Lazy::new(dependency, view) -} - -/// Turns an implementor of [`Component`] into an [`Element`] that can be -/// embedded in any application. -pub fn component<'a, C, Message, Renderer>( - component: C, -) -> Element<'a, Message, Renderer> -where - C: Component + 'a, - C::State: 'static, - Message: 'a, - Renderer: iced_native::Renderer + 'a, -{ - component::view(component) -} - -pub fn responsive<'a, Message, Renderer>( - f: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, -) -> Responsive<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - Responsive::new(f) -} diff --git a/lazy/src/responsive.rs b/lazy/src/responsive.rs deleted file mode 100644 index 57c07de1..00000000 --- a/lazy/src/responsive.rs +++ /dev/null @@ -1,425 +0,0 @@ -use iced_native::event; -use iced_native::layout::{self, Layout}; -use iced_native::mouse; -use iced_native::overlay; -use iced_native::renderer; -use iced_native::widget::tree::{self, Tree}; -use iced_native::widget::{self, horizontal_space}; -use iced_native::{ - Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget, -}; - -use ouroboros::self_referencing; -use std::cell::{RefCell, RefMut}; -use std::marker::PhantomData; -use std::ops::Deref; - -/// A widget that is aware of its dimensions. -/// -/// A [`Responsive`] widget will always try to fill all the available space of -/// its parent. -#[allow(missing_debug_implementations)] -pub struct Responsive<'a, Message, Renderer> { - view: Box Element<'a, Message, Renderer> + 'a>, - content: RefCell>, -} - -impl<'a, Message, Renderer> Responsive<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - /// Creates a new [`Responsive`] widget with a closure that produces its - /// contents. - /// - /// The `view` closure will be provided with the current [`Size`] of - /// the [`Responsive`] widget and, therefore, can be used to build the - /// contents of the widget in a responsive way. - pub fn new( - view: impl Fn(Size) -> Element<'a, Message, Renderer> + 'a, - ) -> Self { - Self { - view: Box::new(view), - content: RefCell::new(Content { - size: Size::ZERO, - layout: layout::Node::new(Size::ZERO), - element: Element::new(horizontal_space(0)), - }), - } - } -} - -struct Content<'a, Message, Renderer> { - size: Size, - layout: layout::Node, - element: Element<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn update( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - new_size: Size, - view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, - ) { - if self.size == new_size { - return; - } - - self.element = view(new_size); - self.size = new_size; - - tree.diff(&self.element); - - self.layout = self - .element - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, self.size)); - } - - fn resolve( - &mut self, - tree: &mut Tree, - renderer: R, - layout: Layout<'_>, - view: &dyn Fn(Size) -> Element<'a, Message, Renderer>, - f: impl FnOnce( - &mut Tree, - R, - Layout<'_>, - &mut Element<'a, Message, Renderer>, - ) -> T, - ) -> T - where - R: Deref, - { - self.update(tree, renderer.deref(), layout.bounds().size(), view); - - let content_layout = Layout::with_offset( - layout.position() - Point::ORIGIN, - &self.layout, - ); - - f(tree, renderer, content_layout, &mut self.element) - } -} - -struct State { - tree: RefCell, -} - -impl<'a, Message, Renderer> Widget - for Responsive<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State { - tree: RefCell::new(Tree::empty()), - }) - } - - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Fill - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout::Node::new(limits.max()) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - let state = tree.state.downcast_mut::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element - .as_widget() - .operate(tree, layout, renderer, operation); - }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let state = tree.state.downcast_mut::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element.as_widget_mut().on_event( - tree, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element.as_widget().draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::(); - let mut content = self.content.borrow_mut(); - - content.resolve( - &mut state.tree.borrow_mut(), - renderer, - layout, - &self.view, - |tree, renderer, layout, element| { - element.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) - }, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - use std::ops::DerefMut; - - let state = tree.state.downcast_ref::(); - - let overlay = OverlayBuilder { - content: self.content.borrow_mut(), - tree: state.tree.borrow_mut(), - types: PhantomData, - overlay_builder: |content: &mut RefMut>, tree| { - content.update( - tree, - renderer, - layout.bounds().size(), - &self.view, - ); - - let Content { - element, - layout: content_layout, - .. - } = content.deref_mut(); - - let content_layout = Layout::with_offset( - layout.bounds().position() - Point::ORIGIN, - content_layout, - ); - - element - .as_widget_mut() - .overlay(tree, content_layout, renderer) - }, - } - .build(); - - let has_overlay = overlay.with_overlay(|overlay| { - overlay.as_ref().map(overlay::Element::position) - }); - - has_overlay - .map(|position| overlay::Element::new(position, Box::new(overlay))) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, - Message: 'a, -{ - fn from(responsive: Responsive<'a, Message, Renderer>) -> Self { - Self::new(responsive) - } -} - -#[self_referencing] -struct Overlay<'a, 'b, Message, Renderer> { - content: RefMut<'a, Content<'b, Message, Renderer>>, - tree: RefMut<'a, Tree>, - types: PhantomData, - - #[borrows(mut content, mut tree)] - #[covariant] - overlay: Option>, -} - -impl<'a, 'b, Message, Renderer> Overlay<'a, 'b, Message, Renderer> { - fn with_overlay_maybe( - &self, - f: impl FnOnce(&overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.borrow_overlay().as_ref().map(f) - } - - fn with_overlay_mut_maybe( - &mut self, - f: impl FnOnce(&mut overlay::Element<'_, Message, Renderer>) -> T, - ) -> Option { - self.with_overlay_mut(|overlay| overlay.as_mut().map(f)) - } -} - -impl<'a, 'b, Message, Renderer> overlay::Overlay - for Overlay<'a, 'b, Message, Renderer> -where - Renderer: iced_native::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.with_overlay_maybe(|overlay| { - let translation = position - overlay.position(); - - overlay.layout(renderer, bounds, translation) - }) - .unwrap_or_default() - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let _ = self.with_overlay_maybe(|overlay| { - overlay.draw(renderer, theme, style, layout, cursor_position); - }); - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.with_overlay_maybe(|overlay| { - overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .unwrap_or_default() - } - - fn on_event( - &mut self, - event: iced_native::Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .unwrap_or(iced_native::event::Status::Ignored) - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.with_overlay_maybe(|overlay| { - overlay.is_over(layout, cursor_position) - }) - .unwrap_or_default() - } -} diff --git a/native/Cargo.toml b/native/Cargo.toml index 1eedf0da..bc4e7ca1 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -11,9 +11,6 @@ repository = "https://github.com/iced-rs/iced" debug = [] [dependencies] -twox-hash = { version = "1.5", default-features = false } -unicode-segmentation = "1.6" -num-traits = "0.2" thiserror = "1" [dependencies.iced_core] @@ -24,7 +21,3 @@ path = "../core" version = "0.6" path = "../futures" features = ["thread-pool"] - -[dependencies.iced_style] -version = "0.7" -path = "../style" diff --git a/native/src/clipboard.rs b/native/src/clipboard.rs index c9105bc0..e727c4a7 100644 --- a/native/src/clipboard.rs +++ b/native/src/clipboard.rs @@ -3,28 +3,6 @@ use iced_futures::MaybeSend; use std::fmt; -/// A buffer for short-term storage and transfer within and between -/// applications. -pub trait Clipboard { - /// Reads the current content of the [`Clipboard`] as text. - fn read(&self) -> Option; - - /// Writes the given text contents to the [`Clipboard`]. - fn write(&mut self, contents: String); -} - -/// A null implementation of the [`Clipboard`] trait. -#[derive(Debug, Clone, Copy)] -pub struct Null; - -impl Clipboard for Null { - fn read(&self) -> Option { - None - } - - fn write(&mut self, _contents: String) {} -} - /// A clipboard action to be performed by some [`Command`]. /// /// [`Command`]: crate::Command diff --git a/native/src/command.rs b/native/src/command.rs index ca9d0b64..39bee8f6 100644 --- a/native/src/command.rs +++ b/native/src/command.rs @@ -28,7 +28,9 @@ impl Command { } /// Creates a [`Command`] that performs a [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation + 'static) -> Self { + pub fn widget( + operation: impl iced_core::widget::Operation + 'static, + ) -> Self { Self(iced_futures::Command::single(Action::Widget( widget::Action::new(operation), ))) diff --git a/native/src/debug/basic.rs b/native/src/debug/basic.rs index 92f614da..32f725a1 100644 --- a/native/src/debug/basic.rs +++ b/native/src/debug/basic.rs @@ -1,5 +1,5 @@ #![allow(missing_docs)] -use crate::time; +use crate::core::time; use std::collections::VecDeque; diff --git a/native/src/element.rs b/native/src/element.rs deleted file mode 100644 index 0a677d20..00000000 --- a/native/src/element.rs +++ /dev/null @@ -1,583 +0,0 @@ -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Color, Layout, Length, Point, Rectangle, Shell, Widget, -}; - -use std::any::Any; -use std::borrow::Borrow; - -/// A generic [`Widget`]. -/// -/// It is useful to build composable user interfaces that do not leak -/// implementation details in their __view logic__. -/// -/// If you have a [built-in widget], you should be able to use `Into` -/// to turn it into an [`Element`]. -/// -/// [built-in widget]: crate::widget -#[allow(missing_debug_implementations)] -pub struct Element<'a, Message, Renderer> { - widget: Box + 'a>, -} - -impl<'a, Message, Renderer> Element<'a, Message, Renderer> { - /// Creates a new [`Element`] containing the given [`Widget`]. - pub fn new(widget: impl Widget + 'a) -> Self - where - Renderer: crate::Renderer, - { - Self { - widget: Box::new(widget), - } - } - - /// Returns a reference to the [`Widget`] of the [`Element`], - pub fn as_widget(&self) -> &dyn Widget { - self.widget.as_ref() - } - - /// Returns a mutable reference to the [`Widget`] of the [`Element`], - pub fn as_widget_mut(&mut self) -> &mut dyn Widget { - self.widget.as_mut() - } - - /// Applies a transformation to the produced message of the [`Element`]. - /// - /// This method is useful when you want to decouple different parts of your - /// UI and make them __composable__. - /// - /// # Example - /// Imagine we want to use [our counter](index.html#usage). But instead of - /// showing a single counter, we want to display many of them. We can reuse - /// the `Counter` type as it is! - /// - /// We use composition to model the __state__ of our new application: - /// - /// ``` - /// # mod counter { - /// # pub struct Counter; - /// # } - /// use counter::Counter; - /// - /// struct ManyCounters { - /// counters: Vec, - /// } - /// ``` - /// - /// We can store the state of multiple counters now. However, the - /// __messages__ we implemented before describe the user interactions - /// of a __single__ counter. Right now, we need to also identify which - /// counter is receiving user interactions. Can we use composition again? - /// Yes. - /// - /// ``` - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # } - /// #[derive(Debug, Clone, Copy)] - /// pub enum Message { - /// Counter(usize, counter::Message) - /// } - /// ``` - /// - /// We compose the previous __messages__ with the index of the counter - /// producing them. Let's implement our __view logic__ now: - /// - /// ``` - /// # mod counter { - /// # type Text<'a> = iced_native::widget::Text<'a, iced_native::renderer::Null>; - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; - /// # - /// # impl Counter { - /// # pub fn view(&mut self) -> Text { - /// # Text::new("") - /// # } - /// # } - /// # } - /// # - /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; - /// # } - /// # - /// # use counter::Counter; - /// # - /// # struct ManyCounters { - /// # counters: Vec, - /// # } - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message { - /// # Counter(usize, counter::Message) - /// # } - /// use iced_native::Element; - /// use iced_native::widget::Row; - /// use iced_wgpu::Renderer; - /// - /// impl ManyCounters { - /// pub fn view(&mut self) -> Row { - /// // We can quickly populate a `Row` by folding over our counters - /// self.counters.iter_mut().enumerate().fold( - /// Row::new().spacing(20), - /// |row, (index, counter)| { - /// // We display the counter - /// let element: Element = - /// counter.view().into(); - /// - /// row.push( - /// // Here we turn our `Element` into - /// // an `Element` by combining the `index` and the - /// // message of the `element`. - /// element.map(move |message| Message::Counter(index, message)) - /// ) - /// } - /// ) - /// } - /// } - /// ``` - /// - /// Finally, our __update logic__ is pretty straightforward: simple - /// delegation. - /// - /// ``` - /// # mod counter { - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message {} - /// # pub struct Counter; - /// # - /// # impl Counter { - /// # pub fn update(&mut self, _message: Message) {} - /// # } - /// # } - /// # - /// # use counter::Counter; - /// # - /// # struct ManyCounters { - /// # counters: Vec, - /// # } - /// # - /// # #[derive(Debug, Clone, Copy)] - /// # pub enum Message { - /// # Counter(usize, counter::Message) - /// # } - /// impl ManyCounters { - /// pub fn update(&mut self, message: Message) { - /// match message { - /// Message::Counter(index, counter_msg) => { - /// if let Some(counter) = self.counters.get_mut(index) { - /// counter.update(counter_msg); - /// } - /// } - /// } - /// } - /// } - /// ``` - pub fn map( - self, - f: impl Fn(Message) -> B + 'a, - ) -> Element<'a, B, Renderer> - where - Message: 'a, - Renderer: crate::Renderer + 'a, - B: 'a, - { - Element::new(Map::new(self.widget, f)) - } - - /// Marks the [`Element`] as _to-be-explained_. - /// - /// The [`Renderer`] will explain the layout of the [`Element`] graphically. - /// This can be very useful for debugging your layout! - /// - /// [`Renderer`]: crate::Renderer - pub fn explain>( - self, - color: C, - ) -> Element<'a, Message, Renderer> - where - Message: 'static, - Renderer: crate::Renderer + 'a, - { - Element { - widget: Box::new(Explain::new(self, color.into())), - } - } -} - -impl<'a, Message, Renderer> Borrow + 'a> - for Element<'a, Message, Renderer> -{ - fn borrow(&self) -> &(dyn Widget + 'a) { - self.widget.borrow() - } -} - -impl<'a, Message, Renderer> Borrow + 'a> - for &Element<'a, Message, Renderer> -{ - fn borrow(&self) -> &(dyn Widget + 'a) { - self.widget.borrow() - } -} - -struct Map<'a, A, B, Renderer> { - widget: Box + 'a>, - mapper: Box B + 'a>, -} - -impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { - pub fn new( - widget: Box + 'a>, - mapper: F, - ) -> Map<'a, A, B, Renderer> - where - F: 'a + Fn(A) -> B, - { - Map { - widget, - mapper: Box::new(mapper), - } - } -} - -impl<'a, A, B, Renderer> Widget for Map<'a, A, B, Renderer> -where - Renderer: crate::Renderer + 'a, - A: 'a, - B: 'a, -{ - fn tag(&self) -> tree::Tag { - self.widget.tag() - } - - fn state(&self) -> tree::State { - self.widget.state() - } - - fn children(&self) -> Vec { - self.widget.children() - } - - fn diff(&self, tree: &mut Tree) { - self.widget.diff(tree) - } - - fn width(&self) -> Length { - self.widget.width() - } - - fn height(&self) -> Length { - self.widget.height() - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.widget.layout(renderer, limits) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - ) { - self.operation.scrollable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id); - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.widget.operate( - tree, - layout, - renderer, - &mut MapOperation { operation }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, B>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let status = self.widget.on_event( - tree, - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ); - - shell.merge(local_shell, &self.mapper); - - status - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - self.widget.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.widget.mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let mapper = &self.mapper; - - self.widget - .overlay(tree, layout, renderer) - .map(move |overlay| overlay.map(mapper)) - } -} - -struct Explain<'a, Message, Renderer: crate::Renderer> { - element: Element<'a, Message, Renderer>, - color: Color, -} - -impl<'a, Message, Renderer> Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn new(element: Element<'a, Message, Renderer>, color: Color) -> Self { - Explain { element, color } - } -} - -impl<'a, Message, Renderer> Widget - for Explain<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn width(&self) -> Length { - self.element.widget.width() - } - - fn height(&self) -> Length { - self.element.widget.height() - } - - fn tag(&self) -> tree::Tag { - self.element.widget.tag() - } - - fn state(&self) -> tree::State { - self.element.widget.state() - } - - fn children(&self) -> Vec { - self.element.widget.children() - } - - fn diff(&self, tree: &mut Tree) { - self.element.widget.diff(tree); - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - self.element.widget.layout(renderer, limits) - } - - fn operate( - &self, - state: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.element - .widget - .operate(state, layout, renderer, operation) - } - - fn on_event( - &mut self, - state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.element.widget.on_event( - state, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - fn explain_layout( - renderer: &mut Renderer, - color: Color, - layout: Layout<'_>, - ) { - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border_color: color, - border_width: 1.0, - border_radius: 0.0.into(), - }, - Color::TRANSPARENT, - ); - - for child in layout.children() { - explain_layout(renderer, color, child); - } - } - - self.element.widget.draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - - explain_layout(renderer, self.color, layout); - } - - fn mouse_interaction( - &self, - state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.element.widget.mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn overlay<'b>( - &'b mut self, - state: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.element.widget.overlay(state, layout, renderer) - } -} diff --git a/native/src/event.rs b/native/src/event.rs deleted file mode 100644 index bcfaf891..00000000 --- a/native/src/event.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Handle events of a user interface. -use crate::keyboard; -use crate::mouse; -use crate::touch; -use crate::window; - -/// A user interface event. -/// -/// _**Note:** This type is largely incomplete! If you need to track -/// additional events, feel free to [open an issue] and share your use case!_ -/// -/// [open an issue]: https://github.com/iced-rs/iced/issues -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A keyboard event - Keyboard(keyboard::Event), - - /// A mouse event - Mouse(mouse::Event), - - /// A window event - Window(window::Event), - - /// A touch event - Touch(touch::Event), - - /// A platform specific event - PlatformSpecific(PlatformSpecific), -} - -/// A platform specific event -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PlatformSpecific { - /// A MacOS specific event - MacOS(MacOS), -} - -/// Describes an event specific to MacOS -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacOS { - /// Triggered when the app receives an URL from the system - /// - /// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_ - /// - /// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19 - ReceivedUrl(String), -} - -/// The status of an [`Event`] after being processed. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Status { - /// The [`Event`] was **NOT** handled by any widget. - Ignored, - - /// The [`Event`] was handled and processed by a widget. - Captured, -} - -impl Status { - /// Merges two [`Status`] into one. - /// - /// `Captured` takes precedence over `Ignored`: - /// - /// ``` - /// use iced_native::event::Status; - /// - /// assert_eq!(Status::Ignored.merge(Status::Ignored), Status::Ignored); - /// assert_eq!(Status::Ignored.merge(Status::Captured), Status::Captured); - /// assert_eq!(Status::Captured.merge(Status::Ignored), Status::Captured); - /// assert_eq!(Status::Captured.merge(Status::Captured), Status::Captured); - /// ``` - pub fn merge(self, b: Self) -> Self { - match self { - Status::Ignored => b, - Status::Captured => Status::Captured, - } - } -} diff --git a/native/src/hasher.rs b/native/src/hasher.rs deleted file mode 100644 index fa52f16d..00000000 --- a/native/src/hasher.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// The hasher used to compare layouts. -#[derive(Debug, Default)] -pub struct Hasher(twox_hash::XxHash64); - -impl core::hash::Hasher for Hasher { - fn write(&mut self, bytes: &[u8]) { - self.0.write(bytes) - } - - fn finish(&self) -> u64 { - self.0.finish() - } -} diff --git a/native/src/image.rs b/native/src/image.rs deleted file mode 100644 index 70fbade0..00000000 --- a/native/src/image.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Load and draw raster graphics. -use crate::{Hasher, Rectangle, Size}; - -use std::hash::{Hash, Hasher as _}; -use std::path::PathBuf; -use std::sync::Arc; - -/// A handle of some image data. -#[derive(Debug, Clone)] -pub struct Handle { - id: u64, - data: Data, -} - -impl Handle { - /// Creates an image [`Handle`] pointing to the image of the given path. - /// - /// Makes an educated guess about the image format by examining the data in the file. - pub fn from_path>(path: T) -> Handle { - Self::from_data(Data::Path(path.into())) - } - - /// Creates an image [`Handle`] containing the image pixels directly. This - /// function expects the input data to be provided as a `Vec` of RGBA - /// pixels. - /// - /// This is useful if you have already decoded your image. - pub fn from_pixels( - width: u32, - height: u32, - pixels: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Rgba { - width, - height, - pixels: Bytes::new(pixels), - }) - } - - /// Creates an image [`Handle`] containing the image data directly. - /// - /// Makes an educated guess about the image format by examining the given data. - /// - /// This is useful if you already have your image loaded in-memory, maybe - /// because you downloaded or generated it procedurally. - pub fn from_memory( - bytes: impl AsRef<[u8]> + Send + Sync + 'static, - ) -> Handle { - Self::from_data(Data::Bytes(Bytes::new(bytes))) - } - - fn from_data(data: Data) -> Handle { - let mut hasher = Hasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data, - } - } - - /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id - } - - /// Returns a reference to the image [`Data`]. - pub fn data(&self) -> &Data { - &self.data - } -} - -impl From for Handle -where - T: Into, -{ - fn from(path: T) -> Handle { - Handle::from_path(path.into()) - } -} - -impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// A wrapper around raw image data. -/// -/// It behaves like a `&[u8]`. -#[derive(Clone)] -pub struct Bytes(Arc + Send + Sync + 'static>); - -impl Bytes { - /// Creates new [`Bytes`] around `data`. - pub fn new(data: impl AsRef<[u8]> + Send + Sync + 'static) -> Self { - Self(Arc::new(data)) - } -} - -impl std::fmt::Debug for Bytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.as_ref().as_ref().fmt(f) - } -} - -impl std::hash::Hash for Bytes { - fn hash(&self, state: &mut H) { - self.0.as_ref().as_ref().hash(state); - } -} - -impl AsRef<[u8]> for Bytes { - fn as_ref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -impl std::ops::Deref for Bytes { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - self.0.as_ref().as_ref() - } -} - -/// The data of a raster image. -#[derive(Clone, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - Bytes(Bytes), - - /// Decoded image pixels in RGBA format. - Rgba { - /// The width of the image. - width: u32, - /// The height of the image. - height: u32, - /// The pixels. - pixels: Bytes, - }, -} - -impl std::fmt::Debug for Data { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - Data::Rgba { width, height, .. } => { - write!(f, "Pixels({width} * {height})") - } - } - } -} - -/// A [`Renderer`] that can render raster graphics. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// The image Handle to be displayed. Iced exposes its own default implementation of a [`Handle`] - /// - /// [`Handle`]: Self::Handle - type Handle: Clone + Hash; - - /// Returns the dimensions of an image for the given [`Handle`]. - fn dimensions(&self, handle: &Self::Handle) -> Size; - - /// Draws an image with the given [`Handle`] and inside the provided - /// `bounds`. - fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); -} diff --git a/native/src/layout.rs b/native/src/layout.rs deleted file mode 100644 index 04954fb9..00000000 --- a/native/src/layout.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Position your widgets properly. -mod limits; -mod node; - -pub mod flex; - -pub use limits::Limits; -pub use node::Node; - -use crate::{Point, Rectangle, Vector}; - -/// The bounds of a [`Node`] and its children, using absolute coordinates. -#[derive(Debug, Clone, Copy)] -pub struct Layout<'a> { - position: Point, - node: &'a Node, -} - -impl<'a> Layout<'a> { - /// Creates a new [`Layout`] for the given [`Node`] at the origin. - pub fn new(node: &'a Node) -> Self { - Self::with_offset(Vector::new(0.0, 0.0), node) - } - - /// Creates a new [`Layout`] for the given [`Node`] with the provided offset - /// from the origin. - pub fn with_offset(offset: Vector, node: &'a Node) -> Self { - let bounds = node.bounds(); - - Self { - position: Point::new(bounds.x, bounds.y) + offset, - node, - } - } - - /// Returns the position of the [`Layout`]. - pub fn position(&self) -> Point { - self.position - } - - /// Returns the bounds of the [`Layout`]. - /// - /// The returned [`Rectangle`] describes the position and size of a - /// [`Node`]. - pub fn bounds(&self) -> Rectangle { - let bounds = self.node.bounds(); - - Rectangle { - x: self.position.x, - y: self.position.y, - width: bounds.width, - height: bounds.height, - } - } - - /// Returns an iterator over the [`Layout`] of the children of a [`Node`]. - pub fn children(self) -> impl Iterator> { - self.node.children().iter().map(move |node| { - Layout::with_offset( - Vector::new(self.position.x, self.position.y), - node, - ) - }) - } -} diff --git a/native/src/layout/DRUID_LICENSE b/native/src/layout/DRUID_LICENSE deleted file mode 100644 index d6456956..00000000 --- a/native/src/layout/DRUID_LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/native/src/layout/flex.rs b/native/src/layout/flex.rs deleted file mode 100644 index 5d70c2fc..00000000 --- a/native/src/layout/flex.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Distribute elements using a flex-based layout. -// This code is heavily inspired by the [`druid`] codebase. -// -// [`druid`]: https://github.com/xi-editor/druid -// -// Copyright 2018 The xi-editor Authors, Héctor Ramón -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use crate::Element; - -use crate::layout::{Limits, Node}; -use crate::{Alignment, Padding, Point, Size}; - -/// The main axis of a flex layout. -#[derive(Debug)] -pub enum Axis { - /// The horizontal axis - Horizontal, - - /// The vertical axis - Vertical, -} - -impl Axis { - fn main(&self, size: Size) -> f32 { - match self { - Axis::Horizontal => size.width, - Axis::Vertical => size.height, - } - } - - fn cross(&self, size: Size) -> f32 { - match self { - Axis::Horizontal => size.height, - Axis::Vertical => size.width, - } - } - - fn pack(&self, main: f32, cross: f32) -> (f32, f32) { - match self { - Axis::Horizontal => (main, cross), - Axis::Vertical => (cross, main), - } - } -} - -/// Computes the flex layout with the given axis and limits, applying spacing, -/// padding and alignment to the items as needed. -/// -/// It returns a new layout [`Node`]. -pub fn resolve( - axis: Axis, - renderer: &Renderer, - limits: &Limits, - padding: Padding, - spacing: f32, - align_items: Alignment, - items: &[Element<'_, Message, Renderer>], -) -> Node -where - Renderer: crate::Renderer, -{ - let limits = limits.pad(padding); - let total_spacing = spacing * items.len().saturating_sub(1) as f32; - let max_cross = axis.cross(limits.max()); - - let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(limits.fill())); - let mut available = axis.main(limits.max()) - total_spacing; - - let mut nodes: Vec = Vec::with_capacity(items.len()); - nodes.resize(items.len(), Node::default()); - - if align_items == Alignment::Fill { - let mut fill_cross = axis.cross(limits.min()); - - items.iter().for_each(|child| { - let cross_fill_factor = match axis { - Axis::Horizontal => child.as_widget().height(), - Axis::Vertical => child.as_widget().width(), - } - .fill_factor(); - - if cross_fill_factor == 0 { - let (max_width, max_height) = axis.pack(available, max_cross); - - let child_limits = - Limits::new(Size::ZERO, Size::new(max_width, max_height)); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - fill_cross = fill_cross.max(axis.cross(size)); - } - }); - - cross = fill_cross; - } - - for (i, child) in items.iter().enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); - - if fill_factor == 0 { - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(0.0, cross) - } else { - axis.pack(0.0, 0.0) - }; - - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(available, cross) - } else { - axis.pack(available, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget().layout(renderer, &child_limits); - let size = layout.size(); - - available -= axis.main(size); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(size)); - } - - nodes[i] = layout; - } else { - fill_sum += fill_factor; - } - } - - let remaining = available.max(0.0); - - for (i, child) in items.iter().enumerate() { - let fill_factor = match axis { - Axis::Horizontal => child.as_widget().width(), - Axis::Vertical => child.as_widget().height(), - } - .fill_factor(); - - if fill_factor != 0 { - let max_main = remaining * fill_factor as f32 / fill_sum as f32; - let min_main = if max_main.is_infinite() { - 0.0 - } else { - max_main - }; - - let (min_width, min_height) = if align_items == Alignment::Fill { - axis.pack(min_main, cross) - } else { - axis.pack(min_main, axis.cross(limits.min())) - }; - - let (max_width, max_height) = if align_items == Alignment::Fill { - axis.pack(max_main, cross) - } else { - axis.pack(max_main, max_cross) - }; - - let child_limits = Limits::new( - Size::new(min_width, min_height), - Size::new(max_width, max_height), - ); - - let layout = child.as_widget().layout(renderer, &child_limits); - - if align_items != Alignment::Fill { - cross = cross.max(axis.cross(layout.size())); - } - - nodes[i] = layout; - } - } - - let pad = axis.pack(padding.left, padding.top); - let mut main = pad.0; - - for (i, node) in nodes.iter_mut().enumerate() { - if i > 0 { - main += spacing; - } - - let (x, y) = axis.pack(main, pad.1); - - node.move_to(Point::new(x, y)); - - match axis { - Axis::Horizontal => { - node.align( - Alignment::Start, - align_items, - Size::new(0.0, cross), - ); - } - Axis::Vertical => { - node.align( - align_items, - Alignment::Start, - Size::new(cross, 0.0), - ); - } - } - - let size = node.size(); - - main += axis.main(size); - } - - let (width, height) = axis.pack(main - pad.0, cross); - let size = limits.resolve(Size::new(width, height)); - - Node::with_children(size.pad(padding), nodes) -} diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs deleted file mode 100644 index 5d3c1556..00000000 --- a/native/src/layout/limits.rs +++ /dev/null @@ -1,163 +0,0 @@ -#![allow(clippy::manual_clamp)] -use crate::{Length, Padding, Size}; - -/// A set of size constraints for layouting. -#[derive(Debug, Clone, Copy)] -pub struct Limits { - min: Size, - max: Size, - fill: Size, -} - -impl Limits { - /// No limits - pub const NONE: Limits = Limits { - min: Size::ZERO, - max: Size::INFINITY, - fill: Size::INFINITY, - }; - - /// Creates new [`Limits`] with the given minimum and maximum [`Size`]. - pub const fn new(min: Size, max: Size) -> Limits { - Limits { - min, - max, - fill: Size::INFINITY, - } - } - - /// Returns the minimum [`Size`] of the [`Limits`]. - pub fn min(&self) -> Size { - self.min - } - - /// Returns the maximum [`Size`] of the [`Limits`]. - pub fn max(&self) -> Size { - self.max - } - - /// Returns the fill [`Size`] of the [`Limits`]. - pub fn fill(&self) -> Size { - self.fill - } - - /// Applies a width constraint to the current [`Limits`]. - pub fn width(mut self, width: impl Into) -> Limits { - match width.into() { - Length::Shrink => { - self.fill.width = self.min.width; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.width = self.fill.width.min(self.max.width); - } - Length::Fixed(amount) => { - let new_width = amount.min(self.max.width).max(self.min.width); - - self.min.width = new_width; - self.max.width = new_width; - self.fill.width = new_width; - } - } - - self - } - - /// Applies a height constraint to the current [`Limits`]. - pub fn height(mut self, height: impl Into) -> Limits { - match height.into() { - Length::Shrink => { - self.fill.height = self.min.height; - } - Length::Fill | Length::FillPortion(_) => { - self.fill.height = self.fill.height.min(self.max.height); - } - Length::Fixed(amount) => { - let new_height = - amount.min(self.max.height).max(self.min.height); - - self.min.height = new_height; - self.max.height = new_height; - self.fill.height = new_height; - } - } - - self - } - - /// Applies a minimum width constraint to the current [`Limits`]. - pub fn min_width(mut self, min_width: f32) -> Limits { - self.min.width = self.min.width.max(min_width).min(self.max.width); - - self - } - - /// Applies a maximum width constraint to the current [`Limits`]. - pub fn max_width(mut self, max_width: f32) -> Limits { - self.max.width = self.max.width.min(max_width).max(self.min.width); - - self - } - - /// Applies a minimum height constraint to the current [`Limits`]. - pub fn min_height(mut self, min_height: f32) -> Limits { - self.min.height = self.min.height.max(min_height).min(self.max.height); - - self - } - - /// Applies a maximum height constraint to the current [`Limits`]. - pub fn max_height(mut self, max_height: f32) -> Limits { - self.max.height = self.max.height.min(max_height).max(self.min.height); - - self - } - - /// Shrinks the current [`Limits`] to account for the given padding. - pub fn pad(&self, padding: Padding) -> Limits { - self.shrink(Size::new(padding.horizontal(), padding.vertical())) - } - - /// Shrinks the current [`Limits`] by the given [`Size`]. - pub fn shrink(&self, size: Size) -> Limits { - let min = Size::new( - (self.min().width - size.width).max(0.0), - (self.min().height - size.height).max(0.0), - ); - - let max = Size::new( - (self.max().width - size.width).max(0.0), - (self.max().height - size.height).max(0.0), - ); - - let fill = Size::new( - (self.fill.width - size.width).max(0.0), - (self.fill.height - size.height).max(0.0), - ); - - Limits { min, max, fill } - } - - /// Removes the minimum width constraint for the current [`Limits`]. - pub fn loose(&self) -> Limits { - Limits { - min: Size::ZERO, - max: self.max, - fill: self.fill, - } - } - - /// Computes the resulting [`Size`] that fits the [`Limits`] given the - /// intrinsic size of some content. - pub fn resolve(&self, intrinsic_size: Size) -> Size { - Size::new( - intrinsic_size - .width - .min(self.max.width) - .max(self.fill.width), - intrinsic_size - .height - .min(self.max.height) - .max(self.fill.height), - ) - } -} diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs deleted file mode 100644 index e0c7dcb2..00000000 --- a/native/src/layout/node.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::{Alignment, Point, Rectangle, Size, Vector}; - -/// The bounds of an element and its children. -#[derive(Debug, Clone, Default)] -pub struct Node { - bounds: Rectangle, - children: Vec, -} - -impl Node { - /// Creates a new [`Node`] with the given [`Size`]. - pub const fn new(size: Size) -> Self { - Self::with_children(size, Vec::new()) - } - - /// Creates a new [`Node`] with the given [`Size`] and children. - pub const fn with_children(size: Size, children: Vec) -> Self { - Node { - bounds: Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - children, - } - } - - /// Returns the [`Size`] of the [`Node`]. - pub fn size(&self) -> Size { - Size::new(self.bounds.width, self.bounds.height) - } - - /// Returns the bounds of the [`Node`]. - pub fn bounds(&self) -> Rectangle { - self.bounds - } - - /// Returns the children of the [`Node`]. - pub fn children(&self) -> &[Node] { - &self.children - } - - /// Aligns the [`Node`] in the given space. - pub fn align( - &mut self, - horizontal_alignment: Alignment, - vertical_alignment: Alignment, - space: Size, - ) { - match horizontal_alignment { - Alignment::Start => {} - Alignment::Center => { - self.bounds.x += (space.width - self.bounds.width) / 2.0; - } - Alignment::End => { - self.bounds.x += space.width - self.bounds.width; - } - Alignment::Fill => { - self.bounds.width = space.width; - } - } - - match vertical_alignment { - Alignment::Start => {} - Alignment::Center => { - self.bounds.y += (space.height - self.bounds.height) / 2.0; - } - Alignment::End => { - self.bounds.y += space.height - self.bounds.height; - } - Alignment::Fill => { - self.bounds.height = space.height; - } - } - } - - /// Moves the [`Node`] to the given position. - pub fn move_to(&mut self, position: Point) { - self.bounds.x = position.x; - self.bounds.y = position.y; - } - - /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: Vector) -> Self { - Self { - bounds: self.bounds + translation, - ..self - } - } -} diff --git a/native/src/lib.rs b/native/src/lib.rs index c98827e7..0fc4f324 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -42,32 +42,19 @@ clippy::useless_conversion )] #![forbid(unsafe_code, rust_2018_idioms)] -#![allow(clippy::inherent_to_string, clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] pub mod clipboard; pub mod command; -pub mod event; pub mod font; -pub mod image; pub mod keyboard; -pub mod layout; -pub mod mouse; -pub mod overlay; pub mod program; -pub mod renderer; pub mod subscription; -pub mod svg; pub mod system; -pub mod text; -pub mod touch; pub mod user_interface; pub mod widget; pub mod window; -mod element; -mod hasher; mod runtime; -mod shell; // We disable debug capabilities on release builds unless the `debug` feature // is explicitly enabled. @@ -78,35 +65,13 @@ mod debug; #[path = "debug/null.rs"] mod debug; -pub use iced_core::alignment; -pub use iced_core::gradient; -pub use iced_core::time; -pub use iced_core::{ - color, Alignment, Background, Color, ContentFit, Length, Padding, Pixels, - Point, Rectangle, Size, Vector, -}; -pub use iced_futures::{executor, futures}; -pub use iced_style::application; -pub use iced_style::theme; +pub use iced_core as core; +pub use iced_futures as futures; -#[doc(no_inline)] -pub use executor::Executor; - -pub use clipboard::Clipboard; pub use command::Command; pub use debug::Debug; -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 overlay::Overlay; pub use program::Program; -pub use renderer::Renderer; pub use runtime::Runtime; -pub use shell::Shell; pub use subscription::Subscription; -pub use theme::Theme; pub use user_interface::UserInterface; -pub use widget::Widget; diff --git a/native/src/mouse.rs b/native/src/mouse.rs deleted file mode 100644 index 9ee406cf..00000000 --- a/native/src/mouse.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Track mouse events. - -pub mod click; - -pub use click::Click; -pub use iced_core::mouse::*; diff --git a/native/src/mouse/click.rs b/native/src/mouse/click.rs deleted file mode 100644 index 4a7d796c..00000000 --- a/native/src/mouse/click.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Track mouse clicks. -use crate::time::Instant; -use crate::Point; - -/// A mouse click. -#[derive(Debug, Clone, Copy)] -pub struct Click { - kind: Kind, - position: Point, - time: Instant, -} - -/// The kind of mouse click. -#[derive(Debug, Clone, Copy)] -pub enum Kind { - /// A single click - Single, - - /// A double click - Double, - - /// A triple click - Triple, -} - -impl Kind { - fn next(&self) -> Kind { - match self { - Kind::Single => Kind::Double, - Kind::Double => Kind::Triple, - Kind::Triple => Kind::Double, - } - } -} - -impl Click { - /// Creates a new [`Click`] with the given position and previous last - /// [`Click`]. - pub fn new(position: Point, previous: Option) -> Click { - let time = Instant::now(); - - let kind = if let Some(previous) = previous { - if previous.is_consecutive(position, time) { - previous.kind.next() - } else { - Kind::Single - } - } else { - Kind::Single - }; - - Click { - kind, - position, - time, - } - } - - /// Returns the [`Kind`] of [`Click`]. - pub fn kind(&self) -> Kind { - self.kind - } - - fn is_consecutive(&self, new_position: Point, time: Instant) -> bool { - let duration = if time > self.time { - Some(time - self.time) - } else { - None - }; - - self.position == new_position - && duration - .map(|duration| duration.as_millis() <= 300) - .unwrap_or(false) - } -} diff --git a/native/src/overlay.rs b/native/src/overlay.rs deleted file mode 100644 index 6cada416..00000000 --- a/native/src/overlay.rs +++ /dev/null @@ -1,125 +0,0 @@ -//! Display interactive elements on top of other widgets. -mod element; -mod group; - -pub mod menu; - -pub use element::Element; -pub use group::Group; -pub use menu::Menu; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::widget::Tree; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size}; - -/// An interactive component that can be displayed on top of other widgets. -pub trait Overlay -where - Renderer: crate::Renderer, -{ - /// Returns the layout [`Node`] of the [`Overlay`]. - /// - /// This [`Node`] is used by the runtime to compute the [`Layout`] of the - /// user interface. - /// - /// [`Node`]: layout::Node - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node; - - /// Draws the [`Overlay`] using the associated `Renderer`. - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ); - - /// Applies a [`widget::Operation`] to the [`Overlay`]. - fn operate( - &mut self, - _layout: Layout<'_>, - _renderer: &Renderer, - _operation: &mut dyn widget::Operation, - ) { - } - - /// Processes a runtime [`Event`]. - /// - /// It receives: - /// * an [`Event`] describing user interaction - /// * the computed [`Layout`] of the [`Overlay`] - /// * the current cursor position - /// * a mutable `Message` list, allowing the [`Overlay`] to produce - /// new messages based on user interaction. - /// * the `Renderer` - /// * a [`Clipboard`], if available - /// - /// By default, it does nothing. - fn on_event( - &mut self, - _event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - event::Status::Ignored - } - - /// Returns the current [`mouse::Interaction`] of the [`Overlay`]. - /// - /// By default, it returns [`mouse::Interaction::Idle`]. - fn mouse_interaction( - &self, - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse::Interaction::Idle - } - - /// Returns true if the cursor is over the [`Overlay`]. - /// - /// By default, it returns true if the bounds of the `layout` contain - /// the `cursor_position`. - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - layout.bounds().contains(cursor_position) - } -} - -/// Returns a [`Group`] of overlay [`Element`] children. -/// -/// This method will generally only be used by advanced users that are -/// implementing the [`Widget`](crate::Widget) trait. -pub fn from_children<'a, Message, Renderer>( - children: &'a mut [crate::Element<'_, Message, Renderer>], - tree: &'a mut Tree, - layout: Layout<'_>, - renderer: &Renderer, -) -> Option> -where - Renderer: crate::Renderer, -{ - let children = children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) - }) - .collect::>(); - - (!children.is_empty()).then(|| Group::with_children(children).overlay()) -} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs deleted file mode 100644 index 237d25d1..00000000 --- a/native/src/overlay/element.rs +++ /dev/null @@ -1,270 +0,0 @@ -pub use crate::Overlay; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector}; - -use std::any::Any; - -/// A generic [`Overlay`]. -#[allow(missing_debug_implementations)] -pub struct Element<'a, Message, Renderer> { - position: Point, - overlay: Box + 'a>, -} - -impl<'a, Message, Renderer> Element<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - /// Creates a new [`Element`] containing the given [`Overlay`]. - pub fn new( - position: Point, - overlay: Box + 'a>, - ) -> Self { - Self { position, overlay } - } - - /// Returns the position of the [`Element`]. - pub fn position(&self) -> Point { - self.position - } - - /// Translates the [`Element`]. - pub fn translate(mut self, translation: Vector) -> Self { - self.position = self.position + translation; - self - } - - /// Applies a transformation to the produced message of the [`Element`]. - pub fn map(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> - where - Message: 'a, - Renderer: 'a, - B: 'a, - { - Element { - position: self.position, - overlay: Box::new(Map::new(self.overlay, f)), - } - } - - /// Computes the layout of the [`Element`] in the given bounds. - pub fn layout( - &self, - renderer: &Renderer, - bounds: Size, - translation: Vector, - ) -> layout::Node { - self.overlay - .layout(renderer, bounds, self.position + translation) - } - - /// Processes a runtime [`Event`]. - pub fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.overlay.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - /// Returns the current [`mouse::Interaction`] of the [`Element`]. - pub fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.overlay.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - /// Draws the [`Element`] and its children using the given [`Layout`]. - pub fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - self.overlay - .draw(renderer, theme, style, layout, cursor_position) - } - - /// Applies a [`widget::Operation`] to the [`Element`]. - pub fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - self.overlay.operate(layout, renderer, operation); - } - - /// Returns true if the cursor is over the [`Element`]. - pub fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.overlay.is_over(layout, cursor_position) - } -} - -struct Map<'a, A, B, Renderer> { - content: Box + 'a>, - mapper: &'a dyn Fn(A) -> B, -} - -impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { - pub fn new( - content: Box + 'a>, - mapper: &'a dyn Fn(A) -> B, - ) -> Map<'a, A, B, Renderer> { - Map { content, mapper } - } -} - -impl<'a, A, B, Renderer> Overlay for Map<'a, A, B, Renderer> -where - Renderer: crate::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - self.content.layout(renderer, bounds, position) - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - self.operation.container(id, &mut |operation| { - operate_on_children(&mut MapOperation { operation }); - }); - } - - fn focusable( - &mut self, - state: &mut dyn widget::operation::Focusable, - id: Option<&widget::Id>, - ) { - self.operation.focusable(state, id); - } - - fn scrollable( - &mut self, - state: &mut dyn widget::operation::Scrollable, - id: Option<&widget::Id>, - ) { - self.operation.scrollable(state, id); - } - - fn text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(state, id) - } - - fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) { - self.operation.custom(state, id); - } - } - - self.content - .operate(layout, renderer, &mut MapOperation { operation }); - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, B>, - ) -> event::Status { - let mut local_messages = Vec::new(); - let mut local_shell = Shell::new(&mut local_messages); - - let event_status = self.content.on_event( - event, - layout, - cursor_position, - renderer, - clipboard, - &mut local_shell, - ); - - shell.merge(local_shell, self.mapper); - - event_status - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - self.content - .draw(renderer, theme, style, layout, cursor_position) - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.content.is_over(layout, cursor_position) - } -} diff --git a/native/src/overlay/group.rs b/native/src/overlay/group.rs deleted file mode 100644 index 1126f0cf..00000000 --- a/native/src/overlay/group.rs +++ /dev/null @@ -1,174 +0,0 @@ -use iced_core::{Point, Rectangle, Size}; - -use crate::event; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget; -use crate::{Clipboard, Event, Layout, Overlay, Shell}; - -/// An [`Overlay`] container that displays multiple overlay [`overlay::Element`] -/// children. -#[allow(missing_debug_implementations)] -pub struct Group<'a, Message, Renderer> { - children: Vec>, -} - -impl<'a, Message, Renderer> Group<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - /// Creates an empty [`Group`]. - pub fn new() -> Self { - Self::default() - } - - /// Creates a [`Group`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Group { children } - } - - /// Adds an [`overlay::Element`] to the [`Group`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } - - /// Turns the [`Group`] into an overlay [`overlay::Element`]. - pub fn overlay(self) -> overlay::Element<'a, Message, Renderer> { - overlay::Element::new(Point::ORIGIN, Box::new(self)) - } -} - -impl<'a, Message, Renderer> Default for Group<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - fn default() -> Self { - Self::with_children(Vec::new()) - } -} - -impl<'a, Message, Renderer> Overlay - for Group<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - let translation = position - Point::ORIGIN; - - layout::Node::with_children( - bounds, - self.children - .iter() - .map(|child| child.layout(renderer, bounds, translation)) - .collect(), - ) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(layout.children()) - .map(|(child, layout)| { - child.on_event( - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - for (child, layout) in self.children.iter().zip(layout.children()) { - child.draw(renderer, theme, style, layout, cursor_position); - } - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(layout.children()) - .map(|(child, layout)| { - child.mouse_interaction( - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn operate( - &mut self, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - operation.container(None, &mut |operation| { - self.children.iter_mut().zip(layout.children()).for_each( - |(child, layout)| { - child.operate(layout, renderer, operation); - }, - ) - }); - } - - fn is_over(&self, layout: Layout<'_>, cursor_position: Point) -> bool { - self.children - .iter() - .zip(layout.children()) - .any(|(child, layout)| child.is_over(layout, cursor_position)) - } -} - -impl<'a, Message, Renderer> From> - for overlay::Element<'a, Message, Renderer> -where - Renderer: 'a + crate::Renderer, - Message: 'a, -{ - fn from(group: Group<'a, Message, Renderer>) -> Self { - group.overlay() - } -} diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs deleted file mode 100644 index bd58a122..00000000 --- a/native/src/overlay/menu.rs +++ /dev/null @@ -1,519 +0,0 @@ -//! Build and show dropdown menus. -use crate::alignment; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::text::{self, Text}; -use crate::touch; -use crate::widget::container::{self, Container}; -use crate::widget::scrollable::{self, Scrollable}; -use crate::widget::Tree; -use crate::{ - Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, -}; - -pub use iced_style::menu::{Appearance, StyleSheet}; - -/// A list of selectable options. -#[allow(missing_debug_implementations)] -pub struct Menu<'a, T, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - state: &'a mut State, - options: &'a [T], - hovered_option: &'a mut Option, - last_selection: &'a mut Option, - width: f32, - padding: Padding, - text_size: Option, - font: Option, - style: ::Style, -} - -impl<'a, T, Renderer> Menu<'a, T, Renderer> -where - T: ToString + Clone, - Renderer: text::Renderer + 'a, - Renderer::Theme: - StyleSheet + container::StyleSheet + scrollable::StyleSheet, -{ - /// Creates a new [`Menu`] with the given [`State`], a list of options, and - /// the message to produced when an option is selected. - pub fn new( - state: &'a mut State, - options: &'a [T], - hovered_option: &'a mut Option, - last_selection: &'a mut Option, - ) -> Self { - Menu { - state, - options, - hovered_option, - last_selection, - width: 0.0, - padding: Padding::ZERO, - text_size: None, - font: None, - style: Default::default(), - } - } - - /// Sets the width of the [`Menu`]. - pub fn width(mut self, width: f32) -> Self { - self.width = width; - self - } - - /// Sets the [`Padding`] of the [`Menu`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`Menu`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the font of the [`Menu`]. - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the style of the [`Menu`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Turns the [`Menu`] into an overlay [`Element`] at the given target - /// position. - /// - /// The `target_height` will be used to display the menu either on top - /// of the target or under it, depending on the screen position and the - /// dimensions of the [`Menu`]. - pub fn overlay( - self, - position: Point, - target_height: f32, - ) -> overlay::Element<'a, Message, Renderer> { - overlay::Element::new( - position, - Box::new(Overlay::new(self, target_height)), - ) - } -} - -/// The local state of a [`Menu`]. -#[derive(Debug)] -pub struct State { - tree: Tree, -} - -impl State { - /// Creates a new [`State`] for a [`Menu`]. - pub fn new() -> Self { - Self { - tree: Tree::empty(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -struct Overlay<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - state: &'a mut Tree, - container: Container<'a, Message, Renderer>, - width: f32, - target_height: f32, - style: ::Style, -} - -impl<'a, Message, Renderer> Overlay<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a, - Renderer: text::Renderer, - Renderer::Theme: - StyleSheet + container::StyleSheet + scrollable::StyleSheet, -{ - pub fn new(menu: Menu<'a, T, Renderer>, target_height: f32) -> Self - where - T: Clone + ToString, - { - let Menu { - state, - options, - hovered_option, - last_selection, - width, - padding, - font, - text_size, - style, - } = menu; - - let container = Container::new(Scrollable::new(List { - options, - hovered_option, - last_selection, - font, - text_size, - padding, - style: style.clone(), - })); - - state.tree.diff(&container as &dyn Widget<_, _>); - - Self { - state: &mut state.tree, - container, - width, - target_height, - style, - } - } -} - -impl<'a, Message, Renderer> crate::Overlay - for Overlay<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn layout( - &self, - renderer: &Renderer, - bounds: Size, - position: Point, - ) -> layout::Node { - let space_below = bounds.height - (position.y + self.target_height); - let space_above = position.y; - - let limits = layout::Limits::new( - Size::ZERO, - Size::new( - bounds.width - position.x, - if space_below > space_above { - space_below - } else { - space_above - }, - ), - ) - .width(self.width); - - let mut node = self.container.layout(renderer, &limits); - - node.move_to(if space_below > space_above { - position + Vector::new(0.0, self.target_height) - } else { - position - Vector::new(0.0, node.size().height) - }); - - node - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.container.on_event( - self.state, - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.container.mouse_interaction( - self.state, - layout, - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - ) { - let appearance = theme.appearance(&self.style); - let bounds = layout.bounds(); - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: appearance.border_color, - border_width: appearance.border_width, - border_radius: appearance.border_radius.into(), - }, - appearance.background, - ); - - self.container.draw( - self.state, - renderer, - theme, - style, - layout, - cursor_position, - &bounds, - ); - } -} - -struct List<'a, T, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - options: &'a [T], - hovered_option: &'a mut Option, - last_selection: &'a mut Option, - padding: Padding, - text_size: Option, - font: Option, - style: ::Style, -} - -impl<'a, T, Message, Renderer> Widget - for List<'a, T, Renderer> -where - T: Clone + ToString, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - Length::Fill - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits.width(Length::Fill).height(Length::Shrink); - let text_size = - self.text_size.unwrap_or_else(|| renderer.default_size()); - - let size = { - let intrinsic = Size::new( - 0.0, - (text_size * 1.2 + self.padding.vertical()) - * self.options.len() as f32, - ); - - limits.resolve(intrinsic) - }; - - layout::Node::new(size) - } - - fn on_event( - &mut self, - _state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - if let Some(index) = *self.hovered_option { - if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); - } - } - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let text_size = self - .text_size - .unwrap_or_else(|| renderer.default_size()); - - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) - / (text_size * 1.2 + self.padding.vertical())) - as usize, - ); - } - } - Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let text_size = self - .text_size - .unwrap_or_else(|| renderer.default_size()); - - *self.hovered_option = Some( - ((cursor_position.y - bounds.y) - / (text_size * 1.2 + self.padding.vertical())) - as usize, - ); - - if let Some(index) = *self.hovered_option { - if let Some(option) = self.options.get(index) { - *self.last_selection = Some(option.clone()); - } - } - } - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - viewport: &Rectangle, - ) { - let appearance = theme.appearance(&self.style); - let bounds = layout.bounds(); - - let text_size = - self.text_size.unwrap_or_else(|| renderer.default_size()); - let option_height = - (text_size * 1.2 + self.padding.vertical()) as usize; - - let offset = viewport.y - bounds.y; - let start = (offset / option_height as f32) as usize; - let end = - ((offset + viewport.height) / option_height as f32).ceil() as usize; - - let visible_options = &self.options[start..end.min(self.options.len())]; - - for (i, option) in visible_options.iter().enumerate() { - let i = start + i; - let is_selected = *self.hovered_option == Some(i); - - let bounds = Rectangle { - x: bounds.x, - y: bounds.y + (option_height * i) as f32, - width: bounds.width, - height: text_size * 1.2 + self.padding.vertical(), - }; - - if is_selected { - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: Color::TRANSPARENT, - border_width: 0.0, - border_radius: appearance.border_radius.into(), - }, - appearance.selected_background, - ); - } - - renderer.fill_text(Text { - content: &option.to_string(), - bounds: Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: f32::INFINITY, - ..bounds - }, - size: text_size, - font: self.font.unwrap_or_else(|| renderer.default_font()), - color: if is_selected { - appearance.selected_text_color - } else { - appearance.text_color - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - } - } -} - -impl<'a, T, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: ToString + Clone, - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from(list: List<'a, T, Renderer>) -> Self { - Element::new(list) - } -} diff --git a/native/src/program.rs b/native/src/program.rs index 25cab332..44585cc5 100644 --- a/native/src/program.rs +++ b/native/src/program.rs @@ -1,6 +1,8 @@ //! Build interactive programs using The Elm Architecture. -use crate::text; -use crate::{Command, Element, Renderer}; +use crate::Command; + +use iced_core::text; +use iced_core::{Element, Renderer}; mod state; diff --git a/native/src/program/state.rs b/native/src/program/state.rs index 8ae1cacb..2fa9934d 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -1,9 +1,9 @@ -use crate::application; -use crate::event::{self, Event}; -use crate::mouse; -use crate::renderer; +use crate::core::event::{self, Event}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::{Clipboard, Point, Size}; use crate::user_interface::{self, UserInterface}; -use crate::{Clipboard, Command, Debug, Point, Program, Size}; +use crate::{Command, Debug, Program}; /// The execution state of a [`Program`]. It leverages caching, event /// processing, and rendering primitive storage. @@ -22,7 +22,6 @@ where impl

State

where P: Program + 'static, - ::Theme: application::StyleSheet, { /// Creates a new [`State`] with the provided [`Program`], initializing its /// primitive with the given logical bounds and renderer. @@ -91,7 +90,7 @@ where bounds: Size, cursor_position: Point, renderer: &mut P::Renderer, - theme: &::Theme, + theme: &::Theme, style: &renderer::Style, clipboard: &mut dyn Clipboard, debug: &mut Debug, @@ -182,10 +181,7 @@ fn build_user_interface<'a, P: Program>( renderer: &mut P::Renderer, size: Size, debug: &mut Debug, -) -> UserInterface<'a, P::Message, P::Renderer> -where - ::Theme: application::StyleSheet, -{ +) -> UserInterface<'a, P::Message, P::Renderer> { debug.view_started(); let view = program.view(); debug.view_finished(); diff --git a/native/src/renderer.rs b/native/src/renderer.rs deleted file mode 100644 index 2ac78982..00000000 --- a/native/src/renderer.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Write your own renderer. -#[cfg(debug_assertions)] -mod null; -#[cfg(debug_assertions)] -pub use null::Null; - -use crate::layout; -use crate::{Background, Color, Element, Rectangle, Vector}; - -/// A component that can be used by widgets to draw themselves on a screen. -pub trait Renderer: Sized { - /// The supported theme of the [`Renderer`]. - type Theme; - - /// Lays out the elements of a user interface. - /// - /// You should override this if you need to perform any operations before or - /// after layouting. For instance, trimming the measurements cache. - fn layout( - &mut self, - element: &Element<'_, Message, Self>, - limits: &layout::Limits, - ) -> layout::Node { - element.as_widget().layout(self, limits) - } - - /// 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)); - - /// Applies a `translation` to the primitives recorded in the given closure. - fn with_translation( - &mut self, - translation: Vector, - f: impl FnOnce(&mut Self), - ); - - /// Fills a [`Quad`] with the provided [`Background`]. - fn fill_quad(&mut self, quad: Quad, background: impl Into); - - /// Clears all of the recorded primitives in the [`Renderer`]. - fn clear(&mut self); -} - -/// A polygon with four sides. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Quad { - /// The bounds of the [`Quad`]. - pub bounds: Rectangle, - - /// The border radius of the [`Quad`]. - pub border_radius: BorderRadius, - - /// The border width of the [`Quad`]. - pub border_width: f32, - - /// The border color of the [`Quad`]. - pub border_color: Color, -} - -/// The border radi for the corners of a graphics primitive in the order: -/// top-left, top-right, bottom-right, bottom-left. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct BorderRadius([f32; 4]); - -impl From for BorderRadius { - fn from(w: f32) -> Self { - Self([w; 4]) - } -} - -impl From<[f32; 4]> for BorderRadius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - -impl From for [f32; 4] { - fn from(radi: BorderRadius) -> Self { - radi.0 - } -} - -/// The styling attributes of a [`Renderer`]. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Style { - /// The text color - pub text_color: Color, -} - -impl Default for Style { - fn default() -> Self { - Style { - text_color: Color::BLACK, - } - } -} diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs deleted file mode 100644 index 150ee786..00000000 --- a/native/src/renderer/null.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::renderer::{self, Renderer}; -use crate::text::{self, Text}; -use crate::{Background, Font, Point, Rectangle, Size, Theme, Vector}; - -use std::borrow::Cow; - -/// A renderer that does nothing. -/// -/// It can be useful if you are writing tests! -#[derive(Debug, Clone, Copy, Default)] -pub struct Null; - -impl Null { - /// Creates a new [`Null`] renderer. - pub fn new() -> Null { - Null - } -} - -impl Renderer for Null { - type Theme = Theme; - - fn with_layer(&mut self, _bounds: Rectangle, _f: impl FnOnce(&mut Self)) {} - - fn with_translation( - &mut self, - _translation: Vector, - _f: impl FnOnce(&mut Self), - ) { - } - - fn clear(&mut self) {} - - fn fill_quad( - &mut self, - _quad: renderer::Quad, - _background: impl Into, - ) { - } -} - -impl text::Renderer for Null { - type Font = Font; - - const ICON_FONT: Font = Font::SansSerif; - const CHECKMARK_ICON: char = '0'; - const ARROW_DOWN_ICON: char = '0'; - - fn default_font(&self) -> Self::Font { - Font::SansSerif - } - - fn default_size(&self) -> f32 { - 16.0 - } - - fn load_font(&mut self, _font: Cow<'static, [u8]>) {} - - fn measure( - &self, - _content: &str, - _size: f32, - _font: Font, - _bounds: Size, - ) -> (f32, f32) { - (0.0, 20.0) - } - - fn hit_test( - &self, - _contents: &str, - _size: f32, - _font: Self::Font, - _bounds: Size, - _point: Point, - _nearest_only: bool, - ) -> Option { - None - } - - fn fill_text(&mut self, _text: Text<'_, Self::Font>) {} -} diff --git a/native/src/runtime.rs b/native/src/runtime.rs index 5b0a6925..1b81314f 100644 --- a/native/src/runtime.rs +++ b/native/src/runtime.rs @@ -1,6 +1,6 @@ //! Run commands and subscriptions. -use crate::event::{self, Event}; -use crate::Hasher; +use iced_core::event::{self, Event}; +use iced_core::Hasher; /// A native runtime with a generic executor and receiver of results. /// diff --git a/native/src/shell.rs b/native/src/shell.rs deleted file mode 100644 index 74a5c616..00000000 --- a/native/src/shell.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::window; - -/// A connection to the state of a shell. -/// -/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application, -/// like publishing messages or invalidating the current layout. -/// -/// [`Widget`]: crate::Widget -#[derive(Debug)] -pub struct Shell<'a, Message> { - messages: &'a mut Vec, - redraw_request: Option, - is_layout_invalid: bool, - are_widgets_invalid: bool, -} - -impl<'a, Message> Shell<'a, Message> { - /// Creates a new [`Shell`] with the provided buffer of messages. - pub fn new(messages: &'a mut Vec) -> Self { - Self { - messages, - redraw_request: None, - is_layout_invalid: false, - are_widgets_invalid: false, - } - } - - /// Returns true if the [`Shell`] contains no published messages - pub fn is_empty(&self) -> bool { - self.messages.is_empty() - } - - /// Publish the given `Message` for an application to process it. - pub fn publish(&mut self, message: Message) { - self.messages.push(message); - } - - /// Requests a new frame to be drawn at the given [`Instant`]. - pub fn request_redraw(&mut self, request: window::RedrawRequest) { - match self.redraw_request { - None => { - self.redraw_request = Some(request); - } - Some(current) if request < current => { - self.redraw_request = Some(request); - } - _ => {} - } - } - - /// Returns the requested [`Instant`] a redraw should happen, if any. - pub fn redraw_request(&self) -> Option { - self.redraw_request - } - - /// Returns whether the current layout is invalid or not. - pub fn is_layout_invalid(&self) -> bool { - self.is_layout_invalid - } - - /// Invalidates the current application layout. - /// - /// The shell will relayout the application widgets. - pub fn invalidate_layout(&mut self) { - self.is_layout_invalid = true; - } - - /// Triggers the given function if the layout is invalid, cleaning it in the - /// process. - pub fn revalidate_layout(&mut self, f: impl FnOnce()) { - if self.is_layout_invalid { - self.is_layout_invalid = false; - - f() - } - } - - /// Returns whether the widgets of the current application have been - /// invalidated. - pub fn are_widgets_invalid(&self) -> bool { - self.are_widgets_invalid - } - - /// Invalidates the current application widgets. - /// - /// The shell will rebuild and relayout the widget tree. - pub fn invalidate_widgets(&mut self) { - self.are_widgets_invalid = true; - } - - /// Merges the current [`Shell`] with another one by applying the given - /// function to the messages of the latter. - /// - /// This method is useful for composition. - pub fn merge(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) { - self.messages.extend(other.messages.drain(..).map(f)); - - if let Some(at) = other.redraw_request { - self.request_redraw(at); - } - - self.is_layout_invalid = - self.is_layout_invalid || other.is_layout_invalid; - - self.are_widgets_invalid = - self.are_widgets_invalid || other.are_widgets_invalid; - } -} diff --git a/native/src/subscription.rs b/native/src/subscription.rs index 16e78e82..b16bcb03 100644 --- a/native/src/subscription.rs +++ b/native/src/subscription.rs @@ -1,10 +1,9 @@ //! Listen to external events in your application. -use crate::event::{self, Event}; -use crate::window; -use crate::Hasher; - -use iced_futures::futures::{self, Future, Stream}; -use iced_futures::{BoxStream, MaybeSend}; +use crate::core::event::{self, Event}; +use crate::core::window; +use crate::core::Hasher; +use crate::futures::futures::{self, Future, Stream}; +use crate::futures::{BoxStream, MaybeSend}; use std::hash::Hash; @@ -144,7 +143,9 @@ where /// /// ``` /// use iced_native::subscription::{self, Subscription}; -/// use iced_native::futures::channel::mpsc; +/// use iced_native::futures::futures; +/// +/// use futures::channel::mpsc; /// /// pub enum Event { /// Ready(mpsc::Sender), @@ -174,7 +175,7 @@ where /// (Some(Event::Ready(sender)), State::Ready(receiver)) /// } /// State::Ready(mut receiver) => { -/// use iced_native::futures::StreamExt; +/// use futures::StreamExt; /// /// // Read next input sent from `Application` /// let input = receiver.select_next_some().await; diff --git a/native/src/svg.rs b/native/src/svg.rs deleted file mode 100644 index 9b98877a..00000000 --- a/native/src/svg.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Load and draw vector graphics. -use crate::{Color, Hasher, Rectangle, Size}; - -use std::borrow::Cow; -use std::hash::{Hash, Hasher as _}; -use std::path::PathBuf; -use std::sync::Arc; - -/// A handle of Svg data. -#[derive(Debug, Clone)] -pub struct Handle { - id: u64, - data: Arc, -} - -impl Handle { - /// Creates an SVG [`Handle`] pointing to the vector image of the given - /// path. - pub fn from_path(path: impl Into) -> Handle { - Self::from_data(Data::Path(path.into())) - } - - /// Creates an SVG [`Handle`] from raw bytes containing either an SVG string - /// or gzip compressed data. - /// - /// This is useful if you already have your SVG data in-memory, maybe - /// because you downloaded or generated it procedurally. - pub fn from_memory(bytes: impl Into>) -> Handle { - Self::from_data(Data::Bytes(bytes.into())) - } - - fn from_data(data: Data) -> Handle { - let mut hasher = Hasher::default(); - data.hash(&mut hasher); - - Handle { - id: hasher.finish(), - data: Arc::new(data), - } - } - - /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id - } - - /// Returns a reference to the SVG [`Data`]. - pub fn data(&self) -> &Data { - &self.data - } -} - -impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -/// The data of a vectorial image. -#[derive(Clone, Hash)] -pub enum Data { - /// File data - Path(PathBuf), - - /// In-memory data - /// - /// Can contain an SVG string or a gzip compressed data. - Bytes(Cow<'static, [u8]>), -} - -impl std::fmt::Debug for Data { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), - } - } -} - -/// A [`Renderer`] that can render vector graphics. -/// -/// [renderer]: crate::renderer -pub trait Renderer: crate::Renderer { - /// Returns the default dimensions of an SVG for the given [`Handle`]. - fn dimensions(&self, handle: &Handle) -> Size; - - /// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`. - fn draw(&mut self, handle: Handle, color: Option, bounds: Rectangle); -} diff --git a/native/src/text.rs b/native/src/text.rs deleted file mode 100644 index 4c72abc3..00000000 --- a/native/src/text.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Draw and interact with text. -use crate::alignment; -use crate::{Color, Point, Rectangle, Size}; - -use std::borrow::Cow; - -/// A paragraph. -#[derive(Debug, Clone, Copy)] -pub struct Text<'a, Font> { - /// The content of the paragraph. - pub content: &'a str, - - /// The bounds of the paragraph. - pub bounds: Rectangle, - - /// The size of the [`Text`]. - pub size: f32, - - /// The color of the [`Text`]. - pub color: Color, - - /// 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 result of hit testing on text. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Hit { - /// The point was within the bounds of the returned character index. - CharOffset(usize), -} - -impl Hit { - /// Computes the cursor position of the [`Hit`] . - pub fn cursor(self) -> usize { - match self { - Self::CharOffset(i) => i, - } - } -} - -/// A renderer capable of measuring and drawing [`Text`]. -pub trait Renderer: crate::Renderer { - /// The font type used. - type Font: Copy; - - /// The icon font of the backend. - const ICON_FONT: Self::Font; - - /// The `char` representing a ✔ icon in the [`ICON_FONT`]. - /// - /// [`ICON_FONT`]: Self::ICON_FONT - const CHECKMARK_ICON: char; - - /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`]. - /// - /// [`ICON_FONT`]: Self::ICON_FONT - const ARROW_DOWN_ICON: char; - - /// Returns the default [`Self::Font`]. - fn default_font(&self) -> Self::Font; - - /// Returns the default size of [`Text`]. - fn default_size(&self) -> f32; - - /// Measures the text in the given bounds and returns the minimum boundaries - /// that can fit the contents. - fn measure( - &self, - content: &str, - size: f32, - font: Self::Font, - bounds: Size, - ) -> (f32, f32); - - /// Measures the width of the text as if it were laid out in a single line. - fn measure_width(&self, content: &str, size: f32, font: Self::Font) -> f32 { - let (width, _) = self.measure(content, size, font, Size::INFINITY); - - width - } - - /// Tests whether the provided point is within the boundaries of text - /// laid out with the given parameters, returning information about - /// the nearest character. - /// - /// If `nearest_only` is true, the hit test does not consider whether the - /// the point is interior to any glyph bounds, returning only the character - /// with the nearest centeroid. - fn hit_test( - &self, - contents: &str, - size: f32, - font: Self::Font, - bounds: Size, - point: Point, - nearest_only: bool, - ) -> Option; - - /// Loads a [`Self::Font`] from its bytes. - fn load_font(&mut self, font: Cow<'static, [u8]>); - - /// Draws the given [`Text`]. - fn fill_text(&mut self, text: Text<'_, Self::Font>); -} diff --git a/native/src/touch.rs b/native/src/touch.rs deleted file mode 100644 index 18120644..00000000 --- a/native/src/touch.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Build touch events. -use crate::Point; - -/// A touch interaction. -#[derive(Debug, Clone, Copy, PartialEq)] -#[allow(missing_docs)] -pub enum Event { - /// A touch interaction was started. - FingerPressed { id: Finger, position: Point }, - - /// An on-going touch interaction was moved. - FingerMoved { id: Finger, position: Point }, - - /// A touch interaction was ended. - FingerLifted { id: Finger, position: Point }, - - /// A touch interaction was canceled. - FingerLost { id: Finger, position: Point }, -} - -/// A unique identifier representing a finger on a touch interaction. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Finger(pub u64); diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 68ccda55..315027fa 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,14 +1,12 @@ //! Implement your own event loop to drive a user interface. -use crate::application; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget; -use crate::window; -use crate::{ - Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, -}; +use crate::core::event::{self, Event}; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::widget; +use crate::core::window; +use crate::core::{Clipboard, Point, Rectangle, Size, Vector}; +use crate::core::{Element, Layout, Shell}; /// A set of interactive graphical elements with a specific [`Layout`]. /// @@ -34,8 +32,7 @@ pub struct UserInterface<'a, Message, Renderer> { impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer> where - Renderer: crate::Renderer, - Renderer::Theme: application::StyleSheet, + Renderer: iced_core::Renderer, { /// Builds a user interface for an [`Element`]. /// @@ -48,24 +45,21 @@ where /// is naive way to set up our application loop: /// /// ```no_run - /// use iced_native::Size; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_native::core::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_native::core::Size; + /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_wgpu::Renderer; + /// /// // Initialization /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); @@ -124,25 +118,21 @@ where /// completing [the previous example](#example): /// /// ```no_run - /// use iced_native::{clipboard, Size, Point}; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_native::core::renderer::Null as Renderer; /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } - /// # pub fn update(&mut self, message: ()) {} + /// # pub fn view(&self) -> iced_core::Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_native::core::{clipboard, Size, Point}; + /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_wgpu::Renderer; + /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); @@ -357,27 +347,24 @@ where /// [completing the last example](#example-1): /// /// ```no_run - /// use iced_native::clipboard; - /// use iced_native::renderer; - /// use iced_native::user_interface::{self, UserInterface}; - /// use iced_native::{Size, Point, Theme}; - /// use iced_wgpu::Renderer; - /// /// # mod iced_wgpu { - /// # pub use iced_native::renderer::Null as Renderer; + /// # pub use iced_native::core::renderer::Null as Renderer; + /// # pub type Theme = (); /// # } /// # - /// # use iced_native::widget::Column; - /// # /// # pub struct Counter; /// # /// # impl Counter { /// # pub fn new() -> Self { Counter } - /// # pub fn view(&self) -> Column<(), Renderer> { - /// # Column::new() - /// # } - /// # pub fn update(&mut self, message: ()) {} + /// # pub fn view(&self) -> Element<(), Renderer> { unimplemented!() } + /// # pub fn update(&mut self, _: ()) {} /// # } + /// use iced_native::core::clipboard; + /// use iced_native::core::renderer; + /// use iced_native::core::{Element, Size, Point}; + /// use iced_native::user_interface::{self, UserInterface}; + /// use iced_wgpu::{Renderer, Theme}; + /// /// let mut counter = Counter::new(); /// let mut cache = user_interface::Cache::new(); /// let mut renderer = Renderer::new(); @@ -386,6 +373,7 @@ where /// let mut clipboard = clipboard::Null; /// let mut events = Vec::new(); /// let mut messages = Vec::new(); + /// let mut theme = Theme::default(); /// /// loop { /// // Obtain system events... @@ -407,7 +395,7 @@ where /// ); /// /// // Draw the user interface - /// let mouse_cursor = user_interface.draw(&mut renderer, &Theme::default(), &renderer::Style::default(), cursor_position); + /// let mouse_cursor = user_interface.draw(&mut renderer, &theme, &renderer::Style::default(), cursor_position); /// /// cache = user_interface.into_cache(); /// diff --git a/native/src/widget.rs b/native/src/widget.rs index 2b3ca7be..0fdade54 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -11,212 +11,6 @@ //! source of inspiration. //! //! [renderer]: crate::renderer -pub mod button; -pub mod checkbox; -pub mod column; -pub mod container; -pub mod helpers; -pub mod image; -pub mod operation; -pub mod pane_grid; -pub mod pick_list; -pub mod progress_bar; -pub mod radio; -pub mod row; -pub mod rule; -pub mod scrollable; -pub mod slider; -pub mod space; -pub mod svg; -pub mod text; -pub mod text_input; -pub mod toggler; -pub mod tooltip; -pub mod tree; -pub mod vertical_slider; - mod action; -mod id; - -#[doc(no_inline)] -pub use button::Button; -#[doc(no_inline)] -pub use checkbox::Checkbox; -#[doc(no_inline)] -pub use column::Column; -#[doc(no_inline)] -pub use container::Container; -#[doc(no_inline)] -pub use helpers::*; -#[doc(no_inline)] -pub use image::Image; -#[doc(no_inline)] -pub use pane_grid::PaneGrid; -#[doc(no_inline)] -pub use pick_list::PickList; -#[doc(no_inline)] -pub use progress_bar::ProgressBar; -#[doc(no_inline)] -pub use radio::Radio; -#[doc(no_inline)] -pub use row::Row; -#[doc(no_inline)] -pub use rule::Rule; -#[doc(no_inline)] -pub use scrollable::Scrollable; -#[doc(no_inline)] -pub use slider::Slider; -#[doc(no_inline)] -pub use space::Space; -#[doc(no_inline)] -pub use svg::Svg; -#[doc(no_inline)] -pub use text::Text; -#[doc(no_inline)] -pub use text_input::TextInput; -#[doc(no_inline)] -pub use toggler::Toggler; -#[doc(no_inline)] -pub use tooltip::Tooltip; -#[doc(no_inline)] -pub use tree::Tree; -#[doc(no_inline)] -pub use vertical_slider::VerticalSlider; pub use action::Action; -pub use id::Id; -pub use operation::Operation; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::{Clipboard, Layout, Length, Point, Rectangle, Shell}; - -/// A component that displays information and allows interaction. -/// -/// If you want to build your own widgets, you will need to implement this -/// trait. -/// -/// # Examples -/// The repository has some [examples] showcasing how to implement a custom -/// widget: -/// -/// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using -/// [`lyon`]. -/// - [`custom_widget`], a demonstration of how to build a custom widget that -/// draws a circle. -/// - [`geometry`], a custom widget showcasing how to draw geometry with the -/// `Mesh2D` primitive in [`iced_wgpu`]. -/// -/// [examples]: https://github.com/iced-rs/iced/tree/0.8/examples -/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.8/examples/bezier_tool -/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.8/examples/custom_widget -/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.8/examples/geometry -/// [`lyon`]: https://github.com/nical/lyon -/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.8/wgpu -pub trait Widget -where - Renderer: crate::Renderer, -{ - /// Returns the width of the [`Widget`]. - fn width(&self) -> Length; - - /// Returns the height of the [`Widget`]. - fn height(&self) -> Length; - - /// Returns the [`layout::Node`] of the [`Widget`]. - /// - /// This [`layout::Node`] is used by the runtime to compute the [`Layout`] of the - /// user interface. - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node; - - /// Draws the [`Widget`] using the associated `Renderer`. - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ); - - /// Returns the [`Tag`] of the [`Widget`]. - /// - /// [`Tag`]: tree::Tag - fn tag(&self) -> tree::Tag { - tree::Tag::stateless() - } - - /// Returns the [`State`] of the [`Widget`]. - /// - /// [`State`]: tree::State - fn state(&self) -> tree::State { - tree::State::None - } - - /// Returns the state [`Tree`] of the children of the [`Widget`]. - fn children(&self) -> Vec { - Vec::new() - } - - /// Reconciliates the [`Widget`] with the provided [`Tree`]. - fn diff(&self, _tree: &mut Tree) {} - - /// Applies an [`Operation`] to the [`Widget`]. - fn operate( - &self, - _state: &mut Tree, - _layout: Layout<'_>, - _renderer: &Renderer, - _operation: &mut dyn Operation, - ) { - } - - /// Processes a runtime [`Event`]. - /// - /// By default, it does nothing. - fn on_event( - &mut self, - _state: &mut Tree, - _event: Event, - _layout: Layout<'_>, - _cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - event::Status::Ignored - } - - /// Returns the current [`mouse::Interaction`] of the [`Widget`]. - /// - /// By default, it returns [`mouse::Interaction::Idle`]. - fn mouse_interaction( - &self, - _state: &Tree, - _layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse::Interaction::Idle - } - - /// Returns the overlay of the [`Widget`], if there is any. - fn overlay<'a>( - &'a mut self, - _state: &'a mut Tree, - _layout: Layout<'_>, - _renderer: &Renderer, - ) -> Option> { - None - } -} diff --git a/native/src/widget/action.rs b/native/src/widget/action.rs index 3f1b6b6c..f50d7aec 100644 --- a/native/src/widget/action.rs +++ b/native/src/widget/action.rs @@ -1,8 +1,7 @@ -use crate::widget::operation::{ +use iced_core::widget::operation::{ self, Focusable, Operation, Scrollable, TextInput, }; -use crate::widget::Id; - +use iced_core::widget::Id; use iced_futures::MaybeSend; use std::any::Any; diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs deleted file mode 100644 index 39387173..00000000 --- a/native/src/widget/button.rs +++ /dev/null @@ -1,455 +0,0 @@ -//! Allow your users to perform actions by pressing a button. -//! -//! A [`Button`] has some local [`State`]. -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::touch; -use crate::widget::tree::{self, Tree}; -use crate::widget::Operation; -use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Point, - Rectangle, Shell, Vector, Widget, -}; - -pub use iced_style::button::{Appearance, StyleSheet}; - -/// A generic widget that produces a message when pressed. -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); -/// ``` -/// -/// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will -/// be disabled: -/// -/// ``` -/// # type Button<'a, Message> = -/// # iced_native::widget::Button<'a, Message, iced_native::renderer::Null>; -/// # -/// #[derive(Clone)] -/// enum Message { -/// ButtonPressed, -/// } -/// -/// fn disabled_button<'a>() -> Button<'a, Message> { -/// Button::new("I'm disabled!") -/// } -/// -/// fn enabled_button<'a>() -> Button<'a, Message> { -/// disabled_button().on_press(Message::ButtonPressed) -/// } -/// ``` -#[allow(missing_debug_implementations)] -pub struct Button<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - content: Element<'a, Message, Renderer>, - on_press: Option, - width: Length, - height: Length, - padding: Padding, - style: ::Style, -} - -impl<'a, Message, Renderer> Button<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`Button`] with the given content. - pub fn new(content: impl Into>) -> Self { - Button { - content: content.into(), - on_press: None, - width: Length::Shrink, - height: Length::Shrink, - padding: Padding::new(5.0), - style: ::Style::default(), - } - } - - /// Sets the width of the [`Button`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Button`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the [`Padding`] of the [`Button`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the message that will be produced when the [`Button`] is pressed. - /// - /// Unless `on_press` is called, the [`Button`] will be disabled. - pub fn on_press(mut self, msg: Message) -> Self { - self.on_press = Some(msg); - self - } - - /// Sets the style variant of this [`Button`]. - pub fn style( - mut self, - style: ::Style, - ) -> Self { - self.style = style; - self - } -} - -impl<'a, Message, Renderer> Widget - for Button<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.padding, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( - &mut tree.children[0], - event.clone(), - layout.children().next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ) { - return event::Status::Captured; - } - - update( - event, - layout, - cursor_position, - shell, - &self.on_press, - || tree.state.downcast_mut::(), - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - - let styling = draw( - renderer, - bounds, - cursor_position, - self.on_press.is_some(), - theme, - &self.style, - || tree.state.downcast_ref::(), - ); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: styling.text_color, - }, - content_layout, - cursor_position, - &bounds, - ); - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position, self.on_press.is_some()) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: Clone + 'a, - Renderer: crate::Renderer + 'a, - Renderer::Theme: StyleSheet, -{ - fn from(button: Button<'a, Message, Renderer>) -> Self { - Self::new(button) - } -} - -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_pressed: bool, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} - -/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] -/// accordingly. -pub fn update<'a, Message: Clone>( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - on_press: &Option, - state: impl FnOnce() -> &'a mut State, -) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if on_press.is_some() { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - let state = state(); - - state.is_pressed = true; - - return event::Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = on_press.clone() { - let state = state(); - - if state.is_pressed { - state.is_pressed = false; - - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = state(); - - state.is_pressed = false; - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws a [`Button`]. -pub fn draw<'a, Renderer: crate::Renderer>( - renderer: &mut Renderer, - bounds: Rectangle, - cursor_position: Point, - is_enabled: bool, - style_sheet: &dyn StyleSheet< - Style = ::Style, - >, - style: &::Style, - state: impl FnOnce() -> &'a State, -) -> Appearance -where - Renderer::Theme: StyleSheet, -{ - let is_mouse_over = bounds.contains(cursor_position); - - let styling = if !is_enabled { - style_sheet.disabled(style) - } else if is_mouse_over { - let state = state(); - - if state.is_pressed { - style_sheet.pressed(style) - } else { - style_sheet.hovered(style) - } - } else { - style_sheet.active(style) - }; - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - ..bounds - }, - border_radius: styling.border_radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: styling.border_radius.into(), - border_width: styling.border_width, - border_color: styling.border_color, - }, - styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - styling -} - -/// Computes the layout of a [`Button`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - padding: Padding, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits.width(width).height(height); - - let mut content = layout_content(renderer, &limits.pad(padding)); - let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()).pad(padding); - - content.move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(size, vec![content]) -} - -/// Returns the [`mouse::Interaction`] of a [`Button`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor_position: Point, - is_enabled: bool, -) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - - if is_mouse_over && is_enabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs deleted file mode 100644 index cd8b9c6b..00000000 --- a/native/src/widget/checkbox.rs +++ /dev/null @@ -1,321 +0,0 @@ -//! Show toggle controls using checkboxes. -use crate::alignment; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::text; -use crate::touch; -use crate::widget::{self, Row, Text, Tree}; -use crate::{ - Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Widget, -}; - -pub use iced_style::checkbox::{Appearance, StyleSheet}; - -/// The icon in a [`Checkbox`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Icon { - /// Font that will be used to display the `code_point`, - pub font: Font, - /// The unicode code point that will be used as the icon. - pub code_point: char, - /// Font size of the content. - pub size: Option, -} - -/// A box that can be checked. -/// -/// # Example -/// -/// ``` -/// # type Checkbox<'a, Message> = iced_native::widget::Checkbox<'a, Message, iced_native::renderer::Null>; -/// # -/// pub enum Message { -/// CheckboxToggled(bool), -/// } -/// -/// let is_checked = true; -/// -/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled); -/// ``` -/// -/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) -#[allow(missing_debug_implementations)] -pub struct Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - is_checked: bool, - on_toggle: Box Message + 'a>, - label: String, - width: Length, - size: f32, - spacing: f32, - text_size: Option, - font: Option, - icon: Icon, - style: ::Style, -} - -impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - /// The default size of a [`Checkbox`]. - const DEFAULT_SIZE: f32 = 20.0; - - /// The default spacing of a [`Checkbox`]. - const DEFAULT_SPACING: f32 = 15.0; - - /// Creates a new [`Checkbox`]. - /// - /// It expects: - /// * a boolean describing whether the [`Checkbox`] is checked or not - /// * the label of the [`Checkbox`] - /// * a function that will be called when the [`Checkbox`] is toggled. It - /// will receive the new state of the [`Checkbox`] and must produce a - /// `Message`. - pub fn new(label: impl Into, is_checked: bool, f: F) -> Self - where - F: 'a + Fn(bool) -> Message, - { - Checkbox { - is_checked, - on_toggle: Box::new(f), - label: label.into(), - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, - text_size: None, - font: None, - icon: Icon { - font: Renderer::ICON_FONT, - code_point: Renderer::CHECKMARK_ICON, - size: None, - }, - style: Default::default(), - } - } - - /// Sets the size of the [`Checkbox`]. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into().0; - self - } - - /// Sets the width of the [`Checkbox`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the spacing between the [`Checkbox`] and the text. - pub fn spacing(mut self, spacing: impl Into) -> Self { - self.spacing = spacing.into().0; - self - } - - /// Sets the text size of the [`Checkbox`]. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the [`Font`] of the text of the [`Checkbox`]. - /// - /// [`Font`]: crate::text::Renderer::Font - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the [`Icon`] of the [`Checkbox`]. - pub fn icon(mut self, icon: Icon) -> Self { - self.icon = icon; - self - } - - /// Sets the style of the [`Checkbox`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Checkbox<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center) - .push(Row::new().width(self.size).height(self.size)) - .push( - Text::new(&self.label) - .font(self.font.unwrap_or_else(|| renderer.default_font())) - .width(self.width) - .size( - self.text_size - .unwrap_or_else(|| renderer.default_size()), - ), - ) - .layout(renderer, limits) - } - - fn on_event( - &mut self, - _tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let mouse_over = layout.bounds().contains(cursor_position); - - if mouse_over { - shell.publish((self.on_toggle)(!self.is_checked)); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - _tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let mut children = layout.children(); - - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_checked) - } else { - theme.active(&self.style, self.is_checked) - }; - - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: custom_style.border_radius.into(), - border_width: custom_style.border_width, - border_color: custom_style.border_color, - }, - custom_style.background, - ); - - let Icon { - font, - code_point, - size, - } = &self.icon; - let size = size.unwrap_or(bounds.height * 0.7); - - if self.is_checked { - renderer.fill_text(text::Text { - content: &code_point.to_string(), - font: *font, - size, - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }, - color: custom_style.icon_color, - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - }); - } - } - - { - let label_layout = children.next().unwrap(); - - widget::text::draw( - renderer, - style, - label_layout, - &self.label, - self.text_size, - self.font, - widget::text::Appearance { - color: custom_style.text_color, - }, - alignment::Horizontal::Left, - alignment::Vertical::Center, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from( - checkbox: Checkbox<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(checkbox) - } -} diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs deleted file mode 100644 index ebe579d5..00000000 --- a/native/src/widget/column.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Distribute content vertically. -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::{Operation, Tree}; -use crate::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Widget, -}; - -/// A container that distributes its contents vertically. -#[allow(missing_debug_implementations)] -pub struct Column<'a, Message, Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - max_width: f32, - align_items: Alignment, - children: Vec>, -} - -impl<'a, Message, Renderer> Column<'a, Message, Renderer> { - /// Creates an empty [`Column`]. - pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Column`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Column { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - max_width: f32::INFINITY, - align_items: Alignment::Start, - children, - } - } - - /// Sets the vertical spacing _between_ elements. - /// - /// Custom margins per element do not exist in iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the [`Padding`] of the [`Column`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Column`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Column`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the maximum width of the [`Column`]. - pub fn max_width(mut self, max_width: impl Into) -> Self { - self.max_width = max_width.into().0; - self - } - - /// Sets the horizontal alignment of the contents of the [`Column`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; - self - } - - /// Adds an element to the [`Column`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } -} - -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget - for Column<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children); - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .max_width(self.max_width) - .width(self.width) - .height(self.height); - - layout::flex::resolve( - layout::flex::Axis::Vertical, - renderer, - &limits, - self.padding, - self.spacing, - self.align_items, - &self.children, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, &mut |operation| { - self.children - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|((child, state), layout)| { - child - .as_widget() - .operate(state, layout, renderer, operation); - }) - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: crate::Renderer + 'a, -{ - fn from(column: Column<'a, Message, Renderer>) -> Self { - Self::new(column) - } -} diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs deleted file mode 100644 index b77bf50d..00000000 --- a/native/src/widget/container.rs +++ /dev/null @@ -1,368 +0,0 @@ -//! Decorate content and apply alignment. -use crate::alignment::{self, Alignment}; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::{self, Operation, Tree}; -use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, - Point, Rectangle, Shell, Widget, -}; - -pub use iced_style::container::{Appearance, StyleSheet}; - -/// An element decorating some content. -/// -/// It is normally used for alignment purposes. -#[allow(missing_debug_implementations)] -pub struct Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - id: Option, - padding: Padding, - width: Length, - height: Length, - max_width: f32, - max_height: f32, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - style: ::Style, - content: Element<'a, Message, Renderer>, -} - -impl<'a, Message, Renderer> Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates an empty [`Container`]. - pub fn new(content: T) -> Self - where - T: Into>, - { - Container { - id: None, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - max_width: f32::INFINITY, - max_height: f32::INFINITY, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - style: Default::default(), - content: content.into(), - } - } - - /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// Sets the [`Padding`] of the [`Container`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Container`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Container`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the maximum width of the [`Container`]. - pub fn max_width(mut self, max_width: impl Into) -> Self { - self.max_width = max_width.into().0; - self - } - - /// Sets the maximum height of the [`Container`]. - pub fn max_height(mut self, max_height: impl Into) -> Self { - self.max_height = max_height.into().0; - self - } - - /// Sets the content alignment for the horizontal axis of the [`Container`]. - pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { - self.horizontal_alignment = alignment; - self - } - - /// Sets the content alignment for the vertical axis of the [`Container`]. - pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { - self.vertical_alignment = alignment; - self - } - - /// Centers the contents in the horizontal axis of the [`Container`]. - pub fn center_x(mut self) -> Self { - self.horizontal_alignment = alignment::Horizontal::Center; - self - } - - /// Centers the contents in the vertical axis of the [`Container`]. - pub fn center_y(mut self) -> Self { - self.vertical_alignment = alignment::Vertical::Center; - self - } - - /// Sets the style of the [`Container`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Widget - for Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - self.max_width, - self.max_height, - self.padding, - self.horizontal_alignment, - self.vertical_alignment, - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container( - self.id.as_ref().map(|id| &id.0), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout.children().next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout.children().next().unwrap(), - cursor_position, - viewport, - renderer, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - renderer_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - let style = theme.appearance(&self.style); - - draw_background(renderer, &style, layout.bounds()); - - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &renderer::Style { - text_color: style - .text_color - .unwrap_or(renderer_style.text_color), - }, - layout.children().next().unwrap(), - cursor_position, - viewport, - ); - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content.as_widget_mut().overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - column: Container<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(column) - } -} - -/// Computes the layout of a [`Container`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - max_width: f32, - max_height: f32, - padding: Padding, - horizontal_alignment: alignment::Horizontal, - vertical_alignment: alignment::Vertical, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits - .loose() - .max_width(max_width) - .max_height(max_height) - .width(width) - .height(height); - - let mut content = layout_content(renderer, &limits.pad(padding).loose()); - let padding = padding.fit(content.size(), limits.max()); - let size = limits.pad(padding).resolve(content.size()); - - content.move_to(Point::new(padding.left, padding.top)); - content.align( - Alignment::from(horizontal_alignment), - Alignment::from(vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(padding), vec![content]) -} - -/// Draws the background of a [`Container`] given its [`Appearance`] and its `bounds`. -pub fn draw_background( - renderer: &mut Renderer, - appearance: &Appearance, - bounds: Rectangle, -) where - Renderer: crate::Renderer, -{ - if appearance.background.is_some() || appearance.border_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: appearance.border_radius.into(), - border_width: appearance.border_width, - border_color: appearance.border_color, - }, - appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } -} - -/// The identifier of a [`Container`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} diff --git a/native/src/widget/helpers.rs b/native/src/widget/helpers.rs deleted file mode 100644 index d13eca75..00000000 --- a/native/src/widget/helpers.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! Helper functions to create pure widgets. -use crate::overlay; -use crate::widget; -use crate::{Element, Length, Pixels}; - -use std::borrow::Cow; -use std::ops::RangeInclusive; - -/// Creates a [`Column`] with the given children. -/// -/// [`Column`]: widget::Column -#[macro_export] -macro_rules! column { - () => ( - $crate::widget::Column::new() - ); - ($($x:expr),+ $(,)?) => ( - $crate::widget::Column::with_children(vec![$($crate::Element::from($x)),+]) - ); -} - -/// Creates a [`Row`] with the given children. -/// -/// [`Row`]: widget::Row -#[macro_export] -macro_rules! row { - () => ( - $crate::widget::Row::new() - ); - ($($x:expr),+ $(,)?) => ( - $crate::widget::Row::with_children(vec![$($crate::Element::from($x)),+]) - ); -} - -/// Creates a new [`Container`] with the provided content. -/// -/// [`Container`]: widget::Container -pub fn container<'a, Message, Renderer>( - content: impl Into>, -) -> widget::Container<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: widget::container::StyleSheet, -{ - widget::Container::new(content) -} - -/// Creates a new [`Column`] with the given children. -/// -/// [`Column`]: widget::Column -pub fn column( - children: Vec>, -) -> widget::Column<'_, Message, Renderer> { - widget::Column::with_children(children) -} - -/// Creates a new [`Row`] with the given children. -/// -/// [`Row`]: widget::Row -pub fn row( - children: Vec>, -) -> widget::Row<'_, Message, Renderer> { - widget::Row::with_children(children) -} - -/// Creates a new [`Scrollable`] with the provided content. -/// -/// [`Scrollable`]: widget::Scrollable -pub fn scrollable<'a, Message, Renderer>( - content: impl Into>, -) -> widget::Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: widget::scrollable::StyleSheet, -{ - widget::Scrollable::new(content) -} - -/// Creates a new [`Button`] with the provided content. -/// -/// [`Button`]: widget::Button -pub fn button<'a, Message, Renderer>( - content: impl Into>, -) -> widget::Button<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: widget::button::StyleSheet, - ::Style: Default, -{ - widget::Button::new(content) -} - -/// Creates a new [`Tooltip`] with the provided content, tooltip text, and [`tooltip::Position`]. -/// -/// [`Tooltip`]: widget::Tooltip -/// [`tooltip::Position`]: widget::tooltip::Position -pub fn tooltip<'a, Message, Renderer>( - content: impl Into>, - tooltip: impl ToString, - position: widget::tooltip::Position, -) -> widget::Tooltip<'a, Message, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::container::StyleSheet + widget::text::StyleSheet, -{ - widget::Tooltip::new(content, tooltip.to_string(), position) -} - -/// Creates a new [`Text`] widget with the provided content. -/// -/// [`Text`]: widget::Text -pub fn text<'a, Renderer>(text: impl ToString) -> widget::Text<'a, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::text::StyleSheet, -{ - widget::Text::new(text.to_string()) -} - -/// Creates a new [`Checkbox`]. -/// -/// [`Checkbox`]: widget::Checkbox -pub fn checkbox<'a, Message, Renderer>( - label: impl Into, - is_checked: bool, - f: impl Fn(bool) -> Message + 'a, -) -> widget::Checkbox<'a, Message, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::checkbox::StyleSheet + widget::text::StyleSheet, -{ - widget::Checkbox::new(label, is_checked, f) -} - -/// Creates a new [`Radio`]. -/// -/// [`Radio`]: widget::Radio -pub fn radio( - label: impl Into, - value: V, - selected: Option, - on_click: impl FnOnce(V) -> Message, -) -> widget::Radio -where - Message: Clone, - Renderer: crate::text::Renderer, - Renderer::Theme: widget::radio::StyleSheet, - V: Copy + Eq, -{ - widget::Radio::new(value, label, selected, on_click) -} - -/// Creates a new [`Toggler`]. -/// -/// [`Toggler`]: widget::Toggler -pub fn toggler<'a, Message, Renderer>( - label: impl Into>, - is_checked: bool, - f: impl Fn(bool) -> Message + 'a, -) -> widget::Toggler<'a, Message, Renderer> -where - Renderer: crate::text::Renderer, - Renderer::Theme: widget::toggler::StyleSheet, -{ - widget::Toggler::new(label, is_checked, f) -} - -/// Creates a new [`TextInput`]. -/// -/// [`TextInput`]: widget::TextInput -pub fn text_input<'a, Message, Renderer>( - placeholder: &str, - value: &str, - on_change: impl Fn(String) -> Message + 'a, -) -> widget::TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: crate::text::Renderer, - Renderer::Theme: widget::text_input::StyleSheet, -{ - widget::TextInput::new(placeholder, value, on_change) -} - -/// Creates a new [`Slider`]. -/// -/// [`Slider`]: widget::Slider -pub fn slider<'a, T, Message, Renderer>( - range: std::ops::RangeInclusive, - value: T, - on_change: impl Fn(T) -> Message + 'a, -) -> widget::Slider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: widget::slider::StyleSheet, -{ - widget::Slider::new(range, value, on_change) -} - -/// Creates a new [`VerticalSlider`]. -/// -/// [`VerticalSlider`]: widget::VerticalSlider -pub fn vertical_slider<'a, T, Message, Renderer>( - range: std::ops::RangeInclusive, - value: T, - on_change: impl Fn(T) -> Message + 'a, -) -> widget::VerticalSlider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: widget::slider::StyleSheet, -{ - widget::VerticalSlider::new(range, value, on_change) -} - -/// Creates a new [`PickList`]. -/// -/// [`PickList`]: widget::PickList -pub fn pick_list<'a, Message, Renderer, T>( - options: impl Into>, - selected: Option, - on_selected: impl Fn(T) -> Message + 'a, -) -> widget::PickList<'a, T, Message, Renderer> -where - T: ToString + Eq + 'static, - [T]: ToOwned>, - Renderer: crate::text::Renderer, - Renderer::Theme: widget::pick_list::StyleSheet - + widget::scrollable::StyleSheet - + overlay::menu::StyleSheet - + widget::container::StyleSheet, - ::Style: - From<::Style>, -{ - widget::PickList::new(options, selected, on_selected) -} - -/// Creates a new [`Image`]. -/// -/// [`Image`]: widget::Image -pub fn image(handle: impl Into) -> widget::Image { - widget::Image::new(handle.into()) -} - -/// Creates a new horizontal [`Space`] with the given [`Length`]. -/// -/// [`Space`]: widget::Space -pub fn horizontal_space(width: impl Into) -> widget::Space { - widget::Space::with_width(width) -} - -/// Creates a new vertical [`Space`] with the given [`Length`]. -/// -/// [`Space`]: widget::Space -pub fn vertical_space(height: impl Into) -> widget::Space { - widget::Space::with_height(height) -} - -/// Creates a horizontal [`Rule`] with the given height. -/// -/// [`Rule`]: widget::Rule -pub fn horizontal_rule( - height: impl Into, -) -> widget::Rule -where - Renderer: crate::Renderer, - Renderer::Theme: widget::rule::StyleSheet, -{ - widget::Rule::horizontal(height) -} - -/// Creates a vertical [`Rule`] with the given width. -/// -/// [`Rule`]: widget::Rule -pub fn vertical_rule( - width: impl Into, -) -> widget::Rule -where - Renderer: crate::Renderer, - Renderer::Theme: widget::rule::StyleSheet, -{ - widget::Rule::vertical(width) -} - -/// Creates a new [`ProgressBar`]. -/// -/// It expects: -/// * an inclusive range of possible values, and -/// * the current value of the [`ProgressBar`]. -/// -/// [`ProgressBar`]: widget::ProgressBar -pub fn progress_bar( - range: RangeInclusive, - value: f32, -) -> widget::ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: widget::progress_bar::StyleSheet, -{ - widget::ProgressBar::new(range, value) -} - -/// Creates a new [`Svg`] widget from the given [`Handle`]. -/// -/// [`Svg`]: widget::Svg -/// [`Handle`]: widget::svg::Handle -pub fn svg( - handle: impl Into, -) -> widget::Svg -where - Renderer: crate::svg::Renderer, - Renderer::Theme: widget::svg::StyleSheet, -{ - widget::Svg::new(handle) -} diff --git a/native/src/widget/id.rs b/native/src/widget/id.rs deleted file mode 100644 index 4b8fedf1..00000000 --- a/native/src/widget/id.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::borrow; -use std::sync::atomic::{self, AtomicUsize}; - -static NEXT_ID: AtomicUsize = AtomicUsize::new(0); - -/// The identifier of a generic widget. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(Internal); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(Internal::Custom(id.into())) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - let id = NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed); - - Self(Internal::Unique(id)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Internal { - Unique(usize), - Custom(borrow::Cow<'static, str>), -} - -#[cfg(test)] -mod tests { - use super::Id; - - #[test] - fn unique_generates_different_ids() { - let a = Id::unique(); - let b = Id::unique(); - - assert_ne!(a, b); - } -} diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs deleted file mode 100644 index 73257a74..00000000 --- a/native/src/widget/image.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Display images in your user interface. -pub mod viewer; -pub use viewer::Viewer; - -use crate::image; -use crate::layout; -use crate::renderer; -use crate::widget::Tree; -use crate::{ - ContentFit, Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, -}; - -use std::hash::Hash; - -/// Creates a new [`Viewer`] with the given image `Handle`. -pub fn viewer(handle: Handle) -> Viewer { - Viewer::new(handle) -} - -/// A frame that displays an image while keeping aspect ratio. -/// -/// # Example -/// -/// ``` -/// # use iced_native::widget::Image; -/// # use iced_native::image; -/// # -/// let image = Image::::new("resources/ferris.png"); -/// ``` -/// -/// -#[derive(Debug)] -pub struct Image { - handle: Handle, - width: Length, - height: Length, - content_fit: ContentFit, -} - -impl Image { - /// Creates a new [`Image`] with the given path. - pub fn new>(handle: T) -> Self { - Image { - handle: handle.into(), - width: Length::Shrink, - height: Length::Shrink, - content_fit: ContentFit::Contain, - } - } - - /// Sets the width of the [`Image`] boundaries. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Image`] boundaries. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the [`ContentFit`] of the [`Image`]. - /// - /// Defaults to [`ContentFit::Contain`] - pub fn content_fit(self, content_fit: ContentFit) -> Self { - Self { - content_fit, - ..self - } - } -} - -/// Computes the layout of an [`Image`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - handle: &Handle, - width: Length, - height: Length, - content_fit: ContentFit, -) -> layout::Node -where - Renderer: image::Renderer, -{ - // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.dimensions(handle); - - Size::new(width as f32, height as f32) - }; - - // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.width(width).height(height).resolve(image_size); - - // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); - - // Shrink the widget to fit the resized image, if requested - let final_size = Size { - width: match width { - Length::Shrink => f32::min(raw_size.width, full_size.width), - _ => raw_size.width, - }, - height: match height { - Length::Shrink => f32::min(raw_size.height, full_size.height), - _ => raw_size.height, - }, - }; - - layout::Node::new(final_size) -} - -/// Draws an [`Image`] -pub fn draw( - renderer: &mut Renderer, - layout: Layout<'_>, - handle: &Handle, - content_fit: ContentFit, -) where - Renderer: image::Renderer, - Handle: Clone + Hash, -{ - let Size { width, height } = renderer.dimensions(handle); - let image_size = Size::new(width as f32, height as f32); - - let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); - - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - renderer.draw(handle.clone(), drawing_bounds + offset) - }; - - if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height - { - renderer.with_layer(bounds, render); - } else { - render(renderer) - } -} - -impl Widget for Image -where - Renderer: image::Renderer, - Handle: Clone + Hash, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - &self.handle, - self.width, - self.height, - self.content_fit, - ) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - _theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - draw(renderer, layout, &self.handle, self.content_fit) - } -} - -impl<'a, Message, Renderer, Handle> From> - for Element<'a, Message, Renderer> -where - Renderer: image::Renderer, - Handle: Clone + Hash + 'a, -{ - fn from(image: Image) -> Element<'a, Message, Renderer> { - Element::new(image) - } -} diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs deleted file mode 100644 index 1f8d5d7a..00000000 --- a/native/src/widget/image/viewer.rs +++ /dev/null @@ -1,428 +0,0 @@ -//! Zoom and pan on an image. -use crate::event::{self, Event}; -use crate::image; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, -}; - -use std::hash::Hash; - -/// A frame that displays an image with the ability to zoom in/out and pan. -#[allow(missing_debug_implementations)] -pub struct Viewer { - padding: f32, - width: Length, - height: Length, - min_scale: f32, - max_scale: f32, - scale_step: f32, - handle: Handle, -} - -impl Viewer { - /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(handle: Handle) -> Self { - Viewer { - padding: 0.0, - width: Length::Shrink, - height: Length::Shrink, - min_scale: 0.25, - max_scale: 10.0, - scale_step: 0.10, - handle, - } - } - - /// Sets the padding of the [`Viewer`]. - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into().0; - self - } - - /// Sets the width of the [`Viewer`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Viewer`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the max scale applied to the image of the [`Viewer`]. - /// - /// Default is `10.0` - pub fn max_scale(mut self, max_scale: f32) -> Self { - self.max_scale = max_scale; - self - } - - /// Sets the min scale applied to the image of the [`Viewer`]. - /// - /// Default is `0.25` - pub fn min_scale(mut self, min_scale: f32) -> Self { - self.min_scale = min_scale; - self - } - - /// Sets the percentage the image of the [`Viewer`] will be scaled by - /// when zoomed in / out. - /// - /// Default is `0.10` - pub fn scale_step(mut self, scale_step: f32) -> Self { - self.scale_step = scale_step; - self - } -} - -impl Widget for Viewer -where - Renderer: image::Renderer, - Handle: Clone + Hash, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let Size { width, height } = renderer.dimensions(&self.handle); - - let mut size = limits - .width(self.width) - .height(self.height) - .resolve(Size::new(width as f32, height as f32)); - - let expansion_size = if height > width { - self.width - } else { - self.height - }; - - // Only calculate viewport sizes if the images are constrained to a limited space. - // If they are Fill|Portion let them expand within their alotted space. - match expansion_size { - Length::Shrink | Length::Fixed(_) => { - let aspect_ratio = width as f32 / height as f32; - let viewport_aspect_ratio = size.width / size.height; - if viewport_aspect_ratio > aspect_ratio { - size.width = width as f32 * size.height / height as f32; - } else { - size.height = height as f32 * size.width / width as f32; - } - } - Length::Fill | Length::FillPortion(_) => {} - } - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) - if is_mouse_over => - { - match delta { - mouse::ScrollDelta::Lines { y, .. } - | mouse::ScrollDelta::Pixels { y, .. } => { - let state = tree.state.downcast_mut::(); - let previous_scale = state.scale; - - if y < 0.0 && previous_scale > self.min_scale - || y > 0.0 && previous_scale < self.max_scale - { - state.scale = (if y > 0.0 { - state.scale * (1.0 + self.scale_step) - } else { - state.scale / (1.0 + self.scale_step) - }) - .clamp(self.min_scale, self.max_scale); - - let image_size = image_size( - renderer, - &self.handle, - state, - bounds.size(), - ); - - let factor = state.scale / previous_scale - 1.0; - - let cursor_to_center = - cursor_position - bounds.center(); - - let adjustment = cursor_to_center * factor - + state.current_offset * factor; - - state.current_offset = Vector::new( - if image_size.width > bounds.width { - state.current_offset.x + adjustment.x - } else { - 0.0 - }, - if image_size.height > bounds.height { - state.current_offset.y + adjustment.y - } else { - 0.0 - }, - ); - } - } - } - - event::Status::Captured - } - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - if is_mouse_over => - { - let state = tree.state.downcast_mut::(); - - state.cursor_grabbed_at = Some(cursor_position); - state.starting_offset = state.current_offset; - - event::Status::Captured - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - let state = tree.state.downcast_mut::(); - - if state.cursor_grabbed_at.is_some() { - state.cursor_grabbed_at = None; - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Mouse(mouse::Event::CursorMoved { position }) => { - let state = tree.state.downcast_mut::(); - - if let Some(origin) = state.cursor_grabbed_at { - let image_size = image_size( - renderer, - &self.handle, - state, - bounds.size(), - ); - - let hidden_width = (image_size.width - bounds.width / 2.0) - .max(0.0) - .round(); - - let hidden_height = (image_size.height - - bounds.height / 2.0) - .max(0.0) - .round(); - - let delta = position - origin; - - let x = if bounds.width < image_size.width { - (state.starting_offset.x - delta.x) - .clamp(-hidden_width, hidden_width) - } else { - 0.0 - }; - - let y = if bounds.height < image_size.height { - (state.starting_offset.y - delta.y) - .clamp(-hidden_height, hidden_height) - } else { - 0.0 - }; - - state.current_offset = Vector::new(x, y); - - event::Status::Captured - } else { - event::Status::Ignored - } - } - _ => event::Status::Ignored, - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if state.is_cursor_grabbed() { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::Idle - } - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - _theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let state = tree.state.downcast_ref::(); - let bounds = layout.bounds(); - - let image_size = - image_size(renderer, &self.handle, state, bounds.size()); - - let translation = { - let image_top_left = Vector::new( - bounds.width / 2.0 - image_size.width / 2.0, - bounds.height / 2.0 - image_size.height / 2.0, - ); - - image_top_left - state.offset(bounds, image_size) - }; - - renderer.with_layer(bounds, |renderer| { - renderer.with_translation(translation, |renderer| { - image::Renderer::draw( - renderer, - self.handle.clone(), - Rectangle { - x: bounds.x, - y: bounds.y, - ..Rectangle::with_size(image_size) - }, - ) - }); - }); - } -} - -/// The local state of a [`Viewer`]. -#[derive(Debug, Clone, Copy)] -pub struct State { - scale: f32, - starting_offset: Vector, - current_offset: Vector, - cursor_grabbed_at: Option, -} - -impl Default for State { - fn default() -> Self { - Self { - scale: 1.0, - starting_offset: Vector::default(), - current_offset: Vector::default(), - cursor_grabbed_at: None, - } - } -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> Self { - State::default() - } - - /// Returns the current offset of the [`State`], given the bounds - /// of the [`Viewer`] and its image. - fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { - let hidden_width = - (image_size.width - bounds.width / 2.0).max(0.0).round(); - - let hidden_height = - (image_size.height - bounds.height / 2.0).max(0.0).round(); - - Vector::new( - self.current_offset.x.clamp(-hidden_width, hidden_width), - self.current_offset.y.clamp(-hidden_height, hidden_height), - ) - } - - /// Returns if the cursor is currently grabbed by the [`Viewer`]. - pub fn is_cursor_grabbed(&self) -> bool { - self.cursor_grabbed_at.is_some() - } -} - -impl<'a, Message, Renderer, Handle> From> - for Element<'a, Message, Renderer> -where - Renderer: 'a + image::Renderer, - Message: 'a, - Handle: Clone + Hash + 'a, -{ - fn from(viewer: Viewer) -> Element<'a, Message, Renderer> { - Element::new(viewer) - } -} - -/// Returns the bounds of the underlying image, given the bounds of -/// the [`Viewer`]. Scaling will be applied and original aspect ratio -/// will be respected. -pub fn image_size( - renderer: &Renderer, - handle: &::Handle, - state: &State, - bounds: Size, -) -> Size -where - Renderer: image::Renderer, -{ - let Size { width, height } = renderer.dimensions(handle); - - let (width, height) = { - let dimensions = (width as f32, height as f32); - - let width_ratio = bounds.width / dimensions.0; - let height_ratio = bounds.height / dimensions.1; - - let ratio = width_ratio.min(height_ratio); - let scale = state.scale; - - if ratio < 1.0 { - (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) - } else { - (dimensions.0 * scale, dimensions.1 * scale) - } - }; - - Size::new(width, height) -} diff --git a/native/src/widget/operation.rs b/native/src/widget/operation.rs deleted file mode 100644 index 53688a21..00000000 --- a/native/src/widget/operation.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Query or update internal widget state. -pub mod focusable; -pub mod scrollable; -pub mod text_input; - -pub use focusable::Focusable; -pub use scrollable::Scrollable; -pub use text_input::TextInput; - -use crate::widget::Id; - -use std::any::Any; -use std::fmt; - -/// A piece of logic that can traverse the widget tree of an application in -/// order to query or update some widget state. -pub trait Operation { - /// Operates on a widget that contains other widgets. - /// - /// The `operate_on_children` function can be called to return control to - /// the widget tree and keep traversing it. - fn container( - &mut self, - id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ); - - /// Operates on a widget that can be focused. - fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {} - - /// Operates on a widget that can be scrolled. - fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {} - - /// Operates on a widget that has text input. - fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {} - - /// Operates on a custom widget with some state. - fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {} - - /// Finishes the [`Operation`] and returns its [`Outcome`]. - fn finish(&self) -> Outcome { - Outcome::None - } -} - -/// The result of an [`Operation`]. -pub enum Outcome { - /// The [`Operation`] produced no result. - None, - - /// The [`Operation`] produced some result. - Some(T), - - /// The [`Operation`] needs to be followed by another [`Operation`]. - Chain(Box>), -} - -impl fmt::Debug for Outcome -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => write!(f, "Outcome::None"), - Self::Some(output) => write!(f, "Outcome::Some({output:?})"), - Self::Chain(_) => write!(f, "Outcome::Chain(...)"), - } - } -} - -/// Produces an [`Operation`] that applies the given [`Operation`] to the -/// children of a container with the given [`Id`]. -pub fn scoped( - target: Id, - operation: impl Operation + 'static, -) -> impl Operation { - struct ScopedOperation { - target: Id, - operation: Box>, - } - - impl Operation for ScopedOperation { - fn container( - &mut self, - id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - if id == Some(&self.target) { - operate_on_children(self.operation.as_mut()); - } else { - operate_on_children(self); - } - } - - fn finish(&self) -> Outcome { - match self.operation.finish() { - Outcome::Chain(next) => { - Outcome::Chain(Box::new(ScopedOperation { - target: self.target.clone(), - operation: next, - })) - } - outcome => outcome, - } - } - } - - ScopedOperation { - target, - operation: Box::new(operation), - } -} diff --git a/native/src/widget/operation/focusable.rs b/native/src/widget/operation/focusable.rs deleted file mode 100644 index 312e4894..00000000 --- a/native/src/widget/operation/focusable.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Operate on widgets that can be focused. -use crate::widget::operation::{Operation, Outcome}; -use crate::widget::Id; - -/// The internal state of a widget that can be focused. -pub trait Focusable { - /// Returns whether the widget is focused or not. - fn is_focused(&self) -> bool; - - /// Focuses the widget. - fn focus(&mut self); - - /// Unfocuses the widget. - fn unfocus(&mut self); -} - -/// A summary of the focusable widgets present on a widget tree. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct Count { - /// The index of the current focused widget, if any. - pub focused: Option, - - /// The total amount of focusable widgets. - pub total: usize, -} - -/// Produces an [`Operation`] that focuses the widget with the given [`Id`]. -pub fn focus(target: Id) -> impl Operation { - struct Focus { - target: Id, - } - - impl Operation for Focus { - fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.focus(); - } - _ => { - state.unfocus(); - } - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - Focus { target } -} - -/// Produces an [`Operation`] that generates a [`Count`] and chains it with the -/// provided function to build a new [`Operation`]. -pub fn count(f: fn(Count) -> O) -> impl Operation -where - O: Operation + 'static, -{ - struct CountFocusable { - count: Count, - next: fn(Count) -> O, - } - - impl Operation for CountFocusable - where - O: Operation + 'static, - { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { - if state.is_focused() { - self.count.focused = Some(self.count.total); - } - - self.count.total += 1; - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - - fn finish(&self) -> Outcome { - Outcome::Chain(Box::new((self.next)(self.count))) - } - } - - CountFocusable { - count: Count::default(), - next: f, - } -} - -/// Produces an [`Operation`] that searches for the current focused widget, and -/// - if found, focuses the previous focusable widget. -/// - if not found, focuses the last focusable widget. -pub fn focus_previous() -> impl Operation { - struct FocusPrevious { - count: Count, - current: usize, - } - - impl Operation for FocusPrevious { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { - if self.count.total == 0 { - return; - } - - match self.count.focused { - None if self.current == self.count.total - 1 => state.focus(), - Some(0) if self.current == 0 => state.unfocus(), - Some(0) => {} - Some(focused) if focused == self.current => state.unfocus(), - Some(focused) if focused - 1 == self.current => state.focus(), - _ => {} - } - - self.current += 1; - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - count(|count| FocusPrevious { count, current: 0 }) -} - -/// Produces an [`Operation`] that searches for the current focused widget, and -/// - if found, focuses the next focusable widget. -/// - if not found, focuses the first focusable widget. -pub fn focus_next() -> impl Operation { - struct FocusNext { - count: Count, - current: usize, - } - - impl Operation for FocusNext { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { - match self.count.focused { - None if self.current == 0 => state.focus(), - Some(focused) if focused == self.current => state.unfocus(), - Some(focused) if focused + 1 == self.current => state.focus(), - _ => {} - } - - self.current += 1; - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - count(|count| FocusNext { count, current: 0 }) -} - -/// Produces an [`Operation`] that searches for the current focused widget -/// and stores its ID. This ignores widgets that do not have an ID. -pub fn find_focused() -> impl Operation { - struct FindFocused { - focused: Option, - } - - impl Operation for FindFocused { - fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) { - if state.is_focused() && id.is_some() { - self.focused = id.cloned(); - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - - fn finish(&self) -> Outcome { - if let Some(id) = &self.focused { - Outcome::Some(id.clone()) - } else { - Outcome::None - } - } - } - - FindFocused { focused: None } -} diff --git a/native/src/widget/operation/scrollable.rs b/native/src/widget/operation/scrollable.rs deleted file mode 100644 index 3b20631f..00000000 --- a/native/src/widget/operation/scrollable.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Operate on widgets that can be scrolled. -use crate::widget::{Id, Operation}; - -/// The internal state of a widget that can be scrolled. -pub trait Scrollable { - /// Snaps the scroll of the widget to the given `percentage` along the horizontal & vertical axis. - fn snap_to(&mut self, offset: RelativeOffset); -} - -/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to -/// the provided `percentage`. -pub fn snap_to(target: Id, offset: RelativeOffset) -> impl Operation { - struct SnapTo { - target: Id, - offset: RelativeOffset, - } - - impl Operation for SnapTo { - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - - fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) { - if Some(&self.target) == id { - state.snap_to(self.offset); - } - } - } - - SnapTo { target, offset } -} - -/// The amount of offset in each direction of a [`Scrollable`]. -/// -/// A value of `0.0` means start, while `1.0` means end. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct RelativeOffset { - /// The amount of horizontal offset - pub x: f32, - /// The amount of vertical offset - pub y: f32, -} - -impl RelativeOffset { - /// A relative offset that points to the top-left of a [`Scrollable`]. - pub const START: Self = Self { x: 0.0, y: 0.0 }; - - /// A relative offset that points to the bottom-right of a [`Scrollable`]. - pub const END: Self = Self { x: 1.0, y: 1.0 }; -} diff --git a/native/src/widget/operation/text_input.rs b/native/src/widget/operation/text_input.rs deleted file mode 100644 index 4c773e99..00000000 --- a/native/src/widget/operation/text_input.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Operate on widgets that have text input. -use crate::widget::operation::Operation; -use crate::widget::Id; - -/// The internal state of a widget that has text input. -pub trait TextInput { - /// Moves the cursor of the text input to the front of the input text. - fn move_cursor_to_front(&mut self); - /// Moves the cursor of the text input to the end of the input text. - fn move_cursor_to_end(&mut self); - /// Moves the cursor of the text input to an arbitrary location. - fn move_cursor_to(&mut self, position: usize); - /// Selects all the content of the text input. - fn select_all(&mut self); -} - -/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the -/// front. -pub fn move_cursor_to_front(target: Id) -> impl Operation { - struct MoveCursor { - target: Id, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.move_cursor_to_front(); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target } -} - -/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the -/// end. -pub fn move_cursor_to_end(target: Id) -> impl Operation { - struct MoveCursor { - target: Id, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.move_cursor_to_end(); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target } -} - -/// Produces an [`Operation`] that moves the cursor of the widget with the given [`Id`] to the -/// provided position. -pub fn move_cursor_to(target: Id, position: usize) -> impl Operation { - struct MoveCursor { - target: Id, - position: usize, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.move_cursor_to(self.position); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target, position } -} - -/// Produces an [`Operation`] that selects all the content of the widget with the given [`Id`]. -pub fn select_all(target: Id) -> impl Operation { - struct MoveCursor { - target: Id, - } - - impl Operation for MoveCursor { - fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) { - match id { - Some(id) if id == &self.target => { - state.select_all(); - } - _ => {} - } - } - - fn container( - &mut self, - _id: Option<&Id>, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } - - MoveCursor { target } -} diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs deleted file mode 100644 index bcb17ebd..00000000 --- a/native/src/widget/pane_grid.rs +++ /dev/null @@ -1,991 +0,0 @@ -//! Let your users split regions of your application and organize layout dynamically. -//! -//! [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish) -//! -//! # Example -//! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, -//! drag and drop, and hotkey support. -//! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.8/examples/pane_grid -mod axis; -mod configuration; -mod content; -mod direction; -mod draggable; -mod node; -mod pane; -mod split; -mod title_bar; - -pub mod state; - -pub use axis::Axis; -pub use configuration::Configuration; -pub use content::Content; -pub use direction::Direction; -pub use draggable::Draggable; -pub use node::Node; -pub use pane::Pane; -pub use split::Split; -pub use state::State; -pub use title_bar::TitleBar; - -pub use iced_style::pane_grid::{Line, StyleSheet}; - -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay::{self, Group}; -use crate::renderer; -use crate::touch; -use crate::widget; -use crate::widget::container; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, - Size, Vector, Widget, -}; - -/// A collection of panes distributed using either vertical or horizontal splits -/// to completely fill the space available. -/// -/// [![Pane grid - Iced](https://thumbs.gfycat.com/FrailFreshAiredaleterrier-small.gif)](https://gfycat.com/frailfreshairedaleterrier) -/// -/// This distribution of space is common in tiling window managers (like -/// [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even -/// [`tmux`](https://github.com/tmux/tmux)). -/// -/// A [`PaneGrid`] supports: -/// -/// * Vertical and horizontal splits -/// * Tracking of the last active pane -/// * Mouse-based resizing -/// * Drag and drop to reorganize panes -/// * Hotkey support -/// * Configurable modifier keys -/// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) -/// -/// ## Example -/// -/// ``` -/// # use iced_native::widget::{pane_grid, text}; -/// # -/// # type PaneGrid<'a, Message> = -/// # iced_native::widget::PaneGrid<'a, Message, iced_native::renderer::Null>; -/// # -/// enum PaneState { -/// SomePane, -/// AnotherKindOfPane, -/// } -/// -/// enum Message { -/// PaneDragged(pane_grid::DragEvent), -/// PaneResized(pane_grid::ResizeEvent), -/// } -/// -/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); -/// -/// let pane_grid = -/// PaneGrid::new(&state, |pane, state, is_maximized| { -/// pane_grid::Content::new(match state { -/// PaneState::SomePane => text("This is some pane"), -/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), -/// }) -/// }) -/// .on_drag(Message::PaneDragged) -/// .on_resize(10, Message::PaneResized); -/// ``` -#[allow(missing_debug_implementations)] -pub struct PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - contents: Contents<'a, Content<'a, Message, Renderer>>, - width: Length, - height: Length, - spacing: f32, - on_click: Option Message + 'a>>, - on_drag: Option Message + 'a>>, - on_resize: Option<(f32, Box Message + 'a>)>, - style: ::Style, -} - -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - /// Creates a [`PaneGrid`] with the given [`State`] and view function. - /// - /// The view function will be called to display each [`Pane`] present in the - /// [`State`]. [`bool`] is set if the pane is maximized. - pub fn new( - state: &'a State, - view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Renderer>, - ) -> Self { - let contents = if let Some((pane, pane_state)) = - state.maximized.and_then(|pane| { - state.panes.get(&pane).map(|pane_state| (pane, pane_state)) - }) { - Contents::Maximized( - pane, - view(pane, pane_state, true), - Node::Pane(pane), - ) - } else { - Contents::All( - state - .panes - .iter() - .map(|(pane, pane_state)| { - (*pane, view(*pane, pane_state, false)) - }) - .collect(), - &state.internal, - ) - }; - - Self { - contents, - width: Length::Fill, - height: Length::Fill, - spacing: 0.0, - on_click: None, - on_drag: None, - on_resize: None, - style: Default::default(), - } - } - - /// Sets the width of the [`PaneGrid`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`PaneGrid`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the spacing _between_ the panes of the [`PaneGrid`]. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the message that will be produced when a [`Pane`] of the - /// [`PaneGrid`] is clicked. - pub fn on_click(mut self, f: F) -> Self - where - F: 'a + Fn(Pane) -> Message, - { - self.on_click = Some(Box::new(f)); - self - } - - /// Enables the drag and drop interactions of the [`PaneGrid`], which will - /// use the provided function to produce messages. - pub fn on_drag(mut self, f: F) -> Self - where - F: 'a + Fn(DragEvent) -> Message, - { - self.on_drag = Some(Box::new(f)); - self - } - - /// Enables the resize interactions of the [`PaneGrid`], which will - /// use the provided function to produce messages. - /// - /// The `leeway` describes the amount of space around a split that can be - /// used to grab it. - /// - /// The grabbable area of a split will have a length of `spacing + leeway`, - /// properly centered. In other words, a length of - /// `(spacing + leeway) / 2.0` on either side of the split line. - pub fn on_resize(mut self, leeway: impl Into, f: F) -> Self - where - F: 'a + Fn(ResizeEvent) -> Message, - { - self.on_resize = Some((leeway.into().0, Box::new(f))); - self - } - - /// Sets the style of the [`PaneGrid`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - fn drag_enabled(&self) -> bool { - (!self.contents.is_maximized()) - .then(|| self.on_drag.is_some()) - .unwrap_or_default() - } -} - -impl<'a, Message, Renderer> Widget - for PaneGrid<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(state::Action::Idle) - } - - fn children(&self) -> Vec { - self.contents - .iter() - .map(|(_, content)| content.state()) - .collect() - } - - fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), - Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], - |state, content| content.diff(state), - |content| content.state(), - ), - } - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.contents.layout(), - self.width, - self.height, - self.spacing, - self.contents.iter(), - |content, renderer, limits| content.layout(renderer, limits), - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - operation.container(None, &mut |operation| { - self.contents - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|(((_pane, content), state), layout)| { - content.operate(state, layout, renderer, operation); - }) - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let action = tree.state.downcast_mut::(); - - let on_drag = if self.drag_enabled() { - &self.on_drag - } else { - &None - }; - - let event_status = update( - action, - self.contents.layout(), - &event, - layout, - cursor_position, - shell, - self.spacing, - self.contents.iter(), - &self.on_click, - on_drag, - &self.on_resize, - ); - - let picked_pane = action.picked_pane().map(|(pane, _)| pane); - - self.contents - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|(((pane, content), tree), layout)| { - let is_picked = picked_pane == Some(pane); - - content.on_event( - tree, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - is_picked, - ) - }) - .fold(event_status, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor_position, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - ) - .unwrap_or_else(|| { - self.contents - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|(((_pane, content), tree), layout)| { - content.mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - self.drag_enabled(), - ) - }) - .max() - .unwrap_or_default() - }) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - draw( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor_position, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - &self.style, - self.contents - .iter() - .zip(&tree.children) - .map(|((pane, content), tree)| (pane, (content, tree))), - |(content, tree), - renderer, - style, - layout, - cursor_position, - rectangle| { - content.draw( - tree, - renderer, - theme, - style, - layout, - cursor_position, - rectangle, - ); - }, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let children = self - .contents - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .filter_map(|(((_, content), state), layout)| { - content.overlay(state, layout, renderer) - }) - .collect::>(); - - (!children.is_empty()).then(|| Group::with_children(children).overlay()) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet + container::StyleSheet, -{ - fn from( - pane_grid: PaneGrid<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(pane_grid) - } -} - -/// Calculates the [`Layout`] of a [`PaneGrid`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - node: &Node, - width: Length, - height: Length, - spacing: f32, - contents: impl Iterator, - layout_content: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits.width(width).height(height); - let size = limits.resolve(Size::ZERO); - - let regions = node.pane_regions(spacing, size); - let children = contents - .filter_map(|(pane, content)| { - let region = regions.get(&pane)?; - let size = Size::new(region.width, region.height); - - let mut node = layout_content( - content, - renderer, - &layout::Limits::new(size, size), - ); - - node.move_to(Point::new(region.x, region.y)); - - Some(node) - }) - .collect(); - - layout::Node::with_children(size, children) -} - -/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] -/// accordingly. -pub fn update<'a, Message, T: Draggable>( - action: &mut state::Action, - node: &Node, - event: &Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - spacing: f32, - contents: impl Iterator, - on_click: &Option Message + 'a>>, - on_drag: &Option Message + 'a>>, - on_resize: &Option<(f32, Box Message + 'a>)>, -) -> event::Status { - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - event_status = event::Status::Captured; - - match on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - ); - - if let Some((split, axis, _)) = clicked_split { - if action.picked_pane().is_none() { - *action = - state::Action::Resizing { split, axis }; - } - } else { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - on_drag, - ); - } - } - None => { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - on_drag, - ); - } - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if let Some((pane, _)) = action.picked_pane() { - if let Some(on_drag) = on_drag { - let mut dropped_region = contents - .zip(layout.children()) - .filter(|(_, layout)| { - layout.bounds().contains(cursor_position) - }); - - let event = match dropped_region.next() { - Some(((target, _), _)) if pane != target => { - DragEvent::Dropped { pane, target } - } - _ => DragEvent::Canceled { pane }, - }; - - shell.publish(on_drag(event)); - } - - *action = state::Action::Idle; - - event_status = event::Status::Captured; - } else if action.picked_split().is_some() { - *action = state::Action::Idle; - - event_status = event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some((_, on_resize)) = on_resize { - if let Some((split, _)) = action.picked_split() { - let bounds = layout.bounds(); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - if let Some((axis, rectangle, _)) = splits.get(&split) { - let ratio = match axis { - Axis::Horizontal => { - let position = - cursor_position.y - bounds.y - rectangle.y; - - (position / rectangle.height).clamp(0.1, 0.9) - } - Axis::Vertical => { - let position = - cursor_position.x - bounds.x - rectangle.x; - - (position / rectangle.width).clamp(0.1, 0.9) - } - }; - - shell.publish(on_resize(ResizeEvent { split, ratio })); - - event_status = event::Status::Captured; - } - } - } - } - _ => {} - } - - event_status -} - -fn click_pane<'a, Message, T>( - action: &mut state::Action, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - contents: impl Iterator, - on_click: &Option Message + 'a>>, - on_drag: &Option Message + 'a>>, -) where - T: Draggable, -{ - let mut clicked_region = contents - .zip(layout.children()) - .filter(|(_, layout)| layout.bounds().contains(cursor_position)); - - if let Some(((pane, content), layout)) = clicked_region.next() { - if let Some(on_click) = &on_click { - shell.publish(on_click(pane)); - } - - if let Some(on_drag) = &on_drag { - if content.can_be_dragged_at(layout, cursor_position) { - let pane_position = layout.position(); - - let origin = cursor_position - - Vector::new(pane_position.x, pane_position.y); - - *action = state::Action::Dragging { pane, origin }; - - shell.publish(on_drag(DragEvent::Picked { pane })); - } - } - } -} - -/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. -pub fn mouse_interaction( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor_position: Point, - spacing: f32, - resize_leeway: Option, -) -> Option { - if action.picked_pane().is_some() { - return Some(mouse::Interaction::Grabbing); - } - - let resize_axis = - action.picked_split().map(|(_, axis)| axis).or_else(|| { - resize_leeway.and_then(|leeway| { - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split(splits.iter(), spacing + leeway, relative_cursor) - .map(|(_, axis, _)| axis) - }) - }); - - if let Some(resize_axis) = resize_axis { - return Some(match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }); - } - - None -} - -/// Draws a [`PaneGrid`]. -pub fn draw( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor_position: Point, - renderer: &mut Renderer, - theme: &Renderer::Theme, - default_style: &renderer::Style, - viewport: &Rectangle, - spacing: f32, - resize_leeway: Option, - style: &::Style, - contents: impl Iterator, - draw_pane: impl Fn( - T, - &mut Renderer, - &renderer::Style, - Layout<'_>, - Point, - &Rectangle, - ), -) where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - let picked_pane = action.picked_pane(); - - let picked_split = action - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_axis, region, ratio) = splits.get(&split)?; - - let region = axis.split_line_bounds(*region, *ratio, spacing); - - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match resize_leeway { - Some(leeway) => { - let bounds = layout.bounds(); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - )?; - - Some((axis, region + Vector::new(bounds.x, bounds.y), false)) - } - None => None, - }); - - let pane_cursor_position = if picked_pane.is_some() { - // TODO: Remove once cursor availability is encoded in the type - // system - Point::new(-1.0, -1.0) - } else { - cursor_position - }; - - let mut render_picked_pane = None; - - for ((id, pane), layout) in contents.zip(layout.children()) { - match picked_pane { - Some((dragging, origin)) if id == dragging => { - render_picked_pane = Some((pane, origin, layout)); - } - _ => { - draw_pane( - pane, - renderer, - default_style, - layout, - pane_cursor_position, - viewport, - ); - } - } - } - - // Render picked pane last - if let Some((pane, origin, layout)) = render_picked_pane { - let bounds = layout.bounds(); - - renderer.with_translation( - cursor_position - - Point::new(bounds.x + origin.x, bounds.y + origin.y), - |renderer| { - renderer.with_layer(bounds, |renderer| { - draw_pane( - pane, - renderer, - default_style, - layout, - pane_cursor_position, - viewport, - ); - }); - }, - ); - }; - - if let Some((axis, split_region, is_picked)) = picked_split { - let highlight = if is_picked { - theme.picked_split(style) - } else { - theme.hovered_split(style) - }; - - if let Some(highlight) = highlight { - renderer.fill_quad( - renderer::Quad { - bounds: match axis { - Axis::Horizontal => Rectangle { - x: split_region.x, - y: (split_region.y - + (split_region.height - highlight.width) - / 2.0) - .round(), - width: split_region.width, - height: highlight.width, - }, - Axis::Vertical => Rectangle { - x: (split_region.x - + (split_region.width - highlight.width) / 2.0) - .round(), - y: split_region.y, - width: highlight.width, - height: split_region.height, - }, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - highlight.color, - ); - } - } -} - -/// An event produced during a drag and drop interaction of a [`PaneGrid`]. -#[derive(Debug, Clone, Copy)] -pub enum DragEvent { - /// A [`Pane`] was picked for dragging. - Picked { - /// The picked [`Pane`]. - pane: Pane, - }, - - /// A [`Pane`] was dropped on top of another [`Pane`]. - Dropped { - /// The picked [`Pane`]. - pane: Pane, - - /// The [`Pane`] where the picked one was dropped on. - target: Pane, - }, - - /// A [`Pane`] was picked and then dropped outside of other [`Pane`] - /// boundaries. - Canceled { - /// The picked [`Pane`]. - pane: Pane, - }, -} - -/// An event produced during a resize interaction of a [`PaneGrid`]. -#[derive(Debug, Clone, Copy)] -pub struct ResizeEvent { - /// The [`Split`] that is being dragged for resizing. - pub split: Split, - - /// The new ratio of the [`Split`]. - /// - /// The ratio is a value in [0, 1], representing the exact position of a - /// [`Split`] between two panes. - pub ratio: f32, -} - -/* - * Helpers - */ -fn hovered_split<'a>( - splits: impl Iterator, - spacing: f32, - cursor_position: Point, -) -> Option<(Split, Axis, Rectangle)> { - splits - .filter_map(|(split, (axis, region, ratio))| { - let bounds = axis.split_line_bounds(*region, *ratio, spacing); - - if bounds.contains(cursor_position) { - Some((*split, *axis, bounds)) - } else { - None - } - }) - .next() -} - -/// The visible contents of the [`PaneGrid`] -#[derive(Debug)] -pub enum Contents<'a, T> { - /// All panes are visible - All(Vec<(Pane, T)>, &'a state::Internal), - /// A maximized pane is visible - Maximized(Pane, T, Node), -} - -impl<'a, T> Contents<'a, T> { - /// Returns the layout [`Node`] of the [`Contents`] - pub fn layout(&self) -> &Node { - match self { - Contents::All(_, state) => state.layout(), - Contents::Maximized(_, _, layout) => layout, - } - } - - /// Returns an iterator over the values of the [`Contents`] - pub fn iter(&self) -> Box + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn iter_mut(&mut self) -> Box + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter_mut().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn is_maximized(&self) -> bool { - matches!(self, Self::Maximized(..)) - } -} diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs deleted file mode 100644 index 02bde064..00000000 --- a/native/src/widget/pane_grid/axis.rs +++ /dev/null @@ -1,241 +0,0 @@ -use crate::Rectangle; - -/// A fixed reference line for the measurement of coordinates. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum Axis { - /// The horizontal axis: — - Horizontal, - /// The vertical axis: | - Vertical, -} - -impl Axis { - /// Splits the provided [`Rectangle`] on the current [`Axis`] with the - /// given `ratio` and `spacing`. - pub fn split( - &self, - rectangle: &Rectangle, - ratio: f32, - spacing: f32, - ) -> (Rectangle, Rectangle) { - match self { - Axis::Horizontal => { - let height_top = - (rectangle.height * ratio - spacing / 2.0).round(); - let height_bottom = rectangle.height - height_top - spacing; - - ( - Rectangle { - height: height_top, - ..*rectangle - }, - Rectangle { - y: rectangle.y + height_top + spacing, - height: height_bottom, - ..*rectangle - }, - ) - } - Axis::Vertical => { - let width_left = - (rectangle.width * ratio - spacing / 2.0).round(); - let width_right = rectangle.width - width_left - spacing; - - ( - Rectangle { - width: width_left, - ..*rectangle - }, - Rectangle { - x: rectangle.x + width_left + spacing, - width: width_right, - ..*rectangle - }, - ) - } - } - } - - /// Calculates the bounds of the split line in a [`Rectangle`] region. - pub fn split_line_bounds( - &self, - rectangle: Rectangle, - ratio: f32, - spacing: f32, - ) -> Rectangle { - match self { - Axis::Horizontal => Rectangle { - x: rectangle.x, - y: (rectangle.y + rectangle.height * ratio - spacing / 2.0) - .round(), - width: rectangle.width, - height: spacing, - }, - Axis::Vertical => Rectangle { - x: (rectangle.x + rectangle.width * ratio - spacing / 2.0) - .round(), - y: rectangle.y, - width: spacing, - height: rectangle.height, - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - enum Case { - Horizontal { - overall_height: f32, - spacing: f32, - top_height: f32, - bottom_y: f32, - bottom_height: f32, - }, - Vertical { - overall_width: f32, - spacing: f32, - left_width: f32, - right_x: f32, - right_width: f32, - }, - } - - #[test] - fn split() { - let cases = vec![ - // Even height, even spacing - Case::Horizontal { - overall_height: 10.0, - spacing: 2.0, - top_height: 4.0, - bottom_y: 6.0, - bottom_height: 4.0, - }, - // Odd height, even spacing - Case::Horizontal { - overall_height: 9.0, - spacing: 2.0, - top_height: 4.0, - bottom_y: 6.0, - bottom_height: 3.0, - }, - // Even height, odd spacing - Case::Horizontal { - overall_height: 10.0, - spacing: 1.0, - top_height: 5.0, - bottom_y: 6.0, - bottom_height: 4.0, - }, - // Odd height, odd spacing - Case::Horizontal { - overall_height: 9.0, - spacing: 1.0, - top_height: 4.0, - bottom_y: 5.0, - bottom_height: 4.0, - }, - // Even width, even spacing - Case::Vertical { - overall_width: 10.0, - spacing: 2.0, - left_width: 4.0, - right_x: 6.0, - right_width: 4.0, - }, - // Odd width, even spacing - Case::Vertical { - overall_width: 9.0, - spacing: 2.0, - left_width: 4.0, - right_x: 6.0, - right_width: 3.0, - }, - // Even width, odd spacing - Case::Vertical { - overall_width: 10.0, - spacing: 1.0, - left_width: 5.0, - right_x: 6.0, - right_width: 4.0, - }, - // Odd width, odd spacing - Case::Vertical { - overall_width: 9.0, - spacing: 1.0, - left_width: 4.0, - right_x: 5.0, - right_width: 4.0, - }, - ]; - for case in cases { - match case { - Case::Horizontal { - overall_height, - spacing, - top_height, - bottom_y, - bottom_height, - } => { - let a = Axis::Horizontal; - let r = Rectangle { - x: 0.0, - y: 0.0, - width: 10.0, - height: overall_height, - }; - let (top, bottom) = a.split(&r, 0.5, spacing); - assert_eq!( - top, - Rectangle { - height: top_height, - ..r - } - ); - assert_eq!( - bottom, - Rectangle { - y: bottom_y, - height: bottom_height, - ..r - } - ); - } - Case::Vertical { - overall_width, - spacing, - left_width, - right_x, - right_width, - } => { - let a = Axis::Vertical; - let r = Rectangle { - x: 0.0, - y: 0.0, - width: overall_width, - height: 10.0, - }; - let (left, right) = a.split(&r, 0.5, spacing); - assert_eq!( - left, - Rectangle { - width: left_width, - ..r - } - ); - assert_eq!( - right, - Rectangle { - x: right_x, - width: right_width, - ..r - } - ); - } - } - } - } -} diff --git a/native/src/widget/pane_grid/configuration.rs b/native/src/widget/pane_grid/configuration.rs deleted file mode 100644 index 7d68fb46..00000000 --- a/native/src/widget/pane_grid/configuration.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::widget::pane_grid::Axis; - -/// The arrangement of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone)] -pub enum Configuration { - /// A split of the available space. - Split { - /// The direction of the split. - axis: Axis, - - /// The ratio of the split in [0.0, 1.0]. - ratio: f32, - - /// The left/top [`Configuration`] of the split. - a: Box>, - - /// The right/bottom [`Configuration`] of the split. - b: Box>, - }, - /// A [`Pane`]. - /// - /// [`Pane`]: crate::widget::pane_grid::Pane - Pane(T), -} diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs deleted file mode 100644 index c9b0df07..00000000 --- a/native/src/widget/pane_grid/content.rs +++ /dev/null @@ -1,373 +0,0 @@ -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::container; -use crate::widget::pane_grid::{Draggable, TitleBar}; -use crate::widget::{self, Tree}; -use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size}; - -/// The content of a [`Pane`]. -/// -/// [`Pane`]: crate::widget::pane_grid::Pane -#[allow(missing_debug_implementations)] -pub struct Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - title_bar: Option>, - body: Element<'a, Message, Renderer>, - style: ::Style, -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// Creates a new [`Content`] with the provided body. - pub fn new(body: impl Into>) -> Self { - Self { - title_bar: None, - body: body.into(), - style: Default::default(), - } - } - - /// Sets the [`TitleBar`] of this [`Content`]. - pub fn title_bar( - mut self, - title_bar: TitleBar<'a, Message, Renderer>, - ) -> Self { - self.title_bar = Some(title_bar); - self - } - - /// Sets the style of the [`Content`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, Message, Renderer> Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - pub(super) fn state(&self) -> Tree { - let children = if let Some(title_bar) = self.title_bar.as_ref() { - vec![Tree::new(&self.body), title_bar.state()] - } else { - vec![Tree::new(&self.body), Tree::empty()] - }; - - Tree { - children, - ..Tree::empty() - } - } - - pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { - if let Some(title_bar) = self.title_bar.as_ref() { - title_bar.diff(&mut tree.children[1]); - } - - tree.children[0].diff(&self.body); - } else { - *tree = self.state(); - } - } - - /// Draws the [`Content`] with the provided [`Renderer`] and [`Layout`]. - /// - /// [`Renderer`]: crate::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - use container::StyleSheet; - - let bounds = layout.bounds(); - - { - let style = theme.appearance(&self.style); - - container::draw_background(renderer, &style, bounds); - } - - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - let body_layout = children.next().unwrap(); - - let show_controls = bounds.contains(cursor_position); - - self.body.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - body_layout, - cursor_position, - viewport, - ); - - title_bar.draw( - &tree.children[1], - renderer, - theme, - style, - title_bar_layout, - cursor_position, - viewport, - show_controls, - ); - } else { - self.body.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - pub(crate) fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - if let Some(title_bar) = &self.title_bar { - let max_size = limits.max(); - - let title_bar_layout = title_bar - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); - - let title_bar_size = title_bar_layout.size(); - - let mut body_layout = self.body.as_widget().layout( - renderer, - &layout::Limits::new( - Size::ZERO, - Size::new( - max_size.width, - max_size.height - title_bar_size.height, - ), - ), - ); - - body_layout.move_to(Point::new(0.0, title_bar_size.height)); - - layout::Node::with_children( - max_size, - vec![title_bar_layout, body_layout], - ) - } else { - self.body.as_widget().layout(renderer, limits) - } - } - - pub(crate) fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - let body_layout = if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - - title_bar.operate( - &mut tree.children[1], - children.next().unwrap(), - renderer, - operation, - ); - - children.next().unwrap() - } else { - layout - }; - - self.body.as_widget().operate( - &mut tree.children[0], - body_layout, - renderer, - operation, - ); - } - - pub(crate) fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - is_picked: bool, - ) -> event::Status { - let mut event_status = event::Status::Ignored; - - let body_layout = if let Some(title_bar) = &mut self.title_bar { - let mut children = layout.children(); - - event_status = title_bar.on_event( - &mut tree.children[1], - event.clone(), - children.next().unwrap(), - cursor_position, - renderer, - clipboard, - shell, - ); - - children.next().unwrap() - } else { - layout - }; - - let body_status = if is_picked { - event::Status::Ignored - } else { - self.body.as_widget_mut().on_event( - &mut tree.children[0], - event, - body_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }; - - event_status.merge(body_status) - } - - pub(crate) fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - drag_enabled: bool, - ) -> mouse::Interaction { - let (body_layout, title_bar_interaction) = - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - let is_over_pick_area = title_bar - .is_over_pick_area(title_bar_layout, cursor_position); - - if is_over_pick_area && drag_enabled { - return mouse::Interaction::Grab; - } - - let mouse_interaction = title_bar.mouse_interaction( - &tree.children[1], - title_bar_layout, - cursor_position, - viewport, - renderer, - ); - - (children.next().unwrap(), mouse_interaction) - } else { - (layout, mouse::Interaction::default()) - }; - - self.body - .as_widget() - .mouse_interaction( - &tree.children[0], - body_layout, - cursor_position, - viewport, - renderer, - ) - .max(title_bar_interaction) - } - - pub(crate) fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - if let Some(title_bar) = self.title_bar.as_mut() { - let mut children = layout.children(); - let title_bar_layout = children.next()?; - - let mut states = tree.children.iter_mut(); - let body_state = states.next().unwrap(); - let title_bar_state = states.next().unwrap(); - - match title_bar.overlay(title_bar_state, title_bar_layout, renderer) - { - Some(overlay) => Some(overlay), - None => self.body.as_widget_mut().overlay( - body_state, - children.next()?, - renderer, - ), - } - } else { - self.body.as_widget_mut().overlay( - &mut tree.children[0], - layout, - renderer, - ) - } - } -} - -impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - fn can_be_dragged_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if let Some(title_bar) = &self.title_bar { - let mut children = layout.children(); - let title_bar_layout = children.next().unwrap(); - - title_bar.is_over_pick_area(title_bar_layout, cursor_position) - } else { - false - } - } -} - -impl<'a, T, Message, Renderer> From for Content<'a, Message, Renderer> -where - T: Into>, - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - fn from(element: T) -> Self { - Self::new(element) - } -} diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs deleted file mode 100644 index b31a8737..00000000 --- a/native/src/widget/pane_grid/direction.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// A four cardinal direction. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Direction { - /// ↑ - Up, - /// ↓ - Down, - /// ← - Left, - /// → - Right, -} diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs deleted file mode 100644 index 6044871d..00000000 --- a/native/src/widget/pane_grid/draggable.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::{Layout, Point}; - -/// A pane that can be dragged. -pub trait Draggable { - /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked - /// at the provided cursor position. - fn can_be_dragged_at( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool; -} diff --git a/native/src/widget/pane_grid/node.rs b/native/src/widget/pane_grid/node.rs deleted file mode 100644 index cc304b96..00000000 --- a/native/src/widget/pane_grid/node.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::widget::pane_grid::{Axis, Pane, Split}; -use crate::{Rectangle, Size}; - -use std::collections::BTreeMap; - -/// A layout node of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone)] -pub enum Node { - /// The region of this [`Node`] is split into two. - Split { - /// The [`Split`] of this [`Node`]. - id: Split, - - /// The direction of the split. - axis: Axis, - - /// The ratio of the split in [0.0, 1.0]. - ratio: f32, - - /// The left/top [`Node`] of the split. - a: Box, - - /// The right/bottom [`Node`] of the split. - b: Box, - }, - /// The region of this [`Node`] is taken by a [`Pane`]. - Pane(Pane), -} - -impl Node { - /// Returns an iterator over each [`Split`] in this [`Node`]. - pub fn splits(&self) -> impl Iterator { - let mut unvisited_nodes = vec![self]; - - std::iter::from_fn(move || { - while let Some(node) = unvisited_nodes.pop() { - if let Node::Split { id, a, b, .. } = node { - unvisited_nodes.push(a); - unvisited_nodes.push(b); - - return Some(id); - } - } - - None - }) - } - - /// Returns the rectangular region for each [`Pane`] in the [`Node`] given - /// the spacing between panes and the total available space. - pub fn pane_regions( - &self, - spacing: f32, - size: Size, - ) -> BTreeMap { - let mut regions = BTreeMap::new(); - - self.compute_regions( - spacing, - &Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - &mut regions, - ); - - regions - } - - /// Returns the axis, rectangular region, and ratio for each [`Split`] in - /// the [`Node`] given the spacing between panes and the total available - /// space. - pub fn split_regions( - &self, - spacing: f32, - size: Size, - ) -> BTreeMap { - let mut splits = BTreeMap::new(); - - self.compute_splits( - spacing, - &Rectangle { - x: 0.0, - y: 0.0, - width: size.width, - height: size.height, - }, - &mut splits, - ); - - splits - } - - pub(crate) fn find(&mut self, pane: &Pane) -> Option<&mut Node> { - match self { - Node::Split { a, b, .. } => { - a.find(pane).or_else(move || b.find(pane)) - } - Node::Pane(p) => { - if p == pane { - Some(self) - } else { - None - } - } - } - } - - pub(crate) fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) { - *self = Node::Split { - id, - axis, - ratio: 0.5, - a: Box::new(self.clone()), - b: Box::new(Node::Pane(new_pane)), - }; - } - - pub(crate) fn update(&mut self, f: &impl Fn(&mut Node)) { - if let Node::Split { a, b, .. } = self { - a.update(f); - b.update(f); - } - - f(self); - } - - pub(crate) fn resize(&mut self, split: &Split, percentage: f32) -> bool { - match self { - Node::Split { - id, ratio, a, b, .. - } => { - if id == split { - *ratio = percentage; - - true - } else if a.resize(split, percentage) { - true - } else { - b.resize(split, percentage) - } - } - Node::Pane(_) => false, - } - } - - pub(crate) fn remove(&mut self, pane: &Pane) -> Option { - match self { - Node::Split { a, b, .. } => { - if a.pane() == Some(*pane) { - *self = *b.clone(); - Some(self.first_pane()) - } else if b.pane() == Some(*pane) { - *self = *a.clone(); - Some(self.first_pane()) - } else { - a.remove(pane).or_else(|| b.remove(pane)) - } - } - Node::Pane(_) => None, - } - } - - fn pane(&self) -> Option { - match self { - Node::Split { .. } => None, - Node::Pane(pane) => Some(*pane), - } - } - - fn first_pane(&self) -> Pane { - match self { - Node::Split { a, .. } => a.first_pane(), - Node::Pane(pane) => *pane, - } - } - - fn compute_regions( - &self, - spacing: f32, - current: &Rectangle, - regions: &mut BTreeMap, - ) { - match self { - Node::Split { - axis, ratio, a, b, .. - } => { - let (region_a, region_b) = axis.split(current, *ratio, spacing); - - a.compute_regions(spacing, ®ion_a, regions); - b.compute_regions(spacing, ®ion_b, regions); - } - Node::Pane(pane) => { - let _ = regions.insert(*pane, *current); - } - } - } - - fn compute_splits( - &self, - spacing: f32, - current: &Rectangle, - splits: &mut BTreeMap, - ) { - match self { - Node::Split { - axis, - ratio, - a, - b, - id, - } => { - let (region_a, region_b) = axis.split(current, *ratio, spacing); - - let _ = splits.insert(*id, (*axis, *current, *ratio)); - - a.compute_splits(spacing, ®ion_a, splits); - b.compute_splits(spacing, ®ion_b, splits); - } - Node::Pane(_) => {} - } - } -} - -impl std::hash::Hash for Node { - fn hash(&self, state: &mut H) { - match self { - Node::Split { - id, - axis, - ratio, - a, - b, - } => { - id.hash(state); - axis.hash(state); - ((ratio * 100_000.0) as u32).hash(state); - a.hash(state); - b.hash(state); - } - Node::Pane(pane) => { - pane.hash(state); - } - } - } -} diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs deleted file mode 100644 index d6fbab83..00000000 --- a/native/src/widget/pane_grid/pane.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// A rectangular region in a [`PaneGrid`] used to display widgets. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Pane(pub(super) usize); diff --git a/native/src/widget/pane_grid/split.rs b/native/src/widget/pane_grid/split.rs deleted file mode 100644 index 8132272a..00000000 --- a/native/src/widget/pane_grid/split.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// A divider that splits a region in a [`PaneGrid`] into two different panes. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Split(pub(super) usize); diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs deleted file mode 100644 index c4ae0a0e..00000000 --- a/native/src/widget/pane_grid/state.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! The state of a [`PaneGrid`]. -//! -//! [`PaneGrid`]: crate::widget::PaneGrid -use crate::widget::pane_grid::{ - Axis, Configuration, Direction, Node, Pane, Split, -}; -use crate::{Point, Size}; - -use std::collections::HashMap; - -/// The state of a [`PaneGrid`]. -/// -/// It keeps track of the state of each [`Pane`] and the position of each -/// [`Split`]. -/// -/// The [`State`] needs to own any mutable contents a [`Pane`] may need. This is -/// why this struct is generic over the type `T`. Values of this type are -/// provided to the view function of [`PaneGrid::new`] for displaying each -/// [`Pane`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -/// [`PaneGrid::new`]: crate::widget::PaneGrid::new -#[derive(Debug, Clone)] -pub struct State { - /// The panes of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub panes: HashMap, - - /// The internal state of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub internal: Internal, - - /// The maximized [`Pane`] of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub(super) maximized: Option, -} - -impl State { - /// Creates a new [`State`], initializing the first pane with the provided - /// state. - /// - /// Alongside the [`State`], it returns the first [`Pane`] identifier. - pub fn new(first_pane_state: T) -> (Self, Pane) { - ( - Self::with_configuration(Configuration::Pane(first_pane_state)), - Pane(0), - ) - } - - /// Creates a new [`State`] with the given [`Configuration`]. - pub fn with_configuration(config: impl Into>) -> Self { - let mut panes = HashMap::new(); - - let internal = - Internal::from_configuration(&mut panes, config.into(), 0); - - State { - panes, - internal, - maximized: None, - } - } - - /// Returns the total amount of panes in the [`State`]. - pub fn len(&self) -> usize { - self.panes.len() - } - - /// Returns `true` if the amount of panes in the [`State`] is 0. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the internal state of the given [`Pane`], if it exists. - pub fn get(&self, pane: &Pane) -> Option<&T> { - self.panes.get(pane) - } - - /// Returns the internal state of the given [`Pane`] with mutability, if it - /// exists. - pub fn get_mut(&mut self, pane: &Pane) -> Option<&mut T> { - self.panes.get_mut(pane) - } - - /// Returns an iterator over all the panes of the [`State`], alongside its - /// internal state. - pub fn iter(&self) -> impl Iterator { - self.panes.iter() - } - - /// Returns a mutable iterator over all the panes of the [`State`], - /// alongside its internal state. - pub fn iter_mut(&mut self) -> impl Iterator { - self.panes.iter_mut() - } - - /// Returns the layout of the [`State`]. - pub fn layout(&self) -> &Node { - &self.internal.layout - } - - /// Returns the adjacent [`Pane`] of another [`Pane`] in the given - /// direction, if there is one. - pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option { - let regions = self - .internal - .layout - .pane_regions(0.0, Size::new(4096.0, 4096.0)); - - let current_region = regions.get(pane)?; - - let target = match direction { - Direction::Left => { - Point::new(current_region.x - 1.0, current_region.y + 1.0) - } - Direction::Right => Point::new( - current_region.x + current_region.width + 1.0, - current_region.y + 1.0, - ), - Direction::Up => { - Point::new(current_region.x + 1.0, current_region.y - 1.0) - } - Direction::Down => Point::new( - current_region.x + 1.0, - current_region.y + current_region.height + 1.0, - ), - }; - - let mut colliding_regions = - regions.iter().filter(|(_, region)| region.contains(target)); - - let (pane, _) = colliding_regions.next()?; - - Some(*pane) - } - - /// Splits the given [`Pane`] into two in the given [`Axis`] and - /// initializing the new [`Pane`] with the provided internal state. - pub fn split( - &mut self, - axis: Axis, - pane: &Pane, - state: T, - ) -> Option<(Pane, Split)> { - let node = self.internal.layout.find(pane)?; - - let new_pane = { - self.internal.last_id = self.internal.last_id.checked_add(1)?; - - Pane(self.internal.last_id) - }; - - let new_split = { - self.internal.last_id = self.internal.last_id.checked_add(1)?; - - Split(self.internal.last_id) - }; - - node.split(new_split, axis, new_pane); - - let _ = self.panes.insert(new_pane, state); - let _ = self.maximized.take(); - - Some((new_pane, new_split)) - } - - /// Swaps the position of the provided panes in the [`State`]. - /// - /// If you want to swap panes on drag and drop in your [`PaneGrid`], you - /// will need to call this method when handling a [`DragEvent`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - /// [`DragEvent`]: crate::widget::pane_grid::DragEvent - pub fn swap(&mut self, a: &Pane, b: &Pane) { - self.internal.layout.update(&|node| match node { - Node::Split { .. } => {} - Node::Pane(pane) => { - if pane == a { - *node = Node::Pane(*b); - } else if pane == b { - *node = Node::Pane(*a); - } - } - }); - } - - /// Resizes two panes by setting the position of the provided [`Split`]. - /// - /// The ratio is a value in [0, 1], representing the exact position of a - /// [`Split`] between two panes. - /// - /// If you want to enable resize interactions in your [`PaneGrid`], you will - /// need to call this method when handling a [`ResizeEvent`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - /// [`ResizeEvent`]: crate::widget::pane_grid::ResizeEvent - pub fn resize(&mut self, split: &Split, ratio: f32) { - let _ = self.internal.layout.resize(split, ratio); - } - - /// Closes the given [`Pane`] and returns its internal state and its closest - /// sibling, if it exists. - pub fn close(&mut self, pane: &Pane) -> Option<(T, Pane)> { - if self.maximized == Some(*pane) { - let _ = self.maximized.take(); - } - - if let Some(sibling) = self.internal.layout.remove(pane) { - self.panes.remove(pane).map(|state| (state, sibling)) - } else { - None - } - } - - /// Maximize the given [`Pane`]. Only this pane will be rendered by the - /// [`PaneGrid`] until [`Self::restore()`] is called. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn maximize(&mut self, pane: &Pane) { - self.maximized = Some(*pane); - } - - /// Restore the currently maximized [`Pane`] to it's normal size. All panes - /// will be rendered by the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn restore(&mut self) { - let _ = self.maximized.take(); - } - - /// Returns the maximized [`Pane`] of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn maximized(&self) -> Option { - self.maximized - } -} - -/// The internal state of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone)] -pub struct Internal { - layout: Node, - last_id: usize, -} - -impl Internal { - /// Initializes the [`Internal`] state of a [`PaneGrid`] from a - /// [`Configuration`]. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - pub fn from_configuration( - panes: &mut HashMap, - content: Configuration, - next_id: usize, - ) -> Self { - let (layout, last_id) = match content { - Configuration::Split { axis, ratio, a, b } => { - let Internal { - layout: a, - last_id: next_id, - .. - } = Self::from_configuration(panes, *a, next_id); - - let Internal { - layout: b, - last_id: next_id, - .. - } = Self::from_configuration(panes, *b, next_id); - - ( - Node::Split { - id: Split(next_id), - axis, - ratio, - a: Box::new(a), - b: Box::new(b), - }, - next_id + 1, - ) - } - Configuration::Pane(state) => { - let id = Pane(next_id); - let _ = panes.insert(id, state); - - (Node::Pane(id), next_id + 1) - } - }; - - Self { layout, last_id } - } -} - -/// The current action of a [`PaneGrid`]. -/// -/// [`PaneGrid`]: crate::widget::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Action { - /// The [`PaneGrid`] is idle. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - Idle, - /// A [`Pane`] in the [`PaneGrid`] is being dragged. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - Dragging { - /// The [`Pane`] being dragged. - pane: Pane, - /// The starting [`Point`] of the drag interaction. - origin: Point, - }, - /// A [`Split`] in the [`PaneGrid`] is being dragged. - /// - /// [`PaneGrid`]: crate::widget::PaneGrid - Resizing { - /// The [`Split`] being dragged. - split: Split, - /// The [`Axis`] of the [`Split`]. - axis: Axis, - }, -} - -impl Action { - /// Returns the current [`Pane`] that is being dragged, if any. - pub fn picked_pane(&self) -> Option<(Pane, Point)> { - match *self { - Action::Dragging { pane, origin, .. } => Some((pane, origin)), - _ => None, - } - } - - /// Returns the current [`Split`] that is being dragged, if any. - pub fn picked_split(&self) -> Option<(Split, Axis)> { - match *self { - Action::Resizing { split, axis, .. } => Some((split, axis)), - _ => None, - } - } -} - -impl Internal { - /// The layout [`Node`] of the [`Internal`] state - pub fn layout(&self) -> &Node { - &self.layout - } -} diff --git a/native/src/widget/pane_grid/title_bar.rs b/native/src/widget/pane_grid/title_bar.rs deleted file mode 100644 index 107078ef..00000000 --- a/native/src/widget/pane_grid/title_bar.rs +++ /dev/null @@ -1,432 +0,0 @@ -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::container; -use crate::widget::{self, Tree}; -use crate::{ - Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, -}; - -/// The title bar of a [`Pane`]. -/// -/// [`Pane`]: crate::widget::pane_grid::Pane -#[allow(missing_debug_implementations)] -pub struct TitleBar<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - content: Element<'a, Message, Renderer>, - controls: Option>, - padding: Padding, - always_show_controls: bool, - style: ::Style, -} - -impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - /// Creates a new [`TitleBar`] with the given content. - pub fn new(content: E) -> Self - where - E: Into>, - { - Self { - content: content.into(), - controls: None, - padding: Padding::ZERO, - always_show_controls: false, - style: Default::default(), - } - } - - /// Sets the controls of the [`TitleBar`]. - pub fn controls( - mut self, - controls: impl Into>, - ) -> Self { - self.controls = Some(controls.into()); - self - } - - /// Sets the [`Padding`] of the [`TitleBar`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the style of the [`TitleBar`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Sets whether or not the [`controls`] attached to this [`TitleBar`] are - /// always visible. - /// - /// By default, the controls are only visible when the [`Pane`] of this - /// [`TitleBar`] is hovered. - /// - /// [`controls`]: Self::controls - /// [`Pane`]: crate::widget::pane_grid::Pane - pub fn always_show_controls(mut self) -> Self { - self.always_show_controls = true; - self - } -} - -impl<'a, Message, Renderer> TitleBar<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: container::StyleSheet, -{ - pub(super) fn state(&self) -> Tree { - let children = if let Some(controls) = self.controls.as_ref() { - vec![Tree::new(&self.content), Tree::new(controls)] - } else { - vec![Tree::new(&self.content), Tree::empty()] - }; - - Tree { - children, - ..Tree::empty() - } - } - - pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { - if let Some(controls) = self.controls.as_ref() { - tree.children[1].diff(controls); - } - - tree.children[0].diff(&self.content); - } else { - *tree = self.state(); - } - } - - /// Draws the [`TitleBar`] with the provided [`Renderer`] and [`Layout`]. - /// - /// [`Renderer`]: crate::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - inherited_style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - show_controls: bool, - ) { - use container::StyleSheet; - - let bounds = layout.bounds(); - let style = theme.appearance(&self.style); - let inherited_style = renderer::Style { - text_color: style.text_color.unwrap_or(inherited_style.text_color), - }; - - container::draw_background(renderer, &style, bounds); - - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - let mut show_title = true; - - if let Some(controls) = &self.controls { - if show_controls || self.always_show_controls { - let controls_layout = children.next().unwrap(); - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - controls.as_widget().draw( - &tree.children[1], - renderer, - theme, - &inherited_style, - controls_layout, - cursor_position, - viewport, - ); - } - } - - if show_title { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - &inherited_style, - title_layout, - cursor_position, - viewport, - ); - } - } - - /// Returns whether the mouse cursor is over the pick area of the - /// [`TitleBar`] or not. - /// - /// The whole [`TitleBar`] is a pick area, except its controls. - pub fn is_over_pick_area( - &self, - layout: Layout<'_>, - cursor_position: Point, - ) -> bool { - if layout.bounds().contains(cursor_position) { - let mut children = layout.children(); - let padded = children.next().unwrap(); - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - - if self.controls.is_some() { - let controls_layout = children.next().unwrap(); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - !controls_layout.bounds().contains(cursor_position) - } else { - !controls_layout.bounds().contains(cursor_position) - && !title_layout.bounds().contains(cursor_position) - } - } else { - !title_layout.bounds().contains(cursor_position) - } - } else { - false - } - } - - pub(crate) fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.pad(self.padding); - let max_size = limits.max(); - - let title_layout = self - .content - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); - - let title_size = title_layout.size(); - - let mut node = if let Some(controls) = &self.controls { - let mut controls_layout = controls - .as_widget() - .layout(renderer, &layout::Limits::new(Size::ZERO, max_size)); - - let controls_size = controls_layout.size(); - let space_before_controls = max_size.width - controls_size.width; - - let height = title_size.height.max(controls_size.height); - - controls_layout.move_to(Point::new(space_before_controls, 0.0)); - - layout::Node::with_children( - Size::new(max_size.width, height), - vec![title_layout, controls_layout], - ) - } else { - layout::Node::with_children( - Size::new(max_size.width, title_size.height), - vec![title_layout], - ) - }; - - node.move_to(Point::new(self.padding.left, self.padding.top)); - - layout::Node::with_children(node.size().pad(self.padding), vec![node]) - } - - pub(crate) fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn widget::Operation, - ) { - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - let mut show_title = true; - - if let Some(controls) = &self.controls { - let controls_layout = children.next().unwrap(); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - controls.as_widget().operate( - &mut tree.children[1], - controls_layout, - renderer, - operation, - ) - }; - - if show_title { - self.content.as_widget().operate( - &mut tree.children[0], - title_layout, - renderer, - operation, - ) - } - } - - pub(crate) fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - let mut show_title = true; - - let control_status = if let Some(controls) = &mut self.controls { - let controls_layout = children.next().unwrap(); - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - show_title = false; - } - - controls.as_widget_mut().on_event( - &mut tree.children[1], - event.clone(), - controls_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } else { - event::Status::Ignored - }; - - let title_status = if show_title { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - title_layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } else { - event::Status::Ignored - }; - - control_status.merge(title_status) - } - - pub(crate) fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let mut children = layout.children(); - let padded = children.next().unwrap(); - - let mut children = padded.children(); - let title_layout = children.next().unwrap(); - - let title_interaction = self.content.as_widget().mouse_interaction( - &tree.children[0], - title_layout, - cursor_position, - viewport, - renderer, - ); - - if let Some(controls) = &self.controls { - let controls_layout = children.next().unwrap(); - let controls_interaction = controls.as_widget().mouse_interaction( - &tree.children[1], - controls_layout, - cursor_position, - viewport, - renderer, - ); - - if title_layout.bounds().width + controls_layout.bounds().width - > padded.bounds().width - { - controls_interaction - } else { - controls_interaction.max(title_interaction) - } - } else { - title_interaction - } - } - - pub(crate) fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let mut children = layout.children(); - let padded = children.next()?; - - let mut children = padded.children(); - let title_layout = children.next()?; - - let Self { - content, controls, .. - } = self; - - let mut states = tree.children.iter_mut(); - let title_state = states.next().unwrap(); - let controls_state = states.next().unwrap(); - - content - .as_widget_mut() - .overlay(title_state, title_layout, renderer) - .or_else(move || { - controls.as_mut().and_then(|controls| { - let controls_layout = children.next()?; - - controls.as_widget_mut().overlay( - controls_state, - controls_layout, - renderer, - ) - }) - }) - } -} diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs deleted file mode 100644 index 8ff82f3b..00000000 --- a/native/src/widget/pick_list.rs +++ /dev/null @@ -1,657 +0,0 @@ -//! Display a dropdown list of selectable values. -use crate::alignment; -use crate::event::{self, Event}; -use crate::keyboard; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::overlay::menu::{self, Menu}; -use crate::renderer; -use crate::text::{self, Text}; -use crate::touch; -use crate::widget::container; -use crate::widget::scrollable; -use crate::widget::tree::{self, Tree}; -use crate::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, - Shell, Size, Widget, -}; -use std::borrow::Cow; - -pub use iced_style::pick_list::{Appearance, StyleSheet}; - -/// A widget for selecting a single value from a list of options. -#[allow(missing_debug_implementations)] -pub struct PickList<'a, T, Message, Renderer> -where - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - on_selected: Box Message + 'a>, - options: Cow<'a, [T]>, - placeholder: Option, - selected: Option, - width: Length, - padding: Padding, - text_size: Option, - font: Option, - handle: Handle, - style: ::Style, -} - -impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer> -where - T: ToString + Eq, - [T]: ToOwned>, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - /// The default padding of a [`PickList`]. - pub const DEFAULT_PADDING: Padding = Padding::new(5.0); - - /// Creates a new [`PickList`] with the given list of options, the current - /// selected value, and the message to produce when an option is selected. - pub fn new( - options: impl Into>, - selected: Option, - on_selected: impl Fn(T) -> Message + 'a, - ) -> Self { - Self { - on_selected: Box::new(on_selected), - options: options.into(), - placeholder: None, - selected, - width: Length::Shrink, - padding: Self::DEFAULT_PADDING, - text_size: None, - font: None, - handle: Default::default(), - style: Default::default(), - } - } - - /// Sets the placeholder of the [`PickList`]. - pub fn placeholder(mut self, placeholder: impl Into) -> Self { - self.placeholder = Some(placeholder.into()); - self - } - - /// Sets the width of the [`PickList`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the [`Padding`] of the [`PickList`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`PickList`]. - pub fn text_size(mut self, size: impl Into) -> Self { - self.text_size = Some(size.into().0); - self - } - - /// Sets the font of the [`PickList`]. - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the [`Handle`] of the [`PickList`]. - pub fn handle(mut self, handle: Handle) -> Self { - self.handle = handle; - self - } - - /// Sets the style of the [`PickList`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl<'a, T: 'a, Message, Renderer> Widget - for PickList<'a, T, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::>() - } - - fn state(&self) -> tree::State { - tree::State::new(State::::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.text_size, - self.font, - self.placeholder.as_deref(), - &self.options, - ) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - event, - layout, - cursor_position, - shell, - self.on_selected.as_ref(), - self.selected.as_ref(), - &self.options, - || tree.state.downcast_mut::>(), - ) - } - - fn mouse_interaction( - &self, - _tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let font = self.font.unwrap_or_else(|| renderer.default_font()); - draw( - renderer, - theme, - layout, - cursor_position, - self.padding, - self.text_size, - font, - self.placeholder.as_deref(), - self.selected.as_ref(), - &self.handle, - &self.style, - || tree.state.downcast_ref::>(), - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - let state = tree.state.downcast_mut::>(); - - overlay( - layout, - state, - self.padding, - self.text_size, - self.font.unwrap_or_else(|| renderer.default_font()), - &self.options, - self.style.clone(), - ) - } -} - -impl<'a, T: 'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: Clone + ToString + Eq + 'static, - [T]: ToOwned>, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self { - Self::new(pick_list) - } -} - -/// The local state of a [`PickList`]. -#[derive(Debug)] -pub struct State { - menu: menu::State, - keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, - last_selection: Option, -} - -impl State { - /// Creates a new [`State`] for a [`PickList`]. - pub fn new() -> Self { - Self { - menu: menu::State::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - is_open: bool::default(), - hovered_option: Option::default(), - last_selection: Option::default(), - } - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} - -/// The handle to the right side of the [`PickList`]. -#[derive(Debug, Clone, PartialEq)] -pub enum Handle { - /// Displays an arrow icon (▼). - /// - /// This is the default. - Arrow { - /// Font size of the content. - size: Option, - }, - /// A custom static handle. - Static(Icon), - /// A custom dynamic handle. - Dynamic { - /// The [`Icon`] used when [`PickList`] is closed. - closed: Icon, - /// The [`Icon`] used when [`PickList`] is open. - open: Icon, - }, - /// No handle will be shown. - None, -} - -impl Default for Handle { - fn default() -> Self { - Self::Arrow { size: None } - } -} - -/// The icon of a [`Handle`]. -#[derive(Debug, Clone, PartialEq)] -pub struct Icon { - /// Font that will be used to display the `code_point`, - pub font: Font, - /// The unicode code point that will be used as the icon. - pub code_point: char, - /// Font size of the content. - pub size: Option, -} - -/// Computes the layout of a [`PickList`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - text_size: Option, - font: Option, - placeholder: Option<&str>, - options: &[T], -) -> layout::Node -where - Renderer: text::Renderer, - T: ToString, -{ - use std::f32; - - let limits = limits.width(width).height(Length::Shrink).pad(padding); - let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - - let max_width = match width { - Length::Shrink => { - let measure = |label: &str| -> f32 { - let (width, _) = renderer.measure( - label, - text_size, - font.unwrap_or_else(|| renderer.default_font()), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() - }; - - let labels = options.iter().map(ToString::to_string); - - let labels_width = labels - .map(|label| measure(&label)) - .fold(100.0, |candidate, current| current.max(candidate)); - - let placeholder_width = placeholder.map(measure).unwrap_or(100.0); - - labels_width.max(placeholder_width) - } - _ => 0.0, - }; - - let size = { - let intrinsic = - Size::new(max_width + text_size + padding.left, text_size * 1.2); - - limits.resolve(intrinsic).pad(padding) - }; - - layout::Node::new(size) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] -/// accordingly. -pub fn update<'a, T, Message>( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - on_selected: &dyn Fn(T) -> Message, - selected: Option<&T>, - options: &[T], - state: impl FnOnce() -> &'a mut State, -) -> event::Status -where - T: PartialEq + Clone + 'a, -{ - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - let event_status = if state.is_open { - // Event wasn't processed by overlay, so cursor was clicked either outside it's - // bounds or on the drop-down, either way we close the overlay. - state.is_open = false; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - state.is_open = true; - state.hovered_option = - options.iter().position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; - - if let Some(last_selection) = state.last_selection.take() { - shell.publish((on_selected)(last_selection)); - - state.is_open = false; - - event::Status::Captured - } else { - event_status - } - } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) => { - let state = state(); - - if state.keyboard_modifiers.command() - && layout.bounds().contains(cursor_position) - && !state.is_open - { - fn find_next<'a, T: PartialEq>( - selected: &'a T, - mut options: impl Iterator, - ) -> Option<&'a T> { - let _ = options.find(|&option| option == selected); - - options.next() - } - - let next_option = if y < 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter()) - } else { - options.first() - } - } else if y > 0.0 { - if let Some(selected) = selected { - find_next(selected, options.iter().rev()) - } else { - options.last() - } - } else { - None - }; - - if let Some(next_option) = next_option { - shell.publish((on_selected)(next_option.clone())); - } - - event::Status::Captured - } else { - event::Status::Ignored - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - - event::Status::Ignored - } - _ => event::Status::Ignored, - } -} - -/// Returns the current [`mouse::Interaction`] of a [`PickList`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor_position: Point, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} - -/// Returns the current overlay of a [`PickList`]. -pub fn overlay<'a, T, Message, Renderer>( - layout: Layout<'_>, - state: &'a mut State, - padding: Padding, - text_size: Option, - font: Renderer::Font, - options: &'a [T], - style: ::Style, -) -> Option> -where - T: Clone + ToString, - Message: 'a, - Renderer: text::Renderer + 'a, - Renderer::Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - ::Style: - From<::Style>, -{ - if state.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut state.menu, - options, - &mut state.hovered_option, - &mut state.last_selection, - ) - .width(bounds.width) - .padding(padding) - .font(font) - .style(style); - - if let Some(text_size) = text_size { - menu = menu.text_size(text_size); - } - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } -} - -/// Draws a [`PickList`]. -pub fn draw<'a, T, Renderer>( - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - padding: Padding, - text_size: Option, - font: Renderer::Font, - placeholder: Option<&str>, - selected: Option<&T>, - handle: &Handle, - style: &::Style, - state: impl FnOnce() -> &'a State, -) where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, - T: ToString + 'a, -{ - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = selected.is_some(); - - let style = if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius.into(), - }, - style.background, - ); - - let handle = match handle { - Handle::Arrow { size } => { - Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size)) - } - Handle::Static(Icon { - font, - code_point, - size, - }) => Some((*font, *code_point, *size)), - Handle::Dynamic { open, closed } => { - if state().is_open { - Some((open.font, open.code_point, open.size)) - } else { - Some((closed.font, closed.code_point, closed.size)) - } - } - Handle::None => None, - }; - - if let Some((font, code_point, size)) = handle { - let size = size.unwrap_or_else(|| renderer.default_size()); - - renderer.fill_text(Text { - content: &code_point.to_string(), - size, - font, - color: style.handle_color, - bounds: Rectangle { - x: bounds.x + bounds.width - padding.horizontal(), - y: bounds.center_y(), - height: size * 1.2, - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - } - - let label = selected.map(ToString::to_string); - - if let Some(label) = label.as_deref().or(placeholder) { - let text_size = text_size.unwrap_or_else(|| renderer.default_size()); - - renderer.fill_text(Text { - content: label, - size: text_size, - font, - color: if is_selected { - style.text_color - } else { - style.placeholder_color - }, - bounds: Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.horizontal(), - height: text_size * 1.2, - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - } -} diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs deleted file mode 100644 index dd46fa76..00000000 --- a/native/src/widget/progress_bar.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Provide progress feedback to your users. -use crate::layout; -use crate::renderer; -use crate::widget::Tree; -use crate::{Color, Element, Layout, Length, Point, Rectangle, Size, Widget}; - -use std::ops::RangeInclusive; - -pub use iced_style::progress_bar::{Appearance, StyleSheet}; - -/// A bar that displays progress. -/// -/// # Example -/// ``` -/// # type ProgressBar = iced_native::widget::ProgressBar; -/// let value = 50.0; -/// -/// ProgressBar::new(0.0..=100.0, value); -/// ``` -/// -/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) -#[allow(missing_debug_implementations)] -pub struct ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - range: RangeInclusive, - value: f32, - width: Length, - height: Option, - style: ::Style, -} - -impl ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default height of a [`ProgressBar`]. - pub const DEFAULT_HEIGHT: f32 = 30.0; - - /// Creates a new [`ProgressBar`]. - /// - /// It expects: - /// * an inclusive range of possible values - /// * the current value of the [`ProgressBar`] - pub fn new(range: RangeInclusive, value: f32) -> Self { - ProgressBar { - value: value.clamp(*range.start(), *range.end()), - range, - width: Length::Fill, - height: None, - style: Default::default(), - } - } - - /// Sets the width of the [`ProgressBar`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`ProgressBar`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = Some(height.into()); - self - } - - /// Sets the style of the [`ProgressBar`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl Widget for ProgressBar -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)) - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .width(self.width) - .height(self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT))); - - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let (range_start, range_end) = self.range.clone().into_inner(); - - let active_progress_width = if range_start >= range_end { - 0.0 - } else { - bounds.width * (self.value - range_start) - / (range_end - range_start) - }; - - let style = theme.appearance(&self.style); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { ..bounds }, - border_radius: style.border_radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.background, - ); - - if active_progress_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - width: active_progress_width, - ..bounds - }, - border_radius: style.border_radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.bar, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - progress_bar: ProgressBar, - ) -> Element<'a, Message, Renderer> { - Element::new(progress_bar) - } -} diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs deleted file mode 100644 index 5f60eaef..00000000 --- a/native/src/widget/radio.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! Create choices using radio buttons. -use crate::alignment; -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::text; -use crate::touch; -use crate::widget::{self, Row, Text, Tree}; -use crate::{ - Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Widget, -}; - -pub use iced_style::radio::{Appearance, StyleSheet}; - -/// A circular button representing a choice. -/// -/// # Example -/// ``` -/// # type Radio = -/// # iced_native::widget::Radio; -/// # -/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// pub enum Choice { -/// A, -/// B, -/// } -/// -/// #[derive(Debug, Clone, Copy)] -/// pub enum Message { -/// RadioSelected(Choice), -/// } -/// -/// let selected_choice = Some(Choice::A); -/// -/// Radio::new(Choice::A, "This is A", selected_choice, Message::RadioSelected); -/// -/// Radio::new(Choice::B, "This is B", selected_choice, Message::RadioSelected); -/// ``` -/// -/// ![Radio buttons drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/radio.png?raw=true) -#[allow(missing_debug_implementations)] -pub struct Radio -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - is_selected: bool, - on_click: Message, - label: String, - width: Length, - size: f32, - spacing: f32, - text_size: Option, - font: Option, - style: ::Style, -} - -impl Radio -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default size of a [`Radio`] button. - pub const DEFAULT_SIZE: f32 = 28.0; - - /// The default spacing of a [`Radio`] button. - pub const DEFAULT_SPACING: f32 = 15.0; - - /// Creates a new [`Radio`] button. - /// - /// It expects: - /// * the value related to the [`Radio`] button - /// * the label of the [`Radio`] button - /// * the current selected value - /// * a function that will be called when the [`Radio`] is selected. It - /// receives the value of the radio and must produce a `Message`. - pub fn new( - value: V, - label: impl Into, - selected: Option, - f: F, - ) -> Self - where - V: Eq + Copy, - F: FnOnce(V) -> Message, - { - Radio { - is_selected: Some(value) == selected, - on_click: f(value), - label: label.into(), - width: Length::Shrink, - size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, //15 - text_size: None, - font: None, - style: Default::default(), - } - } - - /// Sets the size of the [`Radio`] button. - pub fn size(mut self, size: impl Into) -> Self { - self.size = size.into().0; - self - } - - /// Sets the width of the [`Radio`] button. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the spacing between the [`Radio`] button and the text. - pub fn spacing(mut self, spacing: impl Into) -> Self { - self.spacing = spacing.into().0; - self - } - - /// Sets the text size of the [`Radio`] button. - pub fn text_size(mut self, text_size: impl Into) -> Self { - self.text_size = Some(text_size.into().0); - self - } - - /// Sets the text font of the [`Radio`] button. - pub fn font(mut self, font: impl Into) -> Self { - self.font = Some(font.into()); - self - } - - /// Sets the style of the [`Radio`] button. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl Widget for Radio -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - Row::<(), Renderer>::new() - .width(self.width) - .spacing(self.spacing) - .align_items(Alignment::Center) - .push(Row::new().width(self.size).height(self.size)) - .push(Text::new(&self.label).width(self.width).size( - self.text_size.unwrap_or_else(|| renderer.default_size()), - )) - .layout(renderer, limits) - } - - fn on_event( - &mut self, - _state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - shell.publish(self.on_click.clone()); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let mut children = layout.children(); - - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_selected) - } else { - theme.active(&self.style, self.is_selected) - }; - - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); - - let size = bounds.width; - let dot_size = size / 2.0; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: (size / 2.0).into(), - border_width: custom_style.border_width, - border_color: custom_style.border_color, - }, - custom_style.background, - ); - - if self.is_selected { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + dot_size / 2.0, - y: bounds.y + dot_size / 2.0, - width: bounds.width - dot_size, - height: bounds.height - dot_size, - }, - border_radius: (dot_size / 2.0).into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - custom_style.dot_color, - ); - } - } - - { - let label_layout = children.next().unwrap(); - - widget::text::draw( - renderer, - style, - label_layout, - &self.label, - self.text_size, - self.font, - widget::text::Appearance { - color: custom_style.text_color, - }, - alignment::Horizontal::Left, - alignment::Vertical::Center, - ); - } - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet + widget::text::StyleSheet, -{ - fn from(radio: Radio) -> Element<'a, Message, Renderer> { - Element::new(radio) - } -} diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs deleted file mode 100644 index 286c1c2d..00000000 --- a/native/src/widget/row.rs +++ /dev/null @@ -1,253 +0,0 @@ -//! Distribute content horizontally. -use crate::event::{self, Event}; -use crate::layout::{self, Layout}; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::widget::{Operation, Tree}; -use crate::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, - Shell, Widget, -}; - -/// A container that distributes its contents horizontally. -#[allow(missing_debug_implementations)] -pub struct Row<'a, Message, Renderer> { - spacing: f32, - padding: Padding, - width: Length, - height: Length, - align_items: Alignment, - children: Vec>, -} - -impl<'a, Message, Renderer> Row<'a, Message, Renderer> { - /// Creates an empty [`Row`]. - pub fn new() -> Self { - Self::with_children(Vec::new()) - } - - /// Creates a [`Row`] with the given elements. - pub fn with_children( - children: Vec>, - ) -> Self { - Row { - spacing: 0.0, - padding: Padding::ZERO, - width: Length::Shrink, - height: Length::Shrink, - align_items: Alignment::Start, - children, - } - } - - /// Sets the horizontal spacing _between_ elements. - /// - /// Custom margins per element do not exist in iced. You should use this - /// method instead! While less flexible, it helps you keep spacing between - /// elements consistent. - pub fn spacing(mut self, amount: impl Into) -> Self { - self.spacing = amount.into().0; - self - } - - /// Sets the [`Padding`] of the [`Row`]. - pub fn padding>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the width of the [`Row`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Row`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; - self - } - - /// Adds an [`Element`] to the [`Row`]. - pub fn push( - mut self, - child: impl Into>, - ) -> Self { - self.children.push(child.into()); - self - } -} - -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget - for Row<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn children(&self) -> Vec { - self.children.iter().map(Tree::new).collect() - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(&self.children) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::flex::resolve( - layout::flex::Axis::Horizontal, - renderer, - &limits, - self.padding, - self.spacing, - self.align_items, - &self.children, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - operation.container(None, &mut |operation| { - self.children - .iter() - .zip(&mut tree.children) - .zip(layout.children()) - .for_each(|((child, state), layout)| { - child - .as_widget() - .operate(state, layout, renderer, operation); - }) - }); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.children - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - self.children - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - for ((child, state), layout) in self - .children - .iter() - .zip(&tree.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: crate::Renderer + 'a, -{ - fn from(row: Row<'a, Message, Renderer>) -> Self { - Self::new(row) - } -} diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs deleted file mode 100644 index 1ab6a0d3..00000000 --- a/native/src/widget/rule.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Display a horizontal or vertical rule for dividing content. -use crate::layout; -use crate::renderer; -use crate::widget::Tree; -use crate::{ - Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Widget, -}; - -pub use iced_style::rule::{Appearance, FillMode, StyleSheet}; - -/// Display a horizontal or vertical rule for dividing content. -#[allow(missing_debug_implementations)] -pub struct Rule -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - width: Length, - height: Length, - is_horizontal: bool, - style: ::Style, -} - -impl Rule -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a horizontal [`Rule`] with the given height. - pub fn horizontal(height: impl Into) -> Self { - Rule { - width: Length::Fill, - height: Length::Fixed(height.into().0), - is_horizontal: true, - style: Default::default(), - } - } - - /// Creates a vertical [`Rule`] with the given width. - pub fn vertical(width: impl Into) -> Self { - Rule { - width: Length::Fixed(width.into().0), - height: Length::Fill, - is_horizontal: false, - style: Default::default(), - } - } - - /// Sets the style of the [`Rule`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -impl Widget for Rule -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - - layout::Node::new(limits.resolve(Size::ZERO)) - } - - fn draw( - &self, - _state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - _cursor_position: Point, - _viewport: &Rectangle, - ) { - let bounds = layout.bounds(); - let style = theme.appearance(&self.style); - - let bounds = if self.is_horizontal { - let line_y = (bounds.y + (bounds.height / 2.0) - - (style.width as f32 / 2.0)) - .round(); - - let (offset, line_width) = style.fill_mode.fill(bounds.width); - let line_x = bounds.x + offset; - - Rectangle { - x: line_x, - y: line_y, - width: line_width, - height: style.width as f32, - } - } else { - let line_x = (bounds.x + (bounds.width / 2.0) - - (style.width as f32 / 2.0)) - .round(); - - let (offset, line_height) = style.fill_mode.fill(bounds.height); - let line_y = bounds.y + offset; - - Rectangle { - x: line_x, - y: line_y, - width: style.width as f32, - height: line_height, - } - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: style.radius.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.color, - ); - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from(rule: Rule) -> Element<'a, Message, Renderer> { - Element::new(rule) - } -} diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs deleted file mode 100644 index c1df8c39..00000000 --- a/native/src/widget/scrollable.rs +++ /dev/null @@ -1,1327 +0,0 @@ -//! Navigate an endless amount of content with a scrollbar. -use crate::event::{self, Event}; -use crate::keyboard; -use crate::layout; -use crate::mouse; -use crate::overlay; -use crate::renderer; -use crate::touch; -use crate::widget; -use crate::widget::operation::{self, Operation}; -use crate::widget::tree::{self, Tree}; -use crate::{ - Background, Clipboard, Color, Command, Element, Layout, Length, Pixels, - Point, Rectangle, Shell, Size, Vector, Widget, -}; - -pub use iced_style::scrollable::StyleSheet; -pub use operation::scrollable::RelativeOffset; - -pub mod style { - //! The styles of a [`Scrollable`]. - //! - //! [`Scrollable`]: crate::widget::Scrollable - pub use iced_style::scrollable::{Scrollbar, Scroller}; -} - -/// A widget that can vertically display an infinite amount of content with a -/// scrollbar. -#[allow(missing_debug_implementations)] -pub struct Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - id: Option, - height: Length, - vertical: Properties, - horizontal: Option, - content: Element<'a, Message, Renderer>, - on_scroll: Option Message + 'a>>, - style: ::Style, -} - -impl<'a, Message, Renderer> Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`Scrollable`]. - pub fn new(content: impl Into>) -> Self { - Scrollable { - id: None, - height: Length::Shrink, - vertical: Properties::default(), - horizontal: None, - content: content.into(), - on_scroll: None, - style: Default::default(), - } - } - - /// Sets the [`Id`] of the [`Scrollable`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// Sets the height of the [`Scrollable`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into(); - self - } - - /// Configures the vertical scrollbar of the [`Scrollable`] . - pub fn vertical_scroll(mut self, properties: Properties) -> Self { - self.vertical = properties; - self - } - - /// Configures the horizontal scrollbar of the [`Scrollable`] . - pub fn horizontal_scroll(mut self, properties: Properties) -> Self { - self.horizontal = Some(properties); - self - } - - /// Sets a function to call when the [`Scrollable`] is scrolled. - /// - /// The function takes the new relative x & y offset of the [`Scrollable`] - /// (e.g. `0` means beginning, while `1` means end). - pub fn on_scroll( - mut self, - f: impl Fn(RelativeOffset) -> Message + 'a, - ) -> Self { - self.on_scroll = Some(Box::new(f)); - self - } - - /// Sets the style of the [`Scrollable`] . - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } -} - -/// Properties of a scrollbar within a [`Scrollable`]. -#[derive(Debug)] -pub struct Properties { - width: f32, - margin: f32, - scroller_width: f32, -} - -impl Default for Properties { - fn default() -> Self { - Self { - width: 10.0, - margin: 0.0, - scroller_width: 10.0, - } - } -} - -impl Properties { - /// Creates new [`Properties`] for use in a [`Scrollable`]. - pub fn new() -> Self { - Self::default() - } - - /// Sets the scrollbar width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into().0.max(1.0); - self - } - - /// Sets the scrollbar margin of the [`Scrollable`] . - pub fn margin(mut self, margin: impl Into) -> Self { - self.margin = margin.into().0; - self - } - - /// Sets the scroller width of the [`Scrollable`] . - /// Silently enforces a minimum width of 1. - pub fn scroller_width(mut self, scroller_width: impl Into) -> Self { - self.scroller_width = scroller_width.into().0.max(1.0); - self - } -} - -impl<'a, Message, Renderer> Widget - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn children(&self) -> Vec { - vec![Tree::new(&self.content)] - } - - fn diff(&self, tree: &mut Tree) { - tree.diff_children(std::slice::from_ref(&self.content)) - } - - fn width(&self) -> Length { - self.content.as_widget().width() - } - - fn height(&self) -> Length { - self.height - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout( - renderer, - limits, - Widget::::width(self), - self.height, - self.horizontal.is_some(), - |renderer, limits| { - self.content.as_widget().layout(renderer, limits) - }, - ) - } - - fn operate( - &self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn Operation, - ) { - let state = tree.state.downcast_mut::(); - - operation.scrollable(state, self.id.as_ref().map(|id| &id.0)); - - operation.container( - self.id.as_ref().map(|id| &id.0), - &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - operation, - ); - }, - ); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - tree.state.downcast_mut::(), - event, - layout, - cursor_position, - clipboard, - shell, - &self.vertical, - self.horizontal.as_ref(), - &self.on_scroll, - |event, layout, cursor_position, clipboard, shell| { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - tree.state.downcast_ref::(), - renderer, - theme, - layout, - cursor_position, - &self.vertical, - self.horizontal.as_ref(), - &self.style, - |renderer, layout, cursor_position, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ) - }, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref::(), - layout, - cursor_position, - &self.vertical, - self.horizontal.as_ref(), - |layout, cursor_position, viewport| { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - }, - ) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - self.content - .as_widget_mut() - .overlay( - &mut tree.children[0], - layout.children().next().unwrap(), - renderer, - ) - .map(|overlay| { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = tree - .state - .downcast_ref::() - .offset(bounds, content_bounds); - - overlay.translate(Vector::new(-offset.x, -offset.y)) - }) - } -} - -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - Message: 'a, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: Scrollable<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} - -/// The identifier of a [`Scrollable`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - -/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage` along the x & y axis. -pub fn snap_to( - id: Id, - offset: RelativeOffset, -) -> Command { - Command::widget(operation::scrollable::snap_to(id.0, offset)) -} - -/// Computes the layout of a [`Scrollable`]. -pub fn layout( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - horizontal_enabled: bool, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - let limits = limits - .max_height(f32::INFINITY) - .max_width(if horizontal_enabled { - f32::INFINITY - } else { - limits.max().width - }) - .width(width) - .height(height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), - Size::new( - if horizontal_enabled { - f32::INFINITY - } else { - limits.max().width - }, - f32::MAX, - ), - ); - - let content = layout_content(renderer, &child_limits); - let size = limits.resolve(content.size()); - - layout::Node::with_children(size, vec![content]) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] -/// accordingly. -pub fn update( - state: &mut State, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - vertical: &Properties, - horizontal: Option<&Properties>, - on_scroll: &Option Message + '_>>, - update_content: impl FnOnce( - Event, - Layout<'_>, - Point, - &mut dyn Clipboard, - &mut Shell<'_, Message>, - ) -> event::Status, -) -> event::Status { - let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); - - let event_status = { - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + state.offset(bounds, content_bounds) - } else { - // TODO: Make `cursor_position` an `Option` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(-1.0, -1.0) - }; - - update_content( - event.clone(), - content, - cursor_position, - clipboard, - shell, - ) - }; - - if let event::Status::Captured = event_status { - return event::Status::Captured; - } - - if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event - { - state.keyboard_modifiers = modifiers; - - return event::Status::Ignored; - } - - if mouse_over_scrollable { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - let delta = match delta { - mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { - Vector::new(y, x) - } else { - Vector::new(x, y) - }; - - movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), - }; - - state.scroll(delta, bounds, content_bounds); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - Event::Touch(event) - if state.scroll_area_touched_at.is_some() - || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => - { - match event { - touch::Event::FingerPressed { .. } => { - state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, - ); - - state.scroll(delta, bounds, content_bounds); - - state.scroll_area_touched_at = - Some(cursor_position); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - state.scroll_area_touched_at = None; - } - } - - return event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.y_scroller_grabbed_at = None; - - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) - { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.x_scroller_grabbed_at = None; - - return event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) - { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } - - event::Status::Ignored -} - -/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. -pub fn mouse_interaction( - state: &State, - layout: Layout<'_>, - cursor_position: Point, - vertical: &Properties, - horizontal: Option<&Properties>, - content_interaction: impl FnOnce( - Layout<'_>, - Point, - &Rectangle, - ) -> mouse::Interaction, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let mouse_over_scrollable = bounds.contains(cursor_position); - - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); - - if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) - || state.scrollers_grabbed() - { - mouse::Interaction::Idle - } else { - let offset = state.offset(bounds, content_bounds); - - let cursor_position = if mouse_over_scrollable - && !(mouse_over_y_scrollbar || mouse_over_x_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) - }; - - content_interaction( - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, - ..bounds - }, - ) - } -} - -/// Draws a [`Scrollable`]. -pub fn draw( - state: &State, - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - vertical: &Properties, - horizontal: Option<&Properties>, - style: &::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), -) where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = - Scrollbars::new(state, vertical, horizontal, bounds, content_bounds); - - let mouse_over_scrollable = bounds.contains(cursor_position); - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor_position); - - let offset = state.offset(bounds, content_bounds); - - let cursor_position = if mouse_over_scrollable - && !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) - { - cursor_position + offset - } else { - Point::new(-1.0, -1.0) - }; - - // Draw inner content - if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(-offset.x, -offset.y), - |renderer| { - draw_content( - renderer, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset.y, - x: bounds.x + offset.x, - ..bounds - }, - ); - }, - ); - }); - - let draw_scrollbar = - |renderer: &mut Renderer, - style: style::Scrollbar, - scrollbar: &Scrollbar| { - //track - if style.background.is_some() - || (style.border_color != Color::TRANSPARENT - && style.border_width > 0.0) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius.into(), - border_width: style.border_width, - border_color: style.border_color, - }, - style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - //thumb - if style.scroller.color != Color::TRANSPARENT - || (style.scroller.border_color != Color::TRANSPARENT - && style.scroller.border_width > 0.0) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius.into(), - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } - }; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - //draw y scrollbar - if let Some(scrollbar) = scrollbars.y { - let style = if state.y_scroller_grabbed_at.is_some() { - theme.dragging(style) - } else if mouse_over_y_scrollbar { - theme.hovered(style) - } else { - theme.active(style) - }; - - draw_scrollbar(renderer, style, &scrollbar); - } - - //draw x scrollbar - if let Some(scrollbar) = scrollbars.x { - let style = if state.x_scroller_grabbed_at.is_some() { - theme.dragging_horizontal(style) - } else if mouse_over_x_scrollbar { - theme.hovered_horizontal(style) - } else { - theme.active_horizontal(style) - }; - - draw_scrollbar(renderer, style, &scrollbar); - } - }, - ); - } else { - draw_content( - renderer, - content_layout, - cursor_position, - &Rectangle { - x: bounds.x + offset.x, - y: bounds.y + offset.y, - ..bounds - }, - ); - } -} - -fn notify_on_scroll( - state: &State, - on_scroll: &Option Message + '_>>, - bounds: Rectangle, - content_bounds: Rectangle, - shell: &mut Shell<'_, Message>, -) { - if let Some(on_scroll) = on_scroll { - if content_bounds.width <= bounds.width - && content_bounds.height <= bounds.height - { - return; - } - - let x = state.offset_x.absolute(bounds.width, content_bounds.width) - / (content_bounds.width - bounds.width); - - let y = state - .offset_y - .absolute(bounds.height, content_bounds.height) - / (content_bounds.height - bounds.height); - - shell.publish(on_scroll(RelativeOffset { x, y })) - } -} - -/// The local state of a [`Scrollable`]. -#[derive(Debug, Clone, Copy)] -pub struct State { - scroll_area_touched_at: Option, - offset_y: Offset, - y_scroller_grabbed_at: Option, - offset_x: Offset, - x_scroller_grabbed_at: Option, - keyboard_modifiers: keyboard::Modifiers, -} - -impl Default for State { - fn default() -> Self { - Self { - scroll_area_touched_at: None, - offset_y: Offset::Absolute(0.0), - y_scroller_grabbed_at: None, - offset_x: Offset::Absolute(0.0), - x_scroller_grabbed_at: None, - keyboard_modifiers: keyboard::Modifiers::default(), - } - } -} - -impl operation::Scrollable for State { - fn snap_to(&mut self, offset: RelativeOffset) { - State::snap_to(self, offset); - } -} - -#[derive(Debug, Clone, Copy)] -enum Offset { - Absolute(f32), - Relative(f32), -} - -impl Offset { - fn absolute(self, window: f32, content: f32) -> f32 { - match self { - Offset::Absolute(absolute) => { - absolute.min((content - window).max(0.0)) - } - Offset::Relative(percentage) => { - ((content - window) * percentage).max(0.0) - } - } - } -} - -impl State { - /// Creates a new [`State`] with the scrollbar(s) at the beginning. - pub fn new() -> Self { - State::default() - } - - /// Apply a scrolling offset to the current [`State`], given the bounds of - /// the [`Scrollable`] and its contents. - pub fn scroll( - &mut self, - delta: Vector, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - if bounds.height < content_bounds.height { - self.offset_y = Offset::Absolute( - (self.offset_y.absolute(bounds.height, content_bounds.height) - - delta.y) - .clamp(0.0, content_bounds.height - bounds.height), - ) - } - - if bounds.width < content_bounds.width { - self.offset_x = Offset::Absolute( - (self.offset_x.absolute(bounds.width, content_bounds.width) - - delta.x) - .clamp(0.0, content_bounds.width - bounds.width), - ); - } - } - - /// Scrolls the [`Scrollable`] to a relative amount along the y axis. - /// - /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at - /// the end. - pub fn scroll_y_to( - &mut self, - percentage: f32, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - self.offset_y = Offset::Relative(percentage.clamp(0.0, 1.0)); - self.unsnap(bounds, content_bounds); - } - - /// Scrolls the [`Scrollable`] to a relative amount along the x axis. - /// - /// `0` represents scrollbar at the beginning, while `1` represents scrollbar at - /// the end. - pub fn scroll_x_to( - &mut self, - percentage: f32, - bounds: Rectangle, - content_bounds: Rectangle, - ) { - self.offset_x = Offset::Relative(percentage.clamp(0.0, 1.0)); - self.unsnap(bounds, content_bounds); - } - - /// Snaps the scroll position to a [`RelativeOffset`]. - pub fn snap_to(&mut self, offset: RelativeOffset) { - self.offset_x = Offset::Relative(offset.x.clamp(0.0, 1.0)); - self.offset_y = Offset::Relative(offset.y.clamp(0.0, 1.0)); - } - - /// Unsnaps the current scroll position, if snapped, given the bounds of the - /// [`Scrollable`] and its contents. - pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { - self.offset_x = Offset::Absolute( - self.offset_x.absolute(bounds.width, content_bounds.width), - ); - self.offset_y = Offset::Absolute( - self.offset_y.absolute(bounds.height, content_bounds.height), - ); - } - - /// Returns the scrolling offset of the [`State`], given the bounds of the - /// [`Scrollable`] and its contents. - pub fn offset( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Vector { - Vector::new( - self.offset_x.absolute(bounds.width, content_bounds.width), - self.offset_y.absolute(bounds.height, content_bounds.height), - ) - } - - /// Returns whether any scroller is currently grabbed or not. - pub fn scrollers_grabbed(&self) -> bool { - self.x_scroller_grabbed_at.is_some() - || self.y_scroller_grabbed_at.is_some() - } -} - -#[derive(Debug)] -/// State of both [`Scrollbar`]s. -struct Scrollbars { - y: Option, - x: Option, -} - -impl Scrollbars { - /// Create y and/or x scrollbar(s) if content is overflowing the [`Scrollable`] bounds. - fn new( - state: &State, - vertical: &Properties, - horizontal: Option<&Properties>, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Self { - let offset = state.offset(bounds, content_bounds); - - let show_scrollbar_x = horizontal.and_then(|h| { - if content_bounds.width > bounds.width { - Some(h) - } else { - None - } - }); - - let y_scrollbar = if content_bounds.height > bounds.height { - let Properties { - width, - margin, - scroller_width, - } = *vertical; - - // Adjust the height of the vertical scrollbar if the horizontal scrollbar - // is present - let x_scrollbar_height = show_scrollbar_x - .map_or(0.0, |h| h.width.max(h.scroller_width) + h.margin); - - let total_scrollbar_width = - width.max(scroller_width) + 2.0 * margin; - - // Total bounds of the scrollbar + margin + scroller width - let total_scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - total_scrollbar_width, - y: bounds.y, - width: total_scrollbar_width, - height: (bounds.height - x_scrollbar_height).max(0.0), - }; - - // Bounds of just the scrollbar - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - width / 2.0, - y: bounds.y, - width, - height: (bounds.height - x_scrollbar_height).max(0.0), - }; - - let ratio = bounds.height / content_bounds.height; - // min height for easier grabbing with super tall content - let scroller_height = (bounds.height * ratio).max(2.0); - let scroller_offset = offset.y * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - scroller_width / 2.0, - y: (scrollbar_bounds.y + scroller_offset - x_scrollbar_height) - .max(0.0), - width: scroller_width, - height: scroller_height, - }; - - Some(Scrollbar { - total_bounds: total_scrollbar_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - }; - - let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { - let Properties { - width, - margin, - scroller_width, - } = *horizontal; - - // Need to adjust the width of the horizontal scrollbar if the vertical scrollbar - // is present - let scrollbar_y_width = y_scrollbar.map_or(0.0, |_| { - vertical.width.max(vertical.scroller_width) + vertical.margin - }); - - let total_scrollbar_height = - width.max(scroller_width) + 2.0 * margin; - - // Total bounds of the scrollbar + margin + scroller width - let total_scrollbar_bounds = Rectangle { - x: bounds.x, - y: bounds.y + bounds.height - total_scrollbar_height, - width: (bounds.width - scrollbar_y_width).max(0.0), - height: total_scrollbar_height, - }; - - // Bounds of just the scrollbar - let scrollbar_bounds = Rectangle { - x: bounds.x, - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - width / 2.0, - width: (bounds.width - scrollbar_y_width).max(0.0), - height: width, - }; - - let ratio = bounds.width / content_bounds.width; - // min width for easier grabbing with extra wide content - let scroller_length = (bounds.width * ratio).max(2.0); - let scroller_offset = offset.x * ratio; - - let scroller_bounds = Rectangle { - x: (scrollbar_bounds.x + scroller_offset - scrollbar_y_width) - .max(0.0), - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - scroller_width / 2.0, - width: scroller_length, - height: scroller_width, - }; - - Some(Scrollbar { - total_bounds: total_scrollbar_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - }; - - Self { - y: y_scrollbar, - x: x_scrollbar, - } - } - - fn is_mouse_over(&self, cursor_position: Point) -> (bool, bool) { - ( - self.y - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false), - self.x - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false), - ) - } - - fn grab_y_scroller(&self, cursor_position: Point) -> Option { - self.y.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.y - scrollbar.scroller.bounds.y) - / scrollbar.scroller.bounds.height - } else { - 0.5 - }) - } else { - None - } - }) - } - - fn grab_x_scroller(&self, cursor_position: Point) -> Option { - self.x.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.x - scrollbar.scroller.bounds.x) - / scrollbar.scroller.bounds.width - } else { - 0.5 - }) - } else { - None - } - }) - } - - fn active(&self) -> bool { - self.y.is_some() || self.x.is_some() - } -} - -/// The scrollbar of a [`Scrollable`]. -#[derive(Debug, Copy, Clone)] -struct Scrollbar { - /// The total bounds of the [`Scrollbar`], including the scrollbar, the scroller, - /// and the scrollbar margin. - total_bounds: Rectangle, - - /// The bounds of just the [`Scrollbar`]. - bounds: Rectangle, - - /// The state of this scrollbar's [`Scroller`]. - scroller: Scroller, -} - -impl Scrollbar { - /// Returns whether the mouse is over the scrollbar or not. - fn is_mouse_over(&self, cursor_position: Point) -> bool { - self.total_bounds.contains(cursor_position) - } - - /// Returns the y-axis scrolled percentage from the cursor position. - fn scroll_percentage_y( - &self, - grabbed_at: f32, - cursor_position: Point, - ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { - // cursor position is unavailable! Set to either end or beginning of scrollbar depending - // on where the thumb currently is in the track - (self.scroller.bounds.y / self.total_bounds.height).round() - } else { - (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height) - } - } - - /// Returns the x-axis scrolled percentage from the cursor position. - fn scroll_percentage_x( - &self, - grabbed_at: f32, - cursor_position: Point, - ) -> f32 { - if cursor_position.x < 0.0 && cursor_position.y < 0.0 { - (self.scroller.bounds.x / self.total_bounds.width).round() - } else { - (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width) - } - } -} - -/// The handle of a [`Scrollbar`]. -#[derive(Debug, Clone, Copy)] -struct Scroller { - /// The bounds of the [`Scroller`]. - bounds: Rectangle, -} diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs deleted file mode 100644 index d3715b1c..00000000 --- a/native/src/widget/slider.rs +++ /dev/null @@ -1,473 +0,0 @@ -//! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. -use crate::event::{self, Event}; -use crate::layout; -use crate::mouse; -use crate::renderer; -use crate::touch; -use crate::widget::tree::{self, Tree}; -use crate::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Widget, -}; - -use std::ops::RangeInclusive; - -pub use iced_style::slider::{Appearance, Handle, HandleShape, StyleSheet}; - -/// An horizontal bar and a handle that selects a single value from a range of -/// values. -/// -/// A [`Slider`] will try to fill the horizontal space of its container. -/// -/// The [`Slider`] range of numeric values is generic and its step size defaults -/// to 1 unit. -/// -/// # Example -/// ``` -/// # use iced_native::widget::slider; -/// # use iced_native::renderer::Null; -/// # -/// # type Slider<'a, T, Message> = slider::Slider<'a, T, Message, Null>; -/// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), -/// } -/// -/// let value = 50.0; -/// -/// Slider::new(0.0..=100.0, value, Message::SliderChanged); -/// ``` -/// -/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) -#[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Renderer> -where - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - range: RangeInclusive, - step: T, - value: T, - on_change: Box Message + 'a>, - on_release: Option, - width: Length, - height: f32, - style: ::Style, -} - -impl<'a, T, Message, Renderer> Slider<'a, T, Message, Renderer> -where - T: Copy + From + std::cmp::PartialOrd, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - /// The default height of a [`Slider`]. - pub const DEFAULT_HEIGHT: f32 = 22.0; - - /// Creates a new [`Slider`]. - /// - /// It expects: - /// * an inclusive range of possible values - /// * the current value of the [`Slider`] - /// * a function that will be called when the [`Slider`] is dragged. - /// It receives the new value of the [`Slider`] and must produce a - /// `Message`. - pub fn new(range: RangeInclusive, value: T, on_change: F) -> Self - where - F: 'a + Fn(T) -> Message, - { - let value = if value >= *range.start() { - value - } else { - *range.start() - }; - - let value = if value <= *range.end() { - value - } else { - *range.end() - }; - - Slider { - value, - range, - step: T::from(1), - on_change: Box::new(on_change), - on_release: None, - width: Length::Fill, - height: Self::DEFAULT_HEIGHT, - style: Default::default(), - } - } - - /// Sets the release message of the [`Slider`]. - /// This is called when the mouse is released from the slider. - /// - /// Typically, the user's interaction with the slider is finished when this message is produced. - /// This is useful if you need to spawn a long-running task from the slider's result, where - /// the default on_change message could create too many events. - pub fn on_release(mut self, on_release: Message) -> Self { - self.on_release = Some(on_release); - self - } - - /// Sets the width of the [`Slider`]. - pub fn width(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - /// Sets the height of the [`Slider`]. - pub fn height(mut self, height: impl Into) -> Self { - self.height = height.into().0; - self - } - - /// Sets the style of the [`Slider`]. - pub fn style( - mut self, - style: impl Into<::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Sets the step size of the [`Slider`]. - pub fn step(mut self, step: T) -> Self { - self.step = step; - self - } -} - -impl<'a, T, Message, Renderer> Widget - for Slider<'a, T, Message, Renderer> -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, - Renderer: crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - _renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits.width(self.width).height(self.height); - let size = limits.resolve(Size::ZERO); - - layout::Node::new(size) - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - event, - layout, - cursor_position, - shell, - tree.state.downcast_mut::(), - &mut self.value, - &self.range, - self.step, - self.on_change.as_ref(), - &self.on_release, - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - layout, - cursor_position, - tree.state.downcast_ref::(), - self.value, - &self.range, - theme, - &self.style, - ) - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction( - layout, - cursor_position, - tree.state.downcast_ref::(), - ) - } -} - -impl<'a, T, Message, Renderer> From> - for Element<'a, Message, Renderer> -where - T: 'a + Copy + Into + num_traits::FromPrimitive, - Message: 'a + Clone, - Renderer: 'a + crate::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - slider: Slider<'a, T, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(slider) - } -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] -/// accordingly. -pub fn update( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - shell: &mut Shell<'_, Message>, - state: &mut State, - value: &mut T, - range: &RangeInclusive, - step: T, - on_change: &dyn Fn(T) -> Message, - on_release: &Option, -) -> event::Status -where - T: Copy + Into + num_traits::FromPrimitive, - Message: Clone, -{ - let is_dragging = state.is_dragging; - - let mut change = || { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - *range.start() - } else if cursor_position.x >= bounds.x + bounds.width { - *range.end() - } else { - let step = step.into(); - let start = (*range.start()).into(); - let end = (*range.end()).into(); - - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } - }; - - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); - - *value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); - state.is_dragging = true; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = on_release.clone() { - shell.publish(on_release); - } - state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - change(); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws a [`Slider`]. -pub fn draw( - renderer: &mut R, - layout: Layout<'_>, - cursor_position: Point, - state: &State, - value: T, - range: &RangeInclusive, - style_sheet: &dyn StyleSheet