feat(dtmm): Implement rudimentary mod managing UI

This commit is contained in:
Lucas Schwiderski 2023-02-17 23:06:10 +01:00
parent 8b2ca0e45a
commit 2cda35032c
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
2 changed files with 320 additions and 97 deletions

View file

@ -1,11 +1,14 @@
use druid::im::Vector;
use druid::widget::{
Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split,
ViewSwitcher,
TextBox, ViewSwitcher,
};
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::widget::ExtraWidgetExt;
@ -67,32 +70,21 @@ fn build_mod_list() -> impl Widget<State> {
let list = List::new(|| {
Flex::row()
.must_fill_main_axis(true)
// .with_child(
// Label::dynamic(|enabled, _env| {
// if *enabled {
// "Enabled".into()
// } else {
// "Disabled".into()
// }
// })
// .lens(
// lens::Identity
// .map(
// |(i, info)| info,
// |(i, info), new_info| {
// todo!();
// },
// )
// .then(ModInfo::enabled),
// ),
// )
// .with_child(Label::raw().lens(ModInfo::name))
.on_click(|_ctx, state, _env| {
todo!();
.with_child(
Label::dynamic(|enabled, _env| {
if *enabled {
"Enabled".into()
} else {
"Disabled".into()
}
})
.lens(lens!((usize, ModInfo), 1).then(ModInfo::enabled)),
)
.with_child(Label::raw().lens(lens!((usize, ModInfo), 1).then(ModInfo::name)))
.on_click(|ctx, (i, _info), _env| ctx.submit_notification(ACTION_SELECT_MOD.with(*i)))
});
Scroll::new(list)
let scroll = Scroll::new(list)
.vertical()
.lens(State::mods.map(
|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> {
@ -120,23 +117,16 @@ fn build_mod_details() -> impl Widget<State> {
},
Flex::column,
)
.padding(Insets::uniform_xy(5.0, 1.0))
.lens(State::selected_mod);
let button_move_up = Button::new("Move Up")
.on_click(|_ctx, index: &mut Option<usize>, _env| {
if let Some(i) = index.as_mut() {
*i = i.saturating_sub(1)
}
})
.lens(State::selected_mod_index);
.on_click(|ctx, _state, _env| ctx.submit_notification(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, index: &mut Option<usize>, _env| {
if let Some(i) = index.as_mut() {
*i = i.saturating_add(1)
}
})
.lens(State::selected_mod_index);
.on_click(|ctx, _state, _env| ctx.submit_notification(ACTION_SELECTED_MOD_DOWN))
.disabled_if(|state: &State, _env: &druid::Env| !state.can_move_mod_down());
let button_toggle_mod = Maybe::new(
|| {
@ -144,17 +134,18 @@ fn build_mod_details() -> impl Widget<State> {
if *enabled {
"Disable Mod".into()
} else {
"Enabled Mod".into()
"Enable Mod".into()
}
})
.on_click(|_ctx, info: &mut bool, _env| {
*info = !*info;
.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| {
@ -164,7 +155,9 @@ fn build_mod_details() -> impl Widget<State> {
});
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()
.with_child(
@ -204,7 +197,29 @@ fn build_view_mods() -> 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> {
@ -233,4 +248,5 @@ fn build_window() -> impl Widget<State> {
.must_fill_main_axis(true)
.with_child(build_top_bar())
.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 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 {
Mods,
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 {
name: String,
description: Arc<String>,
enabled: bool,
#[lens(ignore)]
packages: Vector<PackageInfo>,
}
impl ModInfo {
pub fn new() -> Self {
Self {
name: format!("Test Mod: {:?}", std::time::SystemTime::now()),
description: Arc::new(String::from("A test dummy")),
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 {
@ -38,54 +78,15 @@ impl PartialEq for ModInfo {
}
}
#[derive(Clone, Data, Default, Lens)]
#[derive(Clone, Data, Lens)]
pub(crate) struct State {
current_view: View,
mods: Vector<ModInfo>,
selected_mod_index: Option<usize>,
}
pub(crate) struct SelectedModLens;
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!()
}
is_deployment_in_progress: bool,
game_dir: Arc<PathBuf>,
data_dir: Arc<PathBuf>,
ctx: Arc<sdk::Context>,
}
impl State {
@ -93,7 +94,26 @@ impl State {
pub const selected_mod: SelectedModLens = SelectedModLens;
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 {
@ -104,16 +124,203 @@ impl State {
self.current_view = view;
}
pub fn delete_selected_mod(&mut self) {
let Some(index) = self.selected_mod_index else {
return;
};
pub fn get_mods(&self) -> Vector<ModInfo> {
self.mods.clone()
}
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) {
self.mods.push_back(info);
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))
}
}