T26: the arrow over the terminal; the settings window drops General, its Shell and Start in leading Terminal, which lists every shortcut from the keymap with a search, and its categories and page are two columns that scroll apart
This commit is contained in:
9 files changed
+210
-84
No files matched your search
+58
-62
@@ -3,11 +3,11 @@ use std::fmt::Debug;
|
||||
use ely_gpui_component::{
|
||||
buttons::SegmentedControl,
|
||||
feedback::InlineMessage,
|
||||
forms::{InputEvent, PathInput, Switch, TextInput},
|
||||
primitives::{FocusScope, IconName, Severity},
|
||||
settings::{SettingsLayout, SettingsRow, SettingsSection},
|
||||
forms::{InputEvent, Switch, TextInput},
|
||||
lists::{ListItem, SelectableList},
|
||||
primitives::{FocusScope, Icon, IconName, Severity},
|
||||
shell::{ColumnShell, ResizeBorder, ShellColumn},
|
||||
theme::{ActiveTheme, ControlSize},
|
||||
theme::{ActiveTheme, ControlSize, IconSize},
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
@@ -16,7 +16,7 @@ use gpui::{
|
||||
actions, div, prelude::FluentBuilder, px, size,
|
||||
};
|
||||
|
||||
use super::{Settings, SettingsStore, StartIn};
|
||||
use super::{Settings, SettingsStore};
|
||||
use crate::{app::APP_ID, menus, term::BLINK};
|
||||
|
||||
mod appearance;
|
||||
@@ -28,8 +28,7 @@ actions!(elyterminal, [OpenSettings, CloseSettings]);
|
||||
const CONTEXT: &str = "Settings";
|
||||
|
||||
/// Each category's key, name and icon.
|
||||
const SECTIONS: [(&str, &str, IconName); 3] = [
|
||||
("general", "General", IconName::Settings),
|
||||
const SECTIONS: [(&str, &str, IconName); 2] = [
|
||||
("appearance", "Appearance", IconName::Palette),
|
||||
("terminal", "Terminal", IconName::Terminal),
|
||||
];
|
||||
@@ -83,11 +82,13 @@ fn open_window(cx: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings window: categories down the side, the open one's rows beside them.
|
||||
/// The settings window: categories in one column, the open one's rows in the next, each scrolling on its own.
|
||||
pub struct SettingsWindow {
|
||||
focus: FocusHandle,
|
||||
section: SharedString,
|
||||
shell: Entity<TextInput>,
|
||||
/// The shortcut list's search.
|
||||
search: Entity<TextInput>,
|
||||
/// Why the typed shell is not saved.
|
||||
refused: Option<SharedString>,
|
||||
/// Why the last change did not save.
|
||||
@@ -110,6 +111,7 @@ impl SettingsWindow {
|
||||
input
|
||||
});
|
||||
let font = cx.new(|cx| TextInput::new(window, cx));
|
||||
let search = cx.new(|cx| TextInput::new(window, cx).placeholder("Search shortcuts"));
|
||||
let typed = cx.subscribe(&shell, |this, input, event: &InputEvent, cx| {
|
||||
if let InputEvent::Changed = event {
|
||||
let shell = input.read(cx).text().to_string();
|
||||
@@ -123,6 +125,7 @@ impl SettingsWindow {
|
||||
focus,
|
||||
section: SECTIONS[0].0.into(),
|
||||
shell,
|
||||
search,
|
||||
refused: None,
|
||||
failed: None,
|
||||
drafts: 0,
|
||||
@@ -167,46 +170,36 @@ impl SettingsWindow {
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn general(&self, cx: &Context<Self>) -> SettingsSection {
|
||||
let start = match SettingsStore::get(cx).start_in {
|
||||
StartIn::Home => "home",
|
||||
StartIn::Session => "session",
|
||||
};
|
||||
let shell = div()
|
||||
.w(cx.theme().prose_width())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(PathInput::new("settings-shell", &self.shell))
|
||||
.children((self.refused.clone()).map(|why| InlineMessage::new(Severity::Danger, why)));
|
||||
let start = SegmentedControl::new("settings-start-in", start)
|
||||
.segment("home", "Home", None)
|
||||
.segment("session", "Session's folder", None)
|
||||
.size(ControlSize::Sm)
|
||||
.on_change({
|
||||
let this = cx.entity();
|
||||
move |value, _, cx| start_in(&this, value, cx)
|
||||
});
|
||||
SettingsSection::new("General")
|
||||
.row(SettingsRow::new("Shell").control(shell))
|
||||
.row(SettingsRow::new("Start in").control(start))
|
||||
}
|
||||
}
|
||||
|
||||
/// Where new sessions start, as the settings keep it.
|
||||
fn start_in(this: &Entity<SettingsWindow>, value: &SharedString, cx: &mut App) {
|
||||
let start = match value.as_ref() {
|
||||
"home" => StartIn::Home,
|
||||
"session" => StartIn::Session,
|
||||
other => unreachable!("settings: no start {other}"),
|
||||
};
|
||||
set(
|
||||
this,
|
||||
format!("new sessions start in {start:?}"),
|
||||
cx,
|
||||
|settings| settings.start_in = start,
|
||||
/// The categories; a press, Up and Down, or Enter opens one.
|
||||
fn sections(open: &SharedString, cx: &Context<SettingsWindow>) -> SelectableList {
|
||||
let muted = cx.theme().colors.fg_muted;
|
||||
let list = SECTIONS.iter().fold(
|
||||
SelectableList::new("settings-section-list").selected([open.clone()]),
|
||||
|list, (key, name, icon)| {
|
||||
let item = ListItem::new(SharedString::from(format!("settings-section-{key}")), *name)
|
||||
.leading(Icon::new(*icon).size(IconSize::Sm).color(muted));
|
||||
list.row(*key, item)
|
||||
},
|
||||
);
|
||||
let show = {
|
||||
let this = cx.entity();
|
||||
move |key: &SharedString, cx: &mut App| {
|
||||
log::info!("settings: open {key}");
|
||||
this.update(cx, |this, cx| {
|
||||
this.section = key.clone();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
};
|
||||
let enter = show.clone();
|
||||
list.on_change(move |keys, _, cx| {
|
||||
if let Some(key) = keys.first() {
|
||||
show(key, cx);
|
||||
}
|
||||
})
|
||||
.on_activate(move |key, _, cx| enter(key, cx))
|
||||
}
|
||||
|
||||
/// A switch that saves through `put`.
|
||||
@@ -289,9 +282,7 @@ fn set(
|
||||
|
||||
impl Render for SettingsWindow {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let pick = cx.entity();
|
||||
let page = match self.section.as_ref() {
|
||||
"general" => self.general(cx).into_any_element(),
|
||||
"appearance" => self.appearance(window, cx).into_any_element(),
|
||||
"terminal" => self.terminal(cx).into_any_element(),
|
||||
other => unreachable!("settings: no section {other}"),
|
||||
@@ -302,26 +293,31 @@ impl Render for SettingsWindow {
|
||||
.gap_4()
|
||||
.children((self.failed.clone()).map(|why| InlineMessage::new(Severity::Danger, why)))
|
||||
.child(page);
|
||||
let layout = SettingsLayout::new("settings", SECTIONS, self.section.clone(), {
|
||||
move |key, _, cx| {
|
||||
pick.update(cx, |this, cx| {
|
||||
this.section = key.clone();
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
})
|
||||
.page(page);
|
||||
let (_, name, _) = SECTIONS
|
||||
.iter()
|
||||
.find(|(key, ..)| *key == self.section.as_ref())
|
||||
.expect("the open section is listed");
|
||||
let list = div()
|
||||
.id("settings-list")
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.p_2()
|
||||
.child(sections(&self.section, cx));
|
||||
let body = div()
|
||||
.id("settings-body")
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.p_6()
|
||||
.child(layout);
|
||||
let shell = ColumnShell::new("settings").column(
|
||||
ShellColumn::new("settings-column")
|
||||
.title("Settings")
|
||||
.child(body),
|
||||
);
|
||||
.child(page);
|
||||
// The categories and the page scroll apart.
|
||||
let shell = ColumnShell::new("settings")
|
||||
.column(
|
||||
ShellColumn::new("settings-sections")
|
||||
.width(cx.theme().label_width() * 1.25)
|
||||
.title("Settings")
|
||||
.child(list),
|
||||
)
|
||||
.column(ShellColumn::new("settings-page").title(*name).child(body));
|
||||
// The context sits above the focus root, so its keys work wherever focus is.
|
||||
div()
|
||||
.size_full()
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
use ely_gpui_component::{
|
||||
forms::NumberInput,
|
||||
settings::{SettingsRow, SettingsSection},
|
||||
theme::ControlSize,
|
||||
buttons::SegmentedControl,
|
||||
feedback::InlineMessage,
|
||||
forms::{Copy, NumberInput, Paste, PathInput, SelectAll},
|
||||
primitives::Severity,
|
||||
settings::{KeyboardShortcutsList, SettingsRow, SettingsSection, Shortcut},
|
||||
theme::{ActiveTheme, ControlSize},
|
||||
};
|
||||
use gpui::{
|
||||
Action, App, Context, Div, Entity, KeyContext, ParentElement, SharedString, Styled, div,
|
||||
};
|
||||
use gpui::Context;
|
||||
|
||||
use super::{SettingsWindow, choice, set, switch};
|
||||
use crate::settings::{OptionAsAlt, SCROLLBACK, SettingsStore};
|
||||
use super::{OpenSettings, SettingsWindow, choice, set, switch};
|
||||
use crate::{
|
||||
menus::{Bigger, Minimize, Quit, ResetSize, Smaller},
|
||||
settings::{OptionAsAlt, SCROLLBACK, SettingsStore, StartIn},
|
||||
term::input::{self, Find},
|
||||
workspace::{
|
||||
self, CloseSession, LastSession, NewSession, NextSession, PreviousSession, SelectSession,
|
||||
TogglePanel, ToggleSessions,
|
||||
},
|
||||
};
|
||||
|
||||
/// Each way Option types Alt, its key and its name.
|
||||
const OPTIONS: [(OptionAsAlt, &str, &str); 4] = [
|
||||
@@ -16,11 +29,42 @@ const OPTIONS: [(OptionAsAlt, &str, &str); 4] = [
|
||||
(OptionAsAlt::Both, "both", "Both"),
|
||||
];
|
||||
|
||||
/// The names of the sessions a digit shows.
|
||||
const PLACES: [&str; 8] = [
|
||||
"Session 1",
|
||||
"Session 2",
|
||||
"Session 3",
|
||||
"Session 4",
|
||||
"Session 5",
|
||||
"Session 6",
|
||||
"Session 7",
|
||||
"Session 8",
|
||||
];
|
||||
|
||||
impl SettingsWindow {
|
||||
/// Scrollback, Option as Alt on macOS, and copy on select.
|
||||
pub(super) fn terminal(&self, cx: &Context<Self>) -> SettingsSection {
|
||||
/// Shell, start in, scrollback, Option as Alt on macOS and copy on select, then the shortcuts.
|
||||
pub(super) fn terminal(&self, cx: &Context<Self>) -> Div {
|
||||
let settings = SettingsStore::get(cx);
|
||||
let this = cx.entity();
|
||||
let shell = div()
|
||||
.w(cx.theme().prose_width())
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(PathInput::new("settings-shell", &self.shell))
|
||||
.children((self.refused.clone()).map(|why| InlineMessage::new(Severity::Danger, why)));
|
||||
let start = match settings.start_in {
|
||||
StartIn::Home => "home",
|
||||
StartIn::Session => "session",
|
||||
};
|
||||
let start = SegmentedControl::new("settings-start-in", start)
|
||||
.segment("home", "Home", None)
|
||||
.segment("session", "Session's folder", None)
|
||||
.size(ControlSize::Sm)
|
||||
.on_change({
|
||||
let this = this.clone();
|
||||
move |value, _, cx| start_in(&this, value, cx)
|
||||
});
|
||||
let lines = NumberInput::new(("scrollback", self.drafts), f64::from(settings.scrollback))
|
||||
.range(f64::from(*SCROLLBACK.start()), f64::from(*SCROLLBACK.end()))
|
||||
.step(1000.)
|
||||
@@ -35,6 +79,8 @@ impl SettingsWindow {
|
||||
}
|
||||
});
|
||||
let mut section = SettingsSection::new("Terminal")
|
||||
.row(SettingsRow::new("Shell").control(shell))
|
||||
.row(SettingsRow::new("Start in").control(start))
|
||||
.row(SettingsRow::new("Scrollback lines").control(lines));
|
||||
// Off macOS, Alt is always Alt.
|
||||
if cfg!(target_os = "macos") {
|
||||
@@ -53,6 +99,86 @@ impl SettingsWindow {
|
||||
settings.copy_on_select,
|
||||
|settings, on| settings.copy_on_select = on,
|
||||
);
|
||||
section.row(SettingsRow::new("Copy on select").control(copy))
|
||||
let section = section.row(SettingsRow::new("Copy on select").control(copy));
|
||||
let keys = SettingsSection::new("Shortcuts")
|
||||
.row(KeyboardShortcutsList::new(shortcuts(cx), &self.search));
|
||||
div().flex().flex_col().gap_6().child(section).child(keys)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where new sessions start, as the settings keep it.
|
||||
fn start_in(this: &Entity<SettingsWindow>, value: &SharedString, cx: &mut App) {
|
||||
let start = match value.as_ref() {
|
||||
"home" => StartIn::Home,
|
||||
"session" => StartIn::Session,
|
||||
other => unreachable!("settings: no start {other}"),
|
||||
};
|
||||
set(
|
||||
this,
|
||||
format!("new sessions start in {start:?}"),
|
||||
cx,
|
||||
|settings| settings.start_in = start,
|
||||
);
|
||||
}
|
||||
|
||||
/// The listed commands, in order: group, name and action.
|
||||
fn listed() -> Vec<(&'static str, &'static str, Box<dyn Action>)> {
|
||||
let mut listed = vec![
|
||||
("Sessions", "New session", NewSession.boxed_clone()),
|
||||
("Sessions", "Close session", CloseSession.boxed_clone()),
|
||||
("Sessions", "Next session", NextSession.boxed_clone()),
|
||||
(
|
||||
"Sessions",
|
||||
"Previous session",
|
||||
PreviousSession.boxed_clone(),
|
||||
),
|
||||
];
|
||||
let places = PLACES.iter().enumerate();
|
||||
listed.extend(places.map(|(ix, name)| ("Sessions", *name, SelectSession(ix).boxed_clone())));
|
||||
listed.extend([
|
||||
("Sessions", "Last session", LastSession.boxed_clone()),
|
||||
("Edit", "Copy", Copy.boxed_clone()),
|
||||
("Edit", "Paste", Paste.boxed_clone()),
|
||||
("Edit", "Select all", SelectAll.boxed_clone()),
|
||||
("Edit", "Find", Find.boxed_clone()),
|
||||
("View", "Toggle sessions", ToggleSessions.boxed_clone()),
|
||||
("View", "Toggle right panel", TogglePanel.boxed_clone()),
|
||||
("View", "Bigger", Bigger.boxed_clone()),
|
||||
("View", "Smaller", Smaller.boxed_clone()),
|
||||
("View", "Reset size", ResetSize.boxed_clone()),
|
||||
("App", "Settings", OpenSettings.boxed_clone()),
|
||||
("App", "Minimize", Minimize.boxed_clone()),
|
||||
("App", "Quit", Quit.boxed_clone()),
|
||||
]);
|
||||
listed
|
||||
}
|
||||
|
||||
/// Each listed command's keys in the main window's terminal, a line per binding, from the keymap.
|
||||
fn shortcuts(cx: &App) -> Vec<Shortcut> {
|
||||
let stack = [workspace::CONTEXT, input::CONTEXT]
|
||||
.map(|name| KeyContext::parse(name).expect("a context name parses"));
|
||||
let keymap = cx.key_bindings();
|
||||
let keymap = keymap.borrow();
|
||||
let mut shortcuts = Vec::new();
|
||||
for (group, name, action) in listed() {
|
||||
for binding in keymap.bindings_for_action(action.as_ref()) {
|
||||
// gpui matches a binding at any depth of the context stack.
|
||||
let applies = binding.predicate().is_none_or(|predicate| {
|
||||
(1..=stack.len()).any(|depth| predicate.eval(&stack[..depth]))
|
||||
});
|
||||
if applies {
|
||||
let strokes: Vec<String> = binding
|
||||
.keystrokes()
|
||||
.iter()
|
||||
.map(|key| key.unparse())
|
||||
.collect();
|
||||
shortcuts.push(Shortcut {
|
||||
group: group.into(),
|
||||
name: name.into(),
|
||||
keys: strokes.join(" ").into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcuts
|
||||
}
|
||||
@@ -360,7 +360,6 @@ impl Render for TerminalView {
|
||||
.on_any_mouse_down(cx.listener(Self::mouse_down))
|
||||
.on_mouse_move(cx.listener(Self::mouse_move))
|
||||
.on_scroll_wheel(cx.listener(Self::wheel))
|
||||
.cursor_text()
|
||||
.size_full()
|
||||
.px(px(settings.padding_x))
|
||||
.py(px(settings.padding_y))
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ actions!(
|
||||
pub struct SelectSession(pub usize);
|
||||
|
||||
/// The key context of the main window's content.
|
||||
const CONTEXT: &str = "Workspace";
|
||||
pub const CONTEXT: &str = "Workspace";
|
||||
|
||||
/// The window's title while the program names none.
|
||||
pub const TITLE: &str = "ELYTerminal";
|
||||
|
||||
Reference in new issue
Block a user