Add preference section navigation

This commit is contained in:
Georg Bauer
2026-07-27 15:26:25 +02:00
parent c96675c462
commit c4e92f6206
4 changed files with 212 additions and 65 deletions

View File

@@ -46,6 +46,7 @@ pub(super) const MAX_SIDEBAR_WIDTH: i32 = 520;
pub(crate) struct App { pub(crate) struct App {
main_window: window::Id, main_window: window::Id,
pub(super) preferences_window: Option<window::Id>,
pub(super) model_manager_window: Option<window::Id>, pub(super) model_manager_window: Option<window::Id>,
pub(super) help_window: Option<window::Id>, pub(super) help_window: Option<window::Id>,
pub(super) pending_model_delete: Option<ManagedArtifactId>, pub(super) pending_model_delete: Option<ManagedArtifactId>,
@@ -59,7 +60,6 @@ pub(crate) struct App {
projects: Vec<ProjectWithSessions>, projects: Vec<ProjectWithSessions>,
config: Config, config: Config,
preference_draft: PreferenceDraft, preference_draft: PreferenceDraft,
preferences_open: bool,
preference_error: Option<String>, preference_error: Option<String>,
selected_project: Option<i32>, selected_project: Option<i32>,
selected_session: Option<i32>, selected_session: Option<i32>,
@@ -153,6 +153,53 @@ pub(super) enum DetailTab {
Stats, Stats,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum PreferenceSection {
Model,
Endpoint,
Generation,
Execution,
Acceleration,
KvCache,
Steering,
}
impl PreferenceSection {
const ALL: [Self; 7] = [
Self::Model,
Self::Endpoint,
Self::Generation,
Self::Execution,
Self::Acceleration,
Self::KvCache,
Self::Steering,
];
fn anchor(self) -> &'static str {
match self {
Self::Model => "preferences-model",
Self::Endpoint => "preferences-endpoint",
Self::Generation => "preferences-generation",
Self::Execution => "preferences-execution",
Self::Acceleration => "preferences-acceleration",
Self::KvCache => "preferences-kv-cache",
Self::Steering => "preferences-steering",
}
}
fn label(self) -> &'static str {
match self {
Self::Model => "Model & lifecycle",
Self::Endpoint => "Local endpoint",
Self::Generation => "Generation",
Self::Execution => "Execution",
Self::Acceleration => "Acceleration & memory",
Self::KvCache => "KV cache",
Self::Steering => "Steering & diagnostics",
}
}
}
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub(super) struct MetricsPoint { pub(super) struct MetricsPoint {
pub(super) decode_tokens_per_second: f32, pub(super) decode_tokens_per_second: f32,
@@ -171,6 +218,9 @@ pub(crate) enum Message {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
NativeEdit(crate::native_edit::EditCommand), NativeEdit(crate::native_edit::EditCommand),
OpenPreferences, OpenPreferences,
PreferencesOpened(window::Id),
ClosePreferences,
ScrollPreferences(PreferenceSection),
OpenModelManager, OpenModelManager,
OpenHelp, OpenHelp,
HelpOpened(window::Id), HelpOpened(window::Id),
@@ -180,6 +230,7 @@ pub(crate) enum Message {
ModelManagerOpened(window::Id), ModelManagerOpened(window::Id),
WindowOpened(window::Id), WindowOpened(window::Id),
WindowClosed(window::Id), WindowClosed(window::Id),
Escape(window::Id),
DismissPanel, DismissPanel,
/// Tab and shift-tab: iced leaves the key to the application, so the fields /// Tab and shift-tab: iced leaves the key to the application, so the fields
/// of a form are only linked once we move the focus ourselves. /// of a form are only linked once we move the focus ourselves.
@@ -325,6 +376,7 @@ impl App {
spawn_services(&config, Arc::clone(&metrics)); spawn_services(&config, Arc::clone(&metrics));
Self { Self {
main_window, main_window,
preferences_window: None,
model_manager_window: None, model_manager_window: None,
help_window: None, help_window: None,
pending_model_delete: None, pending_model_delete: None,
@@ -338,7 +390,6 @@ impl App {
projects, projects,
config, config,
preference_draft, preference_draft,
preferences_open: false,
preference_error: None, preference_error: None,
selected_project: last_project, selected_project: last_project,
selected_session: None, selected_session: None,
@@ -452,6 +503,7 @@ impl App {
let startup_error = None::<String>; let startup_error = None::<String>;
Self { Self {
main_window, main_window,
preferences_window: None,
model_manager_window: None, model_manager_window: None,
help_window: None, help_window: None,
pending_model_delete: None, pending_model_delete: None,
@@ -465,7 +517,6 @@ impl App {
projects: Vec::new(), projects: Vec::new(),
config, config,
preference_draft, preference_draft,
preferences_open: false,
preference_error: None, preference_error: None,
selected_project: None, selected_project: None,
selected_session: None, selected_session: None,
@@ -555,7 +606,19 @@ impl App {
Message::NativeEdit(command) => { Message::NativeEdit(command) => {
crate::native_edit::queue_command(&self.native_edit_commands, command) crate::native_edit::queue_command(&self.native_edit_commands, command)
} }
Message::OpenPreferences => self.open_preferences(), Message::OpenPreferences => return self.open_preferences(),
Message::PreferencesOpened(id) => {
if self.preferences_window == Some(id) {
return window::gain_focus(id);
}
}
Message::ClosePreferences => {
if let Some(id) = self.preferences_window {
self.preference_error = None;
return window::close(id);
}
}
Message::ScrollPreferences(section) => return scroll_preferences_to(section),
Message::OpenModelManager => return self.open_model_manager(), Message::OpenModelManager => return self.open_model_manager(),
Message::OpenHelp => return self.open_help(), Message::OpenHelp => return self.open_help(),
Message::HelpOpened(id) => { Message::HelpOpened(id) => {
@@ -625,10 +688,22 @@ impl App {
self.model_manager_window = None; self.model_manager_window = None;
self.pending_model_delete = None; self.pending_model_delete = None;
} }
if self.preferences_window == Some(id) {
self.preferences_window = None;
self.preference_error = None;
}
if self.help_window == Some(id) { if self.help_window == Some(id) {
self.help_window = None; self.help_window = None;
} }
} }
Message::Escape(id) => {
if self.preferences_window == Some(id) {
return self.update(Message::ClosePreferences);
}
if id == self.main_window {
return self.update(Message::DismissPanel);
}
}
Message::DismissPanel => { Message::DismissPanel => {
if self.pending_session_delete.is_some() { if self.pending_session_delete.is_some() {
self.pending_session_delete = None; self.pending_session_delete = None;
@@ -637,9 +712,6 @@ impl App {
} else if self.session_rename.is_some() || self.session_menu.is_some() { } else if self.session_rename.is_some() || self.session_menu.is_some() {
self.session_rename = None; self.session_rename = None;
self.session_menu = None; self.session_menu = None;
} else if self.preferences_open {
self.preferences_open = false;
self.preference_error = None;
} else if self.pending_project_path.is_some() { } else if self.pending_project_path.is_some() {
self.pending_project_path = None; self.pending_project_path = None;
self.project_name_input.clear(); self.project_name_input.clear();
@@ -914,7 +986,14 @@ impl App {
self.preference_draft.reset(); self.preference_draft.reset();
self.preference_error = None; self.preference_error = None;
} }
Message::SavePreferences => self.save_preferences(), Message::SavePreferences => {
self.save_preferences();
if self.preference_error.is_none()
&& let Some(id) = self.preferences_window
{
return window::close(id);
}
}
Message::DownloadArtifact(artifact) => { Message::DownloadArtifact(artifact) => {
self.start_model_operation(artifact, ModelOperation::Download) self.start_model_operation(artifact, ModelOperation::Download)
} }
@@ -1509,7 +1588,7 @@ impl App {
}), }),
// A focused text field takes escape for itself to drop its own // A focused text field takes escape for itself to drop its own
// focus, so a dialog would never see it through `on_key_press`. // focus, so a dialog would never see it through `on_key_press`.
iced::event::listen_with(|event, _, _| { iced::event::listen_with(|event, _, id| {
matches!( matches!(
event, event,
iced::Event::Keyboard(keyboard::Event::KeyPressed { iced::Event::Keyboard(keyboard::Event::KeyPressed {
@@ -1517,7 +1596,7 @@ impl App {
.. ..
}) })
) )
.then_some(Message::DismissPanel) .then_some(Message::Escape(id))
}), }),
window::close_requests().map(Message::WindowClosed), window::close_requests().map(Message::WindowClosed),
window::close_events().map(Message::WindowClosed), window::close_events().map(Message::WindowClosed),
@@ -1572,7 +1651,9 @@ impl App {
} }
pub(crate) fn title(&self, id: window::Id) -> String { pub(crate) fn title(&self, id: window::Id) -> String {
if self.model_manager_window == Some(id) { if self.preferences_window == Some(id) {
"Preferences — DS4Server".to_owned()
} else if self.model_manager_window == Some(id) {
"Model Manager — DS4Server".to_owned() "Model Manager — DS4Server".to_owned()
} else if self.help_window == Some(id) { } else if self.help_window == Some(id) {
"Help — DS4Server".to_owned() "Help — DS4Server".to_owned()
@@ -1880,6 +1961,10 @@ pub(super) fn preferences_scroll_id() -> iced::widget::Id {
iced::widget::Id::new("preferences-fields") iced::widget::Id::new("preferences-fields")
} }
fn preferences_section_id(section: PreferenceSection) -> iced::widget::Id {
iced::widget::Id::new(section.anchor())
}
/// Scrolls the preferences form so the field that just took focus is inside /// Scrolls the preferences form so the field that just took focus is inside
/// the viewport. Iced moves focus without touching the scroll offset, so a tab /// the viewport. Iced moves focus without touching the scroll offset, so a tab
/// past the fold would otherwise send the typing to a field nobody can see. /// past the fold would otherwise send the typing to a field nobody can see.
@@ -1888,13 +1973,22 @@ pub(super) fn preferences_scroll_id() -> iced::widget::Id {
/// innermost container around the focused widget — the row that holds the /// innermost container around the focused widget — the row that holds the
/// label and its input. /// label and its input.
fn reveal_focused() -> Task<Message> { fn reveal_focused() -> Task<Message> {
reveal_preferences(None)
}
fn scroll_preferences_to(section: PreferenceSection) -> Task<Message> {
reveal_preferences(Some(preferences_section_id(section)))
}
fn reveal_preferences(target: Option<iced::widget::Id>) -> Task<Message> {
use iced::advanced::widget::{Id, Operation, operation}; use iced::advanced::widget::{Id, Operation, operation};
use iced::{Rectangle, Vector}; use iced::{Rectangle, Vector};
struct Locate { struct Locate {
target: Option<Id>,
rows: Vec<Rectangle>, rows: Vec<Rectangle>,
next_container: Option<Rectangle>, next_container: Option<Rectangle>,
focused: Option<Rectangle>, found: Option<Rectangle>,
} }
impl<T> Operation<T> for Locate { impl<T> Operation<T> for Locate {
@@ -1909,7 +2003,14 @@ fn reveal_focused() -> Task<Message> {
} }
} }
fn container(&mut self, _id: Option<&Id>, bounds: Rectangle) { fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
if self
.target
.as_ref()
.is_some_and(|target| id == Some(target))
{
self.found = Some(bounds);
}
self.next_container = Some(bounds); self.next_container = Some(bounds);
} }
@@ -1919,20 +2020,24 @@ fn reveal_focused() -> Task<Message> {
bounds: Rectangle, bounds: Rectangle,
state: &mut dyn operation::Focusable, state: &mut dyn operation::Focusable,
) { ) {
if state.is_focused() { if self.target.is_none() && state.is_focused() {
self.focused = self.rows.last().copied().or(Some(bounds)); self.found = self.rows.last().copied().or(Some(bounds));
} }
} }
fn finish(&self) -> operation::Outcome<T> { fn finish(&self) -> operation::Outcome<T> {
self.focused.map_or(operation::Outcome::None, |field| { self.found.map_or(operation::Outcome::None, |field| {
operation::Outcome::Chain(Box::new(Reveal { field })) operation::Outcome::Chain(Box::new(Reveal {
field,
align_top: self.target.is_some(),
}))
}) })
} }
} }
struct Reveal { struct Reveal {
field: Rectangle, field: Rectangle,
align_top: bool,
} }
impl<T> Operation<T> for Reveal { impl<T> Operation<T> for Reveal {
@@ -1951,8 +2056,13 @@ fn reveal_focused() -> Task<Message> {
if id != Some(&preferences_scroll_id()) { if id != Some(&preferences_scroll_id()) {
return; return;
} }
let Some(offset) = reveal_offset(self.field, bounds, translation.y) else { let offset = if self.align_top {
return; section_offset(self.field, bounds, translation.y)
} else {
let Some(offset) = reveal_offset(self.field, bounds, translation.y) else {
return;
};
offset
}; };
state.scroll_by( state.scroll_by(
scrollable::AbsoluteOffset { x: 0.0, y: offset }, scrollable::AbsoluteOffset { x: 0.0, y: offset },
@@ -1963,12 +2073,17 @@ fn reveal_focused() -> Task<Message> {
} }
iced::advanced::widget::operate(Locate { iced::advanced::widget::operate(Locate {
target,
rows: Vec::new(), rows: Vec::new(),
next_container: None, next_container: None,
focused: None, found: None,
}) })
} }
fn section_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f32) -> f32 {
field.y - scrolled - viewport.y
}
/// How far the scroll area has to move for `field` to sit fully inside /// How far the scroll area has to move for `field` to sit fully inside
/// `viewport`, or `None` when it already does. A field taller than the viewport /// `viewport`, or `None` when it already does. A field taller than the viewport
/// lines up with its top edge. /// lines up with its top edge.
@@ -2128,6 +2243,7 @@ mod tests {
// further down moves by the gap alone, not by the whole scroll. // further down moves by the gap alone, not by the whole scroll.
assert_eq!(reveal_offset(field(690.0), viewport, 500.0), None); assert_eq!(reveal_offset(field(690.0), viewport, 500.0), None);
assert_eq!(reveal_offset(field(990.0), viewport, 500.0), Some(32.0)); assert_eq!(reveal_offset(field(990.0), viewport, 500.0), Some(32.0));
assert_eq!(section_offset(field(990.0), viewport, 500.0), 390.0);
} }
#[test] #[test]

View File

@@ -312,13 +312,23 @@ fn update_runtime_config(runtime_config: &RwLock<Config>, config: &Config) {
} }
impl App { impl App {
pub(super) fn open_preferences(&mut self) { pub(super) fn open_preferences(&mut self) -> Task<Message> {
if let Some(id) = self.preferences_window {
return window::gain_focus(id);
}
if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder { if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder {
return; return Task::none();
} }
self.preference_draft = PreferenceDraft::from_saved(&self.config); self.preference_draft = PreferenceDraft::from_saved(&self.config);
self.preference_error = None; self.preference_error = None;
self.preferences_open = true; let (id, open) = window::open(window::Settings {
size: Size::new(920.0, 700.0),
min_size: Some(Size::new(720.0, 480.0)),
icon: Some(app_icon()),
..Default::default()
});
self.preferences_window = Some(id);
open.map(Message::PreferencesOpened)
} }
pub(super) fn save_preferences(&mut self) { pub(super) fn save_preferences(&mut self) {
@@ -413,7 +423,6 @@ impl App {
self._endpoint = Some(endpoint); self._endpoint = Some(endpoint);
} }
self.preference_draft = PreferenceDraft::from_saved(&self.config); self.preference_draft = PreferenceDraft::from_saved(&self.config);
self.preferences_open = false;
self.preference_error = None; self.preference_error = None;
self.error = None; self.error = None;
} }

View File

@@ -8,7 +8,8 @@ use model_manager::{download_status_bar, format_bytes, format_duration};
use super::{ use super::{
ActiveDownload, App, DetailTab, MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, Message, MetricsPoint, ActiveDownload, App, DetailTab, MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, Message, MetricsPoint,
ModelDownload, ModelOperation, chat_scroll_id, composer_id, models_path, preferences_scroll_id, ModelDownload, ModelOperation, PreferenceSection, chat_scroll_id, composer_id, models_path,
preferences_scroll_id,
}; };
use crate::database::{ProjectWithSessions, Session, SessionState}; use crate::database::{ProjectWithSessions, Session, SessionState};
use crate::model::{ use crate::model::{
@@ -53,7 +54,9 @@ const TRAFFIC_LIGHT_WIDTH: f32 = 78.0;
impl App { impl App {
pub(crate) fn view(&self, id: window::Id) -> Element<'_, Message> { pub(crate) fn view(&self, id: window::Id) -> Element<'_, Message> {
let content = if self.model_manager_window == Some(id) { let content = if self.preferences_window == Some(id) {
self.preferences_panel()
} else if self.model_manager_window == Some(id) {
self.model_manager() self.model_manager()
} else if self.help_window == Some(id) { } else if self.help_window == Some(id) {
self.help_view() self.help_view()
@@ -98,8 +101,7 @@ impl App {
/// Whether a dialog covers the window. Focus moves through the whole widget /// Whether a dialog covers the window. Focus moves through the whole widget
/// tree, so the layers below have to stay out of the dialog's field order. /// tree, so the layers below have to stay out of the dialog's field order.
pub(super) fn modal_open(&self) -> bool { pub(super) fn modal_open(&self) -> bool {
self.preferences_open self.pending_project_path.is_some()
|| self.pending_project_path.is_some()
|| self.pending_session_delete.is_some() || self.pending_session_delete.is_some()
|| self.session_rename.is_some() || self.session_rename.is_some()
|| self.menu_session().is_some() || self.menu_session().is_some()
@@ -161,8 +163,6 @@ impl App {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Some((prompt, _)) = &self.pending_tool_approval { if let Some((prompt, _)) = &self.pending_tool_approval {
layers.push(self.tool_approval_panel(prompt)); layers.push(self.tool_approval_panel(prompt));
} else if self.preferences_open {
layers.push(self.preferences_panel());
} else if let Some(path) = &self.pending_project_path { } else if let Some(path) = &self.pending_project_path {
layers.push(self.project_dialog(path)); layers.push(self.project_dialog(path));
} else if let Some((_, title)) = &self.session_rename { } else if let Some((_, title)) = &self.session_rename {
@@ -177,9 +177,7 @@ impl App {
layers.push(panel); layers.push(panel);
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
if self.preferences_open { if let Some(path) = &self.pending_project_path {
layers.push(self.preferences_panel());
} else if let Some(path) = &self.pending_project_path {
layers.push(self.project_dialog(path)); layers.push(self.project_dialog(path));
} else if let Some((_, title)) = &self.session_rename { } else if let Some((_, title)) = &self.session_rename {
layers.push(self.rename_dialog(title)); layers.push(self.rename_dialog(title));
@@ -780,10 +778,12 @@ fn hint<'a>(title: impl Into<Element<'a, Message>>, description: &'a str) -> Too
} }
fn preference_group<'a>( fn preference_group<'a>(
section: PreferenceSection,
title: &'a str, title: &'a str,
content: impl Into<Element<'a, Message>>, content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
container(column![text(title).size(11).color(muted_text()), content.into(),].spacing(10)) container(column![text(title).size(11).color(muted_text()), content.into(),].spacing(10))
.id(iced::widget::Id::new(section.anchor()))
.width(Length::Fill) .width(Length::Fill)
.padding(14) .padding(14)
.style(preference_group_style) .style(preference_group_style)

View File

@@ -81,6 +81,7 @@ impl App {
} }
let model_group = preference_group( let model_group = preference_group(
PreferenceSection::Model,
"MODEL & LIFECYCLE", "MODEL & LIFECYCLE",
column![ column![
hint( hint(
@@ -129,6 +130,7 @@ impl App {
.spacing(10), .spacing(10),
); );
let endpoint_group = preference_group( let endpoint_group = preference_group(
PreferenceSection::Endpoint,
"LOCAL ENDPOINT", "LOCAL ENDPOINT",
column![ column![
hint( hint(
@@ -155,6 +157,7 @@ impl App {
.spacing(10), .spacing(10),
); );
let generation_group = preference_group( let generation_group = preference_group(
PreferenceSection::Generation,
"GENERATION", "GENERATION",
column![ column![
preference_input_row( preference_input_row(
@@ -239,6 +242,7 @@ impl App {
.spacing(10), .spacing(10),
); );
let execution_group = preference_group( let execution_group = preference_group(
PreferenceSection::Execution,
"EXECUTION", "EXECUTION",
column![ column![
preference_input_row( preference_input_row(
@@ -295,6 +299,7 @@ impl App {
.spacing(10), .spacing(10),
); );
let acceleration_group = preference_group( let acceleration_group = preference_group(
PreferenceSection::Acceleration,
"ACCELERATION & MEMORY", "ACCELERATION & MEMORY",
column![ column![
text("SPECULATIVE DECODING").size(11).color(muted_text()), text("SPECULATIVE DECODING").size(11).color(muted_text()),
@@ -424,6 +429,7 @@ impl App {
.spacing(10), .spacing(10),
); );
let steering_group = preference_group( let steering_group = preference_group(
PreferenceSection::Steering,
"STEERING & DIAGNOSTICS", "STEERING & DIAGNOSTICS",
column![ column![
text("DIRECTIONAL STEERING").size(11).color(muted_text()), text("DIRECTIONAL STEERING").size(11).color(muted_text()),
@@ -493,6 +499,7 @@ impl App {
.spacing(10), .spacing(10),
); );
let kv_cache_group = preference_group( let kv_cache_group = preference_group(
PreferenceSection::KvCache,
"KV CACHE", "KV CACHE",
column![ column![
preference_input_row( preference_input_row(
@@ -556,45 +563,60 @@ impl App {
if let Some(error) = &self.preference_error { if let Some(error) = &self.preference_error {
fields = fields.push(text(error).style(iced::widget::text::danger)); fields = fields.push(text(error).style(iced::widget::text::danger));
} }
let header = row![ let navigation = PreferenceSection::ALL.iter().fold(
icon(ICON_SETTINGS, 22), column![
text("Preferences").size(24), row![icon(ICON_SETTINGS, 20), text("Preferences").size(20)]
Space::new().width(Length::Fill), .spacing(9)
text("⌘,").size(12), .align_y(Alignment::Center),
] text("Jump to section").size(12).color(muted_text()),
.spacing(10) rule::horizontal(1),
.align_y(Alignment::Center); ]
.spacing(8),
|navigation, section| {
navigation.push(
button(text(section.label()).size(13))
.on_press(Message::ScrollPreferences(*section))
.width(Length::Fill)
.padding([8, 10])
.style(button::text),
)
},
);
let footer = row![ let footer = row![
action_button("Reset DS4 defaults").on_press(Message::ResetPreferences), action_button("Reset DS4 defaults").on_press(Message::ResetPreferences),
Space::new().width(Length::Fill), Space::new().width(Length::Fill),
action_button("Cancel").on_press(Message::DismissPanel), action_button("Cancel").on_press(Message::ClosePreferences),
action_button("Save").on_press(Message::SavePreferences), action_button("Save").on_press(Message::SavePreferences),
] ]
.spacing(8); .spacing(8);
let panel = container( container(row![
column![ container(navigation)
header, .width(210)
scrollable(container(fields).padding(iced::Padding::ZERO.right(18))) .height(Length::Fill)
.id(preferences_scroll_id()) .padding(20)
.height(Length::Fill), .style(sidebar_style),
footer container(
] column![
.spacing(16), row![
) text("Settings").size(24),
.padding(24) Space::new().width(Length::Fill),
.width(700) text("⌘,").size(12).color(muted_text()),
]
.align_y(Alignment::Center),
scrollable(container(fields).padding(iced::Padding::ZERO.right(18)))
.id(preferences_scroll_id())
.height(Length::Fill),
footer,
]
.spacing(16),
)
.padding(24)
.width(Length::Fill)
.height(Length::Fill),
])
.width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.max_height(660) .into()
.style(overview_style);
opaque(
container(panel)
.padding(24)
.center_x(Length::Fill)
.center_y(Length::Fill)
.style(|_| {
container::Style::default().background(Color::from_rgba8(0, 0, 0, 0.68))
}),
)
} }
} }