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
8pub 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}