316 lines
11 KiB
Rust
316 lines
11 KiB
Rust
use druid::im::Vector;
|
|
use druid::widget::{
|
|
Align, Button, Checkbox, CrossAxisAlignment, Flex, Label, LineBreaking, List,
|
|
MainAxisAlignment, Maybe, Scroll, SizedBox, Split, TextBox, ViewSwitcher,
|
|
};
|
|
use druid::{
|
|
lens, Color, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, Key, LensExt,
|
|
SingleUse, TextAlignment, Widget, WidgetExt, WindowDesc,
|
|
};
|
|
|
|
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
|
|
use crate::state::{
|
|
ACTION_ADD_MOD, ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
|
|
ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY,
|
|
};
|
|
use crate::ui::theme;
|
|
use crate::ui::widget::controller::AutoScrollController;
|
|
use crate::ui::widget::ExtraWidgetExt;
|
|
|
|
const TITLE: &str = "Darktide Mod Manager";
|
|
const WINDOW_SIZE: (f64, f64) = (1080., 720.);
|
|
const MOD_DETAILS_MIN_WIDTH: f64 = 325.;
|
|
|
|
const KEY_MOD_LIST_ITEM_BG_COLOR: Key<Color> = Key::new("dtmm.mod-list.item.background-color");
|
|
|
|
pub(crate) fn new() -> WindowDesc<State> {
|
|
WindowDesc::new(build_window())
|
|
.title(TITLE)
|
|
.window_size(WINDOW_SIZE)
|
|
}
|
|
|
|
fn build_top_bar() -> impl Widget<State> {
|
|
Flex::row()
|
|
.must_fill_main_axis(true)
|
|
.main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
|
.with_child(
|
|
Flex::row()
|
|
.with_child(
|
|
Button::new("Mods")
|
|
.on_click(|_ctx, state: &mut State, _env| state.current_view = View::Mods),
|
|
)
|
|
.with_default_spacer()
|
|
.with_child(
|
|
Button::new("Settings").on_click(|_ctx, state: &mut State, _env| {
|
|
state.current_view = View::Settings;
|
|
}),
|
|
)
|
|
.with_default_spacer()
|
|
.with_child(
|
|
Button::new("About")
|
|
.on_click(|_ctx, state: &mut State, _env| state.current_view = View::About),
|
|
),
|
|
)
|
|
.with_child(
|
|
Flex::row()
|
|
.with_child(
|
|
Button::new("Deploy Mods")
|
|
.on_click(|ctx, _state: &mut State, _env| {
|
|
ctx.submit_command(ACTION_START_DEPLOY);
|
|
})
|
|
.disabled_if(|data, _| {
|
|
data.is_deployment_in_progress || data.is_reset_in_progress
|
|
}),
|
|
)
|
|
.with_default_spacer()
|
|
.with_child(
|
|
Button::new("Reset Mods")
|
|
.on_click(|ctx, _state: &mut State, _env| {
|
|
ctx.submit_command(ACTION_START_RESET_DEPLOYMENT);
|
|
})
|
|
.disabled_if(|data, _| {
|
|
data.is_deployment_in_progress || data.is_reset_in_progress
|
|
}),
|
|
),
|
|
)
|
|
.padding(theme::TOP_BAR_INSETS)
|
|
.background(theme::TOP_BAR_BACKGROUND_COLOR)
|
|
// TODO: Add bottom border. Need a custom widget for that, as the built-in only provides
|
|
// uniform borders on all sides
|
|
}
|
|
|
|
fn build_mod_list() -> impl Widget<State> {
|
|
let list = List::new(|| {
|
|
let checkbox =
|
|
Checkbox::new("").lens(lens!((usize, ModInfo, bool), 1).then(ModInfo::enabled));
|
|
let name = Label::raw().lens(lens!((usize, ModInfo, bool), 1).then(ModInfo::name));
|
|
|
|
Flex::row()
|
|
.must_fill_main_axis(true)
|
|
.with_child(checkbox)
|
|
.with_child(name)
|
|
.padding((5.0, 4.0))
|
|
.background(KEY_MOD_LIST_ITEM_BG_COLOR)
|
|
.on_click(|ctx, (i, _, _), _env| ctx.submit_command(ACTION_SELECT_MOD.with(*i)))
|
|
.env_scope(|env, (i, _, selected)| {
|
|
if *selected {
|
|
env.set(KEY_MOD_LIST_ITEM_BG_COLOR, Color::NAVY);
|
|
} else if (i % 2) == 1 {
|
|
env.set(KEY_MOD_LIST_ITEM_BG_COLOR, Color::WHITE.with_alpha(0.05));
|
|
} else {
|
|
env.set(KEY_MOD_LIST_ITEM_BG_COLOR, Color::TRANSPARENT);
|
|
}
|
|
})
|
|
});
|
|
|
|
let scroll = Scroll::new(list)
|
|
.vertical()
|
|
.lens(lens::Identity.map(
|
|
|state: &State| {
|
|
state
|
|
.mods
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, val)| (i, val.clone(), Some(i) == state.selected_mod_index))
|
|
.collect::<Vector<_>>()
|
|
},
|
|
|state, infos| {
|
|
infos.into_iter().for_each(|(i, info, _)| {
|
|
state.mods.set(i, info);
|
|
});
|
|
},
|
|
))
|
|
.content_must_fill();
|
|
|
|
Flex::column()
|
|
.must_fill_main_axis(true)
|
|
.with_child(Flex::row())
|
|
.with_flex_child(scroll, 1.0)
|
|
}
|
|
|
|
fn build_mod_details_buttons() -> impl Widget<State> {
|
|
let button_move_up = Button::new("Move Up")
|
|
.on_click(|ctx, _state, _env| ctx.submit_command(ACTION_SELECTED_MOD_UP))
|
|
.disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_up());
|
|
|
|
let button_move_down = Button::new("Move Down")
|
|
.on_click(|ctx, _state, _env| ctx.submit_command(ACTION_SELECTED_MOD_DOWN))
|
|
.disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_down());
|
|
|
|
let button_toggle_mod = Maybe::new(
|
|
|| {
|
|
Button::dynamic(|enabled, _env| {
|
|
if *enabled {
|
|
"Disable Mod".into()
|
|
} else {
|
|
"Enable Mod".into()
|
|
}
|
|
})
|
|
.on_click(|_ctx, enabled: &mut bool, _env| {
|
|
*enabled = !(*enabled);
|
|
})
|
|
.lens(ModInfo::enabled)
|
|
},
|
|
// TODO: Gray out
|
|
|| Button::new("Enable Mod"),
|
|
)
|
|
.disabled_if(|info: &Option<ModInfo>, _env: &druid::Env| info.is_none())
|
|
.lens(State::selected_mod);
|
|
|
|
let button_add_mod = Button::new("Add Mod").on_click(|ctx, _state: &mut State, _env| {
|
|
let zip = FileSpec::new("Zip file", &["zip"]);
|
|
let opts = FileDialogOptions::new()
|
|
.allowed_types(vec![zip])
|
|
.default_type(zip)
|
|
.name_label("Mod Archive")
|
|
.title("Choose a mod to add")
|
|
.accept_command(ACTION_ADD_MOD);
|
|
ctx.submit_command(druid::commands::SHOW_OPEN_PANEL.with(opts))
|
|
});
|
|
|
|
let button_delete_mod = Button::new("Delete Mod")
|
|
.on_click(|ctx, data: &mut Option<ModInfo>, _env| {
|
|
if let Some(info) = data {
|
|
ctx.submit_command(
|
|
ACTION_START_DELETE_SELECTED_MOD.with(SingleUse::new(info.clone())),
|
|
);
|
|
}
|
|
})
|
|
.disabled_if(|info: &Option<ModInfo>, _env: &druid::Env| info.is_none())
|
|
.lens(State::selected_mod);
|
|
|
|
Flex::column()
|
|
.cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_child(
|
|
Flex::row()
|
|
.main_axis_alignment(MainAxisAlignment::End)
|
|
.with_child(button_move_up)
|
|
.with_default_spacer()
|
|
.with_child(button_move_down),
|
|
)
|
|
.with_default_spacer()
|
|
.with_child(
|
|
Flex::row()
|
|
.main_axis_alignment(MainAxisAlignment::End)
|
|
.with_child(button_toggle_mod)
|
|
.with_default_spacer()
|
|
.with_child(button_add_mod)
|
|
.with_default_spacer()
|
|
.with_child(button_delete_mod),
|
|
)
|
|
.expand_width()
|
|
}
|
|
|
|
fn build_mod_details_info() -> impl Widget<State> {
|
|
Maybe::new(
|
|
|| {
|
|
let name = Label::raw()
|
|
.with_text_alignment(TextAlignment::Center)
|
|
.with_text_size(24.)
|
|
// Force the label to take up the entire details' pane width,
|
|
// so that we can center-align it.
|
|
.expand_width()
|
|
.lens(ModInfo::name);
|
|
let description = Label::raw()
|
|
.with_line_break_mode(LineBreaking::WordWrap)
|
|
.lens(ModInfo::description);
|
|
|
|
Flex::column()
|
|
.cross_axis_alignment(CrossAxisAlignment::Start)
|
|
.main_axis_alignment(MainAxisAlignment::Start)
|
|
.with_child(name)
|
|
.with_spacer(4.)
|
|
.with_child(description)
|
|
},
|
|
Flex::column,
|
|
)
|
|
.padding((4., 4.))
|
|
.lens(State::selected_mod)
|
|
}
|
|
|
|
fn build_mod_details() -> impl Widget<State> {
|
|
Flex::column()
|
|
.must_fill_main_axis(true)
|
|
.cross_axis_alignment(CrossAxisAlignment::Start)
|
|
.main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
|
.with_flex_child(build_mod_details_info(), 1.0)
|
|
.with_child(build_mod_details_buttons().padding(4.))
|
|
}
|
|
|
|
fn build_view_mods() -> impl Widget<State> {
|
|
Split::columns(build_mod_list(), build_mod_details())
|
|
.split_point(0.75)
|
|
.min_size(0.0, MOD_DETAILS_MIN_WIDTH)
|
|
.solid_bar(true)
|
|
.bar_size(2.0)
|
|
.draggable(true)
|
|
}
|
|
|
|
fn build_view_settings() -> impl Widget<State> {
|
|
let game_dir_setting = Flex::row()
|
|
.main_axis_alignment(MainAxisAlignment::Start)
|
|
.with_child(Label::new("Game Directory:"))
|
|
.with_default_spacer()
|
|
.with_child(
|
|
TextBox::new()
|
|
.with_formatter(PathBufFormatter::new())
|
|
.lens(State::game_dir),
|
|
);
|
|
let data_dir_setting = Flex::row()
|
|
.main_axis_alignment(MainAxisAlignment::Start)
|
|
.with_child(Label::new("Data Directory:"))
|
|
.with_default_spacer()
|
|
.with_child(
|
|
TextBox::new()
|
|
.with_formatter(PathBufFormatter::new())
|
|
.lens(State::data_dir),
|
|
);
|
|
|
|
Flex::column()
|
|
.with_child(data_dir_setting)
|
|
.with_child(game_dir_setting)
|
|
.padding(Insets::uniform(5.0))
|
|
}
|
|
|
|
fn build_view_about() -> impl Widget<State> {
|
|
Align::centered(
|
|
Flex::column()
|
|
.with_child(Label::new("Darktide Mod Manager"))
|
|
.with_child(Label::new(
|
|
"Website: https://git.sclu1034.dev/bitsquid_dt/dtmt",
|
|
)),
|
|
)
|
|
}
|
|
|
|
fn build_main() -> impl Widget<State> {
|
|
ViewSwitcher::new(
|
|
|state: &State, _env| state.current_view,
|
|
|selector, _state, _env| match selector {
|
|
View::Mods => Box::new(build_view_mods()),
|
|
View::Settings => Box::new(build_view_settings()),
|
|
View::About => Box::new(build_view_about()),
|
|
},
|
|
)
|
|
}
|
|
|
|
fn build_log_view() -> impl Widget<State> {
|
|
let font = FontDescriptor::new(FontFamily::MONOSPACE);
|
|
let label = Label::raw()
|
|
.with_font(font)
|
|
.with_line_break_mode(LineBreaking::WordWrap)
|
|
.lens(State::log)
|
|
.scroll()
|
|
.vertical()
|
|
.controller(AutoScrollController);
|
|
|
|
SizedBox::new(label).expand_width().height(128.0)
|
|
}
|
|
|
|
fn build_window() -> impl Widget<State> {
|
|
// TODO: Add borders between the sections
|
|
Flex::column()
|
|
.must_fill_main_axis(true)
|
|
.with_child(build_top_bar())
|
|
.with_flex_child(build_main(), 1.0)
|
|
.with_child(build_log_view())
|
|
}
|