fix(gui): disable text selection on UI labels, add edge-scroll during drag
UI labels (status, headings, control text) no longer selectable. Console/hex content remains selectable for copy. ScrollArea now nudges when drag-selecting near the bottom edge (~240px/s).
This commit is contained in:
@@ -8,7 +8,7 @@ use crate::panels::{config, console, font_settings, hex_view, plot_view, send_pa
|
||||
use crate::perf::{DrainStats, GuiProfiler, GuiSnapshot};
|
||||
use crate::shortcuts::{self, default_bindings};
|
||||
use crate::ui_fonts::{self, FontCandidate, UiFontSettings};
|
||||
use egui::{Color32, Layout, Panel, Pos2, Rect, UiBuilder};
|
||||
use egui::{Color32, Label, Layout, Panel, Pos2, Rect, UiBuilder};
|
||||
use pipeview_client::SessionManager;
|
||||
use pipeview_client::config::SessionConfig;
|
||||
use pipeview_client::session::SessionEvent;
|
||||
@@ -537,7 +537,7 @@ impl XserialApp {
|
||||
fn render_main_panel(&mut self, ui: &mut egui::Ui) {
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
if self.tabs.is_empty() {
|
||||
ui.heading("No sessions.");
|
||||
ui.add(Label::new(egui::RichText::new("No sessions.").heading()).selectable(false));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -557,10 +557,10 @@ impl XserialApp {
|
||||
let (badge, status_text) = tab.status.badge();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading(format!("Session {}", tab.id));
|
||||
ui.label(badge);
|
||||
ui.add(Label::new(egui::RichText::new(format!("Session {}", tab.id)).heading()).selectable(false));
|
||||
ui.add(Label::new(badge).selectable(false));
|
||||
ui.separator();
|
||||
ui.label(status_text);
|
||||
ui.add(Label::new(status_text).selectable(false));
|
||||
ui.separator();
|
||||
if ui
|
||||
.selectable_label(tab.view == View::Text, "Text")
|
||||
@@ -584,7 +584,7 @@ impl XserialApp {
|
||||
|
||||
let mut display_changed = false;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Show:");
|
||||
ui.add(Label::new("Show:").selectable(false));
|
||||
display_changed |= ui
|
||||
.checkbox(&mut display.show_timestamp, "Timestamp")
|
||||
.changed();
|
||||
@@ -598,7 +598,7 @@ impl XserialApp {
|
||||
session_controls::render_search_bar(ui, tab);
|
||||
|
||||
if let ConnectionStatus::Error(message) = &tab.status {
|
||||
ui.label(egui::RichText::new(message).color(Color32::RED));
|
||||
ui.add(Label::new(egui::RichText::new(message).color(Color32::RED)).selectable(false));
|
||||
}
|
||||
|
||||
let full = ui.available_rect_before_wrap();
|
||||
@@ -629,11 +629,22 @@ impl XserialApp {
|
||||
}
|
||||
View::Plot => {
|
||||
if tab.plot_view.detached {
|
||||
ui.heading(format!(
|
||||
ui.add(
|
||||
Label::new(
|
||||
egui::RichText::new(format!(
|
||||
"Plot window detached for Session {}",
|
||||
tab.id
|
||||
));
|
||||
ui.label("The plot is currently shown in a floating window.");
|
||||
tab.id,
|
||||
))
|
||||
.heading(),
|
||||
)
|
||||
.selectable(false),
|
||||
);
|
||||
ui.add(
|
||||
Label::new(
|
||||
"The plot is currently shown in a floating window.",
|
||||
)
|
||||
.selectable(false),
|
||||
);
|
||||
if ui.button("Dock Plot Back").clicked() {
|
||||
tab.plot_view.detached = false;
|
||||
}
|
||||
@@ -720,7 +731,7 @@ impl XserialApp {
|
||||
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
||||
Panel::top("top_bar").show_inside(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.heading("pipeview");
|
||||
ui.add(Label::new(egui::RichText::new("pipeview").heading()).selectable(false));
|
||||
ui.separator();
|
||||
// ui.label(format!(
|
||||
// "Fonts: {} + {} {:.1} pt",
|
||||
|
||||
@@ -6,6 +6,11 @@ use egui::{
|
||||
text::{LayoutJob, TextFormat},
|
||||
};
|
||||
|
||||
/// Nudge amount (pixels/frame) for auto-scroll during text-selection drag
|
||||
const EDGE_SCROLL_NUDGE: f32 = 4.0;
|
||||
/// Distance from the bottom edge (pixels) that triggers auto-scroll
|
||||
const EDGE_SCROLL_ZONE: f32 = 30.0;
|
||||
|
||||
pub fn render(
|
||||
ui: &mut Ui,
|
||||
buf: &TextBuffer,
|
||||
@@ -14,12 +19,30 @@ pub fn render(
|
||||
) -> usize {
|
||||
let line_count = buf.len();
|
||||
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||
ScrollArea::both().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
line_count,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let mut near_bottom_edge = false;
|
||||
|
||||
let scroll_area = ScrollArea::both()
|
||||
.id_salt("console_text_area")
|
||||
.stick_to_bottom(true);
|
||||
|
||||
let output = scroll_area.show_viewport(ui, |ui, viewport| {
|
||||
let start_row = (viewport.min.y / row_height).floor().max(0.0) as usize;
|
||||
let end_row = ((viewport.max.y / row_height).ceil() as usize).min(line_count);
|
||||
|
||||
// Check if user is drag-selecting near the bottom edge of the scroll area.
|
||||
// We compare pointer position against the screen-space bottom of the
|
||||
// allocated scroll area (ui.max_rect) rather than the content viewport,
|
||||
// so that any overflow content area counts.
|
||||
near_bottom_edge = ui.ctx().input(|input| {
|
||||
input.pointer.button_down(egui::PointerButton::Primary)
|
||||
&& input.pointer.hover_pos().is_some_and(|pos| {
|
||||
let area = ui.max_rect();
|
||||
pos.y > area.bottom() - EDGE_SCROLL_ZONE
|
||||
&& pos.y < area.bottom() + 20.0
|
||||
})
|
||||
});
|
||||
|
||||
for row in start_row..end_row {
|
||||
if let Some(line) = buf.get(row) {
|
||||
ui.add(
|
||||
Label::new(format_console_line(line, display, search, ui.style()))
|
||||
@@ -27,8 +50,20 @@ pub fn render(
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// If the user is dragging a selection near the bottom edge, nudge the scroll
|
||||
// offset for the next frame and request a repaint for smooth continuous scrolling.
|
||||
if near_bottom_edge && line_count > 0 {
|
||||
let mut state = output.state;
|
||||
let max_offset = (line_count as f32 * row_height)
|
||||
- output.inner_rect.height()
|
||||
+ EDGE_SCROLL_ZONE;
|
||||
state.offset.y = (state.offset.y + EDGE_SCROLL_NUDGE).min(max_offset.max(0.0));
|
||||
state.store(ui.ctx(), output.id);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
line_count
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ use egui::{
|
||||
text::{LayoutJob, TextFormat},
|
||||
};
|
||||
|
||||
/// Nudge amount (pixels/frame) for auto-scroll during text-selection drag
|
||||
const EDGE_SCROLL_NUDGE: f32 = 4.0;
|
||||
/// Distance from the bottom edge (pixels) that triggers auto-scroll
|
||||
const EDGE_SCROLL_ZONE: f32 = 30.0;
|
||||
|
||||
pub fn render(
|
||||
ui: &mut Ui,
|
||||
buf: &HexBuffer,
|
||||
@@ -13,16 +18,32 @@ pub fn render(
|
||||
) -> usize {
|
||||
let line_count = buf.len();
|
||||
if line_count == 0 {
|
||||
ui.label("no hex data");
|
||||
ui.add(Label::new("no hex data").selectable(false));
|
||||
return 0;
|
||||
}
|
||||
|
||||
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||
ScrollArea::both().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
line_count,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let mut near_bottom_edge = false;
|
||||
|
||||
let scroll_area = ScrollArea::both()
|
||||
.id_salt("hex_text_area")
|
||||
.stick_to_bottom(true);
|
||||
|
||||
let output = scroll_area.show_viewport(ui, |ui, viewport| {
|
||||
let start_row = (viewport.min.y / row_height).floor().max(0.0) as usize;
|
||||
let end_row = ((viewport.max.y / row_height).ceil() as usize).min(line_count);
|
||||
|
||||
// Check if user is drag-selecting near the bottom edge of the scroll area
|
||||
near_bottom_edge = ui.ctx().input(|input| {
|
||||
input.pointer.button_down(egui::PointerButton::Primary)
|
||||
&& input.pointer.hover_pos().is_some_and(|pos| {
|
||||
let area = ui.max_rect();
|
||||
pos.y > area.bottom() - EDGE_SCROLL_ZONE
|
||||
&& pos.y < area.bottom() + 20.0
|
||||
})
|
||||
});
|
||||
|
||||
for row in start_row..end_row {
|
||||
if let Some(line) = buf.get(row) {
|
||||
ui.add(
|
||||
Label::new(format_hex_line(line, display, search, ui.style()))
|
||||
@@ -30,8 +51,20 @@ pub fn render(
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// If the user is dragging a selection near the bottom edge, nudge the scroll
|
||||
// offset for the next frame and request a repaint for smooth continuous scrolling.
|
||||
if near_bottom_edge {
|
||||
let mut state = output.state;
|
||||
let max_offset = (line_count as f32 * row_height)
|
||||
- output.inner_rect.height()
|
||||
+ EDGE_SCROLL_ZONE;
|
||||
state.offset.y = (state.offset.y + EDGE_SCROLL_NUDGE).min(max_offset.max(0.0));
|
||||
state.store(ui.ctx(), output.id);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
line_count
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
||||
use crate::perf::PlotRenderStats;
|
||||
use egui::{Button, Ui, vec2};
|
||||
use egui::{Button, Label, Ui, vec2};
|
||||
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -46,7 +46,7 @@ pub fn render(
|
||||
if ui.button(toggle_label).clicked() {
|
||||
toggle_detached = true;
|
||||
}
|
||||
ui.label("no plot data");
|
||||
ui.add(Label::new("no plot data").selectable(false));
|
||||
});
|
||||
return PlotRenderOutput {
|
||||
stats: PlotRenderStats {
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::path::PathBuf;
|
||||
use crate::app_state;
|
||||
use crate::logging::{self, LogWriter};
|
||||
use crate::models::{LineEnding, SendMode, SessionTab};
|
||||
use egui::{Color32, TextEdit};
|
||||
use egui::{Color32, Label, TextEdit};
|
||||
use pipeview_client::SessionManager;
|
||||
|
||||
pub fn render_send_panel(ui: &mut egui::Ui, manager: &SessionManager, tab: &mut SessionTab) {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.heading("Send");
|
||||
ui.add(Label::new(egui::RichText::new("Send").heading()).selectable(false));
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut tab.send_mode, SendMode::Text, "Text");
|
||||
ui.selectable_value(&mut tab.send_mode, SendMode::Hex, "Hex");
|
||||
@@ -95,12 +95,15 @@ pub fn render_send_panel(ui: &mut egui::Ui, manager: &SessionManager, tab: &mut
|
||||
ui.horizontal(|ui| {
|
||||
send_clicked = ui.button("Send").clicked();
|
||||
if let Some(status) = &tab.send_status {
|
||||
ui.label(
|
||||
ui.add(
|
||||
Label::new(
|
||||
egui::RichText::new(status).color(if status.starts_with("Send failed") {
|
||||
Color32::RED
|
||||
} else {
|
||||
Color32::GRAY
|
||||
}),
|
||||
)
|
||||
.selectable(false),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::models::{ConnectionStatus, SessionTab, View};
|
||||
use egui::TextEdit;
|
||||
use egui::{Label, TextEdit};
|
||||
use pipeview_client::SessionManager;
|
||||
use pipeview_core::transport::TransportConfig;
|
||||
|
||||
@@ -30,11 +30,14 @@ pub fn render_search_bar(ui: &mut egui::Ui, tab: &mut SessionTab) {
|
||||
tab.search.next();
|
||||
}
|
||||
|
||||
ui.label(format!(
|
||||
ui.add(
|
||||
Label::new(format!(
|
||||
"{}/{}",
|
||||
tab.search.current_display(),
|
||||
tab.search.match_count()
|
||||
));
|
||||
))
|
||||
.selectable(false),
|
||||
);
|
||||
|
||||
if ui.button("▲").clicked() {
|
||||
tab.search.prev();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::app::ConnectionStatus;
|
||||
use egui::{Color32, RichText, Ui};
|
||||
use egui::{Color32, Label, RichText, Ui};
|
||||
|
||||
pub struct SessionListItem {
|
||||
pub id: u64,
|
||||
@@ -15,7 +15,7 @@ pub fn render(
|
||||
on_edit: &mut Option<usize>,
|
||||
on_delete: &mut Option<usize>,
|
||||
) {
|
||||
ui.heading("Sessions");
|
||||
ui.add(Label::new(egui::RichText::new("Sessions").heading()).selectable(false));
|
||||
|
||||
if ui
|
||||
.button(RichText::new("+ New Session").color(Color32::GREEN))
|
||||
|
||||
Reference in New Issue
Block a user