fix: fix border style for BottomPane (#893)
This PR fixes things so that: * when the `BottomPane` is in the `StatusIndicator` state, the border should be dim * when the `BottomPane` does not have input focus, the border should be dim To make it easier to enforce this invariant, this PR introduces `BottomPane::set_state()` that will: * update `self.state` * call `update_border_for_input_focus()` * request a repaint This should make it easier to enforce other updates for state changes going forward.
This commit is contained in:
@@ -79,7 +79,7 @@ pub(crate) struct BottomPaneParams {
|
|||||||
pub(crate) has_input_focus: bool,
|
pub(crate) has_input_focus: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BottomPane<'_> {
|
impl<'a> BottomPane<'a> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
BottomPaneParams {
|
BottomPaneParams {
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
@@ -89,11 +89,12 @@ impl BottomPane<'_> {
|
|||||||
let mut textarea = TextArea::default();
|
let mut textarea = TextArea::default();
|
||||||
textarea.set_placeholder_text("send a message");
|
textarea.set_placeholder_text("send a message");
|
||||||
textarea.set_cursor_line_style(Style::default());
|
textarea.set_cursor_line_style(Style::default());
|
||||||
update_border_for_input_focus(&mut textarea, has_input_focus);
|
let state = PaneState::TextInput;
|
||||||
|
update_border_for_input_focus(&mut textarea, &state, has_input_focus);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
textarea,
|
textarea,
|
||||||
state: PaneState::TextInput,
|
state,
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
has_input_focus,
|
has_input_focus,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
@@ -112,7 +113,7 @@ impl BottomPane<'_> {
|
|||||||
|
|
||||||
pub(crate) fn set_input_focus(&mut self, has_input_focus: bool) {
|
pub(crate) fn set_input_focus(&mut self, has_input_focus: bool) {
|
||||||
self.has_input_focus = has_input_focus;
|
self.has_input_focus = has_input_focus;
|
||||||
update_border_for_input_focus(&mut self.textarea, has_input_focus);
|
update_border_for_input_focus(&mut self.textarea, &self.state, has_input_focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forward a key event to the appropriate child widget.
|
/// Forward a key event to the appropriate child widget.
|
||||||
@@ -144,14 +145,14 @@ impl BottomPane<'_> {
|
|||||||
text_rows as u16 + TEXTAREA_BORDER_LINES
|
text_rows as u16 + TEXTAREA_BORDER_LINES
|
||||||
};
|
};
|
||||||
|
|
||||||
self.state = PaneState::StatusIndicator {
|
self.set_state(PaneState::StatusIndicator {
|
||||||
view: StatusIndicatorWidget::new(
|
view: StatusIndicatorWidget::new(
|
||||||
self.app_event_tx.clone(),
|
self.app_event_tx.clone(),
|
||||||
desired_height,
|
desired_height,
|
||||||
),
|
),
|
||||||
};
|
})?;
|
||||||
} else {
|
} else {
|
||||||
self.state = PaneState::TextInput;
|
self.set_state(PaneState::TextInput)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,13 +192,13 @@ impl BottomPane<'_> {
|
|||||||
match self.state {
|
match self.state {
|
||||||
PaneState::TextInput => {
|
PaneState::TextInput => {
|
||||||
if is_task_running {
|
if is_task_running {
|
||||||
self.state = PaneState::StatusIndicator {
|
self.set_state(PaneState::StatusIndicator {
|
||||||
view: StatusIndicatorWidget::new(self.app_event_tx.clone(), {
|
view: StatusIndicatorWidget::new(self.app_event_tx.clone(), {
|
||||||
let text_rows =
|
let text_rows =
|
||||||
self.textarea.lines().len().max(MIN_TEXTAREA_ROWS) as u16;
|
self.textarea.lines().len().max(MIN_TEXTAREA_ROWS) as u16;
|
||||||
text_rows + TEXTAREA_BORDER_LINES
|
text_rows + TEXTAREA_BORDER_LINES
|
||||||
}),
|
}),
|
||||||
};
|
})?;
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -206,7 +207,7 @@ impl BottomPane<'_> {
|
|||||||
if is_task_running {
|
if is_task_running {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
self.state = PaneState::TextInput;
|
self.set_state(PaneState::TextInput)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PaneState::ApprovalModal { .. } => {
|
PaneState::ApprovalModal { .. } => {
|
||||||
@@ -220,35 +221,37 @@ impl BottomPane<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Enqueue a new approval request coming from the agent.
|
/// Enqueue a new approval request coming from the agent.
|
||||||
///
|
pub fn push_approval_request(
|
||||||
/// Returns `true` when this is the *first* modal - in that case the caller
|
&mut self,
|
||||||
/// should trigger a redraw so that the modal becomes visible.
|
request: ApprovalRequest,
|
||||||
pub fn push_approval_request(&mut self, request: ApprovalRequest) -> bool {
|
) -> Result<(), SendError<AppEvent>> {
|
||||||
let widget = UserApprovalWidget::new(request, self.app_event_tx.clone());
|
let widget = UserApprovalWidget::new(request, self.app_event_tx.clone());
|
||||||
|
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
PaneState::StatusIndicator { .. } => {
|
PaneState::StatusIndicator { .. } => self.set_state(PaneState::ApprovalModal {
|
||||||
self.state = PaneState::ApprovalModal {
|
current: widget,
|
||||||
current: widget,
|
queue: Vec::new(),
|
||||||
queue: Vec::new(),
|
}),
|
||||||
};
|
|
||||||
true // Needs redraw so the modal appears.
|
|
||||||
}
|
|
||||||
PaneState::TextInput => {
|
PaneState::TextInput => {
|
||||||
// Transition to modal state with an empty queue.
|
// Transition to modal state with an empty queue.
|
||||||
self.state = PaneState::ApprovalModal {
|
self.set_state(PaneState::ApprovalModal {
|
||||||
current: widget,
|
current: widget,
|
||||||
queue: Vec::new(),
|
queue: Vec::new(),
|
||||||
};
|
})
|
||||||
true // Needs redraw so the modal appears.
|
|
||||||
}
|
}
|
||||||
PaneState::ApprovalModal { queue, .. } => {
|
PaneState::ApprovalModal { queue, .. } => {
|
||||||
queue.push(widget);
|
queue.push(widget);
|
||||||
false // Already in modal mode - no redraw required.
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_state(&mut self, state: PaneState<'a>) -> Result<(), SendError<AppEvent>> {
|
||||||
|
self.state = state;
|
||||||
|
update_border_for_input_focus(&mut self.textarea, &self.state, self.has_input_focus);
|
||||||
|
self.request_redraw()
|
||||||
|
}
|
||||||
|
|
||||||
fn request_redraw(&self) -> Result<(), SendError<AppEvent>> {
|
fn request_redraw(&self) -> Result<(), SendError<AppEvent>> {
|
||||||
self.app_event_tx.send(AppEvent::Redraw)
|
self.app_event_tx.send(AppEvent::Redraw)
|
||||||
}
|
}
|
||||||
@@ -277,21 +280,40 @@ impl WidgetRef for &BottomPane<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_border_for_input_focus(textarea: &mut TextArea, has_input_focus: bool) {
|
// Note this sets the border for the TextArea, but the TextArea is not visible
|
||||||
let (title, border_style) = if has_input_focus {
|
// for all variants of PaneState.
|
||||||
(
|
fn update_border_for_input_focus(textarea: &mut TextArea, state: &PaneState, has_focus: bool) {
|
||||||
"use Enter to send for now (Ctrl‑D to quit)",
|
struct BlockState {
|
||||||
Style::default().dim(),
|
title: &'static str,
|
||||||
)
|
right_title: Line<'static>,
|
||||||
} else {
|
border_style: Style,
|
||||||
("", Style::default())
|
}
|
||||||
};
|
|
||||||
let right_title = if has_input_focus {
|
let accepting_input = match state {
|
||||||
Line::from("press enter to send").alignment(Alignment::Right)
|
PaneState::TextInput => true,
|
||||||
} else {
|
PaneState::ApprovalModal { .. } => true,
|
||||||
Line::from("")
|
PaneState::StatusIndicator { .. } => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let block_state = if has_focus && accepting_input {
|
||||||
|
BlockState {
|
||||||
|
title: "use Enter to send for now (Ctrl-D to quit)",
|
||||||
|
right_title: Line::from("press enter to send").alignment(Alignment::Right),
|
||||||
|
border_style: Style::default(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BlockState {
|
||||||
|
title: "",
|
||||||
|
right_title: Line::from(""),
|
||||||
|
border_style: Style::default().dim(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let BlockState {
|
||||||
|
title,
|
||||||
|
right_title,
|
||||||
|
border_style,
|
||||||
|
} = block_state;
|
||||||
textarea.set_block(
|
textarea.set_block(
|
||||||
ratatui::widgets::Block::default()
|
ratatui::widgets::Block::default()
|
||||||
.title_bottom(title)
|
.title_bottom(title)
|
||||||
|
|||||||
@@ -252,10 +252,7 @@ impl ChatWidget<'_> {
|
|||||||
cwd,
|
cwd,
|
||||||
reason,
|
reason,
|
||||||
};
|
};
|
||||||
let needs_redraw = self.bottom_pane.push_approval_request(request);
|
self.bottom_pane.push_approval_request(request)?;
|
||||||
if needs_redraw {
|
|
||||||
self.request_redraw()?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EventMsg::ApplyPatchApprovalRequest {
|
EventMsg::ApplyPatchApprovalRequest {
|
||||||
changes,
|
changes,
|
||||||
@@ -284,8 +281,7 @@ impl ChatWidget<'_> {
|
|||||||
reason,
|
reason,
|
||||||
grant_root,
|
grant_root,
|
||||||
};
|
};
|
||||||
let _needs_redraw = self.bottom_pane.push_approval_request(request);
|
self.bottom_pane.push_approval_request(request)?;
|
||||||
// Redraw is always need because the history has changed.
|
|
||||||
self.request_redraw()?;
|
self.request_redraw()?;
|
||||||
}
|
}
|
||||||
EventMsg::ExecCommandBegin {
|
EventMsg::ExecCommandBegin {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ impl WidgetRef for StatusIndicatorWidget {
|
|||||||
.padding(Padding::new(1, 0, 0, 0))
|
.padding(Padding::new(1, 0, 0, 0))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(widget_style);
|
.border_style(widget_style.dim());
|
||||||
// Animated 3‑dot pattern inside brackets. The *active* dot is bold
|
// Animated 3‑dot pattern inside brackets. The *active* dot is bold
|
||||||
// white, the others are dim.
|
// white, the others are dim.
|
||||||
const DOT_COUNT: usize = 3;
|
const DOT_COUNT: usize = 3;
|
||||||
|
|||||||
Reference in New Issue
Block a user