use crate::{
model::{settings::Shortcuts, workspace::Workspace},
view::{
hotkey,
keymap::{self, Command, Menu},
settings::{self, SettingsWindow},
},
};
use bezel::{
gpui::{
AnyElement, App, Context, Entity, FocusHandle, KeyDownEvent, SharedString, Subscription,
Window, div, prelude::*, px,
},
theme::{TextStyle, Theme, Typeset},
ui::widgets::{Buttons, Scaffolding},
};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Target {
Activate,
Command(Command),
}
impl Target {
fn key(self) -> &'static str {
match self {
Self::Activate => Shortcuts::ACTIVATE,
Self::Command(command) => command.key(),
}
}
}
pub(super) struct Recording {
target: Target,
focus: FocusHandle,
refused: Option<SharedString>,
_blur: Subscription,
}
const UNBOUND: &str = "None";
impl SettingsWindow {
pub(super) fn shortcuts_body(&self, cx: &mut Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
div()
.flex()
.flex_col()
.gap(px(settings::GROUP_GAP))
.child(
div()
.flex()
.flex_col()
.gap(px(settings::LABEL_GAP))
.child(theme.field_label("System"))
.child(theme.group_box().child(self.activate_row(cx))),
)
.children(Menu::ALL.into_iter().map(|menu| {
div()
.flex()
.flex_col()
.gap(px(settings::LABEL_GAP))
.child(theme.field_label(menu.title()))
.child(
theme.group_box().children(
Command::ALL
.into_iter()
.filter(|command| command.menu() == menu)
.enumerate()
.map(|(ix, command)| self.command_row(ix, command, cx)),
),
)
}))
.into_any_element()
}
fn activate_row(&self, cx: &mut Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
let held = self
.workspace
.read(cx)
.settings
.shortcuts
.activate()
.map(str::to_owned);
let refused = hotkey::refused(cx);
let blurb = match refused.clone() {
Some(why) => why,
None if held.is_some() => SharedString::from("Brings cydonia forward from anywhere."),
None => {
SharedString::from("Off. A key held across the whole desktop is one to ask for.")
}
};
theme
.card_row(true)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(theme.row_title("Activate cydonia"))
.child(
div()
.mt(px(4.))
.text_style(TextStyle::Subheadline)
.text_color(if refused.is_some() {
theme.danger
} else {
theme.text_muted
})
.child(blurb),
),
)
.children(
held.is_some()
.then(|| self.clear_button(Target::Activate, cx)),
)
.child(self.chord_button(Target::Activate, held.as_deref(), cx))
.into_any_element()
}
fn command_row(&self, ix: usize, command: Command, cx: &mut Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
let shortcuts = &self.workspace.read(cx).settings.shortcuts;
let held = keymap::chord(command, shortcuts).map(str::to_owned);
let moved = shortcuts.get(command.key()).is_some();
let note = self.row_note(command, cx);
theme
.card_row(ix == 0)
.child(
div()
.flex_1()
.min_w_0()
.flex()
.flex_col()
.child(theme.row_title(command.title()))
.children(note),
)
.children(moved.then(|| self.clear_button(Target::Command(command), cx)))
.child(self.chord_button(Target::Command(command), held.as_deref(), cx))
.into_any_element()
}
fn row_note(&self, command: Command, cx: &Context<Self>) -> Option<AnyElement> {
let theme = Theme::of(cx).clone();
let shortcuts = &self.workspace.read(cx).settings.shortcuts;
let held = keymap::chord(command, shortcuts);
let clash = held
.map(|chord| keymap::claimed(chord, command, shortcuts))
.unwrap_or_default();
let (copy, colour) = match (clash.first(), shortcuts.get(command.key())) {
(Some(other), _) => (
format!("Also {}, which wins it.", other.title()),
theme.danger,
),
(None, Some(_)) => (
match command.default() {
Some(default) => format!(
"Moved from {}.",
keymap::glyphs(default).unwrap_or_else(|| default.into())
),
None => "Moved from nothing.".to_owned(),
},
theme.text_muted,
),
(None, None) => return None,
};
Some(
div()
.mt(px(4.))
.text_style(TextStyle::Subheadline)
.text_color(colour)
.child(copy)
.into_any_element(),
)
}
fn chord_button(
&self,
target: Target,
held: Option<&str>,
cx: &mut Context<Self>,
) -> AnyElement {
let theme = Theme::of(cx).clone();
let recording = self
.recording
.as_ref()
.filter(|recording| recording.target == target);
let label: SharedString = match (recording, held) {
(Some(recording), _) => recording
.refused
.clone()
.unwrap_or_else(|| "Press a key…".into()),
(None, Some(chord)) => keymap::glyphs(chord).unwrap_or_else(|| chord.into()),
(None, None) => UNBOUND.into(),
};
let button = theme
.ghost(SharedString::from(format!("chord-{}", target.key())))
.px(px(10.))
.py(px(3.))
.border_1()
.text_style(TextStyle::Callout)
.child(label);
match recording {
Some(recording) => button
.track_focus(&recording.focus)
.border_color(theme.accent)
.text_color(theme.accent)
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
this.captured(event, window, cx)
}))
.into_any_element(),
None => button
.border_color(theme.border)
.bg(theme.input_bg)
.text_color(if held.is_some() {
theme.text
} else {
theme.text_muted
})
.on_click(cx.listener(move |this, _, window, cx| this.record(target, window, cx)))
.into_any_element(),
}
}
fn clear_button(&self, target: Target, cx: &mut Context<Self>) -> AnyElement {
let theme = Theme::of(cx).clone();
theme
.ghost(SharedString::from(format!("revert-{}", target.key())))
.px(px(8.))
.py(px(3.))
.text_style(TextStyle::Subheadline)
.text_color(theme.text_muted)
.child(match target {
Target::Activate => "Off",
Target::Command(_) => "Reset",
})
.on_click(cx.listener(move |this, _, _, cx| this.put(target, None, cx)))
.into_any_element()
}
fn record(&mut self, target: Target, window: &mut Window, cx: &mut Context<Self>) {
cx.clear_key_bindings();
cx.set_menus(Vec::new());
let focus = cx.focus_handle();
focus.focus(window, cx);
let weak = cx.entity().downgrade();
let blur = window.on_focus_out(&focus, cx, move |_, _, cx| {
let weak = weak.clone();
cx.defer(move |cx| {
let _ = weak.update(cx, |this: &mut Self, cx| {
if this.recording.as_ref().map(|held| held.target) == Some(target) {
this.stop(cx);
}
});
});
});
self.recording = Some(Recording {
target,
focus,
refused: None,
_blur: blur,
});
cx.notify();
}
fn captured(&mut self, event: &KeyDownEvent, _: &mut Window, cx: &mut Context<Self>) {
let Some(recording) = self.recording.as_mut() else {
return;
};
if event.is_held {
return;
}
let target = recording.target;
let stroke = &event.keystroke;
let modified = stroke.modifiers.control
|| stroke.modifiers.alt
|| stroke.modifiers.platform
|| stroke.modifiers.function;
match stroke.key.as_str() {
"escape" if !modified => self.stop(cx),
"delete" | "backspace" if !modified => self.put(target, Some(""), cx),
_ if !modified => {
recording.refused = Some("Needs ⌘, ⌃ or ⌥".into());
cx.notify();
}
_ => {
let chord = stroke.unparse();
if target == Target::Activate && !hotkey::holdable(&chord) {
recording.refused = Some("The system cannot hold that".into());
cx.notify();
return;
}
self.put(target, Some(&chord), cx);
}
}
}
fn stop(&mut self, cx: &mut Context<Self>) {
self.recording = None;
self.restore(cx);
cx.notify();
}
fn put(&mut self, target: Target, chord: Option<&str>, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.set_shortcut(target.key(), chord, cx)
});
self.recording = None;
self.restore(cx);
cx.notify();
}
fn restore(&self, cx: &mut Context<Self>) {
restore(&self.workspace, cx);
}
}
pub(super) fn restore(workspace: &Entity<Workspace>, cx: &mut App) {
let shortcuts = workspace.read(cx).settings.shortcuts.clone();
keymap::rebind(&shortcuts, cx);
hotkey::apply(shortcuts.activate(), cx);
}