use gpui::{
AnyElement, App, Component, IntoElement, MouseButton, Pixels, RenderOnce, SharedString, Window,
div, prelude::*, px,
};
use liora_core::{Config, stable_unique_id};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GridGap {
Xs,
Sm,
Md,
Lg,
Xl,
Px(Pixels),
}
impl GridGap {
fn pixels(self) -> Pixels {
match self {
GridGap::Xs => px(4.0),
GridGap::Sm => px(8.0),
GridGap::Md => px(12.0),
GridGap::Lg => px(16.0),
GridGap::Xl => px(24.0),
GridGap::Px(value) => value,
}
}
}
impl From<Pixels> for GridGap {
fn from(value: Pixels) -> Self {
Self::Px(value)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GridMode {
FitItem {
item_size: Pixels,
},
FitColumns {
columns: u16,
},
}
pub struct Grid {
children: Vec<AnyElement>,
mode: GridMode,
gap: GridGap,
align_start: bool,
}
impl Grid {
pub fn new() -> Self {
Self {
children: Vec::new(),
mode: GridMode::FitItem {
item_size: px(96.0),
},
gap: GridGap::Md,
align_start: true,
}
}
pub fn fit_item(mut self, item_size: impl Into<Pixels>) -> Self {
self.mode = GridMode::FitItem {
item_size: item_size.into().max(px(1.0)),
};
self
}
pub fn fit_item_sm(self) -> Self {
self.fit_item(px(88.0))
}
pub fn fit_item_md(self) -> Self {
self.fit_item(px(104.0))
}
pub fn fit_item_lg(self) -> Self {
self.fit_item(px(132.0))
}
pub fn fit_columns(mut self, columns: u16) -> Self {
self.mode = GridMode::FitColumns {
columns: columns.max(1),
};
self
}
pub fn columns(self, columns: u16) -> Self {
self.fit_columns(columns)
}
pub fn min_item_width(self, width: impl Into<Pixels>) -> Self {
self.fit_item(width)
}
pub fn auto_fit(self) -> Self {
self.fit_item(px(96.0))
}
pub fn gap(mut self, gap: impl Into<GridGap>) -> Self {
self.gap = gap.into();
self
}
pub fn gap_xs(self) -> Self {
self.gap(GridGap::Xs)
}
pub fn gap_sm(self) -> Self {
self.gap(GridGap::Sm)
}
pub fn gap_md(self) -> Self {
self.gap(GridGap::Md)
}
pub fn gap_lg(self) -> Self {
self.gap(GridGap::Lg)
}
pub fn gap_xl(self) -> Self {
self.gap(GridGap::Xl)
}
pub fn align_start(mut self) -> Self {
self.align_start = true;
self
}
pub fn align_center(mut self) -> Self {
self.align_start = false;
self
}
pub fn child(mut self, child: impl IntoElement) -> Self {
self.children.push(child.into_any_element());
self
}
pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
self.children
.extend(children.into_iter().map(|child| child.into_any_element()));
self
}
}
impl Default for Grid {
fn default() -> Self {
Self::new()
}
}
impl RenderOnce for Grid {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let gap = self.gap.pixels();
let mut el = div().w_full().gap(gap);
match self.mode {
GridMode::FitColumns { columns } => {
el = el.grid().grid_cols(columns);
}
GridMode::FitItem { .. } => {
el = el.flex().flex_row().flex_wrap();
}
}
if self.align_start {
el = el.items_start();
} else {
el = el.items_center();
}
let mode = self.mode;
el.children(self.children.into_iter().map(move |child| {
let item = div().child(child);
match mode {
GridMode::FitColumns { .. } => item.w_full().into_any_element(),
GridMode::FitItem { item_size } => item.w(item_size).flex_none().into_any_element(),
}
}))
}
}
impl IntoElement for Grid {
type Element = Component<Self>;
fn into_element(self) -> Self::Element {
Component::new(self)
}
}
type GridItemClick = dyn Fn(&mut Window, &mut App) + 'static;
pub struct GridItem {
id: Option<String>,
body: AnyElement,
on_click: Option<Arc<GridItemClick>>,
hover_group: Option<SharedString>,
square: bool,
centered: bool,
}
impl GridItem {
pub fn new(body: impl IntoElement) -> Self {
Self {
id: None,
body: body.into_any_element(),
on_click: None,
hover_group: None,
square: true,
centered: true,
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn square(mut self) -> Self {
self.square = true;
self
}
pub fn rectangular(mut self) -> Self {
self.square = false;
self
}
pub fn centered(mut self) -> Self {
self.centered = true;
self
}
pub fn align_start(mut self) -> Self {
self.centered = false;
self
}
pub fn on_click(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Arc::new(callback));
self
}
pub fn hover_group(mut self, group: impl Into<SharedString>) -> Self {
self.hover_group = Some(group.into());
self
}
}
impl RenderOnce for GridItem {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.global::<Config>().theme.clone();
let id = self
.id
.unwrap_or_else(|| stable_unique_id("grid-item", "grid-item", window, cx).to_string());
let click = self.on_click.clone();
let has_hover_group = self.hover_group.is_some();
let hover_group = self
.hover_group
.unwrap_or_else(|| SharedString::from(format!("liora-grid-item-hover-{id}")));
div()
.id(id)
.w_full()
.when(self.square, |s| s.aspect_square())
.when(self.centered, |s| s.flex().items_center().justify_center())
.rounded(px(theme.radius.md))
.border_1()
.border_color(theme.neutral.border)
.bg(theme.neutral.card)
.p_3()
.text_color(theme.neutral.text_2)
.when(click.is_some() || has_hover_group, |s| {
s.group(hover_group).hover(|s| {
s.bg(theme.neutral.hover)
.border_color(theme.primary.base)
.text_color(theme.primary.base)
})
})
.when(click.is_some(), |s| {
s.cursor_pointer()
.on_mouse_up(MouseButton::Left, move |_, window, cx| {
if let Some(click) = &click {
click(window, cx);
}
})
})
.child(self.body)
}
}
impl IntoElement for GridItem {
type Element = Component<Self>;
fn into_element(self) -> Self::Element {
Component::new(self)
}
}
#[cfg(test)]
mod tests {
#[test]
fn grid_supports_auto_fit_and_fixed_columns() {
let source = include_str!("grid.rs");
assert!(source.contains("pub struct Grid"));
assert!(source.contains("pub enum GridMode"));
assert!(source.contains("pub fn fit_item"));
assert!(source.contains("pub fn fit_columns"));
assert!(source.contains("pub fn fit_item_md"));
assert!(source.contains("flex_wrap"));
assert!(source.contains("grid_cols(columns)"));
}
#[test]
fn grid_item_is_clickable_with_pointer_hover_feedback() {
let source = include_str!("grid.rs");
assert!(source.contains("pub struct GridItem"));
assert!(source.contains("pub fn on_click"));
assert!(source.contains("pub fn hover_group"));
assert!(source.contains("liora-grid-item-hover-{id}"));
assert!(source.contains(".cursor_pointer()"));
assert!(source.contains("click.is_some() || has_hover_group"));
assert!(source.contains(".aspect_square()"));
assert!(source.contains("pub fn rectangular"));
assert!(source.contains(".text_color(theme.primary.base)"));
assert!(source.contains(".on_mouse_up(MouseButton::Left"));
}
}