Add complete keyboard navigation.

This commit is contained in:
2026-08-07 17:00:49 +02:00
parent 87a161058f
commit 5709c80209
38 changed files with 1943 additions and 355 deletions

View File

@@ -238,6 +238,7 @@ pub enum Message {
// Settings
SetOfflineMode(bool),
SetUiLocale(UiLocale),
KeyboardNavigation(crate::components::keyboard::Navigation),
ToggleLocaleDropdown,
ToggleProjectDropdown,
@@ -1457,6 +1458,10 @@ impl BdsApp {
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::KeyboardNavigation(navigation) => match navigation {
crate::components::keyboard::Navigation::Next => iced::widget::focus_next(),
crate::components::keyboard::Navigation::Previous => iced::widget::focus_previous(),
},
Message::WindowCloseRequested => {
self.persist_project_ui_state();
flush_embeddings_and_exit(std::process::exit)
@@ -3869,7 +3874,11 @@ impl BdsApp {
) && (action != MenuAction::DisconnectServer || self.remote_client.is_some())
});
native_edit::native_edit(content, Arc::clone(&self.native_edit_commands)).into()
crate::components::keyboard::scope(native_edit::native_edit(
content,
Arc::clone(&self.native_edit_commands),
))
.into()
}
pub fn subscription(&self) -> Subscription<Message> {
@@ -3894,6 +3903,15 @@ impl BdsApp {
_ => None,
});
let window_close_sub = window::close_requests().map(|_| Message::WindowCloseRequested);
let keyboard_navigation_sub = iced::event::listen_with(|event, status, _id| {
let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { key, modifiers, .. }) =
event
else {
return None;
};
crate::components::keyboard::tab_navigation(&key, modifiers, status)
.map(Message::KeyboardNavigation)
});
// Global mouse tracking for sidebar resize dragging.
// The 4px drag handle mouse_area only fires on_press; move/release
@@ -3950,6 +3968,7 @@ impl BdsApp {
toast_tick,
file_drop_sub,
window_close_sub,
keyboard_navigation_sub,
drag_sub,
menu_interaction_sub,
menu_expand_tick,

View File

@@ -5,6 +5,8 @@ use iced::widget::{
};
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector};
use super::keyboard;
/// Standard form field label color.
pub const LABEL_COLOR: Color = rgb8(0xB5, 0xBA, 0xC4);
pub const SECTION_COLOR: Color = rgb8(0x9D, 0xA5, 0xB4);
@@ -243,10 +245,13 @@ where
.size(12)
.color(LABEL_COLOR)
.shaping(Shaping::Advanced),
pick_list(list, selected.cloned(), on_select)
.padding([8, 10])
.width(Length::Fill)
.style(select_style),
keyboard::focusable(
pick_list(list, selected.cloned(), on_select)
.padding([8, 10])
.width(Length::Fill)
.style(select_style),
true,
),
]
.spacing(6)
.width(Length::Fill)
@@ -259,11 +264,14 @@ pub fn labeled_checkbox<'a, Message: Clone + 'a>(
is_checked: bool,
on_toggle: impl Fn(bool) -> Message + 'a,
) -> Element<'a, Message> {
checkbox(label, is_checked)
.on_toggle(on_toggle)
.size(16)
.text_size(14)
.into()
keyboard::focusable(
checkbox(label, is_checked)
.on_toggle(on_toggle)
.size(16)
.text_size(14),
true,
)
.into()
}
/// A section header with optional separator line.

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
pub mod inputs;
pub mod keyboard;
pub mod native_edit;
pub mod popover;
pub mod webview;

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use iced::widget::{
Column, button, column, container, horizontal_space, mouse_area, row, scrollable, text,
Column, column, container, horizontal_space, mouse_area, row, scrollable, text,
};
use iced::{
Alignment, Background, Border, Color, Element, Length, Shadow, Subscription, Theme, Vector,
@@ -10,7 +10,7 @@ use muda::accelerator::{Accelerator, CMD_OR_CTRL, Code, Modifiers};
use muda::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu};
use crate::app::Message;
use crate::components::{inputs, popover};
use crate::components::{inputs, keyboard, popover};
use crate::state::tabs::TabType;
use bds_core::i18n::{UiLocale, translate};
@@ -640,9 +640,12 @@ fn window_menu_popup<'a>(
]
.align_y(Alignment::Center)
.spacing(16);
let mut item = button(content).padding([6, 8]).width(Length::Fill).style(
move |theme, status| menu_item_style(selected == Some(action), theme, status),
);
let mut item = keyboard::button(content)
.padding([6, 8])
.width(Length::Fill)
.style(move |theme, status| {
menu_item_style(selected == Some(action), theme, status)
});
if is_enabled {
item = item.on_press(Message::WindowMenu(WindowMenuEvent::Action(action)));
}
@@ -692,7 +695,7 @@ pub fn window_menu_view<'a>(
label
};
let trigger = mouse_area(
button(text(label).size(12))
keyboard::button(text(label).size(12))
.padding([5, 8])
.style(move |theme, status| menu_button_style(active, theme, status))
.on_press(Message::WindowMenu(WindowMenuEvent::Toggle(

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping;
use iced::widget::{Column, Space, button, column, container, svg, text, tooltip};
use iced::widget::{Column, Space, column, container, svg, text, tooltip};
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -28,6 +30,21 @@ fn icon_svg(view: SidebarView) -> &'static [u8] {
}
}
pub fn access_key(view: SidebarView) -> char {
match view {
SidebarView::Posts => '1',
SidebarView::Pages => '2',
SidebarView::Media => '3',
SidebarView::Scripts => '4',
SidebarView::Templates => '5',
SidebarView::Tags => '6',
SidebarView::Chat => '7',
SidebarView::Import => '8',
SidebarView::Git => '9',
SidebarView::Settings => '0',
}
}
/// Top group of activity items.
const TOP_ACTIVITIES: &[SidebarView] = &[
SidebarView::Posts,
@@ -108,7 +125,7 @@ pub fn view(
.height(Length::Fixed(24.0))
.opacity(if is_active { 1.0_f32 } else { 0.4_f32 });
let btn = button(
let btn = iced::widget::button(
container(icon)
.center_x(Length::Fixed(48.0))
.center_y(Length::Fixed(48.0)),
@@ -140,13 +157,18 @@ pub fn view(
// Wrap in tooltip per layout.allium ActivityButton.label_key
let tip_text = t(locale, view.i18n_key());
tooltip(
btn_row,
text(tip_text).size(12).shaping(Shaping::Advanced),
tooltip::Position::Right,
keyboard::focusable(
tooltip(
btn_row,
text(tip_text).size(12).shaping(Shaping::Advanced),
tooltip::Position::Right,
)
.gap(4)
.style(inputs::tooltip_style),
true,
)
.gap(4)
.style(inputs::tooltip_style)
.hotkey(access_key(view))
.hint_position(keyboard::HintPosition::BottomTrailing)
.into()
};

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::{HashMap, HashSet};
use std::f32::consts::{FRAC_PI_2, TAU};
@@ -7,9 +8,7 @@ use bds_core::engine::chat_surfaces::{
};
use bds_core::i18n::UiLocale;
use iced::widget::canvas::{self, Path, Stroke, path};
use iced::widget::{
Space, button, checkbox, column, container, row, scrollable, text, text_editor,
};
use iced::widget::{Space, checkbox, column, container, row, scrollable, text, text_editor};
use iced::{
Alignment, Color, Element, Length, Point, Radians, Rectangle, Renderer, Size, Theme, mouse,
};
@@ -76,7 +75,7 @@ fn surface_view<'a>(
}
if dismissible {
header = header.push(
button(text(t(locale, "chat.surface.dismiss")))
keyboard::button(text(t(locale, "chat.surface.dismiss")))
.on_press(Message::ChatSurfaceDismissed(surface.id.clone()))
.padding([4, 8])
.style(inputs::secondary_button),
@@ -113,7 +112,7 @@ fn surface_content<'a>(
.iter()
.fold(row![].spacing(8), |actions, item| {
actions.push(
button(text(item.label.clone()))
keyboard::button(text(item.label.clone()))
.on_press(Message::ChatSurfaceAction {
surface_id: surface.id.clone(),
action: item.action.clone(),
@@ -196,15 +195,18 @@ fn form<'a>(
let surface_id = surface.id.clone();
let key = field.key.clone();
let control: Element<'a, Message> = match field.input_type {
FormInputType::Checkbox => checkbox(label, field.value.as_bool().unwrap_or(false))
.on_toggle(move |value| Message::ChatSurfaceFieldChanged {
surface_id: surface_id.clone(),
field: key.clone(),
value: value.into(),
})
.size(16)
.text_size(13)
.into(),
FormInputType::Checkbox => keyboard::focusable(
checkbox(label, field.value.as_bool().unwrap_or(false))
.on_toggle(move |value| Message::ChatSurfaceFieldChanged {
surface_id: surface_id.clone(),
field: key.clone(),
value: value.into(),
})
.size(16)
.text_size(13),
true,
)
.into(),
FormInputType::Select => {
let selected = field.options.iter().find(|option| {
field
@@ -278,7 +280,7 @@ fn form<'a>(
}
if let Some(action) = &surface.submit_action {
children.push(
button(text(
keyboard::button(text(
surface
.submit_label
.clone()
@@ -437,7 +439,7 @@ fn tabs<'a>(
.enumerate()
.fold(row![].spacing(6), |controls, (index, tab)| {
controls.push(
button(text(tab.label.clone()))
keyboard::button(text(tab.label.clone()))
.on_press(Message::ChatSurfaceTabSelected {
surface_id: surface.id.clone(),
index,

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::HashMap;
use std::time::Instant;
@@ -8,7 +9,7 @@ use bds_core::i18n::UiLocale;
use bds_core::model::{ChatConversation, ChatMessage, ChatRole};
use iced::widget::text::Shaping;
use iced::widget::{
Space, button, column, container, markdown, row, scrollable, text, text_editor, text_input,
Space, column, container, markdown, row, scrollable, text, text_editor, text_input,
};
use iced::{Alignment, Color, Element, Length};
@@ -212,7 +213,7 @@ pub fn view<'a>(
text(t(locale, "chat.unavailable.guidance"))
.size(13)
.color(inputs::SECTION_COLOR),
button(text(t(locale, "chat.unavailable.openSettings")))
keyboard::button(text(t(locale, "chat.unavailable.openSettings")))
.on_press(Message::OpenSettingsSection(
crate::views::settings_view::SettingsSection::AI,
))
@@ -254,11 +255,11 @@ pub fn view<'a>(
.size(18)
.padding([7, 9])
.style(inputs::field_style),
button(text(t(locale, "chat.rename.action")))
keyboard::button(text(t(locale, "chat.rename.action")))
.on_press(Message::ChatRename)
.padding([8, 12])
.style(inputs::secondary_button),
button(text(t(locale, "common.delete")))
keyboard::button(text(t(locale, "common.delete")))
.on_press(Message::ChatDelete(state.conversation.id.clone()))
.padding([8, 12])
.style(inputs::danger_button),
@@ -349,13 +350,14 @@ pub fn view<'a>(
} else {
t(locale, "chat.send")
};
let mut send_button = button(text(send_label))
.padding([8, 16])
.style(if state.streaming {
inputs::danger_button
} else {
inputs::primary_button
});
let mut send_button =
keyboard::button(text(send_label))
.padding([8, 16])
.style(if state.streaming {
inputs::danger_button
} else {
inputs::primary_button
});
if state.streaming {
send_button = send_button.on_press(Message::ChatCancel);
} else if !state.input.text().trim().is_empty() {

View File

@@ -1,4 +1,5 @@
use iced::widget::{Space, button, column, container, row, scrollable, text, tooltip};
use crate::components::keyboard;
use iced::widget::{Space, column, container, row, scrollable, text, tooltip};
use iced::{Alignment, Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -339,7 +340,7 @@ fn recent_posts<'a>(posts: &'a [DashboardRecentPost], locale: UiLocale) -> Eleme
.map(|post| {
container(
row![
button(
keyboard::button(
column![
text(post.title.clone()).size(14).color(Color::WHITE),
text(post.date.clone())
@@ -357,7 +358,7 @@ fn recent_posts<'a>(posts: &'a [DashboardRecentPost], locale: UiLocale) -> Eleme
}))
.width(Length::Fill),
status_badge(&post.status),
button(text(t(locale, "dashboard.pin")).size(12))
keyboard::button(text(t(locale, "dashboard.pin")).size(12))
.on_press(Message::OpenTab(Tab {
id: post.post_id.clone(),
tab_type: TabType::Post,

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::fs;
use std::hash::{Hash, Hasher};
@@ -5,7 +6,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, UNIX_EPOCH};
use bds_core::i18n::UiLocale;
use iced::widget::{button, column, container, markdown, row, scrollable, text};
use iced::widget::{column, container, markdown, row, scrollable, text};
use iced::{Element, Length};
use crate::app::Message;
@@ -170,7 +171,7 @@ pub fn view(state: &DocumentationState, locale: UiLocale) -> Element<'_, Message
.into(),
],
vec![
button(text(t(locale, "common.refresh")).size(13))
keyboard::button(text(t(locale, "common.refresh")).size(13))
.on_press(Message::DocumentationRefresh(state.kind))
.padding([6, 16])
.style(inputs::secondary_button)

View File

@@ -1,9 +1,10 @@
use crate::components::keyboard;
use std::collections::HashSet;
use bds_core::engine::embedding::DuplicateSearchResult;
use bds_core::i18n::UiLocale;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, checkbox, column, container, row, scrollable, text};
use iced::widget::{Space, checkbox, column, container, row, scrollable, text};
use iced::{Color, Element, Length};
use crate::app::Message;
@@ -23,19 +24,20 @@ pub struct DuplicatesState {
pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
let refresh = if state.is_loading {
button(text(t(locale, "duplicates.searching")).size(13)).style(inputs::secondary_button)
keyboard::button(text(t(locale, "duplicates.searching")).size(13))
.style(inputs::secondary_button)
} else {
button(text(t(locale, "common.refresh")).size(13))
keyboard::button(text(t(locale, "common.refresh")).size(13))
.on_press(Message::DuplicatesRefresh)
.style(inputs::secondary_button)
}
.padding([6, 16]);
let dismiss_checked = if state.selected.is_empty() || state.is_loading {
button(text(t(locale, "duplicates.dismissChecked")).size(13))
keyboard::button(text(t(locale, "duplicates.dismissChecked")).size(13))
.style(inputs::secondary_button)
} else {
button(
keyboard::button(
text(tw(
locale,
"duplicates.dismissCheckedCount",
@@ -64,12 +66,12 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
.into(),
],
vec![
button(text(t(locale, "duplicates.checkAll")).size(13))
keyboard::button(text(t(locale, "duplicates.checkAll")).size(13))
.on_press(Message::DuplicatesCheckAll)
.padding([6, 12])
.style(inputs::secondary_button)
.into(),
button(text(t(locale, "duplicates.uncheckAll")).size(13))
keyboard::button(text(t(locale, "duplicates.uncheckAll")).size(13))
.on_press(Message::DuplicatesUncheckAll)
.padding([6, 12])
.style(inputs::secondary_button)
@@ -119,14 +121,17 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
};
pairs = pairs.push(inputs::card(
row![
checkbox("", checked)
.on_toggle({
let a = pair.post_id_a.clone();
let b = pair.post_id_b.clone();
move |_| Message::DuplicatesToggle(a.clone(), b.clone())
})
.size(16),
button(
keyboard::focusable(
checkbox("", checked)
.on_toggle({
let a = pair.post_id_a.clone();
let b = pair.post_id_b.clone();
move |_| Message::DuplicatesToggle(a.clone(), b.clone())
})
.size(16),
true,
),
keyboard::button(
text(pair.title_a.clone())
.size(13)
.shaping(Shaping::Advanced)
@@ -135,7 +140,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
.padding([5, 8])
.style(inputs::disclosure_button),
text("").size(14).color(inputs::LABEL_COLOR),
button(
keyboard::button(
text(pair.title_b.clone())
.size(13)
.shaping(Shaping::Advanced)
@@ -149,7 +154,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
} else {
Color::from_rgb(0.55, 0.76, 0.92)
}),
button(text(t(locale, "duplicates.dismiss")).size(12))
keyboard::button(text(t(locale, "duplicates.dismiss")).size(12))
.on_press(Message::DuplicatesDismiss(
pair.post_id_a.clone(),
pair.post_id_b.clone()
@@ -163,7 +168,7 @@ pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
}
if state.result.has_more {
pairs = pairs.push(
button(text(t(locale, "duplicates.showMore")).size(13))
keyboard::button(text(t(locale, "duplicates.showMore")).size(13))
.on_press(Message::DuplicatesShowMore)
.padding([7, 16])
.style(inputs::secondary_button),

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::sync::{Arc, Mutex};
use bds_core::engine::git::{
@@ -6,7 +7,7 @@ use bds_core::engine::git::{
};
use bds_core::i18n::UiLocale;
use iced::widget::text::{Shaping, Wrapping};
use iced::widget::{Space, button, column, container, row, scrollable, text, text_input, tooltip};
use iced::widget::{Space, column, container, row, scrollable, text, text_input, tooltip};
use iced::{Alignment, Background, Color, Element, Font, Length};
use crate::app::Message;
@@ -152,7 +153,7 @@ pub fn sidebar_view(
text(t(locale, "git.notRepository"))
.size(12)
.color(Color::from_rgb(0.6, 0.6, 0.65)),
button(text(t(locale, "git.initialize")).size(12))
keyboard::button(text(t(locale, "git.initialize")).size(12))
.on_press(Message::GitInitialize)
.padding([5, 8])
.style(inputs::primary_button),
@@ -180,7 +181,7 @@ pub fn sidebar_view(
let network_running = state.network_run.is_some();
let network_button =
|key: &'static str, icon: &'static str, message: Message| -> Element<'static, Message> {
let mut control = button(text(icon).size(16).shaping(Shaping::Advanced))
let mut control = keyboard::button(text(icon).size(16).shaping(Shaping::Advanced))
.width(Length::Fixed(30.0))
.height(Length::Fixed(28.0))
.padding(0)
@@ -189,13 +190,16 @@ pub fn sidebar_view(
control = control.on_press(message);
}
tooltip(
control,
text(t(locale, key)).size(12),
tooltip::Position::Bottom,
keyboard::focusable(
tooltip(
control,
text(t(locale, key)).size(12),
tooltip::Position::Bottom,
)
.gap(4)
.style(inputs::tooltip_style),
!offline_mode && !network_running,
)
.gap(4)
.style(inputs::tooltip_style)
.into()
};
let actions = row![
@@ -240,7 +244,7 @@ pub fn sidebar_view(
.padding([5, 7])
.style(inputs::field_style),
{
let commit = button(text(t(locale, "git.commit")).size(11))
let commit = keyboard::button(text(t(locale, "git.commit")).size(11))
.padding([5, 7])
.style(inputs::primary_button);
if state.files.is_empty() || state.commit_message.trim().is_empty() {
@@ -265,7 +269,7 @@ pub fn sidebar_view(
content.extend(state.history.iter().take(20).map(history_button));
}
content.push(
button(text(t(locale, "git.pruneLfs")).size(11))
keyboard::button(text(t(locale, "git.pruneLfs")).size(11))
.on_press(Message::GitPruneLfs)
.padding([4, 7])
.style(inputs::secondary_button)
@@ -303,7 +307,7 @@ fn error_view(error: Option<&str>) -> Element<'static, Message> {
fn status_button(file: &GitFileStatus) -> Element<'static, Message> {
let path = file.path.clone();
button(
keyboard::button(
row![
text(path.clone()).size(11),
Space::with_width(Length::Fill),
@@ -322,7 +326,7 @@ fn history_button(commit: &GitCommit) -> Element<'static, Message> {
let hash = commit.hash.clone();
let subject = commit.subject.clone().unwrap_or_else(|| hash.clone());
let short = hash.chars().take(7).collect::<String>();
button(
keyboard::button(
column![
text(subject.clone()).size(11),
row![
@@ -399,7 +403,7 @@ fn network_output(run: &GitNetworkRunState, locale: UiLocale) -> Element<'static
.wrapping(Wrapping::Word)
)
.padding(6),
button(text(t(locale, "common.cancel")).size(11))
keyboard::button(text(t(locale, "common.cancel")).size(11))
.on_press(Message::CancelTask(
crate::state::navigation::TaskSource::Local,
run.task_id,
@@ -445,7 +449,7 @@ pub fn diff_view(
.iter()
.map(|change| {
let selected = state.selected_path.as_deref() == Some(&change.path);
let button = button(text(change.path.clone()).size(11))
let button = keyboard::button(text(change.path.clone()).size(11))
.padding([4, 7])
.style(if selected {
inputs::primary_button

View File

@@ -1,3 +1,4 @@
use crate::components::keyboard;
use std::collections::HashSet;
use bds_core::i18n::UiLocale;
@@ -5,7 +6,7 @@ use bds_core::model::{
ImportCandidate, ImportDefinition, ImportExecutionResult, ImportItemKind, ImportItemStatus,
ImportPhase, ImportProgress, ImportReport, ImportResolution, TaxonomyKind,
};
use iced::widget::{Space, button, column, container, progress_bar, row, scrollable, text};
use iced::widget::{Space, column, container, progress_bar, row, scrollable, text};
use iced::{Alignment, Color, Element, Length};
use crate::app::Message;
@@ -50,6 +51,25 @@ impl ImportEditorState {
error: None,
}
}
pub fn section_is_visible(&self, section: ImportSection) -> bool {
let Some(report) = &self.report else {
return false;
};
match section {
ImportSection::Conflicts => report
.posts
.iter()
.chain(&report.pages)
.chain(&report.media)
.any(|item| item.status == ImportItemStatus::Conflict),
ImportSection::Posts => !report.posts.is_empty(),
ImportSection::Pages => !report.pages.is_empty(),
ImportSection::Media => !report.media.is_empty(),
ImportSection::Taxonomy => !report.taxonomies.is_empty(),
ImportSection::Macros => !report.macros.is_empty(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -62,6 +82,32 @@ pub enum ImportSection {
Macros,
}
impl ImportSection {
pub fn access_key(self) -> char {
match self {
Self::Conflicts => 'c',
Self::Posts => 'o',
Self::Pages => 'a',
Self::Media => 'm',
Self::Taxonomy => 't',
Self::Macros => 'r',
}
}
pub fn from_access_key(key: char) -> Option<Self> {
[
Self::Conflicts,
Self::Posts,
Self::Pages,
Self::Media,
Self::Taxonomy,
Self::Macros,
]
.into_iter()
.find(|section| section.access_key() == key)
}
}
#[derive(Debug, Clone)]
pub enum ImportEditorMsg {
NameChanged(String),
@@ -120,13 +166,13 @@ pub fn view<'a>(state: &'a ImportEditorState, locale: UiLocale) -> Element<'a, M
column![
row![
name,
button(text(t(locale, "modal.confirmDelete.delete")))
keyboard::button(text(t(locale, "modal.confirmDelete.delete")))
.on_press_maybe(
(!busy).then_some(Message::ImportEditor(ImportEditorMsg::DeleteRequested,))
)
.padding([8, 12])
.style(inputs::danger_button),
button(text(t(locale, "import.analyze")))
keyboard::button(text(t(locale, "import.analyze")))
.on_press_maybe(
(!busy && state.definition.wxr_file_path.is_some())
.then_some(Message::ImportEditor(ImportEditorMsg::Analyze),)
@@ -290,7 +336,7 @@ fn path_row<'a>(
]
.spacing(5)
.width(Length::Fill),
button(text(t(locale, "common.open")))
keyboard::button(text(t(locale, "common.open")))
.on_press(Message::ImportEditor(action))
.padding([7, 12])
.style(inputs::secondary_button),
@@ -457,7 +503,7 @@ fn execute_toolbar<'a>(
.into(),
],
vec![
button(text(t(locale, "import.autoMap")))
keyboard::button(text(t(locale, "import.autoMap")))
.on_press_maybe(
(!state.is_analyzing && !state.is_executing)
.then_some(Message::ImportEditor(ImportEditorMsg::AutoMapTaxonomy)),
@@ -465,7 +511,7 @@ fn execute_toolbar<'a>(
.padding([8, 12])
.style(inputs::secondary_button)
.into(),
button(text(tw(
keyboard::button(text(tw(
locale,
"import.execute",
&[("count", &count.to_string())],
@@ -514,7 +560,7 @@ fn section<'a>(
) -> Vec<Element<'a, Message>> {
let expanded = state.expanded.contains(&section);
let header = inputs::card(
button(
keyboard::button(
row![
text(if expanded { "" } else { "" }).size(12),
text(t(locale, title_key)).size(13),
@@ -524,6 +570,7 @@ fn section<'a>(
.on_press(Message::ImportEditor(ImportEditorMsg::ToggleSection(
section,
)))
.hotkey(section.access_key())
.padding([6, 8])
.width(Length::Fill)
.style(inputs::disclosure_button),
@@ -673,7 +720,7 @@ fn taxonomy_rows<'a>(
}),
))
.width(Length::Fixed(260.0)),
button(text(t(locale, "common.clear")))
keyboard::button(text(t(locale, "common.clear")))
.on_press_maybe(item.mapped_to.is_some().then_some(Message::ImportEditor(
ImportEditorMsg::SetTaxonomyMapping {
kind,

View File

@@ -1,8 +1,9 @@
use crate::components::keyboard;
use std::collections::HashMap;
use std::path::Path;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, image, row, scrollable, text};
use iced::widget::{Space, column, container, image, row, scrollable, text};
use iced::{Color, Element, Length};
use bds_core::i18n::{self, UiLocale};
@@ -271,7 +272,7 @@ pub fn view<'a>(
.style(status_bar::dropdown_bg)
.into();
let quick_actions_button: Element<'a, Message> =
button(text(t(locale, "editor.quickActions")).size(13))
keyboard::button(text(t(locale, "editor.quickActions")).size(13))
.on_press_maybe(
ai_enabled.then_some(Message::MediaEditor(MediaEditorMsg::ToggleQuickActions)),
)
@@ -290,17 +291,17 @@ pub fn view<'a>(
vec![text(state.original_name.clone()).size(18).into()],
vec![
quick_actions,
button(text(t(locale, "editor.replaceFile")).size(13))
keyboard::button(text(t(locale, "editor.replaceFile")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::ReplaceFile))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "common.save")).size(13))
keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::Save))
.style(inputs::primary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::MediaEditor(MediaEditorMsg::Delete))
.style(inputs::danger_button)
.padding([6, 16])
@@ -324,7 +325,7 @@ pub fn view<'a>(
} else {
Color::from_rgb(0.55, 0.58, 0.65)
};
button(text(label).size(12).shaping(Shaping::Advanced).color(color))
keyboard::button(text(label).size(12).shaping(Shaping::Advanced).color(color))
.on_press(Message::MediaEditor(MediaEditorMsg::SwitchLanguage(
flag.language.clone(),
)))
@@ -430,7 +431,7 @@ pub fn view<'a>(
.size(12)
.color(Color::from_rgb(0.55, 0.58, 0.65)),
Space::with_width(Length::Fill),
button(text(t(locale, "editor.linkToPost")).size(12))
keyboard::button(text(t(locale, "editor.linkToPost")).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker))
.style(inputs::secondary_button)
.padding([4, 10]),
@@ -457,7 +458,7 @@ pub fn view<'a>(
.post_picker_results
.iter()
.map(|post| {
button(text(post.title.clone()).size(12))
keyboard::button(text(post.title.clone()).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::LinkPost(
post.post_id.clone(),
)))
@@ -497,13 +498,13 @@ pub fn view<'a>(
.iter()
.map(|post| {
row![
button(text(post.title.clone()).size(12))
keyboard::button(text(post.title.clone()).size(12))
.on_press(Message::MediaEditor(MediaEditorMsg::OpenLinkedPost(
post.post_id.clone()
)))
.padding([4, 0]),
Space::with_width(Length::Fill),
button(text(t(locale, "editor.unlinkMedia")).size(11))
keyboard::button(text(t(locale, "editor.unlinkMedia")).size(11))
.on_press(Message::MediaEditor(MediaEditorMsg::UnlinkPost(
post.post_id.clone()
)))
@@ -569,7 +570,7 @@ fn quick_action_item<'a>(
msg: MediaEditorMsg,
enabled: bool,
) -> Element<'a, Message> {
button(text(label).size(12).shaping(Shaping::Advanced))
keyboard::button(text(label).size(12).shaping(Shaping::Advanced))
.on_press_maybe(enabled.then_some(Message::MediaEditor(msg)))
.padding([6, 12])
.style(status_bar::dropdown_item)

View File

@@ -1,10 +1,12 @@
use crate::components::keyboard;
use iced::widget::button;
use std::collections::HashSet;
use std::time::{Duration, Instant};
use bds_core::engine::menu::{MenuItem, MenuItemKind};
use bds_core::i18n::UiLocale;
use iced::widget::{
Space, button, column, container, mouse_area, row, scrollable, svg, text, text_input, tooltip,
Space, column, container, mouse_area, row, scrollable, svg, text, text_input, tooltip,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Point, Theme};
use uuid::Uuid;
@@ -810,19 +812,22 @@ fn toolbar_icon(
} else {
secondary_button
};
let mut control = button(text(glyph).size(18))
let mut control = keyboard::button(text(glyph).size(18))
.width(Length::Fixed(38.0))
.height(Length::Fixed(34.0))
.style(style);
if enabled {
control = control.on_press(Message::MenuEditor(message));
}
tooltip(
control,
text(t(locale, key)).size(12),
tooltip::Position::Bottom,
keyboard::focusable(
tooltip(
control,
text(t(locale, key)).size(12),
tooltip::Position::Bottom,
)
.gap(4),
enabled,
)
.gap(4)
.into()
}
@@ -856,7 +861,7 @@ fn tree_item<'a>(
let collapsed = state.collapsed.contains(&item.id);
let id = item.id.clone();
let toggle: Element<'_, Message> = if is_submenu {
button(text(if collapsed { "" } else { "" }).size(14))
keyboard::button(text(if collapsed { "" } else { "" }).size(14))
.on_press(Message::MenuEditor(MenuEditorMsg::ToggleExpanded(
id.clone(),
)))
@@ -884,7 +889,7 @@ fn tree_item<'a>(
} else {
item.label.clone()
};
let label_button = button(
let label_button = keyboard::button(
row![
kind_icon(&item.kind),
text(label).size(14),
@@ -1016,7 +1021,7 @@ fn draft_editor<'a>(
.filter(|page| page_matches_query(page, &draft.query))
{
choices = choices.push(
button(text(page.title.clone()))
keyboard::button(text(page.title.clone()))
.on_press(Message::MenuEditor(MenuEditorMsg::ChoosePage(
page.id.clone(),
)))
@@ -1032,7 +1037,7 @@ fn draft_editor<'a>(
.filter(|name| category_matches_query(name, &draft.query))
{
choices = choices.push(
button(text(category.clone()))
keyboard::button(text(category.clone()))
.on_press(Message::MenuEditor(MenuEditorMsg::ChooseCategory(
category.clone(),
)))
@@ -1047,10 +1052,10 @@ fn draft_editor<'a>(
DraftKind::Category => "menuEditor.useCategory",
};
let actions = row![
button(text(t(locale, submit_label)))
keyboard::button(text(t(locale, submit_label)))
.on_press(Message::MenuEditor(MenuEditorMsg::SubmitDraft))
.style(primary_button),
button(text(t(locale, "common.cancel")))
keyboard::button(text(t(locale, "common.cancel")))
.on_press(Message::MenuEditor(MenuEditorMsg::CancelDraft))
.style(secondary_button),
]

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, row, scrollable, text};
use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length};
use bds_core::engine::metadata_diff::{DiffReport, RepairDirection};
@@ -18,10 +19,11 @@ pub struct MetadataDiffState {
}
pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, Message> {
let run = button(text(t(locale, "metadataDiff.run")).size(13))
let run = keyboard::button(text(t(locale, "metadataDiff.run")).size(13))
.on_press_maybe(
(!state.is_running && !state.is_repairing).then_some(Message::RunMetadataDiff),
)
.hotkey('r')
.style(inputs::primary_button)
.padding([6, 16]);
let mut content = column![
@@ -65,7 +67,7 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
)
});
let actions = row![
button(text(t(locale, "metadataDiff.fileToDb")).size(12))
keyboard::button(text(t(locale, "metadataDiff.fileToDb")).size(12))
.on_press_maybe((!state.is_repairing).then_some(
Message::RepairMetadataDiffItem {
index,
@@ -74,7 +76,7 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
))
.style(inputs::secondary_button)
.padding([5, 10]),
button(text(t(locale, "metadataDiff.dbToFile")).size(12))
keyboard::button(text(t(locale, "metadataDiff.dbToFile")).size(12))
.on_press_maybe((!state.is_repairing).then_some(
Message::RepairMetadataDiffItem {
index,
@@ -116,7 +118,7 @@ pub fn view<'a>(state: &'a MetadataDiffState, locale: UiLocale) -> Element<'a, M
.align_y(iced::Alignment::Center);
if orphan.reason == "file_without_db_entry" {
item = item.push(
button(text(t(locale, "metadataDiff.importOrphan")).size(12))
keyboard::button(text(t(locale, "metadataDiff.importOrphan")).size(12))
.on_press_maybe(
(!state.is_repairing)
.then_some(Message::ImportMetadataOrphan(index)),

View File

@@ -1,9 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use std::path::Path;
use iced::widget::text::Shaping;
use iced::widget::{
Space, button, checkbox, column, container, image, row, scrollable, text, text_input,
};
use iced::widget::{Space, checkbox, column, container, image, row, scrollable, text, text_input};
use iced::{Alignment, Background, Border, Color, Element, Length, Shadow, Theme, Vector};
use bds_core::i18n::UiLocale;
@@ -282,7 +282,7 @@ pub fn view(
let selected = selected_project_id.as_deref() == Some(project.id.as_str());
let marker = if selected { "" } else { "" };
project_rows = project_rows.push(
button(
keyboard::button(
row![
text(marker).size(12),
text(project.name.clone()).size(13),
@@ -307,12 +307,12 @@ pub fn view(
content = content.push(project_rows);
}
let cancel = button(text(t(locale, "remoteConnection.cancel")).size(13))
let cancel = keyboard::button(text(t(locale, "remoteConnection.cancel")).size(13))
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style);
let action = if !connected {
let button = button(
let button = keyboard::button(
text(if connecting {
t(locale, "remoteConnection.connecting")
} else {
@@ -328,7 +328,7 @@ pub fn view(
button.on_press(Message::RemoteConnectRequested)
}
} else {
let button = button(text(t(locale, "remoteConnection.open")).size(13))
let button = keyboard::button(text(t(locale, "remoteConnection.open")).size(13))
.padding([6, 16])
.style(confirm_button_style);
if selected_project_id.is_some() {
@@ -393,7 +393,7 @@ pub fn view(
let on_confirm_clone = on_confirm.clone();
let buttons = row![
button(
keyboard::button(
text(t(locale, "modal.confirmDelete.cancel"))
.size(13)
.shaping(Shaping::Advanced)
@@ -402,7 +402,7 @@ pub fn view(
.padding([6, 16])
.style(cancel_button_style),
Space::with_width(Length::Fill),
button(
keyboard::button(
text(t(locale, "modal.confirmDelete.delete"))
.size(13)
.shaping(Shaping::Advanced)
@@ -438,7 +438,7 @@ pub fn view(
let on_confirm_clone = on_confirm.clone();
let buttons = row![
button(
keyboard::button(
text(t(locale, "modal.confirm.cancel"))
.size(13)
.shaping(Shaping::Advanced)
@@ -447,7 +447,7 @@ pub fn view(
.padding([6, 16])
.style(cancel_button_style),
Space::with_width(Length::Fill),
button(
keyboard::button(
text(t(locale, "modal.confirm.confirm"))
.size(13)
.shaping(Shaping::Advanced)
@@ -474,12 +474,12 @@ pub fn view(
ModalState::SearchIndexRepair => {
let buttons = row![
button(text(t(locale, "searchIndexRepair.later")).size(13))
keyboard::button(text(t(locale, "searchIndexRepair.later")).size(13))
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style),
Space::with_width(Length::Fill),
button(text(t(locale, "searchIndexRepair.rebuildNow")).size(13))
keyboard::button(text(t(locale, "searchIndexRepair.rebuildNow")).size(13))
.on_press(Message::ConfirmModal(ConfirmAction::RebuildSearchIndex))
.padding([6, 16])
.style(confirm_button_style),
@@ -520,7 +520,7 @@ pub fn view(
);
}
let mut actions = row![
button(text(t(locale, "find.next")))
keyboard::button(text(t(locale, "find.next")))
.on_press(Message::FindNext)
.style(inputs::primary_button),
]
@@ -528,12 +528,12 @@ pub fn view(
if show_replace {
actions = actions
.push(
button(text(t(locale, "find.replace")))
keyboard::button(text(t(locale, "find.replace")))
.on_press(Message::ReplaceCurrent)
.style(inputs::secondary_button),
)
.push(
button(text(t(locale, "find.replaceAll")))
keyboard::button(text(t(locale, "find.replaceAll")))
.on_press(Message::ReplaceAll)
.style(inputs::secondary_button),
);
@@ -582,7 +582,7 @@ pub fn view(
} else {
content.push(Space::with_height(16.0)).push(row![
Space::with_width(Length::Fill),
button(text(t(locale, "tasks.cancelTask")).size(13))
keyboard::button(text(t(locale, "tasks.cancelTask")).size(13))
.on_press(Message::CancelTask(
crate::state::navigation::TaskSource::Local,
task_id,
@@ -607,7 +607,7 @@ pub fn view(
external_url,
external_text,
} => {
let internal_tab = button(
let internal_tab = keyboard::button(
text(t(locale, "modal.postInsertLink.tabInternal"))
.size(13)
.shaping(Shaping::Advanced)
@@ -623,7 +623,7 @@ pub fn view(
.padding([8, 16])
.style(cancel_button_style);
let external_tab = button(
let external_tab = keyboard::button(
text(t(locale, "modal.postInsertLink.tabExternal"))
.size(13)
.shaping(Shaping::Advanced)
@@ -666,7 +666,7 @@ pub fn view(
let mut column = column![search_input, Space::with_height(12.0)];
for link in results {
column = column.push(
button(
keyboard::button(
row![
column![
text(link.title.clone())
@@ -749,7 +749,7 @@ pub fn view(
Space::with_height(12.0),
row![
Space::with_width(Length::Fill),
button(text(t(locale, "modal.postInsertLink.insert")))
keyboard::button(text(t(locale, "modal.postInsertLink.insert")))
.on_press(Message::PostEditor(
PostEditorMsg::PostInsertLinkExternalInsert
))
@@ -760,7 +760,7 @@ pub fn view(
.spacing(8)
.into();
let create_post_btn: Element<'static, Message> = button(
let create_post_btn: Element<'static, Message> = keyboard::button(
text(t(locale, "modal.postInsertLink.createPost"))
.size(12)
.shaping(Shaping::Advanced)
@@ -792,7 +792,7 @@ pub fn view(
};
let buttons = row![
button(cancel_text)
keyboard::button(cancel_text)
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style),
@@ -903,7 +903,7 @@ pub fn view(
.spacing(4)
.align_x(Alignment::Center);
let btn = button(media_col)
let btn = keyboard::button(media_col)
.on_press(Message::PostEditor(PostEditorMsg::PostInsertMediaSelected(
m.id.clone(),
)))
@@ -963,7 +963,7 @@ pub fn view(
.shaping(Shaping::Advanced);
let buttons = row![
button(cancel_text)
keyboard::button(cancel_text)
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style),
@@ -1045,7 +1045,7 @@ pub fn view(
.spacing(4)
.align_x(Alignment::Center);
let btn = button(media_col)
let btn = keyboard::button(media_col)
.on_press(Message::PostEditor(
PostEditorMsg::PostGalleryImageSelected(index),
))
@@ -1082,7 +1082,7 @@ pub fn view(
.size(13)
.shaping(Shaping::Advanced);
let close_button = button(close_text)
let close_button = keyboard::button(close_text)
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style);
@@ -1106,7 +1106,7 @@ pub fn view(
.width(Length::Fill)
.center_x(Length::Fill),
row![
button(text("<"))
keyboard::button(text("<"))
.on_press(Message::PostEditor(PostEditorMsg::PostGalleryPrevious))
.padding([6, 12])
.style(cancel_button_style),
@@ -1115,12 +1115,12 @@ pub fn view(
.size(12)
.shaping(Shaping::Advanced),
Space::with_width(Length::Fill),
button(text(">"))
keyboard::button(text(">"))
.on_press(Message::PostEditor(PostEditorMsg::PostGalleryNext))
.padding([6, 12])
.style(cancel_button_style),
Space::with_width(12.0),
button(text(t(locale, "modal.postGallery.backToGrid")))
keyboard::button(text(t(locale, "modal.postGallery.backToGrid")))
.on_press(Message::PostEditor(
PostEditorMsg::PostGalleryCloseLightbox
))
@@ -1166,13 +1166,15 @@ pub fn view(
.iter()
.enumerate()
.map(|(index, field)| {
let toggle =
let toggle = keyboard::focusable(
checkbox(field.label.clone(), field.accepted)
.on_toggle_maybe((!field.locked).then_some(move |value| {
Message::ToggleAiSuggestionField(index, value)
}))
.size(16)
.text_size(13);
.text_size(13),
!field.locked,
);
container(
column![
toggle,
@@ -1211,12 +1213,12 @@ pub fn view(
.collect::<Vec<Element<'static, Message>>>();
let buttons = row![
button(text(t(locale, "common.cancel")).size(13))
keyboard::button(text(t(locale, "common.cancel")).size(13))
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style),
Space::with_width(Length::Fill),
button(text(t(locale, "modal.aiSuggestions.applySelected")).size(13))
keyboard::button(text(t(locale, "modal.aiSuggestions.applySelected")).size(13))
.on_press(Message::ApplyAiSuggestions(target, fields))
.padding([6, 16])
.style(confirm_button_style),
@@ -1275,7 +1277,7 @@ pub fn view(
),
};
let status = language.existing_status.clone().unwrap_or_default();
button(
keyboard::button(
row![
text(format!("{} {}", language.flag_emoji, language.name))
.size(13)
@@ -1302,7 +1304,7 @@ pub fn view(
Space::with_height(12.0),
column(rows).spacing(6),
Space::with_height(16.0),
button(text(t(locale, "common.cancel")).size(13))
keyboard::button(text(t(locale, "common.cancel")).size(13))
.on_press(Message::DismissModal)
.padding([6, 16])
.style(cancel_button_style),

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, progress_bar, row, scrollable, text};
use iced::widget::{Space, column, container, progress_bar, row, scrollable, text};
use iced::{Alignment, Background, Border, Color, Element, Font, Length, Theme};
use bds_core::engine::git::GitCommit;
@@ -80,7 +82,7 @@ fn task_row(
rows.push(
row![
Space::with_width(Length::Fill),
button(text(t(locale, "tasks.cancelTask")).size(10))
keyboard::button(text(t(locale, "tasks.cancelTask")).size(10))
.on_press(Message::CancelTask(snapshot.source, snapshot.id))
.padding([3, 8])
.style(inputs::secondary_button),
@@ -267,7 +269,7 @@ pub fn view(
// Tab header — per layout.allium: tasks, output, post_links (only when
// active editor tab is a post), git_log (only when active tab is post or
// media).
let tasks_btn = button(
let tasks_btn = keyboard::button(
text(t(locale, "common.tasks"))
.size(12)
.shaping(Shaping::Advanced),
@@ -280,7 +282,7 @@ pub fn view(
tab_inactive
});
let output_btn = button(
let output_btn = keyboard::button(
text(t(locale, "panel.output"))
.size(12)
.shaping(Shaping::Advanced),
@@ -293,7 +295,7 @@ pub fn view(
tab_inactive
});
let close_btn = button(text("\u{2715}").size(12).shaping(Shaping::Advanced))
let close_btn = keyboard::button(text("\u{2715}").size(12).shaping(Shaping::Advanced))
.on_press(Message::TogglePanel)
.padding([4, 6])
.style(close_btn_style);
@@ -301,7 +303,7 @@ pub fn view(
let mut tab_row: Vec<Element<'static, Message>> = vec![tasks_btn.into(), output_btn.into()];
if active_tab_is_post {
let post_links_btn = button(
let post_links_btn = keyboard::button(
text(t(locale, "panel.postLinks"))
.size(12)
.shaping(Shaping::Advanced),
@@ -317,7 +319,7 @@ pub fn view(
}
if active_tab_is_post_or_media {
let git_log_btn = button(
let git_log_btn = keyboard::button(
text(t(locale, "panel.gitLog"))
.size(12)
.shaping(Shaping::Advanced),
@@ -372,7 +374,7 @@ pub fn view(
let collapsed = collapsed_task_groups.contains(group_id);
let group_name = snapshot.group_name.as_deref().unwrap_or(group_id);
items.push(
button(
keyboard::button(
row![
text(if collapsed { "\u{25b8}" } else { "\u{25be}" }).size(11),
text(format!("{} ({})", group_name, members.len()))
@@ -524,7 +526,7 @@ pub fn view(
let hash = commit.hash.clone();
let subject = commit.subject.clone().unwrap_or_else(|| hash.clone());
let short = hash.chars().take(7).collect::<String>();
button(
keyboard::button(
row![
text(short).size(11).font(iced::Font::MONOSPACE),
text(subject.clone()).size(11),
@@ -560,7 +562,7 @@ pub fn view(
}
fn post_link_button(locale: UiLocale, link: &ResolvedPostLink) -> Element<'static, Message> {
button(text(link.title.clone()).size(11).shaping(Shaping::Advanced))
keyboard::button(text(link.title.clone()).size(11).shaping(Shaping::Advanced))
.on_press(Message::OpenTab(Tab {
id: link.post_id.clone(),
title: if link.title.is_empty() {

View File

@@ -1,10 +1,12 @@
use crate::components::keyboard;
use iced::widget::button;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use iced::widget::text::{Shaping, Wrapping};
use iced::widget::{Column, Space, button, column, container, row, scrollable, text, text_input};
use iced::widget::{Column, Space, column, container, row, scrollable, text, text_input};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::{self, UiLocale};
@@ -489,7 +491,7 @@ pub fn view<'a>(
format!("{}{}", truncate_header_title(&state.title), dirty_indicator)
};
let quick_actions_button: Element<'a, Message> = button(
let quick_actions_button: Element<'a, Message> = keyboard::button(
text(t(locale, "editor.quickActions"))
.size(13)
.shaping(Shaping::Advanced),
@@ -569,7 +571,7 @@ pub fn view<'a>(
let mut header_action_items: Vec<Element<'a, Message>> = vec![
status_badge(locale, &state.status),
quick_actions,
button(
keyboard::button(
text(t(locale, "common.save"))
.size(13)
.shaping(Shaping::Advanced),
@@ -581,7 +583,7 @@ pub fn view<'a>(
];
if state.status == PostStatus::Draft {
header_action_items.push(
button(
keyboard::button(
text(t(locale, "editor.publish"))
.size(13)
.shaping(Shaping::Advanced),
@@ -594,7 +596,7 @@ pub fn view<'a>(
}
if !on_translation && state.status == PostStatus::Draft && state.published_at.is_some() {
header_action_items.push(
button(
keyboard::button(
text(t(locale, "editor.discard"))
.size(13)
.shaping(Shaping::Advanced),
@@ -606,7 +608,7 @@ pub fn view<'a>(
);
}
header_action_items.push(
button(
keyboard::button(
text(t(locale, "modal.confirmDelete.delete"))
.size(13)
.shaping(Shaping::Advanced),
@@ -643,13 +645,14 @@ pub fn view<'a>(
} else {
format!("\u{25B6} {}", t(locale, "editor.metadata"))
};
let meta_toggle = button(
let meta_toggle = keyboard::button(
text(meta_toggle_label)
.size(12)
.color(inputs::SECTION_COLOR)
.shaping(Shaping::Advanced),
)
.on_press(Message::PostEditor(PostEditorMsg::ToggleMetadata))
.hotkey('m')
.padding([8, 10])
.width(Length::Fill)
.style(inputs::disclosure_button);
@@ -663,7 +666,7 @@ pub fn view<'a>(
for flag in &flags {
let lang = flag.language.clone();
let label = flag.flag_emoji.to_string();
let btn = button(text(label).size(14).shaping(Shaping::Advanced))
let btn = keyboard::button(text(label).size(14).shaping(Shaping::Advanced))
.on_press(Message::PostEditor(PostEditorMsg::SwitchLanguage(lang)))
.padding([2, 4])
.style(if flag.is_active {
@@ -756,7 +759,7 @@ pub fn view<'a>(
.align_y(iced::Alignment::Center);
for tag in semantic_suggestions {
chips = chips.push(
button(text(format!("+ {tag}")).size(11))
keyboard::button(text(format!("+ {tag}")).size(11))
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(
tag.to_string(),
)))
@@ -784,7 +787,7 @@ pub fn view<'a>(
.align_y(iced::Alignment::Center);
for tag in matching_suggestions {
chips = chips.push(
button(text(tag).size(11))
keyboard::button(text(tag).size(11))
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(
tag.to_string(),
)))
@@ -795,10 +798,12 @@ pub fn view<'a>(
if query_addable {
let query = state.tags_input.trim().to_string();
chips = chips.push(
button(text(tw(locale, "editor.createTag", &[("name", &query)])).size(11))
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(query)))
.padding([4, 8])
.style(inputs::secondary_button),
keyboard::button(
text(tw(locale, "editor.createTag", &[("name", &query)])).size(11),
)
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(query)))
.padding([4, 8])
.style(inputs::secondary_button),
);
}
chips.wrap().into()
@@ -860,7 +865,7 @@ pub fn view<'a>(
Column::with_children(items).spacing(2).into()
};
let link_existing_button: Element<'a, Message> = button(
let link_existing_button: Element<'a, Message> = keyboard::button(
text(t(locale, "editor.linkExistingMedia"))
.size(11)
.shaping(Shaping::Advanced),
@@ -916,14 +921,14 @@ pub fn view<'a>(
]
.spacing(2)
.width(Length::Fill),
button(
keyboard::button(
text(t(locale, "common.open"))
.size(11)
.shaping(Shaping::Advanced)
)
.on_press(Message::PostEditor(PostEditorMsg::OpenLinkedMedia(open_id)))
.padding([4, 10]),
button(
keyboard::button(
text(t(locale, "editor.unlinkMedia"))
.size(11)
.shaping(Shaping::Advanced)
@@ -981,13 +986,14 @@ pub fn view<'a>(
} else {
format!("\u{25B6} {}", t(locale, "editor.excerpt"))
};
let excerpt_toggle = button(
let excerpt_toggle = keyboard::button(
text(excerpt_toggle_label)
.size(12)
.color(inputs::SECTION_COLOR)
.shaping(Shaping::Advanced),
)
.on_press(Message::PostEditor(PostEditorMsg::ToggleExcerpt))
.hotkey('x')
.padding([8, 10])
.width(Length::Fill)
.style(inputs::disclosure_button);
@@ -1036,7 +1042,7 @@ pub fn view<'a>(
],
vec![
if show_content_actions {
button(
keyboard::button(
text(t(locale, "editor.insertLink"))
.size(13)
.shaping(Shaping::Advanced),
@@ -1049,7 +1055,7 @@ pub fn view<'a>(
Space::new(0, 0).into()
},
if show_content_actions {
button(
keyboard::button(
text(t(locale, "editor.insertMedia"))
.size(13)
.shaping(Shaping::Advanced),
@@ -1062,7 +1068,7 @@ pub fn view<'a>(
Space::new(0, 0).into()
},
if show_content_actions {
button(
keyboard::button(
text(t(locale, "editor.gallery"))
.size(13)
.shaping(Shaping::Advanced),
@@ -1209,7 +1215,7 @@ fn chip_input_field<'a>(
.map(|chip| {
let label = format!("{} \u{2715}", chip);
let chip_val = chip.clone();
button(text(label).size(11).shaping(Shaping::Advanced))
keyboard::button(text(label).size(11).shaping(Shaping::Advanced))
.on_press(on_remove(chip_val))
.padding([2, 6])
.style(chip_button_style)
@@ -1266,7 +1272,7 @@ fn mode_button<'a>(
"preview" => t(locale, "editor.modePreview"),
_ => t(locale, "editor.modeMarkdown"),
};
button(text(label).size(12).shaping(Shaping::Advanced))
keyboard::button(text(label).size(12).shaping(Shaping::Advanced))
.on_press(message)
.padding([4, 10])
.style(if active_mode == mode {
@@ -1284,7 +1290,7 @@ fn quick_action_item<'a>(
enabled: bool,
) -> Element<'a, Message> {
let _ = locale;
button(text(label).size(12).shaping(Shaping::Advanced))
keyboard::button(text(label).size(12).shaping(Shaping::Advanced))
.on_press_maybe(enabled.then_some(Message::PostEditor(msg)))
.padding([6, 12])
.style(status_bar::dropdown_item)

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping;
use iced::widget::{Column, Space, button, container, row, svg, text};
use iced::widget::{Column, Space, container, row, svg, text};
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -148,7 +150,7 @@ pub fn view(
};
items.push(
button(label)
keyboard::button(label)
.on_press(Message::SwitchProject(id))
.padding([4, 8])
.width(Length::Fill)
@@ -170,7 +172,7 @@ pub fn view(
// Open and create project actions
items.push(
button(
keyboard::button(
row![
text("")
.size(14)
@@ -190,7 +192,7 @@ pub fn view(
);
items.push(
button(
keyboard::button(
row![
text("+")
.size(14)
@@ -256,7 +258,7 @@ pub fn trigger_button(project_name: &str) -> Element<'static, Message> {
.shaping(Shaping::Advanced)
.color(Color::from_rgb(0.55, 0.55, 0.60));
button(
keyboard::button(
row![folder_icon, name, chevron]
.spacing(4)
.align_y(iced::Alignment::Center),

View File

@@ -1,6 +1,7 @@
use crate::components::keyboard;
use std::cell::RefCell;
use iced::widget::{Space, button, column, container, row, scrollable, text};
use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -125,22 +126,22 @@ pub fn view<'a>(state: &'a ScriptEditorState, locale: UiLocale) -> Element<'a, M
status_badge(&state.status),
],
vec![
button(text(t(locale, "common.save")).size(13))
keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Save))
.style(inputs::primary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "editor.run")).size(13))
keyboard::button(text(t(locale, "editor.run")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Run))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "editor.checkSyntax")).size(13))
keyboard::button(text(t(locale, "editor.checkSyntax")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::CheckSyntax))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::ScriptEditor(ScriptEditorMsg::Delete))
.style(inputs::danger_button)
.padding([6, 16])

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping;
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
use iced::widget::{column, container, row, scrollable, text, text_editor, text_input};
use iced::{Alignment, Color, Element, Length};
use std::collections::BTreeMap;
@@ -49,6 +50,34 @@ pub enum SettingsSection {
MCP,
}
impl SettingsSection {
pub fn access_key(&self) -> char {
match self {
Self::Project => 'p',
Self::Editor => 'r',
Self::AI => 'a',
Self::Technology => 't',
Self::Publishing => 'u',
Self::Data => 'd',
Self::MCP => 'c',
}
}
pub fn from_access_key(key: char) -> Option<Self> {
[
Self::Project,
Self::Editor,
Self::AI,
Self::Technology,
Self::Publishing,
Self::Data,
Self::MCP,
]
.into_iter()
.find(|section| section.access_key() == key)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettingsCategoryRow {
pub name: String,
@@ -261,6 +290,13 @@ impl Default for SettingsViewState {
}
impl SettingsViewState {
pub fn section_is_visible(&self, section: &SettingsSection, locale: UiLocale) -> bool {
self.search_query.is_empty()
|| t(locale, section.i18n_key())
.to_lowercase()
.contains(&self.search_query.to_lowercase())
}
pub fn focus_section(&mut self, section: SettingsSection) {
self.collapsed = SettingsSection::all()
.iter()
@@ -369,12 +405,10 @@ pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, M
.padding([8, 10])
.style(inputs::field_style);
let query_lower = state.search_query.to_lowercase();
let mut section_items = Vec::new();
for section in state.ordered_sections() {
let label = t(locale, section.i18n_key());
if !query_lower.is_empty() && !label.to_lowercase().contains(&query_lower) {
if !state.section_is_visible(&section, locale) {
continue;
}
let collapsed = state.collapsed.contains(&section);
@@ -387,7 +421,7 @@ pub fn view<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, M
text(t(locale, "common.noResults"))
.size(14)
.color(Color::from_rgb(0.7, 0.72, 0.78)),
button(text(t(locale, "common.clear")).size(13))
keyboard::button(text(t(locale, "common.clear")).size(13))
.on_press(Message::Settings(SettingsMsg::SearchChanged(String::new())))
.style(inputs::secondary_button)
.padding([6, 12]),
@@ -423,7 +457,7 @@ fn render_section<'a>(
locale: UiLocale,
) -> Element<'a, Message> {
let toggle_char = if collapsed { "\u{25B6}" } else { "\u{25BC}" };
let header = button(
let header = keyboard::button(
row![
text(toggle_char).size(12),
text(label.to_string()).size(14).color(Color::WHITE),
@@ -434,6 +468,7 @@ fn render_section<'a>(
.on_press(Message::Settings(SettingsMsg::ToggleSection(
section.clone(),
)))
.hotkey(section.access_key())
.padding([6, 8])
.width(Length::Fill)
.style(inputs::disclosure_button);
@@ -478,11 +513,11 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
inputs::labeled_input(&t(locale, "settings.dataPath"), "", &state.data_path, |s| {
Message::Settings(SettingsMsg::DataPathChanged(s))
},),
button(text(t(locale, "settings.browse")).size(12))
keyboard::button(text(t(locale, "settings.browse")).size(12))
.on_press(Message::Settings(SettingsMsg::BrowseDataPath))
.style(inputs::secondary_button)
.padding([6, 12]),
button(text(t(locale, "settings.reset")).size(12))
keyboard::button(text(t(locale, "settings.reset")).size(12))
.on_press(Message::Settings(SettingsMsg::ResetDataPath))
.style(inputs::secondary_button)
.padding([6, 12]),
@@ -559,11 +594,11 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|row| Message::Settings(SettingsMsg::BlogmarkCategoryChanged(row.name)),
);
let copy_blogmark_bookmarklet =
button(text(t(locale, "settings.copyBlogmarkBookmarklet")).size(13))
keyboard::button(text(t(locale, "settings.copyBlogmarkBookmarklet")).size(13))
.on_press(Message::Settings(SettingsMsg::CopyBlogmarkBookmarklet))
.style(inputs::secondary_button)
.padding([6, 16]);
let save = button(text(t(locale, "common.save")).size(13))
let save = keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SaveProject))
.style(inputs::primary_button)
.padding([6, 16]);
@@ -619,7 +654,7 @@ fn section_editor<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element
state.hide_unchanged_regions,
|b| Message::Settings(SettingsMsg::HideUnchangedRegionsChanged(b)),
);
let save = button(text(t(locale, "common.save")).size(13))
let save = keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SaveEditor))
.style(inputs::primary_button)
.padding([6, 16]);
@@ -656,11 +691,11 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
]
.spacing(4);
let btns = row![
button(text(t(locale, "common.save")).size(13))
keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SaveAi))
.style(inputs::primary_button)
.padding([6, 16]),
button(text(t(locale, "settings.resetToDefault")).size(13))
keyboard::button(text(t(locale, "settings.resetToDefault")).size(13))
.on_press(Message::Settings(SettingsMsg::ResetSystemPrompt))
.style(inputs::secondary_button)
.padding([6, 16]),
@@ -727,7 +762,7 @@ fn ai_mode_block<'a>(
Message::Settings(SettingsMsg::AiEndpointUrlChanged(kind, value))
}
),
button(text(t(locale, "settings.refreshModels")).size(13))
keyboard::button(text(t(locale, "settings.refreshModels")).size(13))
.on_press(Message::Settings(SettingsMsg::RefreshAiModels(kind)))
.style(inputs::secondary_button)
.padding([6, 16]),
@@ -772,7 +807,7 @@ fn ai_mode_block<'a>(
state.image_supports_vision,
move |value| Message::Settings(SettingsMsg::AiVisionChanged(kind, value)),
),
button(text(t(locale, "settings.testChat")).size(13))
keyboard::button(text(t(locale, "settings.testChat")).size(13))
.on_press(Message::Settings(SettingsMsg::TestAi(kind)))
.style(inputs::secondary_button)
.padding([6, 16]),
@@ -815,11 +850,11 @@ fn section_publishing<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Ele
|s| Message::Settings(SettingsMsg::SshRemotePathChanged(s)),
);
let btns = row![
button(text(t(locale, "common.save")).size(13))
keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Settings(SettingsMsg::SavePublishing))
.style(inputs::primary_button)
.padding([6, 16]),
button(text(t(locale, "settings.clear")).size(13))
keyboard::button(text(t(locale, "settings.clear")).size(13))
.on_press(Message::Settings(SettingsMsg::ClearPublishing))
.style(inputs::danger_button)
.padding([6, 16]),
@@ -834,37 +869,37 @@ fn section_publishing<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Ele
fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
let rebuild_btns = column![
button(text(t(locale, "settings.rebuildPosts")).size(13))
keyboard::button(text(t(locale, "settings.rebuildPosts")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildPosts))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildMedia")).size(13))
keyboard::button(text(t(locale, "settings.rebuildMedia")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildMedia))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildScripts")).size(13))
keyboard::button(text(t(locale, "settings.rebuildScripts")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildScripts))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildTemplates")).size(13))
keyboard::button(text(t(locale, "settings.rebuildTemplates")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildTemplates))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildLinks")).size(13))
keyboard::button(text(t(locale, "settings.rebuildLinks")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildLinks))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.rebuildSearchIndex")).size(13))
keyboard::button(text(t(locale, "settings.rebuildSearchIndex")).size(13))
.on_press(Message::Settings(SettingsMsg::RebuildSearchIndex))
.style(inputs::secondary_button)
.padding([6, 16])
.width(Length::Fill),
button(text(t(locale, "settings.regenerateThumbnails")).size(13))
keyboard::button(text(t(locale, "settings.regenerateThumbnails")).size(13))
.on_press(Message::Settings(SettingsMsg::RegenerateThumbnails))
.style(inputs::secondary_button)
.padding([6, 16])
@@ -872,12 +907,12 @@ fn section_data<'a>(locale: UiLocale) -> Element<'a, Message> {
]
.spacing(4);
let open = button(text(t(locale, "settings.openDataFolder")).size(13))
let open = keyboard::button(text(t(locale, "settings.openDataFolder")).size(13))
.on_press(Message::Settings(SettingsMsg::OpenDataFolder))
.style(inputs::secondary_button)
.padding([6, 16]);
let install_cli = button(text(t(locale, "settings.installCli")).size(13))
let install_cli = keyboard::button(text(t(locale, "settings.installCli")).size(13))
.on_press(Message::Settings(SettingsMsg::InstallCli))
.style(inputs::secondary_button)
.padding([6, 16]);
@@ -900,14 +935,17 @@ fn section_mcp<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a
inputs::LABEL_COLOR
};
let server = column![
iced::widget::checkbox(t(locale, "settings.mcpEnable"), state.mcp_enabled)
.on_toggle(|value| Message::Settings(SettingsMsg::McpEnabledChanged(value))),
keyboard::focusable(
iced::widget::checkbox(t(locale, "settings.mcpEnable"), state.mcp_enabled)
.on_toggle(|value| Message::Settings(SettingsMsg::McpEnabledChanged(value))),
true,
),
row![
text(status).size(13).color(status_color),
text(state.mcp_endpoint.clone())
.size(12)
.color(inputs::LABEL_COLOR),
button(text(t(locale, "settings.mcpRefresh")).size(12))
keyboard::button(text(t(locale, "settings.mcpRefresh")).size(12))
.on_press(Message::Settings(SettingsMsg::McpRefresh))
.style(inputs::secondary_button)
.padding([5, 10]),
@@ -940,13 +978,13 @@ fn section_mcp<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a
]
.spacing(2)
.width(Length::Fill),
button(text(t(locale, "settings.mcpApprove")).size(12))
keyboard::button(text(t(locale, "settings.mcpApprove")).size(12))
.on_press(Message::Settings(SettingsMsg::McpProposalAccepted(
proposal.id.clone()
)))
.style(inputs::primary_button)
.padding([5, 10]),
button(text(t(locale, "settings.mcpReject")).size(12))
keyboard::button(text(t(locale, "settings.mcpReject")).size(12))
.on_press(Message::Settings(SettingsMsg::McpProposalRejected(
proposal.id.clone()
)))
@@ -964,10 +1002,13 @@ fn section_mcp<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a
let agents = column(state.mcp_agents.iter().map(|agent| {
let configured = agent.configured;
column![
iced::widget::checkbox(agent.label.clone(), configured).on_toggle({
let agent = agent.agent;
move |_| Message::Settings(SettingsMsg::McpAgentToggled(agent))
}),
keyboard::focusable(
iced::widget::checkbox(agent.label.clone(), configured).on_toggle({
let agent = agent.agent;
move |_| Message::Settings(SettingsMsg::McpAgentToggled(agent))
}),
true,
),
text(agent.config_path.clone())
.size(11)
.color(inputs::LABEL_COLOR),

View File

@@ -1,7 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use std::path::{Path, PathBuf};
use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, image, row, scrollable, text, text_input};
use iced::widget::{Space, column, container, image, row, scrollable, text, text_input};
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -301,10 +303,10 @@ fn row_delete_style(_theme: &Theme, status: button::Status) -> button::Style {
/// Per sidebar_views.allium *ListItemEntry RowLayout: right-aligned, visible
/// only on row hover, routed to the row's delete message.
fn with_row_delete(
open_button: iced::widget::Button<'static, Message>,
open_button: impl Into<Element<'static, Message>>,
on_delete: Message,
) -> Element<'static, Message> {
let delete_button = button(
let delete_button = keyboard::button(
text("\u{2715}") // ✕
.size(11)
.shaping(Shaping::Advanced),
@@ -313,7 +315,7 @@ fn with_row_delete(
.padding([2, 6])
.style(row_delete_style);
iced::widget::hover(
open_button,
open_button.into(),
container(delete_button)
.width(Length::Fill)
.height(Length::Fill)
@@ -362,7 +364,7 @@ fn calendar_widget(
} else {
calendar_style
};
let year_btn = button(text(label).size(11).shaping(Shaping::Advanced))
let year_btn = keyboard::button(text(label).size(11).shaping(Shaping::Advanced))
.on_press(if year_selected {
on_year_clone(None)
} else {
@@ -384,7 +386,7 @@ fn calendar_widget(
} else {
calendar_style
};
let month_btn = button(text(label).size(10).shaping(Shaping::Advanced))
let month_btn = keyboard::button(text(label).size(10).shaping(Shaping::Advanced))
.on_press(if month_selected {
on_month_clone(None)
} else {
@@ -428,7 +430,7 @@ fn chip_selector(
} else {
chip_style
};
let chip = button(text(tag.clone()).size(10).shaping(Shaping::Advanced))
let chip = keyboard::button(text(tag.clone()).size(10).shaping(Shaping::Advanced))
.on_press(on_toggle_clone(tag_clone))
.padding([2, 6])
.style(style_fn);
@@ -476,7 +478,7 @@ fn single_select_chip_selector(
};
let value = value.clone();
let on_toggle = on_toggle.clone();
button(text(display.clone()).size(10).shaping(Shaping::Advanced))
keyboard::button(text(display.clone()).size(10).shaping(Shaping::Advanced))
.on_press(if is_selected {
on_toggle(None)
} else {
@@ -599,7 +601,7 @@ fn post_filter_panel(
// Clear all filters button
if filter.has_active_filters() {
sections.push(
button(
keyboard::button(
text(t(locale, "sidebar.filter.clearAll"))
.size(10)
.shaping(Shaping::Advanced),
@@ -645,7 +647,7 @@ fn media_filter_panel(filter: &MediaFilter, locale: UiLocale) -> Element<'static
if filter.has_active_filters() {
sections.push(
button(
keyboard::button(
text(t(locale, "sidebar.filter.clearAll"))
.size(10)
.shaping(Shaping::Advanced),
@@ -711,7 +713,7 @@ pub fn view(
row![
header,
Space::with_width(Length::Fill),
button(
keyboard::button(
text(t(locale, "common.add"))
.size(11)
.shaping(Shaping::Advanced)
@@ -752,10 +754,11 @@ pub fn view(
} else {
"\u{25BC}" // ▼ toggle icon
};
let filter_toggle = button(text(toggle_label).size(11).shaping(Shaping::Advanced))
.on_press(Message::TogglePostFilterPanel)
.padding([4, 6])
.style(toggle_style);
let filter_toggle =
keyboard::button(text(toggle_label).size(11).shaping(Shaping::Advanced))
.on_press(Message::TogglePostFilterPanel)
.padding([4, 6])
.style(toggle_style);
top_items.push(row![search, filter_toggle].spacing(4).into());
@@ -817,7 +820,7 @@ pub fn view(
} else {
item_style
};
button(
keyboard::button(
container(column![label_text, date_text].spacing(1))
.width(Length::Fill)
.clip(true),
@@ -901,10 +904,11 @@ pub fn view(
} else {
"\u{25BC}" // ▼ toggle icon
};
let filter_toggle = button(text(toggle_label).size(11).shaping(Shaping::Advanced))
.on_press(Message::ToggleMediaFilterPanel)
.padding([4, 6])
.style(toggle_style);
let filter_toggle =
keyboard::button(text(toggle_label).size(11).shaping(Shaping::Advanced))
.on_press(Message::ToggleMediaFilterPanel)
.padding([4, 6])
.style(toggle_style);
top_items.push(row![search, filter_toggle].spacing(4).into());
@@ -995,7 +999,7 @@ pub fn view(
.spacing(8)
.align_y(iced::Alignment::Center);
button(container(content).width(Length::Fill).clip(true))
keyboard::button(container(content).width(Length::Fill).clip(true))
.on_press(Message::OpenTab(Tab {
id: m.id.clone(),
tab_type: TabType::Media,
@@ -1046,7 +1050,7 @@ pub fn view(
} else {
item_style
};
let open_button = button(
let open_button = keyboard::button(
container(column![label_text, date_text].spacing(1))
.width(Length::Fill)
.clip(true),
@@ -1097,7 +1101,7 @@ pub fn view(
} else {
item_style
};
let open_button = button(
let open_button = keyboard::button(
container(column![label_text, date_text].spacing(1))
.width(Length::Fill)
.clip(true),
@@ -1145,7 +1149,7 @@ pub fn view(
.size(10)
.shaping(Shaping::Advanced)
.color(muted);
let open_button = button(
let open_button = keyboard::button(
container(
column![
text(definition_name).size(12).shaping(Shaping::Advanced),
@@ -1197,7 +1201,7 @@ pub fn view(
.size(10)
.shaping(Shaping::Advanced)
.color(muted);
let open_button = button(
let open_button = keyboard::button(
container(
column![
text(conversation.title.clone())
@@ -1257,7 +1261,7 @@ pub fn view(
is_dirty: false,
})
};
button(container(label_text).width(Length::Fill))
keyboard::button(container(label_text).width(Length::Fill))
.on_press(msg)
.padding([5, 8])
.width(Length::Fill)
@@ -1291,7 +1295,7 @@ pub fn view(
.map(|(key, section)| {
let label = t(locale, key);
let label_text = text(label).size(12).shaping(Shaping::Advanced);
button(container(label_text).width(Length::Fill))
keyboard::button(container(label_text).width(Length::Fill))
.on_press(Message::OpenTagsSection(*section))
.padding([5, 8])
.width(Length::Fill)

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping;
use iced::widget::{button, column, container, row, scrollable, text};
use iced::widget::{column, container, row, scrollable, text};
use iced::{Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -21,7 +22,7 @@ pub struct SiteValidationState {
pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a, Message> {
let run_button = if state.is_running {
button(
keyboard::button(
text(t(locale, "siteValidation.running"))
.size(13)
.shaping(Shaping::Advanced),
@@ -29,12 +30,13 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.style(inputs::primary_button)
.padding([6, 16])
} else {
button(
keyboard::button(
text(t(locale, "siteValidation.run"))
.size(13)
.shaping(Shaping::Advanced),
)
.on_press(Message::RunSiteValidation)
.hotkey('r')
.style(inputs::primary_button)
.padding([6, 16])
};
@@ -42,7 +44,7 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
|| !state.extra_files.is_empty()
|| !state.stale_files.is_empty();
let apply_button = if state.is_applying {
button(
keyboard::button(
text(t(locale, "siteValidation.applying"))
.size(13)
.shaping(Shaping::Advanced),
@@ -50,16 +52,17 @@ pub fn view<'a>(state: &'a SiteValidationState, locale: UiLocale) -> Element<'a,
.style(inputs::secondary_button)
.padding([6, 16])
} else if !state.is_running && state.error_message.is_none() && has_issues {
button(
keyboard::button(
text(t(locale, "siteValidation.apply"))
.size(13)
.shaping(Shaping::Advanced),
)
.on_press(Message::ApplySiteValidation)
.hotkey('a')
.style(inputs::primary_button)
.padding([6, 16])
} else {
button(
keyboard::button(
text(t(locale, "siteValidation.apply"))
.size(13)
.shaping(Shaping::Advanced),

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, container, row, text};
use iced::widget::{Space, container, row, text};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
use bds_core::engine::task::TaskStatus;
@@ -235,7 +237,7 @@ pub fn view(
);
// Airplane mode toggle — ✈ icon
let airplane_btn = button(text("\u{2708}").size(13).shaping(Shaping::Advanced))
let airplane_btn = keyboard::button(text("\u{2708}").size(13).shaping(Shaping::Advanced))
.on_press(Message::SetOfflineMode(!offline_mode))
.padding([2, 4])
.style(if offline_mode {
@@ -249,7 +251,7 @@ pub fn view(
.size(14)
.shaping(Shaping::Advanced);
let locale_trigger = button(trigger_flag)
let locale_trigger = keyboard::button(trigger_flag)
.on_press(Message::ToggleLocaleDropdown)
.padding([1, 4])
.style(dropdown_trigger);

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping;
use iced::widget::{Column, Space, button, column, container, row, scrollable, text};
use iced::widget::{Column, Space, column, container, row, scrollable, text};
use iced::{Background, Border, Color, Element, Length, Radians, Theme, gradient};
use bds_core::i18n::UiLocale;
@@ -220,7 +222,7 @@ fn swatch(background: Background, width_portion: u16) -> Element<'static, Messag
fn theme_button<'a>(theme: &StyleTheme, selected_theme: &str) -> Element<'a, Message> {
let selected = theme.name == selected_theme;
let theme_name = theme.name.to_string();
button(
keyboard::button(
column![
row![
swatch(theme_accent_background(theme), 2),
@@ -318,13 +320,13 @@ pub fn view<'a>(
.size(13)
.shaping(Shaping::Advanced);
let apply_button: Element<'a, Message> = if state.can_apply() {
button(apply_label)
keyboard::button(apply_label)
.on_press(Message::Style(StyleMsg::Apply))
.padding([8, 16])
.style(inputs::primary_button)
.into()
} else {
button(apply_label)
keyboard::button(apply_label)
.padding([8, 16])
.style(inputs::primary_button)
.into()

View File

@@ -1,7 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::scrollable::Direction;
use iced::widget::text::Shaping;
use iced::widget::tooltip::Position;
use iced::widget::{Space, button, container, row, scrollable, text, tooltip};
use iced::widget::{Space, container, row, scrollable, text, tooltip};
use iced::{Background, Border, Color, Element, Font, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -177,9 +179,20 @@ pub fn view(tabs: &[Tab], active_tab: Option<&str>, locale: UiLocale) -> Element
.width(Length::Fill)
.clip(true);
// Keep the tooltip around non-interactive title content so it does
// not hide the tab and close controls from widget operations.
let tooltip_text = build_tooltip_text(tab, locale);
let title_area = tooltip(
title_area,
text(tooltip_text).size(11).shaping(Shaping::Advanced),
Position::Bottom,
)
.gap(4)
.style(inputs::tooltip_style);
let label = row![
title_area,
button(text("\u{2715}").size(10).shaping(Shaping::Advanced))
keyboard::button(text("\u{2715}").size(10).shaping(Shaping::Advanced))
.on_press(Message::CloseTab(close_id))
.padding(2)
.style(close_style),
@@ -188,24 +201,12 @@ pub fn view(tabs: &[Tab], active_tab: Option<&str>, locale: UiLocale) -> Element
.align_y(iced::Alignment::Center);
// tabs.allium: tab_min_width=100, tab_max_width=160
let tab_btn = button(label)
keyboard::button(label)
.on_press(Message::SelectTab(tab_id))
.padding([6, 8])
.width(Length::Fixed(TAB_WIDTH))
.style(if is_active { tab_active } else { tab_inactive });
// tabs.allium tooltip: title + "(Preview)" if transient + "* Modified" if dirty
let tooltip_text = build_tooltip_text(tab, locale);
let tip: Element<'static, Message> = tooltip(
tab_btn,
text(tooltip_text).size(11).shaping(Shaping::Advanced),
Position::Bottom,
)
.gap(4)
.style(inputs::tooltip_style)
.into();
tip
.style(if is_active { tab_active } else { tab_inactive })
.into()
})
.collect();

View File

@@ -1,7 +1,9 @@
use crate::components::keyboard;
use iced::widget::button;
use std::collections::HashMap;
use iced::widget::{
Space, button, checkbox, column, container, pick_list, row, scrollable, text, text_input,
Space, checkbox, column, container, pick_list, row, scrollable, text, text_input,
};
use iced::{Alignment, Background, Color, Element, Length, Theme};
@@ -247,69 +249,79 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
}));
let name = category.name.clone();
cells.push(
container(
container(keyboard::focusable(
checkbox("", category.render_in_lists).on_toggle(move |value| {
Message::Settings(SettingsMsg::CategoryRenderInListsChanged(
name.clone(),
value,
))
}),
)
true,
))
.width(Length::Fixed(TOGGLE_WIDTH))
.into(),
);
let name = category.name.clone();
cells.push(
container(checkbox("", category.show_title).on_toggle(move |value| {
Message::Settings(SettingsMsg::CategoryShowTitleChanged(name.clone(), value))
}))
container(keyboard::focusable(
checkbox("", category.show_title).on_toggle(move |value| {
Message::Settings(SettingsMsg::CategoryShowTitleChanged(name.clone(), value))
}),
true,
))
.width(Length::Fixed(TOGGLE_WIDTH))
.into(),
);
let name = category.name.clone();
cells.push(
pick_list(
template_options.clone(),
Some(category.post_template_slug.clone()),
move |value| {
Message::Settings(SettingsMsg::CategoryPostTemplateChanged(
name.clone(),
value,
))
},
keyboard::focusable(
pick_list(
template_options.clone(),
Some(category.post_template_slug.clone()),
move |value| {
Message::Settings(SettingsMsg::CategoryPostTemplateChanged(
name.clone(),
value,
))
},
)
.padding([7, 9])
.style(inputs::select_style)
.width(Length::Fixed(TEMPLATE_WIDTH)),
true,
)
.padding([7, 9])
.style(inputs::select_style)
.width(Length::Fixed(TEMPLATE_WIDTH))
.into(),
);
let name = category.name.clone();
cells.push(
pick_list(
template_options.clone(),
Some(category.list_template_slug.clone()),
move |value| {
Message::Settings(SettingsMsg::CategoryListTemplateChanged(
name.clone(),
value,
))
},
keyboard::focusable(
pick_list(
template_options.clone(),
Some(category.list_template_slug.clone()),
move |value| {
Message::Settings(SettingsMsg::CategoryListTemplateChanged(
name.clone(),
value,
))
},
)
.padding([7, 9])
.style(inputs::select_style)
.width(Length::Fixed(TEMPLATE_WIDTH)),
true,
)
.padding([7, 9])
.style(inputs::select_style)
.width(Length::Fixed(TEMPLATE_WIDTH))
.into(),
);
cells.push(
container(
row![
button(text(t(locale, "common.save")).size(12))
keyboard::button(text(t(locale, "common.save")).size(12))
.on_press(Message::Settings(SettingsMsg::SaveCategory(
category.name.clone(),
)))
.style(inputs::primary_button)
.padding([6, 10]),
button(text(t(locale, "common.remove")).size(12))
keyboard::button(text(t(locale, "common.remove")).size(12))
.on_press_maybe((!category.is_protected).then(|| Message::Settings(
SettingsMsg::RemoveCategory(category.name.clone()),
)))
@@ -336,11 +348,11 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
&state.new_category_name,
|value| Message::Settings(SettingsMsg::AddCategoryNameChanged(value)),
),
button(text(t(locale, "common.add")).size(13))
keyboard::button(text(t(locale, "common.add")).size(13))
.on_press(Message::Settings(SettingsMsg::AddCategory))
.style(inputs::primary_button)
.padding([6, 12]),
button(text(t(locale, "settings.resetCategories")).size(13))
keyboard::button(text(t(locale, "settings.resetCategories")).size(13))
.on_press(Message::Settings(SettingsMsg::ResetCategoriesToDefaults))
.style(inputs::secondary_button)
.padding([6, 12]),
@@ -382,7 +394,7 @@ fn view_categories<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
}
fn section_tab<'a>(label: &str, active: bool, section: TagsSection) -> Element<'a, Message> {
button(text(label.to_string()).size(13))
keyboard::button(text(label.to_string()).size(13))
.on_press(Message::Tags(TagsMsg::SetSection(section)))
.padding([6, 12])
.style(if active {
@@ -443,7 +455,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
.selected_tags
.iter()
.any(|selected_id| selected_id == &tag.id);
button(
keyboard::button(
row![
text(&tag.name).size(font_size).color(Color::WHITE),
text(post_count.to_string())
@@ -488,7 +500,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
))
.size(12)
.color(Color::from_rgb(0.75, 0.77, 0.82)),
button(text(t(locale, "tags.clearSelection")).size(12))
keyboard::button(text(t(locale, "tags.clearSelection")).size(12))
.on_press(Message::Tags(TagsMsg::ClearSelection))
.style(inputs::secondary_button)
.padding([4, 8]),
@@ -533,7 +545,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|value| Message::Tags(TagsMsg::CreateColorChanged(value)),
),
color_swatches(locale, true),
button(text(t(locale, "tags.createButton")).size(13))
keyboard::button(text(t(locale, "tags.createButton")).size(13))
.on_press_maybe(
(!state.create_name.trim().is_empty()).then_some(Message::Tags(TagsMsg::CreateTag))
)
@@ -573,7 +585,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
.get(&tag.name.to_lowercase())
.copied()
.unwrap_or(0);
button(
keyboard::button(
row![
container(Space::new(12, 12)).style(move |_: &Theme| container::Style {
background: Some(Background::Color(color)),
@@ -620,7 +632,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
.iter()
.find(|option| option.slug == editing.template_slug);
let delete_button: Element<'a, Message> =
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::Tags(TagsMsg::DeleteTag(editing.id.clone())))
.style(inputs::danger_button)
.padding([6, 16])
@@ -645,7 +657,7 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|choice| Message::Tags(TagsMsg::EditTagTemplate(choice)),
),
row![
button(text(t(locale, "common.save")).size(13))
keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::Tags(TagsMsg::SaveTag))
.style(inputs::primary_button)
.padding([6, 16]),
@@ -744,7 +756,7 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
.padding([4, 0])
.into()
},
button(text(t(locale, "tags.merge")).size(13))
keyboard::button(text(t(locale, "tags.merge")).size(13))
.on_press_maybe(
state
.merge_target
@@ -768,7 +780,7 @@ fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a,
text(t(locale, "tags.discoverDescription"))
.size(12)
.color(Color::from_rgb(0.60, 0.60, 0.65)),
button(text(t(locale, "tags.discoverButton")).size(13))
keyboard::button(text(t(locale, "tags.discoverButton")).size(13))
.on_press(Message::Tags(TagsMsg::SyncTags))
.style(inputs::primary_button)
.padding([6, 16]),
@@ -819,7 +831,7 @@ fn color_swatches<'a>(locale: UiLocale, create_mode: bool) -> Element<'a, Messag
} else {
TagsMsg::EditTagColor((*hex).to_string())
};
button(Space::new(18, 18))
keyboard::button(Space::new(18, 18))
.on_press(Message::Tags(msg))
.padding(0)
.style(move |_theme: &Theme, _status| button::Style {

View File

@@ -1,6 +1,7 @@
use crate::components::keyboard;
use std::cell::RefCell;
use iced::widget::{Space, button, column, container, row, scrollable, text};
use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -114,17 +115,17 @@ pub fn view<'a>(state: &'a TemplateEditorState, locale: UiLocale) -> Element<'a,
status_badge(&state.status),
],
vec![
button(text(t(locale, "common.save")).size(13))
keyboard::button(text(t(locale, "common.save")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Save))
.style(inputs::primary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "editor.validate")).size(13))
keyboard::button(text(t(locale, "editor.validate")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Validate))
.style(inputs::secondary_button)
.padding([6, 16])
.into(),
button(text(t(locale, "modal.confirmDelete.delete")).size(13))
keyboard::button(text(t(locale, "modal.confirmDelete.delete")).size(13))
.on_press(Message::TemplateEditor(TemplateEditorMsg::Delete))
.style(inputs::danger_button)
.padding([6, 16])

View File

@@ -1,5 +1,7 @@
use crate::components::keyboard;
use iced::widget::button;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, container, row, text};
use iced::widget::{Space, container, row, text};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme};
use crate::app::Message;
@@ -62,7 +64,7 @@ pub fn view(toasts: &[Toast]) -> Option<Element<'static, Message>> {
.iter()
.map(|toast| {
let level = toast.level;
let dismiss = button(text("\u{2715}").size(11).shaping(Shaping::Advanced))
let dismiss = keyboard::button(text("\u{2715}").size(11).shaping(Shaping::Advanced))
.on_press(Message::DismissToast(toast.id))
.padding([2, 4])
.style(dismiss_btn);

View File

@@ -1,5 +1,6 @@
use crate::components::keyboard;
use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, row, scrollable, text};
use iced::widget::{Space, column, container, row, scrollable, text};
use iced::{Color, Element, Length};
use bds_core::engine::validate_translations::{
@@ -19,7 +20,7 @@ pub struct TranslationValidationState {
}
pub fn view<'a>(state: &'a TranslationValidationState, locale: UiLocale) -> Element<'a, Message> {
let run = button(text(t(locale, "translationValidation.run")).size(13))
let run = keyboard::button(text(t(locale, "translationValidation.run")).size(13))
.on_press_maybe((!state.is_running).then_some(Message::ValidateTranslations))
.style(inputs::primary_button)
.padding([6, 16]);

View File

@@ -1,8 +1,9 @@
use crate::components::keyboard;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use iced::widget::text::Shaping;
use iced::widget::{Space, button, column, container, mouse_area, row, stack, text};
use iced::widget::{Space, column, container, mouse_area, row, stack, text};
use iced::{Alignment, Background, Color, Element, Length, Padding, Theme};
use bds_core::engine::git::GitCommit;
@@ -305,7 +306,7 @@ pub fn view<'a>(
.map(|&l| {
let flag_text = text(l.flag_emoji()).size(16).shaping(Shaping::Advanced);
button(flag_text)
keyboard::button(flag_text)
.on_press(Message::SetUiLocale(l))
.padding([4, 8])
.style(status_bar::dropdown_item)
@@ -371,15 +372,32 @@ pub fn view<'a>(
None
};
// Only the top blocking layer participates in keyboard traversal/access keys.
let modal_is_active = active_modal.is_some();
let blocking_overlay = modal_is_active || overlay.is_some();
let base_layout: Element<'a, Message> = if blocking_overlay {
keyboard::suspend(base_layout).into()
} else {
base_layout
};
// Collect overlays: dropdowns and toasts
let mut overlays: Vec<Element<'a, Message>> = Vec::new();
if let Some(toast_overlay) = toast::view(toasts) {
overlays.push(toast_overlay);
overlays.push(if blocking_overlay {
keyboard::suspend(toast_overlay).into()
} else {
toast_overlay
});
}
if let Some(overlay) = overlay {
overlays.push(overlay);
overlays.push(if modal_is_active {
keyboard::suspend(overlay).into()
} else {
overlay
});
}
// Modal overlay (highest z-index)