Skip to content

Commit d0cbfec

Browse files
authored
fix(core): improve tui minimal view display and prevent flashing scrollbar (#32045)
## Current Behavior - When the minimal view in the TUI is shown, some borders are initially shown for a fraction of a second. - When rendering terminal panes, the scrollbar can be wrongly shown for a fraction of a second when the PTY dimensions are stale. This looks like the scrollbar flashes. ## Expected Behavior - When the minimal view in the TUI is shown, no borders should ever be shown. - The scrollbar should only be shown when rendering terminal panes when needed.
1 parent 0e8d449 commit d0cbfec

3 files changed

Lines changed: 82 additions & 18 deletions

File tree

packages/nx/src/native/tui/app.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,10 @@ impl App {
10301030
})
10311031
.collect();
10321032

1033+
// Calculate minimal view context once for all panes
1034+
let is_minimal =
1035+
self.is_task_list_hidden() && get_task_count(&self.task_graph) == 1;
1036+
10331037
for (pane_idx, pane_area, relevant_pane_task) in terminal_panes_data {
10341038
if let Some(task_name) = relevant_pane_task {
10351039
let task_status = self
@@ -1039,11 +1043,11 @@ impl App {
10391043
// If task is pending, show dependency view instead of terminal pane
10401044
if task_status == TaskStatus::NotStarted {
10411045
self.render_dependency_view_internal(
1042-
f, pane_idx, pane_area, task_name,
1046+
f, pane_idx, pane_area, task_name, is_minimal,
10431047
);
10441048
} else {
10451049
self.render_terminal_pane_internal(
1046-
f, pane_idx, pane_area, task_name,
1050+
f, pane_idx, pane_area, task_name, is_minimal,
10471051
);
10481052
}
10491053
} else {
@@ -1660,6 +1664,7 @@ impl App {
16601664
pane_idx: usize,
16611665
pane_area: Rect,
16621666
task_name: String,
1667+
is_minimal: bool,
16631668
) {
16641669
// Calculate values that were previously passed in
16651670
let task_status = self
@@ -1704,7 +1709,8 @@ impl App {
17041709

17051710
// No need to update status in DependencyViewState - we pass the full map to the widget
17061711
if let Some(dep_state) = &mut self.dependency_view_states[pane_idx] {
1707-
let dependency_view = DependencyView::new(&self.task_status_map, &self.task_graph);
1712+
let dependency_view =
1713+
DependencyView::new(&self.task_status_map, &self.task_graph, is_minimal);
17081714
f.render_stateful_widget(dependency_view, pane_area, dep_state);
17091715
}
17101716
}
@@ -1716,13 +1722,13 @@ impl App {
17161722
pane_idx: usize,
17171723
pane_area: Rect,
17181724
task_name: String,
1725+
is_minimal: bool,
17191726
) {
17201727
// Calculate values that were previously passed in
17211728
let task_status = self
17221729
.get_task_status(&task_name)
17231730
.unwrap_or(TaskStatus::NotStarted);
17241731
let task_continuous = self.is_task_continuous(&task_name);
1725-
let tasks_list_hidden = self.is_task_list_hidden();
17261732
let has_pty = self.pty_instances.contains_key(&task_name);
17271733

17281734
let is_focused = match self.focus {
@@ -1769,7 +1775,7 @@ impl App {
17691775
);
17701776

17711777
let terminal_pane = TerminalPane::new()
1772-
.minimal(tasks_list_hidden && get_task_count(&self.task_graph) == 1)
1778+
.minimal(is_minimal)
17731779
.pty_data(terminal_pane_data)
17741780
.continuous(task_continuous);
17751781

packages/nx/src/native/tui/components/dependency_view.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,19 @@ impl DependencyViewState {
168168
pub struct DependencyView<'a> {
169169
status_map: &'a HashMap<String, TaskStatus>,
170170
task_graph: &'a TaskGraph,
171+
is_minimal: bool,
171172
}
172173

173174
impl<'a> DependencyView<'a> {
174-
pub fn new(status_map: &'a HashMap<String, TaskStatus>, task_graph: &'a TaskGraph) -> Self {
175+
pub fn new(
176+
status_map: &'a HashMap<String, TaskStatus>,
177+
task_graph: &'a TaskGraph,
178+
is_minimal: bool,
179+
) -> Self {
175180
Self {
176181
status_map,
177182
task_graph,
183+
is_minimal,
178184
}
179185
}
180186

@@ -375,17 +381,23 @@ impl<'a> StatefulWidget for DependencyView<'a> {
375381
),
376382
];
377383

378-
let block = Block::default()
379-
.title(title)
380-
.title_alignment(Alignment::Left)
381-
.borders(Borders::ALL)
382-
.border_type(if state.is_focused {
383-
BorderType::Thick
384-
} else {
385-
BorderType::Plain
386-
})
387-
.border_style(border_style)
388-
.padding(Padding::new(2, 2, 1, 1));
384+
let block = if self.is_minimal {
385+
Block::default()
386+
.borders(Borders::NONE)
387+
.padding(Padding::new(2, 2, 1, 1))
388+
} else {
389+
Block::default()
390+
.title(title)
391+
.title_alignment(Alignment::Left)
392+
.borders(Borders::ALL)
393+
.border_type(if state.is_focused {
394+
BorderType::Thick
395+
} else {
396+
BorderType::Plain
397+
})
398+
.border_style(border_style)
399+
.padding(Padding::new(2, 2, 1, 1))
400+
};
389401

390402
let inner_area = block.inner(area);
391403
block.render(area, buf);

packages/nx/src/native/tui/components/terminal_pane.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ pub struct TerminalPaneState {
185185
pub has_pty: bool,
186186
pub is_next_tab_target: bool,
187187
pub console_available: bool,
188+
// Cache expected viewport dimensions for consistent scrollbar calculations
189+
pub expected_viewport_height: Option<u16>,
188190
}
189191

190192
impl TerminalPaneState {
@@ -207,6 +209,7 @@ impl TerminalPaneState {
207209
has_pty,
208210
is_next_tab_target,
209211
console_available,
212+
expected_viewport_height: None,
210213
}
211214
}
212215
}
@@ -323,6 +326,39 @@ impl<'a> TerminalPane<'a> {
323326
.map(|data| data.is_interactive)
324327
.unwrap_or(false)
325328
}
329+
330+
/// Calculate content rows based on expected viewport height, not current PTY dimensions.
331+
/// This provides consistent scrollbar calculations even when PTY hasn't been resized yet.
332+
fn calculate_content_rows_for_viewport(
333+
&self,
334+
pty: &crate::native::tui::pty::PtyInstance,
335+
expected_viewport_height: u16,
336+
) -> usize {
337+
// Try to get current content rows from PTY
338+
let current_content_rows = pty.get_total_content_rows();
339+
340+
// If we have a cached viewport height and it differs from expected,
341+
// we need to estimate content based on the expected dimensions
342+
if let Some(screen) = pty.get_screen() {
343+
let (current_rows, _current_cols) = screen.size();
344+
345+
// If current PTY dimensions match expected viewport, use current calculation
346+
if current_rows == expected_viewport_height {
347+
return current_content_rows;
348+
}
349+
350+
// Otherwise, estimate content rows based on expected viewport height
351+
// This is a simple heuristic: assume content scales linearly with viewport height
352+
if current_rows > 0 {
353+
let scale_factor = expected_viewport_height as f64 / current_rows as f64;
354+
let estimated_content = (current_content_rows as f64 * scale_factor) as usize;
355+
return estimated_content.max(expected_viewport_height as usize);
356+
}
357+
}
358+
359+
// Fallback to current calculation
360+
current_content_rows
361+
}
326362
}
327363

328364
// This lifetime is needed for our terminal pane data, it breaks without it
@@ -543,11 +579,21 @@ impl<'a> StatefulWidget for TerminalPane<'a> {
543579
if let Some(pty) = &pty_data.pty {
544580
if let Some(screen) = pty.get_screen() {
545581
let viewport_height = inner_area.height;
582+
583+
// Cache expected viewport height for consistent calculations
584+
state.expected_viewport_height = Some(viewport_height);
585+
546586
let current_scroll = pty.get_scroll_offset();
547587

548-
let total_content_rows = pty.get_total_content_rows();
588+
// Calculate content based on expected dimensions, not current PTY dimensions
589+
// This prevents scrollbar flash when PTY hasn't been resized yet
590+
let total_content_rows =
591+
self.calculate_content_rows_for_viewport(pty, viewport_height);
549592
let scrollable_rows =
550593
total_content_rows.saturating_sub(viewport_height as usize);
594+
595+
// Determine if scrollbar is needed based on content vs viewport size
596+
// This is deterministic and doesn't depend on actual PTY dimensions
551597
let needs_scrollbar = scrollable_rows > 0;
552598

553599
// Reset scrollbar state if no scrolling needed

0 commit comments

Comments
 (0)