diff --git a/packages/oneclient_app/src/components/grid_columns.rs b/packages/oneclient_app/src/components/grid_columns.rs new file mode 100644 index 00000000..a5a1b3c1 --- /dev/null +++ b/packages/oneclient_app/src/components/grid_columns.rs @@ -0,0 +1,27 @@ +use freya::prelude::*; + +use super::Dropdown; + +pub const GRID_COLUMNS_MIN: u8 = 1; +pub const GRID_COLUMNS_MAX: u8 = 5; + +pub fn resolved_columns(columns: u8) -> usize { + columns.clamp(GRID_COLUMNS_MIN, GRID_COLUMNS_MAX) as usize +} + +pub fn grid_columns_picker(mut columns: State, height: f32) -> impl IntoElement { + let options: Vec = (GRID_COLUMNS_MIN..=GRID_COLUMNS_MAX) + .map(|n| n.to_string()) + .collect(); + let selected = resolved_columns(*columns.read()).to_string(); + + Dropdown::new(selected, options) + .width(Size::px(60.)) + .height(Size::px(height)) + .on_select(move |idx: usize| columns.set(GRID_COLUMNS_MIN + idx as u8)) +} + +pub fn cell_width(total: f32, cols: usize, gap: f32) -> f32 { + let cols = cols.max(1); + ((total - (cols - 1) as f32 * gap) / cols as f32).max(0.) +} diff --git a/packages/oneclient_app/src/components/mod.rs b/packages/oneclient_app/src/components/mod.rs index 09558bb3..1c004678 100644 --- a/packages/oneclient_app/src/components/mod.rs +++ b/packages/oneclient_app/src/components/mod.rs @@ -9,6 +9,7 @@ mod context_menu; mod dropdown; mod dynamic_art; mod file_drop; +mod grid_columns; mod icons; mod java_install_manager; mod generic_prompt; @@ -51,6 +52,7 @@ pub use context_menu::ContextMenu; pub use dropdown::Dropdown; pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt}; pub use file_drop::FileDropOverlay; +pub use grid_columns::{cell_width, grid_columns_picker, resolved_columns}; pub use icons::{Icon, IconTint, IconType}; pub use java_install_manager::JavaInstallManager; pub use generic_prompt::GenericPromptOverlay; diff --git a/packages/oneclient_app/src/hooks/view_state.rs b/packages/oneclient_app/src/hooks/view_state.rs index 64696a34..7379d707 100644 --- a/packages/oneclient_app/src/hooks/view_state.rs +++ b/packages/oneclient_app/src/hooks/view_state.rs @@ -7,6 +7,7 @@ use super::{use_dispatch, use_settings_snapshot}; pub struct PersistedView { pub layout: State, pub sort: State>, + pub columns: State, } pub fn use_view_state(key: &str) -> PersistedView { @@ -17,14 +18,17 @@ pub fn use_view_state(key: &str) -> PersistedView { let initial = settings.view_state(&key); let init_layout = initial.layout; let init_sort = initial.sort; + let init_columns = initial.columns; let layout = use_state(move || init_layout); let sort = use_state(move || init_sort.clone()); + let columns = use_state(move || init_columns); let mut first = use_state(|| true); use_side_effect(move || { let next = ViewState { layout: *layout.read(), sort: sort.read().clone(), + columns: *columns.read(), }; if *first.peek() { first.set(false); @@ -38,5 +42,9 @@ pub fn use_view_state(key: &str) -> PersistedView { dispatch.set_settings(updated); }); - PersistedView { layout, sort } + PersistedView { + layout, + sort, + columns, + } } diff --git a/packages/oneclient_app/src/ui.rs b/packages/oneclient_app/src/ui.rs index eec4c621..4b3a9a6b 100644 --- a/packages/oneclient_app/src/ui.rs +++ b/packages/oneclient_app/src/ui.rs @@ -114,15 +114,6 @@ pub fn centered_note(text: &str) -> Element { .into_element() } -/// Returns 1 until the grid has measured itself so the first frame still renders -pub fn grid_columns_for_width(width: f32, max_col: f32, gap: f32) -> usize { - if width <= 0. { - return 1; - } - (((width + gap) / (max_col + gap)).ceil() as usize).max(1) -} - -/// Reports its measured size back through `width` for [`grid_columns_for_width`] /// Short final rows are padded with empty flex cells so tiles keep the column width pub fn flow_grid(items: Vec, cols: usize, mut width: State, gap: f32) -> Element { let cols = cols.max(1); diff --git a/packages/oneclient_app/src/view/app/browser/index/cards.rs b/packages/oneclient_app/src/view/app/browser/index/cards.rs index c5edbc32..f75869c6 100644 --- a/packages/oneclient_app/src/view/app/browser/index/cards.rs +++ b/packages/oneclient_app/src/view/app/browser/index/cards.rs @@ -31,9 +31,10 @@ pub(super) fn grid_row( cluster_id: i64, package_type: &str, installed: &InstalledMap, + metrics: GridMetrics, ) -> impl IntoElement { let package_type = package_type.to_string(); - let fill = GRID_COLUMNS - row.len(); + let fill = metrics.cols.saturating_sub(row.len()); let installed: Vec> = row .iter() .map(|item| installed_for(installed, item)) @@ -42,14 +43,14 @@ pub(super) fn grid_row( rect() .horizontal() .width(Size::fill()) - .height(Size::px(CARD_H)) + .height(Size::px(metrics.card_h)) .spacing(GRID_SPACING) .content(Content::Flex) .children( row.into_iter() .zip(installed) .map(move |(item, installed)| { - PackageCard::new(item, cluster_id, package_type.clone(), installed) + PackageCard::new(item, cluster_id, package_type.clone(), installed, metrics) .into_element() }), ) @@ -76,6 +77,7 @@ struct PackageCard { cluster_id: i64, package_type: String, installed: Option, + metrics: GridMetrics, } impl PackageCard { @@ -84,12 +86,14 @@ impl PackageCard { cluster_id: i64, package_type: String, installed: Option, + metrics: GridMetrics, ) -> Self { Self { item, cluster_id, package_type, installed, + metrics, } } } @@ -109,7 +113,7 @@ impl Component for PackageCard { rect() .vertical() .width(Size::flex(1.0)) - .height(Size::px(CARD_H)) + .height(Size::px(self.metrics.card_h)) .corner_radius(CornerRadius::new_all(10.)) .background(CARD_BG) .border(border_all_color( @@ -131,7 +135,8 @@ impl Component for PackageCard { .margin(Gaps::new_all(1.)) .corner_radius(CornerRadius::new(10., 10., 0., 0.)) .overflow(Overflow::Clip) - .child(PackageBanner::new(icon_url, BANNER_H)) + .child(PackageBanner::new(icon_url, self.metrics.banner_h)) + // Top-left of the banner the first thing worth knowing about a result and the opposite corner is the install button's .maybe_child(self.installed.map(|installed| { rect() .position(Position::new_absolute().top(8.).left(8.)) diff --git a/packages/oneclient_app/src/view/app/browser/index/mod.rs b/packages/oneclient_app/src/view/app/browser/index/mod.rs index a5e38650..53b30f17 100644 --- a/packages/oneclient_app/src/view/app/browser/index/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/index/mod.rs @@ -16,6 +16,7 @@ use oneclient_core::settings::ViewLayout; use crate::components::{ Dropdown, Icon, IconType, Pagination, ScrollArea, Segment, SegmentedControl, TextInput, + cell_width, grid_columns_picker, resolved_columns, }; use crate::hooks::use_cluster; use crate::hooks::{ @@ -43,15 +44,24 @@ use skeletons::{SkeletonListRow, skeleton_grid_row}; const CARD_BG: Color = Color::from_rgb(26, 34, 41); const CARD_NAME: Color = Color::from_rgb(213, 219, 255); -const GRID_COLUMNS: usize = 3; const SCROLLBAR_GUTTER: f32 = 18.; const CARD_H: f32 = 240.; const BANNER_H: f32 = 100.; +const CARD_TEXT_H: f32 = CARD_H - BANNER_H; +const BANNER_RATIO: f32 = 0.35; +const BANNER_MAX_H: f32 = 200.; const LIST_ROW_H: f32 = 78.; const GRID_SPACING: f32 = 12.; const LIST_SPACING: f32 = 8.; const SEARCH_DEBOUNCE_MS: u64 = 250; +#[derive(Clone, Copy, PartialEq)] +pub(super) struct GridMetrics { + pub cols: usize, + pub card_h: f32, + pub banner_h: f32, +} + fn type_title(package_type: &str) -> &'static str { match package_type { "shader" => "Shaders", @@ -84,7 +94,9 @@ impl Component for Browser { let query = use_state(|| saved.query.clone()); let provider = use_state(|| saved.provider); - let view_mode = use_view_state(&format!("browser.{package_type}")).layout; + let view = use_view_state(&format!("browser.{package_type}")); + let view_mode = view.layout; + let grid_columns = view.columns; let compatible_only = use_browser_compat(); let selected_categories = use_state(|| saved.categories.clone()); let page = use_state(|| saved.page); @@ -178,6 +190,16 @@ impl Component for Browser { let mode = *view_mode.read(); + let mut grid_width = use_state(|| 0f32); + let cols = resolved_columns(*grid_columns.read()); + let card_w = cell_width( + (*grid_width.read() - SCROLLBAR_GUTTER).max(0.), + cols, + GRID_SPACING, + ); + let banner_h = (card_w * BANNER_RATIO).clamp(BANNER_H, BANNER_MAX_H); + let card_h = banner_h + CARD_TEXT_H; + let fade_dep = (current_page, packages.is_empty(), pending, mode); let fade = use_animation_with_dependencies(&fade_dep, |conf, _| { conf.on_creation(OnCreation::Run); @@ -198,10 +220,21 @@ impl Component for Browser { match mode { ViewLayout::Grid => { let rows: Vec> = - packages.chunks(GRID_COLUMNS).map(|c| c.to_vec()).collect(); + packages.chunks(cols).map(|c| c.to_vec()).collect(); - sa.lazy(rows.len(), CARD_H, GRID_SPACING, move |i| { - grid_row(rows[i].clone(), cluster_id, &pkg, &installed).into_element() + sa.lazy(rows.len(), card_h, GRID_SPACING, move |i| { + grid_row( + rows[i].clone(), + cluster_id, + &pkg, + &installed, + GridMetrics { + cols, + card_h, + banner_h, + }, + ) + .into_element() }) } ViewLayout::List => sa.lazy(packages.len(), LIST_ROW_H, LIST_SPACING, move |i| { @@ -211,9 +244,14 @@ impl Component for Browser { } else if pending { match mode { ViewLayout::Grid => { - let rows = BROWSE_PAGE_SIZE.div_ceil(GRID_COLUMNS); - sa.lazy(rows, CARD_H, GRID_SPACING, |_| { - skeleton_grid_row().into_element() + let rows = BROWSE_PAGE_SIZE.div_ceil(cols); + sa.lazy(rows, card_h, GRID_SPACING, move |_| { + skeleton_grid_row(GridMetrics { + cols, + card_h, + banner_h, + }) + .into_element() }) } ViewLayout::List => sa.lazy(BROWSE_PAGE_SIZE, LIST_ROW_H, LIST_SPACING, |_| { @@ -240,7 +278,7 @@ impl Component for Browser { package_type: package_type.clone(), }), )) - .child(controls(provider, query, view_mode)) + .child(controls(provider, query, view_mode, grid_columns)) .child( rect() .horizontal() @@ -265,7 +303,13 @@ impl Component for Browser { .width(Size::fill()) .height(Size::flex(1.0)) .child(results) - .opacity(fade_opacity), + .opacity(fade_opacity) + .on_sized(move |event: Event| { + let w = event.data().area.width(); + if (w - *grid_width.peek()).abs() > 0.5 { + *grid_width.write() = w; + } + }), ) .maybe_child(pages.map(|pages| { Pagination::new(page, pages) @@ -379,6 +423,7 @@ fn controls( provider: State, query: State, view_mode: State, + grid_columns: State, ) -> impl IntoElement { rect() .horizontal() @@ -387,6 +432,10 @@ fn controls( .spacing(12.) .content(Content::Flex) .child(rect().width(Size::flex(1.0))) + .maybe_child( + (*view_mode.read() == ViewLayout::Grid) + .then(|| grid_columns_picker(grid_columns, 30.).into_element()), + ) .child( SegmentedControl::new(view_mode) .equal_width(30.) diff --git a/packages/oneclient_app/src/view/app/browser/index/skeletons.rs b/packages/oneclient_app/src/view/app/browser/index/skeletons.rs index 30f6d2aa..ee6dca57 100644 --- a/packages/oneclient_app/src/view/app/browser/index/skeletons.rs +++ b/packages/oneclient_app/src/view/app/browser/index/skeletons.rs @@ -27,7 +27,9 @@ fn use_skeleton_pulse() -> f32 { } #[derive(PartialEq)] -struct SkeletonCard; +struct SkeletonCard { + metrics: GridMetrics, +} impl Component for SkeletonCard { fn render(&self) -> impl IntoElement { @@ -35,13 +37,13 @@ impl Component for SkeletonCard { rect() .vertical() .width(Size::flex(1.0)) - .height(Size::px(CARD_H)) + .height(Size::px(self.metrics.card_h)) .corner_radius(CornerRadius::new_all(10.)) .background(CARD_BG) .border(border_all_color(1., colors::component_border())) .overflow(Overflow::Clip) .opacity(pulse) - .child(skeleton_block(Size::fill(), BANNER_H)) + .child(skeleton_block(Size::fill(), self.metrics.banner_h)) .child( rect() .vertical() @@ -88,13 +90,13 @@ impl Component for SkeletonListRow { } } -pub(super) fn skeleton_grid_row() -> impl IntoElement { +pub(super) fn skeleton_grid_row(metrics: GridMetrics) -> impl IntoElement { rect() .horizontal() .width(Size::fill()) - .height(Size::px(CARD_H)) + .height(Size::px(metrics.card_h)) .spacing(GRID_SPACING) .content(Content::Flex) - .children((0..GRID_COLUMNS).map(|_| SkeletonCard.into_element())) + .children((0..metrics.cols).map(move |_| SkeletonCard { metrics }.into_element())) .into_element() } diff --git a/packages/oneclient_app/src/view/app/browser/package/gallery.rs b/packages/oneclient_app/src/view/app/browser/package/gallery.rs index a8b66d45..8eae22da 100644 --- a/packages/oneclient_app/src/view/app/browser/package/gallery.rs +++ b/packages/oneclient_app/src/view/app/browser/package/gallery.rs @@ -4,15 +4,18 @@ use super::*; use oneclient_content::packages::types::GalleryImage; -use crate::components::{Button, Icon, IconType, OverlayPopup}; -use crate::hooks::{loaded_image, query_is_busy, use_cached_image}; +use crate::components::{ + Button, Icon, IconType, OverlayPopup, cell_width, grid_columns_picker, resolved_columns, +}; +use crate::hooks::{loaded_image, query_is_busy, use_cached_image, use_view_state}; use crate::theme::colors; -use crate::ui::{border_all_color, flow_grid, grid_columns_for_width}; +use crate::ui::{border_all_color, flow_grid}; -/// How wide a tile is allowed to get before the grid takes another column const MAX_COL_W: f32 = 400.; const GRID_GAP: f32 = 16.; const TILE_PREVIEW_H: f32 = 168.; +const TILE_PREVIEW_RATIO: f32 = TILE_PREVIEW_H / MAX_COL_W; +const TILE_PREVIEW_MAX_H: f32 = 420.; /// Downscale ceilings for the image cache full-size tiles made the grid slow to fill const TILE_EDGE: u32 = 640; @@ -32,6 +35,11 @@ impl Component for Gallery { let grid_width = use_state(|| 0f32); let mut viewing = use_state(|| None::); + let grid_columns = use_view_state("browser.gallery").columns; + let cols = resolved_columns(*grid_columns.read()); + let tile_w = cell_width(*grid_width.read(), cols, GRID_GAP); + let preview_h = (tile_w * TILE_PREVIEW_RATIO).clamp(TILE_PREVIEW_H, TILE_PREVIEW_MAX_H); + let tiles: Vec = self .images .iter() @@ -40,6 +48,7 @@ impl Component for Gallery { let key = image.url.clone(); GalleryTile { image: image.clone(), + preview_h, on_open: (move |()| viewing.set(Some(idx))).into(), key: DiffKey::None, } @@ -48,11 +57,21 @@ impl Component for Gallery { }) .collect(); - let cols = grid_columns_for_width(*grid_width.read(), MAX_COL_W, GRID_GAP); let images = self.images.clone(); rect() + .vertical() .width(Size::fill()) + .spacing(10.) + .child( + rect() + .horizontal() + .width(Size::fill()) + .cross_align(Alignment::Center) + .content(Content::Flex) + .child(rect().width(Size::flex(1.0))) + .child(grid_columns_picker(grid_columns, 30.)), + ) .child(flow_grid(tiles, cols, grid_width, GRID_GAP)) .maybe_child((*viewing.read()).map(|start| { GalleryViewer { @@ -68,6 +87,7 @@ impl Component for Gallery { #[derive(PartialEq)] struct GalleryTile { image: GalleryImage, + preview_h: f32, on_open: EventHandler<()>, key: DiffKey, } @@ -88,7 +108,7 @@ impl Component for GalleryTile { let preview = rect() .width(Size::fill()) - .height(Size::px(TILE_PREVIEW_H)) + .height(Size::px(self.preview_h)) .center() .overflow(Overflow::Clip) .background(colors::component_bg()) @@ -104,7 +124,6 @@ impl Component for GalleryTile { rect() .vertical() .width(Size::flex(1.0)) - .max_width(Size::px(MAX_COL_W)) .corner_radius(CornerRadius::new_all(10.)) .overflow(Overflow::Clip) .background(PANEL_BG) diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs index 22587ce6..0a3f8381 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs @@ -386,6 +386,7 @@ impl Component for PackageManager { let view = use_view_state("cluster.packages"); let sort = view.sort; let layout = view.layout; + let grid_columns = view.columns; let query = SearchQuery::new(&search.read()); let sort_mode = sort .read() @@ -438,6 +439,7 @@ impl Component for PackageManager { enabled_filter, hidden_filter, layout, + grid_columns, cluster_id, package_type, )) @@ -450,6 +452,7 @@ impl Component for PackageManager { cluster_id, content_kind, card_layout, + *grid_columns.read(), )) } } diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs index becfcf63..00eaa42b 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs @@ -7,7 +7,7 @@ use oneclient_core::settings::ViewLayout; use crate::components::{ Button, CardLayout, Icon, IconType, PackageEntry, PackageRow, ScrollArea, ScrollAreaCtx, - Segment, SegmentedControl, TabBar, TabItem, TextInput, + Segment, SegmentedControl, TabBar, TabItem, TextInput, grid_columns_picker, resolved_columns, }; use crate::hooks::{use_dispatch, use_overlay_claim}; use crate::routes::Route; @@ -140,6 +140,7 @@ pub(super) fn toolbar_bar( enabled_filter: State, hidden_filter: State, layout: State, + grid_columns: State, cluster_id: i64, package_type: &'static str, ) -> impl IntoElement { @@ -193,6 +194,10 @@ pub(super) fn toolbar_bar( enabled_filter, hidden_filter, }) + .maybe_child( + (*layout.read() == ViewLayout::Grid) + .then(|| grid_columns_picker(grid_columns, 34.).into_element()), + ) .child( SegmentedControl::new(layout) .height(34.) @@ -557,6 +562,7 @@ pub(super) struct ContentBox { cluster_id: i64, kind: ContentKind, layout: CardLayout, + grid_columns: u8, } impl ContentBox { @@ -569,6 +575,7 @@ impl ContentBox { cluster_id: i64, kind: ContentKind, layout: CardLayout, + grid_columns: u8, ) -> Self { Self { items, @@ -578,6 +585,7 @@ impl ContentBox { cluster_id, kind, layout, + grid_columns, } } } @@ -591,6 +599,7 @@ impl Component for ContentBox { let noun_plural = self.noun_plural; let kind = &self.kind; let layout = self.layout; + let grid_columns = self.grid_columns; let dispatch = use_dispatch(); @@ -615,7 +624,7 @@ impl Component for ContentBox { .width(Size::fill()) .height(Size::fill()) .content(move |ctx: ScrollAreaCtx| { - grid_content(&items, package_type, cluster_id, ctx).into_element() + grid_content(&items, package_type, cluster_id, grid_columns, ctx).into_element() }) .into_element(), }); @@ -680,11 +689,12 @@ fn grid_content( items: &[PackageEntry], package_type: &'static str, cluster_id: i64, + grid_columns: u8, ctx: ScrollAreaCtx, ) -> impl IntoElement { let count = items.len(); - let cols = - (((ctx.viewport_w + GRID_GAP) / (GRID_MIN_W + GRID_GAP)).floor() as usize).clamp(1, 3); + let fits = ((ctx.viewport_w + GRID_GAP) / (GRID_MIN_W + GRID_GAP)).floor() as usize; + let cols = resolved_columns(grid_columns).min(fits.max(1)); let rows_total = count.div_ceil(cols); let slot = CARD_GRID_H + GRID_GAP; diff --git a/packages/oneclient_app/src/view/app/cluster/screenshots.rs b/packages/oneclient_app/src/view/app/cluster/screenshots.rs index 4236ee15..b6e7521e 100644 --- a/packages/oneclient_app/src/view/app/cluster/screenshots.rs +++ b/packages/oneclient_app/src/view/app/cluster/screenshots.rs @@ -7,12 +7,13 @@ use oneclient_core::settings::ViewLayout; use crate::components::{ Button, ContextMenu, Icon, IconType, LocalImage, OverlayPopup, ScreenshotViewer, ScrollArea, - Segment, SegmentedControl, open_folder_button, + Segment, SegmentedControl, cell_width, grid_columns_picker, open_folder_button, + resolved_columns, }; use crate::hooks::{ScreenshotAction, query_is_loading, try_cluster_screenshots, use_cluster_screenshots, use_dispatch, use_screenshot_action, use_screenshot_folder_watch, use_view_state}; use crate::layout::cluster_content; use crate::theme::colors; -use crate::ui::{border_all_color, flow_grid, fmt_date, grid_columns_for_width}; +use crate::ui::{border_all_color, flow_grid, fmt_date}; use crate::utils::{format_res, format_size}; use super::cluster_not_found; @@ -22,6 +23,8 @@ const THUMB_EDGE: u32 = 480; const MAX_COL_W: f32 = 400.; const GRID_GAP: f32 = 16.; const TILE_PREVIEW_H: f32 = 168.; +const TILE_PREVIEW_RATIO: f32 = TILE_PREVIEW_H / MAX_COL_W; +const TILE_PREVIEW_MAX_H: f32 = 420.; const CARD_BG: Color = Color::from_rgb(26, 34, 41); const CARD_NAME: Color = Color::from_rgb(213, 219, 255); @@ -45,7 +48,9 @@ impl Component for ClusterScreenshots { let shots = try_cluster_screenshots(&query).unwrap_or_default(); - let view_mode = use_view_state("cluster.screenshots").layout; + let view = use_view_state("cluster.screenshots"); + let view_mode = view.layout; + let grid_columns = view.columns; let menu_dispatch = use_dispatch(); let mut edit_mode = use_state(|| false); @@ -59,11 +64,16 @@ impl Component for ClusterScreenshots { &shots, folder, view_mode, + grid_columns, edit_mode, selected, confirm_delete, ); + let cols = resolved_columns(*grid_columns.read()); + let tile_w = cell_width(*grid_width.read(), cols, GRID_GAP); + let preview_h = (tile_w * TILE_PREVIEW_RATIO).clamp(TILE_PREVIEW_H, TILE_PREVIEW_MAX_H); + let content: Element = if shots.is_empty() { empty_state(query_is_loading(&query)) .into_element() @@ -98,6 +108,7 @@ impl Component for ClusterScreenshots { info, selected: is_selected, edit_mode: editing, + preview_h, on_activate: on_activate.into(), on_context, } @@ -115,10 +126,7 @@ impl Component for ClusterScreenshots { } match *view_mode.read() { - ViewLayout::Grid => { - let cols = grid_columns_for_width(*grid_width.read(), MAX_COL_W, GRID_GAP); - flow_grid(items, cols, grid_width, GRID_GAP) - } + ViewLayout::Grid => flow_grid(items, cols, grid_width, GRID_GAP), ViewLayout::List => rect() .vertical() .width(Size::fill()) @@ -200,6 +208,7 @@ fn toolbar_row( shots: &[ScreenshotInfo], folder: Option, view_mode: State, + grid_columns: State, mut edit_mode: State, mut selected: State>, mut confirm_delete: State, @@ -267,6 +276,10 @@ fn toolbar_row( ); } else { right = right + .maybe_child( + (*view_mode.read() == ViewLayout::Grid) + .then(|| grid_columns_picker(grid_columns, 36.).into_element()), + ) .child( SegmentedControl::new(view_mode) .equal_width(40.) @@ -416,6 +429,7 @@ struct ScreenshotTile { info: ScreenshotInfo, selected: bool, edit_mode: bool, + preview_h: f32, on_activate: EventHandler<()>, on_context: EventHandler<(f32, f32)>, } @@ -440,7 +454,6 @@ impl Component for ScreenshotTile { rect() .vertical() .width(Size::flex(1.0)) - .max_width(Size::px(MAX_COL_W)) .corner_radius(CornerRadius::new_all(10.)) .background(CARD_BG) .overflow(Overflow::Clip) @@ -456,7 +469,7 @@ impl Component for ScreenshotTile { }) .child(preview_box( info.path.clone(), - TILE_PREVIEW_H, + self.preview_h, info.resolution, )) .child( diff --git a/packages/oneclient_core/src/settings/launcher.rs b/packages/oneclient_core/src/settings/launcher.rs index 1b7d62bb..6c279618 100644 --- a/packages/oneclient_core/src/settings/launcher.rs +++ b/packages/oneclient_core/src/settings/launcher.rs @@ -13,12 +13,25 @@ pub enum ViewLayout { List, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub const DEFAULT_GRID_COLUMNS: u8 = 3; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(default)] pub struct ViewState { pub layout: ViewLayout, #[serde(skip_serializing_if = "Option::is_none")] pub sort: Option, + pub columns: u8, +} + +impl Default for ViewState { + fn default() -> Self { + Self { + layout: ViewLayout::default(), + sort: None, + columns: DEFAULT_GRID_COLUMNS, + } + } } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/packages/oneclient_core/src/settings/mod.rs b/packages/oneclient_core/src/settings/mod.rs index 2c54d3af..56bdd5a2 100644 --- a/packages/oneclient_core/src/settings/mod.rs +++ b/packages/oneclient_core/src/settings/mod.rs @@ -2,7 +2,7 @@ mod launcher; pub mod data_dir; -pub use launcher::{LauncherSettings, ViewLayout, ViewState}; +pub use launcher::{DEFAULT_GRID_COLUMNS, LauncherSettings, ViewLayout, ViewState}; pub use oneclient_cluster::{ GameSettingsProfile, PackageUpdateMode, ProfileUpdate, SettingsOsExtra, };