Upgrade Iced and render Markdown tables

This commit is contained in:
Georg Bauer
2026-07-26 18:45:33 +02:00
parent 4420b81117
commit fd3f8e45dc
12 changed files with 932 additions and 1063 deletions

1618
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ cc = "1.3.0"
[dependencies] [dependencies]
diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] } diesel = { version = "2.3.11", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "64-column-tables"] }
diesel_migrations = "2.3.2" diesel_migrations = "2.3.2"
iced = { version = "0.13.1", features = ["advanced", "highlighter", "markdown", "svg", "tokio"] } iced = { version = "0.14.0", features = ["advanced", "highlighter", "markdown", "svg", "tokio"] }
memmap2 = "0.9.11" memmap2 = "0.9.11"
png = "0.17.16" png = "0.17.16"
rfd = "0.15.4" rfd = "0.15.4"
@@ -23,6 +23,7 @@ serde_json = { version = "1.0.149", features = ["preserve_order", "raw_value"] }
serde_norway = "0.9.42" serde_norway = "0.9.42"
sha2 = "0.11.0" sha2 = "0.11.0"
ureq = { version = "3.3.0", default-features = false, features = ["rustls"] } ureq = { version = "3.3.0", default-features = false, features = ["rustls"] }
url = "2.5.8"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
muda = "0.15.3" muda = "0.15.3"

View File

@@ -62,6 +62,9 @@ execution targets one self-contained Mac.
- The remaining model-independent execution gaps are fine-grained SSD cache - The remaining model-independent execution gaps are fine-grained SSD cache
telemetry, the DS4 expert-locality profiler, and resident multi-session telemetry, the DS4 expert-locality profiler, and resident multi-session
server batching/scheduling. server batching/scheduling.
- The native UI is on Iced 0.14. Chat transcripts use its table-aware Markdown
content and viewer path, with a regression for code-styled line-count tables
produced by coding models.
## Delivery order ## Delivery order

View File

