Improve GUI plot workflow and performance
This commit is contained in:
@@ -1,36 +1,53 @@
|
||||
use crate::app::DisplayOptions;
|
||||
use crate::buffers::{LineDirection, TextBuffer};
|
||||
use egui::{ScrollArea, Ui};
|
||||
use crate::buffers::{ConsoleLine, LineDirection, TextBuffer};
|
||||
use egui::{Label, RichText, ScrollArea, TextStyle, TextWrapMode, Ui};
|
||||
|
||||
pub fn render(ui: &mut Ui, buf: &TextBuffer, display: DisplayOptions) {
|
||||
ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
||||
for line in buf.iter() {
|
||||
let mut prefix = Vec::new();
|
||||
if display.show_timestamp {
|
||||
let ts = line.elapsed.as_secs_f64();
|
||||
let time = if ts < 60.0 {
|
||||
format!("{:05.2}", ts)
|
||||
} else {
|
||||
format!("{:02}:{:02}", (ts / 60.0) as u64, (ts % 60.0) as u64)
|
||||
};
|
||||
prefix.push(format!("[{time}]"));
|
||||
pub fn render(ui: &mut Ui, buf: &TextBuffer, display: DisplayOptions) -> usize {
|
||||
let line_count = buf.len();
|
||||
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
line_count,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
if let Some(line) = buf.get(row) {
|
||||
ui.add(
|
||||
Label::new(RichText::new(format_console_line(line, display)).monospace())
|
||||
.wrap_mode(TextWrapMode::Extend),
|
||||
);
|
||||
}
|
||||
}
|
||||
if display.show_direction {
|
||||
let dir = match line.direction {
|
||||
LineDirection::In => "IN",
|
||||
LineDirection::Out => "OUT",
|
||||
};
|
||||
prefix.push(format!("[{dir}]"));
|
||||
}
|
||||
if display.show_pipeline {
|
||||
prefix.push(format!("[{}]", line.pipeline));
|
||||
}
|
||||
let prefix = if prefix.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{} ", prefix.join(" "))
|
||||
};
|
||||
ui.monospace(format!("{prefix}{}", line.text));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
line_count
|
||||
}
|
||||
|
||||
fn format_console_line(line: &ConsoleLine, display: DisplayOptions) -> String {
|
||||
let mut prefix = Vec::new();
|
||||
if display.show_timestamp {
|
||||
let ts = line.elapsed.as_secs_f64();
|
||||
let time = if ts < 60.0 {
|
||||
format!("{:05.2}", ts)
|
||||
} else {
|
||||
format!("{:02}:{:02}", (ts / 60.0) as u64, (ts % 60.0) as u64)
|
||||
};
|
||||
prefix.push(format!("[{time}]"));
|
||||
}
|
||||
if display.show_direction {
|
||||
let dir = match line.direction {
|
||||
LineDirection::In => "IN",
|
||||
LineDirection::Out => "OUT",
|
||||
};
|
||||
prefix.push(format!("[{dir}]"));
|
||||
}
|
||||
if display.show_pipeline {
|
||||
prefix.push(format!("[{}]", line.pipeline));
|
||||
}
|
||||
let prefix = if prefix.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{} ", prefix.join(" "))
|
||||
};
|
||||
format!("{prefix}{}", line.text)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
use crate::app::DisplayOptions;
|
||||
use crate::buffers::{HexBuffer, LineDirection};
|
||||
use egui::{ScrollArea, Ui};
|
||||
use crate::buffers::{HexBuffer, HexLine, LineDirection};
|
||||
use egui::{Label, RichText, ScrollArea, TextStyle, TextWrapMode, Ui};
|
||||
|
||||
pub fn render(ui: &mut Ui, buf: &HexBuffer, display: DisplayOptions) {
|
||||
let mut lines = buf.iter().peekable();
|
||||
if lines.peek().is_none() {
|
||||
pub fn render(ui: &mut Ui, buf: &HexBuffer, display: DisplayOptions) -> usize {
|
||||
let line_count = buf.len();
|
||||
if line_count == 0 {
|
||||
ui.label("no hex data");
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| {
|
||||
for line in lines {
|
||||
let mut prefix = Vec::new();
|
||||
if display.show_timestamp {
|
||||
let ts = line.elapsed.as_secs_f64();
|
||||
let time = if ts < 60.0 {
|
||||
format!("{:05.2}", ts)
|
||||
} else {
|
||||
format!("{:02}:{:02}", (ts / 60.0) as u64, (ts % 60.0) as u64)
|
||||
};
|
||||
prefix.push(format!("[{time}]"));
|
||||
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
||||
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
line_count,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
if let Some(line) = buf.get(row) {
|
||||
ui.add(
|
||||
Label::new(RichText::new(format_hex_line(line, display)).monospace())
|
||||
.wrap_mode(TextWrapMode::Extend),
|
||||
);
|
||||
}
|
||||
}
|
||||
if display.show_direction {
|
||||
let dir = match line.direction {
|
||||
LineDirection::In => "IN",
|
||||
LineDirection::Out => "OUT",
|
||||
};
|
||||
prefix.push(format!("[{dir}]"));
|
||||
}
|
||||
if display.show_pipeline {
|
||||
prefix.push(format!("[{}]", line.pipeline));
|
||||
}
|
||||
let prefix = if prefix.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{} ", prefix.join(" "))
|
||||
};
|
||||
ui.monospace(format!("{prefix}{} |{}|", line.hex, line.ascii));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
line_count
|
||||
}
|
||||
|
||||
fn format_hex_line(line: &HexLine, display: DisplayOptions) -> String {
|
||||
let mut prefix = Vec::new();
|
||||
if display.show_timestamp {
|
||||
let ts = line.elapsed.as_secs_f64();
|
||||
let time = if ts < 60.0 {
|
||||
format!("{:05.2}", ts)
|
||||
} else {
|
||||
format!("{:02}:{:02}", (ts / 60.0) as u64, (ts % 60.0) as u64)
|
||||
};
|
||||
prefix.push(format!("[{time}]"));
|
||||
}
|
||||
if display.show_direction {
|
||||
let dir = match line.direction {
|
||||
LineDirection::In => "IN",
|
||||
LineDirection::Out => "OUT",
|
||||
};
|
||||
prefix.push(format!("[{dir}]"));
|
||||
}
|
||||
if display.show_pipeline {
|
||||
prefix.push(format!("[{}]", line.pipeline));
|
||||
}
|
||||
let prefix = if prefix.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{} ", prefix.join(" "))
|
||||
};
|
||||
format!("{prefix}{} |{}|", line.hex, line.ascii)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
use crate::buffers::{PlotBuffer, PlotSeriesKind};
|
||||
use crate::perf::PlotRenderStats;
|
||||
use egui::{Button, Ui, vec2};
|
||||
use egui_plot::{Legend, Line, Plot, PlotBounds, PlotPoints};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PlotDisplayMode {
|
||||
Embedded,
|
||||
Detached,
|
||||
}
|
||||
|
||||
pub struct PlotViewState {
|
||||
pub lock_x: bool,
|
||||
pub lock_y: bool,
|
||||
pub box_zoom: bool,
|
||||
pub follow_latest: bool,
|
||||
pub last_bounds: Option<PlotBounds>,
|
||||
pub detached: bool,
|
||||
}
|
||||
|
||||
impl Default for PlotViewState {
|
||||
@@ -18,14 +26,48 @@ impl Default for PlotViewState {
|
||||
box_zoom: false,
|
||||
follow_latest: false,
|
||||
last_bounds: None,
|
||||
detached: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotViewState) {
|
||||
pub struct PlotRenderOutput {
|
||||
pub stats: PlotRenderStats,
|
||||
pub toggle_detached: bool,
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
ui: &mut Ui,
|
||||
buf: &mut PlotBuffer,
|
||||
session_id: u64,
|
||||
state: &mut PlotViewState,
|
||||
) -> PlotRenderOutput {
|
||||
let mode = if state.detached {
|
||||
PlotDisplayMode::Detached
|
||||
} else {
|
||||
PlotDisplayMode::Embedded
|
||||
};
|
||||
let mut toggle_detached = false;
|
||||
|
||||
if buf.is_empty() {
|
||||
ui.label("no plot data");
|
||||
return;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let toggle_label = match mode {
|
||||
PlotDisplayMode::Embedded => "Pop Out",
|
||||
PlotDisplayMode::Detached => "Dock Back",
|
||||
};
|
||||
if ui.button(toggle_label).clicked() {
|
||||
toggle_detached = true;
|
||||
}
|
||||
ui.label("no plot data");
|
||||
});
|
||||
return PlotRenderOutput {
|
||||
stats: PlotRenderStats {
|
||||
series_count: 0,
|
||||
stored_points: 0,
|
||||
rendered_points: 0,
|
||||
},
|
||||
toggle_detached,
|
||||
};
|
||||
}
|
||||
|
||||
let mut auto_fit = false;
|
||||
@@ -40,6 +82,13 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
||||
if ui.button("Reset View").clicked() {
|
||||
reset_view = true;
|
||||
}
|
||||
let toggle_label = match mode {
|
||||
PlotDisplayMode::Embedded => "Pop Out",
|
||||
PlotDisplayMode::Detached => "Dock Back",
|
||||
};
|
||||
if ui.button(toggle_label).clicked() {
|
||||
toggle_detached = true;
|
||||
}
|
||||
ui.separator();
|
||||
ui.checkbox(&mut state.lock_x, "Lock X");
|
||||
ui.checkbox(&mut state.lock_y, "Lock Y");
|
||||
@@ -92,7 +141,7 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
||||
});
|
||||
}
|
||||
|
||||
let data_bounds = plot_bounds(buf);
|
||||
let data_bounds = buf.plot_bounds();
|
||||
let mut plot = Plot::new(format!("plot_view_{session_id}"))
|
||||
.legend(Legend::default())
|
||||
.allow_boxed_zoom(state.box_zoom)
|
||||
@@ -107,6 +156,7 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
||||
plot = plot.reset();
|
||||
}
|
||||
|
||||
let mut rendered_points = 0usize;
|
||||
plot.show(ui, |plot_ui| {
|
||||
let current_bounds = plot_ui.plot_bounds();
|
||||
state.last_bounds = current_bounds.is_finite().then_some(current_bounds);
|
||||
@@ -123,7 +173,10 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
||||
if let Some(bounds) = data_bounds {
|
||||
let current_width = current_bounds.width();
|
||||
let data_width = bounds.width();
|
||||
let x_range = if current_width.is_finite() && current_width > 0.0 && data_width > current_width {
|
||||
let x_range = if current_width.is_finite()
|
||||
&& current_width > 0.0
|
||||
&& data_width > current_width
|
||||
{
|
||||
(bounds.max()[0] - current_width)..=bounds.max()[0]
|
||||
} else {
|
||||
bounds.min()[0]..=bounds.max()[0]
|
||||
@@ -132,7 +185,10 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
||||
plot_ui.set_plot_bounds_x(x_range.clone());
|
||||
|
||||
let mut followed = current_bounds;
|
||||
followed.set_x(&PlotBounds::from_min_max([*x_range.start(), current_bounds.min()[1]], [*x_range.end(), current_bounds.max()[1]]));
|
||||
followed.set_x(&PlotBounds::from_min_max(
|
||||
[*x_range.start(), current_bounds.min()[1]],
|
||||
[*x_range.end(), current_bounds.max()[1]],
|
||||
));
|
||||
state.last_bounds = Some(followed);
|
||||
}
|
||||
} else if zoom_x != 1.0 || zoom_y != 1.0 {
|
||||
@@ -151,49 +207,25 @@ pub fn render(ui: &mut Ui, buf: &PlotBuffer, session_id: u64, state: &mut PlotVi
|
||||
|
||||
for series in buf.iter() {
|
||||
let sampled_points = match buf.kind() {
|
||||
PlotSeriesKind::TimeSeries => {
|
||||
series.render_points_time_series(
|
||||
current_bounds.min()[0],
|
||||
current_bounds.max()[0],
|
||||
max_render_points,
|
||||
)
|
||||
}
|
||||
PlotSeriesKind::TimeSeries => series.render_points_time_series(
|
||||
current_bounds.min()[0],
|
||||
current_bounds.max()[0],
|
||||
max_render_points,
|
||||
),
|
||||
PlotSeriesKind::XY => series.render_points_xy(max_render_points),
|
||||
};
|
||||
rendered_points += sampled_points.len();
|
||||
let points = PlotPoints::from_iter(sampled_points);
|
||||
plot_ui.line(Line::new(series.name.clone(), points));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn plot_bounds(buf: &PlotBuffer) -> Option<PlotBounds> {
|
||||
let mut bounds = PlotBounds::NOTHING;
|
||||
|
||||
for series in buf.iter() {
|
||||
for [x, y] in series.points() {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
bounds.extend_with(&[x, y].into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bounds.is_finite() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if bounds.width() <= 0.0 {
|
||||
let center_x = bounds.center().x;
|
||||
bounds.set_x_center_width(center_x, 1.0);
|
||||
} else {
|
||||
bounds.expand_x(bounds.width() * 0.05);
|
||||
}
|
||||
|
||||
if bounds.height() <= 0.0 {
|
||||
let center_y = bounds.center().y;
|
||||
bounds.set_y_center_height(center_y, 1.0);
|
||||
} else {
|
||||
bounds.expand_y(bounds.height() * 0.1);
|
||||
}
|
||||
|
||||
Some(bounds)
|
||||
|
||||
PlotRenderOutput {
|
||||
stats: PlotRenderStats {
|
||||
series_count: buf.series_len(),
|
||||
stored_points: buf.total_points(),
|
||||
rendered_points,
|
||||
},
|
||||
toggle_detached,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub fn render(
|
||||
sessions: &[SessionListItem],
|
||||
active: &mut usize,
|
||||
on_new: &mut bool,
|
||||
on_edit: &mut Option<usize>,
|
||||
on_delete: &mut Option<usize>,
|
||||
) {
|
||||
ui.heading("Sessions");
|
||||
@@ -37,12 +38,23 @@ pub fn render(
|
||||
"{} Session {}\n{}",
|
||||
status_tag, session.id, session.transport_summary
|
||||
);
|
||||
if ui
|
||||
.selectable_label(*active == i, RichText::new(label).color(color))
|
||||
.clicked()
|
||||
{
|
||||
*active = i;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.selectable_label(*active == i, RichText::new(label).color(color))
|
||||
.clicked()
|
||||
{
|
||||
*active = i;
|
||||
}
|
||||
|
||||
if ui
|
||||
.small_button("⚙")
|
||||
.on_hover_text("Edit session config")
|
||||
.clicked()
|
||||
{
|
||||
*active = i;
|
||||
*on_edit = Some(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
Reference in New Issue
Block a user