Darktide Mod Manager #39

Merged
lucas merged 91 commits from feat/dtmm into master 2023-03-01 22:27:42 +01:00
2 changed files with 320 additions and 97 deletions
Showing only changes of commit 2cda35032c - Show all commits

View file

@ -1,11 +1,14 @@
use druid::im::Vector; use druid::im::Vector;
use druid::widget::{ use druid::widget::{
Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split,
ViewSwitcher, TextBox, ViewSwitcher,
}; };
use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc}; use druid::{lens, Insets, LensExt, Widget, WidgetExt, WindowDesc};
use crate::state::{ModInfo, State, View}; use crate::state::{
ModInfo, PathBufFormatter, State, StateController, View, ACTION_DELETE_SELECTED_MOD,
ACTION_SELECTED_MOD_DOWN, ACTION_SELECTED_MOD_UP, ACTION_SELECT_MOD,
};
use crate::theme; use crate::theme;
use crate::widget::ExtraWidgetExt; use crate::widget::ExtraWidgetExt;
@ -67,32 +70,21 @@ fn build_mod_list() -> impl Widget<State> {
let list = List::new(|| { let list = List::new(|| {
Flex::row() Flex::row()
.must_fill_main_axis(true) .must_fill_main_axis(true)
// .with_child( .with_child(
// Label::dynamic(|enabled, _env| { Label::dynamic(|enabled, _env| {
// if *enabled { if *enabled {
// "Enabled".into() "Enabled".into()
// } else { } else {
// "Disabled".into() "Disabled".into()
// } }
// }) })
// .lens( .lens(lens!((usize, ModInfo), 1).then(ModInfo::enabled)),
// lens::Identity )
// .map( .with_child(Label::raw().lens(lens!((usize, ModInfo), 1).then(ModInfo::name)))
// |(i, info)| info, .on_click(|ctx, (i, _info), _env| ctx.submit_notification(ACTION_SELECT_MOD.with(*i)))
// |(i, info), new_info| {
// todo!();
// },
// )
// .then(ModInfo::enabled),
// ),
// )
// .with_child(Label::raw().lens(ModInfo::name))
.on_click(|_ctx, state, _env| {
todo!();
})
}); });
Scroll::new(list) let scroll = Scroll::new(list)
.vertical() .vertical()
.lens(State::mods.map( .lens(State::mods.map(
|mods| { |mods| {
@ -107,7 +99,12 @@ fn build_mod_list() -> impl Widget<State> {
}); });
}, },
)) ))
.content_must_fill() .content_must_fill();
Flex::column()
.must_fill_main_axis(true)
.with_child(Flex::row())
.with_flex_child(scroll, 1.0)
} }
fn build_mod_details() -> impl Widget<State> { fn build_mod_details() -> impl Widget<State> {
@ -120,23 +117,16 @@ fn build_mod_details() -> impl Widget<State> {
}, },
Flex::column, Flex::column,
) )
.padding(Insets::uniform_xy(5.0, 1.0))
.lens(State::selected_mod); .lens(State::selected_mod);
let button_move_up = Button::new("Move Up") let button_move_up = Button::new("Move Up")
.on_click(|_ctx, index: &mut Option<usize>, _env| { .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_UP))
if let Some(i) = index.as_mut() { .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_up());
*i = i.saturating_sub(1)
}
})
.lens(State::selected_mod_index);
let button_move_down = Button::new("Move Down") let button_move_down = Button::new("Move Down")
.on_click(|_ctx, index: &mut Option<usize>, _env| { .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_DOWN))
if let Some(i) = index.as_mut() { .disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_down());
*i = i.saturating_add(1)
}
})
.lens(State::selected_mod_index);
let button_toggle_mod = Maybe::new( let button_toggle_mod = Maybe::new(
|| { || {
@ -144,17 +134,18 @@ fn build_mod_details() -> impl Widget<State> {
if *enabled { if *enabled {
"Disable Mod".into() "Disable Mod".into()
} else { } else {
"Enabled Mod".into() "Enable Mod".into()
} }
}) })
.on_click(|_ctx, info: &mut bool, _env| { .on_click(|_ctx, enabled: &mut bool, _env| {
*info = !*info; *enabled = !(*enabled);
}) })
.lens(ModInfo::enabled) .lens(ModInfo::enabled)
}, },
// TODO: Gray out // TODO: Gray out
|| Button::new("Enable Mod"), || Button::new("Enable Mod"),
) )
.disabled_if(|info: &Option<ModInfo>, _env: &druid::Env| info.is_none())
.lens(State::selected_mod); .lens(State::selected_mod);
let button_add_mod = Button::new("Add Mod").on_click(|_ctx, state: &mut State, _env| { let button_add_mod = Button::new("Add Mod").on_click(|_ctx, state: &mut State, _env| {
@ -164,7 +155,9 @@ fn build_mod_details() -> impl Widget<State> {
}); });
let button_delete_mod = Button::new("Delete Mod") let button_delete_mod = Button::new("Delete Mod")
.on_click(|_ctx, data: &mut State, _env| data.delete_selected_mod()); .on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_DELETE_SELECTED_MOD))
.disabled_if(|info: &Option<ModInfo>, _env: &druid::Env| info.is_none())
.lens(State::selected_mod);
let buttons = Flex::column() let buttons = Flex::column()
.with_child( .with_child(
@ -204,7 +197,29 @@ fn build_view_mods() -> impl Widget<State> {
} }
fn build_view_settings() -> impl Widget<State> { fn build_view_settings() -> impl Widget<State> {
Label::new("Settings") 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> { fn build_view_about() -> impl Widget<State> {
@ -233,4 +248,5 @@ fn build_window() -> impl Widget<State> {
.must_fill_main_axis(true) .must_fill_main_axis(true)
.with_child(build_top_bar()) .with_child(build_top_bar())
.with_flex_child(build_main(), 1.0) .with_flex_child(build_main(), 1.0)
.controller(StateController::new())
} }

View file

@ -1,9 +1,21 @@
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use druid::im::Vector; use druid::im::Vector;
use druid::{Data, Lens}; use druid::text::Formatter;
use druid::widget::Controller;
use druid::{
AppDelegate, Data, DelegateCtx, Env, Event, EventCtx, Handled, Lens, Selector, Target,
Widget,
};
use tokio::sync::mpsc::UnboundedSender;
#[derive(Copy, Clone, Data, PartialEq)] pub const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action..select-mod");
pub const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up");
pub const ACTION_SELECTED_MOD_DOWN: Selector = Selector::new("dtmm.action.selected-mod-down");
pub const ACTION_DELETE_SELECTED_MOD: Selector = Selector::new("dtmm.action.delete-selected-mod");
#[derive(Copy, Clone, Data, Debug, PartialEq)]
pub(crate) enum View { pub(crate) enum View {
Mods, Mods,
Settings, Settings,
@ -16,20 +28,48 @@ impl Default for View {
} }
} }
#[derive(Clone, Data, Lens)] #[derive(Clone, Data, Debug)]
pub struct PackageInfo {
name: String,
files: Vector<String>,
}
impl PackageInfo {
pub fn get_name(&self) -> &String {
&self.name
}
pub fn get_files(&self) -> &Vector<String> {
&self.files
}
}
#[derive(Clone, Data, Debug, Lens)]
pub(crate) struct ModInfo { pub(crate) struct ModInfo {
name: String, name: String,
description: Arc<String>, description: Arc<String>,
enabled: bool, enabled: bool,
#[lens(ignore)]
packages: Vector<PackageInfo>,
} }
impl ModInfo { impl ModInfo {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
name: format!("Test Mod: {:?}", std::time::SystemTime::now()), name: format!("Test Mod: {:?}", std::time::SystemTime::now()),
description: Arc::new(String::from("A test dummy")), description: Arc::new(String::from("A test dummy")),
enabled: false, enabled: false,
packages: Vector::new(),
} }
} }
pub fn get_packages(&self) -> &Vector<PackageInfo> {
&self.packages
}
pub(crate) fn get_name(&self) -> &String {
&self.name
}
} }
impl PartialEq for ModInfo { impl PartialEq for ModInfo {
@ -38,54 +78,15 @@ impl PartialEq for ModInfo {
} }
} }
#[derive(Clone, Data, Default, Lens)] #[derive(Clone, Data, Lens)]
pub(crate) struct State { pub(crate) struct State {
current_view: View, current_view: View,
mods: Vector<ModInfo>, mods: Vector<ModInfo>,
selected_mod_index: Option<usize>, selected_mod_index: Option<usize>,
} is_deployment_in_progress: bool,
game_dir: Arc<PathBuf>,
pub(crate) struct SelectedModLens; data_dir: Arc<PathBuf>,
ctx: Arc<sdk::Context>,
impl Lens<State, Option<ModInfo>> for SelectedModLens {
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
let info = data
.selected_mod_index
.and_then(|i| data.mods.get(i).cloned());
f(&info)
}
fn with_mut<V, F: FnOnce(&mut Option<ModInfo>) -> V>(&self, data: &mut State, f: F) -> V {
let mut info = data
.selected_mod_index
.and_then(|i| data.mods.get_mut(i).cloned());
f(&mut info)
}
}
/// A Lens that maps an `im::Vector<T>` to `im::Vector<(usize, T)>`,
/// where each element in the destination vector includes its index in the
/// source vector.
pub(crate) struct IndexedVectorLens;
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, data: &Vector<T>, f: F) -> V {
let data = data
.iter()
.enumerate()
.map(|(i, val)| (i, val.clone()))
.collect();
f(&data)
}
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
&self,
data: &mut Vector<T>,
f: F,
) -> V {
todo!()
}
} }
impl State { impl State {
@ -93,7 +94,26 @@ impl State {
pub const selected_mod: SelectedModLens = SelectedModLens; pub const selected_mod: SelectedModLens = SelectedModLens;
pub fn new() -> Self { pub fn new() -> Self {
Default::default() let ctx = sdk::Context::new();
let (game_dir, data_dir) = if cfg!(debug_assertions) {
(
std::env::current_dir().expect("PWD is borked").join("data"),
PathBuf::from("/tmp/dtmm"),
)
} else {
(PathBuf::new(), PathBuf::new())
};
Self {
ctx: Arc::new(ctx),
current_view: View::default(),
mods: Vector::new(),
selected_mod_index: None,
is_deployment_in_progress: false,
game_dir: Arc::new(game_dir),
data_dir: Arc::new(data_dir),
}
} }
pub fn get_current_view(&self) -> View { pub fn get_current_view(&self) -> View {
@ -104,16 +124,203 @@ impl State {
self.current_view = view; self.current_view = view;
} }
pub fn delete_selected_mod(&mut self) { pub fn get_mods(&self) -> Vector<ModInfo> {
let Some(index) = self.selected_mod_index else { self.mods.clone()
return; }
};
self.mods.remove(index); pub fn select_mod(&mut self, index: usize) {
self.selected_mod_index = Some(index);
} }
pub fn add_mod(&mut self, info: ModInfo) { pub fn add_mod(&mut self, info: ModInfo) {
self.mods.push_back(info); self.mods.push_back(info);
self.selected_mod_index = Some(self.mods.len() - 1); self.selected_mod_index = Some(self.mods.len() - 1);
} }
pub fn can_move_mod_down(&self) -> bool {
self.selected_mod_index
.map(|i| i < (self.mods.len().saturating_sub(1)))
.unwrap_or(false)
}
pub fn can_move_mod_up(&self) -> bool {
self.selected_mod_index.map(|i| i > 0).unwrap_or(false)
}
pub(crate) fn get_game_dir(&self) -> &PathBuf {
&self.game_dir
}
pub(crate) fn get_mod_dir(&self) -> PathBuf {
self.data_dir.join("mods")
}
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
self.ctx.clone()
}
}
pub(crate) struct SelectedModLens;
impl Lens<State, Option<ModInfo>> for SelectedModLens {
#[tracing::instrument(name = "SelectedModLens::with", skip_all)]
fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
let info = data
.selected_mod_index
.and_then(|i| data.mods.get(i).cloned());
f(&info)
}
#[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)]
fn with_mut<V, F: FnOnce(&mut Option<ModInfo>) -> V>(&self, data: &mut State, f: F) -> V {
match data.selected_mod_index {
Some(i) => {
let mut info = data.mods.get_mut(i).cloned();
let ret = f(&mut info);
if let Some(info) = info {
// TODO: Figure out a way to check for equality and
// only update when needed
data.mods.set(i, info);
} else {
data.selected_mod_index = None;
}
ret
}
None => f(&mut None),
}
}
}
/// A Lens that maps an `im::Vector<T>` to `im::Vector<(usize, T)>`,
/// where each element in the destination vector includes its index in the
/// source vector.
pub(crate) struct IndexedVectorLens;
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for IndexedVectorLens {
#[tracing::instrument(name = "IndexedVectorLens::with", skip_all)]
fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, values: &Vector<T>, f: F) -> V {
let indexed = values
.iter()
.enumerate()
.map(|(i, val)| (i, val.clone()))
.collect();
f(&indexed)
}
#[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)]
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
&self,
values: &mut Vector<T>,
f: F,
) -> V {
let mut indexed = values
.iter()
.enumerate()
.map(|(i, val)| (i, val.clone()))
.collect();
let ret = f(&mut indexed);
*values = indexed.into_iter().map(|(_i, val)| val).collect();
ret
}
}
pub struct StateController {}
impl StateController {
pub fn new() -> Self {
Self {}
}
}
// TODO: Turn notifications into commands on the AppDelegate
impl<W: Widget<State>> Controller<State, W> for StateController {
#[tracing::instrument(name = "StateController::event", skip_all)]
fn event(
&mut self,
child: &mut W,
ctx: &mut EventCtx,
event: &Event,
state: &mut State,
env: &Env,
) {
match event {
Event::Notification(notif) if notif.is(ACTION_SELECT_MOD) => {
ctx.set_handled();
let index = notif
.get(ACTION_SELECT_MOD)
.expect("notification type didn't match after check");
state.select_mod(*index);
}
Event::Notification(notif) if notif.is(ACTION_SELECTED_MOD_UP) => {
ctx.set_handled();
let Some(i) = state.selected_mod_index else {
return;
};
let len = state.mods.len();
if len == 0 || i == 0 {
return;
}
state.mods.swap(i, i - 1);
state.selected_mod_index = Some(i - 1);
}
Event::Notification(notif) if notif.is(ACTION_SELECTED_MOD_DOWN) => {
ctx.set_handled();
let Some(i) = state.selected_mod_index else {
return;
};
let len = state.mods.len();
if len == 0 || i == usize::MAX || i >= len - 1 {
return;
}
state.mods.swap(i, i + 1);
state.selected_mod_index = Some(i + 1);
}
Event::Notification(notif) if notif.is(ACTION_DELETE_SELECTED_MOD) => {
ctx.set_handled();
let Some(index) = state.selected_mod_index else {
return;
};
state.mods.remove(index);
}
_ => child.event(ctx, event, state, env),
}
}
}
pub(crate) struct PathBufFormatter;
impl PathBufFormatter {
pub fn new() -> Self {
Self {}
}
}
impl Formatter<Arc<PathBuf>> for PathBufFormatter {
fn format(&self, value: &Arc<PathBuf>) -> String {
value.display().to_string()
}
fn validate_partial_input(
&self,
_input: &str,
_sel: &druid::text::Selection,
) -> druid::text::Validation {
druid::text::Validation::success()
}
fn value(&self, input: &str) -> Result<Arc<PathBuf>, druid::text::ValidationError> {
let p = PathBuf::from(input);
Ok(Arc::new(p))
}
} }