mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
feat(settings): every pick-one setting is a dropdown
Single choices were drawn two ways — segmented buttons for most, a dropdown for a few — and the segmented rows ran to their labels' width, so the right-hand column never lined up. settings_choice now renders a dropdown; an off-preset value shows as a checked "Custom (N)" row. The SSH strip's forward form keeps the segmented control: it lives outside the settings window and has no popover state to use.
This commit is contained in:
@@ -97,6 +97,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- **Every pick-one setting is a dropdown.** Settings drew a single choice two
|
||||
ways — a row of segmented buttons for most, a dropdown for the language, the
|
||||
window backdrop and the rest — with nothing to say which got which, and the
|
||||
segmented rows ran to whatever width their labels made, so the right-hand
|
||||
column never lined up. All of them are dropdowns now, one width. A value set
|
||||
by hand off the presets (scrollback, the notification threshold) shows as a
|
||||
checked "Custom (N)" at the foot of the menu, as the extra segment did.
|
||||
|
||||
- **The mouse wheel no longer zooms the font by default.** ⌘ (Ctrl elsewhere)
|
||||
plus the wheel used to resize the font, and ⌘ is held for so much else that
|
||||
the text jumped size mid-scroll. `mouse_zoom_modifier` now defaults to
|
||||
|
||||
@@ -100,8 +100,9 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
搜索框 28px 浅底(圆角 7);导航行 28px、圆角 7、左右内边距 8,当前项为 selected 阶 + medium,不用强调色。
|
||||
页标题立在 48px 标题栏带内;分组标题 28px、11.5/16rem medium 辅助色;分组之间是 0.5px 分隔线,上下各 16px。
|
||||
设置行:标签正文色 regular,描述 12/16rem 辅助色,行最低 28px、内边距 8(hover/搜索命中的底色外扩 8px);
|
||||
搜索命中行用浅中性底,不用强调色。输入框与下拉框 28px,按钮、分段控件、步进器 26px(圆角 6);
|
||||
输入框/下拉框/分段轨道/次要按钮都用同一浅底(surface 半阶)且无描边,分段当前格为 selected 阶 + medium。
|
||||
搜索命中行用浅中性底,不用强调色。输入框与下拉框 28px,按钮、步进器 26px(圆角 6);
|
||||
输入框/下拉框/次要按钮都用同一浅底(surface 半阶)且无描边。控件一义一种:开关管开/关,下拉管单选
|
||||
(不再用分段控件),滑块管连续值,步进器管整数微调。
|
||||
每个视图唯一的主操作(保存主题草稿、连接、安装更新)用反色中性,同 Git 提交按钮;不可执行时回到浅底。
|
||||
开关与滑块仍用强调色。SSH 主机列表:标题行带 26px 图标按钮,28px 浅底搜索,分组标题 22px,
|
||||
主机行 42px 两行(标题正文、地址 11.5/16rem 辅助色);主机表单字段名为辅助色右对齐标签列。
|
||||
|
||||
+14
-14
@@ -93,7 +93,7 @@ impl Tty7App {
|
||||
|secs| format!("{secs}s"),
|
||||
);
|
||||
|
||||
let startup = self.segmented(
|
||||
let startup = self.settings_choice(
|
||||
"wt-startup",
|
||||
&[
|
||||
t(L10nKey::SettingsStartupNormal),
|
||||
@@ -126,7 +126,7 @@ impl Tty7App {
|
||||
let tray = self.settings_switch("wt-tray-icon", show_tray_icon, cx, |this, on, _, cx| {
|
||||
this.set_show_tray_icon(on, cx)
|
||||
});
|
||||
let notify = self.segmented(
|
||||
let notify = self.settings_choice(
|
||||
"wt-notify",
|
||||
&[
|
||||
t(L10nKey::NotifyModeNever),
|
||||
@@ -144,7 +144,7 @@ impl Tty7App {
|
||||
this.set_notify_mode(mode, cx);
|
||||
},
|
||||
);
|
||||
let threshold = self.segmented_valued(
|
||||
let threshold = self.settings_choice_valued(
|
||||
"wt-notify-threshold",
|
||||
&NOTIFY_THRESHOLD_LABELS,
|
||||
threshold_sel,
|
||||
@@ -400,7 +400,7 @@ impl Tty7App {
|
||||
CursorStyle::Bar => 1,
|
||||
CursorStyle::Underline => 2,
|
||||
};
|
||||
let cursor_style_control = self.segmented(
|
||||
let cursor_style_control = self.settings_choice(
|
||||
"cursor-style",
|
||||
&[
|
||||
t(L10nKey::CursorShapeBlock),
|
||||
@@ -425,7 +425,7 @@ impl Tty7App {
|
||||
let prompt_cursor_labels = prompt_cursor_shape_labels();
|
||||
let prompt_cursor_labels: Vec<&str> =
|
||||
prompt_cursor_labels.iter().map(String::as_str).collect();
|
||||
let prompt_cursor_style_control = self.segmented(
|
||||
let prompt_cursor_style_control = self.settings_choice(
|
||||
"prompt-cursor-style",
|
||||
&prompt_cursor_labels,
|
||||
prompt_cursor_idx,
|
||||
@@ -800,7 +800,7 @@ impl Tty7App {
|
||||
WdStrategy::Home => 1,
|
||||
WdStrategy::Custom => 2,
|
||||
};
|
||||
let wd_radio = self.segmented(
|
||||
let wd_radio = self.settings_choice(
|
||||
"wd-strategy",
|
||||
&[
|
||||
t(L10nKey::SettingsWdInherit),
|
||||
@@ -909,7 +909,7 @@ impl Tty7App {
|
||||
cx,
|
||||
|this, on, _, cx| this.set_ssh_loopback_forward(on, cx),
|
||||
);
|
||||
let link_file_open_radio = self.segmented(
|
||||
let link_file_open_radio = self.settings_choice(
|
||||
"term-link-file-open",
|
||||
&[
|
||||
t(L10nKey::SettingsOpenFilesInternal),
|
||||
@@ -953,7 +953,7 @@ impl Tty7App {
|
||||
)
|
||||
.into_any_element()
|
||||
});
|
||||
let scrollback = self.segmented_valued(
|
||||
let scrollback = self.settings_choice_valued(
|
||||
"term-scrollback",
|
||||
&SCROLLBACK_LABELS,
|
||||
scrollback_sel,
|
||||
@@ -973,7 +973,7 @@ impl Tty7App {
|
||||
BellMode::Audible => 2,
|
||||
BellMode::Both => 3,
|
||||
};
|
||||
let bell_control = self.segmented(
|
||||
let bell_control = self.settings_choice(
|
||||
"term-bell",
|
||||
&[
|
||||
t(L10nKey::SettingsBellModeOff),
|
||||
@@ -1230,7 +1230,7 @@ impl Tty7App {
|
||||
(MouseZoomModifier::None, true) => 3,
|
||||
(MouseZoomModifier::None, false) => 2,
|
||||
};
|
||||
let zoom = self.segmented(
|
||||
let zoom = self.settings_choice(
|
||||
"term-mouse-zoom",
|
||||
&zoom_labels,
|
||||
zoom_idx,
|
||||
@@ -1374,7 +1374,7 @@ impl Tty7App {
|
||||
SshTabTitle::ProfileName => 1,
|
||||
SshTabTitle::Hostname => 2,
|
||||
};
|
||||
let new_tab = self.segmented(
|
||||
let new_tab = self.settings_choice(
|
||||
"wt-new-tab-pos",
|
||||
&[t(L10nKey::SettingsAfterCurrent), t(L10nKey::SettingsAtEnd)],
|
||||
new_tab_idx,
|
||||
@@ -1390,7 +1390,7 @@ impl Tty7App {
|
||||
);
|
||||
},
|
||||
);
|
||||
let tab_bar = self.segmented(
|
||||
let tab_bar = self.settings_choice(
|
||||
"wt-tab-bar-pos",
|
||||
&[t(L10nKey::SettingsTop), t(L10nKey::SettingsLeft)],
|
||||
tab_bar_idx,
|
||||
@@ -1412,7 +1412,7 @@ impl Tty7App {
|
||||
cx,
|
||||
|this, on, _, cx| this.set_sidebar_auto_grouping(on, cx),
|
||||
);
|
||||
let ssh_tab_title = self.segmented(
|
||||
let ssh_tab_title = self.settings_choice(
|
||||
"wt-ssh-tab-title",
|
||||
&[
|
||||
t(L10nKey::SettingsSshTabTitleDynamic),
|
||||
@@ -1719,7 +1719,7 @@ impl Tty7App {
|
||||
UpdateChannel::Stable => 0,
|
||||
UpdateChannel::Nightly => 1,
|
||||
};
|
||||
let channel = self.segmented(
|
||||
let channel = self.settings_choice(
|
||||
"wt-update-channel",
|
||||
&[
|
||||
t(L10nKey::SettingsUpdateChannelStable),
|
||||
|
||||
+78
-53
@@ -595,6 +595,76 @@ impl Tty7App {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// A single choice from a fixed list: a dropdown, the one control every
|
||||
/// pick-one row on the page uses.
|
||||
pub(crate) fn settings_choice(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
options: &[&str],
|
||||
selected: usize,
|
||||
cx: &mut Context<Self>,
|
||||
on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context<Self>) + 'static,
|
||||
) -> AnyElement {
|
||||
self.settings_choice_full(id, options, Some(selected), None, cx, on_pick)
|
||||
}
|
||||
|
||||
/// A choice over a fixed set of values, used where the config accepts
|
||||
/// anything in a range. When the live value matches a bucket exactly that
|
||||
/// bucket is checked; when it does not, a trailing "Custom (N)" row carries
|
||||
/// the check and the trigger's label instead of the nearest bucket getting
|
||||
/// a label it does not have (#550).
|
||||
///
|
||||
/// The custom row writes nothing — there is no bucket value behind it — so
|
||||
/// picking it only closes the menu.
|
||||
pub(crate) fn settings_choice_valued(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
options: &[&str],
|
||||
selected: Option<usize>,
|
||||
custom_label: Option<String>,
|
||||
cx: &mut Context<Self>,
|
||||
on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context<Self>) + 'static,
|
||||
) -> AnyElement {
|
||||
self.settings_choice_full(id, options, selected, custom_label, cx, on_pick)
|
||||
}
|
||||
|
||||
fn settings_choice_full(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
options: &[&str],
|
||||
selected: Option<usize>,
|
||||
custom_label: Option<String>,
|
||||
cx: &mut Context<Self>,
|
||||
on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context<Self>) + 'static,
|
||||
) -> AnyElement {
|
||||
let on_pick = Rc::new(on_pick);
|
||||
let label: SharedString = selected
|
||||
.and_then(|i| options.get(i))
|
||||
.map(|l| SharedString::from(l.to_string()))
|
||||
.or_else(|| custom_label.clone().map(SharedString::from))
|
||||
.unwrap_or_default();
|
||||
let mut entries: Vec<MenuEntry> = options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| {
|
||||
let on_pick = on_pick.clone();
|
||||
MenuEntry::item(
|
||||
l.to_string(),
|
||||
selected == Some(i),
|
||||
move |this, window, cx| on_pick(this, i, window, cx),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if let Some(custom) = custom_label.filter(|_| selected.is_none()) {
|
||||
entries.push(MenuEntry::Separator);
|
||||
entries.push(MenuEntry::item(custom, true, |_, _, _| {}));
|
||||
}
|
||||
self.settings_dropdown(id, label, None, entries, cx)
|
||||
}
|
||||
|
||||
/// A segmented control. Settings rows pick with [`Self::settings_choice`];
|
||||
/// this is for forms outside the settings window — the SSH strip's forward
|
||||
/// form — which have no settings popover to hang a menu from.
|
||||
pub(crate) fn segmented(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
@@ -602,48 +672,10 @@ impl Tty7App {
|
||||
selected: usize,
|
||||
cx: &mut Context<Self>,
|
||||
on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context<Self>) + 'static,
|
||||
) -> AnyElement {
|
||||
self.segmented_full(id, options, Some(selected), None, cx, on_pick)
|
||||
}
|
||||
|
||||
/// A segmented control over a fixed set of values, used where the config
|
||||
/// accepts anything in a range. When the live value matches a bucket
|
||||
/// exactly that bucket is highlighted; when it does not, a trailing
|
||||
/// "Custom (N)" cell carries the highlight instead of the nearest bucket
|
||||
/// getting a label it does not have (#550).
|
||||
///
|
||||
/// The custom cell is not a button — there is no bucket value behind it to
|
||||
/// write — so it takes neither a click handler nor a pointer cursor.
|
||||
pub(crate) fn segmented_valued(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
options: &[&str],
|
||||
selected: Option<usize>,
|
||||
custom_label: Option<String>,
|
||||
cx: &mut Context<Self>,
|
||||
on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context<Self>) + 'static,
|
||||
) -> AnyElement {
|
||||
self.segmented_full(id, options, selected, custom_label, cx, on_pick)
|
||||
}
|
||||
|
||||
fn segmented_full(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
options: &[&str],
|
||||
selected: Option<usize>,
|
||||
custom_label: Option<String>,
|
||||
cx: &mut Context<Self>,
|
||||
on_pick: impl Fn(&mut Self, usize, &mut Window, &mut Context<Self>) + 'static,
|
||||
) -> AnyElement {
|
||||
let tk = Tk::of(cx);
|
||||
let id: SharedString = id.into();
|
||||
let on_pick = Rc::new(on_pick);
|
||||
let cells: Vec<(String, Option<usize>)> = options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, l)| (l.to_string(), Some(i)))
|
||||
.chain(custom_label.map(|l| (l, None)))
|
||||
.collect();
|
||||
h_flex()
|
||||
.id(ElementId::Name(id.clone()))
|
||||
.flex_shrink_0()
|
||||
@@ -651,12 +683,10 @@ impl Tty7App {
|
||||
.gap(px(2.))
|
||||
.rounded(px(7.))
|
||||
.bg(tk.k05)
|
||||
.children(cells.into_iter().enumerate().map(|(i, (label, bucket))| {
|
||||
// A bucket is highlighted only on an exact match, and the
|
||||
// custom cell (`bucket == None`) exactly when no bucket was.
|
||||
let active = bucket == selected;
|
||||
.children(options.iter().enumerate().map(|(i, label)| {
|
||||
let active = i == selected;
|
||||
let on_pick = on_pick.clone();
|
||||
let cell = h_flex()
|
||||
h_flex()
|
||||
.id(ElementId::NamedInteger(id.clone(), i as u64))
|
||||
.items_center()
|
||||
.justify_center()
|
||||
@@ -671,16 +701,11 @@ impl Tty7App {
|
||||
.when(!active, |s| {
|
||||
s.text_color(tk.k5).hover(move |h| h.text_color(tk.fg))
|
||||
})
|
||||
.child(label);
|
||||
match bucket {
|
||||
Some(ix) => {
|
||||
cell.cursor_pointer()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
on_pick(this, ix, window, cx);
|
||||
}))
|
||||
}
|
||||
None => cell,
|
||||
}
|
||||
.child(label.to_string())
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
on_pick(this, i, window, cx);
|
||||
}))
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl Tty7App {
|
||||
|
||||
// The toolbar: the filter, a way back from every change at once, and
|
||||
// the preset the whole table starts from.
|
||||
let presets = self.segmented(
|
||||
let presets = self.settings_choice(
|
||||
"kb-preset",
|
||||
&[t(L10nKey::SettingsDefault), "tmux"],
|
||||
usize::from(tmux),
|
||||
@@ -91,7 +91,7 @@ impl Tty7App {
|
||||
.text_color(tk.k5)
|
||||
.child(t(L10nKey::SettingsPrefix)),
|
||||
)
|
||||
.child(self.segmented(
|
||||
.child(self.settings_choice(
|
||||
"kb-prefix",
|
||||
&["⌃B", "⌃A"],
|
||||
usize::from(prefix == "ctrl-a"),
|
||||
|
||||
Reference in New Issue
Block a user