use iced::widget::{Button, Column, Container, Slider}; use iced::widget::{ button, center_x, center_y, checkbox, column, horizontal_space, image, radio, row, scrollable, slider, text, text_input, toggler, vertical_space, }; use iced::{Center, Color, Element, Fill, Font, Pixels}; pub fn main() -> iced::Result { #[cfg(target_arch = "wasm32")] { console_log::init().expect("Initialize logger"); std::panic::set_hook(Box::new(console_error_panic_hook::hook)); } #[cfg(not(target_arch = "wasm32"))] tracing_subscriber::fmt::init(); iced::application(Tour::title, Tour::update, Tour::view) .centered() .run() } pub struct Tour { screen: Screen, slider: u8, layout: Layout, spacing: u32, text_size: u32, text_color: Color, language: Option, toggler: bool, image_width: u32, image_filter_method: image::FilterMethod, input_value: String, input_is_secure: bool, input_is_showing_icon: bool, debug: bool, } #[derive(Debug, Clone)] pub enum Message { BackPressed, NextPressed, SliderChanged(u8), LayoutChanged(Layout), SpacingChanged(u32), TextSizeChanged(u32), TextColorChanged(Color), LanguageSelected(Language), ImageWidthChanged(u32), ImageUseNearestToggled(bool), InputChanged(String), ToggleSecureInput(bool), ToggleTextInputIcon(bool), DebugToggled(bool), TogglerChanged(bool), } impl Tour { fn title(&self) -> String { let screen = match self.screen { Screen::Welcome => "Welcome", Screen::Radio => "Radio button", Screen::Toggler => "Toggler", Screen::Slider => "Slider", Screen::Text => "Text", Screen::Image => "Image", Screen::RowsAndColumns => "Rows and columns", Screen::Scrollable => "Scrollable", Screen::TextInput => "Text input", Screen::Debugger => "Debugger", Screen::End => "End", }; format!("{} - Iced", screen) } fn update(&mut self, event: Message) { match event { Message::BackPressed => { if let Some(screen) = self.screen.previous() { self.screen = screen; } } Message::NextPressed => { if let Some(screen) = self.screen.next() { self.screen = screen; } } Message::SliderChanged(value) => { self.slider = value; } Message::LayoutChanged(layout) => { self.layout = layout; } Message::SpacingChanged(spacing) => { self.spacing = spacing; } Message::TextSizeChanged(text_size) => { self.text_size = text_size; } Message::TextColorChanged(text_color) => { self.text_color = text_color; } Message::LanguageSelected(language) => { self.language = Some(language); } Message::ImageWidthChanged(image_width) => { self.image_width = image_width; } Message::ImageUseNearestToggled(use_nearest) => { self.image_filter_method = if use_nearest { image::FilterMethod::Nearest } else { image::FilterMethod::Linear }; } Message::InputChanged(input_value) => { self.input_value = input_value; } Message::ToggleSecureInput(is_secure) => { self.input_is_secure = is_secure; } Message::ToggleTextInputIcon(show_icon) => { self.input_is_showing_icon = show_icon; } Message::DebugToggled(debug) => { self.debug = debug; } Message::TogglerChanged(toggler) => { self.toggler = toggler; } } } fn view(&self) -> Element { let controls = row![] .push_maybe(self.screen.previous().is_some().then(|| { padded_button("Back") .on_press(Message::BackPressed) .style(button::secondary) })) .push(horizontal_space()) .push_maybe(self.can_continue().then(|| { padded_button("Next").on_press(Message::NextPressed) })); let screen = match self.screen { Screen::Welcome => self.welcome(), Screen::Radio => self.radio(), Screen::Toggler => self.toggler(), Screen::Slider => self.slider(), Screen::Text => self.text(), Screen::Image => self.image(), Screen::RowsAndColumns => self.rows_and_columns(), Screen::Scrollable => self.scrollable(), Screen::TextInput => self.text_input(), Screen::Debugger => self.debugger(), Screen::End => self.end(), }; let content: Element<_> = column![screen, controls,] .max_width(540) .spacing(20) .padding(20) .into(); let scrollable = scrollable(center_x(if self.debug { content.explain(Color::BLACK) } else { content })); center_y(scrollable).into() } fn can_continue(&self) -> bool { match self.screen { Screen::Welcome => true, Screen::Radio => self.language == Some(Language::Rust), Screen::Toggler => self.toggler, Screen::Slider => true, Screen::Text => true, Screen::Image => true, Screen::RowsAndColumns => true, Screen::Scrollable => true, Screen::TextInput => !self.input_value.is_empty(), Screen::Debugger => true, Screen::End => false, } } fn welcome(&self) -> Column { 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(&self) -> Column { 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, self.slider, Message::SliderChanged)) .push(text(self.slider.to_string()).width(Fill).align_x(Center)) } fn rows_and_columns(&self) -> Column { let row_radio = radio( "Row", Layout::Row, Some(self.layout), Message::LayoutChanged, ); let column_radio = radio( "Column", Layout::Column, Some(self.layout), Message::LayoutChanged, ); let layout_section: Element<_> = match self.layout { Layout::Row => { row![row_radio, column_radio].spacing(self.spacing).into() } Layout::Column => column![row_radio, column_radio] .spacing(self.spacing) .into(), }; let spacing_section = column![ slider(0..=80, self.spacing, Message::SpacingChanged), text!("{} px", self.spacing).width(Fill).align_x(Center), ] .spacing(10); Self::container("Rows and columns") .spacing(self.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(&self) -> Column { let size = self.text_size; let color = self.text_color; let size_section = column![ "You can change its size:", text!("This text is {size} pixels").size(size), slider(10..=70, size, Message::TextSizeChanged), ] .padding(20) .spacing(20); let color_sliders = row![ color_slider(color.r, move |r| Color { r, ..color }), color_slider(color.g, move |g| Color { g, ..color }), color_slider(color.b, move |b| Color { b, ..color }), ] .spacing(10); let color_section = column![ "And its color:", text!("{color:?}").color(color), color_sliders, ] .padding(20) .spacing(20); 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(&self) -> Column { let question = column![ text("Iced is written in...").size(24), column( Language::all() .iter() .copied() .map(|language| { radio( language, language, self.language, Message::LanguageSelected, ) }) .map(Element::from) ) .spacing(10) ] .padding(20) .spacing(10); 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(&self) -> Column { Self::container("Toggler") .push("A toggler is mostly used to enable or disable something.") .push( Container::new( toggler(self.toggler) .label("Toggle me to continue...") .on_toggle(Message::TogglerChanged), ) .padding([0, 40]), ) } fn image(&self) -> Column { let width = self.image_width; let filter_method = self.image_filter_method; Self::container("Image") .push("An image that tries to keep its aspect ratio.") .push(ferris(width, filter_method)) .push(slider(100..=500, width, Message::ImageWidthChanged)) .push(text!("Width: {width} px").width(Fill).align_x(Center)) .push( checkbox( "Use nearest interpolation", filter_method == image::FilterMethod::Nearest, ) .on_toggle(Message::ImageUseNearestToggled), ) .align_x(Center) } fn scrollable(&self) -> Column { 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().height(4096)) .push( text("You are halfway there!") .width(Fill) .size(30) .align_x(Center), ) .push(vertical_space().height(4096)) .push(ferris(300, image::FilterMethod::Linear)) .push(text("You made it!").width(Fill).size(50).align_x(Center)) } fn text_input(&self) -> Column { let value = &self.input_value; let is_secure = self.input_is_secure; let is_showing_icon = self.input_is_showing_icon; let mut text_input = text_input("Type something to continue...", value) .on_input(Message::InputChanged) .padding(10) .size(30); if is_showing_icon { text_input = text_input.icon(text_input::Icon { font: Font::default(), code_point: '🚀', size: Some(Pixels(28.0)), spacing: 10.0, side: text_input::Side::Right, }); } Self::container("Text input") .push("Use a text input to ask for different kinds of information.") .push(text_input.secure(is_secure)) .push( checkbox("Enable password mode", is_secure) .on_toggle(Message::ToggleSecureInput), ) .push( checkbox("Show icon", is_showing_icon) .on_toggle(Message::ToggleTextInputIcon), ) .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(Fill) .align_x(Center), ) } fn debugger(&self) -> Column { 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( checkbox("Explain layout", self.debug) .on_toggle(Message::DebugToggled), ) .push("Feel free to go back and take a look.") } fn end(&self) -> Column { 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 container(title: &str) -> Column<'_, Message> { column![text(title).size(50)].spacing(20) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Screen { Welcome, Slider, RowsAndColumns, Text, Radio, Toggler, Image, Scrollable, TextInput, Debugger, End, } impl Screen { const ALL: &'static [Self] = &[ Self::Welcome, Self::Slider, Self::RowsAndColumns, Self::Text, Self::Radio, Self::Toggler, Self::Image, Self::Scrollable, Self::TextInput, Self::Debugger, Self::End, ]; pub fn next(self) -> Option { Self::ALL .get( Self::ALL .iter() .copied() .position(|screen| screen == self) .expect("Screen must exist") + 1, ) .copied() } pub fn previous(self) -> Option { let position = Self::ALL .iter() .copied() .position(|screen| screen == self) .expect("Screen must exist"); if position > 0 { Some(Self::ALL[position - 1]) } else { None } } } fn ferris<'a>( width: u32, filter_method: image::FilterMethod, ) -> Container<'a, Message> { center_x( // 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!("{}/images/ferris.png", env!("CARGO_MANIFEST_DIR"))) } .filter_method(filter_method) .width(width), ) } fn padded_button(label: &str) -> Button<'_, Message> { button(text(label)).padding([12, 24]) } fn color_slider<'a>( component: f32, update: impl Fn(f32) -> Color + 'a, ) -> Slider<'a, f64, Message> { slider(0.0..=1.0, f64::from(component), move |c| { Message::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, } impl Default for Tour { fn default() -> Self { Self { screen: Screen::Welcome, slider: 50, layout: Layout::Row, spacing: 20, text_size: 30, text_color: Color::BLACK, language: None, toggler: false, image_width: 300, image_filter_method: image::FilterMethod::Linear, input_value: String::new(), input_is_secure: false, input_is_showing_icon: false, debug: false, } } }