feat(client/windows): pinning moves to the tile menu, and the chrome quiets down
Pin/unpin lives in the tile's "…" menu now (review decision, reversing the editor-owns-it rule): one flat prefix-matched entry per profile — "Pin tile: X" / "Unpin tile: X" — beside Copy link and Create shortcut, the other tile-shaped actions. The write is paired with a new `hosts_rev` bump (the hosts-page mirror of `settings_rev`), so the pinned tile appears — or vanishes — in the same gesture instead of on the next discovery tick. UIA-verified: menu -> Pin tile: Work -> the pin is in the store and the second tile is on the grid immediately. The rest is the review list: * Both sheets (host editor, profile) put their content in a scroll_view — a window shorter than the card scrolls it instead of clipping the bottom controls. * The home header keeps ONE labelled action: Add host, in accent. Console/Shortcuts/ Settings drop to icons with tooltips — four written-out buttons read as four competing calls to action, and icon-only removes the compact-width special case too. * The profile switcher becomes one combined element: combo + pencil (icon only, the label is gone) inside a shared control-look wrapper — stock 4-epx radius, CardStroke outline, the pencil a borderless segment. In the defaults scope the bare combo stands alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,10 @@ const MENU_EDIT: &str = "Edit\u{2026}";
|
|||||||
const MENU_WITH: &str = "Connect with: ";
|
const MENU_WITH: &str = "Connect with: ";
|
||||||
const MENU_COPY_LINK: &str = "Copy link";
|
const MENU_COPY_LINK: &str = "Copy link";
|
||||||
const MENU_SHORTCUT: &str = "Create shortcut\u{2026}";
|
const MENU_SHORTCUT: &str = "Create shortcut\u{2026}";
|
||||||
|
/// Pin/unpin a profile's one-click tile for this host — dynamic per profile, prefix-matched
|
||||||
|
/// like [`MENU_WITH`] (same flat-flyout constraint, same collision-proof trailing space).
|
||||||
|
const MENU_PIN: &str = "Pin tile: ";
|
||||||
|
const MENU_UNPIN: &str = "Unpin tile: ";
|
||||||
const MENU_FORGET: &str = "Forget\u{2026}";
|
const MENU_FORGET: &str = "Forget\u{2026}";
|
||||||
|
|
||||||
/// Whether the console (gamepad) UI is available in this build: the session binary ships
|
/// Whether the console (gamepad) UI is available in this build: the session binary ships
|
||||||
@@ -71,10 +75,15 @@ pub(crate) struct HostsProps {
|
|||||||
/// The hovered tile's stable id (saved: fp_hex, discovered: `addr:port`) — root state because
|
/// The hovered tile's stable id (saved: fp_hex, discovered: `addr:port`) — root state because
|
||||||
/// the pointer enter/exit handlers bypass the reconciler flush, like the flyout clicks above.
|
/// the pointer enter/exit handlers bypass the reconciler flush, like the flyout clicks above.
|
||||||
pub(crate) hover: Option<String>,
|
pub(crate) hover: Option<String>,
|
||||||
|
/// Bumped when a menu action changes what the page should SHOW without changing any
|
||||||
|
/// state it already reads — pinning/unpinning a profile tile, which rewrites the
|
||||||
|
/// known-hosts store behind the tiles (the hosts-page mirror of `settings_rev`).
|
||||||
|
pub(crate) hosts_rev: u64,
|
||||||
pub(crate) set_forget: AsyncSetState<Option<(String, String)>>,
|
pub(crate) set_forget: AsyncSetState<Option<(String, String)>>,
|
||||||
pub(crate) set_rename: AsyncSetState<Option<(String, String)>>,
|
pub(crate) set_rename: AsyncSetState<Option<(String, String)>>,
|
||||||
pub(crate) set_show_add: AsyncSetState<bool>,
|
pub(crate) set_show_add: AsyncSetState<bool>,
|
||||||
pub(crate) set_hover: AsyncSetState<Option<String>>,
|
pub(crate) set_hover: AsyncSetState<Option<String>>,
|
||||||
|
pub(crate) set_hosts_rev: AsyncSetState<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for HostsProps {
|
impl PartialEq for HostsProps {
|
||||||
@@ -90,6 +99,7 @@ impl PartialEq for HostsProps {
|
|||||||
&& self.show_add == other.show_add
|
&& self.show_add == other.show_add
|
||||||
&& self.add_anim == other.add_anim
|
&& self.add_anim == other.add_anim
|
||||||
&& self.hover == other.hover
|
&& self.hover == other.hover
|
||||||
|
&& self.hosts_rev == other.hosts_rev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,58 +372,6 @@ fn edit_editor(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
// Pinned tiles: which profiles get their own one-click tile for this host. They used to be
|
|
||||||
// two more flat entries in the tile's flyout, which is what tipped that menu over — and this
|
|
||||||
// is where they belong anyway, beside the default they sit next to (design §5.2a). Each
|
|
||||||
// switch writes immediately, like the profile picker above and unlike the text fields, which
|
|
||||||
// need a Save because they have drafts.
|
|
||||||
let pin_switches: Element = {
|
|
||||||
let catalog = pf_client_core::profiles::ProfilesFile::load();
|
|
||||||
let stored = KnownHosts::load()
|
|
||||||
.hosts
|
|
||||||
.iter()
|
|
||||||
.find(|h| h.fp_hex == fp)
|
|
||||||
.cloned();
|
|
||||||
if catalog.profiles.is_empty() {
|
|
||||||
vstack(Vec::<Element>::new()).into()
|
|
||||||
} else {
|
|
||||||
let mut rows: Vec<Element> = vec![text_block("Pinned tiles")
|
|
||||||
.font_size(12.0)
|
|
||||||
.foreground(ThemeRef::SecondaryText)
|
|
||||||
.horizontal_alignment(HorizontalAlignment::Left)
|
|
||||||
.into()];
|
|
||||||
for p in &catalog.profiles {
|
|
||||||
let on = stored
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|h| h.pinned_profiles.iter().any(|id| id == &p.id));
|
|
||||||
let (fp, id) = (fp.to_string(), p.id.clone());
|
|
||||||
rows.push(
|
|
||||||
ToggleSwitch::new(on)
|
|
||||||
.header(&p.name)
|
|
||||||
.on_content("Shown")
|
|
||||||
.off_content("Hidden")
|
|
||||||
.on_toggled(move |v: bool| {
|
|
||||||
tracing::info!(pin = %id, host = %fp, on = v, "pin toggle");
|
|
||||||
let mut known = KnownHosts::load();
|
|
||||||
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
|
||||||
h.pinned_profiles.retain(|x| x != &id);
|
|
||||||
if v {
|
|
||||||
h.pinned_profiles.push(id.clone());
|
|
||||||
}
|
|
||||||
if let Err(e) = known.save() {
|
|
||||||
tracing::warn!(error = %format!("{e:#}"), "saving a pin");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::warn!(host = %fp, "pin toggle: no such saved host");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
vstack(rows).spacing(6.0).into()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let field = |label: &str, value: String, placeholder: &str, draft: HookRef<String>| {
|
let field = |label: &str, value: String, placeholder: &str, draft: HookRef<String>| {
|
||||||
vstack((
|
vstack((
|
||||||
text_block(label)
|
text_block(label)
|
||||||
@@ -434,12 +392,11 @@ fn edit_editor(
|
|||||||
*clip_draft.borrow(),
|
*clip_draft.borrow(),
|
||||||
);
|
);
|
||||||
// A centred SHEET (scrim + card), not an in-grid tile: as a tile the editor inherited a
|
// A centred SHEET (scrim + card), not an in-grid tile: as a tile the editor inherited a
|
||||||
// grid cell in the middle of the page, and on an ordinary window its lower half — the
|
// grid cell in the middle of the page, and on an ordinary window its lower half sat
|
||||||
// pin switches especially — sat below the fold with nothing hinting at it ("can't pin
|
// below the fold with nothing hinting at it (live-diagnosed 2026-07-29: a control's
|
||||||
// hosts", live-diagnosed 2026-07-29: the switch's visible rect was a 9-px sliver). A
|
// visible rect was a 9-px sliver). A sheet centres at its own height — and its content
|
||||||
// sheet centres at its own height, scrolls internally when it must, and matches where
|
// sits in a scroll_view, so a short window scrolls the card instead of clipping it.
|
||||||
// every other edit flow lives.
|
let modal = dialog_surface(scroll_view(
|
||||||
let modal = dialog_surface(
|
|
||||||
vstack((
|
vstack((
|
||||||
text_block(format!("Edit \u{201c}{initial_name}\u{201d}"))
|
text_block(format!("Edit \u{201c}{initial_name}\u{201d}"))
|
||||||
.font_size(20.0)
|
.font_size(20.0)
|
||||||
@@ -465,7 +422,6 @@ fn edit_editor(
|
|||||||
.horizontal_alignment(HorizontalAlignment::Left),
|
.horizontal_alignment(HorizontalAlignment::Left),
|
||||||
))
|
))
|
||||||
.spacing(4.0),
|
.spacing(4.0),
|
||||||
pin_switches,
|
|
||||||
vstack((
|
vstack((
|
||||||
ToggleSwitch::new(clip0)
|
ToggleSwitch::new(clip0)
|
||||||
.header("Share clipboard with this host")
|
.header("Share clipboard with this host")
|
||||||
@@ -495,7 +451,7 @@ fn edit_editor(
|
|||||||
.horizontal_alignment(HorizontalAlignment::Right),
|
.horizontal_alignment(HorizontalAlignment::Right),
|
||||||
))
|
))
|
||||||
.spacing(10.0),
|
.spacing(10.0),
|
||||||
)
|
))
|
||||||
.max_width(460.0)
|
.max_width(460.0)
|
||||||
.horizontal_alignment(HorizontalAlignment::Center)
|
.horizontal_alignment(HorizontalAlignment::Center)
|
||||||
.vertical_alignment(VerticalAlignment::Center)
|
.vertical_alignment(VerticalAlignment::Center)
|
||||||
@@ -585,20 +541,14 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
let window = cx.use_inner_size();
|
let window = cx.use_inner_size();
|
||||||
let content_w = (window.width - 64.0).clamp(TILE_MIN_WIDTH, 1120.0);
|
let content_w = (window.width - 64.0).clamp(TILE_MIN_WIDTH, 1120.0);
|
||||||
let cols = (((content_w + TILE_GAP) / (TILE_MIN_WIDTH + TILE_GAP)).floor() as usize).max(1);
|
let cols = (((content_w + TILE_GAP) / (TILE_MIN_WIDTH + TILE_GAP)).floor() as usize).max(1);
|
||||||
// Compact header: below this the three labelled buttons would collide with the title
|
|
||||||
// (the Auto grid column can't shrink), so they drop to icon-only with tooltips.
|
|
||||||
let compact = window.width < 700.0;
|
|
||||||
let header_btn = |label: &str, sym: Symbol| {
|
|
||||||
if compact {
|
|
||||||
button("").icon(sym).tooltip(label).automation_name(label)
|
|
||||||
} else {
|
|
||||||
button(label).icon(sym)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut body: Vec<Element> = Vec::new();
|
let mut body: Vec<Element> = Vec::new();
|
||||||
|
|
||||||
// Header: title block + Add host / Help / Settings.
|
// Header: title block + the page actions. ONE labelled primary — Add host, in accent —
|
||||||
|
// and the rest icon-only with tooltips: four written-out buttons in a row read as four
|
||||||
|
// competing calls to action (review feedback), and icon-only needs no compact-width
|
||||||
|
// special case either.
|
||||||
|
let icon_btn =
|
||||||
|
|label: &str, sym: Symbol| button("").icon(sym).tooltip(label).automation_name(label);
|
||||||
body.push(
|
body.push(
|
||||||
grid((
|
grid((
|
||||||
vstack((
|
vstack((
|
||||||
@@ -611,7 +561,8 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.grid_column(0)
|
.grid_column(0)
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
.vertical_alignment(VerticalAlignment::Center),
|
||||||
hstack({
|
hstack({
|
||||||
let mut actions: Vec<Element> = vec![header_btn("Add host", Symbol::Add)
|
let mut actions: Vec<Element> = vec![button("Add host")
|
||||||
|
.icon(Symbol::Add)
|
||||||
.accent()
|
.accent()
|
||||||
.on_click({
|
.on_click({
|
||||||
let sa = set_show_add.clone();
|
let sa = set_show_add.clone();
|
||||||
@@ -622,14 +573,12 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
// where the session binary ships without its Skia console.
|
// where the session binary ships without its Skia console.
|
||||||
if CONSOLE_UI_AVAILABLE {
|
if CONSOLE_UI_AVAILABLE {
|
||||||
actions.push(
|
actions.push(
|
||||||
header_btn("Console UI", Symbol::Play)
|
icon_btn(
|
||||||
.tooltip(
|
"Console UI \u{2014} the controller-driven couch interface",
|
||||||
"The controller-driven couch interface \u{2014} host list, \
|
Symbol::Play,
|
||||||
pairing and libraries, launching streams in the same window.",
|
|
||||||
)
|
)
|
||||||
.on_click({
|
.on_click({
|
||||||
let (c, ss, st) =
|
let (c, ss, st) = (ctx.clone(), set_screen.clone(), set_status.clone());
|
||||||
(ctx.clone(), set_screen.clone(), set_status.clone());
|
|
||||||
// No target: the console opens its OWN host view rather than
|
// No target: the console opens its OWN host view rather than
|
||||||
// one host's library — the couch counterpart of this page.
|
// one host's library — the couch counterpart of this page.
|
||||||
move || open_console(&c, None, &ss, &st)
|
move || open_console(&c, None, &ss, &st)
|
||||||
@@ -638,7 +587,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
actions.push(
|
actions.push(
|
||||||
header_btn("Shortcuts", Symbol::Keyboard)
|
icon_btn("Keyboard shortcuts", Symbol::Keyboard)
|
||||||
.on_click({
|
.on_click({
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
move || ss.call(Screen::Help)
|
move || ss.call(Screen::Help)
|
||||||
@@ -646,7 +595,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
actions.push(
|
actions.push(
|
||||||
header_btn("Settings", Symbol::Setting)
|
icon_btn("Settings", Symbol::Setting)
|
||||||
.on_click({
|
.on_click({
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
move || ss.call(Screen::Settings)
|
move || ss.call(Screen::Settings)
|
||||||
@@ -723,6 +672,8 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
||||||
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
||||||
let menu_profiles = profiles.clone();
|
let menu_profiles = profiles.clone();
|
||||||
|
let pinned_now = k.pinned_profiles.clone();
|
||||||
|
let (hosts_rev, set_hosts_rev) = (props.hosts_rev, props.set_hosts_rev.clone());
|
||||||
let (link_host, link_profile) = (k.clone(), None::<String>);
|
let (link_host, link_profile) = (k.clone(), None::<String>);
|
||||||
let shortcut_host = k.clone();
|
let shortcut_host = k.clone();
|
||||||
button("")
|
button("")
|
||||||
@@ -764,6 +715,15 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
items.push(menu_separator());
|
items.push(menu_separator());
|
||||||
items.push(menu_item(MENU_COPY_LINK));
|
items.push(menu_item(MENU_COPY_LINK));
|
||||||
items.push(menu_item(MENU_SHORTCUT));
|
items.push(menu_item(MENU_SHORTCUT));
|
||||||
|
// Pin/unpin a profile's one-click tile, beside the other tile-shaped
|
||||||
|
// shortcuts (the review moved these here from the editor).
|
||||||
|
for (id, name, _) in profiles.iter() {
|
||||||
|
let pinned = pinned_now.iter().any(|x| x == id);
|
||||||
|
items.push(menu_item(format!(
|
||||||
|
"{}{name}",
|
||||||
|
if pinned { MENU_UNPIN } else { MENU_PIN }
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
items.push(menu_separator());
|
items.push(menu_separator());
|
||||||
items.push(menu_item(MENU_EDIT));
|
items.push(menu_item(MENU_EDIT));
|
||||||
@@ -773,6 +733,32 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.on_item_clicked(move |item: String| match item.as_str() {
|
.on_item_clicked(move |item: String| match item.as_str() {
|
||||||
// The profile items are dynamic, so they are matched by prefix before
|
// The profile items are dynamic, so they are matched by prefix before
|
||||||
// the fixed ones.
|
// the fixed ones.
|
||||||
|
_ if item.starts_with(MENU_PIN) || item.starts_with(MENU_UNPIN) => {
|
||||||
|
let (on, name) = if let Some(n) = item.strip_prefix(MENU_PIN) {
|
||||||
|
(true, n)
|
||||||
|
} else {
|
||||||
|
(false, item.trim_start_matches(MENU_UNPIN))
|
||||||
|
};
|
||||||
|
let Some((id, ..)) = menu_profiles.iter().find(|(_, n, _)| n == name)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
tracing::info!(pin = %id, host = %fp, on, "pin toggle");
|
||||||
|
let mut known = KnownHosts::load();
|
||||||
|
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
||||||
|
h.pinned_profiles.retain(|x| x != id);
|
||||||
|
if on {
|
||||||
|
h.pinned_profiles.push(id.clone());
|
||||||
|
}
|
||||||
|
if let Err(e) = known.save() {
|
||||||
|
tracing::warn!(error = %format!("{e:#}"), "saving a pin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The store changed behind the tiles and nothing the page reads
|
||||||
|
// as state did — the bump is what makes the pinned tile appear
|
||||||
|
// (or vanish) NOW, not on the next discovery tick.
|
||||||
|
set_hosts_rev.call(hosts_rev + 1);
|
||||||
|
}
|
||||||
_ if item.starts_with(MENU_WITH) => {
|
_ if item.starts_with(MENU_WITH) => {
|
||||||
let name = item.trim_start_matches(MENU_WITH);
|
let name = item.trim_start_matches(MENU_WITH);
|
||||||
let mut target = target.clone();
|
let mut target = target.clone();
|
||||||
|
|||||||
@@ -298,6 +298,10 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
|||||||
// its marker as immediately as resetting one clears it), a reset, a profile colour change.
|
// its marker as immediately as resetting one clears it), a reset, a profile colour change.
|
||||||
// Root state comparison makes same-value calls free, so a counter is what forces the pass.
|
// Root state comparison makes same-value calls free, so a counter is what forces the pass.
|
||||||
let (settings_rev, set_settings_rev) = cx.use_async_state(0u64);
|
let (settings_rev, set_settings_rev) = cx.use_async_state(0u64);
|
||||||
|
// The hosts page's mirror of the same idea: pin/unpin from a tile's menu rewrites the
|
||||||
|
// known-hosts store behind the tiles, and the bump is what makes the pinned tile appear
|
||||||
|
// in the same gesture instead of on the next discovery tick.
|
||||||
|
let (hosts_rev, set_hosts_rev) = cx.use_async_state(0u64);
|
||||||
// `punktfunk://` links: the receiver thread queues them (from this launch's argv, or from a
|
// `punktfunk://` links: the receiver thread queues them (from this launch's argv, or from a
|
||||||
// later instance over WM_COPYDATA) and this poll pulls them onto the UI thread. Thread-fed
|
// later instance over WM_COPYDATA) and this poll pulls them onto the UI thread. Thread-fed
|
||||||
// state must be root state, like the pad count below.
|
// state must be root state, like the pad count below.
|
||||||
@@ -620,10 +624,12 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
|
|||||||
show_add,
|
show_add,
|
||||||
add_anim,
|
add_anim,
|
||||||
hover,
|
hover,
|
||||||
|
hosts_rev,
|
||||||
set_forget,
|
set_forget,
|
||||||
set_rename,
|
set_rename,
|
||||||
set_show_add,
|
set_show_add,
|
||||||
set_hover,
|
set_hover,
|
||||||
|
set_hosts_rev: set_hosts_rev.clone(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// connecting_page / request_access_page / waking_page / settings_page / licenses_page /
|
// connecting_page / request_access_page / waking_page / settings_page / licenses_page /
|
||||||
|
|||||||
@@ -332,7 +332,9 @@ fn edit_profile_modal(
|
|||||||
.margin(edges(0.0, 6.0, 0.0, 0.0))
|
.margin(edges(0.0, 6.0, 0.0, 0.0))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
let modal = dialog_surface(vstack(rows).spacing(12.0))
|
// The content scrolls when the window is shorter than the sheet (same rule as the host
|
||||||
|
// editor) — a sheet must never clip its own controls.
|
||||||
|
let modal = dialog_surface(scroll_view(vstack(rows).spacing(12.0)))
|
||||||
.max_width(420.0)
|
.max_width(420.0)
|
||||||
.horizontal_alignment(HorizontalAlignment::Center)
|
.horizontal_alignment(HorizontalAlignment::Center)
|
||||||
.vertical_alignment(VerticalAlignment::Center)
|
.vertical_alignment(VerticalAlignment::Center)
|
||||||
@@ -1336,21 +1338,41 @@ pub(crate) fn settings_page(
|
|||||||
// Every piece is vertically CENTRED against the combo (the tallest control), and
|
// Every piece is vertically CENTRED against the combo (the tallest control), and
|
||||||
// the row shares the content column's 24-left / 28-right page margins, so the bar
|
// the row shares the content column's 24-left / 28-right page margins, so the bar
|
||||||
// reads as part of the page grid rather than floating chrome.
|
// reads as part of the page grid rather than floating chrome.
|
||||||
let mut row: Vec<Element> = vec![
|
// The switcher is ONE combined element (review feedback): the combo and the pencil
|
||||||
text_block("Editing")
|
// share a control-look wrapper — the stock 4-epx control radius and a CardStroke
|
||||||
|
// outline — with the pencil as a borderless icon segment. Only a Border can carry a
|
||||||
|
// corner radius in this toolkit, so the wrapper supplies the joint chrome. In the
|
||||||
|
// defaults scope there is nothing to edit and the bare combo stands alone.
|
||||||
|
let switcher_key = format!("{scope}\u{1}{}", scope_names.join("\u{1}"));
|
||||||
|
let combined: Element = if profile_mode {
|
||||||
|
let set_edit = set_edit.clone();
|
||||||
|
border(
|
||||||
|
hstack(vec![
|
||||||
|
Element::from(make_switcher()),
|
||||||
|
button("")
|
||||||
|
.icon(Symbol::Edit)
|
||||||
|
.subtle()
|
||||||
|
.height(36.0)
|
||||||
|
.width(40.0)
|
||||||
|
.tooltip("Edit profile\u{2026}")
|
||||||
|
.automation_name("Edit profile\u{2026}")
|
||||||
|
.on_click(move || set_edit.call(true))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(0.0),
|
||||||
|
)
|
||||||
|
.corner_radius(4.0)
|
||||||
|
.border_brush(ThemeRef::CardStroke)
|
||||||
|
.border_thickness(uniform(1.0))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
make_switcher().into()
|
||||||
|
};
|
||||||
|
let mut row: Vec<Element> = vec![text_block("Editing")
|
||||||
.font_size(13.0)
|
.font_size(13.0)
|
||||||
.foreground(ThemeRef::SecondaryText)
|
.foreground(ThemeRef::SecondaryText)
|
||||||
.vertical_alignment(VerticalAlignment::Center)
|
.vertical_alignment(VerticalAlignment::Center)
|
||||||
.into(),
|
.into()];
|
||||||
// Keyed by scope + the name list: a rename/create/delete changes the ComboBox's
|
|
||||||
// items, and an in-place diff re-sets items (clearing WinUI's selection) while
|
|
||||||
// skipping `selected_index` when it compares equal — the combo then renders
|
|
||||||
// blank. A remount applies every prop; the hstack's child list takes the keyed
|
|
||||||
// path (a panel).
|
|
||||||
Element::from(make_switcher())
|
|
||||||
.with_key(format!("{scope}\u{1}{}", scope_names.join("\u{1}")))
|
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
|
||||||
];
|
|
||||||
// The profile's colour, right where the choice is made (a ComboBox item is a plain
|
// The profile's colour, right where the choice is made (a ComboBox item is a plain
|
||||||
// string in this toolkit, so the chip cannot ride inside the dropdown).
|
// string in this toolkit, so the chip cannot ride inside the dropdown).
|
||||||
if let Some(c) = active
|
if let Some(c) = active
|
||||||
@@ -1368,18 +1390,15 @@ pub(crate) fn settings_page(
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if profile_mode {
|
// Keyed by scope + the name list: a rename/create/delete changes the ComboBox's
|
||||||
let set_edit = set_edit.clone();
|
// items, and an in-place diff re-sets items (clearing WinUI's selection) while
|
||||||
|
// skipping `selected_index` when it compares equal — the combo then renders blank.
|
||||||
|
// A remount applies every prop; the hstack's child list takes the keyed path.
|
||||||
row.push(
|
row.push(
|
||||||
button("Edit profile\u{2026}")
|
combined
|
||||||
.icon(Symbol::Edit)
|
.with_key(switcher_key)
|
||||||
// Matches the combo — the bar's controls share one height.
|
.vertical_alignment(VerticalAlignment::Center),
|
||||||
.height(36.0)
|
|
||||||
.on_click(move || set_edit.call(true))
|
|
||||||
.vertical_alignment(VerticalAlignment::Center)
|
|
||||||
.into(),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
hstack(row)
|
hstack(row)
|
||||||
.spacing(12.0)
|
.spacing(12.0)
|
||||||
.margin(edges(24.0, 12.0, 28.0, 8.0))
|
.margin(edges(24.0, 12.0, 28.0, 8.0))
|
||||||
|
|||||||
Reference in New Issue
Block a user