feat: added context menu on homepage clusters (#743)

This commit is contained in:
Maciek 2026-09-04 17:32:29 +02:00 • committed by LynithDev
parent f55a86a024
commit 2a36bb8a88
2 changed files with 157 additions and 14 deletions

View file

@ -18,9 +18,23 @@ enum Entry {
Separator,
}
fn separator(item_width: Option<f32>) -> Rect {
let mut sep = rect()
.height(Size::px(1.))
.margin(Gaps::new_symmetric(4., 0.))
.background(MENU_BORDER);
if let Some(w) = item_width {
sep = sep.width(Size::px(w));
}
sep
}
pub struct ContextMenu {
x: f32,
y: f32,
upwards: bool,
backdrop: bool,
title: Option<String>,
entries: Vec<Entry>,
on_close: EventHandler<()>,
}
@ -30,11 +44,30 @@ impl ContextMenu {
Self {
x,
y,
upwards: false,
backdrop: false,
title: None,
entries: Vec::new(),
on_close: (|()| {}).into(),
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn open_upwards(mut self) -> Self {
self.upwards = true;
self
}
#[allow(dead_code)]
pub fn backdrop(mut self, backdrop: bool) -> Self {
self.backdrop = backdrop;
self
}
pub fn on_close(mut self, on_close: impl Into<EventHandler<()>>) -> Self {
self.on_close = on_close.into();
self
@ -78,7 +111,6 @@ impl ContextMenu {
impl PartialEq for ContextMenu {
fn eq(&self, _other: &Self) -> bool {
// Always unequal the menu is rebuilt from scratch whenever it is reopened
false
}
}
@ -88,6 +120,8 @@ impl Component for ContextMenu {
let on_close = self.on_close.clone();
let mut width = use_state(|| 0f32);
let mut height = use_state(|| 0f32);
let mut panel_width = use_state(|| 0f32);
let item_width = {
let w = *width.read();
@ -95,18 +129,26 @@ impl Component for ContextMenu {
};
let mut list = rect().vertical().spacing(4.);
if let Some(title) = &self.title {
list = list
.child(
// Asymmetric on purpose
rect().padding(Gaps::new(6., 8., 4., 8.)).child(
label()
.text(title.clone())
.font_size(11.)
.font_weight(FontWeight::SEMI_BOLD)
.max_lines(1)
.color(colors::fg_secondary()),
),
)
.child(separator(item_width));
}
for entry in &self.entries {
list = match entry {
Entry::Separator => {
let mut sep = rect()
.height(Size::px(1.))
.margin(Gaps::new_symmetric(4., 0.))
.background(MENU_BORDER);
if let Some(w) = item_width {
sep = sep.width(Size::px(w));
}
list.child(sep)
}
Entry::Separator => list.child(separator(item_width)),
Entry::Action {
icon,
label,
@ -133,6 +175,21 @@ impl Component for ContextMenu {
}
});
let platform = Platform::get();
// `root_size` is physical while global positions and measured areas are logical
let scale = (*platform.scale_factor.read() as f32).max(f32::EPSILON);
let root = *platform.root_size.read();
let window_width = root.width / scale;
let window_height = root.height / scale;
let measured = *height.read();
let menu_width = *panel_width.read();
let flips = self.upwards && measured > 0. && self.y - measured >= 0.;
let top = if flips { self.y - measured } else { self.y };
let top = top.min(window_height - measured).max(0.);
let left = self.x.min(window_width - menu_width).max(0.);
let placed = measured > 0. && menu_width > 0.;
let panel = rect()
.vertical()
.padding(Gaps::new_all(6.))
@ -144,11 +201,21 @@ impl Component for ContextMenu {
bottom: 1.,
left: 1.,
}))
.opacity(if placed { 1. } else { 0. })
.on_sized(move |e: Event<SizedEventData>| {
let area = e.data().area;
if (area.height() - *height.peek()).abs() > 0.5 {
height.set(area.height());
}
if (area.width() - *panel_width.peek()).abs() > 0.5 {
panel_width.set(area.width());
}
})
.child(list);
OverlayPopup::new()
.backdrop(false)
.position(Position::new_global().top(self.y).left(self.x))
.backdrop(self.backdrop)
.position(Position::new_global().top(top).left(left))
.on_close(move |_| on_close.call(()))
.child(panel.into_element())
}

View file

@ -3,7 +3,7 @@ use freya::prelude::*;
use freya::router::RouterContext;
use oneclient_core::clusters::Cluster;
use crate::components::{ART_PREVIEW_EDGE, DynamicArt, Icon, IconType};
use crate::components::{ART_PREVIEW_EDGE, ContextMenu, DynamicArt, Icon, IconType};
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters};
use crate::routes::Route;
use crate::theme;
@ -67,7 +67,9 @@ impl Component for RecentsRow {
index,
items,
progress,
key: DiffKey::None,
}
.key(cluster.id)
.into_element()
}))
.child(OtherVersionsTile {
@ -94,11 +96,58 @@ fn stagger_eased(progress: f32, index: usize, items: usize) -> f32 {
1.0 - (1.0 - local).powi(3)
}
fn cluster_menu_entries(cluster_id: i64) -> [(IconType, &'static str, Route); 7] {
[
(
IconType::InfoCircle,
"Overview",
Route::ClusterOverview { cluster_id },
),
(
IconType::Terminal,
"Logs",
Route::ClusterLogs { cluster_id },
),
(
IconType::Eye,
"Screenshots",
Route::ClusterScreenshots { cluster_id },
),
(
IconType::CodeSnippet02,
"Mods",
Route::ClusterMods { cluster_id },
),
(
IconType::PaintPour,
"Shaders",
Route::ClusterShaders { cluster_id },
),
(
IconType::Colors,
"Textures",
Route::ClusterTextures { cluster_id },
),
(
IconType::Settings01,
"Settings",
Route::ClusterSettings { cluster_id },
),
]
}
struct ClusterCard {
cluster: Cluster,
index: usize,
items: usize,
progress: f32,
key: DiffKey,
}
impl KeyExt for ClusterCard {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl PartialEq for ClusterCard {
@ -115,6 +164,7 @@ impl Component for ClusterCard {
let mut active_id = use_active_cluster_id();
let active = *active_id.read() == Some(self.cluster.id);
let mut hovering = use_state(|| false);
let mut menu = use_state(|| None::<(f32, f32)>);
let a11y_id = use_a11y();
let focus = use_focus(a11y_id);
@ -130,6 +180,19 @@ impl Component for ClusterCard {
*active_id.write() = Some(cluster_id);
};
let menu_overlay = (*menu.read()).map(|(x, y)| {
let mut context = ContextMenu::new(x, y)
.open_upwards()
.title(title.clone())
.on_close(move |_| menu.set(None));
for (icon, label, route) in cluster_menu_entries(cluster_id) {
context = context.action(icon, label, move |()| {
let _ = RouterContext::get().push(route.clone());
});
}
context.into_element()
});
rect()
.key(self.cluster.id)
.width(Size::flex(1.0))
@ -174,6 +237,14 @@ impl Component for ClusterCard {
.color(Color::from_af32rgb(0.3, 0, 0, 0)),
)
.on_press(on_press)
.on_secondary_down(move |e: Event<PressEventData>| {
if let PressEventData::Mouse(m) = e.data() {
menu.set(Some((
m.global_location.x as f32,
m.global_location.y as f32,
)));
}
})
.child(
rect()
.width(Size::fill())
@ -223,6 +294,11 @@ impl Component for ClusterCard {
),
),
)
.maybe_child(menu_overlay)
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}