Skip to main content

modde_ui/views/
executables.rs

1use crate::views::selectable_text::text;
2use iced::widget::{button, column, container, row, text_input};
3use iced::{Alignment, Element, Length, color};
4
5use crate::action_button::{ButtonAction, DescribedButtonExt};
6use crate::app::{ExecutableDraftField, Message, ToolState};
7
8/// Render the named executable launch target manager.
9pub fn view(state: &ToolState) -> Element<'_, Message> {
10    let title = state.game_label.as_deref().map_or_else(
11        || "Executables".to_string(),
12        |game| format!("Executables - {game}"),
13    );
14    let title_bar = row![
15        text(title).size(20),
16        iced::widget::space::horizontal(),
17        button(text("Refresh").size(14))
18            .style(button::secondary)
19            .padding([6, 14])
20            .on_action_maybe(
21                (!state.executables_loading).then_some(ButtonAction::RefreshExecutables),
22                "Executables are already loading.",
23            ),
24    ]
25    .align_y(Alignment::Center);
26
27    let mut content = column![title_bar].spacing(10);
28    if state.executables_loading {
29        content = content.push(
30            text("Loading executables...")
31                .size(12)
32                .color(color!(0xAAAAAA)),
33        );
34    }
35    if let Some(error) = &state.executables_load_error {
36        content = content.push(
37            text(format!("Failed to load executables: {error}"))
38                .size(12)
39                .color(color!(0xFF8888)),
40        );
41    }
42
43    content
44        .push(executables_panel(state))
45        .padding(12)
46        .width(Length::Fill)
47        .height(Length::Fill)
48        .into()
49}
50
51fn executables_panel(state: &ToolState) -> Element<'_, Message> {
52    let mut rows = column![].spacing(6);
53    if state.executables.is_empty() {
54        rows = rows.push(
55            container(
56                text("No executables configured")
57                    .size(12)
58                    .color(color!(0xAAAAAA)),
59            )
60            .padding([4, 0]),
61        );
62    } else {
63        for entry in &state.executables {
64            let busy = state.is_executable_busy(&entry.name);
65            let metadata = executable_metadata(entry);
66            rows = rows.push(
67                container(
68                    column![
69                        row![
70                            column![
71                                text(entry.name.as_str()).size(14),
72                                text(entry.executable_path.as_str())
73                                    .size(11)
74                                    .color(color!(0xAAAAAA)),
75                                text(metadata).size(11).color(color!(0x888888)),
76                            ]
77                            .spacing(2)
78                            .width(Length::Fill),
79                            button(text(if busy { "Running" } else { "Run" }).size(12))
80                                .style(button::success)
81                                .padding([4, 10])
82                                .on_action_maybe(
83                                    (!busy)
84                                        .then_some(ButtonAction::RunExecutable(entry.name.clone())),
85                                    "This executable is already running.",
86                                ),
87                            button(text("Edit").size(12))
88                                .style(button::secondary)
89                                .padding([4, 10])
90                                .on_action(ButtonAction::EditExecutable(entry.name.clone())),
91                            button(text("Remove").size(12))
92                                .style(button::danger)
93                                .padding([4, 10])
94                                .on_action_maybe(
95                                    (!busy).then_some(ButtonAction::RemoveExecutable(
96                                        entry.name.clone()
97                                    )),
98                                    "This executable is already running.",
99                                ),
100                        ]
101                        .spacing(8)
102                        .align_y(Alignment::Center),
103                    ]
104                    .spacing(4),
105                )
106                .padding(8)
107                .width(Length::Fill)
108                .style(container::rounded_box),
109            );
110        }
111    }
112
113    let count_label = match state.executables.len() {
114        0 => "0 configured".to_string(),
115        1 => "1 configured".to_string(),
116        count => format!("{count} configured"),
117    };
118    let editor_visible = state.executables.is_empty() || state.executable_editor_open;
119
120    let mut panel = column![
121        row![
122            text("Executables").size(16),
123            text(count_label).size(11).color(color!(0x888888)),
124            iced::widget::space::horizontal(),
125            button(text("Add executable").size(12))
126                .style(button::secondary)
127                .padding([5, 12])
128                .on_action(ButtonAction::OpenExecutableEditor),
129        ]
130        .align_y(Alignment::Center),
131        rows,
132    ]
133    .spacing(8);
134
135    if let Some(error) = &state.executable_error {
136        panel = panel.push(text(error.as_str()).size(12).color(color!(0xFF8888)));
137    }
138    if editor_visible {
139        panel = panel.push(executable_form(state));
140    }
141
142    container(panel)
143        .padding(12)
144        .width(Length::Fill)
145        .style(container::rounded_box)
146        .into()
147}
148
149fn executable_form(state: &ToolState) -> Element<'_, Message> {
150    let draft = &state.executable_draft;
151    let save_label = if draft.name.trim().is_empty() {
152        "Save"
153    } else {
154        "Save / update"
155    };
156
157    column![
158        row![
159            text_input("Name", &draft.name)
160                .on_input(|value| Message::UpdateExecutableDraft {
161                    field: ExecutableDraftField::Name,
162                    value,
163                })
164                .width(Length::FillPortion(2)),
165            text_input("Executable path", &draft.executable_path)
166                .on_input(|value| Message::UpdateExecutableDraft {
167                    field: ExecutableDraftField::Path,
168                    value,
169                })
170                .width(Length::FillPortion(4)),
171            button(text("Browse").size(12))
172                .style(button::secondary)
173                .padding([4, 10])
174                .on_action(ButtonAction::BrowseExecutablePath),
175        ]
176        .spacing(8),
177        row![
178            text_input("Arguments", &draft.arguments)
179                .on_input(|value| Message::UpdateExecutableDraft {
180                    field: ExecutableDraftField::Arguments,
181                    value,
182                })
183                .width(Length::Fill),
184            text_input("Output mod", &draft.output_mod)
185                .on_input(|value| Message::UpdateExecutableDraft {
186                    field: ExecutableDraftField::OutputMod,
187                    value,
188                })
189                .width(Length::FillPortion(1)),
190        ]
191        .spacing(8),
192        row![
193            text_input("Working directory", &draft.working_dir)
194                .on_input(|value| Message::UpdateExecutableDraft {
195                    field: ExecutableDraftField::WorkingDir,
196                    value,
197                })
198                .width(Length::Fill),
199            button(text("Browse").size(12))
200                .style(button::secondary)
201                .padding([4, 10])
202                .on_action(ButtonAction::BrowseExecutableWorkingDir),
203        ]
204        .spacing(8),
205        row![
206            text_input("WINEDLLOVERRIDES", &draft.wine_dll_overrides)
207                .on_input(|value| Message::UpdateExecutableDraft {
208                    field: ExecutableDraftField::WineDllOverrides,
209                    value,
210                })
211                .width(Length::Fill),
212            text_input("Env lines KEY=VALUE", &draft.environment)
213                .on_input(|value| Message::UpdateExecutableDraft {
214                    field: ExecutableDraftField::Environment,
215                    value,
216                })
217                .width(Length::Fill),
218        ]
219        .spacing(8),
220        row![
221            button(text(save_label).size(12))
222                .style(button::primary)
223                .padding([5, 12])
224                .on_action(ButtonAction::SaveExecutable),
225            button(text("Clear").size(12))
226                .style(button::secondary)
227                .padding([5, 12])
228                .on_action(ButtonAction::ClearExecutableDraft),
229        ]
230        .spacing(8),
231    ]
232    .spacing(8)
233    .into()
234}
235
236fn executable_metadata(entry: &crate::app::ExecutableUiEntry) -> String {
237    let mut parts = Vec::new();
238    if !entry.arguments.is_empty() {
239        parts.push(format!("args: {}", entry.arguments));
240    }
241    if !entry.working_dir.is_empty() {
242        parts.push(format!("cwd: {}", entry.working_dir));
243    }
244    if !entry.wine_dll_overrides.is_empty() {
245        parts.push(format!("dll: {}", entry.wine_dll_overrides));
246    }
247    parts.push(format!("output: {}", entry.output_mod));
248    parts.join(" | ")
249}