Skip to main content

ChatController

Struct ChatController 

Source
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

Source

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).

Source

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.

Source

pub fn set_status(&mut self, label: StyledText)

Set the status-strip context label (e.g. "Refining issue #N").

Source

pub fn set_busy(&mut self, busy: bool)

Enable or disable the busy spinner in the status strip.

Source

pub fn set_model_label(&mut self, label: &str)

Set the model chip label in the status strip (e.g. "claude-opus-4-5").

Source

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).

Source

pub fn input_text(&self) -> &str

The current input buffer text, with \n separating lines.

Source

pub fn clear_input(&mut self)

Clear the input buffer and reset cursor + scroll state.

Apps call this after handling a ChatControllerEvent::Submit.

Source

pub fn input_has_focus(&self) -> bool

Whether the input area currently has keyboard focus.

Source

pub fn set_input_has_focus(&mut self, focus: bool)

Set keyboard focus on the input area.

Source

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.

Source

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().

Source

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.

Source

pub fn submit_on_enter(&self) -> bool

Whether Enter currently submits (true) or inserts a newline (false). See set_submit_on_enter.

Source

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.

Source

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].

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn input_insert_char(&mut self, ch: char)

Insert a single character at the cursor. Also used by CharTyped.

Source

pub fn input_insert_str(&mut self, s: &str)

Insert a string at the cursor (e.g. for clipboard paste).

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &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> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more