1use std::rc::Rc;
31
32use gpui::{
33 App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
34 StatefulInteractiveElement, Styled, Window, div, px,
35};
36use gpui_kit_semantics::{NodeSpec, Role, Semantic};
37use gpui_kit_theme::{ActiveTheme, Elevation, Radius, Space, Surface, TextTone, Theme, TypeScale};
38
39use crate::foundation::{FocusRing, Ident, StyledExt, text};
40use crate::strings::{ActiveStrings, StringKey};
41
42type ChangeHandler = Rc<dyn Fn(PermissionChange, &mut Window, &mut App)>;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum PermissionState {
47 Allowed,
49 Denied,
51 #[default]
53 Ask,
54 NotApplicable,
56}
57
58impl PermissionState {
59 pub fn name(self) -> &'static str {
61 match self {
62 Self::Allowed => "allowed",
63 Self::Denied => "denied",
64 Self::Ask => "ask",
65 Self::NotApplicable => "not-applicable",
66 }
67 }
68
69 pub fn label(self, cx: &App) -> SharedString {
71 cx.strings().text(match self {
72 Self::Allowed => StringKey::PermissionAllowed,
73 Self::Denied => StringKey::PermissionDenied,
74 Self::Ask => StringKey::PermissionAsk,
75 Self::NotApplicable => StringKey::PermissionNotApplicable,
76 })
77 }
78
79 pub fn next(self) -> Option<Self> {
84 match self {
85 Self::Allowed => Some(Self::Ask),
86 Self::Ask => Some(Self::Denied),
87 Self::Denied => Some(Self::Allowed),
88 Self::NotApplicable => None,
89 }
90 }
91
92 fn color(self, theme: &Theme) -> gpui::Hsla {
93 match self {
94 Self::Allowed => theme.colors.success,
95 Self::Denied => theme.colors.danger,
96 Self::Ask => theme.colors.warning,
97 Self::NotApplicable => theme.colors.text_faint,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Default)]
104pub enum PermissionSource {
105 #[default]
107 Here,
108 Inherited(SharedString),
110}
111
112impl PermissionSource {
113 pub fn inherited(from: impl Into<SharedString>) -> Self {
114 Self::Inherited(from.into())
115 }
116
117 pub fn name(&self) -> &'static str {
119 match self {
120 Self::Here => "here",
121 Self::Inherited(_) => "inherited",
122 }
123 }
124
125 pub fn label(&self, cx: &App) -> SharedString {
126 match self {
127 Self::Here => cx.strings().text(StringKey::PermissionSetHere),
128 Self::Inherited(from) => cx.strings().format(StringKey::PermissionInherited, &[from]),
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct PermissionEntry {
136 state: PermissionState,
137 source: PermissionSource,
138}
139
140impl PermissionEntry {
141 pub fn new(state: PermissionState) -> Self {
143 Self {
144 state,
145 source: PermissionSource::Here,
146 }
147 }
148
149 pub fn inherited(state: PermissionState, from: impl Into<SharedString>) -> Self {
151 Self {
152 state,
153 source: PermissionSource::inherited(from),
154 }
155 }
156
157 pub fn state(&self) -> PermissionState {
158 self.state
159 }
160
161 pub fn source(&self) -> &PermissionSource {
162 &self.source
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct PermissionAction {
169 key: SharedString,
170 label: SharedString,
171}
172
173impl PermissionAction {
174 pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
175 Self {
176 key: key.into(),
177 label: label.into(),
178 }
179 }
180
181 pub fn key(&self) -> &SharedString {
182 &self.key
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct PermissionSubject {
189 id: SharedString,
190 label: SharedString,
191 cells: Vec<(SharedString, PermissionEntry)>,
192}
193
194impl PermissionSubject {
195 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
196 Self {
197 id: id.into(),
198 label: label.into(),
199 cells: Vec::new(),
200 }
201 }
202
203 pub fn cell(mut self, action: impl Into<SharedString>, entry: PermissionEntry) -> Self {
207 self.cells.push((action.into(), entry));
208 self
209 }
210
211 pub fn id(&self) -> &SharedString {
212 &self.id
213 }
214
215 fn entry(&self, action: &SharedString) -> PermissionEntry {
216 self.cells
217 .iter()
218 .find(|(key, _)| key == action)
219 .map(|(_, entry)| entry.clone())
220 .unwrap_or_else(|| PermissionEntry::new(PermissionState::NotApplicable))
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct PermissionChange {
227 pub subject: SharedString,
228 pub action: SharedString,
229 pub next: PermissionState,
230}
231
232#[derive(IntoElement)]
234pub struct PermissionMatrix {
235 ident: Ident,
236 actions: Vec<PermissionAction>,
237 subjects: Vec<PermissionSubject>,
238 on_change: Option<ChangeHandler>,
239}
240
241impl std::fmt::Debug for PermissionMatrix {
242 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 formatter
244 .debug_struct("PermissionMatrix")
245 .field("ident", &self.ident)
246 .field("actions", &self.actions.len())
247 .field("subjects", &self.subjects.len())
248 .field("editable", &self.on_change.is_some())
249 .finish()
250 }
251}
252
253impl PermissionMatrix {
254 pub fn new(ident: impl Into<Ident>) -> Self {
255 Self {
256 ident: ident.into(),
257 actions: Vec::new(),
258 subjects: Vec::new(),
259 on_change: None,
260 }
261 }
262
263 pub fn action(mut self, action: PermissionAction) -> Self {
264 self.actions.push(action);
265 self
266 }
267
268 pub fn actions(mut self, actions: impl IntoIterator<Item = PermissionAction>) -> Self {
269 self.actions.extend(actions);
270 self
271 }
272
273 pub fn subject(mut self, subject: PermissionSubject) -> Self {
274 self.subjects.push(subject);
275 self
276 }
277
278 pub fn subjects(mut self, subjects: impl IntoIterator<Item = PermissionSubject>) -> Self {
279 self.subjects.extend(subjects);
280 self
281 }
282
283 pub fn on_change(
287 mut self,
288 handler: impl Fn(PermissionChange, &mut Window, &mut App) + 'static,
289 ) -> Self {
290 self.on_change = Some(Rc::new(handler));
291 self
292 }
293}
294
295impl RenderOnce for PermissionMatrix {
296 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
297 let theme = cx.theme().clone();
298 let heading = div()
299 .row()
300 .w_full()
301 .gap_token(&theme, Space::Sm)
302 .px_token(&theme, Space::Sm)
303 .py_token(&theme, Space::Xs)
304 .child(
305 div().w(px(160.0)).flex_none().child(
306 text(
307 &theme,
308 TypeScale::Caption,
309 cx.strings().text(StringKey::PermissionSubjectHeading),
310 )
311 .text_tone(&theme, TextTone::Faint),
312 ),
313 )
314 .children(self.actions.iter().map(|action| {
315 div().flex_1().min_w_0().child(
316 text(&theme, TypeScale::Caption, action.label.clone())
317 .text_tone(&theme, TextTone::Faint),
318 )
319 }));
320
321 let rows: Vec<_> = self
322 .subjects
323 .iter()
324 .map(|subject| {
325 let row_ident = self.ident.child(subject.id.as_ref());
326 div()
327 .row()
328 .items_start()
329 .w_full()
330 .gap_token(&theme, Space::Sm)
331 .px_token(&theme, Space::Sm)
332 .py_token(&theme, Space::Xs)
333 .child(
334 div()
335 .w(px(160.0))
336 .flex_none()
337 .child(text(&theme, TypeScale::Label, subject.label.clone()))
338 .semantic_in(
339 cx,
340 NodeSpec::new(row_ident.semantic_id(), Role::Row)
341 .text(subject.label.clone())
342 .parent(self.ident.semantic_id()),
343 ),
344 )
345 .children(self.actions.iter().map(|action| {
346 cell(
347 &theme,
348 &row_ident,
349 subject,
350 action,
351 self.on_change.clone(),
352 cx,
353 )
354 }))
355 })
356 .collect();
357
358 div()
359 .column()
360 .w_full()
361 .radius(&theme, Radius::Card)
362 .frame(&theme, Surface::Panel, Elevation::Raised)
363 .child(heading)
364 .children(rows)
365 .semantic_in(
366 cx,
367 NodeSpec::new(self.ident.semantic_id(), Role::Table)
368 .value(SharedString::from(self.subjects.len().to_string())),
369 )
370 }
371}
372
373fn cell(
374 theme: &Theme,
375 row_ident: &Ident,
376 subject: &PermissionSubject,
377 action: &PermissionAction,
378 on_change: Option<ChangeHandler>,
379 cx: &App,
380) -> gpui::AnyElement {
381 let entry = subject.entry(&action.key);
382 let state = entry.state();
383 let ident = row_ident.child(action.key.as_ref());
384 let name = cx.strings().format(
385 StringKey::PermissionCellName,
386 &[&action.label, &state.label(cx)],
387 );
388 let next = state.next();
389 let handler = on_change.zip(next);
390
391 let mark = div()
392 .row()
393 .gap_token(theme, Space::Xs)
394 .child(
395 div()
396 .flex_none()
397 .size(px(7.0))
398 .rounded_full()
399 .bg(state.color(theme)),
400 )
401 .child(text(theme, TypeScale::Label, state.label(cx)));
402
403 let source = (state != PermissionState::NotApplicable).then(|| {
406 text(theme, TypeScale::Caption, entry.source().label(cx))
407 .text_tone(theme, TextTone::Faint)
408 .semantic_in(
409 cx,
410 NodeSpec::new(ident.child("source").semantic_id(), Role::Text)
411 .text(entry.source().label(cx))
412 .value(SharedString::new_static(entry.source().name()))
413 .parent(ident.semantic_id()),
414 )
415 });
416
417 let body = div()
418 .column()
419 .gap_token(theme, Space::Xs)
420 .child(mark)
421 .children(source);
422
423 let spec = NodeSpec::new(
424 ident.semantic_id(),
425 if handler.is_some() {
426 Role::Button
427 } else {
428 Role::Cell
429 },
430 )
431 .text(name)
432 .value(SharedString::new_static(state.name()))
433 .parent(row_ident.semantic_id());
434
435 let frame = div()
440 .flex_1()
441 .min_w_0()
442 .px_token(theme, Space::Sm)
443 .py_token(theme, Space::Xs)
444 .hairline(theme)
445 .radius(theme, Radius::Control);
446
447 match handler {
448 Some((handler, next)) => {
449 let subject_id = subject.id.clone();
450 let action_key = action.key.clone();
451 frame
452 .id(ident.element_id())
453 .tab_index(0)
454 .cursor_pointer()
455 .hover(|style| style.bg(theme.colors.hover))
456 .focus_ring(theme)
457 .on_click(move |_event, window, cx| {
458 handler(
459 PermissionChange {
460 subject: subject_id.clone(),
461 action: action_key.clone(),
462 next,
463 },
464 window,
465 cx,
466 );
467 })
468 .child(body)
469 .semantic_in(cx, spec)
470 .into_any_element()
471 }
472 None => frame
473 .border_color(gpui::transparent_black())
474 .child(body)
475 .semantic_in(cx, spec)
476 .into_any_element(),
477 }
478}