summaryrefslogblamecommitdiffstats
path: root/examples/tour/src/main.rs
blob: 2f8421d7bbf42ef6c98ac177ea346f18bd6bbdf3 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                                      
                   

                                                                              
  
                                                       
 
                               

                                  
                                                        



                                                                       
                                    
 
                                                            

                   
 
 
                 
                   

                   

                   


                               
                     



                                             

                
 
                       


                  
                      
                          

                         

                               
                           
                                 
                         
                            
                              
                       
                         
 
 

                               











                                                         







                                          

                                                              

                 
                                     

                                                          

                 

                                              
             

                                               
             

                                                 
             

                                                    
             

                                                      
             

                                                    
             

                                                        
             





                                                             
             

                                                   
             

                                                      
             

                                                        
             






                                                 

     


                                        
                                                                      








                                                                        











                                                              







                                                            




                                                            
 
                                   

     
                                    











                                                                   


         
                                          
                                   
                  

                                                                             

                  
                                                                           
                                                                             

                  

                                                                           

                  


                                                                              

                  

                                                                             

                  

                                                                              
             

     
                                         
                                 
                  

                                                                              

                  

                                                                       
             
                                                                       
                                                                            

     






                                                   
 
                                 
                     
                           

                                   

          
                                                            
                            
                                                                          
             


                                                              

          
                                      
                                                                  
                                                                     

                     

                                           
                                  
                  

                                                                           

                  

                                                                     
             
                                 
                                                                             


                                  



                                       

                                       
                                                           
                                                            












                                                                 
                                            



                          

                               
                  

                                                                           
             



                                
                                        




                                                   
                             



                                     

                                                      


                                       
             
                        


                     

                                       
                  

                                                                           
             
                           
                  


                                                                           
             

     
                                          
                                  
                                                                             
                  
                               

                                                          

                                                            

                                  

     



                                                     
                                
                                                                  
                                               
                                                                       
                                                                         




                                                                  
                                                            
             
                            

     
                                             
                                     
                  

                                                                         
             
                  

                                                                             
             
                                                
                  
                                              
                                
                             
                                     
             
                                                
                                                           
                                                                            

     




                                                         
                                                                               
                                            

                        
 

                                                           

                                      
                                         
                              
                                              

               
 
                                     
                                                                                
                                               

                                                           
                                                           


                                                      
                                                             
             
                  

                                                                               
             
                  
                                          



                                                        

                                 


             
                                           
                                   
                  

                                                                            

                  

                                                                             
             
                  

                                                      
             
                                                          

     
                                      
                                               

                                                                          
     






                                                      
             












                   
             













                                   
                                         




                             

                                                      




                        
                                             


                                

                                              






                                         

 
              
               
                                       
                             
             

                                                                       
                                         
                                           
                
                                                                              
         
                                     
                      
     

 
                                                                      
                                         

 
                    
                   
                                       
                               
                                                      
                                                   



               






















                                            


                                           





                                           
          






                                            
 



                          
                                    















                                                             
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<Language>,
    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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Message> {
        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<Screen> {
        Self::ALL
            .get(
                Self::ALL
                    .iter()
                    .copied()
                    .position(|screen| screen == self)
                    .expect("Screen must exist")
                    + 1,
            )
            .copied()
    }

    pub fn previous(self) -> Option<Screen> {
        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<Message: Clone>(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<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,
}

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,
        }
    }
}