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::perf::{DrainStats, GuiProfiler, GuiSnapshot};
|
||||||
use crate::shortcuts::{self, default_bindings};
|
use crate::shortcuts::{self, default_bindings};
|
||||||
use crate::ui_fonts::{self, FontCandidate, UiFontSettings};
|
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::SessionManager;
|
||||||
use pipeview_client::config::SessionConfig;
|
use pipeview_client::config::SessionConfig;
|
||||||
use pipeview_client::session::SessionEvent;
|
use pipeview_client::session::SessionEvent;
|
||||||
@@ -537,7 +537,7 @@ impl XserialApp {
|
|||||||
fn render_main_panel(&mut self, ui: &mut egui::Ui) {
|
fn render_main_panel(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||||
if self.tabs.is_empty() {
|
if self.tabs.is_empty() {
|
||||||
ui.heading("No sessions.");
|
ui.add(Label::new(egui::RichText::new("No sessions.").heading()).selectable(false));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,10 +557,10 @@ impl XserialApp {
|
|||||||
let (badge, status_text) = tab.status.badge();
|
let (badge, status_text) = tab.status.badge();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.heading(format!("Session {}", tab.id));
|
ui.add(Label::new(egui::RichText::new(format!("Session {}", tab.id)).heading()).selectable(false));
|
||||||
ui.label(badge);
|
ui.add(Label::new(badge).selectable(false));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.label(status_text);
|
ui.add(Label::new(status_text).selectable(false));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(tab.view == View::Text, "Text")
|
.selectable_label(tab.view == View::Text, "Text")
|
||||||
@@ -584,7 +584,7 @@ impl XserialApp {
|
|||||||
|
|
||||||
let mut display_changed = false;
|
let mut display_changed = false;
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.label("Show:");
|
ui.add(Label::new("Show:").selectable(false));
|
||||||
display_changed |= ui
|
display_changed |= ui
|
||||||
.checkbox(&mut display.show_timestamp, "Timestamp")
|
.checkbox(&mut display.show_timestamp, "Timestamp")
|
||||||
.changed();
|
.changed();
|
||||||
@@ -598,7 +598,7 @@ impl XserialApp {
|
|||||||
session_controls::render_search_bar(ui, tab);
|
session_controls::render_search_bar(ui, tab);
|
||||||
|
|
||||||
if let ConnectionStatus::Error(message) = &tab.status {
|
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();
|
let full = ui.available_rect_before_wrap();
|
||||||
@@ -629,11 +629,22 @@ impl XserialApp {
|
|||||||
}
|
}
|
||||||
View::Plot => {
|
View::Plot => {
|
||||||
if tab.plot_view.detached {
|
if tab.plot_view.detached {
|
||||||
ui.heading(format!(
|
ui.add(
|
||||||
"Plot window detached for Session {}",
|
Label::new(
|
||||||
tab.id
|
egui::RichText::new(format!(
|
||||||
));
|
"Plot window detached for Session {}",
|
||||||
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() {
|
if ui.button("Dock Plot Back").clicked() {
|
||||||
tab.plot_view.detached = false;
|
tab.plot_view.detached = false;
|
||||||
}
|
}
|
||||||
@@ -720,7 +731,7 @@ impl XserialApp {
|
|||||||
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
fn render_top_bar(&mut self, ui: &mut egui::Ui) {
|
||||||
Panel::top("top_bar").show_inside(ui, |ui| {
|
Panel::top("top_bar").show_inside(ui, |ui| {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
ui.heading("pipeview");
|
ui.add(Label::new(egui::RichText::new("pipeview").heading()).selectable(false));
|
||||||
ui.separator();
|
ui.separator();
|
||||||
// ui.label(format!(
|
// ui.label(format!(
|
||||||
// "Fonts: {} + {} {:.1} pt",
|
// "Fonts: {} + {} {:.1} pt",
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ use egui::{
|
|||||||
text::{LayoutJob, TextFormat},
|
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(
|
pub fn render(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
buf: &TextBuffer,
|
buf: &TextBuffer,
|
||||||
@@ -14,21 +19,51 @@ pub fn render(
|
|||||||
) -> usize {
|
) -> usize {
|
||||||
let line_count = buf.len();
|
let line_count = buf.len();
|
||||||
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||||
ScrollArea::both().stick_to_bottom(true).show_rows(
|
let mut near_bottom_edge = false;
|
||||||
ui,
|
|
||||||
row_height,
|
let scroll_area = ScrollArea::both()
|
||||||
line_count,
|
.id_salt("console_text_area")
|
||||||
|ui, row_range| {
|
.stick_to_bottom(true);
|
||||||
for row in row_range {
|
|
||||||
if let Some(line) = buf.get(row) {
|
let output = scroll_area.show_viewport(ui, |ui, viewport| {
|
||||||
ui.add(
|
let start_row = (viewport.min.y / row_height).floor().max(0.0) as usize;
|
||||||
Label::new(format_console_line(line, display, search, ui.style()))
|
let end_row = ((viewport.max.y / row_height).ceil() as usize).min(line_count);
|
||||||
.wrap_mode(TextWrapMode::Extend),
|
|
||||||
);
|
// 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()))
|
||||||
|
.wrap_mode(TextWrapMode::Extend),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
// 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
|
line_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ use egui::{
|
|||||||
text::{LayoutJob, TextFormat},
|
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(
|
pub fn render(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
buf: &HexBuffer,
|
buf: &HexBuffer,
|
||||||
@@ -13,25 +18,53 @@ pub fn render(
|
|||||||
) -> usize {
|
) -> usize {
|
||||||
let line_count = buf.len();
|
let line_count = buf.len();
|
||||||
if line_count == 0 {
|
if line_count == 0 {
|
||||||
ui.label("no hex data");
|
ui.add(Label::new("no hex data").selectable(false));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||||
ScrollArea::both().stick_to_bottom(true).show_rows(
|
let mut near_bottom_edge = false;
|
||||||
ui,
|
|
||||||
row_height,
|
let scroll_area = ScrollArea::both()
|
||||||
line_count,
|
.id_salt("hex_text_area")
|
||||||
|ui, row_range| {
|
.stick_to_bottom(true);
|
||||||
for row in row_range {
|
|
||||||
if let Some(line) = buf.get(row) {
|
let output = scroll_area.show_viewport(ui, |ui, viewport| {
|
||||||
ui.add(
|
let start_row = (viewport.min.y / row_height).floor().max(0.0) as usize;
|
||||||
Label::new(format_hex_line(line, display, search, ui.style()))
|
let end_row = ((viewport.max.y / row_height).ceil() as usize).min(line_count);
|
||||||
.wrap_mode(TextWrapMode::Extend),
|
|
||||||
);
|
// 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()))
|
||||||
|
.wrap_mode(TextWrapMode::Extend),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
// 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
|
line_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
||||||
use crate::perf::PlotRenderStats;
|
use crate::perf::PlotRenderStats;
|
||||||
use egui::{Button, Ui, vec2};
|
use egui::{Button, Label, Ui, vec2};
|
||||||
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -46,7 +46,7 @@ pub fn render(
|
|||||||
if ui.button(toggle_label).clicked() {
|
if ui.button(toggle_label).clicked() {
|
||||||
toggle_detached = true;
|
toggle_detached = true;
|
||||||
}
|
}
|
||||||
ui.label("no plot data");
|
ui.add(Label::new("no plot data").selectable(false));
|
||||||
});
|
});
|
||||||
return PlotRenderOutput {
|
return PlotRenderOutput {
|
||||||
stats: PlotRenderStats {
|
stats: PlotRenderStats {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ use std::path::PathBuf;
|
|||||||
use crate::app_state;
|
use crate::app_state;
|
||||||
use crate::logging::{self, LogWriter};
|
use crate::logging::{self, LogWriter};
|
||||||
use crate::models::{LineEnding, SendMode, SessionTab};
|
use crate::models::{LineEnding, SendMode, SessionTab};
|
||||||
use egui::{Color32, TextEdit};
|
use egui::{Color32, Label, TextEdit};
|
||||||
use pipeview_client::SessionManager;
|
use pipeview_client::SessionManager;
|
||||||
|
|
||||||
pub fn render_send_panel(ui: &mut egui::Ui, manager: &SessionManager, tab: &mut SessionTab) {
|
pub fn render_send_panel(ui: &mut egui::Ui, manager: &SessionManager, tab: &mut SessionTab) {
|
||||||
ui.set_width(ui.available_width());
|
ui.set_width(ui.available_width());
|
||||||
ui.heading("Send");
|
ui.add(Label::new(egui::RichText::new("Send").heading()).selectable(false));
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.selectable_value(&mut tab.send_mode, SendMode::Text, "Text");
|
ui.selectable_value(&mut tab.send_mode, SendMode::Text, "Text");
|
||||||
ui.selectable_value(&mut tab.send_mode, SendMode::Hex, "Hex");
|
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| {
|
ui.horizontal(|ui| {
|
||||||
send_clicked = ui.button("Send").clicked();
|
send_clicked = ui.button("Send").clicked();
|
||||||
if let Some(status) = &tab.send_status {
|
if let Some(status) = &tab.send_status {
|
||||||
ui.label(
|
ui.add(
|
||||||
egui::RichText::new(status).color(if status.starts_with("Send failed") {
|
Label::new(
|
||||||
Color32::RED
|
egui::RichText::new(status).color(if status.starts_with("Send failed") {
|
||||||
} else {
|
Color32::RED
|
||||||
Color32::GRAY
|
} else {
|
||||||
}),
|
Color32::GRAY
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.selectable(false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::models::{ConnectionStatus, SessionTab, View};
|
use crate::models::{ConnectionStatus, SessionTab, View};
|
||||||
use egui::TextEdit;
|
use egui::{Label, TextEdit};
|
||||||
use pipeview_client::SessionManager;
|
use pipeview_client::SessionManager;
|
||||||
use pipeview_core::transport::TransportConfig;
|
use pipeview_core::transport::TransportConfig;
|
||||||
|
|
||||||
@@ -30,11 +30,14 @@ pub fn render_search_bar(ui: &mut egui::Ui, tab: &mut SessionTab) {
|
|||||||
tab.search.next();
|
tab.search.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.label(format!(
|
ui.add(
|
||||||
"{}/{}",
|
Label::new(format!(
|
||||||
tab.search.current_display(),
|
"{}/{}",
|
||||||
tab.search.match_count()
|
tab.search.current_display(),
|
||||||
));
|
tab.search.match_count()
|
||||||
|
))
|
||||||
|
.selectable(false),
|
||||||
|
);
|
||||||
|
|
||||||
if ui.button("▲").clicked() {
|
if ui.button("▲").clicked() {
|
||||||
tab.search.prev();
|
tab.search.prev();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::app::ConnectionStatus;
|
use crate::app::ConnectionStatus;
|
||||||
use egui::{Color32, RichText, Ui};
|
use egui::{Color32, Label, RichText, Ui};
|
||||||
|
|
||||||
pub struct SessionListItem {
|
pub struct SessionListItem {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@@ -15,7 +15,7 @@ pub fn render(
|
|||||||
on_edit: &mut Option<usize>,
|
on_edit: &mut Option<usize>,
|
||||||
on_delete: &mut Option<usize>,
|
on_delete: &mut Option<usize>,
|
||||||
) {
|
) {
|
||||||
ui.heading("Sessions");
|
ui.add(Label::new(egui::RichText::new("Sessions").heading()).selectable(false));
|
||||||
|
|
||||||
if ui
|
if ui
|
||||||
.button(RichText::new("+ New Session").color(Color32::GREEN))
|
.button(RichText::new("+ New Session").color(Color32::GREEN))
|
||||||
|
|||||||
Reference in New Issue
Block a user