Improve tour example

This commit is contained in:
Héctor Ramón Jiménez 2019-09-04 11:09:57 +02:00
parent 2c35103035
commit c583a2174d
24 changed files with 644 additions and 239 deletions

View file

@ -16,8 +16,8 @@ pub fn main() -> ggez::GameResult {
let (context, event_loop) = {
&mut ggez::ContextBuilder::new("iced", "ggez")
.window_mode(ggez::conf::WindowMode {
width: 1280.0,
height: 1024.0,
width: 850.0,
height: 850.0,
..ggez::conf::WindowMode::default()
})
.build()?
@ -39,6 +39,7 @@ pub fn main() -> ggez::GameResult {
struct Game {
spritesheet: graphics::Image,
font: graphics::Font,
tour: Tour,
events: Vec<iced::Event>,
@ -51,7 +52,8 @@ impl Game {
Ok(Game {
spritesheet: graphics::Image::new(context, "/ui.png").unwrap(),
tour: Tour::new(),
font: graphics::Font::new(context, "/Roboto-Regular.ttf").unwrap(),
tour: Tour::new(context),
events: Vec::new(),
cache: Some(iced::Cache::default()),
@ -126,7 +128,7 @@ impl event::EventHandler for Game {
}
fn draw(&mut self, context: &mut ggez::Context) -> ggez::GameResult {
graphics::clear(context, [0.3, 0.3, 0.6, 1.0].into());
graphics::clear(context, graphics::WHITE);
let screen = graphics::screen_coordinates(context);
@ -134,14 +136,17 @@ impl event::EventHandler for Game {
let layout = self.tour.layout();
let content = Column::new()
.width(screen.w as u32)
.height(screen.h as u32)
.width(screen.w as u16)
.height(screen.h as u16)
.align_items(iced::Align::Center)
.justify_content(iced::Justify::Center)
.push(layout);
let renderer =
&mut Renderer::new(context, self.spritesheet.clone());
let renderer = &mut Renderer::new(
context,
self.spritesheet.clone(),
self.font,
);
let mut ui = iced::UserInterface::build(
content,

View file

@ -1,24 +1,38 @@
mod button;
mod checkbox;
mod debugger;
mod image;
mod radio;
mod slider;
mod text;
use ggez::graphics::{self, spritebatch::SpriteBatch, Image};
use ggez::graphics::{
self, spritebatch::SpriteBatch, Font, Image, MeshBuilder,
};
use ggez::Context;
pub struct Renderer<'a> {
pub context: &'a mut Context,
pub sprites: SpriteBatch,
pub spritesheet: Image,
pub font: Font,
font_size: f32,
debug_mesh: Option<MeshBuilder>,
}
impl Renderer<'_> {
pub fn new(context: &mut Context, spritesheet: Image) -> Renderer {
pub fn new(
context: &mut Context,
spritesheet: Image,
font: Font,
) -> Renderer {
Renderer {
context,
sprites: SpriteBatch::new(spritesheet.clone()),
spritesheet,
font,
font_size: 20.0,
debug_mesh: None,
}
}
@ -37,5 +51,13 @@ impl Renderer<'_> {
graphics::FilterMode::Linear,
)
.expect("Draw text");
if let Some(debug_mesh) = self.debug_mesh.take() {
let mesh =
debug_mesh.build(self.context).expect("Build debug mesh");
graphics::draw(self.context, &mesh, graphics::DrawParam::default())
.expect("Draw debug mesh");
}
}
}

View file

@ -104,6 +104,7 @@ impl button::Renderer for Renderer<'_> {
let mut text = Text::new(TextFragment {
text: String::from(label),
font: Some(self.font),
scale: Some(Scale { x: 20.0, y: 20.0 }),
..Default::default()
});

View file

@ -0,0 +1,30 @@
use super::Renderer;
use ggez::graphics::{Color, DrawMode, MeshBuilder, Rect};
impl iced::renderer::Debugger for Renderer<'_> {
type Color = Color;
fn explain(&mut self, layout: &iced::Layout<'_>, color: Color) {
let bounds = layout.bounds();
let mut debug_mesh =
self.debug_mesh.take().unwrap_or(MeshBuilder::new());
debug_mesh.rectangle(
DrawMode::stroke(1.0),
Rect {
x: bounds.x,
y: bounds.y,
w: bounds.width,
h: bounds.height,
},
color,
);
self.debug_mesh = Some(debug_mesh);
for child in layout.children() {
self.explain(&child, color);
}
}
}

View file

@ -0,0 +1,51 @@
use super::Renderer;
use ggez::{graphics, nalgebra};
use iced::image;
impl image::Renderer<graphics::Image> for Renderer<'_> {
fn node(
&self,
style: iced::Style,
image: &graphics::Image,
width: Option<u16>,
height: Option<u16>,
_source: Option<iced::Rectangle<u16>>,
) -> iced::Node {
let aspect_ratio = image.width() as f32 / image.height() as f32;
let style = match (width, height) {
(Some(width), Some(height)) => style.width(width).height(height),
(Some(width), None) => style
.width(width)
.height((width as f32 / aspect_ratio).round() as u16),
(None, Some(height)) => style
.height(height)
.width((height as f32 * aspect_ratio).round() as u16),
(None, None) => style.width(image.width()).height(image.height()),
};
iced::Node::new(style)
}
fn draw(
&mut self,
image: &graphics::Image,
bounds: iced::Rectangle,
_source: Option<iced::Rectangle<u16>>,
) {
// We should probably use batches to draw images efficiently and keep
// draw side-effect free, but this is good enough for the example.
graphics::draw(
self.context,
image,
graphics::DrawParam::new()
.dest(nalgebra::Point2::new(bounds.x, bounds.y))
.scale(nalgebra::Vector2::new(
bounds.width / image.width() as f32,
bounds.height / image.height() as f32,
)),
)
.expect("Draw image");
}
}

View file

@ -6,7 +6,13 @@ use std::cell::RefCell;
use std::f32;
impl text::Renderer<Color> for Renderer<'_> {
fn node(&self, style: iced::Style, content: &str, size: f32) -> iced::Node {
fn node(
&self,
style: iced::Style,
content: &str,
size: Option<u16>,
) -> iced::Node {
let font = self.font;
let font_cache = graphics::font_cache(self.context);
let content = String::from(content);
@ -17,6 +23,7 @@ impl text::Renderer<Color> for Renderer<'_> {
// I noticed that the first measure is the one that matters in
// practice. Here, we use a RefCell to store the cached measurement.
let measure = RefCell::new(None);
let size = size.map(f32::from).unwrap_or(self.font_size);
iced::Node::with_measure(style, move |bounds| {
let mut measure = measure.borrow_mut();
@ -35,6 +42,7 @@ impl text::Renderer<Color> for Renderer<'_> {
let mut text = Text::new(TextFragment {
text: content.clone(),
font: Some(font),
scale: Some(Scale { x: size, y: size }),
..Default::default()
});
@ -71,13 +79,16 @@ impl text::Renderer<Color> for Renderer<'_> {
&mut self,
bounds: iced::Rectangle,
content: &str,
size: f32,
size: Option<u16>,
color: Option<Color>,
horizontal_alignment: text::HorizontalAlignment,
_vertical_alignment: text::VerticalAlignment,
) {
let size = size.map(f32::from).unwrap_or(self.font_size);
let mut text = Text::new(TextFragment {
text: String::from(content),
font: Some(self.font),
scale: Some(Scale { x: size, y: size }),
..Default::default()
});
@ -101,7 +112,7 @@ impl text::Renderer<Color> for Renderer<'_> {
x: bounds.x,
y: bounds.y,
},
color,
color.or(Some(graphics::BLACK)),
);
}
}

View file

@ -1,22 +1,26 @@
use super::widget::{
button, slider, Button, Checkbox, Column, Element, Radio, Row, Slider, Text,
button, slider, Button, Checkbox, Column, Element, Image, Radio, Row,
Slider, Text,
};
use ggez::graphics::{Color, BLACK};
use ggez::graphics::{self, Color, FilterMode, BLACK};
use ggez::Context;
use iced::{text::HorizontalAlignment, Align};
pub struct Tour {
steps: Steps,
back_button: button::State,
next_button: button::State,
debug: bool,
}
impl Tour {
pub fn new() -> Tour {
pub fn new(context: &mut Context) -> Tour {
Tour {
steps: Steps::new(),
steps: Steps::new(context),
back_button: button::State::new(),
next_button: button::State::new(),
debug: false,
}
}
@ -29,7 +33,7 @@ impl Tour {
self.steps.advance();
}
Message::StepMessage(step_msg) => {
self.steps.update(step_msg);
self.steps.update(step_msg, &mut self.debug);
}
}
}
@ -39,6 +43,7 @@ impl Tour {
steps,
back_button,
next_button,
..
} = self;
let mut controls = Row::new();
@ -59,12 +64,18 @@ impl Tour {
);
}
Column::new()
let element: Element<_> = Column::new()
.max_width(500)
.spacing(20)
.push(steps.layout().map(Message::StepMessage))
.push(steps.layout(self.debug).map(Message::StepMessage))
.push(controls)
.into()
.into();
if self.debug {
element.explain(BLACK)
} else {
element
}
}
}
@ -81,44 +92,52 @@ struct Steps {
}
impl Steps {
fn new() -> Steps {
fn new(context: &mut Context) -> Steps {
Steps {
steps: vec![
Step::Welcome,
Step::Buttons {
primary: button::State::new(),
secondary: button::State::new(),
positive: button::State::new(),
},
Step::Checkbox { is_checked: false },
Step::Radio { selection: None },
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: BLACK,
},
Step::RowsAndColumns {
layout: Layout::Row,
spacing_slider: slider::State::new(),
spacing: 20,
Step::Radio { selection: None },
Step::Image {
ferris: {
let mut image =
graphics::Image::new(context, "/ferris.png")
.expect("Load ferris image");
image.set_filter(FilterMode::Linear);
image
},
width: 300,
slider: slider::State::new(),
},
Step::Debugger,
Step::End,
],
current: 0,
}
}
fn update(&mut self, msg: StepMessage) {
self.steps[self.current].update(msg);
fn update(&mut self, msg: StepMessage, debug: &mut bool) {
self.steps[self.current].update(msg, debug);
}
fn layout(&mut self) -> Element<StepMessage> {
self.steps[self.current].layout()
fn layout(&mut self, debug: bool) -> Element<StepMessage> {
self.steps[self.current].layout(debug)
}
fn advance(&mut self) {
@ -145,52 +164,51 @@ impl Steps {
enum Step {
Welcome,
Buttons {
primary: button::State,
secondary: button::State,
positive: button::State,
},
Checkbox {
is_checked: bool,
},
Radio {
selection: Option<Language>,
},
Slider {
state: slider::State,
value: u16,
},
RowsAndColumns {
layout: Layout,
spacing_slider: slider::State,
spacing: u16,
},
Text {
size_slider: slider::State,
size: u16,
color_sliders: [slider::State; 3],
color: Color,
},
RowsAndColumns {
layout: Layout,
spacing_slider: slider::State,
spacing: u16,
Radio {
selection: Option<Language>,
},
Image {
ferris: graphics::Image,
width: u16,
slider: slider::State,
},
Debugger,
End,
}
#[derive(Debug, Clone, Copy)]
pub enum StepMessage {
CheckboxToggled(bool),
LanguageSelected(Language),
SliderChanged(f32),
TextSizeChanged(f32),
TextColorChanged(Color),
LayoutChanged(Layout),
SpacingChanged(f32),
TextSizeChanged(f32),
TextColorChanged(Color),
LanguageSelected(Language),
ImageWidthChanged(f32),
DebugToggled(bool),
}
impl<'a> Step {
fn update(&mut self, msg: StepMessage) {
fn update(&mut self, msg: StepMessage, debug: &mut bool) {
match msg {
StepMessage::CheckboxToggled(value) => {
if let Step::Checkbox { is_checked } = self {
*is_checked = value;
StepMessage::DebugToggled(value) => {
if let Step::Debugger = self {
*debug = value;
}
}
StepMessage::LanguageSelected(language) => {
@ -223,31 +241,30 @@ impl<'a> Step {
*spacing = new_spacing.round() as u16;
}
}
StepMessage::ImageWidthChanged(new_width) => {
if let Step::Image { width, .. } = self {
*width = new_width.round() as u16;
}
}
};
}
fn can_continue(&self) -> bool {
match self {
Step::Welcome => true,
Step::Buttons { .. } => true,
Step::Checkbox { is_checked } => *is_checked,
Step::Radio { selection } => *selection == Some(Language::Rust),
Step::Slider { .. } => true,
Step::Text { .. } => true,
Step::Image { .. } => true,
Step::RowsAndColumns { .. } => true,
Step::Debugger => true,
Step::End => false,
}
}
fn layout(&mut self) -> Element<StepMessage> {
fn layout(&mut self, debug: bool) -> Element<StepMessage> {
match self {
Step::Welcome => Self::welcome().into(),
Step::Buttons {
primary,
secondary,
positive,
} => Self::buttons(primary, secondary, positive).into(),
Step::Checkbox { is_checked } => Self::checkbox(*is_checked).into(),
Step::Radio { selection } => Self::radio(*selection).into(),
Step::Slider { state, value } => Self::slider(state, *value).into(),
Step::Text {
@ -256,6 +273,11 @@ impl<'a> Step {
color_sliders,
color,
} => Self::text(size_slider, *size, color_sliders, *color).into(),
Step::Image {
ferris,
width,
slider,
} => Self::image(ferris.clone(), *width, slider).into(),
Step::RowsAndColumns {
layout,
spacing_slider,
@ -263,6 +285,7 @@ impl<'a> Step {
} => {
Self::rows_and_columns(*layout, spacing_slider, *spacing).into()
}
Step::Debugger => Self::debugger(debug).into(),
Step::End => Self::end().into(),
}
}
@ -277,8 +300,21 @@ impl<'a> Step {
fn welcome() -> Column<'a, StepMessage> {
Self::container("Welcome!")
.push(Text::new(
"This is a tour that introduces some of the features and \
concepts related with UI development in Iced.",
"This 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 renderer-agnostic 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(
"Iced does not provide a built-in renderer. This example runs \
on a fairly simple renderer built on top of ggez, another \
game library.",
))
.push(Text::new(
"You will need to interact with the UI in order to reach the \
@ -286,73 +322,6 @@ impl<'a> Step {
))
}
fn buttons(
primary: &'a mut button::State,
secondary: &'a mut button::State,
positive: &'a mut button::State,
) -> Column<'a, StepMessage> {
Self::container("Button")
.push(Text::new("A button can fire actions when clicked."))
.push(Text::new(
"As of now, there are 3 different types of buttons: \
primary, secondary, and positive.",
))
.push(Button::new(primary, "Primary"))
.push(
Button::new(secondary, "Secondary")
.class(button::Class::Secondary),
)
.push(
Button::new(positive, "Positive")
.class(button::Class::Positive),
)
.push(Text::new(
"Additional types will be added in the near future! Choose \
each type smartly depending on the situation.",
))
}
fn checkbox(is_checked: bool) -> Column<'a, StepMessage> {
Self::container("Checkbox")
.push(Text::new(
"A box that can be checked. Useful to build toggle controls.",
))
.push(Checkbox::new(
is_checked,
"Show \"Next\" button",
StepMessage::CheckboxToggled,
))
.push(Text::new(
"A checkbox always has a label, and both the checkbox and its \
label can be clicked to toggle it.",
))
}
fn radio(selection: Option<Language>) -> Column<'a, StepMessage> {
let question = Column::new()
.padding(20)
.spacing(10)
.push(Text::new("Iced is written in..."))
.push(Language::all().iter().cloned().fold(
Column::new().padding(10).spacing(20),
|choices, language| {
choices.push(Radio::new(
language,
language.into(),
selection,
StepMessage::LanguageSelected,
))
},
));
Self::container("Radio button")
.push(Text::new(
"A radio button is normally used to represent a choice. Like \
a checkbox, it always has a label.",
))
.push(question)
}
fn slider(
state: &'a mut slider::State,
value: u16,
@ -378,55 +347,6 @@ impl<'a> Step {
)
}
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 {} points", size)).size(size),
)
.push(Slider::new(
size_slider,
10.0..=50.0,
size as f32,
StepMessage::TextSizeChanged,
));
let [red, green, blue] = color_sliders;
let color_section = Column::new()
.padding(20)
.spacing(20)
.push(Text::new("And its color:"))
.push(Text::new(&format!("{:?}", color)).color(color))
.push(
Row::new()
.spacing(10)
.push(Slider::new(red, 0.0..=1.0, color.r, move |r| {
StepMessage::TextColorChanged(Color { r, ..color })
}))
.push(Slider::new(green, 0.0..=1.0, color.g, move |g| {
StepMessage::TextColorChanged(Color { g, ..color })
}))
.push(Slider::new(blue, 0.0..=1.0, color.b, move |b| {
StepMessage::TextColorChanged(Color { b, ..color })
})),
);
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 rows_and_columns(
layout: Layout,
spacing_slider: &'a mut slider::State,
@ -463,7 +383,7 @@ impl<'a> Step {
.spacing(10)
.push(Slider::new(
spacing_slider,
0.0..=100.0,
0.0..=80.0,
spacing as f32,
StepMessage::SpacingChanged,
))
@ -489,10 +409,127 @@ impl<'a> Step {
.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.0..=70.0,
size as f32,
StepMessage::TextSizeChanged,
));
let [red, green, blue] = color_sliders;
let color_section = Column::new()
.padding(20)
.spacing(20)
.push(Text::new("And its color:"))
.push(Text::new(&format!("{:?}", color)).color(color))
.push(
Row::new()
.spacing(10)
.push(Slider::new(red, 0.0..=1.0, color.r, move |r| {
StepMessage::TextColorChanged(Color { r, ..color })
}))
.push(Slider::new(green, 0.0..=1.0, color.g, move |g| {
StepMessage::TextColorChanged(Color { g, ..color })
}))
.push(Slider::new(blue, 0.0..=1.0, color.b, move |b| {
StepMessage::TextColorChanged(Color { b, ..color })
})),
);
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<Language>) -> 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.into(),
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 image(
ferris: graphics::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(Image::new(ferris).width(width).align_self(Align::Center))
.push(Slider::new(
slider,
100.0..=500.0,
width as f32,
StepMessage::ImageWidthChanged,
))
.push(
Text::new(&format!("Width: {} px", width.to_string()))
.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(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 extended as more features are added.",
"This tour will be updated as more features are added.",
))
.push(Text::new("Make sure to keep an eye on it!"))
}

View file

@ -1,12 +1,13 @@
use super::Renderer;
use ggez::graphics::Color;
use ggez::graphics::{self, Color};
pub use iced::{button, slider, Button, Slider};
pub type Text = iced::Text<Color>;
pub type Checkbox<Message> = iced::Checkbox<Color, Message>;
pub type Radio<Message> = iced::Radio<Color, Message>;
pub type Image = iced::Image<graphics::Image>;
pub type Column<'a, Message> = iced::Column<'a, Message, Renderer<'a>>;
pub type Row<'a, Message> = iced::Row<'a, Message, Renderer<'a>>;