Skip to content

Add Grid helper for grid-based layout#43

Merged
abey79 merged 16 commits into
abey79:masterfrom
afternoon2:whiskers-grid
Oct 15, 2023
Merged

Add Grid helper for grid-based layout#43
abey79 merged 16 commits into
abey79:masterfrom
afternoon2:whiskers-grid

Conversation

@afternoon2

Copy link
Copy Markdown
Contributor

No description provided.

@abey79

abey79 commented Oct 8, 2023

Copy link
Copy Markdown
Owner

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 Grid.

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 Cell is just a very stupid struct that stores some basic stuff (i, j, width, height, dx, dy, etc.

@abey79 abey79 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this feature already, but let's go the extra mile and give it the best API :)

Comment thread crates/whiskers/src/grid/cell.rs Outdated
/// Canvas position
pub canvas_position: Point,
/// User-defined data
pub data: Option<T>,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation will be much cleaner without having to deal with any user data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/whiskers/src/grid/mod.rs Outdated
Comment on lines +13 to +18
pub enum GridSize {
/// Set cell size, grid's size will be computed
CellBased([f64; 2]),
/// Set fixed grid size
GridBased([f64; 2]),
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we can make this sort of thing an implementation detail (i.e. not pub) and expose it through nice builder pattern.

Comment thread crates/whiskers/src/grid/mod.rs Outdated
/// );
/// grid.init(None);
/// ```
pub struct Grid<'a, T> {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grid is a much better name than GridHelper or whatever else 👍🏻

Comment thread crates/whiskers/src/grid/mod.rs Outdated
/// Creates new instance of the grid with empty data vector
#[allow(clippy::too_many_arguments)]
pub fn new(
sketch: &'a mut Sketch,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort of annoyance disappears with my proposal.

Comment thread crates/whiskers/src/grid/mod.rs Outdated
Comment on lines +106 to +124
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);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being a nice "container of user data" is tough and, again, best avoided altogether.

Comment thread crates/whiskers/src/grid/mod.rs Outdated
@afternoon2 afternoon2 marked this pull request as ready for review October 10, 2023 07:38
@afternoon2 afternoon2 requested a review from abey79 October 10, 2023 07:38
@afternoon2

Copy link
Copy Markdown
Contributor Author

@abey79 I made a refactor with builder pattern, but without transform and scale methods. Wdyt about putting them into issues with feature label to be implemented in separate PRs?

@abey79 abey79 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/whiskers/examples/grid.rs Outdated
Comment on lines +52 to +54
let c = cell.clone();
sketch.color(Color::RED);
sketch.add_path(c);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this clone, this is unergonomic. Let's have IntoBezPathTolerance implemented for &GridCell, so we don't need it.

Comment thread crates/whiskers/examples/grid.rs Outdated
Comment on lines +57 to +60
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());
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/whiskers/src/grid.rs Outdated
@@ -0,0 +1,217 @@
//!

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for docstring here.

Comment thread crates/whiskers/src/grid.rs Outdated
}

/// Stores basic grid's cell data, like column, row and canvas position
#[derive(Clone)]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add the usual suspects (Debug, PartialEq etc.) we don't know what the user might need to do with these.

Comment thread crates/whiskers/src/grid.rs Outdated
Comment on lines +16 to +19
column: usize,
row: usize,
position: Point,
size: [f64; 2],

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/whiskers/src/grid.rs Outdated
Comment on lines +207 to +216
/// 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);
});
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what this does, but it should follow the builder pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread crates/whiskers/src/grid.rs Outdated

/// Overrides grid's current number of rows.
/// By default, grid instance will have 4 rows.
pub fn rows(&mut self, value: usize) -> &mut Self {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we should adhere strictly to the builder pattern, so take self and return Self. Likewise for all other functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 */ } 😅

Comment thread crates/whiskers/src/grid.rs Outdated
Comment on lines +186 to +198
/// 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]
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. I just treated these methods as "getters"

Comment thread crates/whiskers/examples/grid.rs Outdated
@@ -0,0 +1,70 @@
use whiskers::{grid::Grid, prelude::*};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grid should be in the prelude so that use here is not needed.

Comment thread crates/whiskers/src/lib.rs Outdated

mod context;

pub mod grid;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove pub here and pull everything in the top-level namespace with pub use grid::{/*:..*/}

@afternoon2 afternoon2 requested a review from abey79 October 14, 2023 11:00

@abey79 abey79 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great now! A nice, clean mvp feature.

Comment on lines +48 to +60
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);
});

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very nice now 👍🏻

@abey79 abey79 changed the title Grid module proposal for whiskers Add Grid helper for grid-based layout Oct 15, 2023
@abey79 abey79 merged commit 1ef1c74 into abey79:master Oct 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants