use std::rc::Rc;
use gpui::{
App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
StatefulInteractiveElement, Styled, Window, div, px,
};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, Elevation, Radius, Space, Surface, TextTone, Theme, TypeScale};
use crate::foundation::{FocusRing, Ident, StyledExt, text};
use crate::strings::{ActiveStrings, StringKey};
type ChangeHandler = Rc<dyn Fn(PermissionChange, &mut Window, &mut App)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PermissionState {
Allowed,
Denied,
#[default]
Ask,
NotApplicable,
}
impl PermissionState {
pub fn name(self) -> &'static str {
match self {
Self::Allowed => "allowed",
Self::Denied => "denied",
Self::Ask => "ask",
Self::NotApplicable => "not-applicable",
}
}
pub fn label(self, cx: &App) -> SharedString {
cx.strings().text(match self {
Self::Allowed => StringKey::PermissionAllowed,
Self::Denied => StringKey::PermissionDenied,
Self::Ask => StringKey::PermissionAsk,
Self::NotApplicable => StringKey::PermissionNotApplicable,
})
}
pub fn next(self) -> Option<Self> {
match self {
Self::Allowed => Some(Self::Ask),
Self::Ask => Some(Self::Denied),
Self::Denied => Some(Self::Allowed),
Self::NotApplicable => None,
}
}
fn color(self, theme: &Theme) -> gpui::Hsla {
match self {
Self::Allowed => theme.colors.success,
Self::Denied => theme.colors.danger,
Self::Ask => theme.colors.warning,
Self::NotApplicable => theme.colors.text_faint,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum PermissionSource {
#[default]
Here,
Inherited(SharedString),
}
impl PermissionSource {
pub fn inherited(from: impl Into<SharedString>) -> Self {
Self::Inherited(from.into())
}
pub fn name(&self) -> &'static str {
match self {
Self::Here => "here",
Self::Inherited(_) => "inherited",
}
}
pub fn label(&self, cx: &App) -> SharedString {
match self {
Self::Here => cx.strings().text(StringKey::PermissionSetHere),
Self::Inherited(from) => cx.strings().format(StringKey::PermissionInherited, &[from]),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionEntry {
state: PermissionState,
source: PermissionSource,
}
impl PermissionEntry {
pub fn new(state: PermissionState) -> Self {
Self {
state,
source: PermissionSource::Here,
}
}
pub fn inherited(state: PermissionState, from: impl Into<SharedString>) -> Self {
Self {
state,
source: PermissionSource::inherited(from),
}
}
pub fn state(&self) -> PermissionState {
self.state
}
pub fn source(&self) -> &PermissionSource {
&self.source
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionAction {
key: SharedString,
label: SharedString,
}
impl PermissionAction {
pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
Self {
key: key.into(),
label: label.into(),
}
}
pub fn key(&self) -> &SharedString {
&self.key
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionSubject {
id: SharedString,
label: SharedString,
cells: Vec<(SharedString, PermissionEntry)>,
}
impl PermissionSubject {
pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
label: label.into(),
cells: Vec::new(),
}
}
pub fn cell(mut self, action: impl Into<SharedString>, entry: PermissionEntry) -> Self {
self.cells.push((action.into(), entry));
self
}
pub fn id(&self) -> &SharedString {
&self.id
}
fn entry(&self, action: &SharedString) -> PermissionEntry {
self.cells
.iter()
.find(|(key, _)| key == action)
.map(|(_, entry)| entry.clone())
.unwrap_or_else(|| PermissionEntry::new(PermissionState::NotApplicable))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PermissionChange {
pub subject: SharedString,
pub action: SharedString,
pub next: PermissionState,
}
#[derive(IntoElement)]
pub struct PermissionMatrix {
ident: Ident,
actions: Vec<PermissionAction>,
subjects: Vec<PermissionSubject>,
on_change: Option<ChangeHandler>,
}
impl std::fmt::Debug for PermissionMatrix {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PermissionMatrix")
.field("ident", &self.ident)
.field("actions", &self.actions.len())
.field("subjects", &self.subjects.len())
.field("editable", &self.on_change.is_some())
.finish()
}
}
impl PermissionMatrix {
pub fn new(ident: impl Into<Ident>) -> Self {
Self {
ident: ident.into(),
actions: Vec::new(),
subjects: Vec::new(),
on_change: None,
}
}
pub fn action(mut self, action: PermissionAction) -> Self {
self.actions.push(action);
self
}
pub fn actions(mut self, actions: impl IntoIterator<Item = PermissionAction>) -> Self {
self.actions.extend(actions);
self
}
pub fn subject(mut self, subject: PermissionSubject) -> Self {
self.subjects.push(subject);
self
}
pub fn subjects(mut self, subjects: impl IntoIterator<Item = PermissionSubject>) -> Self {
self.subjects.extend(subjects);
self
}
pub fn on_change(
mut self,
handler: impl Fn(PermissionChange, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for PermissionMatrix {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let heading = div()
.row()
.w_full()
.gap_token(&theme, Space::Sm)
.px_token(&theme, Space::Sm)
.py_token(&theme, Space::Xs)
.child(
div().w(px(160.0)).flex_none().child(
text(
&theme,
TypeScale::Caption,
cx.strings().text(StringKey::PermissionSubjectHeading),
)
.text_tone(&theme, TextTone::Faint),
),
)
.children(self.actions.iter().map(|action| {
div().flex_1().min_w_0().child(
text(&theme, TypeScale::Caption, action.label.clone())
.text_tone(&theme, TextTone::Faint),
)
}));
let rows: Vec<_> = self
.subjects
.iter()
.map(|subject| {
let row_ident = self.ident.child(subject.id.as_ref());
div()
.row()
.items_start()
.w_full()
.gap_token(&theme, Space::Sm)
.px_token(&theme, Space::Sm)
.py_token(&theme, Space::Xs)
.child(
div()
.w(px(160.0))
.flex_none()
.child(text(&theme, TypeScale::Label, subject.label.clone()))
.semantic_in(
cx,
NodeSpec::new(row_ident.semantic_id(), Role::Row)
.text(subject.label.clone())
.parent(self.ident.semantic_id()),
),
)
.children(self.actions.iter().map(|action| {
cell(
&theme,
&row_ident,
subject,
action,
self.on_change.clone(),
cx,
)
}))
})
.collect();
div()
.column()
.w_full()
.radius(&theme, Radius::Card)
.frame(&theme, Surface::Panel, Elevation::Raised)
.child(heading)
.children(rows)
.semantic_in(
cx,
NodeSpec::new(self.ident.semantic_id(), Role::Table)
.value(SharedString::from(self.subjects.len().to_string())),
)
}
}
fn cell(
theme: &Theme,
row_ident: &Ident,
subject: &PermissionSubject,
action: &PermissionAction,
on_change: Option<ChangeHandler>,
cx: &App,
) -> gpui::AnyElement {
let entry = subject.entry(&action.key);
let state = entry.state();
let ident = row_ident.child(action.key.as_ref());
let name = cx.strings().format(
StringKey::PermissionCellName,
&[&action.label, &state.label(cx)],
);
let next = state.next();
let handler = on_change.zip(next);
let mark = div()
.row()
.gap_token(theme, Space::Xs)
.child(
div()
.flex_none()
.size(px(7.0))
.rounded_full()
.bg(state.color(theme)),
)
.child(text(theme, TypeScale::Label, state.label(cx)));
let source = (state != PermissionState::NotApplicable).then(|| {
text(theme, TypeScale::Caption, entry.source().label(cx))
.text_tone(theme, TextTone::Faint)
.semantic_in(
cx,
NodeSpec::new(ident.child("source").semantic_id(), Role::Text)
.text(entry.source().label(cx))
.value(SharedString::new_static(entry.source().name()))
.parent(ident.semantic_id()),
)
});
let body = div()
.column()
.gap_token(theme, Space::Xs)
.child(mark)
.children(source);
let spec = NodeSpec::new(
ident.semantic_id(),
if handler.is_some() {
Role::Button
} else {
Role::Cell
},
)
.text(name)
.value(SharedString::new_static(state.name()))
.parent(row_ident.semantic_id());
let frame = div()
.flex_1()
.min_w_0()
.px_token(theme, Space::Sm)
.py_token(theme, Space::Xs)
.hairline(theme)
.radius(theme, Radius::Control);
match handler {
Some((handler, next)) => {
let subject_id = subject.id.clone();
let action_key = action.key.clone();
frame
.id(ident.element_id())
.tab_index(0)
.cursor_pointer()
.hover(|style| style.bg(theme.colors.hover))
.focus_ring(theme)
.on_click(move |_event, window, cx| {
handler(
PermissionChange {
subject: subject_id.clone(),
action: action_key.clone(),
next,
},
window,
cx,
);
})
.child(body)
.semantic_in(cx, spec)
.into_any_element()
}
None => frame
.border_color(gpui::transparent_black())
.child(body)
.semantic_in(cx, spec)
.into_any_element(),
}
}