pub struct ChatController { /* private fields */ }Expand description
Cross-backend compose controller for a chat overlay.
Renders a 3-zone modal over its host rect: a 1-row status strip at the top,
a scrollable transcript in the middle, and a multi-line TextInput at the
bottom. All rendering is delegated to existing Backend trait methods —
draw_message_list, draw_scrollbar, draw_text_input, draw_spinner —
so no new trait method is needed.
State ownership follows the TreeController
pattern: the app pushes transcript turns per frame via
set_transcript, and the controller owns only
interaction state (scroll position, input buffer, history ring).
§Wrapping
Plain-text turns (from push_turn /
set_transcript) are word-wrapped via
crate::text_util::word_wrap: rows break at whitespace where
possible, and only a single word wider than the column budget falls
back to a mid-word hard break (e.g. with a 10-column budget
"implementation" becomes "implementa" + "tion").
Styled turns (from push_turn_markdown)
wrap per-span at exactly the display-width budget, ignoring word
boundaries — crate::text_util::wrap_spans with
crate::text_util::WrapPolicy::Char. Word wrapping is available
on the same function (markdown rendering uses it — see
crate::compose::markdown) but is intentionally not used here yet.
§Input soft-wrap and auto-grow
The input box’s TextInput is built from the wrapped buffer, not the
raw one: build_text_input soft-wraps
input_buf to TextInput::content_cols before handing it to the
primitive, so long lines wrap inside the box instead of scrolling
horizontally. Wrapping is exact — a visual row is always a contiguous
byte range of its logical line, with no whitespace collapsed or
dropped (unlike crate::text_util::word_wrap’s transcript wrapping,
which is lossy at wrap points) — so cursor positions map losslessly
between logical (line, byte offset) space (what input_buf/
input_cursor store) and visual (row, char column) space (what the
painted TextInput shows).
↑/↓ move the cursor by visual row, not logical line: pressing
↑ partway through a wrapped paragraph moves to the previous visual
row within it; history recall only triggers at the first / last
visual row of the whole buffer (see the Key::Named(NamedKey::Up) /
Down arms of Self::handle_key).
The input box’s painted height auto-grows with content: it’s the
wrapped visual row count, clamped to
input_min_rows, input_max_rows
(default 1..=8). Once the row count exceeds input_max_rows the box
stops growing and TextInput::layout’s own vertical auto-scroll
keeps the cursor in view, same as any other overflowing TextInput.
Implementations§
Source§impl ChatController
impl ChatController
Sourcepub fn new(id: impl Into<String>) -> Self
pub fn new(id: impl Into<String>) -> Self
Create a new controller. id is used to namespace widget IDs for all
sub-primitives (transcript, scrollbar, input, spinner).
Sourcepub fn set_transcript(&mut self, turns: Vec<ChatTurn>)
pub fn set_transcript(&mut self, turns: Vec<ChatTurn>)
Replace the transcript for the next render pass.
This is the primary streaming hook: the app calls this on every chunk received from the assistant and then triggers a redraw. The controller does not hold a reference to the app’s turns — it re-renders from the fresh slice each frame.
Sourcepub fn set_status(&mut self, label: StyledText)
pub fn set_status(&mut self, label: StyledText)
Set the status-strip context label (e.g. "Refining issue #N").
Sourcepub fn set_model_label(&mut self, label: &str)
pub fn set_model_label(&mut self, label: &str)
Set the model chip label in the status strip (e.g. "claude-opus-4-5").
Sourcepub fn set_spinner_frame(&mut self, frame: usize)
pub fn set_spinner_frame(&mut self, frame: usize)
Advance the spinner animation frame. Apps increment this on their own ticker (~100 ms per frame for a braille-style spinner).
Sourcepub fn input_text(&self) -> &str
pub fn input_text(&self) -> &str
The current input buffer text, with \n separating lines.
Sourcepub fn clear_input(&mut self)
pub fn clear_input(&mut self)
Clear the input buffer and reset cursor + scroll state.
Apps call this after handling a ChatControllerEvent::Submit.
Sourcepub fn input_has_focus(&self) -> bool
pub fn input_has_focus(&self) -> bool
Whether the input area currently has keyboard focus.
Sourcepub fn set_input_has_focus(&mut self, focus: bool)
pub fn set_input_has_focus(&mut self, focus: bool)
Set keyboard focus on the input area.
Sourcepub fn set_input_height_range(&mut self, min: usize, max: usize)
pub fn set_input_height_range(&mut self, min: usize, max: usize)
Configure the input area’s auto-grow row-count clamp.
The input box’s painted height is the wrapped visual row count of
the current buffer, clamped to [min, max], plus the fixed 2-row
border (see Self::compute_layout). min is floored to 1
(the box never fully collapses); max is floored to min.
Default: 1..=8.
Sourcepub fn set_scrollbar_width(&mut self, width: Option<f32>)
pub fn set_scrollbar_width(&mut self, width: Option<f32>)
Override the scrollbar track width.
Pass Some(8.0) for GTK, Some(1.0) for TUI (matching MSV).
None (default) falls back to backend.line_height().
Sourcepub fn set_submit_on_enter(&mut self, submit_on_enter: bool)
pub fn set_submit_on_enter(&mut self, submit_on_enter: bool)
Toggle Zed-style “Enter to send” behaviour.
true — Enter submits, Shift+Enter/Alt+Enter insert a
newline. false (default) — Enter always inserts a newline;
submit is Ctrl+S / Alt+Enter / Ctrl+Enter.
Sourcepub fn submit_on_enter(&self) -> bool
pub fn submit_on_enter(&self) -> bool
Whether Enter currently submits (true) or inserts a newline
(false). See set_submit_on_enter.
Sourcepub fn set_hint(&mut self, hint: Option<StyledText>)
pub fn set_hint(&mut self, hint: Option<StyledText>)
Set (or clear) the persistent hint row painted above the input box.
Pass None to omit the row (default).
Unlike the TextInput placeholder, this stays visible while
typing — see this struct’s Persistent hint line doc section.
Sourcepub fn push_turn(&mut self, role: ChatRole, text: StyledText)
pub fn push_turn(&mut self, role: ChatRole, text: StyledText)
Append a pre-styled turn to the controller’s internal transcript.
This is an alternative to [set_transcript] for apps that build the
transcript incrementally rather than replacing it each frame. Do not
mix push_turn and set_transcript on the same controller — use one
model consistently.
For markdown-formatted assistant messages, prefer
[push_turn_markdown].
Sourcepub fn push_turn_markdown(
&mut self,
role: ChatRole,
markdown: &str,
theme: &Theme,
)
pub fn push_turn_markdown( &mut self, role: ChatRole, markdown: &str, theme: &Theme, )
Append a turn whose body is markdown text.
This is the recommended API for assistant-role messages. The
markdown adapter (render_markdown_to_styled) converts headings,
bold, italic, inline code, bulleted and numbered lists, blockquotes,
links, and fenced code blocks to a StyledText with colours and
decorations baked in. The resulting ChatTurn is appended to the
controller’s internal transcript.
Use [set_transcript] instead if your app maintains its own
transcript vector and passes it each frame. Do not mix push_turn*
and set_transcript on the same controller.
Markdown lines are interleaved into a single StyledText with
'\n' separator spans so the transcript renderer can split them for
wrapping, preserving the structure produced by the adapter (list
markers, blockquote rules, etc.) as plain-text cues.
Sourcepub fn transcript_scroll_top(&self) -> usize
pub fn transcript_scroll_top(&self) -> usize
Current transcript scroll offset (first visible wrapped row).
After render runs with follow-tail engaged, this
reflects the position that was actually painted (tail row).
Sourcepub fn set_transcript_scroll_top(&mut self, top: usize)
pub fn set_transcript_scroll_top(&mut self, top: usize)
Programmatically override the transcript scroll position.
This does not change the stuck_to_bottom flag. If you
want to re-engage follow-tail, call
scroll_transcript_by with a large
positive delta so the flag is recomputed against a known total_rows
and visible_rows.
Sourcepub fn is_turn_collapsed(&self, turn_idx: usize) -> bool
pub fn is_turn_collapsed(&self, turn_idx: usize) -> bool
Whether transcript[turn_idx] is currently rendered collapsed.
false (including out-of-range turn_idx) when there is no such
turn.
Sourcepub fn set_turn_collapsed(&mut self, turn_idx: usize, collapsed: bool)
pub fn set_turn_collapsed(&mut self, turn_idx: usize, collapsed: bool)
Set transcript[turn_idx]’s collapsed state directly. A no-op if
turn_idx is out of range.
Sourcepub fn toggle_turn_collapsed(&mut self, turn_idx: usize)
pub fn toggle_turn_collapsed(&mut self, turn_idx: usize)
Flip transcript[turn_idx]’s collapsed state. A no-op if turn_idx
is out of range.
Apps call this in response to a ChatControllerEvent::TurnClicked
(typically when row_in_turn == 0, the role header row) or to
mirror the keyboard toggle described on focused_turn.
Sourcepub fn set_turn_summary(&mut self, turn_idx: usize, summary: Option<String>)
pub fn set_turn_summary(&mut self, turn_idx: usize, summary: Option<String>)
Override transcript[turn_idx]’s summary line — see
turn_summary_line. Pass None to fall
back to the turn’s first non-blank content line. A no-op (but not an
error) if turn_idx is currently out of range — e.g. an app may set
this ahead of a set_transcript call that
grows the transcript to include it.
Sourcepub fn turn_summary_line(&self, turn_idx: usize) -> String
pub fn turn_summary_line(&self, turn_idx: usize) -> String
The line shown when transcript[turn_idx] is
collapsed: the
set_turn_summary override if set,
otherwise the first non-blank line of the turn’s plain text. Empty
string for an out-of-range turn_idx or a turn with no content.
Sourcepub fn focused_turn(&self) -> Option<usize>
pub fn focused_turn(&self) -> Option<usize>
The transcript’s keyboard focus cursor: Some(turn_idx)
when Tab/Shift+Tab has moved focus onto a turn (so Enter
toggles its collapsed state instead of acting on the input),
None when the input has focus (the default).
Sourcepub fn set_focused_turn(&mut self, turn: Option<usize>)
pub fn set_focused_turn(&mut self, turn: Option<usize>)
Programmatically set the transcript’s keyboard focus cursor. Pass
None to return focus to the input.
Sourcepub fn render(&self, backend: &mut dyn Backend, rect: Rect)
pub fn render(&self, backend: &mut dyn Backend, rect: Rect)
Paint all three zones into rect using backend.
Call this inside crate::AppLogic::render after resolving the
overlay rect. The controller calls begin_frame / end_frame
around the draw calls if your app’s render path wraps frames
inside the overlay rect — otherwise just call it inline.
Sourcepub fn handle(
&mut self,
event: &UiEvent,
backend: &dyn Backend,
rect: Rect,
) -> ChatControllerEvent
pub fn handle( &mut self, event: &UiEvent, backend: &dyn Backend, rect: Rect, ) -> ChatControllerEvent
Dispatch a UiEvent to the controller.
Keyboard events mutate the input buffer; scroll events mutate the
transcript scroll position. Returns a semantic
ChatControllerEvent for the app to act on.
backend is used only for layout measurements (line_height,
char_width, text_input_layout) — no drawing happens here.
Sourcepub fn scroll_transcript_by(
&mut self,
delta: isize,
total_rows: usize,
visible_rows: usize,
)
pub fn scroll_transcript_by( &mut self, delta: isize, total_rows: usize, visible_rows: usize, )
Scroll the transcript by delta wrapped rows, clamped to valid range.
Exposed so callers can drive transcript scrolling without synthesising
a UiEvent::Scroll.
After clamping, stuck_to_bottom is recomputed: scrolling up
disengages follow-tail; scrolling back to the last page re-engages it.
Sourcepub fn input_insert_char(&mut self, ch: char)
pub fn input_insert_char(&mut self, ch: char)
Insert a single character at the cursor. Also used by CharTyped.
Sourcepub fn input_insert_str(&mut self, s: &str)
pub fn input_insert_str(&mut self, s: &str)
Insert a string at the cursor (e.g. for clipboard paste).
Auto Trait Implementations§
impl !Freeze for ChatController
impl !RefUnwindSafe for ChatController
impl !Sync for ChatController
impl Send for ChatController
impl Unpin for ChatController
impl UnsafeUnpin for ChatController
impl UnwindSafe for ChatController
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more