From 45ecf025047ada3a2e084fa38316a6e52ccd8267 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Sat, 26 Jun 2021 16:09:25 +0100 Subject: [PATCH] Add tour_glow for OpenGL platforms --- Cargo.toml | 1 + examples/tour_glow/Cargo.toml | 10 + examples/tour_glow/README.md | 28 ++ examples/tour_glow/src/main.rs | 811 +++++++++++++++++++++++++++++++++ 4 files changed, 850 insertions(+) create mode 100644 examples/tour_glow/Cargo.toml create mode 100644 examples/tour_glow/README.md create mode 100644 examples/tour_glow/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 329877c8..c6f1a687 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ members = [ "examples/svg", "examples/todos", "examples/tour", + "examples/tour_glow", "examples/tooltip", "examples/url_handler", ] diff --git a/examples/tour_glow/Cargo.toml b/examples/tour_glow/Cargo.toml new file mode 100644 index 00000000..90675822 --- /dev/null +++ b/examples/tour_glow/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tour_glow" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../..", features = ["glow", "debug"] } +env_logger = "0.8" diff --git a/examples/tour_glow/README.md b/examples/tour_glow/README.md new file mode 100644 index 00000000..f380931a --- /dev/null +++ b/examples/tour_glow/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__. + +
+ + + +
+ +[`main`]: src/main.rs +[`iced_winit`]: ../../winit +[`iced_native`]: ../../native +[`iced_wgpu`]: ../../wgpu +[`iced_web`]: ../../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`]: ../../web#usage diff --git a/examples/tour_glow/src/main.rs b/examples/tour_glow/src/main.rs new file mode 100644 index 00000000..acf94d26 --- /dev/null +++ b/examples/tour_glow/src/main.rs @@ -0,0 +1,811 @@ +use iced::{ + button, scrollable, slider, text_input, Button, Checkbox, Color, Column, + Container, Element, HorizontalAlignment, Length, Radio, Row, + Sandbox, Scrollable, Settings, Slider, Space, Text, TextInput, Toggler, +}; + +pub fn main() -> iced::Result { + env_logger::init(); + + Tour::run(Settings::default()) +} + +pub struct Tour { + steps: Steps, + scroll: scrollable::State, + back_button: button::State, + next_button: button::State, + debug: bool, +} + +impl Sandbox for Tour { + type Message = Message; + + fn new() -> Tour { + Tour { + steps: Steps::new(), + scroll: scrollable::State::new(), + back_button: button::State::new(), + next_button: button::State::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(&mut self) -> Element { + let Tour { + steps, + scroll, + back_button, + next_button, + .. + } = self; + + let mut controls = Row::new(); + + if steps.has_previous() { + controls = controls.push( + button(back_button, "Back") + .on_press(Message::BackPressed) + .style(style::Button::Secondary), + ); + } + + controls = controls.push(Space::with_width(Length::Fill)); + + if steps.can_continue() { + controls = controls.push( + button(next_button, "Next") + .on_press(Message::NextPressed) + .style(style::Button::Primary), + ); + } + + let content: Element<_> = Column::new() + .max_width(540) + .spacing(20) + .padding(20) + .push(steps.view(self.debug).map(Message::StepMessage)) + .push(controls) + .into(); + + let content = if self.debug { + content.explain(Color::BLACK) + } else { + content + }; + + let scrollable = Scrollable::new(scroll) + .push(Container::new(content).width(Length::Fill).center_x()); + + Container::new(scrollable) + .height(Length::Fill) + .center_y() + .into() + } +} + +#[derive(Debug, Clone)] +pub enum Message { + BackPressed, + NextPressed, + StepMessage(StepMessage), +} + +struct Steps { + steps: Vec, + current: usize, +} + +impl Steps { + fn new() -> Steps { + Steps { + steps: vec![ + Step::Welcome, + Step::Slider { + state: slider::State::new(), + value: 50, + }, + Step::RowsAndColumns { + layout: Layout::Row, + spacing_slider: slider::State::new(), + spacing: 20, + }, + Step::Text { + size_slider: slider::State::new(), + size: 30, + color_sliders: [slider::State::new(); 3], + color: Color::BLACK, + }, + Step::Radio { selection: None }, + Step::Toggler { + can_continue: false, + }, + Step::Image { + width: 300, + slider: slider::State::new(), + }, + Step::Scrollable, + Step::TextInput { + value: String::new(), + is_secure: false, + state: text_input::State::new(), + }, + Step::Debugger, + Step::End, + ], + current: 0, + } + } + + fn update(&mut self, msg: StepMessage, debug: &mut bool) { + self.steps[self.current].update(msg, debug); + } + + fn view(&mut self, debug: bool) -> Element { + 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 { + state: slider::State, + value: u8, + }, + RowsAndColumns { + layout: Layout, + spacing_slider: slider::State, + spacing: u16, + }, + Text { + size_slider: slider::State, + size: u16, + color_sliders: [slider::State; 3], + color: Color, + }, + Radio { + selection: Option, + }, + Toggler { + can_continue: bool, + }, + Image { + width: u16, + slider: slider::State, + }, + Scrollable, + TextInput { + value: String, + is_secure: bool, + state: text_input::State, + }, + 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(&mut self, debug: bool) -> Element { + match self { + Step::Welcome => Self::welcome(), + Step::Radio { selection } => Self::radio(*selection), + Step::Toggler { can_continue } => Self::toggler(*can_continue), + Step::Slider { state, value } => Self::slider(state, *value), + Step::Text { + size_slider, + size, + color_sliders, + color, + } => Self::text(size_slider, *size, color_sliders, *color), + Step::Image { width, slider } => Self::image(*width, slider), + Step::RowsAndColumns { + layout, + spacing_slider, + spacing, + } => Self::rows_and_columns(*layout, spacing_slider, *spacing), + Step::Scrollable => Self::scrollable(), + Step::TextInput { + value, + is_secure, + state, + } => Self::text_input(value, *is_secure, state), + Step::Debugger => Self::debugger(debug), + Step::End => Self::end(), + } + .into() + } + + fn container(title: &str) -> Column<'a, StepMessage> { + Column::new().spacing(20).push(Text::new(title).size(50)) + } + + fn welcome() -> Column<'a, StepMessage> { + Self::container("Welcome!") + .push(Text::new( + "This is a simple tour meant to showcase a bunch of widgets \ + that can be easily implemented on top of Iced.", + )) + .push(Text::new( + "Iced is a cross-platform GUI library for Rust focused on \ + simplicity and type-safety. It is heavily inspired by Elm.", + )) + .push(Text::new( + "It was originally born as part of Coffee, an opinionated \ + 2D game engine for Rust.", + )) + .push(Text::new( + "On native platforms, Iced provides by default a renderer \ + built on top of wgpu, a graphics library supporting Vulkan, \ + Metal, DX11, and DX12.", + )) + .push(Text::new( + "Additionally, this tour can also run on WebAssembly thanks \ + to dodrio, an experimental VDOM library for Rust.", + )) + .push(Text::new( + "You will need to interact with the UI in order to reach the \ + end!", + )) + } + + fn slider( + state: &'a mut slider::State, + value: u8, + ) -> Column<'a, StepMessage> { + Self::container("Slider") + .push(Text::new( + "A slider allows you to smoothly select a value from a range \ + of values.", + )) + .push(Text::new( + "The following slider lets you choose an integer from \ + 0 to 100:", + )) + .push(Slider::new( + state, + 0..=100, + value, + StepMessage::SliderChanged, + )) + .push( + Text::new(&value.to_string()) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn rows_and_columns( + layout: Layout, + spacing_slider: &'a mut slider::State, + spacing: u16, + ) -> Column<'a, StepMessage> { + let row_radio = Radio::new( + Layout::Row, + "Row", + Some(layout), + StepMessage::LayoutChanged, + ); + + let column_radio = Radio::new( + Layout::Column, + "Column", + Some(layout), + StepMessage::LayoutChanged, + ); + + let layout_section: Element<_> = match layout { + Layout::Row => Row::new() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + Layout::Column => Column::new() + .spacing(spacing) + .push(row_radio) + .push(column_radio) + .into(), + }; + + let spacing_section = Column::new() + .spacing(10) + .push(Slider::new( + spacing_slider, + 0..=80, + spacing, + StepMessage::SpacingChanged, + )) + .push( + Text::new(&format!("{} px", spacing)) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ); + + Self::container("Rows and columns") + .spacing(spacing) + .push(Text::new( + "Iced uses a layout model based on flexbox to position UI \ + elements.", + )) + .push(Text::new( + "Rows and columns can be used to distribute content \ + horizontally or vertically, respectively.", + )) + .push(layout_section) + .push(Text::new( + "You can also easily change the spacing between elements:", + )) + .push(spacing_section) + } + + fn text( + size_slider: &'a mut slider::State, + size: u16, + color_sliders: &'a mut [slider::State; 3], + color: Color, + ) -> Column<'a, StepMessage> { + let size_section = Column::new() + .padding(20) + .spacing(20) + .push(Text::new("You can change its size:")) + .push( + Text::new(&format!("This text is {} pixels", size)).size(size), + ) + .push(Slider::new( + size_slider, + 10..=70, + size, + StepMessage::TextSizeChanged, + )); + + let [red, green, blue] = color_sliders; + + let color_sliders = Row::new() + .spacing(10) + .push(color_slider(red, color.r, move |r| Color { r, ..color })) + .push(color_slider(green, color.g, move |g| Color { g, ..color })) + .push(color_slider(blue, color.b, move |b| Color { b, ..color })); + + let color_section = Column::new() + .padding(20) + .spacing(20) + .push(Text::new("And its color:")) + .push(Text::new(&format!("{:?}", color)).color(color)) + .push(color_sliders); + + Self::container("Text") + .push(Text::new( + "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) -> Column<'a, StepMessage> { + let question = Column::new() + .padding(20) + .spacing(10) + .push(Text::new("Iced is written in...").size(24)) + .push(Language::all().iter().cloned().fold( + Column::new().padding(10).spacing(20), + |choices, language| { + choices.push(Radio::new( + language, + language, + selection, + StepMessage::LanguageSelected, + )) + }, + )); + + Self::container("Radio button") + .push(Text::new( + "A radio button is normally used to represent a choice... \ + Surprise test!", + )) + .push(question) + .push(Text::new( + "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(Text::new( + "A toggler is mostly used to enable or disable something.", + )) + .push( + Container::new(Toggler::new( + can_continue, + String::from("Toggle me to continue..."), + StepMessage::TogglerChanged, + )) + .padding([0, 40]), + ) + } + + fn image( + width: u16, + slider: &'a mut slider::State, + ) -> Column<'a, StepMessage> { + Self::container("Image") + .push(Text::new("An image that tries to keep its aspect ratio.")) + .push(ferris(width)) + .push(Slider::new( + slider, + 100..=500, + width, + StepMessage::ImageWidthChanged, + )) + .push( + Text::new(&format!("Width: {} px", width.to_string())) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn scrollable() -> Column<'a, StepMessage> { + Self::container("Scrollable") + .push(Text::new( + "Iced supports scrollable content. Try it out! Find the \ + button further below.", + )) + .push( + Text::new( + "Tip: You can use the scrollbar to scroll down faster!", + ) + .size(16), + ) + .push(Column::new().height(Length::Units(4096))) + .push( + Text::new("You are halfway there!") + .width(Length::Fill) + .size(30) + .horizontal_alignment(HorizontalAlignment::Center), + ) + .push(Column::new().height(Length::Units(4096))) + .push(ferris(300)) + .push( + Text::new("You made it!") + .width(Length::Fill) + .size(50) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn text_input( + value: &str, + is_secure: bool, + state: &'a mut text_input::State, + ) -> Column<'a, StepMessage> { + let text_input = TextInput::new( + state, + "Type something to continue...", + value, + StepMessage::InputChanged, + ) + .padding(10) + .size(30); + Self::container("Text input") + .push(Text::new( + "Use a text input to ask for different kinds of information.", + )) + .push(if is_secure { + text_input.password() + } else { + text_input + }) + .push(Checkbox::new( + is_secure, + "Enable password mode", + StepMessage::ToggleSecureInput, + )) + .push(Text::new( + "A text input produces a message every time it changes. It is \ + very easy to keep track of its contents:", + )) + .push( + Text::new(if value.is_empty() { + "You have not typed anything yet..." + } else { + value + }) + .width(Length::Fill) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } + + fn debugger(debug: bool) -> Column<'a, StepMessage> { + Self::container("Debugger") + .push(Text::new( + "You can ask Iced to visually explain the layouting of the \ + different elements comprising your UI!", + )) + .push(Text::new( + "Give it a shot! Check the following checkbox to be able to \ + see element boundaries.", + )) + .push(if cfg!(target_arch = "wasm32") { + Element::new( + Text::new("Not available on web yet!") + .color([0.7, 0.7, 0.7]) + .horizontal_alignment(HorizontalAlignment::Center), + ) + } else { + Element::new(Checkbox::new( + debug, + "Explain layout", + StepMessage::DebugToggled, + )) + }) + .push(Text::new("Feel free to go back and take a look.")) + } + + fn end() -> Column<'a, StepMessage> { + Self::container("You reached the end!") + .push(Text::new( + "This tour will be updated as more features are added.", + )) + .push(Text::new("Make sure to keep an eye on it!")) + } +} + +fn ferris<'a>(_width: u16) -> Container<'a, StepMessage> { + Container::new( + // This should go away once we unify resource loading on native + // platforms + Text::new( + "sorry ferris, you can't come out in glow.", + ) + ) + .width(Length::Fill) + .center_x() +} + +fn button<'a, Message: Clone>( + state: &'a mut button::State, + label: &str, +) -> Button<'a, Message> { + Button::new( + state, + Text::new(label).horizontal_alignment(HorizontalAlignment::Center), + ) + .padding(12) + .min_width(100) +} + +fn color_slider( + state: &mut slider::State, + component: f32, + update: impl Fn(f32) -> Color + 'static, +) -> Slider { + Slider::new(state, 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 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() + } + } + } +}