summaryrefslogtreecommitdiffstats
path: root/examples/tour
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2019-09-05 07:23:03 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2019-09-05 07:23:03 +0200
commitced3ffc22570048711fefba638782a31d0e06035 (patch)
treec3c3f29be40ec348f15205a3dd920088edb52ace /examples/tour
parent3440ba3cb44bfc9a2b67708b683958a97d8c5e23 (diff)
downloadiced-ced3ffc22570048711fefba638782a31d0e06035.tar.gz
iced-ced3ffc22570048711fefba638782a31d0e06035.tar.bz2
iced-ced3ffc22570048711fefba638782a31d0e06035.zip
Move `ggez` example to `tour`
Diffstat (limited to 'examples/tour')
-rw-r--r--examples/tour/main.rs187
-rw-r--r--examples/tour/renderer.rs63
-rw-r--r--examples/tour/renderer/button.rs145
-rw-r--r--examples/tour/renderer/checkbox.rs64
-rw-r--r--examples/tour/renderer/debugger.rs30
-rw-r--r--examples/tour/renderer/image.rs51
-rw-r--r--examples/tour/renderer/radio.rs63
-rw-r--r--examples/tour/renderer/slider.rs82
-rw-r--r--examples/tour/renderer/text.rs118
-rw-r--r--examples/tour/tour.rs578
-rw-r--r--examples/tour/widget.rs14
11 files changed, 1395 insertions, 0 deletions
diff --git a/examples/tour/main.rs b/examples/tour/main.rs
new file mode 100644
index 00000000..0a6a2005
--- /dev/null
+++ b/examples/tour/main.rs
@@ -0,0 +1,187 @@
+mod renderer;
+mod tour;
+mod widget;
+
+use renderer::Renderer;
+use tour::Tour;
+use widget::Column;
+
+use ggez;
+use ggez::event;
+use ggez::filesystem;
+use ggez::graphics;
+use ggez::input::mouse;
+
+pub fn main() -> ggez::GameResult {
+ let (context, event_loop) = {
+ &mut ggez::ContextBuilder::new("iced", "ggez")
+ .window_mode(ggez::conf::WindowMode {
+ width: 850.0,
+ height: 850.0,
+ ..ggez::conf::WindowMode::default()
+ })
+ .build()?
+ };
+
+ filesystem::mount(
+ context,
+ std::path::Path::new(&format!(
+ "{}/examples/resources",
+ env!("CARGO_MANIFEST_DIR")
+ )),
+ true,
+ );
+
+ let state = &mut Game::new(context)?;
+
+ event::run(context, event_loop, state)
+}
+
+struct Game {
+ spritesheet: graphics::Image,
+ font: graphics::Font,
+ tour: Tour,
+
+ events: Vec<iced::Event>,
+ cache: Option<iced::Cache>,
+}
+
+impl Game {
+ fn new(context: &mut ggez::Context) -> ggez::GameResult<Game> {
+ graphics::set_default_filter(context, graphics::FilterMode::Nearest);
+
+ Ok(Game {
+ spritesheet: graphics::Image::new(context, "/ui.png").unwrap(),
+ font: graphics::Font::new(context, "/Roboto-Regular.ttf").unwrap(),
+ tour: Tour::new(context),
+
+ events: Vec::new(),
+ cache: Some(iced::Cache::default()),
+ })
+ }
+}
+
+impl event::EventHandler for Game {
+ fn update(&mut self, _ctx: &mut ggez::Context) -> ggez::GameResult {
+ Ok(())
+ }
+
+ fn mouse_button_down_event(
+ &mut self,
+ _context: &mut ggez::Context,
+ _button: mouse::MouseButton,
+ _x: f32,
+ _y: f32,
+ ) {
+ self.events.push(iced::Event::Mouse(
+ iced::input::mouse::Event::Input {
+ state: iced::input::ButtonState::Pressed,
+ button: iced::input::mouse::Button::Left, // TODO: Map `button`
+ },
+ ));
+ }
+
+ fn mouse_button_up_event(
+ &mut self,
+ _context: &mut ggez::Context,
+ _button: mouse::MouseButton,
+ _x: f32,
+ _y: f32,
+ ) {
+ self.events.push(iced::Event::Mouse(
+ iced::input::mouse::Event::Input {
+ state: iced::input::ButtonState::Released,
+ button: iced::input::mouse::Button::Left, // TODO: Map `button`
+ },
+ ));
+ }
+
+ fn mouse_motion_event(
+ &mut self,
+ _context: &mut ggez::Context,
+ x: f32,
+ y: f32,
+ _dx: f32,
+ _dy: f32,
+ ) {
+ self.events.push(iced::Event::Mouse(
+ iced::input::mouse::Event::CursorMoved { x, y },
+ ));
+ }
+
+ fn resize_event(
+ &mut self,
+ context: &mut ggez::Context,
+ width: f32,
+ height: f32,
+ ) {
+ graphics::set_screen_coordinates(
+ context,
+ graphics::Rect {
+ x: 0.0,
+ y: 0.0,
+ w: width,
+ h: height,
+ },
+ )
+ .expect("Set screen coordinates");
+ }
+
+ fn draw(&mut self, context: &mut ggez::Context) -> ggez::GameResult {
+ graphics::clear(context, graphics::WHITE);
+
+ let screen = graphics::screen_coordinates(context);
+
+ let (messages, cursor) = {
+ let layout = self.tour.layout();
+
+ let content = Column::new()
+ .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(),
+ self.font,
+ );
+
+ let mut ui = iced::UserInterface::build(
+ content,
+ self.cache.take().unwrap(),
+ renderer,
+ );
+
+ let messages = ui.update(self.events.drain(..));
+ let cursor = ui.draw(renderer);
+
+ self.cache = Some(ui.into_cache());
+
+ renderer.flush();
+
+ (messages, cursor)
+ };
+
+ for message in messages {
+ self.tour.react(message);
+ }
+
+ mouse::set_cursor_type(context, into_cursor_type(cursor));
+
+ graphics::present(context)?;
+ Ok(())
+ }
+}
+
+fn into_cursor_type(cursor: iced::MouseCursor) -> mouse::MouseCursor {
+ match cursor {
+ iced::MouseCursor::OutOfBounds => mouse::MouseCursor::Default,
+ iced::MouseCursor::Idle => mouse::MouseCursor::Default,
+ iced::MouseCursor::Pointer => mouse::MouseCursor::Hand,
+ iced::MouseCursor::Working => mouse::MouseCursor::Progress,
+ iced::MouseCursor::Grab => mouse::MouseCursor::Grab,
+ iced::MouseCursor::Grabbing => mouse::MouseCursor::Grabbing,
+ }
+}
diff --git a/examples/tour/renderer.rs b/examples/tour/renderer.rs
new file mode 100644
index 00000000..8746dd96
--- /dev/null
+++ b/examples/tour/renderer.rs
@@ -0,0 +1,63 @@
+mod button;
+mod checkbox;
+mod debugger;
+mod image;
+mod radio;
+mod slider;
+mod text;
+
+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,
+ font: Font,
+ ) -> Renderer {
+ Renderer {
+ context,
+ sprites: SpriteBatch::new(spritesheet.clone()),
+ spritesheet,
+ font,
+ font_size: 20.0,
+ debug_mesh: None,
+ }
+ }
+
+ pub fn flush(&mut self) {
+ graphics::draw(
+ self.context,
+ &self.sprites,
+ graphics::DrawParam::default(),
+ )
+ .expect("Draw sprites");
+
+ graphics::draw_queued_text(
+ self.context,
+ graphics::DrawParam::default(),
+ Default::default(),
+ 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");
+ }
+ }
+}
diff --git a/examples/tour/renderer/button.rs b/examples/tour/renderer/button.rs
new file mode 100644
index 00000000..486e07ed
--- /dev/null
+++ b/examples/tour/renderer/button.rs
@@ -0,0 +1,145 @@
+use super::Renderer;
+use ggez::graphics::{
+ self, Align, Color, DrawParam, Rect, Scale, Text, TextFragment, WHITE,
+};
+use iced::{button, MouseCursor};
+
+const LEFT: Rect = Rect {
+ x: 0.0,
+ y: 34.0,
+ w: 6.0,
+ h: 49.0,
+};
+
+const BACKGROUND: Rect = Rect {
+ x: LEFT.w,
+ y: LEFT.y,
+ w: 1.0,
+ h: LEFT.h,
+};
+
+const RIGHT: Rect = Rect {
+ x: LEFT.h - LEFT.w,
+ y: LEFT.y,
+ w: LEFT.w,
+ h: LEFT.h,
+};
+
+impl button::Renderer for Renderer<'_> {
+ fn draw(
+ &mut self,
+ cursor_position: iced::Point,
+ mut bounds: iced::Rectangle,
+ state: &button::State,
+ label: &str,
+ class: button::Class,
+ ) -> MouseCursor {
+ let mouse_over = bounds.contains(cursor_position);
+
+ let mut state_offset = 0.0;
+
+ if mouse_over {
+ if state.is_pressed() {
+ bounds.y += 4.0;
+ state_offset = RIGHT.x + RIGHT.w;
+ } else {
+ bounds.y -= 1.0;
+ }
+ }
+
+ let class_index = match class {
+ button::Class::Primary => 0,
+ button::Class::Secondary => 1,
+ button::Class::Positive => 2,
+ };
+
+ let width = self.spritesheet.width() as f32;
+ let height = self.spritesheet.height() as f32;
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (LEFT.x + state_offset) / width,
+ y: (LEFT.y + class_index as f32 * LEFT.h) / height,
+ w: LEFT.w / width,
+ h: LEFT.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x,
+ y: bounds.y,
+ },
+ ..DrawParam::default()
+ });
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (BACKGROUND.x + state_offset) / width,
+ y: (BACKGROUND.y + class_index as f32 * BACKGROUND.h) / height,
+ w: BACKGROUND.w / width,
+ h: BACKGROUND.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x + LEFT.w,
+ y: bounds.y,
+ },
+ scale: ggez::mint::Vector2 {
+ x: bounds.width - LEFT.w - RIGHT.w,
+ y: 1.0,
+ },
+ ..DrawParam::default()
+ });
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (RIGHT.x + state_offset) / width,
+ y: (RIGHT.y + class_index as f32 * RIGHT.h) / height,
+ w: RIGHT.w / width,
+ h: RIGHT.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x + bounds.width - RIGHT.w,
+ y: bounds.y,
+ },
+ ..DrawParam::default()
+ });
+
+ 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()
+ });
+
+ text.set_bounds(
+ ggez::mint::Point2 {
+ x: bounds.width,
+ y: bounds.height,
+ },
+ Align::Center,
+ );
+
+ graphics::queue_text(
+ self.context,
+ &text,
+ ggez::mint::Point2 {
+ x: bounds.x,
+ y: bounds.y + BACKGROUND.h / 4.0,
+ },
+ Some(if mouse_over {
+ WHITE
+ } else {
+ Color {
+ r: 0.9,
+ g: 0.9,
+ b: 0.9,
+ a: 1.0,
+ }
+ }),
+ );
+
+ if mouse_over {
+ MouseCursor::Pointer
+ } else {
+ MouseCursor::OutOfBounds
+ }
+ }
+}
diff --git a/examples/tour/renderer/checkbox.rs b/examples/tour/renderer/checkbox.rs
new file mode 100644
index 00000000..20a91be5
--- /dev/null
+++ b/examples/tour/renderer/checkbox.rs
@@ -0,0 +1,64 @@
+use super::Renderer;
+
+use ggez::graphics::{DrawParam, Rect};
+use iced::{checkbox, MouseCursor};
+
+const SPRITE: Rect = Rect {
+ x: 98.0,
+ y: 0.0,
+ w: 28.0,
+ h: 28.0,
+};
+
+impl checkbox::Renderer for Renderer<'_> {
+ fn draw(
+ &mut self,
+ cursor_position: iced::Point,
+ bounds: iced::Rectangle,
+ text_bounds: iced::Rectangle,
+ is_checked: bool,
+ ) -> MouseCursor {
+ let mouse_over = bounds.contains(cursor_position)
+ || text_bounds.contains(cursor_position);
+
+ let width = self.spritesheet.width() as f32;
+ let height = self.spritesheet.height() as f32;
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (SPRITE.x + (if mouse_over { SPRITE.w } else { 0.0 }))
+ / width,
+ y: SPRITE.y / height,
+ w: SPRITE.w / width,
+ h: SPRITE.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x,
+ y: bounds.y,
+ },
+ ..DrawParam::default()
+ });
+
+ if is_checked {
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (SPRITE.x + SPRITE.w * 2.0) / width,
+ y: SPRITE.y / height,
+ w: SPRITE.w / width,
+ h: SPRITE.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x,
+ y: bounds.y,
+ },
+ ..DrawParam::default()
+ });
+ }
+
+ if mouse_over {
+ MouseCursor::Pointer
+ } else {
+ MouseCursor::OutOfBounds
+ }
+ }
+}
diff --git a/examples/tour/renderer/debugger.rs b/examples/tour/renderer/debugger.rs
new file mode 100644
index 00000000..98124795
--- /dev/null
+++ b/examples/tour/renderer/debugger.rs
@@ -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);
+ }
+ }
+}
diff --git a/examples/tour/renderer/image.rs b/examples/tour/renderer/image.rs
new file mode 100644
index 00000000..c3ead5c9
--- /dev/null
+++ b/examples/tour/renderer/image.rs
@@ -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");
+ }
+}
diff --git a/examples/tour/renderer/radio.rs b/examples/tour/renderer/radio.rs
new file mode 100644
index 00000000..0f7815d6
--- /dev/null
+++ b/examples/tour/renderer/radio.rs
@@ -0,0 +1,63 @@
+use super::Renderer;
+
+use ggez::graphics::{DrawParam, Rect};
+use iced::{radio, MouseCursor, Point, Rectangle};
+
+const SPRITE: Rect = Rect {
+ x: 98.0,
+ y: 28.0,
+ w: 28.0,
+ h: 28.0,
+};
+
+impl radio::Renderer for Renderer<'_> {
+ fn draw(
+ &mut self,
+ cursor_position: Point,
+ bounds: Rectangle,
+ bounds_with_label: Rectangle,
+ is_selected: bool,
+ ) -> MouseCursor {
+ let mouse_over = bounds_with_label.contains(cursor_position);
+
+ let width = self.spritesheet.width() as f32;
+ let height = self.spritesheet.height() as f32;
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (SPRITE.x + (if mouse_over { SPRITE.w } else { 0.0 }))
+ / width,
+ y: SPRITE.y / height,
+ w: SPRITE.w / width,
+ h: SPRITE.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x,
+ y: bounds.y,
+ },
+ ..DrawParam::default()
+ });
+
+ if is_selected {
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (SPRITE.x + SPRITE.w * 2.0) / width,
+ y: SPRITE.y / height,
+ w: SPRITE.w / width,
+ h: SPRITE.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x,
+ y: bounds.y,
+ },
+ ..DrawParam::default()
+ });
+ }
+
+ if mouse_over {
+ MouseCursor::Pointer
+ } else {
+ MouseCursor::OutOfBounds
+ }
+ }
+}
diff --git a/examples/tour/renderer/slider.rs b/examples/tour/renderer/slider.rs
new file mode 100644
index 00000000..146cee18
--- /dev/null
+++ b/examples/tour/renderer/slider.rs
@@ -0,0 +1,82 @@
+use super::Renderer;
+
+use ggez::graphics::{DrawParam, Rect};
+use iced::{slider, MouseCursor, Point, Rectangle};
+use std::ops::RangeInclusive;
+
+const RAIL: Rect = Rect {
+ x: 98.0,
+ y: 56.0,
+ w: 1.0,
+ h: 4.0,
+};
+
+const MARKER: Rect = Rect {
+ x: RAIL.x + 28.0,
+ y: RAIL.y,
+ w: 16.0,
+ h: 24.0,
+};
+
+impl slider::Renderer for Renderer<'_> {
+ fn draw(
+ &mut self,
+ cursor_position: Point,
+ bounds: Rectangle,
+ state: &slider::State,
+ range: RangeInclusive<f32>,
+ value: f32,
+ ) -> MouseCursor {
+ let width = self.spritesheet.width() as f32;
+ let height = self.spritesheet.height() as f32;
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: RAIL.x / width,
+ y: RAIL.y / height,
+ w: RAIL.w / width,
+ h: RAIL.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x + MARKER.w as f32 / 2.0,
+ y: bounds.y + 12.5,
+ },
+ scale: ggez::mint::Vector2 {
+ x: bounds.width - MARKER.w as f32,
+ y: 1.0,
+ },
+ ..DrawParam::default()
+ });
+
+ let (range_start, range_end) = range.into_inner();
+
+ let marker_offset = (bounds.width - MARKER.w as f32)
+ * ((value - range_start) / (range_end - range_start).max(1.0));
+
+ let mouse_over = bounds.contains(cursor_position);
+ let is_active = state.is_dragging() || mouse_over;
+
+ self.sprites.add(DrawParam {
+ src: Rect {
+ x: (MARKER.x + (if is_active { MARKER.w } else { 0.0 }))
+ / width,
+ y: MARKER.y / height,
+ w: MARKER.w / width,
+ h: MARKER.h / height,
+ },
+ dest: ggez::mint::Point2 {
+ x: bounds.x + marker_offset.round(),
+ y: bounds.y + (if state.is_dragging() { 2.0 } else { 0.0 }),
+ },
+ ..DrawParam::default()
+ });
+
+ if state.is_dragging() {
+ MouseCursor::Grabbing
+ } else if mouse_over {
+ MouseCursor::Grab
+ } else {
+ MouseCursor::OutOfBounds
+ }
+ }
+}
diff --git a/examples/tour/renderer/text.rs b/examples/tour/renderer/text.rs
new file mode 100644
index 00000000..ecf1481e
--- /dev/null
+++ b/examples/tour/renderer/text.rs
@@ -0,0 +1,118 @@
+use super::Renderer;
+use ggez::graphics::{self, mint, Align, Color, Scale, Text, TextFragment};
+
+use iced::text;
+use std::cell::RefCell;
+use std::f32;
+
+impl text::Renderer<Color> for Renderer<'_> {
+ 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);
+
+ // TODO: Investigate why stretch tries to measure this MANY times
+ // with every ancestor's bounds.
+ // Bug? Using the library wrong? I should probably open an issue on
+ // the stretch repository.
+ // 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();
+
+ if measure.is_none() {
+ let bounds = (
+ match bounds.width {
+ iced::Number::Undefined => f32::INFINITY,
+ iced::Number::Defined(w) => w,
+ },
+ match bounds.height {
+ iced::Number::Undefined => f32::INFINITY,
+ iced::Number::Defined(h) => h,
+ },
+ );
+
+ let mut text = Text::new(TextFragment {
+ text: content.clone(),
+ font: Some(font),
+ scale: Some(Scale { x: size, y: size }),
+ ..Default::default()
+ });
+
+ text.set_bounds(
+ mint::Point2 {
+ x: bounds.0,
+ y: bounds.1,
+ },
+ Align::Left,
+ );
+
+ let (width, height) = font_cache.dimensions(&text);
+
+ let size = iced::Size {
+ width: width as f32,
+ height: height as f32,
+ };
+
+ // If the text has no width boundary we avoid caching as the
+ // layout engine may just be measuring text in a row.
+ if bounds.0 == f32::INFINITY {
+ return size;
+ } else {
+ *measure = Some(size);
+ }
+ }
+
+ measure.unwrap()
+ })
+ }
+
+ fn draw(
+ &mut self,
+ bounds: iced::Rectangle,
+ content: &str,
+ 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()
+ });
+
+ text.set_bounds(
+ mint::Point2 {
+ x: bounds.width,
+ y: bounds.height,
+ },
+ match horizontal_alignment {
+ text::HorizontalAlignment::Left => graphics::Align::Left,
+ text::HorizontalAlignment::Center => graphics::Align::Center,
+ text::HorizontalAlignment::Right => graphics::Align::Right,
+ },
+ );
+
+ graphics::queue_text(
+ self.context,
+ &text,
+ mint::Point2 {
+ x: bounds.x,
+ y: bounds.y,
+ },
+ color.or(Some(graphics::BLACK)),
+ );
+ }
+}
diff --git a/examples/tour/tour.rs b/examples/tour/tour.rs
new file mode 100644
index 00000000..c2126675
--- /dev/null
+++ b/examples/tour/tour.rs
@@ -0,0 +1,578 @@
+use super::widget::{
+ button, slider, Button, Checkbox, Column, Element, Image, Radio, Row,
+ Slider, Text,
+};
+
+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(context: &mut Context) -> Tour {
+ Tour {
+ steps: Steps::new(context),
+ back_button: button::State::new(),
+ next_button: button::State::new(),
+ debug: false,
+ }
+ }
+
+ pub fn react(&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);
+ }
+ }
+ }
+
+ pub fn layout(&mut self) -> Element<Message> {
+ let Tour {
+ steps,
+ back_button,
+ next_button,
+ ..
+ } = self;
+
+ let mut controls = Row::new();
+
+ if steps.has_previous() {
+ controls = controls.push(
+ Button::new(back_button, "Back")
+ .on_press(Message::BackPressed)
+ .class(button::Class::Secondary),
+ );
+ }
+
+ controls = controls.push(Column::new());
+
+ if steps.can_continue() {
+ controls = controls.push(
+ Button::new(next_button, "Next").on_press(Message::NextPressed),
+ );
+ }
+
+ let element: Element<_> = Column::new()
+ .max_width(500)
+ .spacing(20)
+ .push(steps.layout(self.debug).map(Message::StepMessage))
+ .push(controls)
+ .into();
+
+ if self.debug {
+ element.explain(BLACK)
+ } else {
+ element
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum Message {
+ BackPressed,
+ NextPressed,
+ StepMessage(StepMessage),
+}
+
+struct Steps {
+ steps: Vec<Step>,
+ current: usize,
+}
+
+impl Steps {
+ fn new(context: &mut Context) -> 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: BLACK,
+ },
+ 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, debug: &mut bool) {
+ self.steps[self.current].update(msg, debug);
+ }
+
+ fn layout(&mut self, debug: bool) -> Element<StepMessage> {
+ self.steps[self.current].layout(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()
+ }
+}
+
+enum Step {
+ Welcome,
+ 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,
+ },
+ Radio {
+ selection: Option<Language>,
+ },
+ Image {
+ ferris: graphics::Image,
+ width: u16,
+ slider: slider::State,
+ },
+ Debugger,
+ End,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum StepMessage {
+ SliderChanged(f32),
+ LayoutChanged(Layout),
+ SpacingChanged(f32),
+ TextSizeChanged(f32),
+ TextColorChanged(Color),
+ LanguageSelected(Language),
+ ImageWidthChanged(f32),
+ DebugToggled(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.round() as u16;
+ }
+ }
+ StepMessage::TextSizeChanged(new_size) => {
+ if let Step::Text { size, .. } = self {
+ *size = new_size.round() as u16;
+ }
+ }
+ 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.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::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, debug: bool) -> Element<StepMessage> {
+ match self {
+ Step::Welcome => Self::welcome().into(),
+ Step::Radio { selection } => Self::radio(*selection).into(),
+ Step::Slider { state, value } => Self::slider(state, *value).into(),
+ Step::Text {
+ size_slider,
+ size,
+ 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,
+ spacing,
+ } => {
+ Self::rows_and_columns(*layout, spacing_slider, *spacing).into()
+ }
+ Step::Debugger => Self::debugger(debug).into(),
+ Step::End => Self::end().into(),
+ }
+ }
+
+ fn container(title: &str) -> Column<'a, StepMessage> {
+ Column::new()
+ .spacing(20)
+ .align_items(Align::Stretch)
+ .push(Text::new(title).size(50))
+ }
+
+ fn welcome() -> Column<'a, StepMessage> {
+ Self::container("Welcome!")
+ .push(Text::new(
+ "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 \
+ end!",
+ ))
+ }
+
+ fn slider(
+ state: &'a mut slider::State,
+ value: u16,
+ ) -> 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.0..=100.0,
+ value as f32,
+ StepMessage::SliderChanged,
+ ))
+ .push(
+ Text::new(&value.to_string())
+ .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.0..=80.0,
+ spacing as f32,
+ StepMessage::SpacingChanged,
+ ))
+ .push(
+ Text::new(&format!("{} px", spacing))
+ .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.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 updated as more features are added.",
+ ))
+ .push(Text::new("Make sure to keep an eye on it!"))
+ }
+}
+
+#[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 &str {
+ fn from(language: Language) -> &'static str {
+ 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,
+}
diff --git a/examples/tour/widget.rs b/examples/tour/widget.rs
new file mode 100644
index 00000000..9a141c83
--- /dev/null
+++ b/examples/tour/widget.rs
@@ -0,0 +1,14 @@
+use super::Renderer;
+
+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>>;
+pub type Element<'a, Message> = iced::Element<'a, Message, Renderer<'a>>;