Flatten state in tour example

This commit is contained in:
Héctor Ramón Jiménez 2024-06-16 23:13:20 +02:00
parent 964ae95827
commit e0b4ddf7b7
No known key found for this signature in database
GPG key ID: 7CC46565708259A7

View file

@ -21,190 +21,27 @@ pub fn main() -> iced::Result {
.run() .run()
} }
#[derive(Default)]
pub struct Tour { pub struct Tour {
steps: Steps, step: Step,
slider: u8,
layout: Layout,
spacing: u16,
text_size: u16,
text_color: Color,
language: Option<Language>,
toggler: bool,
image_width: u16,
image_filter_method: image::FilterMethod,
input_value: String,
input_is_secure: bool,
input_is_showing_icon: bool,
debug: bool, debug: bool,
} }
impl Tour {
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 controls =
row![]
.push_maybe(steps.has_previous().then(|| {
padded_button("Back")
.on_press(Message::BackPressed)
.style(button::secondary)
}))
.push(horizontal_space())
.push_maybe(steps.can_continue().then(|| {
padded_button("Next").on_press(Message::NextPressed)
}));
let content: Element<_> = column![
steps.view(self.debug).map(Message::StepMessage),
controls,
]
.max_width(540)
.spacing(20)
.padding(20)
.into();
let scrollable = scrollable(
container(if self.debug {
content.explain(Color::BLACK)
} else {
content
})
.center_x(Length::Fill),
);
container(scrollable).center_y(Length::Fill).into()
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
BackPressed, BackPressed,
NextPressed, 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,
filter_method: image::FilterMethod::Linear,
},
Step::Scrollable,
Step::TextInput {
value: String::new(),
is_secure: false,
is_showing_icon: 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()
}
}
impl Default for Steps {
fn default() -> Self {
Steps::new()
}
}
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,
filter_method: image::FilterMethod,
},
Scrollable,
TextInput {
value: String,
is_secure: bool,
is_showing_icon: bool,
},
Debugger,
End,
}
#[derive(Debug, Clone)]
pub enum StepMessage {
SliderChanged(u8), SliderChanged(u8),
LayoutChanged(Layout), LayoutChanged(Layout),
SpacingChanged(u16), SpacingChanged(u16),
@ -220,147 +57,145 @@ pub enum StepMessage {
TogglerChanged(bool), TogglerChanged(bool),
} }
impl<'a> Step { impl Tour {
fn update(&mut self, msg: StepMessage, debug: &mut bool) { fn title(&self) -> String {
match msg { let screen = match self.step {
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::ImageUseNearestToggled(use_nearest) => {
if let Step::Image { filter_method, .. } = self {
*filter_method = if use_nearest {
image::FilterMethod::Nearest
} else {
image::FilterMethod::Linear
};
}
}
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;
}
}
StepMessage::ToggleTextInputIcon(toggle) => {
if let Step::TextInput {
is_showing_icon, ..
} = self
{
*is_showing_icon = toggle;
}
}
};
}
fn title(&self) -> &str {
match self {
Step::Welcome => "Welcome", Step::Welcome => "Welcome",
Step::Radio { .. } => "Radio button", Step::Radio => "Radio button",
Step::Toggler { .. } => "Toggler", Step::Toggler => "Toggler",
Step::Slider { .. } => "Slider", Step::Slider => "Slider",
Step::Text { .. } => "Text", Step::Text => "Text",
Step::Image { .. } => "Image", Step::Image => "Image",
Step::RowsAndColumns { .. } => "Rows and columns", Step::RowsAndColumns => "Rows and columns",
Step::Scrollable => "Scrollable", Step::Scrollable => "Scrollable",
Step::TextInput { .. } => "Text input", Step::TextInput => "Text input",
Step::Debugger => "Debugger", Step::Debugger => "Debugger",
Step::End => "End", Step::End => "End",
};
format!("{} - Iced", screen)
}
fn update(&mut self, event: Message) {
match event {
Message::BackPressed => {
if let Some(step) = self.step.previous() {
self.step = step;
}
}
Message::NextPressed => {
if let Some(step) = self.step.next() {
self.step = step;
}
}
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.step.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.step {
Step::Welcome => self.welcome(),
Step::Radio => self.radio(),
Step::Toggler => self.toggler(),
Step::Slider => self.slider(),
Step::Text => self.text(),
Step::Image => self.image(),
Step::RowsAndColumns => self.rows_and_columns(),
Step::Scrollable => self.scrollable(),
Step::TextInput => self.text_input(),
Step::Debugger => self.debugger(),
Step::End => self.end(),
};
let content: Element<_> = column![screen, controls,]
.max_width(540)
.spacing(20)
.padding(20)
.into();
let scrollable = scrollable(
container(if self.debug {
content.explain(Color::BLACK)
} else {
content
})
.center_x(Length::Fill),
);
container(scrollable).center_y(Length::Fill).into()
}
fn can_continue(&self) -> bool { fn can_continue(&self) -> bool {
match self { match self.step {
Step::Welcome => true, Step::Welcome => true,
Step::Radio { selection } => *selection == Some(Language::Rust), Step::Radio => self.language == Some(Language::Rust),
Step::Toggler { can_continue } => *can_continue, Step::Toggler => self.toggler,
Step::Slider { .. } => true, Step::Slider => true,
Step::Text { .. } => true, Step::Text => true,
Step::Image { .. } => true, Step::Image => true,
Step::RowsAndColumns { .. } => true, Step::RowsAndColumns => true,
Step::Scrollable => true, Step::Scrollable => true,
Step::TextInput { value, .. } => !value.is_empty(), Step::TextInput => !self.input_value.is_empty(),
Step::Debugger => true, Step::Debugger => true,
Step::End => false, Step::End => false,
} }
} }
fn view(&self, debug: bool) -> Element<StepMessage> { fn welcome(&self) -> Column<Message> {
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,
filter_method,
} => Self::image(*width, *filter_method),
Step::RowsAndColumns { layout, spacing } => {
Self::rows_and_columns(*layout, *spacing)
}
Step::Scrollable => Self::scrollable(),
Step::TextInput {
value,
is_secure,
is_showing_icon,
} => Self::text_input(value, *is_secure, *is_showing_icon),
Step::Debugger => Self::debugger(debug),
Step::End => Self::end(),
}
.into()
}
fn container(title: &str) -> Column<'_, StepMessage> {
column![text(title).size(50)].spacing(20)
}
fn welcome() -> Column<'a, StepMessage> {
Self::container("Welcome!") Self::container("Welcome!")
.push( .push(
"This is a simple tour meant to showcase a bunch of widgets \ "This is a simple tour meant to showcase a bunch of widgets \
@ -389,7 +224,7 @@ impl<'a> Step {
) )
} }
fn slider(value: u8) -> Column<'a, StepMessage> { fn slider(&self) -> Column<Message> {
Self::container("Slider") Self::container("Slider")
.push( .push(
"A slider allows you to smoothly select a value from a range \ "A slider allows you to smoothly select a value from a range \
@ -399,47 +234,48 @@ impl<'a> Step {
"The following slider lets you choose an integer from \ "The following slider lets you choose an integer from \
0 to 100:", 0 to 100:",
) )
.push(slider(0..=100, value, StepMessage::SliderChanged)) .push(slider(0..=100, self.slider, Message::SliderChanged))
.push( .push(
text(value.to_string()) text(self.slider.to_string())
.width(Length::Fill) .width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center), .horizontal_alignment(alignment::Horizontal::Center),
) )
} }
fn rows_and_columns( fn rows_and_columns(&self) -> Column<Message> {
layout: Layout, let row_radio = radio(
spacing: u16, "Row",
) -> Column<'a, StepMessage> { Layout::Row,
let row_radio = Some(self.layout),
radio("Row", Layout::Row, Some(layout), StepMessage::LayoutChanged); Message::LayoutChanged,
);
let column_radio = radio( let column_radio = radio(
"Column", "Column",
Layout::Column, Layout::Column,
Some(layout), Some(self.layout),
StepMessage::LayoutChanged, Message::LayoutChanged,
); );
let layout_section: Element<_> = match layout { let layout_section: Element<_> = match self.layout {
Layout::Row => { Layout::Row => {
row![row_radio, column_radio].spacing(spacing).into() row![row_radio, column_radio].spacing(self.spacing).into()
}
Layout::Column => {
column![row_radio, column_radio].spacing(spacing).into()
} }
Layout::Column => column![row_radio, column_radio]
.spacing(self.spacing)
.into(),
}; };
let spacing_section = column![ let spacing_section = column![
slider(0..=80, spacing, StepMessage::SpacingChanged), slider(0..=80, self.spacing, Message::SpacingChanged),
text!("{spacing} px") text!("{} px", self.spacing)
.width(Length::Fill) .width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center), .horizontal_alignment(alignment::Horizontal::Center),
] ]
.spacing(10); .spacing(10);
Self::container("Rows and columns") Self::container("Rows and columns")
.spacing(spacing) .spacing(self.spacing)
.push( .push(
"Iced uses a layout model based on flexbox to position UI \ "Iced uses a layout model based on flexbox to position UI \
elements.", elements.",
@ -453,11 +289,14 @@ impl<'a> Step {
.push(spacing_section) .push(spacing_section)
} }
fn text(size: u16, color: Color) -> Column<'a, StepMessage> { fn text(&self) -> Column<Message> {
let size = self.text_size;
let color = self.text_color;
let size_section = column![ let size_section = column![
"You can change its size:", "You can change its size:",
text!("This text is {size} pixels").size(size), text!("This text is {size} pixels").size(size),
slider(10..=70, size, StepMessage::TextSizeChanged), slider(10..=70, size, Message::TextSizeChanged),
] ]
.padding(20) .padding(20)
.spacing(20); .spacing(20);
@ -486,7 +325,7 @@ impl<'a> Step {
.push(color_section) .push(color_section)
} }
fn radio(selection: Option<Language>) -> Column<'a, StepMessage> { fn radio(&self) -> Column<Message> {
let question = column![ let question = column![
text("Iced is written in...").size(24), text("Iced is written in...").size(24),
column( column(
@ -497,8 +336,8 @@ impl<'a> Step {
radio( radio(
language, language,
language, language,
selection, self.language,
StepMessage::LanguageSelected, Message::LanguageSelected,
) )
}) })
.map(Element::from) .map(Element::from)
@ -521,27 +360,27 @@ impl<'a> Step {
) )
} }
fn toggler(can_continue: bool) -> Column<'a, StepMessage> { fn toggler(&self) -> Column<Message> {
Self::container("Toggler") Self::container("Toggler")
.push("A toggler is mostly used to enable or disable something.") .push("A toggler is mostly used to enable or disable something.")
.push( .push(
Container::new(toggler( Container::new(toggler(
"Toggle me to continue...".to_owned(), "Toggle me to continue...".to_owned(),
can_continue, self.toggler,
StepMessage::TogglerChanged, Message::TogglerChanged,
)) ))
.padding([0, 40]), .padding([0, 40]),
) )
} }
fn image( fn image(&self) -> Column<Message> {
width: u16, let width = self.image_width;
filter_method: image::FilterMethod, let filter_method = self.image_filter_method;
) -> Column<'a, StepMessage> {
Self::container("Image") Self::container("Image")
.push("An image that tries to keep its aspect ratio.") .push("An image that tries to keep its aspect ratio.")
.push(ferris(width, filter_method)) .push(ferris(width, filter_method))
.push(slider(100..=500, width, StepMessage::ImageWidthChanged)) .push(slider(100..=500, width, Message::ImageWidthChanged))
.push( .push(
text!("Width: {width} px") text!("Width: {width} px")
.width(Length::Fill) .width(Length::Fill)
@ -552,12 +391,12 @@ impl<'a> Step {
"Use nearest interpolation", "Use nearest interpolation",
filter_method == image::FilterMethod::Nearest, filter_method == image::FilterMethod::Nearest,
) )
.on_toggle(StepMessage::ImageUseNearestToggled), .on_toggle(Message::ImageUseNearestToggled),
) )
.align_items(Alignment::Center) .align_items(Alignment::Center)
} }
fn scrollable() -> Column<'a, StepMessage> { fn scrollable(&self) -> Column<Message> {
Self::container("Scrollable") Self::container("Scrollable")
.push( .push(
"Iced supports scrollable content. Try it out! Find the \ "Iced supports scrollable content. Try it out! Find the \
@ -584,13 +423,13 @@ impl<'a> Step {
) )
} }
fn text_input( fn text_input(&self) -> Column<Message> {
value: &str, let value = &self.input_value;
is_secure: bool, let is_secure = self.input_is_secure;
is_showing_icon: bool, let is_showing_icon = self.input_is_showing_icon;
) -> Column<'_, StepMessage> {
let mut text_input = text_input("Type something to continue...", value) let mut text_input = text_input("Type something to continue...", value)
.on_input(StepMessage::InputChanged) .on_input(Message::InputChanged)
.padding(10) .padding(10)
.size(30); .size(30);
@ -609,11 +448,11 @@ impl<'a> Step {
.push(text_input.secure(is_secure)) .push(text_input.secure(is_secure))
.push( .push(
checkbox("Enable password mode", is_secure) checkbox("Enable password mode", is_secure)
.on_toggle(StepMessage::ToggleSecureInput), .on_toggle(Message::ToggleSecureInput),
) )
.push( .push(
checkbox("Show icon", is_showing_icon) checkbox("Show icon", is_showing_icon)
.on_toggle(StepMessage::ToggleTextInputIcon), .on_toggle(Message::ToggleTextInputIcon),
) )
.push( .push(
"A text input produces a message every time it changes. It is \ "A text input produces a message every time it changes. It is \
@ -630,7 +469,7 @@ impl<'a> Step {
) )
} }
fn debugger(debug: bool) -> Column<'a, StepMessage> { fn debugger(&self) -> Column<Message> {
Self::container("Debugger") Self::container("Debugger")
.push( .push(
"You can ask Iced to visually explain the layouting of the \ "You can ask Iced to visually explain the layouting of the \
@ -641,23 +480,85 @@ impl<'a> Step {
see element boundaries.", see element boundaries.",
) )
.push( .push(
checkbox("Explain layout", debug) checkbox("Explain layout", self.debug)
.on_toggle(StepMessage::DebugToggled), .on_toggle(Message::DebugToggled),
) )
.push("Feel free to go back and take a look.") .push("Feel free to go back and take a look.")
} }
fn end() -> Column<'a, StepMessage> { fn end(&self) -> Column<Message> {
Self::container("You reached the end!") Self::container("You reached the end!")
.push("This tour will be updated as more features are added.") .push("This tour will be updated as more features are added.")
.push("Make sure to keep an eye on it!") .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 Step {
Welcome,
Slider,
RowsAndColumns,
Text,
Radio,
Toggler,
Image,
Scrollable,
TextInput,
Debugger,
End,
}
impl Step {
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<Step> {
Self::ALL
.get(
Self::ALL
.iter()
.copied()
.position(|step| step == self)
.expect("Step must exist")
+ 1,
)
.copied()
}
pub fn previous(self) -> Option<Step> {
let position = Self::ALL
.iter()
.copied()
.position(|step| step == self)
.expect("Step must exist");
if position > 0 {
Some(Self::ALL[position - 1])
} else {
None
}
}
} }
fn ferris<'a>( fn ferris<'a>(
width: u16, width: u16,
filter_method: image::FilterMethod, filter_method: image::FilterMethod,
) -> Container<'a, StepMessage> { ) -> Container<'a, Message> {
container( container(
// This should go away once we unify resource loading on native // This should go away once we unify resource loading on native
// platforms // platforms
@ -679,9 +580,9 @@ fn padded_button<Message: Clone>(label: &str) -> Button<'_, Message> {
fn color_slider<'a>( fn color_slider<'a>(
component: f32, component: f32,
update: impl Fn(f32) -> Color + 'a, update: impl Fn(f32) -> Color + 'a,
) -> Slider<'a, f64, StepMessage> { ) -> Slider<'a, f64, Message> {
slider(0.0..=1.0, f64::from(component), move |c| { slider(0.0..=1.0, f64::from(component), move |c| {
StepMessage::TextColorChanged(update(c as f32)) Message::TextColorChanged(update(c as f32))
}) })
.step(0.01) .step(0.01)
} }
@ -727,3 +628,24 @@ pub enum Layout {
Row, Row,
Column, Column,
} }
impl Default for Tour {
fn default() -> Self {
Self {
step: Step::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,
}
}
}