rustrust-iced

How to create a table of custom shapes in iced (Rust GUI framework)?


I'm trying to create a GUI for a board game engine. To that end I'm trying to transform the State (A struct with a grid where game pieces can be) into a rendered grid. I decided to try and do this with Iced, but I'm having trouble getting a custom shape into the grid structures in Iced. I should add that I'm pretty new to rust in general, and so my problems might not be with understanding Iced specifically.

I tried a few approaches, mainly these:

  1. Implement the custom widget trait for the octagon, based on the custom_widget example from the Iced repository. I did not keep the code I used for this attempt, but at some point I realized (I think) that Iced has two classes of widgets, built-in and custom, where the custom ones do not promise full widget behavior and the built-in ones are harder to implement.

  2. Try to encapsulate the struct in a Canvas widget, and implement canvas::Program<Message> with my own draw function.

Both of these resulted in a similar compilation error, when writing the view function for my implementation of Application:

fn view(&self) -> iced::Element<'_, Self::Message> {
    let matrix = self.get_board();
    let mut base = Column::new().padding(20).spacing(20);
    for row in matrix {
        let mut gui_row = Row::new().padding(20).spacing(20);
        for value in row {
            // ...get details about the specific piece in 
            //    this cell
            let oct = crate::gui::octagon::Octagon::new(30.);
            // this is the line I want to change, and push a custom
            // struct with an octagonal shape instead of a button
            gui_row = gui_row.push(Button::new("Pushing a button works"));
        }
        base = base.push(gui_row);
    }
    base.into()
}

Where I get the following:

the trait bound `iced::advanced::iced_graphics::iced_core::Element<'_, (), iced_renderer::Renderer<Theme>>: From<Column<'_, Message>>` is not satisfied
the following other types implement trait `From<T>`:
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<Column<'a, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<MouseArea<'a, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<Row<'a, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<iced::widget::Button<'a, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<iced::widget::Checkbox<'a, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<ComboBox<'a, T, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<iced::widget::Container<'a, Message, Renderer>>>
  <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Renderer> as From<menu::List<'a, T, Message, Renderer>>>
and 15 others
required for `Column<'_, Message>` to implement `Into<iced::advanced::iced_graphics::iced_core::Element<'_, (), iced_renderer::Renderer<Theme>>>`

A minimal(ish) reproduceable example:

use iced::widget::{Canvas, Column, Row, Button};
use iced::{executor, Color, Application, Command, Settings, Theme};


#[derive(Copy, Clone)]
struct Piece; // some sort of Octagon

struct State {
    board: [[Option<Piece>; 4]; 4]
}

impl Application for State {
    type Executor = executor::Default;
    type Flags = ();
    type Message = ();
    type Theme = Theme;

    fn new(_flags: ()) -> (State, Command<Self::Message>) {
        (State {board: [[None; 4]; 4]}, Command::none())
    }

    fn title(&self) -> String {
        String::from("A cool application")
    }

    fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
        Command::none()
    }

    fn view(&self) -> iced::Element<'_, Self::Message> {
        let matrix = self.board;
        let mut base = Column::new().padding(20).spacing(20);
        for row in matrix {
            let mut gui_row = Row::new().padding(20).spacing(20);
            for value in row {
                let color = match value {
                    Some(piece) => Color::BLACK,
                    None => Color::WHITE,
                };
                let oct = crate::octagon::Octagon::new(30.);
                gui_row = gui_row.push(Canvas::new(oct));
            }
            base = base.push(gui_row);
        }
        base.into()
    }
}

pub fn main() -> iced::Result {
    State::run(Settings::default())
}

mod octagon {
    use iced::{Rectangle, mouse, Renderer, Theme, Point, widget};
    use iced::widget::{canvas, container};
    use iced::widget::canvas::{Event, event, Cache, Path};

    pub enum Message {
        None
    }

    pub struct Octagon {
        size: f32,
        cache: Cache
    }

    impl Octagon {
        pub fn new(size: f32) -> Octagon{
            Octagon {size: size, cache: Cache::new()}
        }
    }

    impl canvas::Program<Message> for Octagon {
        type State = ();

        fn update(
            &self,
            _state: &mut Self::State,
            event: Event,
            bounds: Rectangle,
            cursor: mouse::Cursor,
        ) -> (event::Status, Option<Message>) {
            todo!()
        }

        fn draw(
            &self,
            _state: &Self::State,
            renderer: &Renderer,
            _theme: &Theme,
            bounds: Rectangle,
            _cursor: mouse::Cursor,
        ) -> Vec<canvas::Geometry> {
            let geom = self.cache.draw(renderer, bounds.size(), |frame| {
                // this is not an octagon, but it's some sort 
                // of geometric shape that I can't render
                frame.stroke(            
                    &Path::new(|builder| {
                        builder.line_to(Point { x: 0., y: 0. });
                        builder.line_to(Point { x: 206., y: 131. });
                        builder.line_to(Point { x: 9., y: 699. });
                        builder.close();
                    }),
                    canvas::Stroke::default(),
                );
            });

            vec![geom]
        }
    }
}

