diff options
Diffstat (limited to 'examples/pure')
25 files changed, 0 insertions, 3791 deletions
diff --git a/examples/pure/arc/Cargo.toml b/examples/pure/arc/Cargo.toml deleted file mode 100644 index 22113cf1..00000000 --- a/examples/pure/arc/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "arc" -version = "0.1.0" -authors = ["ThatsNoMoon <git@thatsnomoon.dev>"] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure", "canvas", "tokio", "debug"] } diff --git a/examples/pure/arc/README.md b/examples/pure/arc/README.md deleted file mode 100644 index 303253da..00000000 --- a/examples/pure/arc/README.md +++ /dev/null @@ -1,14 +0,0 @@ -## Arc - -An application that uses the `Canvas` widget to draw a rotating arc. - -This is a simple demo for https://github.com/iced-rs/iced/pull/1358. - -The __[`main`]__ file contains all the code of the example. - -You can run it with `cargo run`: -``` -cargo run --package arc -``` - -[`main`]: src/main.rs diff --git a/examples/pure/arc/src/main.rs b/examples/pure/arc/src/main.rs deleted file mode 100644 index df0e1e8a..00000000 --- a/examples/pure/arc/src/main.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{f32::consts::PI, time::Instant}; - -use iced::executor; -use iced::pure::widget::canvas::{ - self, Cache, Canvas, Cursor, Geometry, Path, Stroke, -}; -use iced::pure::{Application, Element}; -use iced::{Command, Length, Point, Rectangle, Settings, Subscription, Theme}; - -pub fn main() -> iced::Result { - Arc::run(Settings { - antialiasing: true, - ..Settings::default() - }) -} - -struct Arc { - start: Instant, - cache: Cache, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - Tick, -} - -impl Application for Arc { - type Executor = executor::Default; - type Message = Message; - type Theme = Theme; - type Flags = (); - - fn new(_flags: ()) -> (Self, Command<Message>) { - ( - Arc { - start: Instant::now(), - cache: Default::default(), - }, - Command::none(), - ) - } - - fn title(&self) -> String { - String::from("Arc - Iced") - } - - fn update(&mut self, _: Message) -> Command<Message> { - self.cache.clear(); - - Command::none() - } - - fn subscription(&self) -> Subscription<Message> { - iced::time::every(std::time::Duration::from_millis(10)) - .map(|_| Message::Tick) - } - - fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() - } - - fn theme(&self) -> Theme { - Theme::Dark - } -} - -impl<Message> canvas::Program<Message> for Arc { - type State = (); - - fn draw( - &self, - _state: &Self::State, - theme: &Theme, - bounds: Rectangle, - _cursor: Cursor, - ) -> Vec<Geometry> { - let geometry = self.cache.draw(bounds.size(), |frame| { - let palette = theme.palette(); - - let center = frame.center(); - let radius = frame.width().min(frame.height()) / 5.0; - - let start = Point::new(center.x, center.y - radius); - - let angle = (self.start.elapsed().as_millis() % 10_000) as f32 - / 10_000.0 - * 2.0 - * PI; - - let end = Point::new( - center.x + radius * angle.cos(), - center.y + radius * angle.sin(), - ); - - let circles = Path::new(|b| { - b.circle(start, 10.0); - b.move_to(end); - b.circle(end, 10.0); - }); - - frame.fill(&circles, palette.text); - - let path = Path::new(|b| { - b.move_to(start); - b.arc_to(center, end, 50.0); - b.line_to(end); - }); - - frame.stroke( - &path, - Stroke { - color: palette.text, - width: 10.0, - ..Stroke::default() - }, - ); - }); - - vec![geometry] - } -} diff --git a/examples/pure/color_palette/Cargo.toml b/examples/pure/color_palette/Cargo.toml deleted file mode 100644 index d08309d5..00000000 --- a/examples/pure/color_palette/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "pure_color_palette" -version = "0.1.0" -authors = ["Clark Moody <clark@clarkmoody.com>"] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure", "canvas", "palette"] } -palette = "0.6.0" diff --git a/examples/pure/color_palette/README.md b/examples/pure/color_palette/README.md deleted file mode 100644 index f90020b1..00000000 --- a/examples/pure/color_palette/README.md +++ /dev/null @@ -1,15 +0,0 @@ -## Color palette - -A color palette generator, based on a user-defined root color. - -<div align="center"> - <a href="https://gfycat.com/dirtylonebighornsheep"> - <img src="screenshot.png"> - </a> -</div> - -You can run it with `cargo run`: - -``` -cargo run --package pure_color_palette -``` diff --git a/examples/pure/color_palette/screenshot.png b/examples/pure/color_palette/screenshot.png Binary files differdeleted file mode 100644 index e8da35c4..00000000 --- a/examples/pure/color_palette/screenshot.png +++ /dev/null diff --git a/examples/pure/color_palette/src/main.rs b/examples/pure/color_palette/src/main.rs deleted file mode 100644 index 8a58afa7..00000000 --- a/examples/pure/color_palette/src/main.rs +++ /dev/null @@ -1,465 +0,0 @@ -use iced::pure::{ - column, row, text, - widget::canvas::{self, Canvas, Cursor, Frame, Geometry, Path}, - widget::Slider, - Element, Sandbox, -}; -use iced::{ - alignment, Alignment, Color, Length, Point, Rectangle, Settings, Size, - Vector, -}; -use palette::{self, convert::FromColor, Hsl, Srgb}; -use std::marker::PhantomData; -use std::ops::RangeInclusive; - -pub fn main() -> iced::Result { - ColorPalette::run(Settings { - antialiasing: true, - ..Settings::default() - }) -} - -#[derive(Default)] -pub struct ColorPalette { - theme: Theme, - rgb: ColorPicker<Color>, - hsl: ColorPicker<palette::Hsl>, - hsv: ColorPicker<palette::Hsv>, - hwb: ColorPicker<palette::Hwb>, - lab: ColorPicker<palette::Lab>, - lch: ColorPicker<palette::Lch>, -} - -#[derive(Debug, Clone, Copy)] -pub enum Message { - RgbColorChanged(Color), - HslColorChanged(palette::Hsl), - HsvColorChanged(palette::Hsv), - HwbColorChanged(palette::Hwb), - LabColorChanged(palette::Lab), - LchColorChanged(palette::Lch), -} - -impl Sandbox for ColorPalette { - type Message = Message; - - fn new() -> Self { - Self::default() - } - - fn title(&self) -> String { - String::from("Color palette - Iced") - } - - fn update(&mut self, message: Message) { - let srgb = match message { - Message::RgbColorChanged(rgb) => palette::Srgb::from(rgb), - Message::HslColorChanged(hsl) => palette::Srgb::from_color(hsl), - Message::HsvColorChanged(hsv) => palette::Srgb::from_color(hsv), - Message::HwbColorChanged(hwb) => palette::Srgb::from_color(hwb), - Message::LabColorChanged(lab) => palette::Srgb::from_color(lab), - Message::LchColorChanged(lch) => palette::Srgb::from_color(lch), - }; - - self.theme = Theme::new(srgb); - } - - fn view(&self) -> Element<Message> { - let base = self.theme.base; - - let srgb = palette::Srgb::from(base); - let hsl = palette::Hsl::from_color(srgb); - let hsv = palette::Hsv::from_color(srgb); - let hwb = palette::Hwb::from_color(srgb); - let lab = palette::Lab::from_color(srgb); - let lch = palette::Lch::from_color(srgb); - - column() - .padding(10) - .spacing(10) - .push(self.rgb.view(base).map(Message::RgbColorChanged)) - .push(self.hsl.view(hsl).map(Message::HslColorChanged)) - .push(self.hsv.view(hsv).map(Message::HsvColorChanged)) - .push(self.hwb.view(hwb).map(Message::HwbColorChanged)) - .push(self.lab.view(lab).map(Message::LabColorChanged)) - .push(self.lch.view(lch).map(Message::LchColorChanged)) - .push(self.theme.view()) - .into() - } -} - -#[derive(Debug)] -struct Theme { - lower: Vec<Color>, - base: Color, - higher: Vec<Color>, - canvas_cache: canvas::Cache, -} - -impl Theme { - pub fn new(base: impl Into<Color>) -> Theme { - use palette::{Hue, Shade}; - - let base = base.into(); - - // Convert to HSL color for manipulation - let hsl = Hsl::from_color(Srgb::from(base)); - - let lower = [ - hsl.shift_hue(-135.0).lighten(0.075), - hsl.shift_hue(-120.0), - hsl.shift_hue(-105.0).darken(0.075), - hsl.darken(0.075), - ]; - - let higher = [ - hsl.lighten(0.075), - hsl.shift_hue(105.0).darken(0.075), - hsl.shift_hue(120.0), - hsl.shift_hue(135.0).lighten(0.075), - ]; - - Theme { - lower: lower - .iter() - .map(|&color| Srgb::from_color(color).into()) - .collect(), - base, - higher: higher - .iter() - .map(|&color| Srgb::from_color(color).into()) - .collect(), - canvas_cache: canvas::Cache::default(), - } - } - - pub fn len(&self) -> usize { - self.lower.len() + self.higher.len() + 1 - } - - pub fn colors(&self) -> impl Iterator<Item = &Color> { - self.lower - .iter() - .chain(std::iter::once(&self.base)) - .chain(self.higher.iter()) - } - - pub fn view(&self) -> Element<Message> { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() - } - - fn draw(&self, frame: &mut Frame) { - let pad = 20.0; - - let box_size = Size { - width: frame.width() / self.len() as f32, - height: frame.height() / 2.0 - pad, - }; - - let triangle = Path::new(|path| { - path.move_to(Point { x: 0.0, y: -0.5 }); - path.line_to(Point { x: -0.5, y: 0.0 }); - path.line_to(Point { x: 0.5, y: 0.0 }); - path.close(); - }); - - let mut text = canvas::Text { - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Top, - size: 15.0, - ..canvas::Text::default() - }; - - for (i, &color) in self.colors().enumerate() { - let anchor = Point { - x: (i as f32) * box_size.width, - y: 0.0, - }; - frame.fill_rectangle(anchor, box_size, color); - - // We show a little indicator for the base color - if color == self.base { - let triangle_x = anchor.x + box_size.width / 2.0; - - frame.with_save(|frame| { - frame.translate(Vector::new(triangle_x, 0.0)); - frame.scale(10.0); - frame.rotate(std::f32::consts::PI); - - frame.fill(&triangle, Color::WHITE); - }); - - frame.with_save(|frame| { - frame.translate(Vector::new(triangle_x, box_size.height)); - frame.scale(10.0); - - frame.fill(&triangle, Color::WHITE); - }); - } - - frame.fill_text(canvas::Text { - content: color_hex_string(&color), - position: Point { - x: anchor.x + box_size.width / 2.0, - y: box_size.height, - }, - ..text - }); - } - - text.vertical_alignment = alignment::Vertical::Bottom; - - let hsl = Hsl::from_color(Srgb::from(self.base)); - for i in 0..self.len() { - let pct = (i as f32 + 1.0) / (self.len() as f32 + 1.0); - let graded = Hsl { - lightness: 1.0 - pct, - ..hsl - }; - let color: Color = Srgb::from_color(graded).into(); - - let anchor = Point { - x: (i as f32) * box_size.width, - y: box_size.height + 2.0 * pad, - }; - - frame.fill_rectangle(anchor, box_size, color); - - frame.fill_text(canvas::Text { - content: color_hex_string(&color), - position: Point { - x: anchor.x + box_size.width / 2.0, - y: box_size.height + 2.0 * pad, - }, - ..text - }); - } - } -} - -impl<Message> canvas::Program<Message> for Theme { - type State = (); - - fn draw( - &self, - _state: &Self::State, - _theme: &iced::Theme, - bounds: Rectangle, - _cursor: Cursor, - ) -> Vec<Geometry> { - let theme = self.canvas_cache.draw(bounds.size(), |frame| { - self.draw(frame); - }); - - vec![theme] - } -} - -impl Default for Theme { - fn default() -> Self { - Theme::new(Color::from_rgb8(75, 128, 190)) - } -} - -fn color_hex_string(color: &Color) -> String { - format!( - "#{:x}{:x}{:x}", - (255.0 * color.r).round() as u8, - (255.0 * color.g).round() as u8, - (255.0 * color.b).round() as u8 - ) -} - -#[derive(Default)] -struct ColorPicker<C: ColorSpace> { - color_space: PhantomData<C>, -} - -trait ColorSpace: Sized { - const LABEL: &'static str; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3]; - - fn new(a: f32, b: f32, c: f32) -> Self; - - fn components(&self) -> [f32; 3]; - - fn to_string(&self) -> String; -} - -impl<C: ColorSpace + Copy> ColorPicker<C> { - fn view(&self, color: C) -> Element<C> { - let [c1, c2, c3] = color.components(); - let [cr1, cr2, cr3] = C::COMPONENT_RANGES; - - fn slider<'a, C: Clone>( - range: RangeInclusive<f64>, - component: f32, - update: impl Fn(f32) -> C + 'a, - ) -> Slider<'a, f64, C, iced::Renderer> { - Slider::new(range, f64::from(component), move |v| update(v as f32)) - .step(0.01) - } - - row() - .spacing(10) - .align_items(Alignment::Center) - .push(text(C::LABEL).width(Length::Units(50))) - .push(slider(cr1, c1, move |v| C::new(v, c2, c3))) - .push(slider(cr2, c2, move |v| C::new(c1, v, c3))) - .push(slider(cr3, c3, move |v| C::new(c1, c2, v))) - .push(text(color.to_string()).width(Length::Units(185)).size(14)) - .into() - } -} - -impl ColorSpace for Color { - const LABEL: &'static str = "RGB"; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = - [0.0..=1.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(r: f32, g: f32, b: f32) -> Self { - Color::from_rgb(r, g, b) - } - - fn components(&self) -> [f32; 3] { - [self.r, self.g, self.b] - } - - fn to_string(&self) -> String { - format!( - "rgb({:.0}, {:.0}, {:.0})", - 255.0 * self.r, - 255.0 * self.g, - 255.0 * self.b - ) - } -} - -impl ColorSpace for palette::Hsl { - const LABEL: &'static str = "HSL"; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = - [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(hue: f32, saturation: f32, lightness: f32) -> Self { - palette::Hsl::new( - palette::RgbHue::from_degrees(hue), - saturation, - lightness, - ) - } - - fn components(&self) -> [f32; 3] { - [ - self.hue.to_positive_degrees(), - self.saturation, - self.lightness, - ] - } - - fn to_string(&self) -> String { - format!( - "hsl({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), - 100.0 * self.saturation, - 100.0 * self.lightness - ) - } -} - -impl ColorSpace for palette::Hsv { - const LABEL: &'static str = "HSV"; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = - [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(hue: f32, saturation: f32, value: f32) -> Self { - palette::Hsv::new(palette::RgbHue::from_degrees(hue), saturation, value) - } - - fn components(&self) -> [f32; 3] { - [self.hue.to_positive_degrees(), self.saturation, self.value] - } - - fn to_string(&self) -> String { - format!( - "hsv({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), - 100.0 * self.saturation, - 100.0 * self.value - ) - } -} - -impl ColorSpace for palette::Hwb { - const LABEL: &'static str = "HWB"; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = - [0.0..=360.0, 0.0..=1.0, 0.0..=1.0]; - - fn new(hue: f32, whiteness: f32, blackness: f32) -> Self { - palette::Hwb::new( - palette::RgbHue::from_degrees(hue), - whiteness, - blackness, - ) - } - - fn components(&self) -> [f32; 3] { - [ - self.hue.to_positive_degrees(), - self.whiteness, - self.blackness, - ] - } - - fn to_string(&self) -> String { - format!( - "hwb({:.1}, {:.1}%, {:.1}%)", - self.hue.to_positive_degrees(), - 100.0 * self.whiteness, - 100.0 * self.blackness - ) - } -} - -impl ColorSpace for palette::Lab { - const LABEL: &'static str = "Lab"; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = - [0.0..=100.0, -128.0..=127.0, -128.0..=127.0]; - - fn new(l: f32, a: f32, b: f32) -> Self { - palette::Lab::new(l, a, b) - } - - fn components(&self) -> [f32; 3] { - [self.l, self.a, self.b] - } - - fn to_string(&self) -> String { - format!("Lab({:.1}, {:.1}, {:.1})", self.l, self.a, self.b) - } -} - -impl ColorSpace for palette::Lch { - const LABEL: &'static str = "Lch"; - const COMPONENT_RANGES: [RangeInclusive<f64>; 3] = - [0.0..=100.0, 0.0..=128.0, 0.0..=360.0]; - - fn new(l: f32, chroma: f32, hue: f32) -> Self { - palette::Lch::new(l, chroma, palette::LabHue::from_degrees(hue)) - } - - fn components(&self) -> [f32; 3] { - [self.l, self.chroma, self.hue.to_positive_degrees()] - } - - fn to_string(&self) -> String { - format!( - "Lch({:.1}, {:.1}, {:.1})", - self.l, - self.chroma, - self.hue.to_positive_degrees() - ) - } -} diff --git a/examples/pure/component/Cargo.toml b/examples/pure/component/Cargo.toml deleted file mode 100644 index b6c7a513..00000000 --- a/examples/pure/component/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[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 deleted file mode 100644 index db22d019..00000000 --- a/examples/pure/component/src/main.rs +++ /dev/null @@ -1,172 +0,0 @@ -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_lazy::pure::{self, Component}; - use iced_native::alignment::{self, Alignment}; - use iced_native::text; - use iced_native::widget; - use iced_native::Length; - use iced_pure::Element; - use iced_pure::{button, row, text, text_input}; - - 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, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, - { - 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_deref() - .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, - Renderer::Theme: widget::button::StyleSheet - + widget::text_input::StyleSheet - + widget::text::StyleSheet, - { - 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 deleted file mode 100644 index 2fcd22d4..00000000 --- a/examples/pure/counter/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[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 deleted file mode 100644 index 726009df..00000000 --- a/examples/pure/counter/src/main.rs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 22e38f00..00000000 --- a/examples/pure/game_of_life/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[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 deleted file mode 100644 index aa39201c..00000000 --- a/examples/pure/game_of_life/README.md +++ /dev/null @@ -1,22 +0,0 @@ -## 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 deleted file mode 100644 index cf6560d6..00000000 --- a/examples/pure/game_of_life/src/main.rs +++ /dev/null @@ -1,903 +0,0 @@ -//! 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; - -use grid::Grid; -use preset::Preset; - -use iced::executor; -use iced::pure::{ - button, checkbox, column, container, pick_list, row, slider, text, -}; -use iced::pure::{Application, Element}; -use iced::theme::{self, Theme}; -use iced::time; -use iced::window; -use iced::{Alignment, Command, Length, Settings, Subscription}; -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 Theme = Theme; - 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 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) - .into() - } - - fn theme(&self) -> Theme { - Theme::Dark - } -} - -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), - ) - .push( - button("Next") - .on_press(Message::Next) - .style(theme::Button::Secondary), - ); - - let speed_controls = row() - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) - .push(slider(1.0..=1000.0, speed as f32, Message::SpeedChanged)) - .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), - ) - .push( - button("Clear") - .on_press(Message::Clear) - .style(theme::Button::Destructive), - ) - .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, Theme, 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(&self) -> Element<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, - _theme: &Theme, - 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 deleted file mode 100644 index 964b9120..00000000 --- a/examples/pure/game_of_life/src/preset.rs +++ /dev/null @@ -1,142 +0,0 @@ -#[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/pane_grid/Cargo.toml b/examples/pure/pane_grid/Cargo.toml deleted file mode 100644 index a51cdaf0..00000000 --- a/examples/pure/pane_grid/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[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 deleted file mode 100644 index e85ed78d..00000000 --- a/examples/pure/pane_grid/src/main.rs +++ /dev/null @@ -1,369 +0,0 @@ -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::theme::{self, Theme}; -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 Theme = Theme; - 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)) - .padding(3); - - let title = row() - .push(pin_button) - .push("Pane") - .push(text(pane.id.to_string()).style(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::title_bar_focused - } else { - style::title_bar_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| { - button( - text(label) - .width(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .size(16), - ) - .width(Length::Fill) - .padding(8) - .on_press(message) - }; - - let mut controls = column() - .spacing(5) - .max_width(150) - .push(button( - "Split horizontally", - Message::Split(pane_grid::Axis::Horizontal, pane), - )) - .push(button( - "Split vertically", - Message::Split(pane_grid::Axis::Vertical, pane), - )); - - if total_panes > 1 && !is_pinned { - controls = controls.push( - button("Close", Message::Close(pane)) - .style(theme::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(theme::Button::Destructive) - .padding(3); - - if total_panes > 1 && !is_pinned { - button = button.on_press(Message::Close(pane)); - } - - button.into() -} - -mod style { - use iced::{container, Theme}; - - pub fn title_bar_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.background.strong.text), - background: Some(palette.background.strong.color.into()), - ..Default::default() - } - } - - pub fn title_bar_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - text_color: Some(palette.primary.strong.text), - background: Some(palette.primary.strong.color.into()), - ..Default::default() - } - } - - pub fn pane_active(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.background.strong.color, - ..Default::default() - } - } - - pub fn pane_focused(theme: &Theme) -> container::Appearance { - let palette = theme.extended_palette(); - - container::Appearance { - background: Some(palette.background.weak.color.into()), - border_width: 2.0, - border_color: palette.primary.strong.color, - ..Default::default() - } - } -} diff --git a/examples/pure/pick_list/Cargo.toml b/examples/pure/pick_list/Cargo.toml deleted file mode 100644 index c0fcac3c..00000000 --- a/examples/pure/pick_list/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[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 deleted file mode 100644 index b9947107..00000000 --- a/examples/pure/pick_list/src/main.rs +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 217179e8..00000000 --- a/examples/pure/todos/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[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 deleted file mode 100644 index 9a74ea71..00000000 --- a/examples/pure/todos/src/main.rs +++ /dev/null @@ -1,556 +0,0 @@ -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::theme::{self, Theme}; -use iced::window; -use iced::{Color, 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 Message = Message; - type Theme = Theme; - type Executor = iced::executor::Default; - 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) - .style(Color::from([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(theme::Button::Text), - ) - .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(theme::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 { - theme::Button::Primary - } else { - theme::Button::Text - }); - - 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) - .style(Color::from([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 { - File, - Format, -} - -#[derive(Debug, Clone)] -enum SaveError { - File, - Write, - Format, -} - -#[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_default() - }; - - 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::File)?; - - file.read_to_string(&mut contents) - .await - .map_err(|_| LoadError::File)?; - - serde_json::from_str(&contents).map_err(|_| LoadError::Format) - } - - async fn save(self) -> Result<(), SaveError> { - use async_std::prelude::*; - - let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::Format)?; - - let path = Self::path(); - - if let Some(dir) = path.parent() { - async_std::fs::create_dir_all(dir) - .await - .map_err(|_| SaveError::File)?; - } - - { - let mut file = async_std::fs::File::create(path) - .await - .map_err(|_| SaveError::File)?; - - file.write_all(json.as_bytes()) - .await - .map_err(|_| SaveError::Write)?; - } - - // 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::File)?; - - let contents = storage - .get_item("state") - .map_err(|_| LoadError::File)? - .ok_or(LoadError::File)?; - - serde_json::from_str(&contents).map_err(|_| LoadError::Format) - } - - async fn save(self) -> Result<(), SaveError> { - let storage = Self::storage().ok_or(SaveError::File)?; - - let json = serde_json::to_string_pretty(&self) - .map_err(|_| SaveError::Format)?; - - storage - .set_item("state", &json) - .map_err(|_| SaveError::Write)?; - - let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; - - Ok(()) - } -} diff --git a/examples/pure/tooltip/Cargo.toml b/examples/pure/tooltip/Cargo.toml deleted file mode 100644 index d84dfb37..00000000 --- a/examples/pure/tooltip/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "pure_tooltip" -version = "0.1.0" -authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>", "Casper Rogild Storm"] -edition = "2021" -publish = false - -[dependencies] -iced = { path = "../../..", features = ["pure"] } diff --git a/examples/pure/tooltip/src/main.rs b/examples/pure/tooltip/src/main.rs deleted file mode 100644 index e9a6c111..00000000 --- a/examples/pure/tooltip/src/main.rs +++ /dev/null @@ -1,76 +0,0 @@ -use iced::pure::widget::tooltip::Position; -use iced::pure::{button, container, tooltip}; -use iced::pure::{Element, Sandbox}; -use iced::theme; -use iced::{Length, Settings}; - -pub fn main() -> iced::Result { - Example::run(Settings::default()) -} - -struct Example { - position: Position, -} - -#[derive(Debug, Clone)] -enum Message { - ChangePosition, -} - -impl Sandbox for Example { - type Message = Message; - - fn new() -> Self { - Self { - position: Position::Bottom, - } - } - - fn title(&self) -> String { - String::from("Tooltip - Iced") - } - - fn update(&mut self, message: Message) { - match message { - Message::ChangePosition => { - let position = match &self.position { - Position::FollowCursor => Position::Top, - Position::Top => Position::Bottom, - Position::Bottom => Position::Left, - Position::Left => Position::Right, - Position::Right => Position::FollowCursor, - }; - - self.position = position - } - } - } - - fn view(&self) -> Element<Message> { - let tooltip = tooltip( - button("Press to change position") - .on_press(Message::ChangePosition), - position_to_text(self.position), - self.position, - ) - .gap(10) - .style(theme::Container::Box); - - container(tooltip) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y() - .into() - } -} - -fn position_to_text<'a>(position: Position) -> &'a str { - match position { - Position::FollowCursor => "Follow Cursor", - Position::Top => "Top", - Position::Bottom => "Bottom", - Position::Left => "Left", - Position::Right => "Right", - } -} diff --git a/examples/pure/tour/Cargo.toml b/examples/pure/tour/Cargo.toml deleted file mode 100644 index 8ce5f198..00000000 --- a/examples/pure/tour/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[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 deleted file mode 100644 index 05c269c3..00000000 --- a/examples/pure/tour/src/main.rs +++ /dev/null @@ -1,664 +0,0 @@ -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::theme; -use iced::{Color, Length, Renderer, 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(theme::Button::Secondary), - ); - } - - controls = controls.push(horizontal_space(Length::Fill)); - - if steps.can_continue() { - controls = controls.push( - button("Next") - .on_press(Message::NextPressed) - .style(theme::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 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)).style(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)) - .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!") - .style(Color::from([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, Renderer> { - 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, -} |