use crate::action_button::{ButtonAction, DescribedButtonExt};
use crate::views::selectable_text::text;
use iced::widget::{button, column, container, mouse_area, opaque, row, stack, text_input};
use iced::{Element, Length, Theme};
use super::{Message, Modde, View, external_refresh_stream};
impl Modde {
pub(super) fn view(&self) -> Element<'_, Message> {
let (mod_details_for_sidebar, save_details_for_sidebar) =
if matches!(self.active_view, View::Saves) {
(None, self.selected_save_details.as_ref())
} else {
(self.selected_mod_details.as_ref(), None)
};
let save_profiles_supported = self.current_game_supports_save_profiles();
let sidebar = crate::views::sidebar::view(
&self.active_view,
&self.collapsed_sidebar_groups,
&self.profiles,
&self.active_profile,
self.experiment_depth,
save_profiles_supported,
mod_details_for_sidebar,
save_details_for_sidebar,
);
let mods = self
.loaded_profile
.as_ref()
.map(|p| p.mods.as_slice())
.unwrap_or(&[]);
let settings_state = self.settings_state();
let content: Element<Message> = match &self.active_view {
View::ModList => crate::views::mod_list::view_filtered(
mods,
&self.mod_id_filter_keys,
&self.mod_filter,
self.selected_mod_index,
self.filter_mode,
&self.filter_criteria,
&self.collapsed_categories,
&self.mod_categories,
self.compact_mod_list,
self.loaded_profile
.as_ref()
.is_some_and(|p| p.load_order_lock.is_some()),
),
View::Collections => crate::views::collections::view(
&self.collection_search,
&self.collections,
&self.active_downloads,
),
View::BrowseNexus => {
let domain = self.browse_game_nexus_domain();
crate::views::browse_nexus::view(
&self.browse_nexus,
self.available_games.as_slice(),
domain,
)
}
View::FOMODWizard(_) => crate::views::fomod_wizard::view(self),
View::Settings => crate::views::settings::view(settings_state),
View::WabbajackInstaller(state) => crate::views::wabbajack::view(
state,
&self.wabbajack_manifest,
self.available_games.as_slice(),
self.current_game_id(),
),
View::Saves => crate::views::saves::view(
&self.save_snapshots,
self.loaded_profile.as_ref().map(|p| p.name.as_str()),
self.current_fingerprint.as_ref(),
save_profiles_supported,
self.selected_save_details
.as_ref()
.map(|d| d.commit_id.as_str()),
),
View::Downloads => {
let tasks = self.downloads_view_tasks();
crate::views::downloads::view(&tasks)
}
View::DataTab => {
crate::views::data_tab::view(&self.data_tab_state, &self.data_tab_conflicts)
}
View::Diagnostics => crate::views::diagnostics::view(&self.diagnostics_state),
View::Tools => crate::views::tools::view(&self.tool_state),
View::Executables => crate::views::executables::view(&self.tool_state),
};
let game_options = crate::views::game_picker::supported_game_options_ordered(
self.available_games.iter(),
&self.detected_games,
);
let selected_game = self.selected_game.as_ref().and_then(|id| {
game_options
.iter()
.find(|option| option.value == *id)
.cloned()
});
let game_picker = crate::views::game_picker::game_pick_list(
game_options,
selected_game,
"Select a game",
|option| Message::SelectGame(option.value),
);
let add_custom_game_button = crate::semantics::test_id(
"game_picker.add_custom_game",
button(text("+ Add custom game").size(12))
.style(button::secondary)
.padding([4, 10])
.on_action(ButtonAction::OpenAddCustomGame),
);
let manage_custom_games_button = crate::semantics::test_id(
"game_picker.manage_custom_games",
button(text("Manage custom games").size(12))
.style(button::secondary)
.padding([4, 10])
.on_action(ButtonAction::OpenManageCustomGames),
);
let title_label = text("modde").size(14);
let window_controls = row![
button(text("\u{2212}").size(12))
.style(button::secondary)
.padding([2, 10])
.on_action(ButtonAction::WindowMinimize),
button(text("\u{25A1}").size(12))
.style(button::secondary)
.padding([2, 10])
.on_action(ButtonAction::WindowToggleMaximize),
button(text("\u{2715}").size(12))
.style(button::danger)
.padding([2, 10])
.on_action(ButtonAction::WindowClose),
]
.spacing(2);
let title_bar_content = row![
game_picker,
add_custom_game_button,
manage_custom_games_button,
iced::widget::Space::new().width(Length::Fill),
title_label,
iced::widget::Space::new().width(Length::Fill),
window_controls,
]
.align_y(iced::Alignment::Center)
.spacing(8);
let title_bar = mouse_area(
container(title_bar_content)
.padding([4, 8])
.width(Length::Fill)
.style(container::rounded_box),
)
.on_press(Message::TitleBarDrag);
let status_bar = container(text(&self.status_message).size(12)).padding(5);
let update_banner = self
.update_available
.as_ref()
.map(crate::components::update_banner::view);
let body: Element<Message> = if let Some(update_banner) = update_banner {
column![
update_banner,
row![sidebar, content].spacing(0).height(Length::Fill)
]
.spacing(0)
.into()
} else {
row![sidebar, content]
.spacing(0)
.height(Length::Fill)
.into()
};
let main_layout = column![title_bar, body, status_bar,].spacing(0);
let mut base: Element<Message> = container(main_layout)
.width(Length::Fill)
.height(Length::Fill)
.into();
let toast_layer: Element<Message> = if let Some(toast) = self.button_hover_toast.visible {
let toast_content: Element<Message> = container(text(toast.description).size(12))
.padding([8, 12])
.width(Length::Shrink)
.style(container::rounded_box)
.into();
container(
column![
iced::widget::Space::new().height(Length::Fill),
row![
iced::widget::Space::new().width(Length::Fill),
toast_content
]
]
.padding(16),
)
.width(Length::Fill)
.height(Length::Fill)
.into()
} else {
container(iced::widget::Space::new())
.width(Length::Fill)
.height(Length::Fill)
.into()
};
base = stack([base, toast_layer]).into();
if self.new_profile_dialog_open {
base = stack([base, self.new_profile_dialog()]).into();
}
if self.game_path_dialog_open {
base = stack([base, self.game_path_dialog()]).into();
}
if self.add_custom_game_dialog_open {
base = stack([base, self.add_custom_game_modal()]).into();
}
if self.manage_custom_games_dialog_open {
base = stack([base, self.manage_custom_games_modal()]).into();
}
crate::shortcut_layer::shortcut_layer(base).into()
}
fn new_profile_dialog(&self) -> Element<'_, Message> {
let trimmed_name = self.new_profile_name.trim();
let can_create = !trimmed_name.is_empty() && self.selected_game.is_some();
let submit = can_create.then_some(Message::SubmitNewProfileDialog);
let submit_action = can_create.then_some(ButtonAction::SubmitNewProfileDialog);
let dialog = container(
column![
text("New Profile").size(18),
text_input("Profile name...", &self.new_profile_name)
.on_input(Message::NewProfileNameChanged)
.on_submit_maybe(submit.clone())
.padding(8)
.width(Length::Fill),
row![
iced::widget::Space::new().width(Length::Fill),
button(text("Cancel").size(13))
.style(button::secondary)
.padding([6, 14])
.on_action(ButtonAction::CancelNewProfileDialog),
button(text("Create").size(13))
.style(button::success)
.padding([6, 14])
.on_action_maybe(
submit_action,
"Enter a profile name and select a game before creating the profile.",
),
]
.spacing(8)
.align_y(iced::Alignment::Center),
]
.spacing(12),
)
.width(Length::Fixed(360.0))
.padding(16)
.style(container::rounded_box);
opaque(
container(dialog)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill),
)
}
fn game_path_dialog(&self) -> Element<'_, Message> {
let game_id = self
.pending_game_path_game_id
.as_deref()
.unwrap_or("the selected game");
let game_label = modde_games::resolve_game_plugin(game_id)
.map(modde_games::GamePlugin::display_name)
.unwrap_or(game_id);
let mut body = column![
text("Game Path Required").size(18),
text(format!(
"modde could not detect {game_label}. Select the game installation directory to continue."
))
.size(13),
]
.spacing(12);
if let Some(error) = &self.game_path_dialog_error {
body = body.push(text(error).size(12).color(iced::color!(0xFF6666)));
}
let dialog = container(
body.push(
row![
iced::widget::Space::new().width(Length::Fill),
button(text("Cancel").size(13))
.style(button::secondary)
.padding([6, 14])
.on_action(ButtonAction::CancelGamePathDialog),
button(text("Browse").size(13))
.style(button::primary)
.padding([6, 14])
.on_action(ButtonAction::GamePathDialogBrowse),
]
.spacing(8)
.align_y(iced::Alignment::Center),
),
)
.width(Length::Fixed(420.0))
.padding(16)
.style(container::rounded_box);
opaque(
container(dialog)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill),
)
}
pub(super) fn theme(&self) -> Theme {
match self.theme_name.as_str() {
"Light" => Theme::Light,
"Dracula" => Theme::Dracula,
"Nord" => Theme::Nord,
"Gruvbox Dark" => Theme::GruvboxDark,
"Catppuccin Mocha" => Theme::CatppuccinMocha,
_ => Theme::Dark,
}
}
pub(super) fn subscription(&self) -> iced::Subscription<Message> {
iced::Subscription::run(external_refresh_stream)
}
}