Skip to content

Commit 8718aad

Browse files
committed
ui has been heavily redesigned
1 parent 4ee2257 commit 8718aad

6 files changed

Lines changed: 808 additions & 281 deletions

File tree

Lines changed: 3 additions & 0 deletions
Loading

src/app.rs

Lines changed: 164 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ const DIAGNOSTICS_PANEL_MIN_HEIGHT: f32 = 120.0;
7979
const POINTS_PARSE_DEBOUNCE_MS: u64 = 180;
8080
const POINTS_HISTORY_LIMIT: usize = 256;
8181
const POINTS_PARSE_ERROR_PREFIX: &str = "Points parse error: ";
82+
const UI_CORNER_RADIUS: u8 = 6;
83+
const PANEL_INNER_MARGIN_X: i8 = 10;
84+
const PANEL_INNER_MARGIN_Y: i8 = 8;
8285
const APP_VERSION_LABEL: &str = concat!("v", env!("CARGO_PKG_VERSION"));
8386
const APP_REPOSITORY_URL: &str = env!("CARGO_PKG_REPOSITORY");
8487

@@ -487,9 +490,7 @@ pub struct CurveFitApp {
487490
show_left_panel: bool,
488491
show_right_panel: bool,
489492
show_diagnostics_panel: bool,
490-
diagnostics_loss_axis_width: f32,
491-
diagnostics_residual_axis_width: f32,
492-
diagnostics_params_axis_width: f32,
493+
diagnostics_shared_axis_width: f32,
493494
iteration_delay_seconds: f64,
494495
spline_knots: usize,
495496
spline_knot_strategy: SplineKnotStrategy,
@@ -1065,6 +1066,141 @@ impl CurveFitApp {
10651066
self.status = Some(self.idle_status_after_points_edit());
10661067
}
10671068
}
1069+
1070+
fn fill_points_with_residuals(&mut self) {
1071+
if self.residual_plot_points.is_empty() {
1072+
return;
1073+
}
1074+
1075+
let points = match self
1076+
.residual_plot_points
1077+
.iter()
1078+
.map(|point| Point::try_new(point.x, point.y))
1079+
.collect::<Result<Vec<_>, _>>()
1080+
{
1081+
Ok(points) => points,
1082+
Err(error) => {
1083+
self.status = Some(StatusMessage::Error(format!(
1084+
"Failed to convert residual into point: {error}"
1085+
)));
1086+
return;
1087+
}
1088+
};
1089+
1090+
self.write_points_text(&points, true);
1091+
}
1092+
1093+
fn apply_visual_style(ctx: &egui::Context) {
1094+
ctx.style_mut(|style| {
1095+
style.spacing.item_spacing = egui::vec2(10.0, 8.0);
1096+
style.spacing.button_padding = egui::vec2(8.0, 5.0);
1097+
style.spacing.interact_size = egui::vec2(44.0, 26.0);
1098+
style.spacing.slider_width = 170.0;
1099+
style.spacing.combo_width = 180.0;
1100+
style.spacing.indent = 14.0;
1101+
1102+
style.text_styles.insert(
1103+
egui::TextStyle::Heading,
1104+
egui::FontId::new(21.0, egui::FontFamily::Proportional),
1105+
);
1106+
style.text_styles.insert(
1107+
egui::TextStyle::Body,
1108+
egui::FontId::new(14.0, egui::FontFamily::Proportional),
1109+
);
1110+
style.text_styles.insert(
1111+
egui::TextStyle::Button,
1112+
egui::FontId::new(14.0, egui::FontFamily::Proportional),
1113+
);
1114+
style.text_styles.insert(
1115+
egui::TextStyle::Monospace,
1116+
egui::FontId::new(13.0, egui::FontFamily::Monospace),
1117+
);
1118+
style.text_styles.insert(
1119+
egui::TextStyle::Small,
1120+
egui::FontId::new(12.0, egui::FontFamily::Proportional),
1121+
);
1122+
1123+
let visuals = &mut style.visuals;
1124+
visuals.widgets.noninteractive.corner_radius =
1125+
egui::CornerRadius::same(UI_CORNER_RADIUS);
1126+
visuals.widgets.inactive.corner_radius = egui::CornerRadius::same(UI_CORNER_RADIUS);
1127+
visuals.widgets.hovered.corner_radius = egui::CornerRadius::same(UI_CORNER_RADIUS);
1128+
visuals.widgets.active.corner_radius = egui::CornerRadius::same(UI_CORNER_RADIUS);
1129+
visuals.widgets.open.corner_radius = egui::CornerRadius::same(UI_CORNER_RADIUS);
1130+
1131+
if visuals.dark_mode {
1132+
visuals.panel_fill = egui::Color32::from_rgb(14, 17, 22);
1133+
visuals.window_fill = egui::Color32::from_rgb(17, 20, 26);
1134+
visuals.faint_bg_color = egui::Color32::from_rgb(24, 30, 38);
1135+
visuals.extreme_bg_color = egui::Color32::from_rgb(8, 11, 16);
1136+
visuals.code_bg_color = egui::Color32::from_rgb(10, 20, 28);
1137+
visuals.window_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(52, 70, 85));
1138+
visuals.selection.bg_fill = egui::Color32::from_rgb(22, 88, 120);
1139+
visuals.selection.stroke =
1140+
egui::Stroke::new(1.0, egui::Color32::from_rgb(152, 226, 255));
1141+
visuals.hyperlink_color = egui::Color32::from_rgb(94, 204, 255);
1142+
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(28, 35, 44);
1143+
visuals.widgets.inactive.bg_stroke =
1144+
egui::Stroke::new(1.0, egui::Color32::from_rgb(52, 70, 85));
1145+
visuals.widgets.hovered.weak_bg_fill = egui::Color32::from_rgb(34, 49, 61);
1146+
visuals.widgets.hovered.bg_stroke =
1147+
egui::Stroke::new(1.0, egui::Color32::from_rgb(70, 113, 138));
1148+
visuals.widgets.active.weak_bg_fill = egui::Color32::from_rgb(27, 84, 108);
1149+
visuals.widgets.active.bg_stroke =
1150+
egui::Stroke::new(1.0, egui::Color32::from_rgb(86, 171, 211));
1151+
visuals.widgets.open.weak_bg_fill = egui::Color32::from_rgb(33, 57, 73);
1152+
visuals.widgets.open.bg_stroke =
1153+
egui::Stroke::new(1.0, egui::Color32::from_rgb(72, 122, 150));
1154+
} else {
1155+
visuals.panel_fill = egui::Color32::from_rgb(239, 245, 249);
1156+
visuals.window_fill = egui::Color32::from_rgb(246, 250, 252);
1157+
visuals.faint_bg_color = egui::Color32::from_rgb(225, 236, 242);
1158+
visuals.extreme_bg_color = egui::Color32::from_rgb(251, 253, 255);
1159+
visuals.code_bg_color = egui::Color32::from_rgb(235, 245, 250);
1160+
visuals.window_stroke =
1161+
egui::Stroke::new(1.0, egui::Color32::from_rgb(165, 188, 201));
1162+
visuals.selection.bg_fill = egui::Color32::from_rgb(150, 214, 235);
1163+
visuals.selection.stroke =
1164+
egui::Stroke::new(1.0, egui::Color32::from_rgb(20, 76, 96));
1165+
visuals.hyperlink_color = egui::Color32::from_rgb(0, 118, 163);
1166+
visuals.widgets.inactive.weak_bg_fill = egui::Color32::from_rgb(220, 234, 241);
1167+
visuals.widgets.inactive.bg_stroke =
1168+
egui::Stroke::new(1.0, egui::Color32::from_rgb(163, 189, 203));
1169+
visuals.widgets.hovered.weak_bg_fill = egui::Color32::from_rgb(208, 227, 237);
1170+
visuals.widgets.hovered.bg_stroke =
1171+
egui::Stroke::new(1.0, egui::Color32::from_rgb(128, 170, 192));
1172+
visuals.widgets.active.weak_bg_fill = egui::Color32::from_rgb(183, 220, 236);
1173+
visuals.widgets.active.bg_stroke =
1174+
egui::Stroke::new(1.0, egui::Color32::from_rgb(87, 151, 182));
1175+
visuals.widgets.open.weak_bg_fill = egui::Color32::from_rgb(198, 224, 236);
1176+
visuals.widgets.open.bg_stroke =
1177+
egui::Stroke::new(1.0, egui::Color32::from_rgb(103, 160, 188));
1178+
}
1179+
});
1180+
}
1181+
1182+
fn side_panel_frame(style: &egui::Style) -> egui::Frame {
1183+
egui::Frame::side_top_panel(style)
1184+
.inner_margin(egui::Margin::symmetric(
1185+
PANEL_INNER_MARGIN_X,
1186+
PANEL_INNER_MARGIN_Y,
1187+
))
1188+
.fill(style.visuals.panel_fill)
1189+
.stroke(egui::Stroke::new(
1190+
1.0,
1191+
style.visuals.widgets.noninteractive.bg_stroke.color,
1192+
))
1193+
}
1194+
1195+
fn top_bottom_panel_frame(style: &egui::Style) -> egui::Frame {
1196+
egui::Frame::side_top_panel(style)
1197+
.inner_margin(egui::Margin::symmetric(PANEL_INNER_MARGIN_X, 6))
1198+
.fill(style.visuals.panel_fill)
1199+
.stroke(egui::Stroke::new(
1200+
1.0,
1201+
style.visuals.widgets.noninteractive.bg_stroke.color,
1202+
))
1203+
}
10681204
}
10691205

