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

View File

@@ -25,7 +25,7 @@ use crate::settings::{
ReasoningMode, RuntimePreferences, SpeculativePreferences, SsdPreferences, SteeringPreferences,
StreamingCacheBudget,
};
use iced::widget::{markdown, scrollable, text_input};
use iced::widget::{markdown, scrollable};
use iced::{Size, Subscription, Task, keyboard, mouse, window};
use rfd::AsyncFileDialog;
use std::collections::{HashMap, HashSet, VecDeque};
@@ -210,7 +210,7 @@ pub(crate) enum Message {
DownloadProgressTick,
ComposerChanged(String),
ToggleReasoning(usize),
OpenLink(markdown::Url),
OpenLink(markdown::Uri),
CopyToolText(String),
OpenToolOutput(PathBuf),
AllowToolOnce,
@@ -514,9 +514,11 @@ impl App {
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 => {
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::ShowStats => {
@@ -807,8 +809,9 @@ impl App {
message.reasoning_open = !message.reasoning_open;
}
}
Message::OpenLink(url) => {
if matches!(url.scheme(), "http" | "https")
Message::OpenLink(uri) => {
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()
{
self.error = Some(format!("Could not open the link: {error}"));
@@ -1165,7 +1168,10 @@ impl App {
pub(crate) fn subscription(&self) -> Subscription<Message> {
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
// focus, so a dialog would never see it through `on_key_press`.
iced::event::listen_with(|event, _, _| {
@@ -1422,16 +1428,16 @@ fn config_path() -> PathBuf {
application_support_path().join("config.yaml")
}
pub(super) fn chat_scroll_id() -> scrollable::Id {
scrollable::Id::new("chat-transcript")
pub(super) fn chat_scroll_id() -> iced::widget::Id {
iced::widget::Id::new("chat-transcript")
}
pub(super) fn composer_id() -> text_input::Id {
text_input::Id::new("chat-composer")
pub(super) fn composer_id() -> iced::widget::Id {
iced::widget::Id::new("chat-composer")
}
pub(super) fn preferences_scroll_id() -> scrollable::Id {
scrollable::Id::new("preferences-fields")
pub(super) fn preferences_scroll_id() -> iced::widget::Id {
iced::widget::Id::new("preferences-fields")
}
/// Scrolls the preferences form so the field that just took focus is inside
@@ -1447,24 +1453,34 @@ fn reveal_focused() -> Task<Message> {
struct Locate {
rows: Vec<Rectangle>,
next_container: Option<Rectangle>,
focused: Option<Rectangle>,
}
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,
_id: Option<&Id>,
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() {
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 {
fn container(
&mut self,
_id: Option<&Id>,
_bounds: Rectangle,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
operate_on_children(self);
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn Operation<T>)) {
operate(self);
}
fn scrollable(
&mut self,
state: &mut dyn operation::Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
state: &mut dyn operation::Scrollable,
) {
if id != Some(&Id::from(preferences_scroll_id())) {
if id != Some(&preferences_scroll_id()) {
return;
}
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 {
rows: Vec::new(),
next_container: None,
focused: None,
})
}
@@ -1541,11 +1553,11 @@ fn reveal_offset(field: iced::Rectangle, viewport: iced::Rectangle, scrolled: f3
}
fn focus_composer() -> Task<Message> {
text_input::focus(composer_id())
iced::widget::operation::focus(composer_id())
}
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
@@ -1723,7 +1735,7 @@ mod tests {
reasoning_complete: false,
reasoning_open: true,
content: String::new(),
markdown: Vec::new(),
markdown: markdown::Content::new(),
};
message.append(true, "working it out");
message.append(false, "**final answer**");
@@ -1731,6 +1743,6 @@ mod tests {
assert_eq!(message.reasoning.as_deref(), Some("working it out"));
assert!(message.reasoning_complete);
assert_eq!(message.content, "**final answer**");
assert!(!message.markdown.is_empty());
assert!(!message.markdown.items().is_empty());
}
}