Skip to content

Commit 785e528

Browse files
authored
Refactor: GUI (#285)
* feat: introduce ALPHA_LIGHT constant and update theme usage across components * refactor: unify dialog window management with a centralized registry * refactor: replace view_no_selection with detail_no_selection for consistency in empty states * refactor: simplify view methods by removing unnecessary clones and improving destructuring * feat: add helper functions and components for various tabs including Validation, Normalization, and Mapping
1 parent 34486ea commit 785e528

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3699
-3458
lines changed

crates/tss-gui/src/app/mod.rs

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use crate::handler::{
3131
MessageHandler, SourceAssignmentHandler,
3232
};
3333
use crate::message::{Message, SettingsCategory};
34-
use crate::state::{AppState, DialogType, Settings, ViewState};
34+
use crate::state::{AppState, DialogState, DialogType, Settings, ViewState};
3535
use crate::theme::clinical_theme;
3636
use crate::view::dialog::third_party::ThirdPartyState;
3737
use crate::view::dialog::update::UpdateState;
@@ -242,41 +242,29 @@ impl App {
242242
// Multi-window dialog management
243243
// =================================================================
244244
Message::DialogWindowOpened(dialog_type, id) => {
245-
match dialog_type {
246-
DialogType::About => {
247-
self.state.dialog_windows.about = Some(id);
245+
// Register dialog in the unified registry
246+
let dialog_state = match dialog_type {
247+
DialogType::About => DialogState::About,
248+
DialogType::Settings => DialogState::Settings(SettingsCategory::default()),
249+
DialogType::ThirdParty => DialogState::ThirdParty(ThirdPartyState::new()),
250+
DialogType::Update => DialogState::Update(UpdateState::Checking),
251+
DialogType::CloseProjectConfirm => DialogState::CloseProjectConfirm,
252+
// These dialogs have their state set elsewhere
253+
DialogType::ExportProgress
254+
| DialogType::ExportComplete
255+
| DialogType::UnsavedChanges => {
256+
return Task::none();
248257
}
249-
DialogType::Settings => {
250-
self.state.dialog_windows.settings =
251-
Some((id, SettingsCategory::default()));
252-
}
253-
DialogType::ThirdParty => {
254-
self.state.dialog_windows.third_party = Some((id, ThirdPartyState::new()));
255-
}
256-
DialogType::Update => {
257-
self.state.dialog_windows.update = Some((id, UpdateState::Checking));
258-
}
259-
DialogType::CloseProjectConfirm => {
260-
self.state.dialog_windows.close_project_confirm = Some(id);
261-
}
262-
DialogType::ExportProgress => {
263-
// Export progress state is set when export starts
264-
}
265-
DialogType::ExportComplete => {
266-
// Export complete state is set when export completes
267-
}
268-
DialogType::UnsavedChanges => {
269-
// Unsaved changes state is set when dialog is opened
270-
}
271-
}
258+
};
259+
self.state.dialog_registry.register(id, dialog_state);
272260
Task::none()
273261
}
274262

275263
Message::DialogWindowClosed(id) => {
276264
// Check if this is a dialog window
277-
if self.state.dialog_windows.is_dialog_window(id) {
265+
if self.state.dialog_registry.is_dialog_window(id) {
278266
// Clean up state and close the dialog window
279-
self.state.dialog_windows.close(id);
267+
self.state.dialog_registry.close(id);
280268
window::close(id)
281269
} else if self.state.main_window_id == Some(id) {
282270
// This is the main window - check for unsaved changes before exiting
@@ -297,7 +285,7 @@ impl App {
297285

298286
Message::CloseWindow(id) => {
299287
// Clean up dialog state before closing the window
300-
self.state.dialog_windows.close(id);
288+
self.state.dialog_registry.close(id);
301289
window::close(id)
302290
}
303291

@@ -397,15 +385,15 @@ impl App {
397385
verified,
398386
} => {
399387
// Update dialog state to ReadyToInstall
400-
if let Some((id, _)) = self.state.dialog_windows.update {
401-
self.state.dialog_windows.update = Some((
388+
if let Some((id, _)) = self.state.dialog_registry.update_mut() {
389+
self.state.dialog_registry.register(
402390
id,
403-
UpdateState::ReadyToInstall {
391+
DialogState::Update(UpdateState::ReadyToInstall {
404392
info,
405393
data,
406394
verified,
407-
},
408-
));
395+
}),
396+
);
409397
}
410398
Task::none()
411399
}
@@ -499,51 +487,45 @@ impl App {
499487
};
500488

501489
// Check if this is a dialog window
502-
if let Some(dialog_type) = self.state.dialog_windows.dialog_type(id) {
490+
if let Some(dialog_type) = self.state.dialog_registry.dialog_type(id) {
503491
return match dialog_type {
504492
DialogType::About => view_about_dialog_content(id),
505493
DialogType::Settings => {
506494
let category = self
507495
.state
508-
.dialog_windows
509-
.settings
510-
.as_ref()
496+
.dialog_registry
497+
.settings()
511498
.map(|(_, cat)| *cat)
512499
.unwrap_or_default();
513500
view_settings_dialog_content(&self.state.settings, category, id)
514501
}
515502
DialogType::ThirdParty => {
516-
if let Some((_, ref third_party_state)) = self.state.dialog_windows.third_party
517-
{
503+
if let Some((_, third_party_state)) = self.state.dialog_registry.third_party() {
518504
view_third_party_dialog_content(third_party_state)
519505
} else {
520506
iced::widget::text("Loading...").into()
521507
}
522508
}
523509
DialogType::Update => {
524-
// Get reference to update state from dialog_windows
525-
if let Some((_, ref update_state)) = self.state.dialog_windows.update {
510+
if let Some((_, update_state)) = self.state.dialog_registry.update() {
526511
view_update_dialog_content(update_state, id)
527512
} else {
528-
// This shouldn't happen - show loading text as fallback
529513
iced::widget::text("Loading...").into()
530514
}
531515
}
532516
DialogType::CloseProjectConfirm => view_close_project_dialog_content(id),
533517
DialogType::ExportProgress => {
534-
if let Some((_, ref progress_state)) = self.state.dialog_windows.export_progress
518+
if let Some((_, progress_state)) = self.state.dialog_registry.export_progress()
535519
{
536520
view_export_progress_dialog_content(progress_state, id)
537521
} else {
538-
// This shouldn't happen - show a simple loading text
539522
iced::widget::text("Loading...").into()
540523
}
541524
}
542525
DialogType::ExportComplete => {
543-
if let Some((_, ref result)) = self.state.dialog_windows.export_complete {
526+
if let Some((_, result)) = self.state.dialog_registry.export_complete() {
544527
view_export_complete_dialog_content(result, id)
545528
} else {
546-
// This shouldn't happen - show a simple close button
547529
iced::widget::text("Export dialog").into()
548530
}
549531
}
@@ -611,7 +593,7 @@ impl App {
611593
/// Get the window title for a specific window.
612594
pub fn title(&self, id: window::Id) -> String {
613595
// Check if this is a dialog window
614-
if let Some(dialog_type) = self.state.dialog_windows.dialog_type(id) {
596+
if let Some(dialog_type) = self.state.dialog_registry.dialog_type(id) {
615597
return match dialog_type {
616598
DialogType::About => "About Trial Submission Studio".to_string(),
617599
DialogType::Settings => "Settings".to_string(),

crates/tss-gui/src/component/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use tss_gui::component::{sidebar, SidebarItem};
4545
let items = vec![
4646
SidebarItem::new("DM", Message::DomainSelected("DM")),
4747
SidebarItem::new("AE", Message::DomainSelected("AE"))
48-
.with_badge("3"), // Error count badge
48+
.badge("3"), // Error count badge
4949
];
5050

5151
let nav = sidebar(items, Some(0), 280.0);

crates/tss-gui/src/component/display/action_button.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::theme::{ClinicalColors, SPACING_SM, SPACING_XS, button_primary, butto
1313
// =============================================================================
1414

1515
/// Button style variants.
16-
#[derive(Clone)]
16+
#[derive(Clone, Copy)]
1717
pub enum ActionButtonStyle {
1818
Primary,
1919
Secondary,
@@ -158,20 +158,25 @@ impl<'a, M: Clone + 'a> ActionButton<'a, M> {
158158

159159
/// Build the action button element.
160160
pub fn view(self) -> Element<'a, M> {
161-
let label = self.label.clone();
162-
let label2 = self.label;
163-
let icon_color = self.icon_color;
161+
let Self {
162+
icon,
163+
icon_color,
164+
label,
165+
on_press,
166+
style,
167+
full_width,
168+
} = self;
164169

165-
let content: Element<'a, M> = if let Some(icon) = self.icon {
170+
let content: Element<'a, M> = if let Some(ic) = icon {
166171
// Wrap the icon in a container for theming if needed
167172
let themed_icon: Element<'a, M> = match icon_color {
168-
IconColor::Themed(color_fn) => container(icon)
173+
IconColor::Themed(color_fn) => container(ic)
169174
.style(move |theme: &Theme| container::Style {
170175
text_color: Some(color_fn(theme)),
171176
..Default::default()
172177
})
173178
.into(),
174-
IconColor::None => icon,
179+
IconColor::None => ic,
175180
};
176181
row![
177182
themed_icon,
@@ -181,14 +186,12 @@ impl<'a, M: Clone + 'a> ActionButton<'a, M> {
181186
.align_y(Alignment::Center)
182187
.into()
183188
} else {
184-
text(label2).size(13).into()
189+
text(label).size(13).into()
185190
};
186191

187-
let style = self.style.clone();
188-
189-
let mut btn = button(content).on_press(self.on_press).padding([8.0, 16.0]);
192+
let mut btn = button(content).on_press(on_press).padding([8.0, 16.0]);
190193

191-
if self.full_width {
194+
if full_width {
192195
btn = btn.width(Length::Fill);
193196
}
194197

crates/tss-gui/src/component/display/data_table.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,16 @@ pub fn data_table<'a, M: Clone + 'a>(
9797
let header_row = {
9898
let mut header = row![].spacing(0);
9999
for col in columns {
100-
let col_header = col.header.clone();
101-
let col_width = col.width;
102100
header = header.push(
103101
container(
104-
text(col_header)
102+
text(&col.header)
105103
.size(12)
106104
.style(|theme: &Theme| text::Style {
107105
color: Some(theme.clinical().text_muted),
108106
})
109107
.font(iced::Font::DEFAULT),
110108
)
111-
.width(col_width)
109+
.width(col.width)
112110
.padding([TABLE_CELL_PADDING_Y, TABLE_CELL_PADDING_X])
113111
.style(|theme: &Theme| container::Style {
114112
background: Some(theme.clinical().background_secondary.into()),
@@ -138,10 +136,9 @@ pub fn data_table<'a, M: Clone + 'a>(
138136
.map(|c| c.width)
139137
.unwrap_or(Length::Fill);
140138
let is_even = row_idx % 2 == 0;
141-
let cell_text = cell.clone();
142139

143140
data_row = data_row.push(
144-
container(text(cell_text).size(13).style(|theme: &Theme| text::Style {
141+
container(text(cell).size(13).style(|theme: &Theme| text::Style {
145142
color: Some(theme.clinical().text_secondary),
146143
}))
147144
.width(width)

crates/tss-gui/src/component/display/domain_badge.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ use crate::theme::ClinicalColors;
2020
///
2121
/// Creates a pill-shaped badge with primary background for domain codes.
2222
pub fn domain_badge<'a, M: 'a>(code: &'a str) -> Element<'a, M> {
23-
let code_owned = code.to_string();
24-
25-
container(text(code_owned.clone()).size(14).style(|theme: &Theme| {
23+
container(text(code).size(14).style(|theme: &Theme| {
2624
let clinical = theme.clinical();
2725
text::Style {
2826
color: Some(clinical.text_on_accent),
@@ -47,9 +45,7 @@ pub fn domain_badge<'a, M: 'a>(code: &'a str) -> Element<'a, M> {
4745
///
4846
/// Smaller variant for compact contexts.
4947
pub fn domain_badge_small<'a, M: 'a>(code: &'a str) -> Element<'a, M> {
50-
let code_owned = code.to_string();
51-
52-
container(text(code_owned.clone()).size(12).style(|theme: &Theme| {
48+
container(text(code).size(12).style(|theme: &Theme| {
5349
let clinical = theme.clinical();
5450
text::Style {
5551
color: Some(clinical.text_on_accent),

crates/tss-gui/src/component/display/selectable_row.rs

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -76,36 +76,37 @@ impl<'a, M: Clone + 'a> SelectableRow<'a, M> {
7676

7777
/// Build the element.
7878
pub fn view(self) -> Element<'a, M> {
79-
let is_selected = self.selected;
80-
let primary_text = self.primary.clone();
79+
let Self {
80+
primary,
81+
secondary,
82+
leading,
83+
trailing,
84+
on_click,
85+
selected,
86+
} = self;
8187

8288
// Build content
8389
let mut content_row = row![].spacing(SPACING_SM).align_y(Alignment::Center);
8490

8591
// Leading element
86-
if let Some(leading) = self.leading {
87-
content_row = content_row.push(leading);
92+
if let Some(lead) = leading {
93+
content_row = content_row.push(lead);
8894
}
8995

9096
// Text section
91-
let text_section: Element<'a, M> = if let Some(secondary) = self.secondary {
92-
let secondary_text = secondary.clone();
97+
let text_section: Element<'a, M> = if let Some(sec) = secondary {
9398
column![
94-
text(primary_text)
95-
.size(13)
96-
.style(|theme: &Theme| text::Style {
97-
color: Some(theme.extended_palette().background.base.text),
98-
}),
99-
text(secondary_text)
100-
.size(11)
101-
.style(|theme: &Theme| text::Style {
102-
color: Some(theme.clinical().text_muted),
103-
}),
99+
text(primary).size(13).style(|theme: &Theme| text::Style {
100+
color: Some(theme.extended_palette().background.base.text),
101+
}),
102+
text(sec).size(11).style(|theme: &Theme| text::Style {
103+
color: Some(theme.clinical().text_muted),
104+
}),
104105
]
105106
.spacing(2.0)
106107
.into()
107108
} else {
108-
text(primary_text)
109+
text(primary)
109110
.size(13)
110111
.style(|theme: &Theme| text::Style {
111112
color: Some(theme.extended_palette().background.base.text),
@@ -118,17 +119,17 @@ impl<'a, M: Clone + 'a> SelectableRow<'a, M> {
118119
content_row = content_row.push(Space::new().width(Length::Fill));
119120

120121
// Trailing element
121-
if let Some(trailing) = self.trailing {
122-
content_row = content_row.push(trailing);
122+
if let Some(trail) = trailing {
123+
content_row = content_row.push(trail);
123124
}
124125

125126
// Button wrapper with styling
126127
button(content_row.padding([SPACING_SM, SPACING_SM]))
127-
.on_press(self.on_click)
128+
.on_press(on_click)
128129
.width(Length::Fill)
129130
.style(move |theme: &Theme, status| {
130131
let clinical = theme.clinical();
131-
let bg = if is_selected {
132+
let bg = if selected {
132133
Some(clinical.accent_primary_light.into())
133134
} else {
134135
match status {
@@ -138,7 +139,7 @@ impl<'a, M: Clone + 'a> SelectableRow<'a, M> {
138139
_ => None,
139140
}
140141
};
141-
let border_color = if is_selected {
142+
let border_color = if selected {
142143
theme.extended_palette().primary.base.color
143144
} else {
144145
clinical.border_default
@@ -150,7 +151,7 @@ impl<'a, M: Clone + 'a> SelectableRow<'a, M> {
150151
border: Border {
151152
radius: BORDER_RADIUS_SM.into(),
152153
color: border_color,
153-
width: if is_selected { 1.0 } else { 0.0 },
154+
width: if selected { 1.0 } else { 0.0 },
154155
},
155156
..Default::default()
156157
}

0 commit comments

Comments
 (0)