summaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2022-02-13 22:19:43 +0700
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2022-02-13 22:19:43 +0700
commitcff891833be68c0e2d4919d4475daf23da821f9b (patch)
treed9eb6944583ae06bec2f6bf8fef3f9eb2981ac8e /examples
parent6689ede6d8ce0d65ec3ce29fd863ec7f26052621 (diff)
downloadiced-cff891833be68c0e2d4919d4475daf23da821f9b.tar.gz
iced-cff891833be68c0e2d4919d4475daf23da821f9b.tar.bz2
iced-cff891833be68c0e2d4919d4475daf23da821f9b.zip
Implement `pure` version of the `tour` example :tada:
Diffstat (limited to 'examples')
-rw-r--r--examples/pure/tour/Cargo.toml10
-rw-r--r--examples/pure/tour/README.md28
-rw-r--r--examples/pure/tour/index.html12
-rw-r--r--examples/pure/tour/src/main.rs702
4 files changed, 752 insertions, 0 deletions
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/README.md b/examples/pure/tour/README.md
new file mode 100644
index 00000000..e7cd2d5c
--- /dev/null
+++ b/examples/pure/tour/README.md
@@ -0,0 +1,28 @@
+## Tour
+
+A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced.
+
+The __[`main`]__ file contains all the code of the example! All the cross-platform GUI is defined in terms of __state__, __messages__, __update logic__ and __view logic__.
+
+<div align="center">
+ <a href="https://gfycat.com/politeadorableiberianmole">
+ <img src="https://thumbs.gfycat.com/PoliteAdorableIberianmole-small.gif">
+ </a>
+</div>
+
+[`main`]: src/main.rs
+[`iced_winit`]: ../../winit
+[`iced_native`]: ../../native
+[`iced_wgpu`]: ../../wgpu
+[`iced_web`]: https://github.com/iced-rs/iced_web
+[`winit`]: https://github.com/rust-windowing/winit
+[`wgpu`]: https://github.com/gfx-rs/wgpu-rs
+
+You can run the native version with `cargo run`:
+```
+cargo run --package tour
+```
+
+The web version can be run by following [the usage instructions of `iced_web`] or by accessing [iced.rs](https://iced.rs/)!
+
+[the usage instructions of `iced_web`]: https://github.com/iced-rs/iced_web#usage
diff --git a/examples/pure/tour/index.html b/examples/pure/tour/index.html
new file mode 100644
index 00000000..c64af912
--- /dev/null
+++ b/examples/pure/tour/index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Tour - Iced</title>
+ <base data-trunk-public-url />
+</head>
+<body>
+<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="z" data-bin="tour" />
+</body>
+</html>
diff --git a/examples/pure/tour/src/main.rs b/examples/pure/tour/src/main.rs
new file mode 100644
index 00000000..7a50bcdc
--- /dev/null
+++ b/examples/pure/tour/src/main.rs
@@ -0,0 +1,702 @@
+use iced::alignment;
+use iced::pure::widget::{
+ checkbox, column, container, horizontal_space, image, radio, row,
+ scrollable, slider, text, text_input, toggler, vertical_space,
+};
+use iced::pure::{Button, Column, Container, Element, Sandbox, Slider};
+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()
+ }
+ }
+ }
+}