Solution

  • You are very close already.

    1. The Message enum must be exported (so that it can be used as an Associated Type in the Application implmentation) and implement Debug (it must implement Debug, because of this trait bound):
    #[derive(Debug)]
    pub(crate) enum Message {
        None,
    }
    
    1. Use the Message enum from the octagon module in your Application implementation: type Message = octagon::Message;

    2. This is not needed for compilation but otherwise the program will panic at runtime. Change the update function of the canvas::Program<Message> implementation, to for example:

    fn update(
        &self,
        _state: &mut Self::State,
        event: Event,
        bounds: Rectangle,
        cursor: mouse::Cursor,
    ) -> (Status, Option<Message>) {
        (Status::Ignored, None)
    }
    

    Here is the full fixed sample:

    use iced::widget::{Canvas, Column, Row};
    use iced::{executor, Application, Color, Command, Settings, Theme};
    
    #[derive(Copy, Clone)]
    struct Piece; // some sort of Octagon
    
    struct State {
        board: [[Option<Piece>; 4]; 4],
    }
    
    impl Application for State {
        type Executor = executor::Default;
        type Flags = ();
        type Message = octagon::Message;
        type Theme = Theme;
    
        fn new(_flags: ()) -> (State, Command<Self::Message>) {
            (
                State {
                    board: [[None; 4]; 4],
                },
                Command::none(),
            )
        }
    
        fn title(&self) -> String {
            String::from("A cool application")
        }
    
        fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
            Command::none()
        }
    
        fn view(&self) -> iced::Element<Self::Message> {
            let matrix = self.board;
            let mut base = Column::new().padding(20).spacing(20);
            for row in matrix {
                let mut gui_row = Row::new().padding(20).spacing(20);
                for value in row {
                    let color = match value {
                        Some(piece) => Color::BLACK,
                        None => Color::WHITE,
                    };
                    let oct = crate::octagon::Octagon::new(30.);
                    gui_row = gui_row.push(Canvas::new(oct));
                }
                base = base.push(gui_row);
            }
            base.into()
        }
    }
    
    pub fn main() -> iced::Result {
        State::run(Settings::default())
    }
    
    mod octagon {
        use iced::event::Status;
        use iced::widget::canvas;
        use iced::widget::canvas::{Cache, Event, Path};
        use iced::{mouse, Point, Rectangle, Renderer, Theme};
    
        #[derive(Debug)]
        pub(crate) enum Message {
            None,
        }
    
        pub struct Octagon {
            size: f32,
            cache: Cache,
        }
    
        impl Octagon {
            pub fn new(size: f32) -> Octagon {
                Octagon {
                    size: size,
                    cache: Cache::new(),
                }
            }
        }
    
        impl canvas::Program<Message> for Octagon {
            type State = ();
    
            fn update(
                &self,
                _state: &mut Self::State,
                event: Event,
                bounds: Rectangle,
                cursor: mouse::Cursor,
            ) -> (Status, Option<Message>) {
                (Status::Ignored, None)
            }
    
            fn draw(
                &self,
                _state: &Self::State,
                renderer: &Renderer,
                _theme: &Theme,
                bounds: Rectangle,
                _cursor: mouse::Cursor,
            ) -> Vec<canvas::Geometry> {
                let geom = self.cache.draw(renderer, bounds.size(), |frame| {
                    // this is not an octagon, but it's some sort
                    // of geometric shape that I can't render
                    frame.stroke(
                        &Path::new(|builder| {
                            builder.line_to(Point { x: 0., y: 0. });
                            builder.line_to(Point { x: 206., y: 131. });
                            builder.line_to(Point { x: 9., y: 699. });
                            builder.close();
                        }),
                        canvas::Stroke::default(),
                    );
                });
    
                vec![geom]
            }
        }
    }
    

    Giving you this output: Iced Canvas Screenshot

    If you want to render actual octagons you can change the draw function to:

    fn draw(
        &self,
        _state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<canvas::Geometry> {
        let geom = self.cache.draw(renderer, bounds.size(), |frame| {
            frame.stroke(
                &Path::new(|builder| {
                    let r = 50.;
                    for i in 0..8 {
                        let angle_rad = 360. / 8. * i as f32 * std::f32::consts::PI / 180.;
                        builder.line_to(Point {
                            x: angle_rad.cos() * r + r,
                            y: angle_rad.sin() * r + r,
                        });
                    }
    
                    builder.close();
                }),
                canvas::Stroke::default(),
            );
        });
    
        vec![geom]
    }