From ced3ffc22570048711fefba638782a31d0e06035 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 5 Sep 2019 07:23:03 +0200 Subject: Move `ggez` example to `tour` --- examples/ggez/main.rs | 187 ------------ examples/ggez/renderer.rs | 63 ---- examples/ggez/renderer/button.rs | 145 ---------- examples/ggez/renderer/checkbox.rs | 64 ---- examples/ggez/renderer/debugger.rs | 30 -- examples/ggez/renderer/image.rs | 51 ---- examples/ggez/renderer/radio.rs | 63 ---- examples/ggez/renderer/slider.rs | 82 ------ examples/ggez/renderer/text.rs | 118 -------- examples/ggez/tour.rs | 578 ------------------------------------- examples/ggez/widget.rs | 14 - examples/tour/main.rs | 187 ++++++++++++ examples/tour/renderer.rs | 63 ++++ examples/tour/renderer/button.rs | 145 ++++++++++ examples/tour/renderer/checkbox.rs | 64 ++++ examples/tour/renderer/debugger.rs | 30 ++ examples/tour/renderer/image.rs | 51 ++++ examples/tour/renderer/radio.rs | 63 ++++ examples/tour/renderer/slider.rs | 82 ++++++ examples/tour/renderer/text.rs | 118 ++++++++ examples/tour/tour.rs | 578 +++++++++++++++++++++++++++++++++++++ examples/tour/widget.rs | 14 + 22 files changed, 1395 insertions(+), 1395 deletions(-) delete mode 100644 examples/ggez/main.rs delete mode 100644 examples/ggez/renderer.rs delete mode 100644 examples/ggez/renderer/button.rs delete mode 100644 examples/ggez/renderer/checkbox.rs delete mode 100644 examples/ggez/renderer/debugger.rs delete mode 100644 examples/ggez/renderer/image.rs delete mode 100644 examples/ggez/renderer/radio.rs delete mode 100644 examples/ggez/renderer/slider.rs delete mode 100644 examples/ggez/renderer/text.rs delete mode 100644 examples/ggez/tour.rs delete mode 100644 examples/ggez/widget.rs create mode 100644 examples/tour/main.rs create mode 100644 examples/tour/renderer.rs create mode 100644 examples/tour/renderer/button.rs create mode 100644 examples/tour/renderer/checkbox.rs create mode 100644 examples/tour/renderer/debugger.rs create mode 100644 examples/tour/renderer/image.rs create mode 100644 examples/tour/renderer/radio.rs create mode 100644 examples/tour/renderer/slider.rs create mode 100644 examples/tour/renderer/text.rs create mode 100644 examples/tour/tour.rs create mode 100644 examples/tour/widget.rs (limited to 'examples') diff --git a/examples/ggez/main.rs b/examples/ggez/main.rs deleted file mode 100644 index 0a6a2005..00000000 --- a/examples/ggez/main.rs +++ /dev/null @@ -1,187 +0,0 @@ -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, - cache: Option, -} - -impl Game { - fn new(context: &mut ggez::Context) -> ggez::GameResult { - 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/ggez/renderer.rs b/examples/ggez/renderer.rs deleted file mode 100644 index 8746dd96..00000000 --- a/examples/ggez/renderer.rs +++ /dev/null @@ -1,63 +0,0 @@ -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, -} - -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/ggez/renderer/button.rs b/examples/ggez/renderer/button.rs deleted file mode 100644 index 486e07ed..00000000 --- a/examples/ggez/renderer/button.rs +++ /dev/null @@ -1,145 +0,0 @@ -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/ggez/renderer/checkbox.rs b/examples/ggez/renderer/checkbox.rs deleted file mode 100644 index 20a91be5..00000000 --- a/examples/ggez/renderer/checkbox.rs +++ /dev/null @@ -1,64 +0,0 @@ -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/ggez/renderer/debugger.rs b/examples/ggez/renderer/debugger.rs deleted file mode 100644 index 98124795..00000000 --- a/examples/ggez/renderer/debugger.rs +++ /dev/null @@ -1,30 +0,0 @@ -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/ggez/renderer/image.rs b/examples/ggez/renderer/image.rs deleted file mode 100644 index c3ead5c9..00000000 --- a/examples/ggez/renderer/image.rs +++ /dev/null @@ -1,51 +0,0 @@ -use super::Renderer; - -use ggez::{graphics, nalgebra}; -use iced::image; - -impl image::Renderer for Renderer<'_> { - fn node( - &self, - style: iced::Style, - image: &graphics::Image, - width: Option, - height: Option, - _source: Option>, - ) -> 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>, - ) { - // 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/ggez/renderer/radio.rs b/examples/ggez/renderer/radio.rs deleted file mode 100644 index 0f7815d6..00000000 --- a/examples/ggez/renderer/radio.rs +++ /dev/null @@ -1,63 +0,0 @@ -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/ggez/renderer/slider.rs b/examples/ggez/renderer/slider.rs deleted file mode 100644 index 146cee18..00000000 --- a/examples/ggez/renderer/slider.rs +++ /dev/null @@ -1,82 +0,0 @@ -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, - 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/ggez/renderer/text.rs b/examples/ggez/renderer/text.rs deleted file mode 100644 index ecf1481e..00000000 --- a/examples/ggez/renderer/text.rs +++ /dev/null @@ -1,118 +0,0 @@ -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 for Renderer<'_> { - fn node( - &self, - style: iced::Style, - content: &str, - size: Option, - ) -> 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, - color: Option, - 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/ggez/tour.rs b/examples/ggez/tour.rs deleted file mode 100644 index c2126675..00000000 --- a/examples/ggez/tour.rs +++ /dev/null @@ -1,578 +0,0 @@ -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 { - 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, - 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 { - 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, - }, - 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 { - 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) -> 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 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/ggez/widget.rs b/examples/ggez/widget.rs deleted file mode 100644 index 9a141c83..00000000 --- a/examples/ggez/widget.rs +++ /dev/null @@ -1,14 +0,0 @@ -use super::Renderer; - -use ggez::graphics::{self, Color}; - -pub use iced::{button, slider, Button, Slider}; - -pub type Text = iced::Text; -pub type Checkbox = iced::Checkbox; -pub type Radio = iced::Radio; -pub type Image = iced::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>>; 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, + cache: Option, +} + +impl Game { + fn new(context: &mut ggez::Context) -> ggez::GameResult { + 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, +} + +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 for Renderer<'_> { + fn node( + &self, + style: iced::Style, + image: &graphics::Image, + width: Option, + height: Option, + _source: Option>, + ) -> 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>, + ) { + // 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, + 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 for Renderer<'_> { + fn node( + &self, + style: iced::Style, + content: &str, + size: Option, + ) -> 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, + color: Option, + 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 { + 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, + 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 { + 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, + }, + 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 { + 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) -> 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 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; +pub type Checkbox = iced::Checkbox; +pub type Radio = iced::Radio; +pub type Image = iced::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>>; -- cgit