@@ -25,7 +25,7 @@ use crate::settings::{
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences, ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
StreamingCacheBudget, StreamingCacheBudget,
}; };
use iced::widget::{markdown, scrollable, text_input}; use iced::widget::{markdown, scrollable};
use iced::{Size, Subscription, Task, keyboard, mouse, window}; use iced::{Size, Subscription, Task, keyboard, mouse, window};
use rfd::AsyncFileDialog; use rfd::AsyncFileDialog;
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
@@ -210,7 +210,7 @@ pub(crate) enum Message {
DownloadProgressTick, DownloadProgressTick,
ComposerChanged(String), ComposerChanged(String),
ToggleReasoning(usize), ToggleReasoning(usize),
OpenLink(markdown::Url), OpenLink(markdown::Uri),
CopyToolText(String), CopyToolText(String),
OpenToolOutput(PathBuf), OpenToolOutput(PathBuf),
AllowToolOnce, AllowToolOnce,
@@ -514,9 +514,11 @@ impl App {
self.error = None; self.error = None;
} }
} }
Message::FocusNext => return iced::widget::focus_next().chain(reveal_focused()), Message::FocusNext => {
return iced::widget::operation::focus_next().chain(reveal_focused());
}
Message::FocusPrevious => { Message::FocusPrevious => {
return iced::widget::focus_previous().chain(reveal_focused()); return iced::widget::operation::focus_previous().chain(reveal_focused());
} }
Message::ShowChat => self.detail_tab = DetailTab::Chat, Message::ShowChat => self.detail_tab = DetailTab::Chat,
Message::ShowStats => { Message::ShowStats => {
@@ -807,8 +809,9 @@ impl App {
message.reasoning_open = !message.reasoning_open; message.reasoning_open = !message.reasoning_open;
} }
} }
Message::OpenLink(url) => { Message::OpenLink(uri) => {
if matches!(url.scheme(), "http" | "https") if let Ok(url) = url::Url::parse(&uri)
&& matches!(url.scheme(), "http" | "https")
&& let Err(error) = std::process::Command::new("open").arg(url.as_str()).spawn() && let Err(error) = std::process::Command::new("open").arg(url.as_str()).spawn()
{ {
self.error = Some(format!("Could not open the link: {error}")); self.error = Some(format!("Could not open the link: {error}"));
@@ -1165,7 +1168,10 @@ impl App {
pub(crate) fn subscription(&self) -> Subscription<Message> { pub(crate) fn subscription(&self) -> Subscription<Message> {
let mut subscriptions = vec![ let mut subscriptions = vec![
keyboard::on_key_press(shortcut), keyboard::listen().filter_map(|event| match event {
keyboard::Event::KeyPressed { key, modifiers, .. } => shortcut(key, modifiers),
_ => None,
}),
// 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, _, _| {
@@ -1422,16 +1428,16 @@ fn config_path() -> PathBuf {
application_support_path().join("config.yaml") application_support_path().join("config.yaml")
} }
pub(super) fn chat_scroll_id() -> scrollable::Id { pub(super) fn chat_scroll_id() -> iced::widget::Id {
scrollable::Id::new("chat-transcript") iced::widget::Id::new("chat-transcript")
} }
pub(super) fn composer_id() -> text_input::Id { pub(super) fn composer_id() -> iced::widget::Id {
text_input::Id::new("chat-composer") iced::widget::Id::new("chat-composer")
} }
pub(super) fn preferences_scroll_id() -> scrollable::Id { pub(super) fn preferences_scroll_id() -> iced::widget::Id {
scrollable::Id::new("preferences-fields") iced::widget::Id::new("preferences-fields")
} }
/// 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
@@ -1447,24 +1453,34 @@ fn reveal_focused() -> Task<Message> {
struct Locate { struct Locate {
rows: Vec<Rectangle>, rows: Vec<Rectangle>,
next_container: Option<Rectangle>,
focused: Option<Rectangle>, focused: Option<Rectangle>,
} }
impl<T> Operation<T> for Locate { impl<T> Operation<T> for Locate {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
let container = self.next_container.take();
if let Some(bounds) = container {
self.rows.push(bounds);
}
operate(self);
if container.is_some() {
self.rows.pop();
}
}
fn container(&mut self, _id: Option<&Id>, bounds: Rectangle) {
self.next_container = Some(bounds);
}
fn focusable(
&mut self, &mut self,
_id: Option<&Id>, _id: Option<&Id>,
bounds: Rectangle, bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>), state: &mut dyn operation::Focusable,
) { ) {
self.rows.push(bounds);
operate_on_children(self);
self.rows.pop();
}
fn focusable(&mut self, state: &mut dyn operation::Focusable, _id: Option<&Id>) {
if state.is_focused() { if state.is_focused() {
self.focused = self.rows.last().copied(); self.focused = self.rows.last().copied().or(Some(bounds));
} }
} }
@@ -1480,24 +1496,19 @@ fn reveal_focused() -> Task<Message> {
} }
impl<T> Operation<T> for Reveal { impl<T> Operation<T> for Reveal {
fn container( fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
&mut self, operate(self);
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
} }
fn scrollable( fn scrollable(
&mut self, &mut self,
state: &mut dyn operation::Scrollable,
id: Option<&Id>, id: Option<&Id>,
bounds: Rectangle, bounds: Rectangle,
content_bounds: Rectangle, content_bounds: Rectangle,
translation: Vector, translation: Vector,
state: &mut dyn operation::Scrollable,
) { ) {
if id != Some(&Id::from(preferences_scroll_id())) { if id != Some(&preferences_scroll_id()) {
return; return;
} }
let Some(offset) = reveal_offset(self.field, bounds, translation.y) else { let Some(offset) = reveal_offset(self.field, bounds, translation.y) else {
@@ -1513,6 +1524,7 @@ fn reveal_focused() -> Task<Message> {
iced::advanced::widget::operate(Locate { iced::advanced::widget::operate(Locate {
rows: Vec::new(), rows: Vec::new(),
next_container: None,
focused: None, focused: None,
}) })
} }
@@ -1541,11 +1553,11 @@ fn reveal_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f3
} }
fn focus_composer() -> Task<Message> { fn focus_composer() -> Task<Message> {
text_input::focus(composer_id()) iced::widget::operation::focus(composer_id())
} }
fn scroll_chat_to_end() -> Task<Message> { fn scroll_chat_to_end() -> Task<Message> {
scrollable::snap_to(chat_scroll_id(), scrollable::RelativeOffset::END) iced::widget::operation::snap_to(chat_scroll_id(), scrollable::RelativeOffset::END)
} }
/// Deletes session checkpoints whose session is gone. Deleting a session or a /// Deletes session checkpoints whose session is gone. Deleting a session or a
@@ -1723,7 +1735,7 @@ mod tests {
reasoning_complete: false, reasoning_complete: false,
reasoning_open: true, reasoning_open: true,
content: String::new(), content: String::new(),
markdown: Vec::new(), markdown: markdown::Content::new(),
}; };
message.append(true, "working it out"); message.append(true, "working it out");
message.append(false, "**final answer**"); message.append(false, "**final answer**");
@@ -1731,6 +1743,6 @@ mod tests {
assert_eq!(message.reasoning.as_deref(), Some("working it out")); assert_eq!(message.reasoning.as_deref(), Some("working it out"));
assert!(message.reasoning_complete); assert!(message.reasoning_complete);
assert_eq!(message.content, "**final answer**"); assert_eq!(message.content, "**final answer**");
assert!(!message.markdown.is_empty()); assert!(!message.markdown.items().is_empty());
} }
} }

View File

@@ -47,7 +47,7 @@ pub(super) struct ToolResultCheck {
stage: ToolCheckStage, stage: ToolCheckStage,
} }
#[derive(Clone, Debug)] #[derive(Debug)]
pub(crate) struct ChatMessage { pub(crate) struct ChatMessage {
pub(super) id: i32, pub(super) id: i32,
pub(super) user: bool, pub(super) user: bool,
@@ -59,7 +59,7 @@ pub(crate) struct ChatMessage {
pub(super) reasoning_complete: bool, pub(super) reasoning_complete: bool,
pub(super) reasoning_open: bool, pub(super) reasoning_open: bool,
pub(super) content: String, pub(super) content: String,
pub(super) markdown: Vec<markdown::Item>, pub(super) markdown: markdown::Content,
} }
impl ChatMessage { impl ChatMessage {
@@ -80,7 +80,7 @@ impl ChatMessage {
} else { } else {
visible visible
}; };
self.markdown = markdown::parse(content).collect(); self.markdown = markdown::Content::parse(content);
} }
} }
} }
@@ -98,7 +98,7 @@ impl From<StoredMessage> for ChatMessage {
reasoning_complete: message.reasoning_complete, reasoning_complete: message.reasoning_complete,
reasoning_open: false, reasoning_open: false,
content: message.content, content: message.content,
markdown: Vec::new(), markdown: iced::widget::markdown::Content::new(),
}; };
message.refresh_markdown(); message.refresh_markdown();
message message
@@ -1349,6 +1349,37 @@ mod tests {
); );
} }
#[test]
fn assistant_markdown_keeps_llm_tables() {
let mut message = ChatMessage {
id: 1,
user: false,
tool: false,
system: false,
compaction: false,
compaction_tail_start: None,
reasoning: None,
reasoning_complete: true,
reasoning_open: false,
content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
.to_owned(),
markdown: iced::widget::markdown::Content::new(),
};
message.refresh_markdown();
let table = message.markdown.items().iter().find_map(|item| {
if let iced::widget::markdown::Item::Table { columns, rows } = item {
Some((columns, rows))
} else {
None
}
});
let (columns, rows) = table.expect("the completed Markdown table must remain renderable");
assert_eq!(columns.len(), 2);
assert_eq!(rows.len(), 2);
}
#[test] #[test]
fn last_compaction_selects_its_tail_without_hiding_history() { fn last_compaction_selects_its_tail_without_hiding_history() {
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage { let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
@@ -1362,7 +1393,7 @@ mod tests {
reasoning_complete: true, reasoning_complete: true,
reasoning_open: false, reasoning_open: false,
content: format!("message {id}"), content: format!("message {id}"),
markdown: Vec::new(), markdown: iced::widget::markdown::Content::new(),
}; };
let history = vec![ let history = vec![
message(1, false, None), message(1, false, None),
@@ -1394,7 +1425,7 @@ mod tests {
reasoning_complete: true, reasoning_complete: true,
reasoning_open: false, reasoning_open: false,
content: format!("message {id}"), content: format!("message {id}"),
markdown: Vec::new(), markdown: iced::widget::markdown::Content::new(),
}; };
let mut history = vec![ let mut history = vec![
message(1, true, false, false, false), message(1, true, false, false, false),

View File

@@ -16,9 +16,8 @@ use crate::model::{
use crate::settings::{GIB, REASONING_MODES}; use crate::settings::{GIB, REASONING_MODES};
use iced::theme::{Palette, palette}; use iced::theme::{Palette, palette};
use iced::widget::{ use iced::widget::{
Button, Space, Svg, Tooltip, button, checkbox, column, container, horizontal_rule, markdown, Button, Space, Svg, Tooltip, button, checkbox, column, container, markdown, mouse_area, opaque,
mouse_area, opaque, pick_list, progress_bar, row, scrollable, stack, svg, text, text_input, pick_list, progress_bar, row, rule, scrollable, stack, svg, text, text_input, tooltip,
tooltip,
}; };
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window}; use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -87,7 +86,7 @@ impl App {
body = body.push(self.sidebar()); body = body.push(self.sidebar());
body = body.push( body = body.push(
mouse_area( mouse_area(
container(Space::with_width(Length::Fill)) container(Space::new().width(Length::Fill))
.width(5) .width(5)
.height(Length::Fill) .height(Length::Fill)
.style(divider_style), .style(divider_style),
@@ -107,7 +106,7 @@ impl App {
// way to the top edge; the spacer below it lets clicks through. // way to the top edge; the spacer below it lets clicks through.
let mut shell = column![stack![ let mut shell = column![stack![
body, body,
column![self.title_bar(), Space::with_height(Length::Fill)], column![self.title_bar(), Space::new().height(Length::Fill)],
]]; ]];
if let ModelDownload::Active(download) = &self.model_download { if let ModelDownload::Active(download) = &self.model_download {
shell = shell.push(download_status_bar(download)); shell = shell.push(download_status_bar(download));
@@ -163,7 +162,7 @@ impl App {
text("Working directory").size(11).color(muted_text()), text("Working directory").size(11).color(muted_text()),
text(prompt.working_directory.display().to_string()).size(13), text(prompt.working_directory.display().to_string()).size(13),
row![ row![
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button("Deny").on_press(Message::DenyTool), action_button("Deny").on_press(Message::DenyTool),
action_button("Allow once").on_press(Message::AllowToolOnce), action_button("Allow once").on_press(Message::AllowToolOnce),
] ]
@@ -188,7 +187,7 @@ impl App {
let preference_content = row![ let preference_content = row![
icon(ICON_SETTINGS, 17), icon(ICON_SETTINGS, 17),
text("Preferences").size(14), text("Preferences").size(14),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text("⌘,").size(12), text("⌘,").size(12),
] ]
.spacing(9) .spacing(9)
@@ -212,14 +211,14 @@ impl App {
}; };
let header = row![ let header = row![
text("DS4Server").size(20), text("DS4Server").size(20),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text("Local").size(12), text("Local").size(12),
] ]
.align_y(Alignment::Center); .align_y(Alignment::Center);
let mut projects = column![ let mut projects = column![
row![ row![
text("PROJECTS").size(11), text("PROJECTS").size(11),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text("LOCAL").size(10), text("LOCAL").size(10),
], ],
add_project.style(button::text), add_project.style(button::text),
@@ -275,7 +274,7 @@ impl App {
let expanded = self.expanded_archives.contains(&project.id); let expanded = self.expanded_archives.contains(&project.id);
projects = projects.push( projects = projects.push(
row![ row![
Space::with_width(26), Space::new().width(26),
button( button(
text(format!( text(format!(
"{} Archived sessions ({})", "{} Archived sessions ({})",
@@ -302,7 +301,7 @@ impl App {
let draft_selected = self.draft_selected(project.id); let draft_selected = self.draft_selected(project.id);
projects = projects.push( projects = projects.push(
row![ row![
Space::with_width(26), Space::new().width(26),
button( button(
row![ row![
icon(ICON_CHAT, 15), icon(ICON_CHAT, 15),
@@ -363,7 +362,7 @@ impl App {
} }
label = label.push(text(&session.title).size(13)); label = label.push(text(&session.title).size(13));
row![ row![
Space::with_width(26), Space::new().width(26),
button(label) button(label)
.width(Length::Fill) .width(Length::Fill)
.on_press(Message::SelectSession(project_id, session.id)) .on_press(Message::SelectSession(project_id, session.id))
@@ -404,12 +403,12 @@ impl App {
}; };
stack![ stack![
row![ row![
Space::with_width(detail_offset), Space::new().width(detail_offset),
container(self.detail_tabs()) container(self.detail_tabs())
.center_x(Length::Fill) .center_x(Length::Fill)
.center_y(Length::Fill), .center_y(Length::Fill),
], ],
container(row![Space::with_width(TRAFFIC_LIGHT_WIDTH), toggle]).center_y(Length::Fill), container(row![Space::new().width(TRAFFIC_LIGHT_WIDTH), toggle]).center_y(Length::Fill),
] ]
.width(Length::Fill) .width(Length::Fill)
.height(TITLE_BAR_HEIGHT) .height(TITLE_BAR_HEIGHT)
@@ -463,7 +462,7 @@ impl App {
.on_submit(Message::ConfirmProject) .on_submit(Message::ConfirmProject)
.padding(10), .padding(10),
row![ row![
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button("Cancel").on_press(Message::CancelProject), action_button("Cancel").on_press(Message::CancelProject),
action_button("Add project").on_press(Message::ConfirmProject), action_button("Add project").on_press(Message::ConfirmProject),
] ]
@@ -547,7 +546,7 @@ impl App {
.color(muted_text()), .color(muted_text()),
actions, actions,
row![ row![
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button("Close").on_press(Message::DismissPanel), action_button("Close").on_press(Message::DismissPanel),
], ],
] ]
@@ -576,7 +575,7 @@ impl App {
.on_submit(Message::ConfirmRenameSession) .on_submit(Message::ConfirmRenameSession)
.padding(10), .padding(10),
row![ row![
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button("Cancel").on_press(Message::DismissPanel), action_button("Cancel").on_press(Message::DismissPanel),
action_button("Rename").on_press(Message::ConfirmRenameSession), action_button("Rename").on_press(Message::ConfirmRenameSession),
] ]
@@ -668,9 +667,10 @@ pub(crate) fn app_theme() -> Theme {
text: Color::from_rgb8(235, 235, 235), text: Color::from_rgb8(235, 235, 235),
primary: Color::from_rgb8(85, 118, 255), primary: Color::from_rgb8(85, 118, 255),
success: Color::from_rgb8(72, 176, 112), success: Color::from_rgb8(72, 176, 112),
warning: Color::from_rgb8(224, 168, 72),
danger: Color::from_rgb8(220, 80, 86), danger: Color::from_rgb8(220, 80, 86),
}; };
Theme::custom_with_fn("DS4Server".into(), palette, |palette| { Theme::custom_with_fn("DS4Server", palette, |palette| {
let mut extended = palette::Extended::generate(palette); let mut extended = palette::Extended::generate(palette);
extended.background.weak = palette::Pair::new(Color::from_rgb8(38, 38, 40), palette.text); extended.background.weak = palette::Pair::new(Color::from_rgb8(38, 38, 40), palette.text);
extended.background.strong = palette::Pair::new(Color::from_rgb8(58, 58, 61), palette.text); extended.background.strong = palette::Pair::new(Color::from_rgb8(58, 58, 61), palette.text);
@@ -714,7 +714,7 @@ fn metric_card(label: &'static str, value: String, detail: String) -> Element<'s
fn metric_row(label: &'static str, value: impl ToString) -> Element<'static, Message> { fn metric_row(label: &'static str, value: impl ToString) -> Element<'static, Message> {
row![ row![
text(label).size(12).color(muted_text()), text(label).size(12).color(muted_text()),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(value.to_string()).size(12), text(value.to_string()).size(12),
] ]
.spacing(12) .spacing(12)
@@ -750,7 +750,7 @@ fn mini_chart(
1.0 1.0
}; };
bars = bars.push( bars = bars.push(
container(Space::new(Length::Fill, height)) container(Space::new().width(Length::Fill).height(height))
.width(Length::FillPortion(1)) .width(Length::FillPortion(1))
.style(move |_| chart_bar_style(color)), .style(move |_| chart_bar_style(color)),
); );

View File

@@ -17,7 +17,7 @@ impl App {
icon(ICON_SPARK, 36), icon(ICON_SPARK, 36),
text("Start a local coding session").size(28), text("Start a local coding session").size(28),
text("Choose a project folder to create your first session.").size(14), text("Choose a project folder to create your first session.").size(14),
Space::with_height(10), Space::new().height(10),
open_project, open_project,
] ]
.spacing(10) .spacing(10)
@@ -40,7 +40,7 @@ impl App {
); );
} }
let header = header let header = header
.push(Space::with_width(Length::Fill)) .push(Space::new().width(Length::Fill))
.spacing(10) .spacing(10)
.align_y(Alignment::Center); .align_y(Alignment::Center);
@@ -124,7 +124,7 @@ impl App {
} }
} }
if !message.content.is_empty() { if !message.content.is_empty() {
if message.user || message.tool || message.markdown.is_empty() { if message.user || message.tool || message.markdown.items().is_empty() {
let content = if message.reasoning.is_some() { let content = if message.reasoning.is_some() {
crate::agent::visible_content(&message.content).trim_start() crate::agent::visible_content(&message.content).trim_start()
} else { } else {
@@ -134,9 +134,8 @@ impl App {
} else { } else {
body = body.push( body = body.push(
markdown::view( markdown::view(
&message.markdown, message.markdown.items(),
markdown::Settings::with_text_size(14), markdown::Settings::with_text_size(14, markdown_style),
markdown_style,
) )
.map(Message::OpenLink), .map(Message::OpenLink),
); );
@@ -283,7 +282,7 @@ impl App {
)) ))
.size(11) .size(11)
.color(muted_text()), .color(muted_text()),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
icon(ICON_MODEL, 16), icon(ICON_MODEL, 16),
text(self.config.model.to_string()).size(12), text(self.config.model.to_string()).size(12),
action, action,
@@ -339,7 +338,7 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
let mut rows = column![].spacing(0); let mut rows = column![].spacing(0);
for (index, card) in cards.into_iter().enumerate() { for (index, card) in cards.into_iter().enumerate() {
if index > 0 { if index > 0 {
rows = rows.push(horizontal_rule(1)); rows = rows.push(rule::horizontal(1));
} }
let parameters = crate::agent::tool_parameters(&card.call); let parameters = crate::agent::tool_parameters(&card.call);
let call = crate::agent::tool_call_text(&card.call); let call = crate::agent::tool_call_text(&card.call);
@@ -391,7 +390,7 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
let mut content = column![ let mut content = column![
row![ row![
text(card.call.name).size(13), text(card.call.name).size(13),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(card.state.label()).size(11).color(muted_text()), text(card.state.label()).size(11).color(muted_text()),
actions, actions,
] ]

View File

@@ -7,7 +7,7 @@ impl App {
let mut artifacts = column![]; let mut artifacts = column![];
for (index, artifact) in model::managed_artifacts(&models_path()).iter().enumerate() { for (index, artifact) in model::managed_artifacts(&models_path()).iter().enumerate() {
if index > 0 { if index > 0 {
artifacts = artifacts.push(horizontal_rule(1)); artifacts = artifacts.push(rule::horizontal(1));
} }
artifacts = artifacts.push(model_artifact_row(artifact, busy)); artifacts = artifacts.push(model_artifact_row(artifact, busy));
} }
@@ -29,7 +29,7 @@ impl App {
if let Some(error) = &self.error { if let Some(error) = &self.error {
content = content.push(text(error).style(iced::widget::text::danger)); content = content.push(text(error).style(iced::widget::text::danger));
} }
content = content.push(Space::with_height(8)).push( content = content.push(Space::new().height(8)).push(
container(scrollable(artifacts).height(Length::Fill)) container(scrollable(artifacts).height(Length::Fill))
.height(Length::Fill) .height(Length::Fill)
.style(overview_style), .style(overview_style),
@@ -51,7 +51,7 @@ impl App {
)) ))
.size(13), .size(13),
row![ row![
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button("Cancel").on_press(Message::CancelDeleteArtifact), action_button("Cancel").on_press(Message::CancelDeleteArtifact),
danger_button("Delete").on_press(Message::ConfirmDeleteArtifact), danger_button("Delete").on_press(Message::ConfirmDeleteArtifact),
] ]
@@ -119,7 +119,7 @@ fn model_artifact_row(artifact: &ManagedArtifact, busy: bool) -> Element<'static
.color(muted_text()), .color(muted_text()),
] ]
.spacing(5), .spacing(5),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
row![download, validate, delete] row![download, validate, delete]
.spacing(8) .spacing(8)
.align_y(Alignment::Center), .align_y(Alignment::Center),
@@ -167,11 +167,11 @@ pub(super) fn download_status_bar(download: &ActiveDownload) -> Element<'_, Mess
row![ row![
text(phase_text(progress.phase)).size(12), text(phase_text(progress.phase)).size(12),
progress_bar(0.0..=1.0, progress.fraction()) progress_bar(0.0..=1.0, progress.fraction())
.width(180) .length(180)
.height(7), .girth(7),
text(measurement).size(12), text(measurement).size(12),
text(transfer).size(12), text(transfer).size(12),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
stop, stop,
] ]
.spacing(12) .spacing(12)
@@ -234,7 +234,7 @@ fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
} else { } else {
text(heading).size(12) text(heading).size(12)
}; };
let mut heading_row = row![heading, Space::with_width(Length::Fill)] let mut heading_row = row![heading, Space::new().width(Length::Fill)]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.spacing(8); .spacing(8);
if let ModelDownload::Active(active) = download { if let ModelDownload::Active(active) = download {
@@ -246,7 +246,7 @@ fn model_download_status(download: &ModelDownload) -> Element<'_, Message> {
} }
let mut status = column![ let mut status = column![
heading_row, heading_row,
progress_bar(0.0..=1.0, progress.fraction()).height(8), progress_bar(0.0..=1.0, progress.fraction()).girth(8),
text(measurement).size(12), text(measurement).size(12),
] ]
.spacing(6); .spacing(6);

View File

@@ -9,10 +9,8 @@ impl App {
.supports_dspark() .supports_dspark()
.then_some(Message::PreferenceLegacyMtpChanged); .then_some(Message::PreferenceLegacyMtpChanged);
let legacy_mtp = hint( let legacy_mtp = hint(
checkbox( checkbox(self.preference_draft.legacy_mtp_enabled)
"Enable legacy MTP for this model", .label("Enable legacy MTP for this model")
self.preference_draft.legacy_mtp_enabled,
)
.on_toggle_maybe(legacy_mtp_toggle), .on_toggle_maybe(legacy_mtp_toggle),
"Uses the managed one-stage MTP support GGUF. The target model verifies every drafted token; it is mutually exclusive with DSpark.", "Uses the managed one-stage MTP support GGUF. The target model verifies every drafted token; it is mutually exclusive with DSpark.",
); );
@@ -22,10 +20,8 @@ impl App {
.supports_dspark() .supports_dspark()
.then_some(Message::PreferenceDsparkChanged); .then_some(Message::PreferenceDsparkChanged);
let dspark = hint( let dspark = hint(
checkbox( checkbox(self.preference_draft.dspark_enabled)
"Enable DSpark for this model", .label("Enable DSpark for this model")
self.preference_draft.dspark_enabled,
)
.on_toggle_maybe(dspark_toggle), .on_toggle_maybe(dspark_toggle),
"Speculative decoding with the managed DSpark draft artifact: a small model proposes tokens that the main model verifies in one pass. Usually a large speedup; the target model may also stream routed experts from SSD.", "Speculative decoding with the managed DSpark draft artifact: a small model proposes tokens that the main model verifies in one pass. Usually a large speedup; the target model may also stream routed experts from SSD.",
); );
@@ -130,10 +126,8 @@ impl App {
"LOCAL ENDPOINT", "LOCAL ENDPOINT",
column![ column![
hint( hint(
checkbox( checkbox(self.preference_draft.endpoint_enabled)
"Enable OpenAI-compatible endpoint", .label("Enable OpenAI-compatible endpoint")
self.preference_draft.endpoint_enabled,
)
.on_toggle(Message::PreferenceEndpointEnabledChanged), .on_toggle(Message::PreferenceEndpointEnabledChanged),
"Serves the loaded model over an OpenAI-style HTTP API, so editors, scripts and agents on this machine can use it. Turned off, only this window can generate.", "Serves the loaded model over an OpenAI-style HTTP API, so editors, scripts and agents on this machine can use it. Turned off, only this window can generate.",
), ),
@@ -144,10 +138,8 @@ impl App {
.on_input(Message::PreferenceEndpointPortChanged), .on_input(Message::PreferenceEndpointPortChanged),
), ),
hint( hint(
checkbox( checkbox(self.preference_draft.endpoint_cors)
"Allow browser clients (CORS)", .label("Allow browser clients (CORS)")
self.preference_draft.endpoint_cors,
)
.on_toggle(Message::PreferenceEndpointCorsChanged), .on_toggle(Message::PreferenceEndpointCorsChanged),
"Answers with permissive CORS headers so JavaScript running in a web page may call the endpoint. Leave it off when only native tools connect.", "Answers with permissive CORS headers so JavaScript running in a web page may call the endpoint. Leave it off when only native tools connect.",
), ),
@@ -181,7 +173,7 @@ impl App {
) )
.on_input(Message::PreferenceSystemPromptChanged) .on_input(Message::PreferenceSystemPromptChanged)
.padding(9), .padding(9),
Space::with_height(4), Space::new().height(4),
text("SAMPLING & REASONING").size(11).color(muted_text()), text("SAMPLING & REASONING").size(11).color(muted_text()),
preference_input_row( preference_input_row(
"Temperature", "Temperature",
@@ -261,12 +253,14 @@ impl App {
prefill, prefill,
), ),
hint( hint(
checkbox("Prefer exact quality kernels", self.preference_draft.quality) checkbox(self.preference_draft.quality)
.label("Prefer exact quality kernels")
.on_toggle(Message::PreferenceQualityChanged), .on_toggle(Message::PreferenceQualityChanged),
"Runs the exact Metal kernels instead of the fast approximations. Slightly slower, and it removes the small numeric differences those approximations introduce.", "Runs the exact Metal kernels instead of the fast approximations. Slightly slower, and it removes the small numeric differences those approximations introduce.",
), ),
hint( hint(
checkbox("Warm mapped weights at load time", self.preference_draft.warm_weights) checkbox(self.preference_draft.warm_weights)
.label("Warm mapped weights at load time")
.on_toggle(Message::PreferenceWarmWeightsChanged), .on_toggle(Message::PreferenceWarmWeightsChanged),
"Reads every mapped weight page once at load, so the first reply is not interrupted by page faults from disk. Loading takes longer and memory pressure rises immediately.", "Reads every mapped weight page once at load, so the first reply is not interrupted by page faults from disk. Loading takes longer and memory pressure rises immediately.",
), ),
@@ -312,15 +306,14 @@ impl App {
.on_input(Message::PreferenceMtpMarginChanged), .on_input(Message::PreferenceMtpMarginChanged),
), ),
hint( hint(
checkbox("Enable integrated GLM MTP", self.preference_draft.glm_mtp) checkbox(self.preference_draft.glm_mtp)
.label("Enable integrated GLM MTP")
.on_toggle_maybe(glm_mtp_toggle), .on_toggle_maybe(glm_mtp_toggle),
"Uses the prediction head built into GLM 5.2 for speculative decoding, so no separate draft model is loaded. Available for GLM 5.2 only.", "Uses the prediction head built into GLM 5.2 for speculative decoding, so no separate draft model is loaded. Available for GLM 5.2 only.",
), ),
hint( hint(
checkbox( checkbox(self.preference_draft.glm_mtp_timing)
"Log GLM MTP timing counters", .label("Log GLM MTP timing counters")
self.preference_draft.glm_mtp_timing,
)
.on_toggle_maybe(glm_mtp_timing_toggle), .on_toggle_maybe(glm_mtp_timing_toggle),
"Records per-stage timings of the speculative path to the log, to show where the acceleration actually goes. A diagnostic aid that costs a little throughput.", "Records per-stage timings of the speculative path to the log, to show where the acceleration actually goes. A diagnostic aid that costs a little throughput.",
), ),
@@ -332,10 +325,8 @@ impl App {
dspark_confidence, dspark_confidence,
), ),
hint( hint(
checkbox( checkbox(self.preference_draft.dspark_strict)
"DSpark target-only decode", .label("DSpark target-only decode")
self.preference_draft.dspark_strict,
)
.on_toggle_maybe(dspark_strict_toggle), .on_toggle_maybe(dspark_strict_toggle),
"Lets the draft model only propose, never decide: every token is sampled by the full model. Gives up some of the speedup in exchange for output identical to non-speculative decoding.", "Lets the draft model only propose, never decide: every token is sampled by the full model. Gives up some of the speedup in exchange for output identical to non-speculative decoding.",
), ),
@@ -367,15 +358,17 @@ impl App {
}, },
)) ))
.size(12), .size(12),
Space::with_height(6), Space::new().height(6),
text("SSD STREAMING").size(11).color(muted_text()), text("SSD STREAMING").size(11).color(muted_text()),
hint( hint(
checkbox("Enable SSD-backed model streaming", self.preference_draft.ssd_streaming) checkbox(self.preference_draft.ssd_streaming)
.label("Enable SSD-backed model streaming")
.on_toggle(Message::PreferenceSsdChanged), .on_toggle(Message::PreferenceSsdChanged),
"Leaves the routed expert weights on disk and pages them in as they are needed, so a model larger than this machine's memory still runs. Every cache miss waits for the SSD; speculative support weights remain resident while target experts stream.", "Leaves the routed expert weights on disk and pages them in as they are needed, so a model larger than this machine's memory still runs. Every cache miss waits for the SSD; speculative support weights remain resident while target experts stream.",
), ),
hint( hint(
checkbox("Skip automatic expert preload", self.preference_draft.ssd_streaming_cold) checkbox(self.preference_draft.ssd_streaming_cold)
.label("Skip automatic expert preload")
.on_toggle(Message::PreferenceSsdColdChanged), .on_toggle(Message::PreferenceSsdColdChanged),
"Starts with an empty expert cache instead of reading the likely experts up front. The model is ready sooner and uses less memory, at the price of slow first replies.", "Starts with an empty expert cache instead of reading the likely experts up front. The model is ready sooner and uses less memory, at the price of slow first replies.",
), ),
@@ -461,7 +454,7 @@ impl App {
), ),
)) ))
.size(12), .size(12),
Space::with_height(6), Space::new().height(6),
text("ADVANCED DIAGNOSTICS").size(11).color(muted_text()), text("ADVANCED DIAGNOSTICS").size(11).color(muted_text()),
preference_input_row( preference_input_row(
"Simulated used memory (GiB)", "Simulated used memory (GiB)",
@@ -561,14 +554,14 @@ impl App {
let header = row![ let header = row![
icon(ICON_SETTINGS, 22), icon(ICON_SETTINGS, 22),
text("Preferences").size(24), text("Preferences").size(24),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text("⌘,").size(12), text("⌘,").size(12),
] ]
.spacing(10) .spacing(10)
.align_y(Alignment::Center); .align_y(Alignment::Center);
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::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button("Cancel").on_press(Message::DismissPanel), action_button("Cancel").on_press(Message::DismissPanel),
action_button("Save").on_press(Message::SavePreferences), action_button("Save").on_press(Message::SavePreferences),
] ]

View File

@@ -47,7 +47,7 @@ impl App {
.color(muted_text()), .color(muted_text()),
] ]
.spacing(5), .spacing(5),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
container(text(stats.phase.label()).size(12).color(phase_color)) container(text(stats.phase.label()).size(12).color(phase_color))
.padding([7, 11]) .padding([7, 11])
.style(move |_| status_badge_style(phase_color)), .style(move |_| status_badge_style(phase_color)),
@@ -109,7 +109,7 @@ impl App {
text("Decode") text("Decode")
.size(12) .size(12)
.color(Color::from_rgb8(84, 170, 255)), .color(Color::from_rgb8(84, 170, 255)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!("{:.1} tok/s", stats.decode_tokens_per_second)) text(format!("{:.1} tok/s", stats.decode_tokens_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -123,7 +123,7 @@ impl App {
text("Prefill") text("Prefill")
.size(12) .size(12)
.color(Color::from_rgb8(157, 119, 255)), .color(Color::from_rgb8(157, 119, 255)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!("{:.1} tok/s", stats.prefill_tokens_per_second)) text(format!("{:.1} tok/s", stats.prefill_tokens_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -142,7 +142,7 @@ impl App {
), ),
row![ row![
text(format!("{} requests", format_count(stats.http_requests))).size(12), text(format!("{} requests", format_count(stats.http_requests))).size(12),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!( text(format!(
"{} errors · {} streaming", "{} errors · {} streaming",
stats.http_errors, stats.http_streaming_requests stats.http_errors, stats.http_streaming_requests
@@ -171,7 +171,7 @@ impl App {
}) })
.size(12) .size(12)
.color(Color::from_rgb8(67, 194, 203)), .color(Color::from_rgb8(67, 194, 203)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format_rate(latest.kv_read_bytes_per_second)) text(format_rate(latest.kv_read_bytes_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -189,7 +189,7 @@ impl App {
}) })
.size(12) .size(12)
.color(Color::from_rgb8(240, 180, 70)), .color(Color::from_rgb8(240, 180, 70)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format_rate(latest.kv_write_bytes_per_second)) text(format_rate(latest.kv_write_bytes_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -210,7 +210,7 @@ impl App {
text("Selected loads") text("Selected loads")
.size(12) .size(12)
.color(Color::from_rgb8(240, 180, 70)), .color(Color::from_rgb8(240, 180, 70)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!("{:.1}/s", latest.ssd_requests_per_second)) text(format!("{:.1}/s", latest.ssd_requests_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -224,7 +224,7 @@ impl App {
text("Requested expert data") text("Requested expert data")
.size(12) .size(12)
.color(Color::from_rgb8(67, 194, 203)), .color(Color::from_rgb8(67, 194, 203)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format_rate(latest.ssd_bytes_per_second)) text(format_rate(latest.ssd_bytes_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -238,7 +238,7 @@ impl App {
text("Inference wait") text("Inference wait")
.size(12) .size(12)
.color(Color::from_rgb8(220, 80, 86)), .color(Color::from_rgb8(220, 80, 86)),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!("{:.0} ms/s", latest.ssd_wait_ms_per_second)) text(format!("{:.0} ms/s", latest.ssd_wait_ms_per_second))
.size(12) .size(12)
.color(muted_text()), .color(muted_text()),
@@ -443,7 +443,7 @@ impl App {
format_milliseconds(stats.last_kv_write_ms), format_milliseconds(stats.last_kv_write_ms),
), ),
), ),
progress_bar(0.0..=1.0, cache_fraction.min(1.0)).height(4), progress_bar(0.0..=1.0, cache_fraction.min(1.0)).girth(4),
] ]
.spacing(9) .spacing(9)
.into(), .into(),
@@ -522,15 +522,16 @@ impl App {
continue; continue;
} }
bar = bar.push( bar = bar.push(
container(Space::new(Length::Fill, Length::Fill)) container(Space::new().width(Length::Fill).height(Length::Fill))
.width(Length::FillPortion(portion(bytes, capacity))) .width(Length::FillPortion(portion(bytes, capacity)))
.style(move |_| chart_bar_style(color)), .style(move |_| chart_bar_style(color)),
); );
legend = legend.push( legend = legend.push(
row![ row![
container(Space::new(9, 9)).style(move |_| chart_bar_style(color)), container(Space::new().width(9).height(9))
.style(move |_| chart_bar_style(color)),
text(label).size(12).color(muted_text()), text(label).size(12).color(muted_text()),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!( text(format!(
"{} · {:.0}%", "{} · {:.0}%",
format_bytes(bytes), format_bytes(bytes),
@@ -544,7 +545,7 @@ impl App {
} }
if total == 0 { if total == 0 {
bar = bar.push( bar = bar.push(
container(Space::new(Length::Fill, Length::Fill)) container(Space::new().width(Length::Fill).height(Length::Fill))
.style(|_| chart_bar_style(muted_text().scale_alpha(0.25))), .style(|_| chart_bar_style(muted_text().scale_alpha(0.25))),
); );
} }
@@ -562,7 +563,7 @@ impl App {
rows = rows.push( rows = rows.push(
row![ row![
text(label).size(12), text(label).size(12),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
text(format!( text(format!(
"{} · {} old", "{} · {} old",
format_bytes(entry.bytes), format_bytes(entry.bytes),
@@ -607,7 +608,7 @@ impl App {
usage.transient_bytes as f32 / usage.budget_bytes.max(1) as f32 * 100.0, usage.transient_bytes as f32 / usage.budget_bytes.max(1) as f32 * 100.0,
), ),
), ),
Space::with_height(2), Space::new().height(2),
text(if usage.entries.is_empty() { text(if usage.entries.is_empty() {
"No checkpoints stored yet." "No checkpoints stored yet."
} else { } else {
@@ -620,7 +621,7 @@ impl App {
text("Discarding a checkpoint costs one prefill to rebuild it.") text("Discarding a checkpoint costs one prefill to rebuild it.")
.size(11) .size(11)
.color(muted_text()), .color(muted_text()),
Space::with_width(Length::Fill), Space::new().width(Length::Fill),
action_button(text("Clear transient cache").size(12)) action_button(text("Clear transient cache").size(12))
.on_press(Message::ClearTransientCache), .on_press(Message::ClearTransientCache),
] ]

View File

@@ -25,10 +25,8 @@ fn main() -> iced::Result {
if let Err(error) = engine::configure_metal_sources() { if let Err(error) = engine::configure_metal_sources() {
eprintln!("DS4Server: {error}"); eprintln!("DS4Server: {error}");
} }
iced::daemon(App::title, App::update, App::view) iced::daemon(
.subscription(App::subscription) || {
.theme(|_, _| app_theme())
.run_with(|| {
let (main_window, open) = window::open(window::Settings { let (main_window, open) = window::open(window::Settings {
size: Size::new(1120.0, 720.0), size: Size::new(1120.0, 720.0),
min_size: Some(Size::new(760.0, 480.0)), min_size: Some(Size::new(760.0, 480.0)),
@@ -44,5 +42,12 @@ fn main() -> iced::Result {
..Default::default() ..Default::default()
}); });
(App::load(main_window), open.map(Message::WindowOpened)) (App::load(main_window), open.map(Message::WindowOpened))
}) },
App::update,
App::view,
)
.title(App::title)
.subscription(App::subscription)
.theme(app_theme())
.run()
} }

View File

@@ -6,7 +6,6 @@ use iced::advanced::overlay;
use iced::advanced::renderer; use iced::advanced::renderer;
use iced::advanced::widget::{Operation, Tree, tree}; use iced::advanced::widget::{Operation, Tree, tree};
use iced::advanced::{Clipboard, Layout, Shell, Widget}; use iced::advanced::{Clipboard, Layout, Shell, Widget};
use iced::event;
use iced::keyboard::{self, Key, Location, Modifiers, key}; use iced::keyboard::{self, Key, Location, Modifiers, key};
use iced::mouse; use iced::mouse;
use iced::{Element, Event, Length, Rectangle, Size, Vector}; use iced::{Element, Event, Length, Rectangle, Size, Vector};
@@ -95,9 +94,12 @@ fn command_events(command: EditCommand) -> [Event; 4] {
location: Location::Standard, location: Location::Standard,
modifiers, modifiers,
text: None, text: None,
repeat: false,
}), }),
Event::Keyboard(keyboard::Event::KeyReleased { Event::Keyboard(keyboard::Event::KeyReleased {
key, key,
modified_key: Key::Character(modified_character.into()),
physical_key: key::Physical::Code(physical_key),
location: Location::Standard, location: Location::Standard,
modifiers, modifiers,
}), }),
@@ -131,67 +133,63 @@ where
} }
fn layout( fn layout(
&self, &mut self,
tree: &mut Tree, tree: &mut Tree,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
self.content self.content
.as_widget() .as_widget_mut()
.layout(&mut tree.children[0], renderer, limits) .layout(&mut tree.children[0], renderer, limits)
} }
fn operate( fn operate(
&self, &mut self,
tree: &mut Tree, tree: &mut Tree,
layout: Layout<'_>, layout: Layout<'_>,
renderer: &Renderer, renderer: &Renderer,
operation: &mut dyn Operation, operation: &mut dyn Operation,
) { ) {
self.content self.content
.as_widget() .as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation); .operate(&mut tree.children[0], layout, renderer, operation);
} }
fn on_event( fn update(
&mut self, &mut self,
tree: &mut Tree, tree: &mut Tree,
event: Event, event: &Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
renderer: &Renderer, renderer: &Renderer,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
viewport: &Rectangle, viewport: &Rectangle,
) -> event::Status { ) {
let mut status = event::Status::Ignored;
if matches!( if matches!(
event, event,
Event::Window(iced::window::Event::RedrawRequested(_)) Event::Window(iced::window::Event::RedrawRequested(_))
) { ) {
while let Some(command) = pop_command(&self.commands) { while let Some(command) = pop_command(&self.commands) {
for command_event in command_events(command) { for command_event in command_events(command) {
if self.content.as_widget_mut().on_event( self.content.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
command_event, &command_event,
layout, layout,
cursor, cursor,
renderer, renderer,
clipboard, clipboard,
shell, shell,
viewport, viewport,
) == event::Status::Captured );
{
status = event::Status::Captured;
}
} }
} }
} }
if let Some(sync_event) = modifier_sync_event(&event) { if let Some(sync_event) = modifier_sync_event(event) {
let _ = self.content.as_widget_mut().on_event( self.content.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
sync_event, &sync_event,
layout, layout,
cursor, cursor,
renderer, renderer,
@@ -201,7 +199,7 @@ where
); );
} }
if self.content.as_widget_mut().on_event( self.content.as_widget_mut().update(
&mut tree.children[0], &mut tree.children[0],
event, event,
layout, layout,
@@ -210,12 +208,7 @@ where
clipboard, clipboard,
shell, shell,
viewport, viewport,
) == event::Status::Captured );
{
event::Status::Captured
} else {
status
}
} }
fn mouse_interaction( fn mouse_interaction(
@@ -259,13 +252,18 @@ where
fn overlay<'b>( fn overlay<'b>(
&'b mut self, &'b mut self,
tree: &'b mut Tree, tree: &'b mut Tree,
layout: Layout<'_>, layout: Layout<'b>,
renderer: &Renderer, renderer: &Renderer,
viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content self.content.as_widget_mut().overlay(
.as_widget_mut() &mut tree.children[0],
.overlay(&mut tree.children[0], layout, renderer, translation) layout,
renderer,
viewport,
translation,
)
} }
} }