Add Grid helper for grid-based layout#43
Conversation
|
I honestly feel that both the implementation and the user-facing API would be much cleaner with a callback approach. For starters, I suggest leaving aside the question of configurability and use a builder pattern for the I suggest implementing for that kind of API: fn update(&mut self, sketch: &mut Sketch, _ctx: &mut Context) -> anyhow::Result<()> {
Grid::from_total_size(400.0, 300.0) // alt: .from_cell_size(dx, dy)?
.rows(12)
.columns(15)
.spacing(15.) // alternative: .vertical_spacing / .horizontal_spacing
// other parameters?
.transform(true) // this has to be optional for perf reason
//.scale(true) // that would be neat, to map the cell area to x = 0..1, y = 0..1
.build(sketch, |sketch, cell| {
sketch.rect(0.0, 0.0, cell.width, cell.height);
});
}Then |
abey79
left a comment
There was a problem hiding this comment.
I love this feature already, but let's go the extra mile and give it the best API :)
| /// Canvas position | ||
| pub canvas_position: Point, | ||
| /// User-defined data | ||
| pub data: Option<T>, |
There was a problem hiding this comment.
The implementation will be much cleaner without having to deal with any user data
There was a problem hiding this comment.
good idea with the builder pattern. Idk why, but I used it when implementing the very same grid in JS, but forgot it when doing same in Rust.
| pub enum GridSize { | ||
| /// Set cell size, grid's size will be computed | ||
| CellBased([f64; 2]), | ||
| /// Set fixed grid size | ||
| GridBased([f64; 2]), | ||
| } |
There was a problem hiding this comment.
Ideally we can make this sort of thing an implementation detail (i.e. not pub) and expose it through nice builder pattern.
| /// ); | ||
| /// grid.init(None); | ||
| /// ``` | ||
| pub struct Grid<'a, T> { |
There was a problem hiding this comment.
Grid is a much better name than GridHelper or whatever else 👍🏻
| /// Creates new instance of the grid with empty data vector | ||
| #[allow(clippy::too_many_arguments)] | ||
| pub fn new( | ||
| sketch: &'a mut Sketch, |
There was a problem hiding this comment.
This sort of annoyance disappears with my proposal.
| pub fn at(&mut self, column: usize, row: usize) -> Option<&mut GridCell<T>> { | ||
| self.data | ||
| .iter_mut() | ||
| .find(|cell| cell.column == column && cell.row == row) | ||
| } | ||
|
|
||
| /// Resets grid cell's data at specific column and row if applicable | ||
| pub fn reset_at(&mut self, column: usize, row: usize) { | ||
| let cell = self.at(column, row); | ||
|
|
||
| if let Some(cell) = cell { | ||
| cell.reset_data(); | ||
| } | ||
| } | ||
|
|
||
| /// Resets all grid cell's data | ||
| pub fn reset(&mut self) { | ||
| self.data.iter_mut().for_each(GridCell::reset_data); | ||
| } |
There was a problem hiding this comment.
Being a nice "container of user data" is tough and, again, best avoided altogether.
|
@abey79 I made a refactor with builder pattern, but without |
abey79
left a comment
There was a problem hiding this comment.
This looks good. Here are bunch of comments to make it even better. The main thing is that I think we should adhere rather strictly to the builder pattern in this case. This makes for better performance and more obvious/standard API.
| let c = cell.clone(); | ||
| sketch.color(Color::RED); | ||
| sketch.add_path(c); |
There was a problem hiding this comment.
I don't like this clone, this is unergonomic. Let's have IntoBezPathTolerance implemented for &GridCell, so we don't need it.
| if let Some(marked_cell) = grid.at(self.marked_cell_col, self.marked_cell_row) { | ||
| sketch.color(Color::GREEN); | ||
| sketch.add_path(marked_cell.clone()); | ||
| } |
There was a problem hiding this comment.
It's weird to me that there are two ways to access a GridCell: via the closure, and later on because they are somehow stored. If GridCell contains row/col information, it's very easy to add this to the closure:
sketch.color(if cell.row == self.marked_cell_row && cell.col == self.marked_cell_col {
Color::GREEN
} else {
Color::RED
});I would remove that GridCell storage all together. If the user really needs to store those cell, it can be done via the closure.
| @@ -0,0 +1,217 @@ | |||
| //! | |||
| } | ||
|
|
||
| /// Stores basic grid's cell data, like column, row and canvas position | ||
| #[derive(Clone)] |
There was a problem hiding this comment.
Let's add the usual suspects (Debug, PartialEq etc.) we don't know what the user might need to do with these.
| column: usize, | ||
| row: usize, | ||
| position: Point, | ||
| size: [f64; 2], |
There was a problem hiding this comment.
These fields need to be pub and well documented (in particular, what position wrt to the current transform, etc.). We don't know what the user might want to do with those.
| /// Aligns grid cells to specific dimensions | ||
| pub fn align_to(&mut self, rect_dimensions: [f64; 2]) { | ||
| let [width, height] = self.module_size(); | ||
| let diff_x = rect_dimensions[0] / width; | ||
| let diff_y = rect_dimensions[1] / height; | ||
|
|
||
| self.data.iter_mut().for_each(|cell| { | ||
| cell.position = Point::new(cell.position.x() + diff_x, cell.position.y() + diff_y); | ||
| }); | ||
| } |
There was a problem hiding this comment.
I'm not sure I understand what this does, but it should follow the builder pattern.
There was a problem hiding this comment.
It was for aligning the grid to a given artboard rectangle, but since we're able to align things to the sketch with existing sketch.center method I think we can delete it
|
|
||
| /// Overrides grid's current number of rows. | ||
| /// By default, grid instance will have 4 rows. | ||
| pub fn rows(&mut self, value: usize) -> &mut Self { |
There was a problem hiding this comment.
In this case, we should adhere strictly to the builder pattern, so take self and return Self. Likewise for all other functions.
There was a problem hiding this comment.
honestly, I didn't know once could use fn rows(self, value: usize) -> Self with just self in return instead of creating everything from scratch there, like: Self { /* properties go here */ } 😅
| /// Returns width of the grid | ||
| #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] | ||
| pub fn width(&self) -> f64 { | ||
| let columns = self.dimensions[0] as f64; | ||
| columns * self.module_size()[0] + columns * self.gutter[0] | ||
| } | ||
|
|
||
| /// Returns height of the grid | ||
| #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] | ||
| pub fn height(&self) -> f64 { | ||
| let rows = self.dimensions[0] as f64; | ||
| rows * self.module_size()[1] + rows * self.gutter[1] | ||
| } |
There was a problem hiding this comment.
I've got mixed feelings about these. They could be useful, but they to fit the builder pattern well. That info could be part of the GridCell (to be made available to the closure).
There was a problem hiding this comment.
good point. I just treated these methods as "getters"
| @@ -0,0 +1,70 @@ | |||
| use whiskers::{grid::Grid, prelude::*}; | |||
There was a problem hiding this comment.
Grid should be in the prelude so that use here is not needed.
|
|
||
| mod context; | ||
|
|
||
| pub mod grid; |
There was a problem hiding this comment.
Let's remove pub here and pull everything in the top-level namespace with pub use grid::{/*:..*/}
abey79
left a comment
There was a problem hiding this comment.
Looks great now! A nice, clean mvp feature.
| grid.columns(self.columns) | ||
| .rows(self.rows) | ||
| .spacing([self.gutter_width, self.gutter_height]) | ||
| .build(sketch, |sketch, cell| { | ||
| sketch.color( | ||
| if cell.row == self.marked_cell_row && cell.column == self.marked_cell_col { | ||
| Color::GREEN | ||
| } else { | ||
| Color::RED | ||
| }, | ||
| ); | ||
| sketch.add_path(cell); | ||
| }); |
Grid helper for grid-based layout
No description provided.