Add preference section navigation
This commit is contained in:
156
src/app.rs
156
src/app.rs
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user