summaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/clock/src/main.rs2
-rw-r--r--examples/color_palette/src/main.rs2
-rw-r--r--examples/pure/component/Cargo.toml12
-rw-r--r--examples/pure/component/src/main.rs166
-rw-r--r--examples/pure/counter/Cargo.toml9
-rw-r--r--examples/pure/counter/src/main.rs49
-rw-r--r--examples/pure/game_of_life/Cargo.toml13
-rw-r--r--examples/pure/game_of_life/README.md22
-rw-r--r--examples/pure/game_of_life/src/main.rs898
-rw-r--r--examples/pure/game_of_life/src/preset.rs142
-rw-r--r--examples/pure/game_of_life/src/style.rs186
-rw-r--r--examples/pure/pane_grid/Cargo.toml11
-rw-r--r--examples/pure/pane_grid/src/main.rs436
-rw-r--r--examples/pure/pick_list/Cargo.toml9
-rw-r--r--examples/pure/pick_list/src/main.rs109
-rw-r--r--examples/pure/todos/Cargo.toml19
-rw-r--r--examples/pure/todos/src/main.rs608
-rw-r--r--examples/pure/tour/Cargo.toml10
-rw-r--r--examples/pure/tour/src/main.rs703
-rw-r--r--examples/stopwatch/src/main.rs2
-rw-r--r--examples/tour/src/main.rs2
21 files changed, 3406 insertions, 4 deletions
diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs
index 325ccc1a..3b8a1d6a 100644
--- a/examples/clock/src/main.rs
+++ b/examples/clock/src/main.rs
@@ -76,7 +76,7 @@ impl Application for Clock {
}
}
-impl canvas::Program<Message> for Clock {
+impl<Message> canvas::Program<Message> for Clock {
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
let clock = self.clock.draw(bounds.size(), |frame| {
let center = frame.center();
diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs
index 682dc65b..f5fab251 100644
--- a/examples/color_palette/src/main.rs
+++ b/examples/color_palette/src/main.rs
@@ -235,7 +235,7 @@ impl Theme {
}
}
-impl canvas::Program<Message> for Theme {
+impl<Message> canvas::Program<Message> for Theme {
fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec<Geometry> {
let theme = self.canvas_cache.draw(bounds.size(), |frame| {
self.draw(frame);
diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml
new file mode 100644
index 00000000..b6c7a513
--- /dev/null
+++ b/examples/pure/component/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "pure_component"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["debug", "pure"] }
+iced_native = { path = "../../../native" }
+iced_lazy = { path = "../../../lazy", features = ["pure"] }
+iced_pure = { path = "../../../pure" }
diff --git a/examples/pure/component/src/main.rs b/examples/pure/component/src/main.rs
new file mode 100644
index 00000000..b38d6fca
--- /dev/null
+++ b/examples/pure/component/src/main.rs
@@ -0,0 +1,166 @@
+use iced::pure::container;
+use iced::pure::{Element, Sandbox};
+use iced::{Length, Settings};
+
+use numeric_input::numeric_input;
+
+pub fn main() -> iced::Result {
+ Component::run(Settings::default())
+}
+
+#[derive(Default)]
+struct Component {
+ value: Option<u32>,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ NumericInputChanged(Option<u32>),
+}
+
+impl Sandbox for Component {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self::default()
+ }
+
+ fn title(&self) -> String {
+ String::from("Component - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::NumericInputChanged(value) => {
+ self.value = value;
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ container(numeric_input(self.value, Message::NumericInputChanged))
+ .padding(20)
+ .height(Length::Fill)
+ .center_y()
+ .into()
+ }
+}
+
+mod numeric_input {
+ use iced::pure::{button, row, text, text_input};
+ use iced_lazy::pure::{self, Component};
+ use iced_native::alignment::{self, Alignment};
+ use iced_native::text;
+ use iced_native::Length;
+ use iced_pure::Element;
+
+ pub struct NumericInput<Message> {
+ value: Option<u32>,
+ on_change: Box<dyn Fn(Option<u32>) -> Message>,
+ }
+
+ pub fn numeric_input<Message>(
+ value: Option<u32>,
+ on_change: impl Fn(Option<u32>) -> Message + 'static,
+ ) -> NumericInput<Message> {
+ NumericInput::new(value, on_change)
+ }
+
+ #[derive(Debug, Clone)]
+ pub enum Event {
+ InputChanged(String),
+ IncrementPressed,
+ DecrementPressed,
+ }
+
+ impl<Message> NumericInput<Message> {
+ pub fn new(
+ value: Option<u32>,
+ on_change: impl Fn(Option<u32>) -> Message + 'static,
+ ) -> Self {
+ Self {
+ value,
+ on_change: Box::new(on_change),
+ }
+ }
+ }
+
+ impl<Message, Renderer> Component<Message, Renderer> for NumericInput<Message>
+ where
+ Renderer: text::Renderer + 'static,
+ {
+ type State = ();
+ type Event = Event;
+
+ fn update(
+ &mut self,
+ _state: &mut Self::State,
+ event: Event,
+ ) -> Option<Message> {
+ match event {
+ Event::IncrementPressed => Some((self.on_change)(Some(
+ self.value.unwrap_or_default().saturating_add(1),
+ ))),
+ Event::DecrementPressed => Some((self.on_change)(Some(
+ self.value.unwrap_or_default().saturating_sub(1),
+ ))),
+ Event::InputChanged(value) => {
+ if value.is_empty() {
+ Some((self.on_change)(None))
+ } else {
+ value
+ .parse()
+ .ok()
+ .map(Some)
+ .map(self.on_change.as_ref())
+ }
+ }
+ }
+ }
+
+ fn view(&self, _state: &Self::State) -> Element<Event, Renderer> {
+ let button = |label, on_press| {
+ button(
+ text(label)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center)
+ .vertical_alignment(alignment::Vertical::Center),
+ )
+ .width(Length::Units(50))
+ .on_press(on_press)
+ };
+
+ row()
+ .push(button("-", Event::DecrementPressed))
+ .push(
+ text_input(
+ "Type a number",
+ self.value
+ .as_ref()
+ .map(u32::to_string)
+ .as_ref()
+ .map(String::as_str)
+ .unwrap_or(""),
+ Event::InputChanged,
+ )
+ .padding(10),
+ )
+ .push(button("+", Event::IncrementPressed))
+ .align_items(Alignment::Fill)
+ .spacing(10)
+ .into()
+ }
+ }
+
+ impl<'a, Message, Renderer> From<NumericInput<Message>>
+ for Element<'a, Message, Renderer>
+ where
+ Message: 'a,
+ Renderer: 'static + text::Renderer,
+ {
+ fn from(numeric_input: NumericInput<Message>) -> Self {
+ pure::component(numeric_input)
+ }
+ }
+}
diff --git a/examples/pure/counter/Cargo.toml b/examples/pure/counter/Cargo.toml
new file mode 100644
index 00000000..2fcd22d4
--- /dev/null
+++ b/examples/pure/counter/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "pure_counter"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["pure"] }
diff --git a/examples/pure/counter/src/main.rs b/examples/pure/counter/src/main.rs
new file mode 100644
index 00000000..726009df
--- /dev/null
+++ b/examples/pure/counter/src/main.rs
@@ -0,0 +1,49 @@
+use iced::pure::{button, column, text, Element, Sandbox};
+use iced::{Alignment, Settings};
+
+pub fn main() -> iced::Result {
+ Counter::run(Settings::default())
+}
+
+struct Counter {
+ value: i32,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ IncrementPressed,
+ DecrementPressed,
+}
+
+impl Sandbox for Counter {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self { value: 0 }
+ }
+
+ fn title(&self) -> String {
+ String::from("Counter - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::IncrementPressed => {
+ self.value += 1;
+ }
+ Message::DecrementPressed => {
+ self.value -= 1;
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ column()
+ .padding(20)
+ .align_items(Alignment::Center)
+ .push(button("Increment").on_press(Message::IncrementPressed))
+ .push(text(self.value.to_string()).size(50))
+ .push(button("Decrement").on_press(Message::DecrementPressed))
+ .into()
+ }
+}
diff --git a/examples/pure/game_of_life/Cargo.toml b/examples/pure/game_of_life/Cargo.toml
new file mode 100644
index 00000000..22e38f00
--- /dev/null
+++ b/examples/pure/game_of_life/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "pure_game_of_life"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["pure", "canvas", "tokio", "debug"] }
+tokio = { version = "1.0", features = ["sync"] }
+itertools = "0.9"
+rustc-hash = "1.1"
+env_logger = "0.9"
diff --git a/examples/pure/game_of_life/README.md b/examples/pure/game_of_life/README.md
new file mode 100644
index 00000000..aa39201c
--- /dev/null
+++ b/examples/pure/game_of_life/README.md
@@ -0,0 +1,22 @@
+## Game of Life
+
+An interactive version of the [Game of Life], invented by [John Horton Conway].
+
+It runs a simulation in a background thread while allowing interaction with a `Canvas` that displays an infinite grid with zooming, panning, and drawing support.
+
+The __[`main`]__ file contains the relevant code of the example.
+
+<div align="center">
+ <a href="https://gfycat.com/WhichPaltryChick">
+ <img src="https://thumbs.gfycat.com/WhichPaltryChick-size_restricted.gif">
+ </a>
+</div>
+
+You can run it with `cargo run`:
+```
+cargo run --package game_of_life
+```
+
+[`main`]: src/main.rs
+[Game of Life]: https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life
+[John Horton Conway]: https://en.wikipedia.org/wiki/John_Horton_Conway
diff --git a/examples/pure/game_of_life/src/main.rs b/examples/pure/game_of_life/src/main.rs
new file mode 100644
index 00000000..a3164701
--- /dev/null
+++ b/examples/pure/game_of_life/src/main.rs
@@ -0,0 +1,898 @@
+//! This example showcases an interactive version of the Game of Life, invented
+//! by John Conway. It leverages a `Canvas` together with other widgets.
+mod preset;
+mod style;
+
+use grid::Grid;
+use iced::executor;
+use iced::pure::{
+ button, checkbox, column, container, pick_list, row, slider, text,
+};
+use iced::pure::{Application, Element};
+use iced::time;
+use iced::window;
+use iced::{Alignment, Color, Command, Length, Settings, Subscription};
+use preset::Preset;
+use std::time::{Duration, Instant};
+
+pub fn main() -> iced::Result {
+ env_logger::builder().format_timestamp(None).init();
+
+ GameOfLife::run(Settings {
+ antialiasing: true,
+ window: window::Settings {
+ position: window::Position::Centered,
+ ..window::Settings::default()
+ },
+ ..Settings::default()
+ })
+}
+
+#[derive(Default)]
+struct GameOfLife {
+ grid: Grid,
+ is_playing: bool,
+ queued_ticks: usize,
+ speed: usize,
+ next_speed: Option<usize>,
+ version: usize,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Grid(grid::Message, usize),
+ Tick(Instant),
+ TogglePlayback,
+ ToggleGrid(bool),
+ Next,
+ Clear,
+ SpeedChanged(f32),
+ PresetPicked(Preset),
+}
+
+impl Application for GameOfLife {
+ type Message = Message;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Self, Command<Message>) {
+ (
+ Self {
+ speed: 5,
+ ..Self::default()
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Game of Life - Iced")
+ }
+
+ fn background_color(&self) -> Color {
+ style::BACKGROUND
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::Grid(message, version) => {
+ if version == self.version {
+ self.grid.update(message);
+ }
+ }
+ Message::Tick(_) | Message::Next => {
+ self.queued_ticks = (self.queued_ticks + 1).min(self.speed);
+
+ if let Some(task) = self.grid.tick(self.queued_ticks) {
+ if let Some(speed) = self.next_speed.take() {
+ self.speed = speed;
+ }
+
+ self.queued_ticks = 0;
+
+ let version = self.version;
+
+ return Command::perform(task, move |message| {
+ Message::Grid(message, version)
+ });
+ }
+ }
+ Message::TogglePlayback => {
+ self.is_playing = !self.is_playing;
+ }
+ Message::ToggleGrid(show_grid_lines) => {
+ self.grid.toggle_lines(show_grid_lines);
+ }
+ Message::Clear => {
+ self.grid.clear();
+ self.version += 1;
+ }
+ Message::SpeedChanged(speed) => {
+ if self.is_playing {
+ self.next_speed = Some(speed.round() as usize);
+ } else {
+ self.speed = speed.round() as usize;
+ }
+ }
+ Message::PresetPicked(new_preset) => {
+ self.grid = Grid::from_preset(new_preset);
+ self.version += 1;
+ }
+ }
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ if self.is_playing {
+ time::every(Duration::from_millis(1000 / self.speed as u64))
+ .map(Message::Tick)
+ } else {
+ Subscription::none()
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ let version = self.version;
+ let selected_speed = self.next_speed.unwrap_or(self.speed);
+ let controls = view_controls(
+ self.is_playing,
+ self.grid.are_lines_visible(),
+ selected_speed,
+ self.grid.preset(),
+ );
+
+ let content = column()
+ .push(
+ self.grid
+ .view()
+ .map(move |message| Message::Grid(message, version)),
+ )
+ .push(controls);
+
+ container(content)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .style(style::Container)
+ .into()
+ }
+}
+
+fn view_controls<'a>(
+ is_playing: bool,
+ is_grid_enabled: bool,
+ speed: usize,
+ preset: Preset,
+) -> Element<'a, Message> {
+ let playback_controls = row()
+ .spacing(10)
+ .push(
+ button(if is_playing { "Pause" } else { "Play" })
+ .on_press(Message::TogglePlayback)
+ .style(style::Button),
+ )
+ .push(button("Next").on_press(Message::Next).style(style::Button));
+
+ let speed_controls = row()
+ .width(Length::Fill)
+ .align_items(Alignment::Center)
+ .spacing(10)
+ .push(
+ slider(1.0..=1000.0, speed as f32, Message::SpeedChanged)
+ .style(style::Slider),
+ )
+ .push(text(format!("x{}", speed)).size(16));
+
+ row()
+ .padding(10)
+ .spacing(20)
+ .align_items(Alignment::Center)
+ .push(playback_controls)
+ .push(speed_controls)
+ .push(
+ checkbox("Grid", is_grid_enabled, Message::ToggleGrid)
+ .size(16)
+ .spacing(5)
+ .text_size(16),
+ )
+ .push(
+ pick_list(preset::ALL, Some(preset), Message::PresetPicked)
+ .padding(8)
+ .text_size(16)
+ .style(style::PickList),
+ )
+ .push(button("Clear").on_press(Message::Clear).style(style::Clear))
+ .into()
+}
+
+mod grid {
+ use crate::Preset;
+ use iced::pure::widget::canvas::event::{self, Event};
+ use iced::pure::widget::canvas::{
+ self, Cache, Canvas, Cursor, Frame, Geometry, Path, Text,
+ };
+ use iced::pure::Element;
+ use iced::{
+ alignment, mouse, Color, Length, Point, Rectangle, Size, Vector,
+ };
+ use rustc_hash::{FxHashMap, FxHashSet};
+ use std::future::Future;
+ use std::ops::RangeInclusive;
+ use std::time::{Duration, Instant};
+
+ pub struct Grid {
+ state: State,
+ preset: Preset,
+ life_cache: Cache,
+ grid_cache: Cache,
+ translation: Vector,
+ scaling: f32,
+ show_lines: bool,
+ last_tick_duration: Duration,
+ last_queued_ticks: usize,
+ }
+
+ #[derive(Debug, Clone)]
+ pub enum Message {
+ Populate(Cell),
+ Unpopulate(Cell),
+ Translated(Vector),
+ Scaled(f32, Option<Vector>),
+ Ticked {
+ result: Result<Life, TickError>,
+ tick_duration: Duration,
+ },
+ }
+
+ #[derive(Debug, Clone)]
+ pub enum TickError {
+ JoinFailed,
+ }
+
+ impl Default for Grid {
+ fn default() -> Self {
+ Self::from_preset(Preset::default())
+ }
+ }
+
+ impl Grid {
+ const MIN_SCALING: f32 = 0.1;
+ const MAX_SCALING: f32 = 2.0;
+
+ pub fn from_preset(preset: Preset) -> Self {
+ Self {
+ state: State::with_life(
+ preset
+ .life()
+ .into_iter()
+ .map(|(i, j)| Cell { i, j })
+ .collect(),
+ ),
+ preset,
+ life_cache: Cache::default(),
+ grid_cache: Cache::default(),
+ translation: Vector::default(),
+ scaling: 1.0,
+ show_lines: true,
+ last_tick_duration: Duration::default(),
+ last_queued_ticks: 0,
+ }
+ }
+
+ pub fn tick(
+ &mut self,
+ amount: usize,
+ ) -> Option<impl Future<Output = Message>> {
+ let tick = self.state.tick(amount)?;
+
+ self.last_queued_ticks = amount;
+
+ Some(async move {
+ let start = Instant::now();
+ let result = tick.await;
+ let tick_duration = start.elapsed() / amount as u32;
+
+ Message::Ticked {
+ result,
+ tick_duration,
+ }
+ })
+ }
+
+ pub fn update(&mut self, message: Message) {
+ match message {
+ Message::Populate(cell) => {
+ self.state.populate(cell);
+ self.life_cache.clear();
+
+ self.preset = Preset::Custom;
+ }
+ Message::Unpopulate(cell) => {
+ self.state.unpopulate(&cell);
+ self.life_cache.clear();
+
+ self.preset = Preset::Custom;
+ }
+ Message::Translated(translation) => {
+ self.translation = translation;
+
+ self.life_cache.clear();
+ self.grid_cache.clear();
+ }
+ Message::Scaled(scaling, translation) => {
+ self.scaling = scaling;
+
+ if let Some(translation) = translation {
+ self.translation = translation;
+ }
+
+ self.life_cache.clear();
+ self.grid_cache.clear();
+ }
+ Message::Ticked {
+ result: Ok(life),
+ tick_duration,
+ } => {
+ self.state.update(life);
+ self.life_cache.clear();
+
+ self.last_tick_duration = tick_duration;
+ }
+ Message::Ticked {
+ result: Err(error), ..
+ } => {
+ dbg!(error);
+ }
+ }
+ }
+
+ pub fn view<'a>(&'a self) -> Element<'a, Message> {
+ Canvas::new(self)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ }
+
+ pub fn clear(&mut self) {
+ self.state = State::default();
+ self.preset = Preset::Custom;
+
+ self.life_cache.clear();
+ }
+
+ pub fn preset(&self) -> Preset {
+ self.preset
+ }
+
+ pub fn toggle_lines(&mut self, enabled: bool) {
+ self.show_lines = enabled;
+ }
+
+ pub fn are_lines_visible(&self) -> bool {
+ self.show_lines
+ }
+
+ fn visible_region(&self, size: Size) -> Region {
+ let width = size.width / self.scaling;
+ let height = size.height / self.scaling;
+
+ Region {
+ x: -self.translation.x - width / 2.0,
+ y: -self.translation.y - height / 2.0,
+ width,
+ height,
+ }
+ }
+
+ fn project(&self, position: Point, size: Size) -> Point {
+ let region = self.visible_region(size);
+
+ Point::new(
+ position.x / self.scaling + region.x,
+ position.y / self.scaling + region.y,
+ )
+ }
+ }
+
+ impl canvas::Program<Message> for Grid {
+ type State = Interaction;
+
+ fn update(
+ &self,
+ interaction: &mut Interaction,
+ event: Event,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> (event::Status, Option<Message>) {
+ if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event {
+ *interaction = Interaction::None;
+ }
+
+ let cursor_position =
+ if let Some(position) = cursor.position_in(&bounds) {
+ position
+ } else {
+ return (event::Status::Ignored, None);
+ };
+
+ let cell = Cell::at(self.project(cursor_position, bounds.size()));
+ let is_populated = self.state.contains(&cell);
+
+ let (populate, unpopulate) = if is_populated {
+ (None, Some(Message::Unpopulate(cell)))
+ } else {
+ (Some(Message::Populate(cell)), None)
+ };
+
+ match event {
+ Event::Mouse(mouse_event) => match mouse_event {
+ mouse::Event::ButtonPressed(button) => {
+ let message = match button {
+ mouse::Button::Left => {
+ *interaction = if is_populated {
+ Interaction::Erasing
+ } else {
+ Interaction::Drawing
+ };
+
+ populate.or(unpopulate)
+ }
+ mouse::Button::Right => {
+ *interaction = Interaction::Panning {
+ translation: self.translation,
+ start: cursor_position,
+ };
+
+ None
+ }
+ _ => None,
+ };
+
+ (event::Status::Captured, message)
+ }
+ mouse::Event::CursorMoved { .. } => {
+ let message = match *interaction {
+ Interaction::Drawing => populate,
+ Interaction::Erasing => unpopulate,
+ Interaction::Panning { translation, start } => {
+ Some(Message::Translated(
+ translation
+ + (cursor_position - start)
+ * (1.0 / self.scaling),
+ ))
+ }
+ _ => None,
+ };
+
+ let event_status = match interaction {
+ Interaction::None => event::Status::Ignored,
+ _ => event::Status::Captured,
+ };
+
+ (event_status, message)
+ }
+ mouse::Event::WheelScrolled { delta } => match delta {
+ mouse::ScrollDelta::Lines { y, .. }
+ | mouse::ScrollDelta::Pixels { y, .. } => {
+ if y < 0.0 && self.scaling > Self::MIN_SCALING
+ || y > 0.0 && self.scaling < Self::MAX_SCALING
+ {
+ let old_scaling = self.scaling;
+
+ let scaling = (self.scaling * (1.0 + y / 30.0))
+ .max(Self::MIN_SCALING)
+ .min(Self::MAX_SCALING);
+
+ let translation =
+ if let Some(cursor_to_center) =
+ cursor.position_from(bounds.center())
+ {
+ let factor = scaling - old_scaling;
+
+ Some(
+ self.translation
+ - Vector::new(
+ cursor_to_center.x * factor
+ / (old_scaling
+ * old_scaling),
+ cursor_to_center.y * factor
+ / (old_scaling
+ * old_scaling),
+ ),
+ )
+ } else {
+ None
+ };
+
+ (
+ event::Status::Captured,
+ Some(Message::Scaled(scaling, translation)),
+ )
+ } else {
+ (event::Status::Captured, None)
+ }
+ }
+ },
+ _ => (event::Status::Ignored, None),
+ },
+ _ => (event::Status::Ignored, None),
+ }
+ }
+
+ fn draw(
+ &self,
+ _interaction: &Interaction,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> Vec<Geometry> {
+ let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0);
+
+ let life = self.life_cache.draw(bounds.size(), |frame| {
+ let background = Path::rectangle(Point::ORIGIN, frame.size());
+ frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B));
+
+ frame.with_save(|frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
+
+ let region = self.visible_region(frame.size());
+
+ for cell in region.cull(self.state.cells()) {
+ frame.fill_rectangle(
+ Point::new(cell.j as f32, cell.i as f32),
+ Size::UNIT,
+ Color::WHITE,
+ );
+ }
+ });
+ });
+
+ let overlay = {
+ let mut frame = Frame::new(bounds.size());
+
+ let hovered_cell =
+ cursor.position_in(&bounds).map(|position| {
+ Cell::at(self.project(position, frame.size()))
+ });
+
+ if let Some(cell) = hovered_cell {
+ frame.with_save(|frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
+
+ frame.fill_rectangle(
+ Point::new(cell.j as f32, cell.i as f32),
+ Size::UNIT,
+ Color {
+ a: 0.5,
+ ..Color::BLACK
+ },
+ );
+ });
+ }
+
+ let text = Text {
+ color: Color::WHITE,
+ size: 14.0,
+ position: Point::new(frame.width(), frame.height()),
+ horizontal_alignment: alignment::Horizontal::Right,
+ vertical_alignment: alignment::Vertical::Bottom,
+ ..Text::default()
+ };
+
+ if let Some(cell) = hovered_cell {
+ frame.fill_text(Text {
+ content: format!("({}, {})", cell.j, cell.i),
+ position: text.position - Vector::new(0.0, 16.0),
+ ..text
+ });
+ }
+
+ let cell_count = self.state.cell_count();
+
+ frame.fill_text(Text {
+ content: format!(
+ "{} cell{} @ {:?} ({})",
+ cell_count,
+ if cell_count == 1 { "" } else { "s" },
+ self.last_tick_duration,
+ self.last_queued_ticks
+ ),
+ ..text
+ });
+
+ frame.into_geometry()
+ };
+
+ if self.scaling < 0.2 || !self.show_lines {
+ vec![life, overlay]
+ } else {
+ let grid = self.grid_cache.draw(bounds.size(), |frame| {
+ frame.translate(center);
+ frame.scale(self.scaling);
+ frame.translate(self.translation);
+ frame.scale(Cell::SIZE as f32);
+
+ let region = self.visible_region(frame.size());
+ let rows = region.rows();
+ let columns = region.columns();
+ let (total_rows, total_columns) =
+ (rows.clone().count(), columns.clone().count());
+ let width = 2.0 / Cell::SIZE as f32;
+ let color = Color::from_rgb8(70, 74, 83);
+
+ frame.translate(Vector::new(-width / 2.0, -width / 2.0));
+
+ for row in region.rows() {
+ frame.fill_rectangle(
+ Point::new(*columns.start() as f32, row as f32),
+ Size::new(total_columns as f32, width),
+ color,
+ );
+ }
+
+ for column in region.columns() {
+ frame.fill_rectangle(
+ Point::new(column as f32, *rows.start() as f32),
+ Size::new(width, total_rows as f32),
+ color,
+ );
+ }
+ });
+
+ vec![life, grid, overlay]
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ interaction: &Interaction,
+ bounds: Rectangle,
+ cursor: Cursor,
+ ) -> mouse::Interaction {
+ match interaction {
+ Interaction::Drawing => mouse::Interaction::Crosshair,
+ Interaction::Erasing => mouse::Interaction::Crosshair,
+ Interaction::Panning { .. } => mouse::Interaction::Grabbing,
+ Interaction::None if cursor.is_over(&bounds) => {
+ mouse::Interaction::Crosshair
+ }
+ _ => mouse::Interaction::default(),
+ }
+ }
+ }
+
+ #[derive(Default)]
+ struct State {
+ life: Life,
+ births: FxHashSet<Cell>,
+ is_ticking: bool,
+ }
+
+ impl State {
+ pub fn with_life(life: Life) -> Self {
+ Self {
+ life,
+ ..Self::default()
+ }
+ }
+
+ fn cell_count(&self) -> usize {
+ self.life.len() + self.births.len()
+ }
+
+ fn contains(&self, cell: &Cell) -> bool {
+ self.life.contains(cell) || self.births.contains(cell)
+ }
+
+ fn cells(&self) -> impl Iterator<Item = &Cell> {
+ self.life.iter().chain(self.births.iter())
+ }
+
+ fn populate(&mut self, cell: Cell) {
+ if self.is_ticking {
+ self.births.insert(cell);
+ } else {
+ self.life.populate(cell);
+ }
+ }
+
+ fn unpopulate(&mut self, cell: &Cell) {
+ if self.is_ticking {
+ let _ = self.births.remove(cell);
+ } else {
+ self.life.unpopulate(cell);
+ }
+ }
+
+ fn update(&mut self, mut life: Life) {
+ self.births.drain().for_each(|cell| life.populate(cell));
+
+ self.life = life;
+ self.is_ticking = false;
+ }
+
+ fn tick(
+ &mut self,
+ amount: usize,
+ ) -> Option<impl Future<Output = Result<Life, TickError>>> {
+ if self.is_ticking {
+ return None;
+ }
+
+ self.is_ticking = true;
+
+ let mut life = self.life.clone();
+
+ Some(async move {
+ tokio::task::spawn_blocking(move || {
+ for _ in 0..amount {
+ life.tick();
+ }
+
+ life
+ })
+ .await
+ .map_err(|_| TickError::JoinFailed)
+ })
+ }
+ }
+
+ #[derive(Clone, Default)]
+ pub struct Life {
+ cells: FxHashSet<Cell>,
+ }
+
+ impl Life {
+ fn len(&self) -> usize {
+ self.cells.len()
+ }
+
+ fn contains(&self, cell: &Cell) -> bool {
+ self.cells.contains(cell)
+ }
+
+ fn populate(&mut self, cell: Cell) {
+ self.cells.insert(cell);
+ }
+
+ fn unpopulate(&mut self, cell: &Cell) {
+ let _ = self.cells.remove(cell);
+ }
+
+ fn tick(&mut self) {
+ let mut adjacent_life = FxHashMap::default();
+
+ for cell in &self.cells {
+ let _ = adjacent_life.entry(*cell).or_insert(0);
+
+ for neighbor in Cell::neighbors(*cell) {
+ let amount = adjacent_life.entry(neighbor).or_insert(0);
+
+ *amount += 1;
+ }
+ }
+
+ for (cell, amount) in adjacent_life.iter() {
+ match amount {
+ 2 => {}
+ 3 => {
+ let _ = self.cells.insert(*cell);
+ }
+ _ => {
+ let _ = self.cells.remove(cell);
+ }
+ }
+ }
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = &Cell> {
+ self.cells.iter()
+ }
+ }
+
+ impl std::iter::FromIterator<Cell> for Life {
+ fn from_iter<I: IntoIterator<Item = Cell>>(iter: I) -> Self {
+ Life {
+ cells: iter.into_iter().collect(),
+ }
+ }
+ }
+
+ impl std::fmt::Debug for Life {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Life")
+ .field("cells", &self.cells.len())
+ .finish()
+ }
+ }
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+ pub struct Cell {
+ i: isize,
+ j: isize,
+ }
+
+ impl Cell {
+ const SIZE: usize = 20;
+
+ fn at(position: Point) -> Cell {
+ let i = (position.y / Cell::SIZE as f32).ceil() as isize;
+ let j = (position.x / Cell::SIZE as f32).ceil() as isize;
+
+ Cell {
+ i: i.saturating_sub(1),
+ j: j.saturating_sub(1),
+ }
+ }
+
+ fn cluster(cell: Cell) -> impl Iterator<Item = Cell> {
+ use itertools::Itertools;
+
+ let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1);
+ let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1);
+
+ rows.cartesian_product(columns).map(|(i, j)| Cell { i, j })
+ }
+
+ fn neighbors(cell: Cell) -> impl Iterator<Item = Cell> {
+ Cell::cluster(cell).filter(move |candidate| *candidate != cell)
+ }
+ }
+
+ pub struct Region {
+ x: f32,
+ y: f32,
+ width: f32,
+ height: f32,
+ }
+
+ impl Region {
+ fn rows(&self) -> RangeInclusive<isize> {
+ let first_row = (self.y / Cell::SIZE as f32).floor() as isize;
+
+ let visible_rows =
+ (self.height / Cell::SIZE as f32).ceil() as isize;
+
+ first_row..=first_row + visible_rows
+ }
+
+ fn columns(&self) -> RangeInclusive<isize> {
+ let first_column = (self.x / Cell::SIZE as f32).floor() as isize;
+
+ let visible_columns =
+ (self.width / Cell::SIZE as f32).ceil() as isize;
+
+ first_column..=first_column + visible_columns
+ }
+
+ fn cull<'a>(
+ &self,
+ cells: impl Iterator<Item = &'a Cell>,
+ ) -> impl Iterator<Item = &'a Cell> {
+ let rows = self.rows();
+ let columns = self.columns();
+
+ cells.filter(move |cell| {
+ rows.contains(&cell.i) && columns.contains(&cell.j)
+ })
+ }
+ }
+
+ pub enum Interaction {
+ None,
+ Drawing,
+ Erasing,
+ Panning { translation: Vector, start: Point },
+ }
+
+ impl Default for Interaction {
+ fn default() -> Self {
+ Self::None
+ }
+ }
+}
diff --git a/examples/pure/game_of_life/src/preset.rs b/examples/pure/game_of_life/src/preset.rs
new file mode 100644
index 00000000..05157b6a
--- /dev/null
+++ b/examples/pure/game_of_life/src/preset.rs
@@ -0,0 +1,142 @@
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Preset {
+ Custom,
+ XKCD,
+ Glider,
+ SmallExploder,
+ Exploder,
+ TenCellRow,
+ LightweightSpaceship,
+ Tumbler,
+ GliderGun,
+ Acorn,
+}
+
+pub static ALL: &[Preset] = &[
+ Preset::Custom,
+ Preset::XKCD,
+ Preset::Glider,
+ Preset::SmallExploder,
+ Preset::Exploder,
+ Preset::TenCellRow,
+ Preset::LightweightSpaceship,
+ Preset::Tumbler,
+ Preset::GliderGun,
+ Preset::Acorn,
+];
+
+impl Preset {
+ pub fn life(self) -> Vec<(isize, isize)> {
+ #[rustfmt::skip]
+ let cells = match self {
+ Preset::Custom => vec![],
+ Preset::XKCD => vec![
+ " xxx ",
+ " x x ",
+ " x x ",
+ " x ",
+ "x xxx ",
+ " x x x ",
+ " x x",
+ " x x ",
+ " x x ",
+ ],
+ Preset::Glider => vec![
+ " x ",
+ " x",
+ "xxx"
+ ],
+ Preset::SmallExploder => vec![
+ " x ",
+ "xxx",
+ "x x",
+ " x ",
+ ],
+ Preset::Exploder => vec![
+ "x x x",
+ "x x",
+ "x x",
+ "x x",
+ "x x x",
+ ],
+ Preset::TenCellRow => vec![
+ "xxxxxxxxxx",
+ ],
+ Preset::LightweightSpaceship => vec![
+ " xxxxx",
+ "x x",
+ " x",
+ "x x ",
+ ],
+ Preset::Tumbler => vec![
+ " xx xx ",
+ " xx xx ",
+ " x x ",
+ "x x x x",
+ "x x x x",
+ "xx xx",
+ ],
+ Preset::GliderGun => vec![
+ " x ",
+ " x x ",
+ " xx xx xx",
+ " x x xx xx",
+ "xx x x xx ",
+ "xx x x xx x x ",
+ " x x x ",
+ " x x ",
+ " xx ",
+ ],
+ Preset::Acorn => vec![
+ " x ",
+ " x ",
+ "xx xxx",
+ ],
+ };
+
+ let start_row = -(cells.len() as isize / 2);
+
+ cells
+ .into_iter()
+ .enumerate()
+ .flat_map(|(i, cells)| {
+ let start_column = -(cells.len() as isize / 2);
+
+ cells
+ .chars()
+ .enumerate()
+ .filter(|(_, c)| !c.is_whitespace())
+ .map(move |(j, _)| {
+ (start_row + i as isize, start_column + j as isize)
+ })
+ })
+ .collect()
+ }
+}
+
+impl Default for Preset {
+ fn default() -> Preset {
+ Preset::XKCD
+ }
+}
+
+impl std::fmt::Display for Preset {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Preset::Custom => "Custom",
+ Preset::XKCD => "xkcd #2293",
+ Preset::Glider => "Glider",
+ Preset::SmallExploder => "Small Exploder",
+ Preset::Exploder => "Exploder",
+ Preset::TenCellRow => "10 Cell Row",
+ Preset::LightweightSpaceship => "Lightweight spaceship",
+ Preset::Tumbler => "Tumbler",
+ Preset::GliderGun => "Gosper Glider Gun",
+ Preset::Acorn => "Acorn",
+ }
+ )
+ }
+}
diff --git a/examples/pure/game_of_life/src/style.rs b/examples/pure/game_of_life/src/style.rs
new file mode 100644
index 00000000..1a64cf4a
--- /dev/null
+++ b/examples/pure/game_of_life/src/style.rs
@@ -0,0 +1,186 @@
+use iced::{button, container, pick_list, slider, Background, Color};
+
+const ACTIVE: Color = Color::from_rgb(
+ 0x72 as f32 / 255.0,
+ 0x89 as f32 / 255.0,
+ 0xDA as f32 / 255.0,
+);
+
+const DESTRUCTIVE: Color = Color::from_rgb(
+ 0xC0 as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+ 0x47 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 const BACKGROUND: Color = Color::from_rgb(
+ 0x2F as f32 / 255.0,
+ 0x31 as f32 / 255.0,
+ 0x36 as f32 / 255.0,
+);
+
+pub struct Container;
+
+impl container::StyleSheet for Container {
+ fn style(&self) -> container::Style {
+ container::Style {
+ text_color: Some(Color::WHITE),
+ ..container::Style::default()
+ }
+ }
+}
+
+pub struct Button;
+
+impl button::StyleSheet for Button {
+ fn active(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(ACTIVE)),
+ border_radius: 3.0,
+ text_color: Color::WHITE,
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(HOVERED)),
+ text_color: Color::WHITE,
+ ..self.active()
+ }
+ }
+
+ fn pressed(&self) -> button::Style {
+ button::Style {
+ border_width: 1.0,
+ border_color: Color::WHITE,
+ ..self.hovered()
+ }
+ }
+}
+
+pub struct Clear;
+
+impl button::StyleSheet for Clear {
+ fn active(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(DESTRUCTIVE)),
+ border_radius: 3.0,
+ text_color: Color::WHITE,
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(Color {
+ a: 0.5,
+ ..DESTRUCTIVE
+ })),
+ text_color: Color::WHITE,
+ ..self.active()
+ }
+ }
+
+ fn pressed(&self) -> button::Style {
+ button::Style {
+ border_width: 1.0,
+ border_color: Color::WHITE,
+ ..self.hovered()
+ }
+ }
+}
+
+pub struct Slider;
+
+impl slider::StyleSheet for Slider {
+ fn active(&self) -> slider::Style {
+ slider::Style {
+ rail_colors: (ACTIVE, Color { a: 0.1, ..ACTIVE }),
+ handle: slider::Handle {
+ shape: slider::HandleShape::Circle { radius: 9.0 },
+ color: ACTIVE,
+ border_width: 0.0,
+ border_color: Color::TRANSPARENT,
+ },
+ }
+ }
+
+ fn hovered(&self) -> slider::Style {
+ let active = self.active();
+
+ slider::Style {
+ handle: slider::Handle {
+ color: HOVERED,
+ ..active.handle
+ },
+ ..active
+ }
+ }
+
+ fn dragging(&self) -> slider::Style {
+ let active = self.active();
+
+ slider::Style {
+ handle: slider::Handle {
+ color: Color::from_rgb(0.85, 0.85, 0.85),
+ ..active.handle
+ },
+ ..active
+ }
+ }
+}
+
+pub struct PickList;
+
+impl pick_list::StyleSheet for PickList {
+ fn menu(&self) -> pick_list::Menu {
+ pick_list::Menu {
+ text_color: Color::WHITE,
+ background: BACKGROUND.into(),
+ border_width: 1.0,
+ border_color: Color {
+ a: 0.7,
+ ..Color::BLACK
+ },
+ selected_background: Color {
+ a: 0.5,
+ ..Color::BLACK
+ }
+ .into(),
+ selected_text_color: Color::WHITE,
+ }
+ }
+
+ fn active(&self) -> pick_list::Style {
+ pick_list::Style {
+ text_color: Color::WHITE,
+ background: BACKGROUND.into(),
+ border_width: 1.0,
+ border_color: Color {
+ a: 0.6,
+ ..Color::BLACK
+ },
+ border_radius: 2.0,
+ icon_size: 0.5,
+ ..pick_list::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> pick_list::Style {
+ let active = self.active();
+
+ pick_list::Style {
+ border_color: Color {
+ a: 0.9,
+ ..Color::BLACK
+ },
+ ..active
+ }
+ }
+}
diff --git a/examples/pure/pane_grid/Cargo.toml b/examples/pure/pane_grid/Cargo.toml
new file mode 100644
index 00000000..a51cdaf0
--- /dev/null
+++ b/examples/pure/pane_grid/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "pure_pane_grid"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["pure", "debug"] }
+iced_native = { path = "../../../native" }
+iced_lazy = { path = "../../../lazy", features = ["pure"] }
diff --git a/examples/pure/pane_grid/src/main.rs b/examples/pure/pane_grid/src/main.rs
new file mode 100644
index 00000000..65516956
--- /dev/null
+++ b/examples/pure/pane_grid/src/main.rs
@@ -0,0 +1,436 @@
+use iced::alignment::{self, Alignment};
+use iced::executor;
+use iced::keyboard;
+use iced::pure::widget::pane_grid::{self, PaneGrid};
+use iced::pure::{button, column, container, row, scrollable, text};
+use iced::pure::{Application, Element};
+use iced::{Color, Command, Length, Settings, Size, Subscription};
+use iced_lazy::pure::responsive;
+use iced_native::{event, subscription, Event};
+
+pub fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+struct Example {
+ panes: pane_grid::State<Pane>,
+ panes_created: usize,
+ focus: Option<pane_grid::Pane>,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ Split(pane_grid::Axis, pane_grid::Pane),
+ SplitFocused(pane_grid::Axis),
+ FocusAdjacent(pane_grid::Direction),
+ Clicked(pane_grid::Pane),
+ Dragged(pane_grid::DragEvent),
+ Resized(pane_grid::ResizeEvent),
+ TogglePin(pane_grid::Pane),
+ Close(pane_grid::Pane),
+ CloseFocused,
+}
+
+impl Application for Example {
+ type Message = Message;
+ type Executor = executor::Default;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Self, Command<Message>) {
+ let (panes, _) = pane_grid::State::new(Pane::new(0));
+
+ (
+ Example {
+ panes,
+ panes_created: 1,
+ focus: None,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ String::from("Pane grid - Iced")
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match message {
+ Message::Split(axis, pane) => {
+ let result = self.panes.split(
+ axis,
+ &pane,
+ Pane::new(self.panes_created),
+ );
+
+ if let Some((pane, _)) = result {
+ self.focus = Some(pane);
+ }
+
+ self.panes_created += 1;
+ }
+ Message::SplitFocused(axis) => {
+ if let Some(pane) = self.focus {
+ let result = self.panes.split(
+ axis,
+ &pane,
+ Pane::new(self.panes_created),
+ );
+
+ if let Some((pane, _)) = result {
+ self.focus = Some(pane);
+ }
+
+ self.panes_created += 1;
+ }
+ }
+ Message::FocusAdjacent(direction) => {
+ if let Some(pane) = self.focus {
+ if let Some(adjacent) =
+ self.panes.adjacent(&pane, direction)
+ {
+ self.focus = Some(adjacent);
+ }
+ }
+ }
+ Message::Clicked(pane) => {
+ self.focus = Some(pane);
+ }
+ 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::TogglePin(pane) => {
+ if let Some(Pane { is_pinned, .. }) = self.panes.get_mut(&pane)
+ {
+ *is_pinned = !*is_pinned;
+ }
+ }
+ Message::Close(pane) => {
+ if let Some((_, sibling)) = self.panes.close(&pane) {
+ self.focus = Some(sibling);
+ }
+ }
+ Message::CloseFocused => {
+ if let Some(pane) = self.focus {
+ if let Some(Pane { is_pinned, .. }) = self.panes.get(&pane)
+ {
+ if !is_pinned {
+ if let Some((_, sibling)) = self.panes.close(&pane)
+ {
+ self.focus = Some(sibling);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Command::none()
+ }
+
+ fn subscription(&self) -> Subscription<Message> {
+ subscription::events_with(|event, status| {
+ if let event::Status::Captured = status {
+ return None;
+ }
+
+ match event {
+ Event::Keyboard(keyboard::Event::KeyPressed {
+ modifiers,
+ key_code,
+ }) if modifiers.command() => handle_hotkey(key_code),
+ _ => None,
+ }
+ })
+ }
+
+ fn view(&self) -> Element<Message> {
+ let focus = self.focus;
+ let total_panes = self.panes.len();
+
+ let pane_grid = PaneGrid::new(&self.panes, |id, pane| {
+ let is_focused = focus == Some(id);
+
+ let pin_button = button(
+ text(if pane.is_pinned { "Unpin" } else { "Pin" }).size(14),
+ )
+ .on_press(Message::TogglePin(id))
+ .style(style::Button::Pin)
+ .padding(3);
+
+ let title = row()
+ .push(pin_button)
+ .push("Pane")
+ .push(text(pane.id.to_string()).color(if is_focused {
+ PANE_ID_COLOR_FOCUSED
+ } else {
+ PANE_ID_COLOR_UNFOCUSED
+ }))
+ .spacing(5);
+
+ let title_bar = pane_grid::TitleBar::new(title)
+ .controls(view_controls(id, total_panes, pane.is_pinned))
+ .padding(10)
+ .style(if is_focused {
+ style::TitleBar::Focused
+ } else {
+ style::TitleBar::Active
+ });
+
+ pane_grid::Content::new(responsive(move |size| {
+ view_content(id, total_panes, pane.is_pinned, size)
+ }))
+ .title_bar(title_bar)
+ .style(if is_focused {
+ style::Pane::Focused
+ } else {
+ style::Pane::Active
+ })
+ })
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .spacing(10)
+ .on_click(Message::Clicked)
+ .on_drag(Message::Dragged)
+ .on_resize(10, Message::Resized);
+
+ container(pane_grid)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(10)
+ .into()
+ }
+}
+
+const PANE_ID_COLOR_UNFOCUSED: Color = Color::from_rgb(
+ 0xFF as f32 / 255.0,
+ 0xC7 as f32 / 255.0,
+ 0xC7 as f32 / 255.0,
+);
+const PANE_ID_COLOR_FOCUSED: Color = Color::from_rgb(
+ 0xFF as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+ 0x47 as f32 / 255.0,
+);
+
+fn handle_hotkey(key_code: keyboard::KeyCode) -> Option<Message> {
+ use keyboard::KeyCode;
+ use pane_grid::{Axis, Direction};
+
+ let direction = match key_code {
+ KeyCode::Up => Some(Direction::Up),
+ KeyCode::Down => Some(Direction::Down),
+ KeyCode::Left => Some(Direction::Left),
+ KeyCode::Right => Some(Direction::Right),
+ _ => None,
+ };
+
+ match 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 Pane {
+ id: usize,
+ pub is_pinned: bool,
+}
+
+impl Pane {
+ fn new(id: usize) -> Self {
+ Self {
+ id,
+ is_pinned: false,
+ }
+ }
+}
+
+fn view_content<'a>(
+ pane: pane_grid::Pane,
+ total_panes: usize,
+ is_pinned: bool,
+ size: Size,
+) -> Element<'a, Message> {
+ let button = |label, message, style| {
+ button(
+ text(label)
+ .width(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center)
+ .size(16),
+ )
+ .width(Length::Fill)
+ .padding(8)
+ .on_press(message)
+ .style(style)
+ };
+
+ let mut controls = column()
+ .spacing(5)
+ .max_width(150)
+ .push(button(
+ "Split horizontally",
+ Message::Split(pane_grid::Axis::Horizontal, pane),
+ style::Button::Primary,
+ ))
+ .push(button(
+ "Split vertically",
+ Message::Split(pane_grid::Axis::Vertical, pane),
+ style::Button::Primary,
+ ));
+
+ if total_panes > 1 && !is_pinned {
+ controls = controls.push(button(
+ "Close",
+ Message::Close(pane),
+ style::Button::Destructive,
+ ));
+ }
+
+ let content = column()
+ .width(Length::Fill)
+ .spacing(10)
+ .align_items(Alignment::Center)
+ .push(text(format!("{}x{}", size.width, size.height)).size(24))
+ .push(controls);
+
+ container(scrollable(content))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(5)
+ .center_y()
+ .into()
+}
+
+fn view_controls<'a>(
+ pane: pane_grid::Pane,
+ total_panes: usize,
+ is_pinned: bool,
+) -> Element<'a, Message> {
+ let mut button = button(text("Close").size(14))
+ .style(style::Button::Control)
+ .padding(3);
+
+ if total_panes > 1 && !is_pinned {
+ button = button.on_press(Message::Close(pane));
+ }
+
+ button.into()
+}
+
+mod style {
+ use crate::PANE_ID_COLOR_FOCUSED;
+ 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 enum TitleBar {
+ Active,
+ Focused,
+ }
+
+ impl container::StyleSheet for TitleBar {
+ fn style(&self) -> container::Style {
+ let pane = match self {
+ Self::Active => Pane::Active,
+ Self::Focused => Pane::Focused,
+ }
+ .style();
+
+ container::Style {
+ text_color: Some(Color::WHITE),
+ background: Some(pane.border_color.into()),
+ ..Default::default()
+ }
+ }
+ }
+
+ pub enum Pane {
+ Active,
+ Focused,
+ }
+
+ impl container::StyleSheet for Pane {
+ fn style(&self) -> container::Style {
+ container::Style {
+ background: Some(Background::Color(SURFACE)),
+ border_width: 2.0,
+ border_color: match self {
+ Self::Active => Color::from_rgb(0.7, 0.7, 0.7),
+ Self::Focused => Color::BLACK,
+ },
+ ..Default::default()
+ }
+ }
+ }
+
+ pub enum Button {
+ Primary,
+ Destructive,
+ Control,
+ Pin,
+ }
+
+ 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::Control => (Some(PANE_ID_COLOR_FOCUSED), Color::WHITE),
+ Button::Pin => (Some(ACTIVE), Color::WHITE),
+ };
+
+ button::Style {
+ text_color,
+ background: background.map(Background::Color),
+ border_radius: 5.0,
+ 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::Control => Some(PANE_ID_COLOR_FOCUSED),
+ Button::Pin => Some(HOVERED),
+ };
+
+ button::Style {
+ background: background.map(Background::Color),
+ ..active
+ }
+ }
+ }
+}
diff --git a/examples/pure/pick_list/Cargo.toml b/examples/pure/pick_list/Cargo.toml
new file mode 100644
index 00000000..c0fcac3c
--- /dev/null
+++ b/examples/pure/pick_list/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "pure_pick_list"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["debug", "pure"] }
diff --git a/examples/pure/pick_list/src/main.rs b/examples/pure/pick_list/src/main.rs
new file mode 100644
index 00000000..b9947107
--- /dev/null
+++ b/examples/pure/pick_list/src/main.rs
@@ -0,0 +1,109 @@
+use iced::pure::{column, container, pick_list, scrollable, vertical_space};
+use iced::pure::{Element, Sandbox};
+use iced::{Alignment, Length, Settings};
+
+pub fn main() -> iced::Result {
+ Example::run(Settings::default())
+}
+
+#[derive(Default)]
+struct Example {
+ selected_language: Option<Language>,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+ LanguageSelected(Language),
+}
+
+impl Sandbox for Example {
+ type Message = Message;
+
+ fn new() -> Self {
+ Self::default()
+ }
+
+ fn title(&self) -> String {
+ String::from("Pick list - Iced")
+ }
+
+ fn update(&mut self, message: Message) {
+ match message {
+ Message::LanguageSelected(language) => {
+ self.selected_language = Some(language);
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ let pick_list = pick_list(
+ &Language::ALL[..],
+ self.selected_language,
+ Message::LanguageSelected,
+ )
+ .placeholder("Choose a language...");
+
+ let content = column()
+ .width(Length::Fill)
+ .align_items(Alignment::Center)
+ .spacing(10)
+ .push(vertical_space(Length::Units(600)))
+ .push("Which is your favorite language?")
+ .push(pick_list)
+ .push(vertical_space(Length::Units(600)));
+
+ container(scrollable(content))
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Language {
+ Rust,
+ Elm,
+ Ruby,
+ Haskell,
+ C,
+ Javascript,
+ Other,
+}
+
+impl Language {
+ const ALL: [Language; 7] = [
+ Language::C,
+ Language::Elm,
+ Language::Ruby,
+ Language::Haskell,
+ Language::Rust,
+ Language::Javascript,
+ Language::Other,
+ ];
+}
+
+impl Default for Language {
+ fn default() -> Language {
+ Language::Rust
+ }
+}
+
+impl std::fmt::Display for Language {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Language::Rust => "Rust",
+ Language::Elm => "Elm",
+ Language::Ruby => "Ruby",
+ Language::Haskell => "Haskell",
+ Language::C => "C",
+ Language::Javascript => "Javascript",
+ Language::Other => "Some other language",
+ }
+ )
+ }
+}
diff --git a/examples/pure/todos/Cargo.toml b/examples/pure/todos/Cargo.toml
new file mode 100644
index 00000000..217179e8
--- /dev/null
+++ b/examples/pure/todos/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "pure_todos"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["async-std", "debug", "default_system_font", "pure"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+async-std = "1.0"
+directories-next = "2.0"
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+web-sys = { version = "0.3", features = ["Window", "Storage"] }
+wasm-timer = "0.2"
diff --git a/examples/pure/todos/src/main.rs b/examples/pure/todos/src/main.rs
new file mode 100644
index 00000000..6a6c6300
--- /dev/null
+++ b/examples/pure/todos/src/main.rs
@@ -0,0 +1,608 @@
+use iced::alignment::{self, Alignment};
+use iced::pure::widget::Text;
+use iced::pure::{
+ button, checkbox, column, container, row, scrollable, text, text_input,
+ Application, Element,
+};
+use iced::window;
+use iced::{Command, Font, Length, Settings};
+use serde::{Deserialize, Serialize};
+
+pub fn main() -> iced::Result {
+ Todos::run(Settings {
+ window: window::Settings {
+ size: (500, 800),
+ ..window::Settings::default()
+ },
+ ..Settings::default()
+ })
+}
+
+#[derive(Debug)]
+enum Todos {
+ Loading,
+ Loaded(State),
+}
+
+#[derive(Debug, Default)]
+struct State {
+ input_value: String,
+ filter: Filter,
+ tasks: Vec<Task>,
+ dirty: bool,
+ saving: bool,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ Loaded(Result<SavedState, LoadError>),
+ Saved(Result<(), SaveError>),
+ InputChanged(String),
+ CreateTask,
+ FilterChanged(Filter),
+ TaskMessage(usize, TaskMessage),
+}
+
+impl Application for Todos {
+ type Executor = iced::executor::Default;
+ type Message = Message;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (Todos, Command<Message>) {
+ (
+ Todos::Loading,
+ Command::perform(SavedState::load(), Message::Loaded),
+ )
+ }
+
+ fn title(&self) -> String {
+ let dirty = match self {
+ Todos::Loading => false,
+ Todos::Loaded(state) => state.dirty,
+ };
+
+ format!("Todos{} - Iced", if dirty { "*" } else { "" })
+ }
+
+ fn update(&mut self, message: Message) -> Command<Message> {
+ match self {
+ Todos::Loading => {
+ match message {
+ Message::Loaded(Ok(state)) => {
+ *self = Todos::Loaded(State {
+ input_value: state.input_value,
+ filter: state.filter,
+ tasks: state.tasks,
+ ..State::default()
+ });
+ }
+ Message::Loaded(Err(_)) => {
+ *self = Todos::Loaded(State::default());
+ }
+ _ => {}
+ }
+
+ Command::none()
+ }
+ Todos::Loaded(state) => {
+ let mut saved = false;
+
+ match message {
+ Message::InputChanged(value) => {
+ state.input_value = value;
+ }
+ Message::CreateTask => {
+ if !state.input_value.is_empty() {
+ state
+ .tasks
+ .push(Task::new(state.input_value.clone()));
+ state.input_value.clear();
+ }
+ }
+ Message::FilterChanged(filter) => {
+ state.filter = filter;
+ }
+ Message::TaskMessage(i, TaskMessage::Delete) => {
+ state.tasks.remove(i);
+ }
+ Message::TaskMessage(i, task_message) => {
+ if let Some(task) = state.tasks.get_mut(i) {
+ task.update(task_message);
+ }
+ }
+ Message::Saved(_) => {
+ state.saving = false;
+ saved = true;
+ }
+ _ => {}
+ }
+
+ if !saved {
+ state.dirty = true;
+ }
+
+ if state.dirty && !state.saving {
+ state.dirty = false;
+ state.saving = true;
+
+ Command::perform(
+ SavedState {
+ input_value: state.input_value.clone(),
+ filter: state.filter,
+ tasks: state.tasks.clone(),
+ }
+ .save(),
+ Message::Saved,
+ )
+ } else {
+ Command::none()
+ }
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ match self {
+ Todos::Loading => loading_message(),
+ Todos::Loaded(State {
+ input_value,
+ filter,
+ tasks,
+ ..
+ }) => {
+ let title = text("todos")
+ .width(Length::Fill)
+ .size(100)
+ .color([0.5, 0.5, 0.5])
+ .horizontal_alignment(alignment::Horizontal::Center);
+
+ let input = text_input(
+ "What needs to be done?",
+ input_value,
+ Message::InputChanged,
+ )
+ .padding(15)
+ .size(30)
+ .on_submit(Message::CreateTask);
+
+ let controls = view_controls(&tasks, *filter);
+ let filtered_tasks =
+ tasks.iter().filter(|task| filter.matches(task));
+
+ let tasks: Element<_> = if filtered_tasks.count() > 0 {
+ tasks
+ .iter()
+ .enumerate()
+ .filter(|(_, task)| filter.matches(task))
+ .fold(column().spacing(20), |column, (i, task)| {
+ column.push(task.view().map(move |message| {
+ Message::TaskMessage(i, message)
+ }))
+ })
+ .into()
+ } else {
+ empty_message(match filter {
+ Filter::All => "You have not created a task yet...",
+ Filter::Active => "All your tasks are done! :D",
+ Filter::Completed => {
+ "You have not completed a task yet..."
+ }
+ })
+ };
+
+ let content = column()
+ .spacing(20)
+ .max_width(800)
+ .push(title)
+ .push(input)
+ .push(controls)
+ .push(tasks);
+
+ scrollable(
+ container(content)
+ .width(Length::Fill)
+ .padding(40)
+ .center_x(),
+ )
+ .into()
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct Task {
+ description: String,
+ completed: bool,
+
+ #[serde(skip)]
+ state: TaskState,
+}
+
+#[derive(Debug, Clone)]
+pub enum TaskState {
+ Idle,
+ Editing,
+}
+
+impl Default for TaskState {
+ fn default() -> Self {
+ Self::Idle
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum TaskMessage {
+ Completed(bool),
+ Edit,
+ DescriptionEdited(String),
+ FinishEdition,
+ Delete,
+}
+
+impl Task {
+ fn new(description: String) -> Self {
+ Task {
+ description,
+ completed: false,
+ state: TaskState::Idle,
+ }
+ }
+
+ fn update(&mut self, message: TaskMessage) {
+ match message {
+ TaskMessage::Completed(completed) => {
+ self.completed = completed;
+ }
+ TaskMessage::Edit => {
+ self.state = TaskState::Editing;
+ }
+ TaskMessage::DescriptionEdited(new_description) => {
+ self.description = new_description;
+ }
+ TaskMessage::FinishEdition => {
+ if !self.description.is_empty() {
+ self.state = TaskState::Idle;
+ }
+ }
+ TaskMessage::Delete => {}
+ }
+ }
+
+ fn view(&self) -> Element<TaskMessage> {
+ match &self.state {
+ TaskState::Idle => {
+ let checkbox = checkbox(
+ &self.description,
+ self.completed,
+ TaskMessage::Completed,
+ )
+ .width(Length::Fill);
+
+ row()
+ .spacing(20)
+ .align_items(Alignment::Center)
+ .push(checkbox)
+ .push(
+ button(edit_icon())
+ .on_press(TaskMessage::Edit)
+ .padding(10)
+ .style(style::Button::Icon),
+ )
+ .into()
+ }
+ TaskState::Editing => {
+ let text_input = text_input(
+ "Describe your task...",
+ &self.description,
+ TaskMessage::DescriptionEdited,
+ )
+ .on_submit(TaskMessage::FinishEdition)
+ .padding(10);
+
+ row()
+ .spacing(20)
+ .align_items(Alignment::Center)
+ .push(text_input)
+ .push(
+ button(
+ row()
+ .spacing(10)
+ .push(delete_icon())
+ .push("Delete"),
+ )
+ .on_press(TaskMessage::Delete)
+ .padding(10)
+ .style(style::Button::Destructive),
+ )
+ .into()
+ }
+ }
+ }
+}
+
+fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
+ let tasks_left = tasks.iter().filter(|task| !task.completed).count();
+
+ let filter_button = |label, filter, current_filter| {
+ let label = text(label).size(16);
+
+ let button = button(label).style(if filter == current_filter {
+ style::Button::FilterSelected
+ } else {
+ style::Button::FilterActive
+ });
+
+ button.on_press(Message::FilterChanged(filter)).padding(8)
+ };
+
+ row()
+ .spacing(20)
+ .align_items(Alignment::Center)
+ .push(
+ text(format!(
+ "{} {} left",
+ tasks_left,
+ if tasks_left == 1 { "task" } else { "tasks" }
+ ))
+ .width(Length::Fill)
+ .size(16),
+ )
+ .push(
+ row()
+ .width(Length::Shrink)
+ .spacing(10)
+ .push(filter_button("All", Filter::All, current_filter))
+ .push(filter_button("Active", Filter::Active, current_filter))
+ .push(filter_button(
+ "Completed",
+ Filter::Completed,
+ current_filter,
+ )),
+ )
+ .into()
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Filter {
+ All,
+ Active,
+ Completed,
+}
+
+impl Default for Filter {
+ fn default() -> Self {
+ Filter::All
+ }
+}
+
+impl Filter {
+ fn matches(&self, task: &Task) -> bool {
+ match self {
+ Filter::All => true,
+ Filter::Active => !task.completed,
+ Filter::Completed => task.completed,
+ }
+ }
+}
+
+fn loading_message<'a>() -> Element<'a, Message> {
+ container(
+ text("Loading...")
+ .horizontal_alignment(alignment::Horizontal::Center)
+ .size(50),
+ )
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_y()
+ .into()
+}
+
+fn empty_message(message: &str) -> Element<'_, Message> {
+ container(
+ text(message)
+ .width(Length::Fill)
+ .size(25)
+ .horizontal_alignment(alignment::Horizontal::Center)
+ .color([0.7, 0.7, 0.7]),
+ )
+ .width(Length::Fill)
+ .height(Length::Units(200))
+ .center_y()
+ .into()
+}
+
+// Fonts
+const ICONS: Font = Font::External {
+ name: "Icons",
+ bytes: include_bytes!("../../../todos/fonts/icons.ttf"),
+};
+
+fn icon(unicode: char) -> Text {
+ Text::new(unicode.to_string())
+ .font(ICONS)
+ .width(Length::Units(20))
+ .horizontal_alignment(alignment::Horizontal::Center)
+ .size(20)
+}
+
+fn edit_icon() -> Text {
+ icon('\u{F303}')
+}
+
+fn delete_icon() -> Text {
+ icon('\u{F1F8}')
+}
+
+// Persistence
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SavedState {
+ input_value: String,
+ filter: Filter,
+ tasks: Vec<Task>,
+}
+
+#[derive(Debug, Clone)]
+enum LoadError {
+ FileError,
+ FormatError,
+}
+
+#[derive(Debug, Clone)]
+enum SaveError {
+ FileError,
+ WriteError,
+ FormatError,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl SavedState {
+ fn path() -> std::path::PathBuf {
+ let mut path = if let Some(project_dirs) =
+ directories_next::ProjectDirs::from("rs", "Iced", "Todos")
+ {
+ project_dirs.data_dir().into()
+ } else {
+ std::env::current_dir().unwrap_or(std::path::PathBuf::new())
+ };
+
+ path.push("todos.json");
+
+ path
+ }
+
+ async fn load() -> Result<SavedState, LoadError> {
+ use async_std::prelude::*;
+
+ let mut contents = String::new();
+
+ let mut file = async_std::fs::File::open(Self::path())
+ .await
+ .map_err(|_| LoadError::FileError)?;
+
+ file.read_to_string(&mut contents)
+ .await
+ .map_err(|_| LoadError::FileError)?;
+
+ serde_json::from_str(&contents).map_err(|_| LoadError::FormatError)
+ }
+
+ async fn save(self) -> Result<(), SaveError> {
+ use async_std::prelude::*;
+
+ let json = serde_json::to_string_pretty(&self)
+ .map_err(|_| SaveError::FormatError)?;
+
+ let path = Self::path();
+
+ if let Some(dir) = path.parent() {
+ async_std::fs::create_dir_all(dir)
+ .await
+ .map_err(|_| SaveError::FileError)?;
+ }
+
+ {
+ let mut file = async_std::fs::File::create(path)
+ .await
+ .map_err(|_| SaveError::FileError)?;
+
+ file.write_all(json.as_bytes())
+ .await
+ .map_err(|_| SaveError::WriteError)?;
+ }
+
+ // This is a simple way to save at most once every couple seconds
+ async_std::task::sleep(std::time::Duration::from_secs(2)).await;
+
+ Ok(())
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+impl SavedState {
+ fn storage() -> Option<web_sys::Storage> {
+ let window = web_sys::window()?;
+
+ window.local_storage().ok()?
+ }
+
+ async fn load() -> Result<SavedState, LoadError> {
+ let storage = Self::storage().ok_or(LoadError::FileError)?;
+
+ let contents = storage
+ .get_item("state")
+ .map_err(|_| LoadError::FileError)?
+ .ok_or(LoadError::FileError)?;
+
+ serde_json::from_str(&contents).map_err(|_| LoadError::FormatError)
+ }
+
+ async fn save(self) -> Result<(), SaveError> {
+ let storage = Self::storage().ok_or(SaveError::FileError)?;
+
+ let json = serde_json::to_string_pretty(&self)
+ .map_err(|_| SaveError::FormatError)?;
+
+ storage
+ .set_item("state", &json)
+ .map_err(|_| SaveError::WriteError)?;
+
+ let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await;
+
+ Ok(())
+ }
+}
+
+mod style {
+ use iced::{button, Background, Color, Vector};
+
+ pub enum Button {
+ FilterActive,
+ FilterSelected,
+ Icon,
+ Destructive,
+ }
+
+ impl button::StyleSheet for Button {
+ fn active(&self) -> button::Style {
+ match self {
+ Button::FilterActive => button::Style::default(),
+ Button::FilterSelected => button::Style {
+ background: Some(Background::Color(Color::from_rgb(
+ 0.2, 0.2, 0.7,
+ ))),
+ border_radius: 10.0,
+ text_color: Color::WHITE,
+ ..button::Style::default()
+ },
+ Button::Icon => button::Style {
+ text_color: Color::from_rgb(0.5, 0.5, 0.5),
+ ..button::Style::default()
+ },
+ Button::Destructive => button::Style {
+ background: Some(Background::Color(Color::from_rgb(
+ 0.8, 0.2, 0.2,
+ ))),
+ border_radius: 5.0,
+ text_color: Color::WHITE,
+ shadow_offset: Vector::new(1.0, 1.0),
+ ..button::Style::default()
+ },
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ let active = self.active();
+
+ button::Style {
+ text_color: match self {
+ Button::Icon => Color::from_rgb(0.2, 0.2, 0.7),
+ Button::FilterActive => Color::from_rgb(0.2, 0.2, 0.7),
+ _ => active.text_color,
+ },
+ shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0),
+ ..active
+ }
+ }
+ }
+}
diff --git a/examples/pure/tour/Cargo.toml b/examples/pure/tour/Cargo.toml
new file mode 100644
index 00000000..8ce5f198
--- /dev/null
+++ b/examples/pure/tour/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "pure_tour"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../../..", features = ["image", "debug", "pure"] }
+env_logger = "0.8"
diff --git a/examples/pure/tour/src/main.rs b/examples/pure/tour/src/main.rs
new file mode 100644
index 00000000..a44d99f3
--- /dev/null
+++ b/examples/pure/tour/src/main.rs
@@ -0,0 +1,703 @@
+use iced::alignment;
+use iced::pure::widget::{Button, Column, Container, Slider};
+use iced::pure::{
+ checkbox, column, container, horizontal_space, image, radio, row,
+ scrollable, slider, text, text_input, toggler, vertical_space,
+};
+use iced::pure::{Element, Sandbox};
+use iced::{Color, Length, Settings};
+
+pub fn main() -> iced::Result {
+ env_logger::init();
+
+ Tour::run(Settings::default())
+}
+
+pub struct Tour {
+ steps: Steps,
+ debug: bool,
+}
+
+impl Sandbox for Tour {
+ type Message = Message;
+
+ fn new() -> Tour {
+ Tour {
+ steps: Steps::new(),
+ debug: false,
+ }
+ }
+
+ fn title(&self) -> String {
+ format!("{} - Iced", self.steps.title())
+ }
+
+ fn update(&mut self, event: Message) {
+ match event {
+ Message::BackPressed => {
+ self.steps.go_back();
+ }
+ Message::NextPressed => {
+ self.steps.advance();
+ }
+ Message::StepMessage(step_msg) => {
+ self.steps.update(step_msg, &mut self.debug);
+ }
+ }
+ }
+
+ fn view(&self) -> Element<Message> {
+ let Tour { steps, .. } = self;
+
+ let mut controls = row();
+
+ if steps.has_previous() {
+ controls = controls.push(
+ button("Back")
+ .on_press(Message::BackPressed)
+ .style(style::Button::Secondary),
+ );
+ }
+
+ controls = controls.push(horizontal_space(Length::Fill));
+
+ if steps.can_continue() {
+ controls = controls.push(
+ button("Next")
+ .on_press(Message::NextPressed)
+ .style(style::Button::Primary),
+ );
+ }
+
+ let content: Element<_> = column()
+ .max_width(540)
+ .spacing(20)
+ .padding(20)
+ .push(steps.view(self.debug).map(Message::StepMessage))
+ .push(controls)
+ .into();
+
+ let content = if self.debug {
+ // TODO
+ //content.explain(Color::BLACK)
+ content
+ } else {
+ content
+ };
+
+ let scrollable =
+ scrollable(container(content).width(Length::Fill).center_x());
+
+ container(scrollable).height(Length::Fill).center_y().into()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ BackPressed,
+ NextPressed,
+ StepMessage(StepMessage),
+}
+
+struct Steps {
+ steps: Vec<Step>,
+ current: usize,
+}
+
+impl Steps {
+ fn new() -> Steps {
+ Steps {
+ steps: vec![
+ Step::Welcome,
+ Step::Slider { value: 50 },
+ Step::RowsAndColumns {
+ layout: Layout::Row,
+ spacing: 20,
+ },
+ Step::Text {
+ size: 30,
+ color: Color::BLACK,
+ },
+ Step::Radio { selection: None },
+ Step::Toggler {
+ can_continue: false,
+ },
+ Step::Image { width: 300 },
+ Step::Scrollable,
+ Step::TextInput {
+ value: String::new(),
+ is_secure: false,
+ },
+ Step::Debugger,
+ Step::End,
+ ],
+ current: 0,
+ }
+ }
+
+ fn update(&mut self, msg: StepMessage, debug: &mut bool) {
+ self.steps[self.current].update(msg, debug);
+ }
+
+ fn view(&self, debug: bool) -> Element<StepMessage> {
+ self.steps[self.current].view(debug)
+ }
+
+ fn advance(&mut self) {
+ if self.can_continue() {
+ self.current += 1;
+ }
+ }
+
+ fn go_back(&mut self) {
+ if self.has_previous() {
+ self.current -= 1;
+ }
+ }
+
+ fn has_previous(&self) -> bool {
+ self.current > 0
+ }
+
+ fn can_continue(&self) -> bool {
+ self.current + 1 < self.steps.len()
+ && self.steps[self.current].can_continue()
+ }
+
+ fn title(&self) -> &str {
+ self.steps[self.current].title()
+ }
+}
+
+enum Step {
+ Welcome,
+ Slider { value: u8 },
+ RowsAndColumns { layout: Layout, spacing: u16 },
+ Text { size: u16, color: Color },
+ Radio { selection: Option<Language> },
+ Toggler { can_continue: bool },
+ Image { width: u16 },
+ Scrollable,
+ TextInput { value: String, is_secure: bool },
+ Debugger,
+ End,
+}
+
+#[derive(Debug, Clone)]
+pub enum StepMessage {
+ SliderChanged(u8),
+ LayoutChanged(Layout),
+ SpacingChanged(u16),
+ TextSizeChanged(u16),
+ TextColorChanged(Color),
+ LanguageSelected(Language),
+ ImageWidthChanged(u16),
+ InputChanged(String),
+ ToggleSecureInput(bool),
+ DebugToggled(bool),
+ TogglerChanged(bool),
+}
+
+impl<'a> Step {
+ fn update(&mut self, msg: StepMessage, debug: &mut bool) {
+ match msg {
+ StepMessage::DebugToggled(value) => {
+ if let Step::Debugger = self {
+ *debug = value;
+ }
+ }
+ StepMessage::LanguageSelected(language) => {
+ if let Step::Radio { selection } = self {
+ *selection = Some(language);
+ }
+ }
+ StepMessage::SliderChanged(new_value) => {
+ if let Step::Slider { value, .. } = self {
+ *value = new_value;
+ }
+ }
+ StepMessage::TextSizeChanged(new_size) => {
+ if let Step::Text { size, .. } = self {
+ *size = new_size;
+ }
+ }
+ StepMessage::TextColorChanged(new_color) => {
+ if let Step::Text { color, .. } = self {
+ *color = new_color;
+ }
+ }
+ StepMessage::LayoutChanged(new_layout) => {
+ if let Step::RowsAndColumns { layout, .. } = self {
+ *layout = new_layout;
+ }
+ }
+ StepMessage::SpacingChanged(new_spacing) => {
+ if let Step::RowsAndColumns { spacing, .. } = self {
+ *spacing = new_spacing;
+ }
+ }
+ StepMessage::ImageWidthChanged(new_width) => {
+ if let Step::Image { width, .. } = self {
+ *width = new_width;
+ }
+ }
+ StepMessage::InputChanged(new_value) => {
+ if let Step::TextInput { value, .. } = self {
+ *value = new_value;
+ }
+ }
+ StepMessage::ToggleSecureInput(toggle) => {
+ if let Step::TextInput { is_secure, .. } = self {
+ *is_secure = toggle;
+ }
+ }
+ StepMessage::TogglerChanged(value) => {
+ if let Step::Toggler { can_continue, .. } = self {
+ *can_continue = value;
+ }
+ }
+ };
+ }
+
+ fn title(&self) -> &str {
+ match self {
+ Step::Welcome => "Welcome",
+ Step::Radio { .. } => "Radio button",
+ Step::Toggler { .. } => "Toggler",
+ Step::Slider { .. } => "Slider",
+ Step::Text { .. } => "Text",
+ Step::Image { .. } => "Image",
+ Step::RowsAndColumns { .. } => "Rows and columns",
+ Step::Scrollable => "Scrollable",
+ Step::TextInput { .. } => "Text input",
+ Step::Debugger => "Debugger",
+ Step::End => "End",
+ }
+ }
+
+ fn can_continue(&self) -> bool {
+ match self {
+ Step::Welcome => true,
+ Step::Radio { selection } => *selection == Some(Language::Rust),
+ Step::Toggler { can_continue } => *can_continue,
+ Step::Slider { .. } => true,
+ Step::Text { .. } => true,
+ Step::Image { .. } => true,
+ Step::RowsAndColumns { .. } => true,
+ Step::Scrollable => true,
+ Step::TextInput { value, .. } => !value.is_empty(),
+ Step::Debugger => true,
+ Step::End => false,
+ }
+ }
+
+ fn view(&self, debug: bool) -> Element<StepMessage> {
+ match self {
+ Step::Welcome => Self::welcome(),
+ Step::Radio { selection } => Self::radio(*selection),
+ Step::Toggler { can_continue } => Self::toggler(*can_continue),
+ Step::Slider { value } => Self::slider(*value),
+ Step::Text { size, color } => Self::text(*size, *color),
+ Step::Image { width } => Self::image(*width),
+ Step::RowsAndColumns { layout, spacing } => {
+ Self::rows_and_columns(*layout, *spacing)
+ }
+ Step::Scrollable => Self::scrollable(),
+ Step::TextInput { value, is_secure } => {
+ Self::text_input(value, *is_secure)
+ }
+ Step::Debugger => Self::debugger(debug),
+ Step::End => Self::end(),
+ }
+ .into()
+ }
+
+ fn container(title: &str) -> Column<'a, StepMessage> {
+ column().spacing(20).push(text(title).size(50))
+ }
+
+ fn welcome() -> Column<'a, StepMessage> {
+ Self::container("Welcome!")
+ .push(
+ "This is a simple tour meant to showcase a bunch of widgets \
+ that can be easily implemented on top of Iced.",
+ )
+ .push(
+ "Iced is a cross-platform GUI library for Rust focused on \
+ simplicity and type-safety. It is heavily inspired by Elm.",
+ )
+ .push(
+ "It was originally born as part of Coffee, an opinionated \
+ 2D game engine for Rust.",
+ )
+ .push(
+ "On native platforms, Iced provides by default a renderer \
+ built on top of wgpu, a graphics library supporting Vulkan, \
+ Metal, DX11, and DX12.",
+ )
+ .push(
+ "Additionally, this tour can also run on WebAssembly thanks \
+ to dodrio, an experimental VDOM library for Rust.",
+ )
+ .push(
+ "You will need to interact with the UI in order to reach the \
+ end!",
+ )
+ }
+
+ fn slider(value: u8) -> Column<'a, StepMessage> {
+ Self::container("Slider")
+ .push(
+ "A slider allows you to smoothly select a value from a range \
+ of values.",
+ )
+ .push(
+ "The following slider lets you choose an integer from \
+ 0 to 100:",
+ )
+ .push(slider(0..=100, value, StepMessage::SliderChanged))
+ .push(
+ text(value.to_string())
+ .width(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center),
+ )
+ }
+
+ fn rows_and_columns(
+ layout: Layout,
+ spacing: u16,
+ ) -> Column<'a, StepMessage> {
+ let row_radio =
+ radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged);
+
+ let column_radio = radio(
+ "Column",
+ Layout::Column,
+ Some(layout),
+ StepMessage::LayoutChanged,
+ );
+
+ let layout_section: Element<_> = match layout {
+ Layout::Row => row()
+ .spacing(spacing)
+ .push(row_radio)
+ .push(column_radio)
+ .into(),
+ Layout::Column => column()
+ .spacing(spacing)
+ .push(row_radio)
+ .push(column_radio)
+ .into(),
+ };
+
+ let spacing_section = column()
+ .spacing(10)
+ .push(slider(0..=80, spacing, StepMessage::SpacingChanged))
+ .push(
+ text(format!("{} px", spacing))
+ .width(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center),
+ );
+
+ Self::container("Rows and columns")
+ .spacing(spacing)
+ .push(
+ "Iced uses a layout model based on flexbox to position UI \
+ elements.",
+ )
+ .push(
+ "Rows and columns can be used to distribute content \
+ horizontally or vertically, respectively.",
+ )
+ .push(layout_section)
+ .push("You can also easily change the spacing between elements:")
+ .push(spacing_section)
+ }
+
+ fn text(size: u16, color: Color) -> Column<'a, StepMessage> {
+ let size_section = column()
+ .padding(20)
+ .spacing(20)
+ .push("You can change its size:")
+ .push(text(format!("This text is {} pixels", size)).size(size))
+ .push(Slider::new(10..=70, size, StepMessage::TextSizeChanged));
+
+ let color_sliders = row()
+ .spacing(10)
+ .push(color_slider(color.r, move |r| Color { r, ..color }))
+ .push(color_slider(color.g, move |g| Color { g, ..color }))
+ .push(color_slider(color.b, move |b| Color { b, ..color }));
+
+ let color_section = column()
+ .padding(20)
+ .spacing(20)
+ .push("And its color:")
+ .push(text(format!("{:?}", color)).color(color))
+ .push(color_sliders);
+
+ Self::container("Text")
+ .push(
+ "Text is probably the most essential widget for your UI. \
+ It will try to adapt to the dimensions of its container.",
+ )
+ .push(size_section)
+ .push(color_section)
+ }
+
+ fn radio(selection: Option<Language>) -> Column<'a, StepMessage> {
+ let question = column()
+ .padding(20)
+ .spacing(10)
+ .push(text("Iced is written in...").size(24))
+ .push(Language::all().iter().cloned().fold(
+ column().padding(10).spacing(20),
+ |choices, language| {
+ choices.push(radio(
+ language,
+ language,
+ selection,
+ StepMessage::LanguageSelected,
+ ))
+ },
+ ));
+
+ Self::container("Radio button")
+ .push(
+ "A radio button is normally used to represent a choice... \
+ Surprise test!",
+ )
+ .push(question)
+ .push(
+ "Iced works very well with iterators! The list above is \
+ basically created by folding a column over the different \
+ choices, creating a radio button for each one of them!",
+ )
+ }
+
+ fn toggler(can_continue: bool) -> Column<'a, StepMessage> {
+ Self::container("Toggler")
+ .push("A toggler is mostly used to enable or disable something.")
+ .push(
+ Container::new(toggler(
+ "Toggle me to continue...".to_owned(),
+ can_continue,
+ StepMessage::TogglerChanged,
+ ))
+ .padding([0, 40]),
+ )
+ }
+
+ fn image(width: u16) -> Column<'a, StepMessage> {
+ Self::container("Image")
+ .push("An image that tries to keep its aspect ratio.")
+ .push(ferris(width))
+ .push(slider(100..=500, width, StepMessage::ImageWidthChanged))
+ .push(
+ text(format!("Width: {} px", width.to_string()))
+ .width(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center),
+ )
+ }
+
+ fn scrollable() -> Column<'a, StepMessage> {
+ Self::container("Scrollable")
+ .push(
+ "Iced supports scrollable content. Try it out! Find the \
+ button further below.",
+ )
+ .push(
+ text("Tip: You can use the scrollbar to scroll down faster!")
+ .size(16),
+ )
+ .push(vertical_space(Length::Units(4096)))
+ .push(
+ text("You are halfway there!")
+ .width(Length::Fill)
+ .size(30)
+ .horizontal_alignment(alignment::Horizontal::Center),
+ )
+ .push(vertical_space(Length::Units(4096)))
+ .push(ferris(300))
+ .push(
+ text("You made it!")
+ .width(Length::Fill)
+ .size(50)
+ .horizontal_alignment(alignment::Horizontal::Center),
+ )
+ }
+
+ fn text_input(value: &str, is_secure: bool) -> Column<'a, StepMessage> {
+ let text_input = text_input(
+ "Type something to continue...",
+ value,
+ StepMessage::InputChanged,
+ )
+ .padding(10)
+ .size(30);
+
+ Self::container("Text input")
+ .push("Use a text input to ask for different kinds of information.")
+ .push(if is_secure {
+ text_input.password()
+ } else {
+ text_input
+ })
+ .push(checkbox(
+ "Enable password mode",
+ is_secure,
+ StepMessage::ToggleSecureInput,
+ ))
+ .push(
+ "A text input produces a message every time it changes. It is \
+ very easy to keep track of its contents:",
+ )
+ .push(
+ text(if value.is_empty() {
+ "You have not typed anything yet..."
+ } else {
+ value
+ })
+ .width(Length::Fill)
+ .horizontal_alignment(alignment::Horizontal::Center),
+ )
+ }
+
+ fn debugger(debug: bool) -> Column<'a, StepMessage> {
+ Self::container("Debugger")
+ .push(
+ "You can ask Iced to visually explain the layouting of the \
+ different elements comprising your UI!",
+ )
+ .push(
+ "Give it a shot! Check the following checkbox to be able to \
+ see element boundaries.",
+ )
+ .push(if cfg!(target_arch = "wasm32") {
+ Element::new(
+ text("Not available on web yet!")
+ .color([0.7, 0.7, 0.7])
+ .horizontal_alignment(alignment::Horizontal::Center),
+ )
+ } else {
+ checkbox("Explain layout", debug, StepMessage::DebugToggled)
+ .into()
+ })
+ .push("Feel free to go back and take a look.")
+ }
+
+ fn end() -> Column<'a, StepMessage> {
+ Self::container("You reached the end!")
+ .push("This tour will be updated as more features are added.")
+ .push("Make sure to keep an eye on it!")
+ }
+}
+
+fn ferris<'a>(width: u16) -> Container<'a, StepMessage> {
+ container(
+ // This should go away once we unify resource loading on native
+ // platforms
+ if cfg!(target_arch = "wasm32") {
+ image("tour/images/ferris.png")
+ } else {
+ image(format!(
+ "{}/../../tour/images/ferris.png",
+ env!("CARGO_MANIFEST_DIR")
+ ))
+ }
+ .width(Length::Units(width)),
+ )
+ .width(Length::Fill)
+ .center_x()
+}
+
+fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
+ iced::pure::button(
+ text(label).horizontal_alignment(alignment::Horizontal::Center),
+ )
+ .padding(12)
+ .width(Length::Units(100))
+}
+
+fn color_slider<'a>(
+ component: f32,
+ update: impl Fn(f32) -> Color + 'a,
+) -> Slider<'a, f64, StepMessage> {
+ slider(0.0..=1.0, f64::from(component), move |c| {
+ StepMessage::TextColorChanged(update(c as f32))
+ })
+ .step(0.01)
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Language {
+ Rust,
+ Elm,
+ Ruby,
+ Haskell,
+ C,
+ Other,
+}
+
+impl Language {
+ fn all() -> [Language; 6] {
+ [
+ Language::C,
+ Language::Elm,
+ Language::Ruby,
+ Language::Haskell,
+ Language::Rust,
+ Language::Other,
+ ]
+ }
+}
+
+impl From<Language> for String {
+ fn from(language: Language) -> String {
+ String::from(match language {
+ Language::Rust => "Rust",
+ Language::Elm => "Elm",
+ Language::Ruby => "Ruby",
+ Language::Haskell => "Haskell",
+ Language::C => "C",
+ Language::Other => "Other",
+ })
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Layout {
+ Row,
+ Column,
+}
+
+mod style {
+ use iced::{button, Background, Color, Vector};
+
+ pub enum Button {
+ Primary,
+ Secondary,
+ }
+
+ impl button::StyleSheet for Button {
+ fn active(&self) -> button::Style {
+ button::Style {
+ background: Some(Background::Color(match self {
+ Button::Primary => Color::from_rgb(0.11, 0.42, 0.87),
+ Button::Secondary => Color::from_rgb(0.5, 0.5, 0.5),
+ })),
+ border_radius: 12.0,
+ shadow_offset: Vector::new(1.0, 1.0),
+ text_color: Color::from_rgb8(0xEE, 0xEE, 0xEE),
+ ..button::Style::default()
+ }
+ }
+
+ fn hovered(&self) -> button::Style {
+ button::Style {
+ text_color: Color::WHITE,
+ shadow_offset: Vector::new(1.0, 2.0),
+ ..self.active()
+ }
+ }
+ }
+}
diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs
index dc8a4de7..377d7a2d 100644
--- a/examples/stopwatch/src/main.rs
+++ b/examples/stopwatch/src/main.rs
@@ -105,8 +105,8 @@ impl Application for Stopwatch {
Text::new(label)
.horizontal_alignment(alignment::Horizontal::Center),
)
- .min_width(80)
.padding(10)
+ .width(Length::Units(80))
.style(style)
};
diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs
index e199c88c..2024d25a 100644
--- a/examples/tour/src/main.rs
+++ b/examples/tour/src/main.rs
@@ -763,7 +763,7 @@ fn button<'a, Message: Clone>(
Text::new(label).horizontal_alignment(alignment::Horizontal::Center),
)
.padding(12)
- .min_width(100)
+ .width(Length::Units(100))
}
fn color_slider(