10701206
impl Default for CurveFitApp {
@@ -1121,9 +1257,7 @@ impl Default for CurveFitApp {
11211257
spline_initial_knot_y_inputs: Vec::new(),
11221258
show_right_panel: true,
11231259
show_diagnostics_panel: true,
1124-
diagnostics_loss_axis_width: 0.0,
1125-
diagnostics_residual_axis_width: 0.0,
1126-
diagnostics_params_axis_width: 0.0,
1260+
diagnostics_shared_axis_width: 0.0,
11271261
iteration_delay_seconds: 0.25,
11281262
fit_in_progress: false,
11291263
fit_preview_params: None,
@@ -1153,6 +1287,7 @@ impl Default for CurveFitApp {
11531287

11541288
impl eframe::App for CurveFitApp {
11551289
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
1290+
Self::apply_visual_style(ctx);
11561291
self.poll_fit_worker(ctx);
11571292
self.maybe_refresh_points_cache_after_debounce();
11581293

@@ -1172,64 +1307,43 @@ impl eframe::App for CurveFitApp {
11721307
}
11731308
}
11741309

1175-
egui::TopBottomPanel::top("header_panel").show(ctx, |ui| {
1176-
self.ui_header(ui);
1177-
});
1310+
let panel_style = ctx.style();
1311+
let panel_style = panel_style.as_ref();
11781312

1179-
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
1180-
self.ui_status_bar(ui);
1181-
});
1313+
egui::TopBottomPanel::top("header_panel")
1314+
.frame(Self::top_bottom_panel_frame(panel_style))
1315+
.show(ctx, |ui| {
1316+
self.ui_header(ui);
1317+
});
1318+
1319+
egui::TopBottomPanel::bottom("status_bar")
1320+
.frame(Self::top_bottom_panel_frame(panel_style))
1321+
.show(ctx, |ui| {
1322+
self.ui_status_bar(ui);
1323+
});
11821324

11831325
if self.show_left_panel {
11841326
egui::SidePanel::left("points_panel")
11851327
.default_width(340.0)
11861328
.resizable(true)
1329+
.frame(Self::side_panel_frame(panel_style))
11871330
.show(ctx, |ui| {
1188-
self.ui_tools(ui);
1189-
ui.separator();
1190-
self.ui_points_editor(ui);
1331+
ui.spacing_mut().item_spacing = egui::vec2(10.0, 8.0);
1332+
Self::panel_card_frame(ui).show(ui, |ui| self.ui_tools(ui));
1333+
Self::panel_card_frame(ui).show(ui, |ui| self.ui_points_editor(ui));
11911334
});
11921335
}
11931336

11941337
if self.show_right_panel {
11951338
egui::SidePanel::right("settings_panel")
11961339
.default_width(320.0)
11971340
.resizable(true)
1341+
.frame(Self::side_panel_frame(panel_style))
11981342
.show(ctx, |ui| {
1199-
let icon_tint = ui.visuals().text_color();
1200-
self.ui_family_and_params(ui);
1201-
self.ui_optimizer(ui);
1202-
1203-
ui.separator();
1204-
let action_button = if self.fit_in_progress {
1205-
egui::Button::image_and_text(
1206-
stop_icon_image(icon_tint),
1207-
tr(self.ui_language, "Stop", "Стоп"),
1208-
)
1209-
} else {
1210-
egui::Button::image_and_text(
1211-
fit_icon_image(icon_tint),
1212-
tr(self.ui_language, "Fit", "Фитинг"),
1213-
)
1214-
};
1215-
if ui.add(action_button).clicked() {
1216-
if self.fit_in_progress {
1217-
self.request_stop_fit();
1218-
} else {
1219-
self.run_fit();
1220-
}
1221-
}
1222-
if self.fit_in_progress
1223-
&& let Some(iteration) = self.fit_preview_iteration
1224-
{
1225-
ui.label(format!(
1226-
"{}: {iteration}",
1227-
tr(self.ui_language, "Iteration", "Итерация")
1228-
));
1229-
}
1230-
1231-
ui.separator();
1232-
self.ui_result(ui);
1343+
ui.spacing_mut().item_spacing = egui::vec2(10.0, 8.0);
1344+
Self::panel_card_frame(ui).show(ui, |ui| self.ui_family_and_params(ui));
1345+
Self::panel_card_frame(ui).show(ui, |ui| self.ui_optimizer(ui));
1346+
Self::panel_card_frame(ui).show(ui, |ui| self.ui_result(ui));
12331347
});
12341348
}
12351349

@@ -1238,6 +1352,7 @@ impl eframe::App for CurveFitApp {
12381352
.resizable(true)
12391353
.default_height(DIAGNOSTICS_PANEL_DEFAULT_HEIGHT)
12401354
.min_height(DIAGNOSTICS_PANEL_MIN_HEIGHT)
1355+
.frame(Self::top_bottom_panel_frame(panel_style))
12411356
.show(ctx, |ui| {
12421357
let available_height = ui.available_height();
12431358
ui.set_height(available_height);

src/app/diagnostics.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ fn upsert_iteration_point(series: &mut Vec<[f64; 2]>, iteration: f64, value: f64
9393
}
9494
}
9595

96-
pub(super) fn diagnostics_plot_y_axis_width(plot_response: &PlotResponse<()>) -> f32 {
97-
(plot_response.transform.frame().left() - plot_response.response.rect.left()).max(0.0)
96+
pub(super) fn diagnostics_plot_y_axis_width(
97+
plot_response: &PlotResponse<()>,
98+
plot_slot_left: f32,
99+
) -> f32 {
100+
(plot_response.response.rect.left() - plot_slot_left).max(0.0)
98101
}

src/app/i18n.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ pub(super) fn language_flag_image(language: UiLanguage) -> egui::Image<'static>
3131
egui::Image::new(source).fit_to_exact_size(egui::vec2(18.0, 13.5))
3232
}
3333

34-
pub(super) fn github_mark_image() -> egui::Image<'static> {
35-
egui::Image::new(egui::include_image!(
36-
"../../assets/tm/Octicons-mark-github.svg"
37-
))
38-
.fit_to_exact_size(egui::vec2(16.0, 16.0))
34+
pub(super) fn github_mark_image(dark_mode: bool) -> egui::Image<'static> {
35+
let source = if dark_mode {
36+
egui::include_image!("../../assets/tm/Octicons-mark-github-white.svg")
37+
} else {
38+
egui::include_image!("../../assets/tm/Octicons-mark-github.svg")
39+
};
40+
egui::Image::new(source).fit_to_exact_size(egui::vec2(16.0, 16.0))
3941
}
4042

4143
pub(super) fn fit_to_content_icon_image(tint: egui::Color32) -> egui::Image<'static> {

src/app/tests.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use super::{
33
ParamInitMethod, StatusMessage, UiLanguage, data_based_params_for_family,
44
};
55
use crate::domain::{CurveFamily, CurveParams, FitResult, OptimizerConfig, Point, Points};
6+
use egui_plot::PlotPoint;
67
#[cfg(not(target_arch = "wasm32"))]
78
use std::sync::{
89
Arc,
@@ -402,3 +403,35 @@ fn points_edit_parse_error_status_restores_completed_when_fixed() {
402403
app.refresh_status_after_points_edit();
403404
assert!(matches!(app.status, Some(StatusMessage::FitCompleted)));
404405
}
406+
407+
#[test]
408+
fn fill_points_with_residuals_replaces_points_text_and_pushes_undo() {
409+
let mut app = CurveFitApp {
410+
points_text: "0 1\n1 2\n".to_string(),
411+
residual_plot_points: vec![PlotPoint::new(0.0, -0.5), PlotPoint::new(1.0, 0.25)],
412+
..Default::default()
413+
};
414+
415+
app.fill_points_with_residuals();
416+
417+
assert_eq!(
418+
app.points_text,
419+
"0.00000000 -0.50000000\n1.00000000 0.25000000\n"
420+
);
421+
assert_eq!(app.points_undo_stack, vec!["0 1\n1 2\n".to_string()]);
422+
assert!(app.points_redo_stack.is_empty());
423+
}
424+
425+
#[test]
426+
fn fill_points_with_residuals_is_noop_when_residuals_are_absent() {
427+
let mut app = CurveFitApp {
428+
points_text: "0 1\n1 2\n".to_string(),
429+
..Default::default()
430+
};
431+
432+
app.fill_points_with_residuals();
433+
434+
assert_eq!(app.points_text, "0 1\n1 2\n");
435+
assert!(app.points_undo_stack.is_empty());
436+
assert!(app.points_redo_stack.is_empty());
437+
}

0 commit comments

Comments
 (0)