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:
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.
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]
}
}
}
You are very close already.
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,
}
Use the Message
enum from the octagon
module in your Application
implementation:
type Message = octagon::Message;
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]
}
}
}
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]
}