commit d885c5b503656bc611c22391f3ce3ba4115dfdbf
parent eacaa41b34c2eb19b2a617b53882c2860c1b69a9
Author: William Casarin <jb55@jb55.com>
Date: Mon, 26 Jan 2026 12:31:06 -0800
dave: fix scene panel overflow with StripBuilder and clipping
- Replace manual rect allocation with StripBuilder for proper layout
- Add explicit clip_rect to panel cell to contain content
- Skip top_buttons_ui in compact mode (scene has its own toolbar)
- Reduce bottom margin in compact mode to prevent overflow
- Fix clippy warning: collapse nested if statement
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
3 files changed, 96 insertions(+), 89 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -303,22 +303,12 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
/// Scene view with RTS-style agent visualization and chat side panel
fn scene_ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse {
+ use egui_extras::{Size, StripBuilder};
+
let mut dave_response = DaveResponse::default();
- let available = ui.available_rect_before_wrap();
+ let mut scene_response: Option<SceneResponse> = None;
let panel_width = 400.0;
- // Scene area (main)
- let scene_rect = egui::Rect::from_min_size(
- available.min,
- egui::vec2(available.width() - panel_width, available.height()),
- );
-
- // Chat panel area (right side)
- let panel_rect = egui::Rect::from_min_size(
- egui::pos2(available.max.x - panel_width, available.min.y),
- egui::vec2(panel_width, available.height()),
- );
-
// Update all session statuses
self.session_manager.update_all_statuses();
@@ -328,86 +318,92 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
self.session_manager.switch_to(attention_id);
}
- // Render scene
- let scene_response = ui
- .allocate_new_ui(egui::UiBuilder::new().max_rect(scene_rect), |ui| {
- // Scene toolbar at top
- ui.horizontal(|ui| {
- if ui.button("+ New Agent").clicked() {
- dave_response = DaveResponse::new(DaveAction::NewChat);
- }
+ StripBuilder::new(ui)
+ .size(Size::remainder()) // Scene area takes remaining space
+ .size(Size::exact(panel_width)) // Chat panel fixed width
+ .clip(true) // Clip content to cell bounds
+ .horizontal(|mut strip| {
+ // Scene area (main)
+ strip.cell(|ui| {
+ // Scene toolbar at top
+ ui.horizontal(|ui| {
+ if ui.button("+ New Agent").clicked() {
+ dave_response = DaveResponse::new(DaveAction::NewChat);
+ }
+ ui.separator();
+ if ui.button("Classic View").clicked() {
+ self.show_scene = false;
+ }
+ });
ui.separator();
- if ui.button("Classic View").clicked() {
- self.show_scene = false;
- }
+
+ // Render the scene
+ scene_response = Some(self.scene.ui(&self.session_manager, ui));
});
- ui.separator();
- // Render the scene
- self.scene.ui(&self.session_manager, ui)
- })
- .inner;
+ // Chat side panel
+ strip.cell(|ui| {
+ egui::Frame::new()
+ .fill(ui.visuals().faint_bg_color)
+ .inner_margin(egui::Margin::symmetric(8, 12))
+ .show(ui, |ui| {
+ if let Some(selected_id) = self.scene.primary_selection() {
+ if let Some(session) = self.session_manager.get_mut(selected_id) {
+ // Show title
+ ui.heading(&session.title);
+ ui.separator();
+
+ // Render chat UI for selected session
+ let response = DaveUi::new(
+ self.model_config.trial,
+ &session.chat,
+ &mut session.input,
+ )
+ .compact(true)
+ .ui(app_ctx, ui);
+
+ if response.action.is_some() {
+ dave_response = response;
+ }
+ }
+ } else {
+ // No selection
+ ui.centered_and_justified(|ui| {
+ ui.label("Select an agent to view chat");
+ });
+ }
+ });
+ });
+ });
- // Handle scene actions
- if let Some(action) = scene_response.action {
- match action {
- SceneAction::SelectionChanged(ids) => {
- // Selection updated, sync with session manager's active
- if let Some(id) = ids.first() {
- self.session_manager.switch_to(*id);
+ // Handle scene actions after strip rendering
+ if let Some(response) = scene_response {
+ if let Some(action) = response.action {
+ match action {
+ SceneAction::SelectionChanged(ids) => {
+ // Selection updated, sync with session manager's active
+ if let Some(id) = ids.first() {
+ self.session_manager.switch_to(*id);
+ }
}
- }
- SceneAction::SpawnAgent => {
- dave_response = DaveResponse::new(DaveAction::NewChat);
- }
- SceneAction::DeleteSelected => {
- for id in self.scene.selected.clone() {
- self.session_manager.delete_session(id);
+ SceneAction::SpawnAgent => {
+ dave_response = DaveResponse::new(DaveAction::NewChat);
}
- self.scene.clear_selection();
- }
- SceneAction::AgentMoved { id, position } => {
- if let Some(session) = self.session_manager.get_mut(id) {
- session.scene_position = position;
+ SceneAction::DeleteSelected => {
+ for id in self.scene.selected.clone() {
+ self.session_manager.delete_session(id);
+ }
+ self.scene.clear_selection();
+ }
+ SceneAction::AgentMoved { id, position } => {
+ if let Some(session) = self.session_manager.get_mut(id) {
+ session.scene_position = position;
+ }
}
}
}
}
- // Render chat side panel
- ui.allocate_new_ui(egui::UiBuilder::new().max_rect(panel_rect), |ui| {
- egui::Frame::new()
- .fill(ui.visuals().faint_bg_color)
- .inner_margin(egui::Margin::symmetric(8, 12))
- .show(ui, |ui| {
- if let Some(selected_id) = self.scene.primary_selection() {
- if let Some(session) = self.session_manager.get_mut(selected_id) {
- // Show title
- ui.heading(&session.title);
- ui.separator();
-
- // Render chat UI for selected session
- let response = DaveUi::new(
- self.model_config.trial,
- &session.chat,
- &mut session.input,
- )
- .compact(true)
- .ui(app_ctx, ui);
-
- if response.action.is_some() {
- dave_response = response;
- }
- }
- } else {
- // No selection
- ui.centered_and_justified(|ui| {
- ui.label("Select an agent to view chat");
- });
- }
- });
- });
-
dave_response
}
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -113,19 +113,27 @@ impl<'a> DaveUi<'a> {
/// The main render function. Call this to render Dave
pub fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse {
- let action = top_buttons_ui(app_ctx, ui);
+ // Skip top buttons in compact mode (scene panel has its own controls)
+ let action = if self.compact {
+ None
+ } else {
+ top_buttons_ui(app_ctx, ui)
+ };
egui::Frame::NONE
.show(ui, |ui| {
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
let margin = self.chat_margin(ui.ctx());
+ // Reduce bottom margin in compact mode to prevent overflow
+ let bottom_margin = if self.compact { 20 } else { 100 };
+
let r = egui::Frame::new()
.outer_margin(egui::Margin {
left: margin,
right: margin,
top: 0,
- bottom: 100,
+ bottom: bottom_margin,
})
.inner_margin(egui::Margin::same(8))
.fill(ui.visuals().extreme_bg_color)
@@ -565,14 +573,18 @@ impl<'a> DaveUi<'a> {
.corner_radius(10.0)
.fill(ui.visuals().widgets.inactive.weak_bg_fill)
.show(ui, |ui| {
- ui.label(msg);
+ ui.add(egui::Label::new(msg).selectable(true));
})
});
}
fn assistant_chat(&self, msg: &str, ui: &mut egui::Ui) {
ui.horizontal_wrapped(|ui| {
- ui.add(egui::Label::new(msg).wrap_mode(egui::TextWrapMode::Wrap));
+ ui.add(
+ egui::Label::new(msg)
+ .wrap_mode(egui::TextWrapMode::Wrap)
+ .selectable(true),
+ );
});
}
}
diff --git a/crates/notedeck_dave/src/ui/scene.rs b/crates/notedeck_dave/src/ui/scene.rs
@@ -259,10 +259,9 @@ impl AgentScene {
// Handle keyboard input (only when no text input has focus)
if !ui.ctx().wants_keyboard_input() {
if ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace))
+ && !self.selected.is_empty()
{
- if !self.selected.is_empty() {
- response = SceneResponse::new(SceneAction::DeleteSelected);
- }
+ response = SceneResponse::new(SceneAction::DeleteSelected);
}
// Handle 'n' key to spawn new agent