summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/format.yml12
-rw-r--r--Cargo.toml2
-rw-r--r--core/src/keyboard.rs6
-rw-r--r--core/src/keyboard/key_code.rs (renamed from native/src/input/keyboard/key_code.rs)0
-rw-r--r--core/src/keyboard/modifiers_state.rs30
-rw-r--r--core/src/lib.rs1
-rw-r--r--core/src/point.rs21
-rw-r--r--core/src/rectangle.rs50
-rw-r--r--core/src/vector.rs13
-rw-r--r--examples/README.md2
-rw-r--r--examples/bezier_tool/src/main.rs5
-rw-r--r--examples/download_progress/Cargo.toml12
-rw-r--r--examples/download_progress/README.md17
-rw-r--r--examples/download_progress/src/download.rs112
-rw-r--r--examples/download_progress/src/main.rs143
-rw-r--r--examples/geometry/src/main.rs4
-rw-r--r--examples/integration/src/main.rs16
-rw-r--r--examples/pane_grid/Cargo.toml9
-rw-r--r--examples/pane_grid/README.md28
-rw-r--r--examples/pane_grid/src/main.rs306
-rw-r--r--examples/pokedex/Cargo.toml14
-rw-r--r--examples/styling/src/main.rs1
-rw-r--r--examples/svg/Cargo.toml1
-rw-r--r--examples/svg/src/main.rs11
-rw-r--r--futures/src/subscription.rs70
-rw-r--r--native/src/element.rs2
-rw-r--r--native/src/input/keyboard.rs5
-rw-r--r--native/src/input/keyboard/modifiers_state.rs15
-rw-r--r--native/src/layout/limits.rs2
-rw-r--r--native/src/layout/node.rs4
-rw-r--r--native/src/lib.rs4
-rw-r--r--native/src/mouse_cursor.rs6
-rw-r--r--native/src/widget.rs3
-rw-r--r--native/src/widget/column.rs13
-rw-r--r--native/src/widget/container.rs2
-rw-r--r--native/src/widget/image.rs30
-rw-r--r--native/src/widget/pane_grid.rs647
-rw-r--r--native/src/widget/pane_grid/axis.rs54
-rw-r--r--native/src/widget/pane_grid/direction.rs12
-rw-r--r--native/src/widget/pane_grid/node.rs199
-rw-r--r--native/src/widget/pane_grid/pane.rs5
-rw-r--r--native/src/widget/pane_grid/split.rs5
-rw-r--r--native/src/widget/pane_grid/state.rs368
-rw-r--r--native/src/widget/row.rs11
-rw-r--r--native/src/widget/scrollable.rs4
-rw-r--r--rustfmt.toml2
-rw-r--r--src/application.rs1
-rw-r--r--src/keyboard.rs6
-rw-r--r--src/lib.rs1
-rw-r--r--src/widget.rs5
-rw-r--r--web/README.md2
-rw-r--r--web/src/lib.rs4
-rw-r--r--web/src/widget/column.rs9
-rw-r--r--web/src/widget/row.rs9
-rw-r--r--wgpu/Cargo.toml3
-rw-r--r--wgpu/src/image.rs356
-rw-r--r--wgpu/src/image/atlas.rs361
-rw-r--r--wgpu/src/image/atlas/allocation.rs35
-rw-r--r--wgpu/src/image/atlas/allocator.rs69
-rw-r--r--wgpu/src/image/atlas/entry.rs26
-rw-r--r--wgpu/src/image/atlas/layer.rs17
-rw-r--r--wgpu/src/image/raster.rs154
-rw-r--r--wgpu/src/image/vector.rs153
-rw-r--r--wgpu/src/lib.rs5
-rw-r--r--wgpu/src/primitive.rs13
-rw-r--r--wgpu/src/quad.rs7
-rw-r--r--wgpu/src/renderer.rs229
-rw-r--r--wgpu/src/renderer/widget.rs1
-rw-r--r--wgpu/src/renderer/widget/pane_grid.rs92
-rw-r--r--wgpu/src/renderer/widget/scrollable.rs14
-rw-r--r--wgpu/src/settings.rs17
-rw-r--r--wgpu/src/shader/image.frag6
-rw-r--r--wgpu/src/shader/image.frag.spvbin684 -> 684 bytes
-rw-r--r--wgpu/src/shader/image.vert27
-rw-r--r--wgpu/src/shader/image.vert.spvbin2136 -> 2504 bytes
-rw-r--r--wgpu/src/text.rs8
-rw-r--r--wgpu/src/triangle.rs43
-rw-r--r--wgpu/src/triangle/msaa.rs12
-rw-r--r--wgpu/src/widget.rs3
-rw-r--r--wgpu/src/widget/canvas.rs6
-rw-r--r--wgpu/src/widget/canvas/frame.rs71
-rw-r--r--wgpu/src/widget/canvas/layer.rs14
-rw-r--r--wgpu/src/widget/canvas/layer/cache.rs34
-rw-r--r--wgpu/src/widget/canvas/text.rs34
-rw-r--r--wgpu/src/widget/pane_grid.rs17
-rw-r--r--wgpu/src/window/backend.rs12
-rw-r--r--wgpu/src/window/swap_chain.rs6
-rw-r--r--winit/Cargo.toml7
-rw-r--r--winit/src/application.rs9
-rw-r--r--winit/src/conversion.rs4
90 files changed, 3633 insertions, 548 deletions
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 00000000..92caff79
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,12 @@
+name: Format
+on: [push, pull_request]
+jobs:
+ all:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: hecrj/setup-rust-action@v1
+ with:
+ components: rustfmt
+ - uses: actions/checkout@master
+ - name: Check format
+ run: cargo fmt --all -- --check
diff --git a/Cargo.toml b/Cargo.toml
index 01231b70..d2444486 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,7 @@ members = [
"web",
"wgpu",
"winit",
+ "examples/download_progress",
"examples/bezier_tool",
"examples/clock",
"examples/counter",
@@ -45,6 +46,7 @@ members = [
"examples/geometry",
"examples/integration",
"examples/pokedex",
+ "examples/pane_grid",
"examples/progress_bar",
"examples/solar_system",
"examples/stopwatch",
diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs
new file mode 100644
index 00000000..d98b2989
--- /dev/null
+++ b/core/src/keyboard.rs
@@ -0,0 +1,6 @@
+//! Reuse basic keyboard types.
+mod key_code;
+mod modifiers_state;
+
+pub use key_code::KeyCode;
+pub use modifiers_state::ModifiersState;
diff --git a/native/src/input/keyboard/key_code.rs b/core/src/keyboard/key_code.rs
index 26020a57..26020a57 100644
--- a/native/src/input/keyboard/key_code.rs
+++ b/core/src/keyboard/key_code.rs
diff --git a/core/src/keyboard/modifiers_state.rs b/core/src/keyboard/modifiers_state.rs
new file mode 100644
index 00000000..4d24266f
--- /dev/null
+++ b/core/src/keyboard/modifiers_state.rs
@@ -0,0 +1,30 @@
+/// The current state of the keyboard modifiers.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct ModifiersState {
+ /// Whether a shift key is pressed
+ pub shift: bool,
+
+ /// Whether a control key is pressed
+ pub control: bool,
+
+ /// Whether an alt key is pressed
+ pub alt: bool,
+
+ /// Whether a logo key is pressed (e.g. windows key, command key...)
+ pub logo: bool,
+}
+
+impl ModifiersState {
+ /// Returns true if the current [`ModifiersState`] has at least the same
+ /// modifiers enabled as the given value, and false otherwise.
+ ///
+ /// [`ModifiersState`]: struct.ModifiersState.html
+ pub fn matches(&self, modifiers: ModifiersState) -> bool {
+ let shift = !modifiers.shift || self.shift;
+ let control = !modifiers.control || self.control;
+ let alt = !modifiers.alt || self.alt;
+ let logo = !modifiers.logo || self.logo;
+
+ shift && control && alt && logo
+ }
+}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index ea5e8b43..c2887a0b 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -14,6 +14,7 @@
#![deny(unused_results)]
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
+pub mod keyboard;
mod align;
mod background;
diff --git a/core/src/point.rs b/core/src/point.rs
index b9a8149c..43ee2143 100644
--- a/core/src/point.rs
+++ b/core/src/point.rs
@@ -22,6 +22,16 @@ impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
+
+ /// Computes the distance to another [`Point`].
+ ///
+ /// [`Point`]: struct.Point.html
+ pub fn distance(&self, to: Point) -> f32 {
+ let a = self.x - to.x;
+ let b = self.y - to.y;
+
+ a.hypot(b)
+ }
}
impl From<[f32; 2]> for Point {
@@ -46,3 +56,14 @@ impl std::ops::Add<Vector> for Point {
}
}
}
+
+impl std::ops::Sub<Vector> for Point {
+ type Output = Self;
+
+ fn sub(self, vector: Vector) -> Self {
+ Self {
+ x: self.x - vector.x,
+ y: self.y - vector.y,
+ }
+ }
+}
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index ee1e3807..7ed3d2df 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -27,6 +27,34 @@ impl Rectangle<f32> {
&& self.y <= point.y
&& point.y <= self.y + self.height
}
+
+ /// Computes the intersection with the given [`Rectangle`].
+ ///
+ /// [`Rectangle`]: struct.Rectangle.html
+ pub fn intersection(
+ &self,
+ other: &Rectangle<f32>,
+ ) -> Option<Rectangle<f32>> {
+ let x = self.x.max(other.x);
+ let y = self.y.max(other.y);
+
+ let lower_right_x = (self.x + self.width).min(other.x + other.width);
+ let lower_right_y = (self.y + self.height).min(other.y + other.height);
+
+ let width = lower_right_x - x;
+ let height = lower_right_y - y;
+
+ if width > 0.0 && height > 0.0 {
+ Some(Rectangle {
+ x,
+ y,
+ width,
+ height,
+ })
+ } else {
+ None
+ }
+ }
}
impl std::ops::Mul<f32> for Rectangle<u32> {
@@ -41,3 +69,25 @@ impl std::ops::Mul<f32> for Rectangle<u32> {
}
}
}
+
+impl From<Rectangle<u32>> for Rectangle<f32> {
+ fn from(rectangle: Rectangle<u32>) -> Rectangle<f32> {
+ Rectangle {
+ x: rectangle.x as f32,
+ y: rectangle.y as f32,
+ width: rectangle.width as f32,
+ height: rectangle.height as f32,
+ }
+ }
+}
+
+impl From<Rectangle<f32>> for Rectangle<u32> {
+ fn from(rectangle: Rectangle<f32>) -> Rectangle<u32> {
+ Rectangle {
+ x: rectangle.x as u32,
+ y: rectangle.y as u32,
+ width: rectangle.width.ceil() as u32,
+ height: rectangle.height.ceil() as u32,
+ }
+ }
+}
diff --git a/core/src/vector.rs b/core/src/vector.rs
index 1c09ee3e..a75053a0 100644
--- a/core/src/vector.rs
+++ b/core/src/vector.rs
@@ -16,7 +16,7 @@ impl<T> Vector<T> {
/// Creates a new [`Vector`] with the given components.
///
/// [`Vector`]: struct.Vector.html
- pub fn new(x: T, y: T) -> Self {
+ pub const fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
@@ -32,6 +32,17 @@ where
}
}
+impl<T> std::ops::Sub for Vector<T>
+where
+ T: std::ops::Sub<Output = T>,
+{
+ type Output = Self;
+
+ fn sub(self, b: Self) -> Self {
+ Self::new(self.x - b.x, self.y - b.y)
+ }
+}
+
impl<T> Default for Vector<T>
where
T: Default,
diff --git a/examples/README.md b/examples/README.md
index 04399b93..5aea51eb 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -73,9 +73,11 @@ A bunch of simpler examples exist:
- [`clock`](clock), an application that uses the `Canvas` widget to draw a clock and its hands to display the current time.
- [`counter`](counter), the classic counter example explained in the [`README`](../README.md).
- [`custom_widget`](custom_widget), a demonstration of how to build a custom widget that draws a circle.
+- [`download_progress`](download_progress), a basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
- [`events`](events), a log of native events displayed using a conditional `Subscription`.
- [`geometry`](geometry), a custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../wgpu).
- [`integration`](integration), a demonstration of how to integrate Iced in an existing graphical application.
+- [`pane_grid`](pane_grid), a grid of panes that can be split, resized, and reorganized.
- [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI].
- [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider.
- [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms.
diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs
index 023eb0f7..c3fbf276 100644
--- a/examples/bezier_tool/src/main.rs
+++ b/examples/bezier_tool/src/main.rs
@@ -23,7 +23,6 @@ mod bezier {
basic_shapes, BuffersBuilder, StrokeAttributes, StrokeOptions,
StrokeTessellator, VertexBuffers,
};
- use std::sync::Arc;
pub struct Bezier<'a, Message> {
state: &'a mut State,
@@ -175,10 +174,10 @@ mod bezier {
let mesh = Primitive::Mesh2D {
origin: Point::new(bounds.x, bounds.y),
- buffers: Arc::new(Mesh2D {
+ buffers: Mesh2D {
vertices: buffer.vertices,
indices: buffer.indices,
- }),
+ },
};
(
diff --git a/examples/download_progress/Cargo.toml b/examples/download_progress/Cargo.toml
new file mode 100644
index 00000000..34e6a132
--- /dev/null
+++ b/examples/download_progress/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "download_progress"
+version = "0.1.0"
+authors = ["Songtronix <contact@songtronix.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["tokio"] }
+iced_native = { path = "../../native" }
+iced_futures = { path = "../../futures" }
+reqwest = "0.10"
diff --git a/examples/download_progress/README.md b/examples/download_progress/README.md
new file mode 100644
index 00000000..c606c5f9
--- /dev/null
+++ b/examples/download_progress/README.md
@@ -0,0 +1,17 @@
+## Download progress
+
+A basic application that asynchronously downloads a dummy file of 100 MB and tracks the download progress.
+
+The example implements a custom `Subscription` in the __[`download`](src/download.rs)__ module. This subscription downloads and produces messages that can be used to keep track of its progress.
+
+<div align="center">
+ <a href="https://gfycat.com/wildearlyafricanwilddog">
+ <img src="https://thumbs.gfycat.com/WildEarlyAfricanwilddog-small.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+
+```
+cargo run --package download_progress
+```
diff --git a/examples/download_progress/src/download.rs b/examples/download_progress/src/download.rs
new file mode 100644
index 00000000..f46a01f7
--- /dev/null
+++ b/examples/download_progress/src/download.rs
@@ -0,0 +1,112 @@
+use iced_futures::futures;
+
+// Just a little utility function
+pub fn file<T: ToString>(url: T) -> iced::Subscription<Progress> {
+ iced::Subscription::from_recipe(Download {
+ url: url.to_string(),
+ })
+}
+
+pub struct Download {
+ url: String,
+}
+
+// Make sure iced can use our download stream
+impl<H, I> iced_native::subscription::Recipe<H, I> for Download
+where
+ H: std::hash::Hasher,
+{
+ type Output = Progress;
+
+ fn hash(&self, state: &mut H) {
+ use std::hash::Hash;
+
+ std::any::TypeId::of::<Self>().hash(state);
+ self.url.hash(state);
+ }
+
+ fn stream(
+ self: Box<Self>,
+ _input: futures::stream::BoxStream<'static, I>,
+ ) -> futures::stream::BoxStream<'static, Self::Output> {
+ Box::pin(futures::stream::unfold(
+ State::Ready(self.url),
+ |state| async move {
+ match state {
+ State::Ready(url) => {
+ let response = reqwest::get(&url).await;
+
+ match response {
+ Ok(response) => {
+ if let Some(total) = response.content_length() {
+ Some((
+ Progress::Started,
+ State::Downloading {
+ response,
+ total,
+ downloaded: 0,
+ },
+ ))
+ } else {
+ Some((Progress::Errored, State::Finished))
+ }
+ }
+ Err(_) => {
+ Some((Progress::Errored, State::Finished))
+ }
+ }
+ }
+ State::Downloading {
+ mut response,
+ total,
+ downloaded,
+ } => match response.chunk().await {
+ Ok(Some(chunk)) => {
+ let downloaded = downloaded + chunk.len() as u64;
+
+ let percentage =
+ (downloaded as f32 / total as f32) * 100.0;
+
+ Some((
+ Progress::Advanced(percentage),
+ State::Downloading {
+ response,
+ total,
+ downloaded,
+ },
+ ))
+ }
+ Ok(None) => Some((Progress::Finished, State::Finished)),
+ Err(_) => Some((Progress::Errored, State::Finished)),
+ },
+ State::Finished => {
+ // We do not let the stream die, as it would start a
+ // new download repeatedly if the user is not careful
+ // in case of errors.
+ let _: () = iced::futures::future::pending().await;
+
+ None
+ }
+ }
+ },
+ ))
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Progress {
+ Started,
+ Advanced(f32),
+ Finished,
+ Errored,
+}
+
+pub enum State {
+ Ready(String),
+ Downloading {
+ response: reqwest::Response,
+ total: u64,
+ downloaded: u64,
+ },
+ Finished,
+}
diff --git a/examples/download_progress/src/main.rs b/examples/download_progress/src/main.rs
new file mode 100644
index 00000000..6c3094f7
--- /dev/null
+++ b/examples/download_progress/src/main.rs
@@ -0,0 +1,143 @@
+use iced::{
+ button, executor, Align, Application, Button, Column, Command, Container,
+ Element, Length, ProgressBar, Settings, Subscription, Text,
+};
+
+mod download;
+
+pub fn main() {
+ Example::run(Settings::default())
+}
+
+#[derive(Debug)]
+enum Example {
+ Idle { button: button::State },
+ Downloading { progress: f32 },
+ Finished { button: button::State },
+ Errored { button: button::State },
+}
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ Download,
+ DownloadProgressed(download::Progress),
+}
+
+impl Application for Example {
+ type Executor = executor::Default;
+ type Message = Message;
+
+ fn new() -> (Example, Command<Message>) {
+ (
+ Example::Idle {
+ button: button::State::new(),
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Download progress - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::Download => match self {
+ Example::Idle { .. }
+ | Example::Finished { .. }
+ | Example::Errored { .. } => {
+ *self = Example::Downloading { progress: 0.0 };
+ }
+ _ => {}
+ },
+ Message::DownloadProgressed(message) => match self {
+ Example::Downloading { progress } => match message {
+ download::Progress::Started => {
+ *progress = 0.0;
+ }
+ download::Progress::Advanced(percentage) => {
+ *progress = percentage;
+ }
+ download::Progress::Finished => {
+ *self = Example::Finished {
+ button: button::State::new(),
+ }
+ }
+ download::Progress::Errored => {
+ *self = Example::Errored {
+ button: button::State::new(),
+ };
+ }
+ },
+ _ => {}
+ },
+ };
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ match self {
+ Example::Downloading { .. } => {
+ download::file("https://speed.hetzner.de/100MB.bin")
+ .map(Message::DownloadProgressed)
+ }
+ _ => Subscription::none(),
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let current_progress = match self {
+ Example::Idle { .. } => 0.0,
+ Example::Downloading { progress } => *progress,
+ Example::Finished { .. } => 100.0,
+ Example::Errored { .. } => 0.0,
+ };
+
+ let progress_bar = ProgressBar::new(0.0..=100.0, current_progress);
+
+ let control: Element<_> = match self {
+ Example::Idle { button } => {
+ Button::new(button, Text::new("Start the download!"))
+ .on_press(Message::Download)
+ .into()
+ }
+ Example::Finished { button } => Column::new()
+ .spacing(10)
+ .align_items(Align::Center)
+ .push(Text::new("Download finished!"))
+ .push(
+ Button::new(button, Text::new("Start again"))
+ .on_press(Message::Download),
+ )
+ .into(),
+ Example::Downloading { .. } => {
+ Text::new(format!("Downloading... {:.2}%", current_progress))
+ .into()
+ }
+ Example::Errored { button } => Column::new()
+ .spacing(10)
+ .align_items(Align::Center)
+ .push(Text::new("Something went wrong :("))
+ .push(
+ Button::new(button, Text::new("Try again"))
+ .on_press(Message::Download),
+ )
+ .into(),
+ };
+
+ let content = Column::new()
+ .spacing(10)
+ .padding(10)
+ .align_items(Align::Center)
+ .push(progress_bar)
+ .push(control);
+
+ Container::new(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs
index 795c6a71..13a687ab 100644
--- a/examples/geometry/src/main.rs
+++ b/examples/geometry/src/main.rs
@@ -87,7 +87,7 @@ mod rainbow {
(
Primitive::Mesh2D {
origin: Point::new(b.x, b.y),
- buffers: std::sync::Arc::new(Mesh2D {
+ buffers: Mesh2D {
vertices: vec![
Vertex2D {
position: posn_center,
@@ -136,7 +136,7 @@ mod rainbow {
0, 7, 8, // BL
0, 8, 1, // L
],
- }),
+ },
},
MouseCursor::OutOfBounds,
)
diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs
index ed36f736..2cb89ffc 100644
--- a/examples/integration/src/main.rs
+++ b/examples/integration/src/main.rs
@@ -10,7 +10,7 @@ use iced_wgpu::{
use iced_winit::{winit, Cache, Clipboard, MouseCursor, Size, UserInterface};
use winit::{
- event::{DeviceEvent, Event, ModifiersState, WindowEvent},
+ event::{Event, ModifiersState, WindowEvent},
event_loop::{ControlFlow, EventLoop},
};
@@ -40,11 +40,12 @@ pub fn main() {
});
let surface = wgpu::Surface::create(&window);
+ let format = wgpu::TextureFormat::Bgra8UnormSrgb;
let mut swap_chain = {
let size = window.inner_size();
- SwapChain::new(&device, &surface, size.width, size.height)
+ SwapChain::new(&device, &surface, format, size.width, size.height)
};
let mut resized = false;
@@ -65,14 +66,11 @@ pub fn main() {
*control_flow = ControlFlow::Wait;
match event {
- Event::DeviceEvent {
- event: DeviceEvent::ModifiersChanged(new_modifiers),
- ..
- } => {
- modifiers = new_modifiers;
- }
Event::WindowEvent { event, .. } => {
match event {
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ modifiers = new_modifiers;
+ }
WindowEvent::Resized(new_size) => {
logical_size =
new_size.to_logical(window.scale_factor());
@@ -81,6 +79,7 @@ pub fn main() {
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
+
_ => {}
}
@@ -163,6 +162,7 @@ pub fn main() {
swap_chain = SwapChain::new(
&device,
&surface,
+ format,
size.width,
size.height,
);
diff --git a/examples/pane_grid/Cargo.toml b/examples/pane_grid/Cargo.toml
new file mode 100644
index 00000000..3ed912ac
--- /dev/null
+++ b/examples/pane_grid/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "pane_grid"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../.." }
diff --git a/examples/pane_grid/README.md b/examples/pane_grid/README.md
new file mode 100644
index 00000000..3653fc5b
--- /dev/null
+++ b/examples/pane_grid/README.md
@@ -0,0 +1,28 @@
+## Pane grid
+
+A grid of panes that can be split, resized, and reorganized.
+
+This example showcases the `PaneGrid` widget, which features:
+
+* Vertical and horizontal splits
+* Tracking of the last active pane
+* Mouse-based resizing
+* Drag and drop to reorganize panes
+* Hotkey support
+* Configurable modifier keys
+* API to perform actions programmatically (`split`, `swap`, `resize`, etc.)
+
+The __[`main`]__ file contains all the code of the example.
+
+<div align="center">
+ <a href="https://gfycat.com/mixedflatjellyfish">
+ <img src="https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package pane_grid
+```
+
+[`main`]: src/main.rs
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
new file mode 100644
index 00000000..dafc396c
--- /dev/null
+++ b/examples/pane_grid/src/main.rs
@@ -0,0 +1,306 @@
+use iced::{
+ button, keyboard, pane_grid, scrollable, Align, Button, Column, Container,
+ Element, HorizontalAlignment, Length, PaneGrid, Sandbox, Scrollable,
+ Settings, Text,
+};
+
+pub fn main() {
+ Example::run(Settings::default())
+}
+
+struct Example {
+ panes: pane_grid::State<Content>,
+ panes_created: usize,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ Split(pane_grid::Axis, pane_grid::Pane),
+ SplitFocused(pane_grid::Axis),
+ FocusAdjacent(pane_grid::Direction),
+ Dragged(pane_grid::DragEvent),
+ Resized(pane_grid::ResizeEvent),
+ Close(pane_grid::Pane),
+ CloseFocused,
+}
+
+impl Sandbox for Example {
+ type Message = Message;
+
+ fn new() -> Self {
+ let (panes, _) = pane_grid::State::new(Content::new(0));
+
+ Example {
+ panes,
+ panes_created: 1,
+ }
+ }
+
+ fn title(&self) -> String {
+ String::from("Pane grid - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::Split(axis, pane) => {
+ let _ = self.panes.split(
+ axis,
+ &pane,
+ Content::new(self.panes_created),
+ );
+
+ self.panes_created += 1;
+ }
+ Message::SplitFocused(axis) => {
+ if let Some(pane) = self.panes.active() {
+ let _ = self.panes.split(
+ axis,
+ &pane,
+ Content::new(self.panes_created),
+ );
+
+ self.panes_created += 1;
+ }
+ }
+ Message::FocusAdjacent(direction) => {
+ if let Some(pane) = self.panes.active() {
+ if let Some(adjacent) =
+ self.panes.adjacent(&pane, direction)
+ {
+ self.panes.focus(&adjacent);
+ }
+ }
+ }
+ Message::Resized(pane_grid::ResizeEvent { split, ratio }) => {
+ self.panes.resize(&split, ratio);
+ }
+ Message::Dragged(pane_grid::DragEvent::Dropped {
+ pane,
+ target,
+ }) => {
+ self.panes.swap(&pane, &target);
+ }
+ Message::Dragged(_) => {}
+ Message::Close(pane) => {
+ let _ = self.panes.close(&pane);
+ }
+ Message::CloseFocused => {
+ if let Some(pane) = self.panes.active() {
+ let _ = self.panes.close(&pane);
+ }
+ }
+ }
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ let total_panes = self.panes.len();
+
+ let pane_grid =
+ PaneGrid::new(&mut self.panes, |pane, content, focus| {
+ content.view(pane, focus, total_panes)
+ })
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .spacing(10)
+ .on_drag(Message::Dragged)
+ .on_resize(Message::Resized)
+ .on_key_press(handle_hotkey);
+
+ Column::new()
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(10)
+ .push(pane_grid)
+ .into()
+ }
+}
+
+fn handle_hotkey(event: pane_grid::KeyPressEvent) -> Option<Message> {
+ use keyboard::KeyCode;
+ use pane_grid::{Axis, Direction};
+
+ let direction = match event.key_code {
+ KeyCode::Up => Some(Direction::Up),
+ KeyCode::Down => Some(Direction::Down),
+ KeyCode::Left => Some(Direction::Left),
+ KeyCode::Right => Some(Direction::Right),
+ _ => None,
+ };
+
+ match event.key_code {
+ KeyCode::V => Some(Message::SplitFocused(Axis::Vertical)),
+ KeyCode::H => Some(Message::SplitFocused(Axis::Horizontal)),
+ KeyCode::W => Some(Message::CloseFocused),
+ _ => direction.map(Message::FocusAdjacent),
+ }
+}
+
+struct Content {
+ id: usize,
+ scroll: scrollable::State,
+ split_horizontally: button::State,
+ split_vertically: button::State,
+ close: button::State,
+}
+
+impl Content {
+ fn new(id: usize) -> Self {
+ Content {
+ id,
+ scroll: scrollable::State::new(),
+ split_horizontally: button::State::new(),
+ split_vertically: button::State::new(),
+ close: button::State::new(),
+ }
+ }
+ fn view(
+ &mut self,
+ pane: pane_grid::Pane,
+ focus: Option<pane_grid::Focus>,
+ total_panes: usize,
+ ) -> Element<Message> {
+ let Content {
+ id,
+ scroll,
+ split_horizontally,
+ split_vertically,
+ close,
+ } = self;
+
+ let button = |state, label, message, style| {
+ Button::new(
+ state,
+ Text::new(label)
+ .width(Length::Fill)
+ .horizontal_alignment(HorizontalAlignment::Center)
+ .size(16),
+ )
+ .width(Length::Fill)
+ .padding(8)
+ .on_press(message)
+ .style(style)
+ };
+
+ let mut controls = Column::new()
+ .spacing(5)
+ .max_width(150)
+ .push(button(
+ split_horizontally,
+ "Split horizontally",
+ Message::Split(pane_grid::Axis::Horizontal, pane),
+ style::Button::Primary,
+ ))
+ .push(button(
+ split_vertically,
+ "Split vertically",
+ Message::Split(pane_grid::Axis::Vertical, pane),
+ style::Button::Primary,
+ ));
+
+ if total_panes > 1 {
+ controls = controls.push(button(
+ close,
+ "Close",
+ Message::Close(pane),
+ style::Button::Destructive,
+ ));
+ }
+
+ let content = Scrollable::new(scroll)
+ .width(Length::Fill)
+ .spacing(10)
+ .align_items(Align::Center)
+ .push(Text::new(format!("Pane {}", id)).size(30))
+ .push(controls);
+
+ Container::new(Column::new().padding(5).push(content))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_y()
+ .style(style::Pane {
+ is_focused: focus.is_some(),
+ })
+ .into()
+ }
+}
+
+mod style {
+ use iced::{button, container, Background, Color, Vector};
+
+ const SURFACE: Color = Color::from_rgb(
+ 0xF2 as f32 / 255.0,
+ 0xF3 as f32 / 255.0,
+ 0xF5 as f32 / 255.0,
+ );
+
+ const ACTIVE: Color = Color::from_rgb(
+ 0x72 as f32 / 255.0,
+ 0x89 as f32 / 255.0,
+ 0xDA as f32 / 255.0,
+ );
+
+ const HOVERED: Color = Color::from_rgb(
+ 0x67 as f32 / 255.0,
+ 0x7B as f32 / 255.0,
+ 0xC4 as f32 / 255.0,
+ );
+
+ pub struct Pane {
+ pub is_focused: bool,
+ }
+
+ impl container::StyleSheet for Pane {
+ fn style(&self) -> container::Style {
+ container::Style {
+ background: Some(Background::Color(SURFACE)),
+ border_width: 2,
+ border_color: Color {
+ a: if self.is_focused { 1.0 } else { 0.3 },
+ ..Color::BLACK
+ },
+ ..Default::default()
+ }
+ }
+ }
+
+ pub enum Button {
+ Primary,
+ Destructive,
+ }
+
+ impl button::StyleSheet for Button {
+ fn active(&self) -> button::Style {
+ let (background, text_color) = match self {
+ Button::Primary => (Some(ACTIVE), Color::WHITE),
+ Button::Destructive => {
+ (None, Color::from_rgb8(0xFF, 0x47, 0x47))
+ }
+ };
+
+ button::Style {
+ text_color,
+ background: background.map(Background::Color),
+ border_radius: 5,
+ shadow_offset: Vector::new(0.0, 0.0),
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ let active = self.active();
+
+ let background = match self {
+ Button::Primary => Some(HOVERED),
+ Button::Destructive => Some(Color {
+ a: 0.2,
+ ..active.text_color
+ }),
+ };
+
+ button::Style {
+ background: background.map(Background::Color),
+ ..active
+ }
+ }
+ }
+}
diff --git a/examples/pokedex/Cargo.toml b/examples/pokedex/Cargo.toml
index c1e3edb5..94320086 100644
--- a/examples/pokedex/Cargo.toml
+++ b/examples/pokedex/Cargo.toml
@@ -7,12 +7,16 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["image", "debug", "tokio"] }
-serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-rand = { version = "0.7", features = ["wasm-bindgen"] }
+
+[dependencies.serde]
+version = "1.0"
+features = ["derive"]
[dependencies.reqwest]
-version = "0.10"
-git = "https://github.com/hecrj/reqwest.git"
-branch = "feature/wasm-deserialize-json"
+version = "0.10.2"
features = ["json"]
+
+[dependencies.rand]
+version = "0.7"
+features = ["wasm-bindgen"]
diff --git a/examples/styling/src/main.rs b/examples/styling/src/main.rs
index e97389e9..dfa168b6 100644
--- a/examples/styling/src/main.rs
+++ b/examples/styling/src/main.rs
@@ -93,6 +93,7 @@ impl Sandbox for Styling {
ProgressBar::new(0.0..=100.0, self.slider_value).style(self.theme);
let scrollable = Scrollable::new(&mut self.scroll)
+ .width(Length::Fill)
.height(Length::Units(100))
.style(self.theme)
.push(Text::new("Scroll me!"))
diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml
index d8f83ac2..161ee6a8 100644
--- a/examples/svg/Cargo.toml
+++ b/examples/svg/Cargo.toml
@@ -7,3 +7,4 @@ publish = false
[dependencies]
iced = { path = "../..", features = ["svg"] }
+env_logger = "0.7"
diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs
index 1fb80534..811fdfb5 100644
--- a/examples/svg/src/main.rs
+++ b/examples/svg/src/main.rs
@@ -1,6 +1,8 @@
use iced::{Column, Container, Element, Length, Sandbox, Settings, Svg};
pub fn main() {
+ env_logger::init();
+
Tiger::run(Settings::default())
}
@@ -22,9 +24,12 @@ impl Sandbox for Tiger {
fn view(&mut self) -> Element<()> {
let content = Column::new().padding(20).push(
- Svg::new(format!("{}/resources/tiger.svg", env!("CARGO_MANIFEST_DIR")))
- .width(Length::Fill)
- .height(Length::Fill),
+ Svg::new(format!(
+ "{}/resources/tiger.svg",
+ env!("CARGO_MANIFEST_DIR")
+ ))
+ .width(Length::Fill)
+ .height(Length::Fill),
);
Container::new(content)
diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs
index b68444cd..8eccb7be 100644
--- a/futures/src/subscription.rs
+++ b/futures/src/subscription.rs
@@ -72,6 +72,34 @@ where
self.recipes
}
+ /// Adds a value to the [`Subscription`] context.
+ ///
+ /// The value will be part of the identity of a [`Subscription`].
+ ///
+ /// This is necessary if you want to use multiple instances of the same
+ /// [`Subscription`] to produce different kinds of messages based on some
+ /// external data.
+ ///
+ /// [`Subscription`]: struct.Subscription.html
+ pub fn with<T>(mut self, value: T) -> Subscription<H, E, (T, O)>
+ where
+ H: 'static,
+ E: 'static,
+ O: 'static,
+ T: std::hash::Hash + Clone + Send + Sync + 'static,
+ {
+ Subscription {
+ recipes: self
+ .recipes
+ .drain(..)
+ .map(|recipe| {
+ Box::new(With::new(recipe, value.clone()))
+ as Box<dyn Recipe<H, E, Output = (T, O)>>
+ })
+ .collect(),
+ }
+ }
+
/// Transforms the [`Subscription`] output with the given function.
///
/// [`Subscription`]: struct.Subscription.html
@@ -187,3 +215,45 @@ where
.boxed()
}
}
+
+struct With<Hasher, Event, A, B> {
+ recipe: Box<dyn Recipe<Hasher, Event, Output = A>>,
+ value: B,
+}
+
+impl<H, E, A, B> With<H, E, A, B> {
+ fn new(recipe: Box<dyn Recipe<H, E, Output = A>>, value: B) -> Self {
+ With { recipe, value }
+ }
+}
+
+impl<H, E, A, B> Recipe<H, E> for With<H, E, A, B>
+where
+ A: 'static,
+ B: 'static + std::hash::Hash + Clone + Send + Sync,
+ H: std::hash::Hasher,
+{
+ type Output = (B, A);
+
+ fn hash(&self, state: &mut H) {
+ use std::hash::Hash;
+
+ std::any::TypeId::of::<B>().hash(state);
+ self.value.hash(state);
+ self.recipe.hash(state);
+ }
+
+ fn stream(
+ self: Box<Self>,
+ input: BoxStream<'static, E>,
+ ) -> futures::stream::BoxStream<'static, Self::Output> {
+ use futures::StreamExt;
+
+ let value = self.value;
+
+ self.recipe
+ .stream(input)
+ .map(move |element| (value.clone(), element))
+ .boxed()
+ }
+}
diff --git a/native/src/element.rs b/native/src/element.rs
index 276f7614..4e7c7fc6 100644
--- a/native/src/element.rs
+++ b/native/src/element.rs
@@ -243,7 +243,7 @@ where
}
/// Computes the _layout_ hash of the [`Element`].
- ///
+ ///
/// [`Element`]: struct.Element.html
pub fn hash_layout(&self, state: &mut Hasher) {
self.widget.hash_layout(state);
diff --git a/native/src/input/keyboard.rs b/native/src/input/keyboard.rs
index 432e75ba..928bf492 100644
--- a/native/src/input/keyboard.rs
+++ b/native/src/input/keyboard.rs
@@ -1,8 +1,5 @@
//! Build keyboard events.
mod event;
-mod key_code;
-mod modifiers_state;
pub use event::Event;
-pub use key_code::KeyCode;
-pub use modifiers_state::ModifiersState;
+pub use iced_core::keyboard::{KeyCode, ModifiersState};
diff --git a/native/src/input/keyboard/modifiers_state.rs b/native/src/input/keyboard/modifiers_state.rs
deleted file mode 100644
index 4e3794b3..00000000
--- a/native/src/input/keyboard/modifiers_state.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-/// The current state of the keyboard modifiers.
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub struct ModifiersState {
- /// Whether a shift key is pressed
- pub shift: bool,
-
- /// Whether a control key is pressed
- pub control: bool,
-
- /// Whether an alt key is pressed
- pub alt: bool,
-
- /// Whether a logo key is pressed (e.g. windows key, command key...)
- pub logo: bool,
-}
diff --git a/native/src/layout/limits.rs b/native/src/layout/limits.rs
index b674b58a..664c881a 100644
--- a/native/src/layout/limits.rs
+++ b/native/src/layout/limits.rs
@@ -20,7 +20,7 @@ impl Limits {
///
/// [`Limits`]: struct.Limits.html
/// [`Size`]: ../struct.Size.html
- pub fn new(min: Size, max: Size) -> Limits {
+ pub const fn new(min: Size, max: Size) -> Limits {
Limits {
min,
max,
diff --git a/native/src/layout/node.rs b/native/src/layout/node.rs
index 11e93b72..a265c46a 100644
--- a/native/src/layout/node.rs
+++ b/native/src/layout/node.rs
@@ -12,7 +12,7 @@ impl Node {
///
/// [`Node`]: struct.Node.html
/// [`Size`]: ../struct.Size.html
- pub fn new(size: Size) -> Self {
+ pub const fn new(size: Size) -> Self {
Self::with_children(size, Vec::new())
}
@@ -20,7 +20,7 @@ impl Node {
///
/// [`Node`]: struct.Node.html
/// [`Size`]: ../struct.Size.html
- pub fn with_children(size: Size, children: Vec<Node>) -> Self {
+ pub const fn with_children(size: Size, children: Vec<Node>) -> Self {
Node {
bounds: Rectangle {
x: 0.0,
diff --git a/native/src/lib.rs b/native/src/lib.rs
index e4e7baee..d17dd918 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -21,8 +21,8 @@
//! # Usage
//! The strategy to use this crate depends on your particular use case. If you
//! want to:
-//! - Implement a custom shell or integrate it in your own system, you should
-//! check out the [`UserInterface`] type.
+//! - Implement a custom shell or integrate it in your own system, check out the
+//! [`UserInterface`] type.
//! - Build a new renderer, see the [renderer] module.
//! - Build a custom widget, start at the [`Widget`] trait.
//!
diff --git a/native/src/mouse_cursor.rs b/native/src/mouse_cursor.rs
index c7297e0e..0dad3edc 100644
--- a/native/src/mouse_cursor.rs
+++ b/native/src/mouse_cursor.rs
@@ -21,6 +21,12 @@ pub enum MouseCursor {
/// The cursor is over a text widget.
Text,
+
+ /// The cursor is resizing a widget horizontally.
+ ResizingHorizontally,
+
+ /// The cursor is resizing a widget vertically.
+ ResizingVertically,
}
impl Default for MouseCursor {
diff --git a/native/src/widget.rs b/native/src/widget.rs
index f9424b02..88f819c9 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -25,6 +25,7 @@ pub mod checkbox;
pub mod column;
pub mod container;
pub mod image;
+pub mod pane_grid;
pub mod progress_bar;
pub mod radio;
pub mod row;
@@ -46,6 +47,8 @@ pub use container::Container;
#[doc(no_inline)]
pub use image::Image;
#[doc(no_inline)]
+pub use pane_grid::PaneGrid;
+#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs
index 104790d4..b1adc6e3 100644
--- a/native/src/widget/column.rs
+++ b/native/src/widget/column.rs
@@ -30,6 +30,15 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
///
/// [`Column`]: struct.Column.html
pub fn new() -> Self {
+ Self::with_children(Vec::new())
+ }
+
+ /// Creates a [`Column`] with the given elements.
+ ///
+ /// [`Column`]: struct.Column.html
+ pub fn with_children(
+ children: Vec<Element<'a, Message, Renderer>>,
+ ) -> Self {
Column {
spacing: 0,
padding: 0,
@@ -38,7 +47,7 @@ impl<'a, Message, Renderer> Column<'a, Message, Renderer> {
max_width: u32::MAX,
max_height: u32::MAX,
align_items: Align::Start,
- children: Vec::new(),
+ children,
}
}
@@ -210,7 +219,7 @@ pub trait Renderer: crate::Renderer + Sized {
/// - the [`Layout`] of the [`Column`] and its children
/// - the cursor position
///
- /// [`Column`]: struct.Row.html
+ /// [`Column`]: struct.Column.html
/// [`Layout`]: ../layout/struct.Layout.html
fn draw<Message>(
&mut self,
diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs
index 3459a832..d1cbb32e 100644
--- a/native/src/widget/container.rs
+++ b/native/src/widget/container.rs
@@ -77,7 +77,7 @@ where
self.max_height = max_height;
self
}
-
+
/// Sets the content alignment for the horizontal axis of the [`Container`].
///
/// [`Container`]: struct.Container.html
diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs
index 200401f9..5b067687 100644
--- a/native/src/widget/image.rs
+++ b/native/src/widget/image.rs
@@ -18,7 +18,7 @@ use std::{
/// ```
///
/// <img src="https://github.com/hecrj/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
-#[derive(Debug)]
+#[derive(Debug, Hash)]
pub struct Image {
handle: Handle,
width: Length,
@@ -125,6 +125,21 @@ impl 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<u8>` of BGRA
+ /// pixels.
+ ///
+ /// This is useful if you have already decoded your image.
+ ///
+ /// [`Handle`]: struct.Handle.html
+ pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Handle {
+ Self::from_data(Data::Pixels {
+ width,
+ height,
+ pixels,
+ })
+ }
+
/// Creates an image [`Handle`] containing the image data directly.
///
/// This is useful if you already have your image loaded in-memory, maybe
@@ -188,6 +203,16 @@ pub enum Data {
/// In-memory data
Bytes(Vec<u8>),
+
+ /// Decoded image pixels in BGRA format.
+ Pixels {
+ /// The width of the image.
+ width: u32,
+ /// The height of the image.
+ height: u32,
+ /// The pixels.
+ pixels: Vec<u8>,
+ },
}
impl std::fmt::Debug for Data {
@@ -195,6 +220,9 @@ impl std::fmt::Debug for Data {
match self {
Data::Path(path) => write!(f, "Path({:?})", path),
Data::Bytes(_) => write!(f, "Bytes(...)"),
+ Data::Pixels { width, height, .. } => {
+ write!(f, "Pixels({} * {})", width, height)
+ }
}
}
}
diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs
new file mode 100644
index 00000000..a88f591a
--- /dev/null
+++ b/native/src/widget/pane_grid.rs
@@ -0,0 +1,647 @@
+//! 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)
+mod axis;
+mod direction;
+mod node;
+mod pane;
+mod split;
+mod state;
+
+pub use axis::Axis;
+pub use direction::Direction;
+pub use pane::Pane;
+pub use split::Split;
+pub use state::{Focus, State};
+
+use crate::{
+ input::{keyboard, mouse, ButtonState},
+ layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Size,
+ 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/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+///
+/// 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::{pane_grid, Text};
+/// #
+/// # type PaneGrid<'a, Message> =
+/// # iced_native::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(&mut state, |pane, state, focus| {
+/// match state {
+/// PaneState::SomePane => Text::new("This is some pane"),
+/// PaneState::AnotherKindOfPane => Text::new("This is another kind of pane"),
+/// }.into()
+/// })
+/// .on_drag(Message::PaneDragged)
+/// .on_resize(Message::PaneResized);
+/// ```
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+/// [`State`]: struct.State.html
+#[allow(missing_debug_implementations)]
+pub struct PaneGrid<'a, Message, Renderer> {
+ state: &'a mut state::Internal,
+ pressed_modifiers: &'a mut keyboard::ModifiersState,
+ elements: Vec<(Pane, Element<'a, Message, Renderer>)>,
+ width: Length,
+ height: Length,
+ spacing: u16,
+ modifier_keys: keyboard::ModifiersState,
+ on_drag: Option<Box<dyn Fn(DragEvent) -> Message>>,
+ on_resize: Option<Box<dyn Fn(ResizeEvent) -> Message>>,
+ on_key_press: Option<Box<dyn Fn(KeyPressEvent) -> Option<Message>>>,
+}
+
+impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> {
+ /// Creates a [`PaneGrid`] with the given [`State`] and view function.
+ ///
+ /// The view function will be called to display each [`Pane`] present in the
+ /// [`State`].
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`State`]: struct.State.html
+ /// [`Pane`]: struct.Pane.html
+ pub fn new<T>(
+ state: &'a mut State<T>,
+ view: impl Fn(
+ Pane,
+ &'a mut T,
+ Option<Focus>,
+ ) -> Element<'a, Message, Renderer>,
+ ) -> Self {
+ let elements = {
+ let action = state.internal.action();
+ let current_focus = action.focus();
+
+ state
+ .panes
+ .iter_mut()
+ .map(move |(pane, pane_state)| {
+ let focus = match current_focus {
+ Some((focused_pane, focus))
+ if *pane == focused_pane =>
+ {
+ Some(focus)
+ }
+ _ => None,
+ };
+
+ (*pane, view(*pane, pane_state, focus))
+ })
+ .collect()
+ };
+
+ Self {
+ state: &mut state.internal,
+ pressed_modifiers: &mut state.modifiers,
+ elements,
+ width: Length::Fill,
+ height: Length::Fill,
+ spacing: 0,
+ modifier_keys: keyboard::ModifiersState {
+ control: true,
+ ..Default::default()
+ },
+ on_drag: None,
+ on_resize: None,
+ on_key_press: None,
+ }
+ }
+
+ /// Sets the width of the [`PaneGrid`].
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ pub fn width(mut self, width: Length) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height of the [`PaneGrid`].
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ pub fn height(mut self, height: Length) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Sets the spacing _between_ the panes of the [`PaneGrid`].
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ pub fn spacing(mut self, units: u16) -> Self {
+ self.spacing = units;
+ self
+ }
+
+ /// Sets the modifier keys of the [`PaneGrid`].
+ ///
+ /// The modifier keys will need to be pressed to trigger dragging, resizing,
+ /// and key events.
+ ///
+ /// The default modifier key is `Ctrl`.
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ pub fn modifier_keys(
+ mut self,
+ modifier_keys: keyboard::ModifiersState,
+ ) -> Self {
+ self.modifier_keys = modifier_keys;
+ self
+ }
+
+ /// Enables the drag and drop interactions of the [`PaneGrid`], which will
+ /// use the provided function to produce messages.
+ ///
+ /// Panes can be dragged using `Modifier keys + Left click`.
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ pub fn on_drag(
+ mut self,
+ f: impl Fn(DragEvent) -> Message + 'static,
+ ) -> Self {
+ self.on_drag = Some(Box::new(f));
+ self
+ }
+
+ /// Enables the resize interactions of the [`PaneGrid`], which will
+ /// use the provided function to produce messages.
+ ///
+ /// Panes can be resized using `Modifier keys + Right click`.
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ pub fn on_resize(
+ mut self,
+ f: impl Fn(ResizeEvent) -> Message + 'static,
+ ) -> Self {
+ self.on_resize = Some(Box::new(f));
+ self
+ }
+
+ /// Captures hotkey interactions with the [`PaneGrid`], using the provided
+ /// function to produce messages.
+ ///
+ /// The function will be called when:
+ /// - a [`Pane`] is focused
+ /// - a key is pressed
+ /// - all the modifier keys are pressed
+ ///
+ /// If the function returns `None`, the key press event will be discarded
+ /// without producing any message.
+ ///
+ /// This method is particularly useful to implement hotkey interactions.
+ /// For instance, you can use it to enable splitting, swapping, or resizing
+ /// panes by pressing combinations of keys.
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`Pane`]: struct.Pane.html
+ pub fn on_key_press(
+ mut self,
+ f: impl Fn(KeyPressEvent) -> Option<Message> + 'static,
+ ) -> Self {
+ self.on_key_press = Some(Box::new(f));
+ self
+ }
+
+ fn trigger_resize(
+ &mut self,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ ) {
+ if let Some(on_resize) = &self.on_resize {
+ if let Some((split, _)) = self.state.picked_split() {
+ let bounds = layout.bounds();
+
+ let splits = self.state.splits(
+ f32::from(self.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).max(0.1).min(0.9)
+ }
+ Axis::Vertical => {
+ let position =
+ cursor_position.x - bounds.x - rectangle.x;
+
+ (position / rectangle.width).max(0.1).min(0.9)
+ }
+ };
+
+ messages.push(on_resize(ResizeEvent { split, ratio }));
+ }
+ }
+ }
+ }
+}
+
+/// An event produced during a drag and drop interaction of a [`PaneGrid`].
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy)]
+pub enum DragEvent {
+ /// A [`Pane`] was picked for dragging.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Picked {
+ /// The picked [`Pane`].
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pane: Pane,
+ },
+
+ /// A [`Pane`] was dropped on top of another [`Pane`].
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Dropped {
+ /// The picked [`Pane`].
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pane: Pane,
+
+ /// The [`Pane`] where the picked one was dropped on.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ target: Pane,
+ },
+
+ /// A [`Pane`] was picked and then dropped outside of other [`Pane`]
+ /// boundaries.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Canceled {
+ /// The picked [`Pane`].
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pane: Pane,
+ },
+}
+
+/// An event produced during a resize interaction of a [`PaneGrid`].
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy)]
+pub struct ResizeEvent {
+ /// The [`Split`] that is being dragged for resizing.
+ ///
+ /// [`Split`]: struct.Split.html
+ 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.
+ ///
+ /// [`Split`]: struct.Split.html
+ pub ratio: f32,
+}
+
+/// An event produced during a key press interaction of a [`PaneGrid`].
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy)]
+pub struct KeyPressEvent {
+ /// The key that was pressed.
+ pub key_code: keyboard::KeyCode,
+
+ /// The state of the modifier keys when the key was pressed.
+ pub modifiers: keyboard::ModifiersState,
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer>
+ for PaneGrid<'a, Message, Renderer>
+where
+ Renderer: self::Renderer + 'static,
+ Message: 'static,
+{
+ 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);
+
+ let regions = self.state.regions(f32::from(self.spacing), size);
+
+ let children = self
+ .elements
+ .iter()
+ .filter_map(|(pane, element)| {
+ let region = regions.get(pane)?;
+ let size = Size::new(region.width, region.height);
+
+ let mut node =
+ element.layout(renderer, &layout::Limits::new(size, size));
+
+ node.move_to(Point::new(region.x, region.y));
+
+ Some(node)
+ })
+ .collect();
+
+ layout::Node::with_children(size, children)
+ }
+
+ fn on_event(
+ &mut self,
+ event: Event,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ messages: &mut Vec<Message>,
+ renderer: &Renderer,
+ clipboard: Option<&dyn Clipboard>,
+ ) {
+ match event {
+ Event::Mouse(mouse::Event::Input {
+ button: mouse::Button::Left,
+ state,
+ }) => match state {
+ ButtonState::Pressed => {
+ let mut clicked_region =
+ self.elements.iter().zip(layout.children()).filter(
+ |(_, layout)| {
+ layout.bounds().contains(cursor_position)
+ },
+ );
+
+ if let Some(((pane, _), _)) = clicked_region.next() {
+ match &self.on_drag {
+ Some(on_drag)
+ if self
+ .pressed_modifiers
+ .matches(self.modifier_keys) =>
+ {
+ self.state.pick_pane(pane);
+
+ messages.push(on_drag(DragEvent::Picked {
+ pane: *pane,
+ }));
+ }
+ _ => {
+ self.state.focus(pane);
+ }
+ }
+ } else {
+ self.state.unfocus();
+ }
+ }
+ ButtonState::Released => {
+ if let Some(pane) = self.state.picked_pane() {
+ self.state.focus(&pane);
+
+ if let Some(on_drag) = &self.on_drag {
+ let mut dropped_region = self
+ .elements
+ .iter()
+ .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: *target,
+ }
+ }
+ _ => DragEvent::Canceled { pane },
+ };
+
+ messages.push(on_drag(event));
+ }
+ }
+ }
+ },
+ Event::Mouse(mouse::Event::Input {
+ button: mouse::Button::Right,
+ state: ButtonState::Pressed,
+ }) if self.on_resize.is_some()
+ && self.state.picked_pane().is_none()
+ && self.pressed_modifiers.matches(self.modifier_keys) =>
+ {
+ let bounds = layout.bounds();
+
+ if bounds.contains(cursor_position) {
+ let relative_cursor = Point::new(
+ cursor_position.x - bounds.x,
+ cursor_position.y - bounds.y,
+ );
+
+ let splits = self.state.splits(
+ f32::from(self.spacing),
+ Size::new(bounds.width, bounds.height),
+ );
+
+ let mut sorted_splits: Vec<_> = splits
+ .iter()
+ .filter(|(_, (axis, rectangle, _))| match axis {
+ Axis::Horizontal => {
+ relative_cursor.x > rectangle.x
+ && relative_cursor.x
+ < rectangle.x + rectangle.width
+ }
+ Axis::Vertical => {
+ relative_cursor.y > rectangle.y
+ && relative_cursor.y
+ < rectangle.y + rectangle.height
+ }
+ })
+ .collect();
+
+ sorted_splits.sort_by_key(
+ |(_, (axis, rectangle, ratio))| {
+ let distance = match axis {
+ Axis::Horizontal => (relative_cursor.y
+ - (rectangle.y + rectangle.height * ratio))
+ .abs(),
+ Axis::Vertical => (relative_cursor.x
+ - (rectangle.x + rectangle.width * ratio))
+ .abs(),
+ };
+
+ distance.round() as u32
+ },
+ );
+
+ if let Some((split, (axis, _, _))) = sorted_splits.first() {
+ self.state.pick_split(split, *axis);
+ self.trigger_resize(layout, cursor_position, messages);
+ }
+ }
+ }
+ Event::Mouse(mouse::Event::Input {
+ button: mouse::Button::Right,
+ state: ButtonState::Released,
+ }) if self.state.picked_split().is_some() => {
+ self.state.drop_split();
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. }) => {
+ self.trigger_resize(layout, cursor_position, messages);
+ }
+ Event::Keyboard(keyboard::Event::Input {
+ modifiers,
+ key_code,
+ state,
+ }) => {
+ if let Some(on_key_press) = &self.on_key_press {
+ // TODO: Discard when event is captured
+ if state == ButtonState::Pressed {
+ if let Some(_) = self.state.active_pane() {
+ if modifiers.matches(self.modifier_keys) {
+ if let Some(message) =
+ on_key_press(KeyPressEvent {
+ key_code,
+ modifiers,
+ })
+ {
+ messages.push(message);
+ }
+ }
+ }
+ }
+ }
+
+ *self.pressed_modifiers = modifiers;
+ }
+ _ => {}
+ }
+
+ if self.state.picked_pane().is_none() {
+ {
+ self.elements.iter_mut().zip(layout.children()).for_each(
+ |((_, pane), layout)| {
+ pane.widget.on_event(
+ event.clone(),
+ layout,
+ cursor_position,
+ messages,
+ renderer,
+ clipboard,
+ )
+ },
+ );
+ }
+ }
+ }
+
+ fn draw(
+ &self,
+ renderer: &mut Renderer,
+ defaults: &Renderer::Defaults,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Renderer::Output {
+ renderer.draw(
+ defaults,
+ &self.elements,
+ self.state.picked_pane(),
+ self.state.picked_split().map(|(_, axis)| axis),
+ layout,
+ cursor_position,
+ )
+ }
+
+ fn hash_layout(&self, state: &mut Hasher) {
+ use std::hash::Hash;
+
+ std::any::TypeId::of::<PaneGrid<'_, Message, Renderer>>().hash(state);
+ self.width.hash(state);
+ self.height.hash(state);
+ self.state.hash_layout(state);
+
+ for (_, element) in &self.elements {
+ element.hash_layout(state);
+ }
+ }
+}
+
+/// The renderer of a [`PaneGrid`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`PaneGrid`] in your user interface.
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+/// [renderer]: ../../renderer/index.html
+pub trait Renderer: crate::Renderer + Sized {
+ /// Draws a [`PaneGrid`].
+ ///
+ /// It receives:
+ /// - the elements of the [`PaneGrid`]
+ /// - the [`Pane`] that is currently being dragged
+ /// - the [`Axis`] that is currently being resized
+ /// - the [`Layout`] of the [`PaneGrid`] and its elements
+ /// - the cursor position
+ ///
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`Pane`]: struct.Pane.html
+ /// [`Layout`]: ../layout/struct.Layout.html
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[(Pane, Element<'_, Message, Self>)],
+ dragging: Option<Pane>,
+ resizing: Option<Axis>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<PaneGrid<'a, Message, Renderer>>
+ for Element<'a, Message, Renderer>
+where
+ Renderer: self::Renderer + 'static,
+ Message: 'static,
+{
+ fn from(
+ pane_grid: PaneGrid<'a, Message, Renderer>,
+ ) -> Element<'a, Message, Renderer> {
+ Element::new(pane_grid)
+ }
+}
diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs
new file mode 100644
index 00000000..f0e3f362
--- /dev/null
+++ b/native/src/widget/pane_grid/axis.rs
@@ -0,0 +1,54 @@
+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 {
+ pub(super) fn split(
+ &self,
+ rectangle: &Rectangle,
+ ratio: f32,
+ halved_spacing: f32,
+ ) -> (Rectangle, Rectangle) {
+ match self {
+ Axis::Horizontal => {
+ let height_top = (rectangle.height * ratio).round();
+ let height_bottom = rectangle.height - height_top;
+
+ (
+ Rectangle {
+ height: height_top - halved_spacing,
+ ..*rectangle
+ },
+ Rectangle {
+ y: rectangle.y + height_top + halved_spacing,
+ height: height_bottom - halved_spacing,
+ ..*rectangle
+ },
+ )
+ }
+ Axis::Vertical => {
+ let width_left = (rectangle.width * ratio).round();
+ let width_right = rectangle.width - width_left;
+
+ (
+ Rectangle {
+ width: width_left - halved_spacing,
+ ..*rectangle
+ },
+ Rectangle {
+ x: rectangle.x + width_left + halved_spacing,
+ width: width_right - halved_spacing,
+ ..*rectangle
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/native/src/widget/pane_grid/direction.rs b/native/src/widget/pane_grid/direction.rs
new file mode 100644
index 00000000..b31a8737
--- /dev/null
+++ b/native/src/widget/pane_grid/direction.rs
@@ -0,0 +1,12 @@
+/// 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/node.rs b/native/src/widget/pane_grid/node.rs
new file mode 100644
index 00000000..4d5970b8
--- /dev/null
+++ b/native/src/widget/pane_grid/node.rs
@@ -0,0 +1,199 @@
+use crate::{
+ pane_grid::{Axis, Pane, Split},
+ Rectangle, Size,
+};
+
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Hash)]
+pub enum Node {
+ Split {
+ id: Split,
+ axis: Axis,
+ ratio: u32,
+ a: Box<Node>,
+ b: Box<Node>,
+ },
+ Pane(Pane),
+}
+
+impl Node {
+ pub 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 fn split(&mut self, id: Split, axis: Axis, new_pane: Pane) {
+ *self = Node::Split {
+ id,
+ axis,
+ ratio: 500_000,
+ a: Box::new(self.clone()),
+ b: Box::new(Node::Pane(new_pane)),
+ };
+ }
+
+ pub fn update(&mut self, f: &impl Fn(&mut Node)) {
+ match self {
+ Node::Split { a, b, .. } => {
+ a.update(f);
+ b.update(f);
+ }
+ _ => {}
+ }
+
+ f(self);
+ }
+
+ pub fn resize(&mut self, split: &Split, percentage: f32) -> bool {
+ match self {
+ Node::Split {
+ id, ratio, a, b, ..
+ } => {
+ if id == split {
+ *ratio = (percentage * 1_000_000.0).round() as u32;
+
+ true
+ } else if a.resize(split, percentage) {
+ true
+ } else {
+ b.resize(split, percentage)
+ }
+ }
+ Node::Pane(_) => false,
+ }
+ }
+
+ pub fn remove(&mut self, pane: &Pane) -> Option<Pane> {
+ 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,
+ }
+ }
+
+ pub fn regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ let mut regions = HashMap::new();
+
+ self.compute_regions(
+ spacing / 2.0,
+ &Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ &mut regions,
+ );
+
+ regions
+ }
+
+ pub fn splits(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ let mut splits = HashMap::new();
+
+ self.compute_splits(
+ spacing / 2.0,
+ &Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.width,
+ height: size.height,
+ },
+ &mut splits,
+ );
+
+ splits
+ }
+
+ pub fn pane(&self) -> Option<Pane> {
+ match self {
+ Node::Split { .. } => None,
+ Node::Pane(pane) => Some(*pane),
+ }
+ }
+
+ pub fn first_pane(&self) -> Pane {
+ match self {
+ Node::Split { a, .. } => a.first_pane(),
+ Node::Pane(pane) => *pane,
+ }
+ }
+
+ fn compute_regions(
+ &self,
+ halved_spacing: f32,
+ current: &Rectangle,
+ regions: &mut HashMap<Pane, Rectangle>,
+ ) {
+ match self {
+ Node::Split {
+ axis, ratio, a, b, ..
+ } => {
+ let ratio = *ratio as f32 / 1_000_000.0;
+ let (region_a, region_b) =
+ axis.split(current, ratio, halved_spacing);
+
+ a.compute_regions(halved_spacing, &region_a, regions);
+ b.compute_regions(halved_spacing, &region_b, regions);
+ }
+ Node::Pane(pane) => {
+ let _ = regions.insert(*pane, *current);
+ }
+ }
+ }
+
+ fn compute_splits(
+ &self,
+ halved_spacing: f32,
+ current: &Rectangle,
+ splits: &mut HashMap<Split, (Axis, Rectangle, f32)>,
+ ) {
+ match self {
+ Node::Split {
+ axis,
+ ratio,
+ a,
+ b,
+ id,
+ } => {
+ let ratio = *ratio as f32 / 1_000_000.0;
+ let (region_a, region_b) =
+ axis.split(current, ratio, halved_spacing);
+
+ let _ = splits.insert(*id, (*axis, *current, ratio));
+
+ a.compute_splits(halved_spacing, &region_a, splits);
+ b.compute_splits(halved_spacing, &region_b, splits);
+ }
+ Node::Pane(_) => {}
+ }
+ }
+}
diff --git a/native/src/widget/pane_grid/pane.rs b/native/src/widget/pane_grid/pane.rs
new file mode 100644
index 00000000..f9866407
--- /dev/null
+++ b/native/src/widget/pane_grid/pane.rs
@@ -0,0 +1,5 @@
+/// A rectangular region in a [`PaneGrid`] used to display widgets.
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq, 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
new file mode 100644
index 00000000..d020c510
--- /dev/null
+++ b/native/src/widget/pane_grid/split.rs
@@ -0,0 +1,5 @@
+/// A divider that splits a region in a [`PaneGrid`] into two different panes.
+///
+/// [`PaneGrid`]: struct.PaneGrid.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq, 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
new file mode 100644
index 00000000..0a8b8419
--- /dev/null
+++ b/native/src/widget/pane_grid/state.rs
@@ -0,0 +1,368 @@
+use crate::{
+ input::keyboard,
+ pane_grid::{node::Node, Axis, Direction, Pane, Split},
+ Hasher, Point, Rectangle, 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`]: struct.PaneGrid.html
+/// [`PaneGrid::new`]: struct.PaneGrid.html#method.new
+/// [`Pane`]: struct.Pane.html
+/// [`Split`]: struct.Split.html
+/// [`State`]: struct.State.html
+#[derive(Debug)]
+pub struct State<T> {
+ pub(super) panes: HashMap<Pane, T>,
+ pub(super) internal: Internal,
+ pub(super) modifiers: keyboard::ModifiersState,
+}
+
+/// The current focus of a [`Pane`].
+///
+/// [`Pane`]: struct.Pane.html
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Focus {
+ /// The [`Pane`] is just focused.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Idle,
+
+ /// The [`Pane`] is being dragged.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ Dragging,
+}
+
+impl<T> State<T> {
+ /// Creates a new [`State`], initializing the first pane with the provided
+ /// state.
+ ///
+ /// Alongside the [`State`], it returns the first [`Pane`] identifier.
+ ///
+ /// [`State`]: struct.State.html
+ /// [`Pane`]: struct.Pane.html
+ pub fn new(first_pane_state: T) -> (Self, Pane) {
+ let first_pane = Pane(0);
+
+ let mut panes = HashMap::new();
+ let _ = panes.insert(first_pane, first_pane_state);
+
+ (
+ State {
+ panes,
+ internal: Internal {
+ layout: Node::Pane(first_pane),
+ last_id: 0,
+ action: Action::Idle { focus: None },
+ },
+ modifiers: keyboard::ModifiersState::default(),
+ },
+ first_pane,
+ )
+ }
+
+ /// Returns the total amount of panes in the [`State`].
+ ///
+ /// [`State`]: struct.State.html
+ pub fn len(&self) -> usize {
+ self.panes.len()
+ }
+
+ /// Returns the internal state of the given [`Pane`], if it exists.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ 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.
+ ///
+ /// [`State`]: struct.State.html
+ pub fn iter(&self) -> impl Iterator<Item = (&Pane, &T)> {
+ self.panes.iter()
+ }
+
+ /// Returns a mutable iterator over all the panes of the [`State`],
+ /// alongside its internal state.
+ ///
+ /// [`State`]: struct.State.html
+ pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Pane, &mut T)> {
+ self.panes.iter_mut()
+ }
+
+ /// Returns the active [`Pane`] of the [`State`], if there is one.
+ ///
+ /// A [`Pane`] is active if it is focused and is __not__ being dragged.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ /// [`State`]: struct.State.html
+ pub fn active(&self) -> Option<Pane> {
+ self.internal.active_pane()
+ }
+
+ /// Returns the adjacent [`Pane`] of another [`Pane`] in the given
+ /// direction, if there is one.
+ ///
+ /// ## Example
+ /// You can combine this with [`State::active`] to find the pane that is
+ /// adjacent to the current active one, and then swap them. For instance:
+ ///
+ /// ```
+ /// # use iced_native::pane_grid;
+ /// #
+ /// # let (mut state, _) = pane_grid::State::new(());
+ /// #
+ /// if let Some(active) = state.active() {
+ /// if let Some(adjacent) = state.adjacent(&active, pane_grid::Direction::Right) {
+ /// state.swap(&active, &adjacent);
+ /// }
+ /// }
+ /// ```
+ ///
+ /// [`Pane`]: struct.Pane.html
+ /// [`State::active`]: struct.State.html#method.active
+ pub fn adjacent(&self, pane: &Pane, direction: Direction) -> Option<Pane> {
+ let regions =
+ self.internal.layout.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)
+ }
+
+ /// Focuses the given [`Pane`].
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pub fn focus(&mut self, pane: &Pane) {
+ self.internal.focus(pane);
+ }
+
+ /// Splits the given [`Pane`] into two in the given [`Axis`] and
+ /// initializing the new [`Pane`] with the provided internal state.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ /// [`Axis`]: enum.Axis.html
+ pub fn split(&mut self, axis: Axis, pane: &Pane, state: T) -> Option<Pane> {
+ 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);
+ self.focus(&new_pane);
+
+ Some(new_pane)
+ }
+
+ /// 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`].
+ ///
+ /// [`State`]: struct.State.html
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`DragEvent`]: struct.DragEvent.html
+ 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`].
+ ///
+ /// [`Split`]: struct.Split.html
+ /// [`PaneGrid`]: struct.PaneGrid.html
+ /// [`ResizeEvent`]: struct.ResizeEvent.html
+ 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, if it exists.
+ ///
+ /// [`Pane`]: struct.Pane.html
+ pub fn close(&mut self, pane: &Pane) -> Option<T> {
+ if let Some(sibling) = self.internal.layout.remove(pane) {
+ self.focus(&sibling);
+ self.panes.remove(pane)
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Internal {
+ layout: Node,
+ last_id: usize,
+ action: Action,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Action {
+ Idle {
+ focus: Option<Pane>,
+ },
+ Dragging {
+ pane: Pane,
+ },
+ Resizing {
+ split: Split,
+ axis: Axis,
+ focus: Option<Pane>,
+ },
+}
+
+impl Action {
+ pub fn focus(&self) -> Option<(Pane, Focus)> {
+ match self {
+ Action::Idle { focus } | Action::Resizing { focus, .. } => {
+ focus.map(|pane| (pane, Focus::Idle))
+ }
+ Action::Dragging { pane } => Some((*pane, Focus::Dragging)),
+ }
+ }
+}
+
+impl Internal {
+ pub fn action(&self) -> Action {
+ self.action
+ }
+
+ pub fn active_pane(&self) -> Option<Pane> {
+ match self.action {
+ Action::Idle { focus } => focus,
+ _ => None,
+ }
+ }
+
+ pub fn picked_pane(&self) -> Option<Pane> {
+ match self.action {
+ Action::Dragging { pane } => Some(pane),
+ _ => None,
+ }
+ }
+
+ pub fn picked_split(&self) -> Option<(Split, Axis)> {
+ match self.action {
+ Action::Resizing { split, axis, .. } => Some((split, axis)),
+ _ => None,
+ }
+ }
+
+ pub fn regions(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Pane, Rectangle> {
+ self.layout.regions(spacing, size)
+ }
+
+ pub fn splits(
+ &self,
+ spacing: f32,
+ size: Size,
+ ) -> HashMap<Split, (Axis, Rectangle, f32)> {
+ self.layout.splits(spacing, size)
+ }
+
+ pub fn focus(&mut self, pane: &Pane) {
+ self.action = Action::Idle { focus: Some(*pane) };
+ }
+
+ pub fn pick_pane(&mut self, pane: &Pane) {
+ self.action = Action::Dragging { pane: *pane };
+ }
+
+ pub fn pick_split(&mut self, split: &Split, axis: Axis) {
+ // TODO: Obtain `axis` from layout itself. Maybe we should implement
+ // `Node::find_split`
+ if self.picked_pane().is_some() {
+ return;
+ }
+
+ let focus = self.action.focus().map(|(pane, _)| pane);
+
+ self.action = Action::Resizing {
+ split: *split,
+ axis,
+ focus,
+ };
+ }
+
+ pub fn drop_split(&mut self) {
+ match self.action {
+ Action::Resizing { focus, .. } => {
+ self.action = Action::Idle { focus };
+ }
+ _ => {}
+ }
+ }
+
+ pub fn unfocus(&mut self) {
+ self.action = Action::Idle { focus: None };
+ }
+
+ pub fn hash_layout(&self, hasher: &mut Hasher) {
+ use std::hash::Hash;
+
+ self.layout.hash(hasher);
+ }
+}
diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs
index 775b953e..c8812ea2 100644
--- a/native/src/widget/row.rs
+++ b/native/src/widget/row.rs
@@ -30,6 +30,15 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
///
/// [`Row`]: struct.Row.html
pub fn new() -> Self {
+ Self::with_children(Vec::new())
+ }
+
+ /// Creates a [`Row`] with the given elements.
+ ///
+ /// [`Row`]: struct.Row.html
+ pub fn with_children(
+ children: Vec<Element<'a, Message, Renderer>>,
+ ) -> Self {
Row {
spacing: 0,
padding: 0,
@@ -38,7 +47,7 @@ impl<'a, Message, Renderer> Row<'a, Message, Renderer> {
max_width: u32::MAX,
max_height: u32::MAX,
align_items: Align::Start,
- children: Vec::new(),
+ children,
}
}
diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs
index e83f25af..ec9746d4 100644
--- a/native/src/widget/scrollable.rs
+++ b/native/src/widget/scrollable.rs
@@ -118,7 +118,7 @@ where
Renderer: 'static + self::Renderer + column::Renderer,
{
fn width(&self) -> Length {
- Length::Fill
+ Widget::<Message, Renderer>::width(&self.content)
}
fn height(&self) -> Length {
@@ -132,7 +132,7 @@ where
) -> layout::Node {
let limits = limits
.max_height(self.max_height)
- .width(Length::Fill)
+ .width(Widget::<Message, Renderer>::width(&self.content))
.height(self.height);
let child_limits = layout::Limits::new(
diff --git a/rustfmt.toml b/rustfmt.toml
index 7e5dded7..d979d317 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1 @@
max_width=80
-wrap_comments=true
-merge_imports=true
diff --git a/src/application.rs b/src/application.rs
index 374810cb..2ee3337f 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -183,6 +183,7 @@ pub trait Application: Sized {
} else {
None
},
+ ..iced_wgpu::Settings::default()
},
);
diff --git a/src/keyboard.rs b/src/keyboard.rs
new file mode 100644
index 00000000..181dd974
--- /dev/null
+++ b/src/keyboard.rs
@@ -0,0 +1,6 @@
+//! Listen and react to keyboard events.
+#[cfg(not(target_arch = "wasm32"))]
+pub use iced_winit::input::keyboard::{KeyCode, ModifiersState};
+
+#[cfg(target_arch = "wasm32")]
+pub use iced_web::keyboard::{KeyCode, ModifiersState};
diff --git a/src/lib.rs b/src/lib.rs
index d492db02..aeec24c2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -183,6 +183,7 @@ mod element;
mod sandbox;
pub mod executor;
+pub mod keyboard;
pub mod settings;
pub mod widget;
pub mod window;
diff --git a/src/widget.rs b/src/widget.rs
index 7d3a1cef..91ea1ed4 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -35,8 +35,9 @@ mod platform {
#[doc(no_inline)]
pub use {
button::Button, checkbox::Checkbox, container::Container, image::Image,
- progress_bar::ProgressBar, radio::Radio, scrollable::Scrollable,
- slider::Slider, svg::Svg, text_input::TextInput,
+ pane_grid::PaneGrid, progress_bar::ProgressBar, radio::Radio,
+ scrollable::Scrollable, slider::Slider, svg::Svg,
+ text_input::TextInput,
};
/// A container that distributes its contents vertically.
diff --git a/web/README.md b/web/README.md
index aceb4a97..83d1500f 100644
--- a/web/README.md
+++ b/web/README.md
@@ -36,7 +36,7 @@ For instance, let's say we want to build the [`tour` example]:
```
cd examples
cargo build --package tour --target wasm32-unknown-unknown
-wasm-bindgen ../target/wasm32-unknown-unknown/debug/examples/tour.wasm --out-dir tour --web
+wasm-bindgen ../target/wasm32-unknown-unknown/debug/tour.wasm --out-dir tour --web
```
*__Note:__ Keep in mind that Iced is still in early exploration stages and most of the work needs to happen on the native side of the ecosystem. At this stage, it is important to be able to batch work without having to constantly jump back and forth. Because of this, there is currently no requirement for the `master` branch to contain a cross-platform API at all times. If you hit an issue when building an example and want to help, it may be a good way to [start contributing]!*
diff --git a/web/src/lib.rs b/web/src/lib.rs
index 258ad9e7..1de00545 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -73,8 +73,8 @@ pub use dodrio;
pub use element::Element;
pub use hasher::Hasher;
pub use iced_core::{
- Align, Background, Color, Font, HorizontalAlignment, Length, Point, Size,
- Vector, VerticalAlignment,
+ keyboard, Align, Background, Color, Font, HorizontalAlignment, Length,
+ Point, Size, Vector, VerticalAlignment,
};
pub use iced_futures::{executor, futures, Command};
pub use subscription::Subscription;
diff --git a/web/src/widget/column.rs b/web/src/widget/column.rs
index 6454ffba..25b88b0e 100644
--- a/web/src/widget/column.rs
+++ b/web/src/widget/column.rs
@@ -25,6 +25,13 @@ impl<'a, Message> Column<'a, Message> {
///
/// [`Column`]: struct.Column.html
pub fn new() -> Self {
+ Self::with_children(Vec::new())
+ }
+
+ /// Creates a [`Column`] with the given elements.
+ ///
+ /// [`Column`]: struct.Column.html
+ pub fn with_children(children: Vec<Element<'a, Message>>) -> Self {
Column {
spacing: 0,
padding: 0,
@@ -33,7 +40,7 @@ impl<'a, Message> Column<'a, Message> {
max_width: u32::MAX,
max_height: u32::MAX,
align_items: Align::Start,
- children: Vec::new(),
+ children,
}
}
diff --git a/web/src/widget/row.rs b/web/src/widget/row.rs
index 02035113..cfa10fdf 100644
--- a/web/src/widget/row.rs
+++ b/web/src/widget/row.rs
@@ -25,6 +25,13 @@ impl<'a, Message> Row<'a, Message> {
///
/// [`Row`]: struct.Row.html
pub fn new() -> Self {
+ Self::with_children(Vec::new())
+ }
+
+ /// Creates a [`Row`] with the given elements.
+ ///
+ /// [`Row`]: struct.Row.html
+ pub fn with_children(children: Vec<Element<'a, Message>>) -> Self {
Row {
spacing: 0,
padding: 0,
@@ -33,7 +40,7 @@ impl<'a, Message> Row<'a, Message> {
max_width: u32::MAX,
max_height: u32::MAX,
align_items: Align::Start,
- children: Vec::new(),
+ children,
}
}
diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml
index 887c2d21..6c75af7b 100644
--- a/wgpu/Cargo.toml
+++ b/wgpu/Cargo.toml
@@ -15,12 +15,13 @@ canvas = ["lyon"]
iced_native = { version = "0.1.0", path = "../native" }
iced_style = { version = "0.1.0-alpha", path = "../style" }
wgpu = "0.4"
+wgpu_glyph = "0.7"
glyph_brush = "0.6"
-wgpu_glyph = { version = "0.7", git = "https://github.com/hecrj/wgpu_glyph", branch = "fix/font-load-panic" }
raw-window-handle = "0.3"
glam = "0.8"
font-kit = "0.4"
log = "0.4"
+guillotiere = "0.4"
[dependencies.image]
version = "0.22"
diff --git a/wgpu/src/image.rs b/wgpu/src/image.rs
index ccc956a9..d3603676 100644
--- a/wgpu/src/image.rs
+++ b/wgpu/src/image.rs
@@ -1,15 +1,23 @@
+mod atlas;
+
#[cfg(feature = "image")]
mod raster;
+
#[cfg(feature = "svg")]
mod vector;
use crate::Transformation;
-use iced_native::{image, svg, Rectangle};
+use atlas::Atlas;
+use iced_native::Rectangle;
+use std::cell::RefCell;
use std::mem;
-#[cfg(any(feature = "image", feature = "svg"))]
-use std::cell::RefCell;
+#[cfg(feature = "image")]
+use iced_native::image;
+
+#[cfg(feature = "svg")]
+use iced_native::svg;
#[derive(Debug)]
pub struct Pipeline {
@@ -24,11 +32,14 @@ pub struct Pipeline {
indices: wgpu::Buffer,
instances: wgpu::Buffer,
constants: wgpu::BindGroup,
+ texture: wgpu::BindGroup,
+ texture_version: usize,
texture_layout: wgpu::BindGroupLayout,
+ texture_atlas: Atlas,
}
impl Pipeline {
- pub fn new(device: &wgpu::Device) -> Self {
+ pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
@@ -135,7 +146,7 @@ impl Pipeline {
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
color_blend: wgpu::BlendDescriptor {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
@@ -174,6 +185,21 @@ impl Pipeline {
format: wgpu::VertexFormat::Float2,
offset: 4 * 2,
},
+ wgpu::VertexAttributeDescriptor {
+ shader_location: 3,
+ format: wgpu::VertexFormat::Float2,
+ offset: 4 * 4,
+ },
+ wgpu::VertexAttributeDescriptor {
+ shader_location: 4,
+ format: wgpu::VertexFormat::Float2,
+ offset: 4 * 6,
+ },
+ wgpu::VertexAttributeDescriptor {
+ shader_location: 5,
+ format: wgpu::VertexFormat::Uint,
+ offset: 4 * 8,
+ },
],
},
],
@@ -191,13 +217,26 @@ impl Pipeline {
.fill_from_slice(&QUAD_INDICES);
let instances = device.create_buffer(&wgpu::BufferDescriptor {
- size: mem::size_of::<Instance>() as u64,
+ size: mem::size_of::<Instance>() as u64 * Instance::MAX as u64,
usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST,
});
+ let texture_atlas = Atlas::new(device);
+
+ let texture = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ layout: &texture_layout,
+ bindings: &[wgpu::Binding {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(
+ &texture_atlas.view(),
+ ),
+ }],
+ });
+
Pipeline {
#[cfg(feature = "image")]
raster_cache: RefCell::new(raster::Cache::new()),
+
#[cfg(feature = "svg")]
vector_cache: RefCell::new(vector::Cache::new()),
@@ -207,7 +246,10 @@ impl Pipeline {
indices,
instances,
constants: constant_bind_group,
+ texture,
+ texture_version: texture_atlas.layer_count(),
texture_layout,
+ texture_atlas,
}
}
@@ -231,12 +273,72 @@ impl Pipeline {
&mut self,
device: &mut wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
- instances: &[Image],
+ images: &[Image],
transformation: Transformation,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
_scale: f32,
) {
+ let instances: &mut Vec<Instance> = &mut Vec::new();
+
+ #[cfg(feature = "image")]
+ let mut raster_cache = self.raster_cache.borrow_mut();
+
+ #[cfg(feature = "svg")]
+ let mut vector_cache = self.vector_cache.borrow_mut();
+
+ for image in images {
+ match &image.handle {
+ #[cfg(feature = "image")]
+ Handle::Raster(handle) => {
+ if let Some(atlas_entry) = raster_cache.upload(
+ handle,
+ device,
+ encoder,
+ &mut self.texture_atlas,
+ ) {
+ add_instances(image, atlas_entry, instances);
+ }
+ }
+ #[cfg(feature = "svg")]
+ Handle::Vector(handle) => {
+ if let Some(atlas_entry) = vector_cache.upload(
+ handle,
+ image.size,
+ _scale,
+ device,
+ encoder,
+ &mut self.texture_atlas,
+ ) {
+ add_instances(image, atlas_entry, instances);
+ }
+ }
+ }
+ }
+
+ if instances.is_empty() {
+ return;
+ }
+
+ let texture_version = self.texture_atlas.layer_count();
+
+ if self.texture_version != texture_version {
+ log::info!("Atlas has grown. Recreating bind group...");
+
+ self.texture =
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ layout: &self.texture_layout,
+ bindings: &[wgpu::Binding {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(
+ &self.texture_atlas.view(),
+ ),
+ }],
+ });
+
+ self.texture_version = texture_version;
+ }
+
let uniforms_buffer = device
.create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC)
.fill_from_slice(&[Uniforms {
@@ -251,123 +353,90 @@ impl Pipeline {
std::mem::size_of::<Uniforms>() as u64,
);
- // TODO: Batch draw calls using a texture atlas
- // Guillotière[1] by @nical can help us a lot here.
- //
- // [1]: https://github.com/nical/guillotiere
- for image in instances {
- let uploaded_texture = match &image.handle {
- Handle::Raster(_handle) => {
- #[cfg(feature = "image")]
- {
- let mut cache = self.raster_cache.borrow_mut();
- let memory = cache.load(&_handle);
-
- memory.upload(device, encoder, &self.texture_layout)
- }
-
- #[cfg(not(feature = "image"))]
- None
- }
- Handle::Vector(_handle) => {
- #[cfg(feature = "svg")]
- {
- let mut cache = self.vector_cache.borrow_mut();
-
- cache.upload(
- _handle,
- image.scale,
- _scale,
- device,
- encoder,
- &self.texture_layout,
- )
- }
-
- #[cfg(not(feature = "svg"))]
- None
- }
- };
-
- if let Some(texture) = uploaded_texture {
- let instance_buffer = device
- .create_buffer_mapped(1, wgpu::BufferUsage::COPY_SRC)
- .fill_from_slice(&[Instance {
- _position: image.position,
- _scale: image.scale,
- }]);
-
- encoder.copy_buffer_to_buffer(
- &instance_buffer,
- 0,
- &self.instances,
- 0,
- mem::size_of::<Instance>() as u64,
- );
-
- {
- let mut render_pass = encoder.begin_render_pass(
- &wgpu::RenderPassDescriptor {
- color_attachments: &[
- wgpu::RenderPassColorAttachmentDescriptor {
- attachment: target,
- resolve_target: None,
- load_op: wgpu::LoadOp::Load,
- store_op: wgpu::StoreOp::Store,
- clear_color: wgpu::Color {
- r: 0.0,
- g: 0.0,
- b: 0.0,
- a: 0.0,
- },
- },
- ],
- depth_stencil_attachment: None,
+ let instances_buffer = device
+ .create_buffer_mapped(instances.len(), wgpu::BufferUsage::COPY_SRC)
+ .fill_from_slice(&instances);
+
+ let mut i = 0;
+ let total = instances.len();
+
+ while i < total {
+ let end = (i + Instance::MAX).min(total);
+ let amount = end - i;
+
+ encoder.copy_buffer_to_buffer(
+ &instances_buffer,
+ (i * std::mem::size_of::<Instance>()) as u64,
+ &self.instances,
+ 0,
+ (amount * std::mem::size_of::<Instance>()) as u64,
+ );
+
+ let mut render_pass =
+ encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ color_attachments: &[
+ wgpu::RenderPassColorAttachmentDescriptor {
+ attachment: target,
+ resolve_target: None,
+ load_op: wgpu::LoadOp::Load,
+ store_op: wgpu::StoreOp::Store,
+ clear_color: wgpu::Color {
+ r: 0.0,
+ g: 0.0,
+ b: 0.0,
+ a: 0.0,
+ },
},
- );
-
- render_pass.set_pipeline(&self.pipeline);
- render_pass.set_bind_group(0, &self.constants, &[]);
- render_pass.set_bind_group(1, &texture, &[]);
- render_pass.set_index_buffer(&self.indices, 0);
- render_pass.set_vertex_buffers(
- 0,
- &[(&self.vertices, 0), (&self.instances, 0)],
- );
- render_pass.set_scissor_rect(
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
- );
-
- render_pass.draw_indexed(
- 0..QUAD_INDICES.len() as u32,
- 0,
- 0..1 as u32,
- );
- }
- }
+ ],
+ depth_stencil_attachment: None,
+ });
+
+ 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, 0);
+ render_pass.set_vertex_buffers(
+ 0,
+ &[(&self.vertices, 0), (&self.instances, 0)],
+ );
+
+ render_pass.set_scissor_rect(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ );
+
+ render_pass.draw_indexed(
+ 0..QUAD_INDICES.len() as u32,
+ 0,
+ 0..amount as u32,
+ );
+
+ i += Instance::MAX;
}
}
pub fn trim_cache(&mut self) {
#[cfg(feature = "image")]
- self.raster_cache.borrow_mut().trim();
+ self.raster_cache.borrow_mut().trim(&mut self.texture_atlas);
#[cfg(feature = "svg")]
- self.vector_cache.borrow_mut().trim();
+ self.vector_cache.borrow_mut().trim(&mut self.texture_atlas);
}
}
pub struct Image {
pub handle: Handle,
pub position: [f32; 2],
- pub scale: [f32; 2],
+ pub size: [f32; 2],
}
pub enum Handle {
+ #[cfg(feature = "image")]
Raster(image::Handle),
+
+ #[cfg(feature = "svg")]
Vector(svg::Handle),
}
@@ -395,10 +464,17 @@ const QUAD_VERTS: [Vertex; 4] = [
];
#[repr(C)]
-#[derive(Clone, Copy)]
+#[derive(Debug, Clone, Copy)]
struct Instance {
_position: [f32; 2],
- _scale: [f32; 2],
+ _size: [f32; 2],
+ _position_in_atlas: [f32; 2],
+ _size_in_atlas: [f32; 2],
+ _layer: u32,
+}
+
+impl Instance {
+ pub const MAX: usize = 1_000;
}
#[repr(C)]
@@ -406,3 +482,67 @@ struct Instance {
struct Uniforms {
transform: [f32; 16],
}
+
+fn add_instances(
+ image: &Image,
+ entry: &atlas::Entry,
+ instances: &mut Vec<Instance>,
+) {
+ match entry {
+ atlas::Entry::Contiguous(allocation) => {
+ add_instance(image.position, image.size, allocation, instances);
+ }
+ atlas::Entry::Fragmented { fragments, size } => {
+ let scaling_x = image.size[0] / size.0 as f32;
+ let scaling_y = image.size[1] / size.1 as f32;
+
+ for fragment in fragments {
+ let allocation = &fragment.allocation;
+
+ let [x, y] = image.position;
+ let (fragment_x, fragment_y) = fragment.position;
+ let (fragment_width, fragment_height) = allocation.size();
+
+ let position = [
+ x + fragment_x as f32 * scaling_x,
+ y + fragment_y as f32 * scaling_y,
+ ];
+
+ let size = [
+ fragment_width as f32 * scaling_x,
+ fragment_height as f32 * scaling_y,
+ ];
+
+ add_instance(position, size, allocation, instances);
+ }
+ }
+ }
+}
+
+#[inline]
+fn add_instance(
+ position: [f32; 2],
+ size: [f32; 2],
+ allocation: &atlas::Allocation,
+ instances: &mut Vec<Instance>,
+) {
+ let (x, y) = allocation.position();
+ let (width, height) = allocation.size();
+ let layer = allocation.layer();
+
+ let instance = Instance {
+ _position: position,
+ _size: size,
+ _position_in_atlas: [
+ (x as f32 + 0.5) / atlas::SIZE as f32,
+ (y as f32 + 0.5) / atlas::SIZE as f32,
+ ],
+ _size_in_atlas: [
+ (width as f32 - 1.0) / atlas::SIZE as f32,
+ (height as f32 - 1.0) / atlas::SIZE as f32,
+ ],
+ _layer: layer as u32,
+ };
+
+ instances.push(instance);
+}
diff --git a/wgpu/src/image/atlas.rs b/wgpu/src/image/atlas.rs
new file mode 100644
index 00000000..86a5ff49
--- /dev/null
+++ b/wgpu/src/image/atlas.rs
@@ -0,0 +1,361 @@
+pub mod entry;
+
+mod allocation;
+mod allocator;
+mod layer;
+
+pub use allocation::Allocation;
+pub use entry::Entry;
+pub use layer::Layer;
+
+use allocator::Allocator;
+
+pub const SIZE: u32 = 2048;
+
+#[derive(Debug)]
+pub struct Atlas {
+ texture: wgpu::Texture,
+ texture_view: wgpu::TextureView,
+ layers: Vec<Layer>,
+}
+
+impl Atlas {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let extent = wgpu::Extent3d {
+ width: SIZE,
+ height: SIZE,
+ depth: 1,
+ };
+
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
+ size: extent,
+ array_layer_count: 2,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ usage: wgpu::TextureUsage::COPY_DST
+ | wgpu::TextureUsage::COPY_SRC
+ | wgpu::TextureUsage::SAMPLED,
+ });
+
+ let texture_view = texture.create_default_view();
+
+ Atlas {
+ texture,
+ texture_view,
+ layers: vec![Layer::Empty, Layer::Empty],
+ }
+ }
+
+ pub fn view(&self) -> &wgpu::TextureView {
+ &self.texture_view
+ }
+
+ pub fn layer_count(&self) -> usize {
+ self.layers.len()
+ }
+
+ pub fn upload<C>(
+ &mut self,
+ width: u32,
+ height: u32,
+ data: &[C],
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ ) -> Option<Entry>
+ where
+ C: Copy + 'static,
+ {
+ let entry = {
+ let current_size = self.layers.len();
+ let entry = self.allocate(width, height)?;
+
+ // We grow the internal texture after allocating if necessary
+ let new_layers = self.layers.len() - current_size;
+ self.grow(new_layers, device, encoder);
+
+ entry
+ };
+
+ log::info!("Allocated atlas entry: {:?}", entry);
+
+ let buffer = device
+ .create_buffer_mapped(data.len(), wgpu::BufferUsage::COPY_SRC)
+ .fill_from_slice(data);
+
+ match &entry {
+ Entry::Contiguous(allocation) => {
+ self.upload_allocation(
+ &buffer,
+ width,
+ height,
+ 0,
+ &allocation,
+ encoder,
+ );
+ }
+ Entry::Fragmented { fragments, .. } => {
+ for fragment in fragments {
+ let (x, y) = fragment.position;
+ let offset = (y * width + x) as usize * 4;
+
+ self.upload_allocation(
+ &buffer,
+ width,
+ height,
+ offset,
+ &fragment.allocation,
+ encoder,
+ );
+ }
+ }
+ }
+
+ log::info!("Current atlas: {:?}", self);
+
+ Some(entry)
+ }
+
+ pub fn remove(&mut self, entry: &Entry) {
+ log::info!("Removing atlas entry: {:?}", entry);
+
+ match entry {
+ Entry::Contiguous(allocation) => {
+ self.deallocate(allocation);
+ }
+ Entry::Fragmented { fragments, .. } => {
+ for fragment in fragments {
+ self.deallocate(&fragment.allocation);
+ }
+ }
+ }
+ }
+
+ fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> {
+ // Allocate one layer if texture fits perfectly
+ if width == SIZE && height == SIZE {
+ let mut empty_layers = self
+ .layers
+ .iter_mut()
+ .enumerate()
+ .filter(|(_, layer)| layer.is_empty());
+
+ if let Some((i, layer)) = empty_layers.next() {
+ *layer = Layer::Full;
+
+ return Some(Entry::Contiguous(Allocation::Full { layer: i }));
+ }
+
+ self.layers.push(Layer::Full);
+
+ return Some(Entry::Contiguous(Allocation::Full {
+ layer: self.layers.len() - 1,
+ }));
+ }
+
+ // Split big textures across multiple layers
+ if width > SIZE || height > SIZE {
+ let mut fragments = Vec::new();
+ let mut y = 0;
+
+ while y < height {
+ let height = std::cmp::min(height - y, SIZE);
+ let mut x = 0;
+
+ while x < width {
+ let width = std::cmp::min(width - x, SIZE);
+
+ let allocation = self.allocate(width, height)?;
+
+ if let Entry::Contiguous(allocation) = allocation {
+ fragments.push(entry::Fragment {
+ position: (x, y),
+ allocation,
+ });
+ }
+
+ x += width;
+ }
+
+ y += height;
+ }
+
+ return Some(Entry::Fragmented {
+ size: (width, height),
+ fragments,
+ });
+ }
+
+ // Try allocating on an existing layer
+ for (i, layer) in self.layers.iter_mut().enumerate() {
+ match layer {
+ Layer::Empty => {
+ let mut allocator = Allocator::new(SIZE);
+
+ if let Some(region) = allocator.allocate(width, height) {
+ *layer = Layer::Busy(allocator);
+
+ return Some(Entry::Contiguous(Allocation::Partial {
+ region,
+ layer: i,
+ }));
+ }
+ }
+ Layer::Busy(allocator) => {
+ if let Some(region) = allocator.allocate(width, height) {
+ return Some(Entry::Contiguous(Allocation::Partial {
+ region,
+ layer: i,
+ }));
+ }
+ }
+ _ => {}
+ }
+ }
+
+ // Create new layer with atlas allocator
+ let mut allocator = Allocator::new(SIZE);
+
+ if let Some(region) = allocator.allocate(width, height) {
+ self.layers.push(Layer::Busy(allocator));
+
+ return Some(Entry::Contiguous(Allocation::Partial {
+ region,
+ layer: self.layers.len() - 1,
+ }));
+ }
+
+ // We ran out of memory (?)
+ None
+ }
+
+ fn deallocate(&mut self, allocation: &Allocation) {
+ log::info!("Deallocating atlas: {:?}", allocation);
+
+ match allocation {
+ Allocation::Full { layer } => {
+ self.layers[*layer] = Layer::Empty;
+ }
+ Allocation::Partial { layer, region } => {
+ let layer = &mut self.layers[*layer];
+
+ if let Layer::Busy(allocator) = layer {
+ allocator.deallocate(region);
+
+ if allocator.is_empty() {
+ *layer = Layer::Empty;
+ }
+ }
+ }
+ }
+ }
+
+ fn upload_allocation(
+ &mut self,
+ buffer: &wgpu::Buffer,
+ image_width: u32,
+ image_height: u32,
+ offset: usize,
+ allocation: &Allocation,
+ encoder: &mut wgpu::CommandEncoder,
+ ) {
+ let (x, y) = allocation.position();
+ let (width, height) = allocation.size();
+ let layer = allocation.layer();
+
+ let extent = wgpu::Extent3d {
+ width,
+ height,
+ depth: 1,
+ };
+
+ encoder.copy_buffer_to_texture(
+ wgpu::BufferCopyView {
+ buffer,
+ offset: offset as u64,
+ row_pitch: 4 * image_width,
+ image_height,
+ },
+ wgpu::TextureCopyView {
+ texture: &self.texture,
+ array_layer: layer as u32,
+ mip_level: 0,
+ origin: wgpu::Origin3d {
+ x: x as f32,
+ y: y as f32,
+ z: 0.0,
+ },
+ },
+ extent,
+ );
+ }
+
+ fn grow(
+ &mut self,
+ amount: usize,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ ) {
+ if amount == 0 {
+ return;
+ }
+
+ let new_texture = device.create_texture(&wgpu::TextureDescriptor {
+ size: wgpu::Extent3d {
+ width: SIZE,
+ height: SIZE,
+ depth: 1,
+ },
+ array_layer_count: self.layers.len() as u32,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ usage: wgpu::TextureUsage::COPY_DST
+ | wgpu::TextureUsage::COPY_SRC
+ | wgpu::TextureUsage::SAMPLED,
+ });
+
+ let amount_to_copy = self.layers.len() - amount;
+
+ for (i, layer) in
+ self.layers.iter_mut().take(amount_to_copy).enumerate()
+ {
+ if layer.is_empty() {
+ continue;
+ }
+
+ encoder.copy_texture_to_texture(
+ wgpu::TextureCopyView {
+ texture: &self.texture,
+ array_layer: i as u32,
+ mip_level: 0,
+ origin: wgpu::Origin3d {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ },
+ },
+ wgpu::TextureCopyView {
+ texture: &new_texture,
+ array_layer: i as u32,
+ mip_level: 0,
+ origin: wgpu::Origin3d {
+ x: 0.0,
+ y: 0.0,
+ z: 0.0,
+ },
+ },
+ wgpu::Extent3d {
+ width: SIZE,
+ height: SIZE,
+ depth: 1,
+ },
+ );
+ }
+
+ self.texture = new_texture;
+ self.texture_view = self.texture.create_default_view();
+ }
+}
diff --git a/wgpu/src/image/atlas/allocation.rs b/wgpu/src/image/atlas/allocation.rs
new file mode 100644
index 00000000..59b7239f
--- /dev/null
+++ b/wgpu/src/image/atlas/allocation.rs
@@ -0,0 +1,35 @@
+use crate::image::atlas::{self, allocator};
+
+#[derive(Debug)]
+pub enum Allocation {
+ Partial {
+ layer: usize,
+ region: allocator::Region,
+ },
+ Full {
+ layer: usize,
+ },
+}
+
+impl Allocation {
+ pub fn position(&self) -> (u32, u32) {
+ match self {
+ Allocation::Partial { region, .. } => region.position(),
+ Allocation::Full { .. } => (0, 0),
+ }
+ }
+
+ pub fn size(&self) -> (u32, u32) {
+ match self {
+ Allocation::Partial { region, .. } => region.size(),
+ Allocation::Full { .. } => (atlas::SIZE, atlas::SIZE),
+ }
+ }
+
+ pub fn layer(&self) -> usize {
+ match self {
+ Allocation::Partial { layer, .. } => *layer,
+ Allocation::Full { layer } => *layer,
+ }
+ }
+}
diff --git a/wgpu/src/image/atlas/allocator.rs b/wgpu/src/image/atlas/allocator.rs
new file mode 100644
index 00000000..7a4ff5b1
--- /dev/null
+++ b/wgpu/src/image/atlas/allocator.rs
@@ -0,0 +1,69 @@
+use guillotiere::{AtlasAllocator, Size};
+
+pub struct Allocator {
+ raw: AtlasAllocator,
+ allocations: usize,
+}
+
+impl Allocator {
+ pub fn new(size: u32) -> Allocator {
+ let raw = AtlasAllocator::new(Size::new(size as i32, size as i32));
+
+ Allocator {
+ raw,
+ allocations: 0,
+ }
+ }
+
+ pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> {
+ let allocation =
+ self.raw.allocate(Size::new(width as i32, height as i32))?;
+
+ self.allocations += 1;
+
+ Some(Region { allocation })
+ }
+
+ pub fn deallocate(&mut self, region: &Region) {
+ self.raw.deallocate(region.allocation.id);
+
+ self.allocations = self.allocations.saturating_sub(1);
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.allocations == 0
+ }
+}
+
+pub struct Region {
+ allocation: guillotiere::Allocation,
+}
+
+impl Region {
+ pub fn position(&self) -> (u32, u32) {
+ let rectangle = &self.allocation.rectangle;
+
+ (rectangle.min.x as u32, rectangle.min.y as u32)
+ }
+
+ pub fn size(&self) -> (u32, u32) {
+ let size = self.allocation.rectangle.size();
+
+ (size.width as u32, size.height as u32)
+ }
+}
+
+impl std::fmt::Debug for Allocator {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "Allocator")
+ }
+}
+
+impl std::fmt::Debug for Region {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Region")
+ .field("id", &self.allocation.id)
+ .field("rectangle", &self.allocation.rectangle)
+ .finish()
+ }
+}
diff --git a/wgpu/src/image/atlas/entry.rs b/wgpu/src/image/atlas/entry.rs
new file mode 100644
index 00000000..0310fc54
--- /dev/null
+++ b/wgpu/src/image/atlas/entry.rs
@@ -0,0 +1,26 @@
+use crate::image::atlas;
+
+#[derive(Debug)]
+pub enum Entry {
+ Contiguous(atlas::Allocation),
+ Fragmented {
+ size: (u32, u32),
+ fragments: Vec<Fragment>,
+ },
+}
+
+impl Entry {
+ #[cfg(feature = "image")]
+ pub fn size(&self) -> (u32, u32) {
+ match self {
+ Entry::Contiguous(allocation) => allocation.size(),
+ Entry::Fragmented { size, .. } => *size,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Fragment {
+ pub position: (u32, u32),
+ pub allocation: atlas::Allocation,
+}
diff --git a/wgpu/src/image/atlas/layer.rs b/wgpu/src/image/atlas/layer.rs
new file mode 100644
index 00000000..b1084ed9
--- /dev/null
+++ b/wgpu/src/image/atlas/layer.rs
@@ -0,0 +1,17 @@
+use crate::image::atlas::Allocator;
+
+#[derive(Debug)]
+pub enum Layer {
+ Empty,
+ Busy(Allocator),
+ Full,
+}
+
+impl Layer {
+ pub fn is_empty(&self) -> bool {
+ match self {
+ Layer::Empty => true,
+ _ => false,
+ }
+ }
+}
diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs
index fa107879..4f69df8c 100644
--- a/wgpu/src/image/raster.rs
+++ b/wgpu/src/image/raster.rs
@@ -1,17 +1,11 @@
+use crate::image::atlas::{self, Atlas};
use iced_native::image;
-use std::{
- collections::{HashMap, HashSet},
- rc::Rc,
-};
+use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub enum Memory {
Host(::image::ImageBuffer<::image::Bgra<u8>, Vec<u8>>),
- Device {
- bind_group: Rc<wgpu::BindGroup>,
- width: u32,
- height: u32,
- },
+ Device(atlas::Entry),
NotFound,
Invalid,
}
@@ -20,97 +14,11 @@ impl Memory {
pub fn dimensions(&self) -> (u32, u32) {
match self {
Memory::Host(image) => image.dimensions(),
- Memory::Device { width, height, .. } => (*width, *height),
+ Memory::Device(entry) => entry.size(),
Memory::NotFound => (1, 1),
Memory::Invalid => (1, 1),
}
}
-
- pub fn upload(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- texture_layout: &wgpu::BindGroupLayout,
- ) -> Option<Rc<wgpu::BindGroup>> {
- match self {
- Memory::Host(image) => {
- let (width, height) = image.dimensions();
-
- let extent = wgpu::Extent3d {
- width,
- height,
- depth: 1,
- };
-
- let texture = device.create_texture(&wgpu::TextureDescriptor {
- size: extent,
- array_layer_count: 1,
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
- usage: wgpu::TextureUsage::COPY_DST
- | wgpu::TextureUsage::SAMPLED,
- });
-
- let temp_buf = {
- let flat_samples = image.as_flat_samples();
- let slice = flat_samples.as_slice();
-
- device
- .create_buffer_mapped(
- slice.len(),
- wgpu::BufferUsage::COPY_SRC,
- )
- .fill_from_slice(slice)
- };
-
- encoder.copy_buffer_to_texture(
- wgpu::BufferCopyView {
- buffer: &temp_buf,
- offset: 0,
- row_pitch: 4 * width as u32,
- image_height: height as u32,
- },
- wgpu::TextureCopyView {
- texture: &texture,
- array_layer: 0,
- mip_level: 0,
- origin: wgpu::Origin3d {
- x: 0.0,
- y: 0.0,
- z: 0.0,
- },
- },
- extent,
- );
-
- let bind_group =
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- layout: texture_layout,
- bindings: &[wgpu::Binding {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(
- &texture.create_default_view(),
- ),
- }],
- });
-
- let bind_group = Rc::new(bind_group);
-
- *self = Memory::Device {
- bind_group: bind_group.clone(),
- width,
- height,
- };
-
- Some(bind_group)
- }
- Memory::Device { bind_group, .. } => Some(bind_group.clone()),
- Memory::NotFound => None,
- Memory::Invalid => None,
- }
- }
}
#[derive(Debug)]
@@ -147,16 +55,66 @@ impl Cache {
Memory::Invalid
}
}
+ image::Data::Pixels {
+ width,
+ height,
+ pixels,
+ } => {
+ if let Some(image) = ::image::ImageBuffer::from_vec(
+ *width,
+ *height,
+ pixels.to_vec(),
+ ) {
+ Memory::Host(image)
+ } else {
+ Memory::Invalid
+ }
+ }
};
self.insert(handle, memory);
self.get(handle).unwrap()
}
- pub fn trim(&mut self) {
+ pub fn upload(
+ &mut self,
+ handle: &image::Handle,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ atlas: &mut Atlas,
+ ) -> Option<&atlas::Entry> {
+ let memory = self.load(handle);
+
+ if let Memory::Host(image) = memory {
+ let (width, height) = image.dimensions();
+
+ let entry = atlas.upload(width, height, &image, device, encoder)?;
+
+ *memory = Memory::Device(entry);
+ }
+
+ if let Memory::Device(allocation) = memory {
+ Some(allocation)
+ } else {
+ None
+ }
+ }
+
+ pub fn trim(&mut self, atlas: &mut Atlas) {
let hits = &self.hits;
- self.map.retain(|k, _| hits.contains(k));
+ self.map.retain(|k, memory| {
+ let retain = hits.contains(k);
+
+ if !retain {
+ if let Memory::Device(entry) = memory {
+ atlas.remove(entry);
+ }
+ }
+
+ retain
+ });
+
self.hits.clear();
}
diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs
index 713978f5..bae0f82f 100644
--- a/wgpu/src/image/vector.rs
+++ b/wgpu/src/image/vector.rs
@@ -1,18 +1,16 @@
+use crate::image::atlas::{self, Atlas};
use iced_native::svg;
-use std::{
- collections::{HashMap, HashSet},
- rc::Rc,
-};
+use std::collections::{HashMap, HashSet};
pub enum Svg {
- Loaded { tree: resvg::usvg::Tree },
+ Loaded(resvg::usvg::Tree),
NotFound,
}
impl Svg {
pub fn viewport_dimensions(&self) -> (u32, u32) {
match self {
- Svg::Loaded { tree } => {
+ Svg::Loaded(tree) => {
let size = tree.svg_node().size;
(size.width() as u32, size.height() as u32)
@@ -22,16 +20,10 @@ impl Svg {
}
}
-impl std::fmt::Debug for Svg {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "Svg")
- }
-}
-
#[derive(Debug)]
pub struct Cache {
svgs: HashMap<u64, Svg>,
- rasterized: HashMap<(u64, u32, u32), Rc<wgpu::BindGroup>>,
+ rasterized: HashMap<(u64, u32, u32), atlas::Entry>,
svg_hits: HashSet<u64>,
rasterized_hits: HashSet<(u64, u32, u32)>,
}
@@ -54,7 +46,7 @@ impl Cache {
let opt = resvg::Options::default();
let svg = match resvg::usvg::Tree::from_file(handle.path(), &opt.usvg) {
- Ok(tree) => Svg::Loaded { tree },
+ Ok(tree) => Svg::Loaded(tree),
Err(_) => Svg::NotFound,
};
@@ -69,8 +61,8 @@ impl Cache {
scale: f32,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
- texture_layout: &wgpu::BindGroupLayout,
- ) -> Option<Rc<wgpu::BindGroup>> {
+ texture_atlas: &mut Atlas,
+ ) -> Option<&atlas::Entry> {
let id = handle.id();
let (width, height) = (
@@ -82,115 +74,78 @@ impl Cache {
// We currently rerasterize the SVG when its size changes. This is slow
// as heck. A GPU rasterizer like `pathfinder` may perform better.
// It would be cool to be able to smooth resize the `svg` example.
- if let Some(bind_group) = self.rasterized.get(&(id, width, height)) {
+ if self.rasterized.contains_key(&(id, width, height)) {
let _ = self.svg_hits.insert(id);
let _ = self.rasterized_hits.insert((id, width, height));
- return Some(bind_group.clone());
+ return self.rasterized.get(&(id, width, height));
}
match self.load(handle) {
- Svg::Loaded { tree } => {
+ Svg::Loaded(tree) => {
if width == 0 || height == 0 {
return None;
}
- let extent = wgpu::Extent3d {
- width,
- height,
- depth: 1,
- };
-
- let texture = device.create_texture(&wgpu::TextureDescriptor {
- size: extent,
- array_layer_count: 1,
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
- usage: wgpu::TextureUsage::COPY_DST
- | wgpu::TextureUsage::SAMPLED,
- });
-
- let temp_buf = {
- let screen_size =
- resvg::ScreenSize::new(width, height).unwrap();
-
- let mut canvas = resvg::raqote::DrawTarget::new(
- width as i32,
- height as i32,
- );
-
- resvg::backend_raqote::render_to_canvas(
- &tree,
- &resvg::Options::default(),
- screen_size,
- &mut canvas,
- );
-
- let slice = canvas.get_data();
-
- device
- .create_buffer_mapped(
- slice.len(),
- wgpu::BufferUsage::COPY_SRC,
- )
- .fill_from_slice(slice)
- };
-
- encoder.copy_buffer_to_texture(
- wgpu::BufferCopyView {
- buffer: &temp_buf,
- offset: 0,
- row_pitch: 4 * width as u32,
- image_height: height as u32,
- },
- wgpu::TextureCopyView {
- texture: &texture,
- array_layer: 0,
- mip_level: 0,
- origin: wgpu::Origin3d {
- x: 0.0,
- y: 0.0,
- z: 0.0,
- },
- },
- extent,
+ // TODO: Optimize!
+ // We currently rerasterize the SVG when its size changes. This is slow
+ // as heck. A GPU rasterizer like `pathfinder` may perform better.
+ // It would be cool to be able to smooth resize the `svg` example.
+ let screen_size =
+ resvg::ScreenSize::new(width, height).unwrap();
+
+ let mut canvas =
+ resvg::raqote::DrawTarget::new(width as i32, height as i32);
+
+ resvg::backend_raqote::render_to_canvas(
+ tree,
+ &resvg::Options::default(),
+ screen_size,
+ &mut canvas,
);
- let bind_group =
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- layout: texture_layout,
- bindings: &[wgpu::Binding {
- binding: 0,
- resource: wgpu::BindingResource::TextureView(
- &texture.create_default_view(),
- ),
- }],
- });
-
- let bind_group = Rc::new(bind_group);
-
- let _ = self
- .rasterized
- .insert((id, width, height), bind_group.clone());
+ let allocation = texture_atlas.upload(
+ width,
+ height,
+ canvas.get_data(),
+ device,
+ encoder,
+ )?;
let _ = self.svg_hits.insert(id);
let _ = self.rasterized_hits.insert((id, width, height));
+ let _ = self.rasterized.insert((id, width, height), allocation);
- Some(bind_group)
+ self.rasterized.get(&(id, width, height))
}
Svg::NotFound => None,
}
}
- pub fn trim(&mut self) {
+ pub fn trim(&mut self, atlas: &mut Atlas) {
let svg_hits = &self.svg_hits;
let rasterized_hits = &self.rasterized_hits;
self.svgs.retain(|k, _| svg_hits.contains(k));
- self.rasterized.retain(|k, _| rasterized_hits.contains(k));
+ self.rasterized.retain(|k, entry| {
+ let retain = rasterized_hits.contains(k);
+
+ if !retain {
+ atlas.remove(entry);
+ }
+
+ retain
+ });
self.svg_hits.clear();
self.rasterized_hits.clear();
}
}
+
+impl std::fmt::Debug for Svg {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Svg::Loaded(_) => write!(f, "Svg::Loaded"),
+ Svg::NotFound => write!(f, "Svg::NotFound"),
+ }
+ }
+}
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 4f2b732d..4e0cbc60 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -30,7 +30,6 @@ pub mod triangle;
pub mod widget;
pub mod window;
-mod image;
mod primitive;
mod quad;
mod renderer;
@@ -51,6 +50,8 @@ pub use viewport::Viewport;
#[doc(no_inline)]
pub use widget::*;
-pub(crate) use self::image::Image;
pub(crate) use quad::Quad;
pub(crate) use transformation::Transformation;
+
+#[cfg(any(feature = "image", feature = "svg"))]
+mod image;
diff --git a/wgpu/src/primitive.rs b/wgpu/src/primitive.rs
index 823b4b72..46d9e624 100644
--- a/wgpu/src/primitive.rs
+++ b/wgpu/src/primitive.rs
@@ -78,7 +78,18 @@ pub enum Primitive {
origin: Point,
/// The vertex and index buffers of the mesh
- buffers: Arc<triangle::Mesh2D>,
+ buffers: triangle::Mesh2D,
+ },
+ /// A cached primitive.
+ ///
+ /// This can be useful if you are implementing a widget where primitive
+ /// generation is expensive.
+ Cached {
+ /// The origin of the coordinate system of the cached primitives
+ origin: Point,
+
+ /// The cached primitive
+ cache: Arc<Primitive>,
},
}
diff --git a/wgpu/src/quad.rs b/wgpu/src/quad.rs
index fe3276a3..9047080d 100644
--- a/wgpu/src/quad.rs
+++ b/wgpu/src/quad.rs
@@ -14,7 +14,10 @@ pub struct Pipeline {
}
impl Pipeline {
- pub fn new(device: &mut wgpu::Device) -> Pipeline {
+ pub fn new(
+ device: &mut wgpu::Device,
+ format: wgpu::TextureFormat,
+ ) -> Pipeline {
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
bindings: &[wgpu::BindGroupLayoutBinding {
@@ -79,7 +82,7 @@ impl Pipeline {
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
color_blend: wgpu::BlendDescriptor {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
diff --git a/wgpu/src/renderer.rs b/wgpu/src/renderer.rs
index 29adcfb6..c06af339 100644
--- a/wgpu/src/renderer.rs
+++ b/wgpu/src/renderer.rs
@@ -1,12 +1,15 @@
use crate::{
- image, quad, text, triangle, Defaults, Image, Primitive, Quad, Settings,
- Target, Transformation,
+ quad, text, triangle, Defaults, Primitive, Quad, Settings, Target,
+ Transformation,
};
+
+#[cfg(any(feature = "image", feature = "svg"))]
+use crate::image::{self, Image};
+
use iced_native::{
layout, Background, Color, Layout, MouseCursor, Point, Rectangle, Vector,
Widget,
};
-use std::sync::Arc;
mod widget;
@@ -16,29 +19,33 @@ mod widget;
#[derive(Debug)]
pub struct Renderer {
quad_pipeline: quad::Pipeline,
- image_pipeline: image::Pipeline,
text_pipeline: text::Pipeline,
- triangle_pipeline: crate::triangle::Pipeline,
+ triangle_pipeline: triangle::Pipeline,
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ image_pipeline: image::Pipeline,
}
struct Layer<'a> {
bounds: Rectangle<u32>,
- offset: Vector<u32>,
quads: Vec<Quad>,
- images: Vec<Image>,
- meshes: Vec<(Point, Arc<triangle::Mesh2D>)>,
+ meshes: Vec<(Point, &'a triangle::Mesh2D)>,
text: Vec<wgpu_glyph::Section<'a>>,
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ images: Vec<Image>,
}
impl<'a> Layer<'a> {
- pub fn new(bounds: Rectangle<u32>, offset: Vector<u32>) -> Self {
+ pub fn new(bounds: Rectangle<u32>) -> Self {
Self {
bounds,
- offset,
quads: Vec::new(),
- images: Vec::new(),
text: Vec::new(),
meshes: Vec::new(),
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ images: Vec::new(),
}
}
}
@@ -48,17 +55,25 @@ impl Renderer {
///
/// [`Renderer`]: struct.Renderer.html
pub fn new(device: &mut wgpu::Device, settings: Settings) -> Self {
- let text_pipeline = text::Pipeline::new(device, settings.default_font);
- let quad_pipeline = quad::Pipeline::new(device);
- let image_pipeline = crate::image::Pipeline::new(device);
- let triangle_pipeline =
- triangle::Pipeline::new(device, settings.antialiasing);
+ let text_pipeline =
+ text::Pipeline::new(device, settings.format, settings.default_font);
+ let quad_pipeline = quad::Pipeline::new(device, settings.format);
+ let triangle_pipeline = triangle::Pipeline::new(
+ device,
+ settings.format,
+ settings.antialiasing,
+ );
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ let image_pipeline = image::Pipeline::new(device, settings.format);
Self {
quad_pipeline,
- image_pipeline,
text_pipeline,
triangle_pipeline,
+
+ #[cfg(any(feature = "image", feature = "svg"))]
+ image_pipeline,
}
}
@@ -85,17 +100,14 @@ impl Renderer {
let mut layers = Vec::new();
- layers.push(Layer::new(
- Rectangle {
- x: 0,
- y: 0,
- width: u32::from(width),
- height: u32::from(height),
- },
- Vector::new(0, 0),
- ));
-
- self.draw_primitive(primitive, &mut layers);
+ layers.push(Layer::new(Rectangle {
+ x: 0,
+ y: 0,
+ width: u32::from(width),
+ height: u32::from(height),
+ }));
+
+ self.draw_primitive(Vector::new(0.0, 0.0), primitive, &mut layers);
self.draw_overlay(overlay, &mut layers);
for layer in layers {
@@ -111,6 +123,7 @@ impl Renderer {
);
}
+ #[cfg(any(feature = "image", feature = "svg"))]
self.image_pipeline.trim_cache();
*mouse_cursor
@@ -118,17 +131,16 @@ impl Renderer {
fn draw_primitive<'a>(
&mut self,
+ translation: Vector,
primitive: &'a Primitive,
layers: &mut Vec<Layer<'a>>,
) {
- let layer = layers.last_mut().unwrap();
-
match primitive {
Primitive::None => {}
Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?)
for primitive in primitives {
- self.draw_primitive(primitive, layers)
+ self.draw_primitive(translation, primitive, layers)
}
}
Primitive::Text {
@@ -160,12 +172,11 @@ impl Renderer {
}
};
+ let layer = layers.last_mut().unwrap();
+
layer.text.push(wgpu_glyph::Section {
text: &content,
- screen_position: (
- x - layer.offset.x as f32,
- y - layer.offset.y as f32,
- ),
+ screen_position: (x + translation.x, y + translation.y),
bounds: (bounds.width, bounds.height),
scale: wgpu_glyph::Scale { x: *size, y: *size },
color: color.into_linear(),
@@ -203,11 +214,13 @@ impl Renderer {
border_width,
border_color,
} => {
- // TODO: Move some of this computations to the GPU (?)
+ let layer = layers.last_mut().unwrap();
+
+ // TODO: Move some of these computations to the GPU (?)
layer.quads.push(Quad {
position: [
- bounds.x - layer.offset.x as f32,
- bounds.y - layer.offset.y as f32,
+ bounds.x + translation.x,
+ bounds.y + translation.y,
],
scale: [bounds.width, bounds.height],
color: match background {
@@ -218,54 +231,81 @@ impl Renderer {
border_color: border_color.into_linear(),
});
}
- Primitive::Image { handle, bounds } => {
- layer.images.push(Image {
- handle: image::Handle::Raster(handle.clone()),
- position: [bounds.x, bounds.y],
- scale: [bounds.width, bounds.height],
- });
- }
- Primitive::Svg { handle, bounds } => {
- layer.images.push(Image {
- handle: image::Handle::Vector(handle.clone()),
- position: [bounds.x, bounds.y],
- scale: [bounds.width, bounds.height],
- });
- }
Primitive::Mesh2D { origin, buffers } => {
- layer.meshes.push((*origin, buffers.clone()));
+ let layer = layers.last_mut().unwrap();
+
+ layer.meshes.push((*origin + translation, buffers));
}
Primitive::Clip {
bounds,
offset,
content,
} => {
- let x = bounds.x - layer.offset.x as f32;
- let y = bounds.y - layer.offset.y as f32;
- let width = (bounds.width + x).min(bounds.width);
- let height = (bounds.height + y).min(bounds.height);
-
- // Only draw visible content on-screen
- // TODO: Also, check for parent layer bounds to avoid further
- // drawing in some circumstances.
- if width > 0.0 && height > 0.0 {
- let clip_layer = Layer::new(
- Rectangle {
- x: x.max(0.0).floor() as u32,
- y: y.max(0.0).floor() as u32,
- width: width.ceil() as u32,
- height: height.ceil() as u32,
- },
- layer.offset + *offset,
- );
+ let layer = layers.last_mut().unwrap();
- let new_layer = Layer::new(layer.bounds, layer.offset);
+ let layer_bounds: Rectangle<f32> = layer.bounds.into();
+
+ let clip = Rectangle {
+ x: bounds.x + translation.x,
+ y: bounds.y + translation.y,
+ ..*bounds
+ };
+
+ // Only draw visible content
+ if let Some(clip_bounds) = layer_bounds.intersection(&clip) {
+ let clip_layer = Layer::new(clip_bounds.into());
+ let new_layer = Layer::new(layer.bounds);
layers.push(clip_layer);
- self.draw_primitive(content, layers);
+ self.draw_primitive(
+ translation
+ - Vector::new(offset.x as f32, offset.y as f32),
+ content,
+ layers,
+ );
layers.push(new_layer);
}
}
+
+ Primitive::Cached { origin, cache } => {
+ self.draw_primitive(
+ translation + Vector::new(origin.x, origin.y),
+ &cache,
+ layers,
+ );
+ }
+
+ #[cfg(feature = "image")]
+ Primitive::Image { handle, bounds } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.images.push(Image {
+ handle: image::Handle::Raster(handle.clone()),
+ position: [
+ bounds.x + translation.x,
+ bounds.y + translation.y,
+ ],
+ size: [bounds.width, bounds.height],
+ });
+ }
+ #[cfg(not(feature = "image"))]
+ Primitive::Image { .. } => {}
+
+ #[cfg(feature = "svg")]
+ Primitive::Svg { handle, bounds } => {
+ let layer = layers.last_mut().unwrap();
+
+ layer.images.push(Image {
+ handle: image::Handle::Vector(handle.clone()),
+ position: [
+ bounds.x + translation.x,
+ bounds.y + translation.y,
+ ],
+ size: [bounds.width, bounds.height],
+ });
+ }
+ #[cfg(not(feature = "svg"))]
+ Primitive::Svg { .. } => {}
}
}
@@ -275,7 +315,7 @@ impl Renderer {
layers: &mut Vec<Layer<'a>>,
) {
let first = layers.first().unwrap();
- let mut overlay = Layer::new(first.bounds, Vector::new(0, 0));
+ let mut overlay = Layer::new(first.bounds);
let font_id = self.text_pipeline.overlay_font();
let scale = wgpu_glyph::Scale { x: 20.0, y: 20.0 };
@@ -317,12 +357,8 @@ impl Renderer {
let bounds = layer.bounds * scale_factor;
if layer.meshes.len() > 0 {
- let translated = transformation
- * Transformation::scale(scale_factor, scale_factor)
- * Transformation::translate(
- -(layer.offset.x as f32),
- -(layer.offset.y as f32),
- );
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
self.triangle_pipeline.draw(
device,
@@ -330,7 +366,7 @@ impl Renderer {
target,
target_width,
target_height,
- translated,
+ scaled,
&layer.meshes,
bounds,
);
@@ -348,23 +384,22 @@ impl Renderer {
);
}
- if layer.images.len() > 0 {
- let translated_and_scaled = transformation
- * Transformation::scale(scale_factor, scale_factor)
- * Transformation::translate(
- -(layer.offset.x as f32),
- -(layer.offset.y as f32),
+ #[cfg(any(feature = "image", feature = "svg"))]
+ {
+ if layer.images.len() > 0 {
+ let scaled = transformation
+ * Transformation::scale(scale_factor, scale_factor);
+
+ self.image_pipeline.draw(
+ device,
+ encoder,
+ &layer.images,
+ scaled,
+ bounds,
+ target,
+ scale_factor,
);
-
- self.image_pipeline.draw(
- device,
- encoder,
- &layer.images,
- translated_and_scaled,
- bounds,
- target,
- scale_factor,
- );
+ }
}
if layer.text.len() > 0 {
diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs
index 84f908e7..37421fbe 100644
--- a/wgpu/src/renderer/widget.rs
+++ b/wgpu/src/renderer/widget.rs
@@ -2,6 +2,7 @@ mod button;
mod checkbox;
mod column;
mod container;
+mod pane_grid;
mod progress_bar;
mod radio;
mod row;
diff --git a/wgpu/src/renderer/widget/pane_grid.rs b/wgpu/src/renderer/widget/pane_grid.rs
new file mode 100644
index 00000000..2d201fec
--- /dev/null
+++ b/wgpu/src/renderer/widget/pane_grid.rs
@@ -0,0 +1,92 @@
+use crate::{Primitive, Renderer};
+use iced_native::{
+ pane_grid::{self, Axis, Pane},
+ Element, Layout, MouseCursor, Point, Rectangle, Vector,
+};
+
+impl pane_grid::Renderer for Renderer {
+ fn draw<Message>(
+ &mut self,
+ defaults: &Self::Defaults,
+ content: &[(Pane, Element<'_, Message, Self>)],
+ dragging: Option<Pane>,
+ resizing: Option<Axis>,
+ layout: Layout<'_>,
+ cursor_position: Point,
+ ) -> Self::Output {
+ let pane_cursor_position = if dragging.is_some() {
+ // TODO: Remove once cursor availability is encoded in the type
+ // system
+ Point::new(-1.0, -1.0)
+ } else {
+ cursor_position
+ };
+
+ let mut mouse_cursor = MouseCursor::OutOfBounds;
+ let mut dragged_pane = None;
+
+ let mut panes: Vec<_> = content
+ .iter()
+ .zip(layout.children())
+ .enumerate()
+ .map(|(i, ((id, pane), layout))| {
+ let (primitive, new_mouse_cursor) =
+ pane.draw(self, defaults, layout, pane_cursor_position);
+
+ if new_mouse_cursor > mouse_cursor {
+ mouse_cursor = new_mouse_cursor;
+ }
+
+ if Some(*id) == dragging {
+ dragged_pane = Some((i, layout));
+ }
+
+ primitive
+ })
+ .collect();
+
+ let primitives = if let Some((index, layout)) = dragged_pane {
+ let pane = panes.remove(index);
+ let bounds = layout.bounds();
+
+ // TODO: Fix once proper layering is implemented.
+ // This is a pretty hacky way to achieve layering.
+ let clip = Primitive::Clip {
+ bounds: Rectangle {
+ x: cursor_position.x - bounds.width / 2.0,
+ y: cursor_position.y - bounds.height / 2.0,
+ width: bounds.width + 0.5,
+ height: bounds.height + 0.5,
+ },
+ offset: Vector::new(0, 0),
+ content: Box::new(Primitive::Cached {
+ origin: Point::new(
+ cursor_position.x - bounds.x - bounds.width / 2.0,
+ cursor_position.y - bounds.y - bounds.height / 2.0,
+ ),
+ cache: std::sync::Arc::new(pane),
+ }),
+ };
+
+ panes.push(clip);
+
+ panes
+ } else {
+ panes
+ };
+
+ (
+ Primitive::Group { primitives },
+ if dragging.is_some() {
+ MouseCursor::Grabbing
+ } else if let Some(axis) = resizing {
+ match axis {
+ Axis::Horizontal => MouseCursor::ResizingVertically,
+ Axis::Vertical => MouseCursor::ResizingHorizontally,
+ }
+ } else {
+ mouse_cursor
+ },
+ )
+ }
+}
diff --git a/wgpu/src/renderer/widget/scrollable.rs b/wgpu/src/renderer/widget/scrollable.rs
index bfee7411..732523e3 100644
--- a/wgpu/src/renderer/widget/scrollable.rs
+++ b/wgpu/src/renderer/widget/scrollable.rs
@@ -58,14 +58,14 @@ impl scrollable::Renderer for Renderer {
style_sheet: &Self::Style,
(content, mouse_cursor): Self::Output,
) -> Self::Output {
- let clip = Primitive::Clip {
- bounds,
- offset: Vector::new(0, offset),
- content: Box::new(content),
- };
-
(
if let Some(scrollbar) = scrollbar {
+ let clip = Primitive::Clip {
+ bounds,
+ offset: Vector::new(0, offset),
+ content: Box::new(content),
+ };
+
let style = if state.is_scroller_grabbed() {
style_sheet.dragging()
} else if is_mouse_over_scrollbar {
@@ -115,7 +115,7 @@ impl scrollable::Renderer for Renderer {
primitives: vec![clip, scrollbar, scroller],
}
} else {
- clip
+ content
},
if is_mouse_over_scrollbar || state.is_scroller_grabbed() {
MouseCursor::Idle
diff --git a/wgpu/src/settings.rs b/wgpu/src/settings.rs
index 65853ce2..f946ce0d 100644
--- a/wgpu/src/settings.rs
+++ b/wgpu/src/settings.rs
@@ -5,8 +5,13 @@
/// The settings of a [`Renderer`].
///
/// [`Renderer`]: ../struct.Renderer.html
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Settings {
+ /// The output format of the [`Renderer`].
+ ///
+ /// [`Renderer`]: ../struct.Renderer.html
+ pub format: wgpu::TextureFormat,
+
/// The bytes of the font that will be used by default.
///
/// If `None` is provided, a default system font will be chosen.
@@ -16,6 +21,16 @@ pub struct Settings {
pub antialiasing: Option<Antialiasing>,
}
+impl Default for Settings {
+ fn default() -> Settings {
+ Settings {
+ format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ default_font: None,
+ antialiasing: None,
+ }
+ }
+}
+
/// An antialiasing strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Antialiasing {
diff --git a/wgpu/src/shader/image.frag b/wgpu/src/shader/image.frag
index e35e455a..2809e9e6 100644
--- a/wgpu/src/shader/image.frag
+++ b/wgpu/src/shader/image.frag
@@ -1,12 +1,12 @@
#version 450
-layout(location = 0) in vec2 v_Uv;
+layout(location = 0) in vec3 v_Uv;
layout(set = 0, binding = 1) uniform sampler u_Sampler;
-layout(set = 1, binding = 0) uniform texture2D u_Texture;
+layout(set = 1, binding = 0) uniform texture2DArray u_Texture;
layout(location = 0) out vec4 o_Color;
void main() {
- o_Color = texture(sampler2D(u_Texture, u_Sampler), v_Uv);
+ o_Color = texture(sampler2DArray(u_Texture, u_Sampler), v_Uv);
}
diff --git a/wgpu/src/shader/image.frag.spv b/wgpu/src/shader/image.frag.spv
index ebee82ac..65b08aa3 100644
--- a/wgpu/src/shader/image.frag.spv
+++ b/wgpu/src/shader/image.frag.spv
Binary files differ
diff --git a/wgpu/src/shader/image.vert b/wgpu/src/shader/image.vert
new file mode 100644
index 00000000..dab53cfe
--- /dev/null
+++ b/wgpu/src/shader/image.vert
@@ -0,0 +1,27 @@
+#version 450
+
+layout(location = 0) in vec2 v_Pos;
+layout(location = 1) in vec2 i_Pos;
+layout(location = 2) in vec2 i_Scale;
+layout(location = 3) in vec2 i_Atlas_Pos;
+layout(location = 4) in vec2 i_Atlas_Scale;
+layout(location = 5) in uint i_Layer;
+
+layout (set = 0, binding = 0) uniform Globals {
+ mat4 u_Transform;
+};
+
+layout(location = 0) out vec3 o_Uv;
+
+void main() {
+ o_Uv = vec3(v_Pos * i_Atlas_Scale + i_Atlas_Pos, i_Layer);
+
+ mat4 i_Transform = mat4(
+ vec4(i_Scale.x, 0.0, 0.0, 0.0),
+ vec4(0.0, i_Scale.y, 0.0, 0.0),
+ vec4(0.0, 0.0, 1.0, 0.0),
+ vec4(i_Pos, 0.0, 1.0)
+ );
+
+ gl_Position = u_Transform * i_Transform * vec4(v_Pos, 0.0, 1.0);
+}
diff --git a/wgpu/src/shader/image.vert.spv b/wgpu/src/shader/image.vert.spv
index 9ba702bc..21f5db2d 100644
--- a/wgpu/src/shader/image.vert.spv
+++ b/wgpu/src/shader/image.vert.spv
Binary files differ
diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs
index ab9a2f71..c5670102 100644
--- a/wgpu/src/text.rs
+++ b/wgpu/src/text.rs
@@ -22,7 +22,11 @@ pub struct Pipeline {
}
impl Pipeline {
- pub fn new(device: &mut wgpu::Device, default_font: Option<&[u8]>) -> Self {
+ pub fn new(
+ device: &mut wgpu::Device,
+ format: wgpu::TextureFormat,
+ default_font: Option<&[u8]>,
+ ) -> Self {
// TODO: Font customization
let font_source = font::Source::new();
@@ -54,7 +58,7 @@ impl Pipeline {
let draw_brush = brush_builder
.initial_cache_size((2048, 2048))
- .build(device, wgpu::TextureFormat::Bgra8UnormSrgb);
+ .build(device, format);
Pipeline {
draw_brush: RefCell::new(draw_brush),
diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs
index d149eebc..51a6f954 100644
--- a/wgpu/src/triangle.rs
+++ b/wgpu/src/triangle.rs
@@ -1,7 +1,7 @@
//! Draw meshes of triangles.
use crate::{settings, Transformation};
use iced_native::{Point, Rectangle};
-use std::{mem, sync::Arc};
+use std::mem;
mod msaa;
@@ -61,6 +61,7 @@ impl<T> Buffer<T> {
impl Pipeline {
pub fn new(
device: &mut wgpu::Device,
+ format: wgpu::TextureFormat,
antialiasing: Option<settings::Antialiasing>,
) -> Pipeline {
let constant_layout =
@@ -127,7 +128,7 @@ impl Pipeline {
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
color_blend: wgpu::BlendDescriptor {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
@@ -169,7 +170,7 @@ impl Pipeline {
Pipeline {
pipeline,
- blit: antialiasing.map(|a| msaa::Blit::new(device, a)),
+ blit: antialiasing.map(|a| msaa::Blit::new(device, format, a)),
constants: constant_bind_group,
uniforms_buffer: constants_buffer,
vertex_buffer: Buffer::new(
@@ -193,7 +194,7 @@ impl Pipeline {
target_width: u32,
target_height: u32,
transformation: Transformation,
- meshes: &Vec<(Point, Arc<Mesh2D>)>,
+ meshes: &[(Point, &Mesh2D)],
bounds: Rectangle<u32>,
) {
// This looks a bit crazy, but we are just counting how many vertices
@@ -247,7 +248,7 @@ impl Pipeline {
&vertex_buffer,
0,
&self.vertex_buffer.raw,
- last_vertex as u64,
+ (std::mem::size_of::<Vertex2D>() * last_vertex) as u64,
(std::mem::size_of::<Vertex2D>() * mesh.vertices.len()) as u64,
);
@@ -255,7 +256,7 @@ impl Pipeline {
&index_buffer,
0,
&self.index_buffer.raw,
- last_index as u64,
+ (std::mem::size_of::<u32>() * last_index) as u64,
(std::mem::size_of::<u32>() * mesh.indices.len()) as u64,
);
@@ -312,26 +313,34 @@ impl Pipeline {
depth_stencil_attachment: None,
});
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.set_scissor_rect(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ );
+
for (i, (vertex_offset, index_offset, indices)) in
- offsets.drain(..).enumerate()
+ offsets.into_iter().enumerate()
{
- render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(
0,
&self.constants,
&[(std::mem::size_of::<Uniforms>() * i) as u64],
);
- render_pass
- .set_index_buffer(&self.index_buffer.raw, index_offset);
+
+ render_pass.set_index_buffer(
+ &self.index_buffer.raw,
+ index_offset * std::mem::size_of::<u32>() as u64,
+ );
+
render_pass.set_vertex_buffers(
0,
- &[(&self.vertex_buffer.raw, vertex_offset)],
- );
- render_pass.set_scissor_rect(
- bounds.x,
- bounds.y,
- bounds.width,
- bounds.height,
+ &[(
+ &self.vertex_buffer.raw,
+ vertex_offset * std::mem::size_of::<Vertex2D>() as u64,
+ )],
);
render_pass.draw_indexed(0..indices as u32, 0, 0..1);
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index 0def5352..7ccfb062 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -2,6 +2,7 @@ use crate::settings;
#[derive(Debug)]
pub struct Blit {
+ format: wgpu::TextureFormat,
pipeline: wgpu::RenderPipeline,
constants: wgpu::BindGroup,
texture_layout: wgpu::BindGroupLayout,
@@ -12,6 +13,7 @@ pub struct Blit {
impl Blit {
pub fn new(
device: &wgpu::Device,
+ format: wgpu::TextureFormat,
antialiasing: settings::Antialiasing,
) -> Blit {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@@ -93,7 +95,7 @@ impl Blit {
}),
primitive_topology: wgpu::PrimitiveTopology::TriangleList,
color_states: &[wgpu::ColorStateDescriptor {
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
color_blend: wgpu::BlendDescriptor {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
@@ -115,6 +117,7 @@ impl Blit {
});
Blit {
+ format,
pipeline,
constants: constant_bind_group,
texture_layout: texture_layout,
@@ -133,6 +136,7 @@ impl Blit {
None => {
self.targets = Some(Targets::new(
&device,
+ self.format,
&self.texture_layout,
self.sample_count,
width,
@@ -143,6 +147,7 @@ impl Blit {
if targets.width != width || targets.height != height {
self.targets = Some(Targets::new(
&device,
+ self.format,
&self.texture_layout,
self.sample_count,
width,
@@ -204,6 +209,7 @@ struct Targets {
impl Targets {
pub fn new(
device: &wgpu::Device,
+ format: wgpu::TextureFormat,
texture_layout: &wgpu::BindGroupLayout,
sample_count: u32,
width: u32,
@@ -221,7 +227,7 @@ impl Targets {
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
});
@@ -231,7 +237,7 @@ impl Targets {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT
| wgpu::TextureUsage::SAMPLED,
});
diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs
index 73cce7e2..b39f2d91 100644
--- a/wgpu/src/widget.rs
+++ b/wgpu/src/widget.rs
@@ -10,6 +10,7 @@
pub mod button;
pub mod checkbox;
pub mod container;
+pub mod pane_grid;
pub mod progress_bar;
pub mod radio;
pub mod scrollable;
@@ -23,6 +24,8 @@ pub use checkbox::Checkbox;
#[doc(no_inline)]
pub use container::Container;
#[doc(no_inline)]
+pub use pane_grid::PaneGrid;
+#[doc(no_inline)]
pub use progress_bar::ProgressBar;
#[doc(no_inline)]
pub use radio::Radio;
diff --git a/wgpu/src/widget/canvas.rs b/wgpu/src/widget/canvas.rs
index 38c1ce62..3a9605c9 100644
--- a/wgpu/src/widget/canvas.rs
+++ b/wgpu/src/widget/canvas.rs
@@ -20,6 +20,7 @@ mod drawable;
mod fill;
mod frame;
mod stroke;
+mod text;
pub use drawable::Drawable;
pub use fill::Fill;
@@ -27,6 +28,7 @@ pub use frame::Frame;
pub use layer::Layer;
pub use path::Path;
pub use stroke::{LineCap, LineJoin, Stroke};
+pub use text::Text;
/// A widget capable of drawing 2D graphics.
///
@@ -121,9 +123,9 @@ impl<'a, Message> Widget<Message, Renderer> for Canvas<'a> {
primitives: self
.layers
.iter()
- .map(|layer| Primitive::Mesh2D {
+ .map(|layer| Primitive::Cached {
origin,
- buffers: layer.draw(size),
+ cache: layer.draw(size),
})
.collect(),
},
diff --git a/wgpu/src/widget/canvas/frame.rs b/wgpu/src/widget/canvas/frame.rs
index fa6d8c0a..7d7ce06a 100644
--- a/wgpu/src/widget/canvas/frame.rs
+++ b/wgpu/src/widget/canvas/frame.rs
@@ -1,8 +1,8 @@
-use iced_native::{Point, Size, Vector};
+use iced_native::{Point, Rectangle, Size, Vector};
use crate::{
- canvas::{Fill, Path, Stroke},
- triangle,
+ canvas::{Fill, Path, Stroke, Text},
+ triangle, Primitive,
};
/// The frame of a [`Canvas`].
@@ -13,6 +13,7 @@ pub struct Frame {
width: f32,
height: f32,
buffers: lyon::tessellation::VertexBuffers<triangle::Vertex2D, u32>,
+ primitives: Vec<Primitive>,
transforms: Transforms,
}
@@ -40,6 +41,7 @@ impl Frame {
width,
height,
buffers: lyon::tessellation::VertexBuffers::new(),
+ primitives: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform {
@@ -154,6 +156,52 @@ impl Frame {
let _ = result.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.
+ ///
+ /// [`Text`]: struct.Text.html
+ /// [`Frame`]: struct.Frame.html
+ pub fn fill_text(&mut self, text: Text) {
+ use std::f32;
+
+ 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.
///
@@ -209,13 +257,20 @@ impl Frame {
self.transforms.current.is_identity = false;
}
- /// Produces the geometry that has been drawn on the [`Frame`].
+ /// Produces the primitive representing everything drawn on the [`Frame`].
///
/// [`Frame`]: struct.Frame.html
- pub fn into_mesh(self) -> triangle::Mesh2D {
- triangle::Mesh2D {
- vertices: self.buffers.vertices,
- indices: self.buffers.indices,
+ pub fn into_primitive(mut self) -> Primitive {
+ self.primitives.push(Primitive::Mesh2D {
+ origin: Point::ORIGIN,
+ buffers: triangle::Mesh2D {
+ vertices: self.buffers.vertices,
+ indices: self.buffers.indices,
+ },
+ });
+
+ Primitive::Group {
+ primitives: self.primitives,
}
}
}
diff --git a/wgpu/src/widget/canvas/layer.rs b/wgpu/src/widget/canvas/layer.rs
index 82d647bb..a46b7fb1 100644
--- a/wgpu/src/widget/canvas/layer.rs
+++ b/wgpu/src/widget/canvas/layer.rs
@@ -3,23 +3,23 @@ mod cache;
pub use cache::Cache;
-use crate::triangle;
-
+use crate::Primitive;
use iced_native::Size;
+
use std::sync::Arc;
/// A layer that can be presented at a [`Canvas`].
///
/// [`Canvas`]: ../struct.Canvas.html
pub trait Layer: std::fmt::Debug {
- /// Draws the [`Layer`] in the given bounds and produces [`Mesh2D`] as a
- /// result.
+ /// Draws the [`Layer`] in the given bounds and produces a [`Primitive`] as
+ /// a result.
///
- /// The [`Layer`] may choose to store the produced [`Mesh2D`] locally and
+ /// The [`Layer`] may choose to store the produced [`Primitive`] locally and
/// only recompute it when the bounds change, its contents change, or is
/// otherwise explicitly cleared by other means.
///
/// [`Layer`]: trait.Layer.html
- /// [`Mesh2D`]: ../../../triangle/struct.Mesh2D.html
- fn draw(&self, bounds: Size) -> Arc<triangle::Mesh2D>;
+ /// [`Primitive`]: ../../../enum.Primitive.html
+ fn draw(&self, bounds: Size) -> Arc<Primitive>;
}
diff --git a/wgpu/src/widget/canvas/layer/cache.rs b/wgpu/src/widget/canvas/layer/cache.rs
index 3071cce0..f7002459 100644
--- a/wgpu/src/widget/canvas/layer/cache.rs
+++ b/wgpu/src/widget/canvas/layer/cache.rs
@@ -1,12 +1,10 @@
use crate::{
canvas::{Drawable, Frame, Layer},
- triangle,
+ Primitive,
};
use iced_native::Size;
-use std::cell::RefCell;
-use std::marker::PhantomData;
-use std::sync::Arc;
+use std::{cell::RefCell, marker::PhantomData, sync::Arc};
/// A simple cache that stores generated geometry to avoid recomputation.
///
@@ -21,12 +19,11 @@ pub struct Cache<T: Drawable> {
state: RefCell<State>,
}
-#[derive(Debug)]
enum State {
Empty,
Filled {
- mesh: Arc<triangle::Mesh2D>,
bounds: Size,
+ primitive: Arc<Primitive>,
},
}
@@ -75,27 +72,40 @@ impl<'a, T> Layer for Bind<'a, T>
where
T: Drawable + std::fmt::Debug,
{
- fn draw(&self, current_bounds: Size) -> Arc<triangle::Mesh2D> {
+ fn draw(&self, current_bounds: Size) -> Arc<Primitive> {
use std::ops::Deref;
- if let State::Filled { mesh, bounds } =
+ if let State::Filled { bounds, primitive } =
self.cache.state.borrow().deref()
{
if *bounds == current_bounds {
- return mesh.clone();
+ return primitive.clone();
}
}
let mut frame = Frame::new(current_bounds.width, current_bounds.height);
self.input.draw(&mut frame);
- let mesh = Arc::new(frame.into_mesh());
+ let primitive = Arc::new(frame.into_primitive());
*self.cache.state.borrow_mut() = State::Filled {
- mesh: mesh.clone(),
bounds: current_bounds,
+ primitive: primitive.clone(),
};
- mesh
+ 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/text.rs b/wgpu/src/widget/canvas/text.rs
new file mode 100644
index 00000000..d1cf1a0f
--- /dev/null
+++ b/wgpu/src/widget/canvas/text.rs
@@ -0,0 +1,34 @@
+use iced_native::{Color, Font, HorizontalAlignment, Point, VerticalAlignment};
+
+/// 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 where to begin drawing the text (top-left corner coordinates)
+ 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: HorizontalAlignment,
+ /// The vertical alignment of the text
+ pub vertical_alignment: VerticalAlignment,
+}
+
+impl Default for Text {
+ fn default() -> Text {
+ Text {
+ content: String::new(),
+ position: Point::ORIGIN,
+ color: Color::BLACK,
+ size: 16.0,
+ font: Font::Default,
+ horizontal_alignment: HorizontalAlignment::Left,
+ vertical_alignment: VerticalAlignment::Top,
+ }
+ }
+}
diff --git a/wgpu/src/widget/pane_grid.rs b/wgpu/src/widget/pane_grid.rs
new file mode 100644
index 00000000..7bc2f7c5
--- /dev/null
+++ b/wgpu/src/widget/pane_grid.rs
@@ -0,0 +1,17 @@
+//! 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)
+use crate::Renderer;
+
+pub use iced_native::pane_grid::{
+ Axis, Direction, DragEvent, Focus, KeyPressEvent, Pane, ResizeEvent, Split,
+ State,
+};
+
+/// A collection of panes distributed using either vertical or horizontal splits
+/// to completely fill the space available.
+///
+/// [![Pane grid - Iced](https://thumbs.gfycat.com/MixedFlatJellyfish-small.gif)](https://gfycat.com/mixedflatjellyfish)
+///
+/// This is an alias of an `iced_native` pane grid with an `iced_wgpu::Renderer`.
+pub type PaneGrid<'a, Message> = iced_native::PaneGrid<'a, Message, Renderer>;
diff --git a/wgpu/src/window/backend.rs b/wgpu/src/window/backend.rs
index 4c9f289b..5b269f36 100644
--- a/wgpu/src/window/backend.rs
+++ b/wgpu/src/window/backend.rs
@@ -8,6 +8,7 @@ use raw_window_handle::HasRawWindowHandle;
pub struct Backend {
device: wgpu::Device,
queue: wgpu::Queue,
+ format: wgpu::TextureFormat,
}
impl iced_native::window::Backend for Backend {
@@ -37,7 +38,14 @@ impl iced_native::window::Backend for Backend {
let renderer = Renderer::new(&mut device, settings);
- (Backend { device, queue }, renderer)
+ (
+ Backend {
+ device,
+ queue,
+ format: settings.format,
+ },
+ renderer,
+ )
}
fn create_surface<W: HasRawWindowHandle>(
@@ -53,7 +61,7 @@ impl iced_native::window::Backend for Backend {
width: u32,
height: u32,
) -> SwapChain {
- SwapChain::new(&self.device, surface, width, height)
+ SwapChain::new(&self.device, surface, self.format, width, height)
}
fn draw<T: AsRef<str>>(
diff --git a/wgpu/src/window/swap_chain.rs b/wgpu/src/window/swap_chain.rs
index 6f545fce..4ca2901b 100644
--- a/wgpu/src/window/swap_chain.rs
+++ b/wgpu/src/window/swap_chain.rs
@@ -18,11 +18,12 @@ impl SwapChain {
pub fn new(
device: &wgpu::Device,
surface: &wgpu::Surface,
+ format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> SwapChain {
SwapChain {
- raw: new_swap_chain(surface, width, height, device),
+ raw: new_swap_chain(surface, format, width, height, device),
viewport: Viewport::new(width, height),
}
}
@@ -38,6 +39,7 @@ impl SwapChain {
fn new_swap_chain(
surface: &wgpu::Surface,
+ format: wgpu::TextureFormat,
width: u32,
height: u32,
device: &wgpu::Device,
@@ -46,7 +48,7 @@ fn new_swap_chain(
&surface,
&wgpu::SwapChainDescriptor {
usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT,
- format: wgpu::TextureFormat::Bgra8UnormSrgb,
+ format,
width,
height,
present_mode: wgpu::PresentMode::Vsync,
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index 63df1d63..ca2018c7 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -14,16 +14,13 @@ categories = ["gui"]
debug = []
[dependencies]
-winit = "0.21"
+winit = "0.22"
+window_clipboard = "0.1"
log = "0.4"
[dependencies.iced_native]
version = "0.1.0-alpha"
path = "../native"
-[dependencies.window_clipboard]
-git = "https://github.com/hecrj/window_clipboard"
-rev = "22c6dd6c04cd05d528029b50a30c56417cd4bebf"
-
[target.'cfg(target_os = "windows")'.dependencies.winapi]
version = "0.3.6"
diff --git a/winit/src/application.rs b/winit/src/application.rs
index f5aa799c..891b8f12 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -347,6 +347,9 @@ pub trait Application: Sized {
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
+ WindowEvent::ModifiersChanged(new_modifiers) => {
+ modifiers = new_modifiers;
+ }
#[cfg(target_os = "macos")]
WindowEvent::KeyboardInput {
input:
@@ -382,12 +385,6 @@ pub trait Application: Sized {
events.push(event);
}
}
- event::Event::DeviceEvent {
- event: event::DeviceEvent::ModifiersChanged(new_modifiers),
- ..
- } => {
- modifiers = new_modifiers;
- }
_ => {
*control_flow = ControlFlow::Wait;
}
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index b6a0b64b..74852876 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -116,6 +116,10 @@ pub fn mouse_cursor(mouse_cursor: MouseCursor) -> winit::window::CursorIcon {
MouseCursor::Grab => winit::window::CursorIcon::Grab,
MouseCursor::Grabbing => winit::window::CursorIcon::Grabbing,
MouseCursor::Text => winit::window::CursorIcon::Text,
+ MouseCursor::ResizingHorizontally => {
+ winit::window::CursorIcon::EwResize
+ }
+ MouseCursor::ResizingVertically => winit::window::CursorIcon::NsResize,
}
}