OneLauncher-Cracked/packages/oneclient_app/src/components/context_menu.rs

293 lines
8.2 KiB
Rust

use freya::prelude::*;
use crate::components::{Icon, IconType, OVERLAY_BASE_LEVEL, OverlayPopup};
use crate::theme::colors;
const MENU_BG: Color = Color::from_rgb(25, 32, 38);
const MENU_BORDER: Color = Color::from_argb(26, 255, 255, 255);
const MENU_FG: Color = Color::from_rgb(155, 161, 166);
const MENU_DANGER: Color = Color::from_rgb(242, 84, 90);
/// Panel padding and border on both axes, which the measured list sits inside of
const PANEL_INSET: f32 = 14.;
/// Gap the clamped menu keeps from the window edges
const EDGE_MARGIN: f32 = 8.;
enum Entry {
Action {
icon: IconType,
label: String,
danger: bool,
on_select: EventHandler<()>,
},
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,
title: Option<String>,
entries: Vec<Entry>,
on_close: EventHandler<()>,
overlay_level: u8,
}
impl ContextMenu {
pub fn new(x: f32, y: f32) -> Self {
Self {
x,
y,
upwards: false,
title: None,
entries: Vec::new(),
on_close: (|()| {}).into(),
overlay_level: OVERLAY_BASE_LEVEL,
}
}
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
}
pub fn on_close(mut self, on_close: impl Into<EventHandler<()>>) -> Self {
self.on_close = on_close.into();
self
}
pub fn overlay_level(mut self, level: u8) -> Self {
self.overlay_level = level;
self
}
pub fn action(
mut self,
icon: IconType,
label: impl Into<String>,
on_select: impl Into<EventHandler<()>>,
) -> Self {
self.entries.push(Entry::Action {
icon,
label: label.into(),
danger: false,
on_select: on_select.into(),
});
self
}
pub fn danger_action(
mut self,
icon: IconType,
label: impl Into<String>,
on_select: impl Into<EventHandler<()>>,
) -> Self {
self.entries.push(Entry::Action {
icon,
label: label.into(),
danger: true,
on_select: on_select.into(),
});
self
}
pub fn separator(mut self) -> Self {
self.entries.push(Entry::Separator);
self
}
}
impl PartialEq for ContextMenu {
fn eq(&self, _other: &Self) -> bool {
// Always unequal the menu is rebuilt from scratch whenever it is reopened
false
}
}
impl Component for ContextMenu {
fn render(&self) -> impl IntoElement {
let on_close = self.on_close.clone();
let mut size = use_state(Size2D::zero);
let measured = *size.read();
let item_width = (measured.width > 0.).then_some(measured.width);
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 => list.child(separator(item_width)),
Entry::Action {
icon,
label,
danger,
on_select,
} => list.child(
ContextMenuRow {
icon: *icon,
label: label.clone(),
danger: *danger,
width: item_width,
on_select: on_select.clone(),
on_close: on_close.clone(),
}
.into_element(),
),
};
}
let list = list.on_sized(move |e: Event<SizedEventData>| {
let area = e.data().area.size;
let prev = *size.peek();
if (area.width - prev.width).abs() > 0.5 || (area.height - prev.height).abs() > 0.5 {
size.set(area);
}
});
let placed = measured.width > 0.;
let menu_width = measured.width + PANEL_INSET;
let menu_height = measured.height + PANEL_INSET;
let (x, y) = if placed {
let top = if self.upwards && self.y - menu_height >= 0. {
self.y - menu_height
} else {
self.y
};
clamp_to_window(self.x, top, menu_width, menu_height)
} else {
(self.x, self.y)
};
let panel = rect()
.vertical()
.padding(Gaps::new_all(6.))
.corner_radius(CornerRadius::new_all(12.))
.background(MENU_BG)
.border(Border::new().fill(MENU_BORDER).width(BorderWidth {
top: 1.,
right: 1.,
bottom: 1.,
left: 1.,
}))
.opacity(if placed { 1. } else { 0. })
.child(list);
OverlayPopup::new()
.backdrop(false)
.overlay_level(self.overlay_level)
.position(Position::new_global().top(y).left(x))
.on_close(move |_| on_close.call(()))
.child(panel.into_element())
}
}
/// `root_size` is physical while the press position is logical, so it has to be
/// scaled down before the two are compared
fn clamp_to_window(x: f32, y: f32, menu_width: f32, menu_height: f32) -> (f32, f32) {
let platform = Platform::get();
let scale = *platform.scale_factor.peek() as f32;
if scale <= 0. {
return (x, y);
}
let window = *platform.root_size.peek();
let clamp = |pos: f32, len: f32, limit: f32| {
pos.clamp(EDGE_MARGIN, (limit - len - EDGE_MARGIN).max(EDGE_MARGIN))
};
(
clamp(x, menu_width, window.width / scale),
clamp(y, menu_height, window.height / scale),
)
}
#[derive(PartialEq)]
struct ContextMenuRow {
icon: IconType,
label: String,
danger: bool,
width: Option<f32>,
on_select: EventHandler<()>,
on_close: EventHandler<()>,
}
impl Component for ContextMenuRow {
fn render(&self) -> impl IntoElement {
let mut hovered = use_state(|| false);
let on_select = self.on_select.clone();
let on_close = self.on_close.clone();
let base = if self.danger { MENU_DANGER } else { MENU_FG };
let color = if *hovered.read() {
colors::fg_primary()
} else {
base
};
let mut root = rect();
if let Some(w) = self.width {
root = root.width(Size::px(w));
}
root.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
.padding(Gaps::new_symmetric(5., 8.))
.corner_radius(CornerRadius::new_all(6.))
.background(if *hovered.read() {
colors::component_bg_hover()
} else {
Color::TRANSPARENT
})
.cursor(CursorIcon::Pointer)
.on_pointer_enter(move |_| hovered.set(true))
.on_pointer_leave(move |_| hovered.set(false))
.on_press(move |_| {
on_select.call(());
on_close.call(());
})
.child(Icon::new(self.icon).size(18.).color(color))
.child(
label()
.text(self.label.clone())
.font_size(12.)
.font_weight(FontWeight::MEDIUM)
.max_lines(1)
.color(color),
)
}
}