fix(ui): stabilize command history layout

Closes #555
This commit is contained in:
Hakan Seven 2026-07-29 22:53:59 +03:00
commit f99337cb9a
2 changed files with 47 additions and 24 deletions

View file

@ -93,6 +93,20 @@ mod util;
mod viewport;
impl OpenCADStudio {
fn sync_open_command_history(&mut self) {
if !self.command_line.history_open {
return;
}
let latest = self.command_line.history_plain_text();
if self.history_content.text() == latest {
return;
}
use iced::widget::text_editor::{Action, Motion};
self.history_content = iced::widget::text_editor::Content::with_text(&latest);
self.history_content
.perform(Action::Move(Motion::DocumentEnd));
}
/// Close the active in-canvas modal (Plan B), mirroring what closing the
/// old OS window did: a style editor discards its staged (un-applied)
/// changes, and the ribbon tool that launched the dialog is de-highlighted.
@ -178,6 +192,7 @@ impl OpenCADStudio {
}
}
let task = self.update_inner(msg);
self.sync_open_command_history();
// Close the document-level first-touch transaction started by
// push_undo_snapshot at this message boundary.
self.finish_all_pending_history();
@ -1295,17 +1310,7 @@ impl OpenCADStudio {
Message::CommandHistoryToggle => {
self.command_line.toggle_history();
// On open, snapshot the current log into the read-only editor
// buffer so it can be drag-selected across lines and copied.
if self.command_line.history_open {
use iced::widget::text_editor::{Action, Motion};
self.history_content = iced::widget::text_editor::Content::with_text(
&self.command_line.history_plain_text(),
);
// Scroll to the newest line (bottom), the most useful when
// opening the log to grab a recent error.
self.history_content.perform(Action::Move(Motion::DocumentEnd));
}
self.sync_open_command_history();
Task::none()
}

View file

@ -5,7 +5,7 @@ use iced::time::Instant;
use crate::app::Message;
use crate::command::CmdOption;
use iced::widget::{
button, column, container, opaque, row, text, text_editor, text_input, tooltip, Space,
button, column, container, opaque, row, rule, text, text_editor, text_input, tooltip, Space,
};
use iced::{Background, Border, Color, Element, Length, Theme};
@ -361,11 +361,18 @@ impl CommandLine {
// Only the most recent entries pushed within the last few
// seconds show on the overlay. The dropdown button keeps the
// full backlog reachable when the user actually wants it.
let visible: Vec<&HistoryEntry> = self
let mut visible: Vec<&HistoryEntry> = self
.history
.iter()
.filter(|e| e.pinned || e.created_at.elapsed().as_secs_f32() < HISTORY_VISIBLE_SECS)
.collect();
// Keep the active prompt/options immediately above the input. Commands
// may emit informational lines while waiting for the next option; those
// lines belong above the pinned interaction row, not below it.
if let Some(index) = visible.iter().position(|entry| entry.pinned) {
let pinned = visible.remove(index);
visible.push(pinned);
}
let start = visible.len().saturating_sub(4);
let history_rows = visible[start..]
.iter()
@ -602,7 +609,6 @@ impl CommandLine {
.width(Length::Fill)
.padding([2, 6]);
let panel = container(column![header, log])
.style(container::bordered_box)
.width(Length::Fill)
.padding([4, 0]);
opaque(panel).into()
@ -610,9 +616,12 @@ impl CommandLine {
container(column![]).height(0).into()
};
container(column![
autocomplete,
dropdown,
// The transient recent lines and the full archive are two views of the
// same history. Showing both while the archive is open creates a fake
// second history region and visually disconnects the input row (#555).
let recent_history: Element<'a, Message> = if self.history_open {
container(column![]).height(0).into()
} else {
container(history_rows)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
@ -621,17 +630,25 @@ impl CommandLine {
..Default::default()
})
.width(Length::Fill)
.padding([2, 0]),
.padding([2, 0])
.into()
};
let history_divider: Element<'a, Message> = if self.history_open {
rule::horizontal(1).into()
} else {
container(column![]).height(0).into()
};
container(column![
autocomplete,
dropdown,
recent_history,
history_divider,
container(input_row)
.style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weakest.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
}
})
@ -652,7 +669,8 @@ impl CommandLine {
..Default::default()
}
})
.width(Length::Fixed(720.0))
.width(Length::Fill)
.max_width(720.0)
.into()
}
}