feat(dtmm): Implement rudimentary mod managing UI
This commit is contained in:
parent
8b2ca0e45a
commit
2cda35032c
2 changed files with 320 additions and 97 deletions
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue