Upgrade Iced and render Markdown tables
This commit is contained in:
82
src/app.rs
82
src/app.rs
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user