This commit is contained in:
Lucas Schwiderski 2023-01-18 00:32:31 +01:00
parent 50569a3c56
commit a25a6b2917
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
7 changed files with 356 additions and 65 deletions

View file

@ -0,0 +1,47 @@
use druid::widget::{Button, Controller};
use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, UpdateCtx, Widget};
pub struct DisabledButtonController;
impl<T: Data> Controller<T, Button<T>> for DisabledButtonController {
fn event(
&mut self,
child: &mut Button<T>,
ctx: &mut EventCtx,
event: &Event,
data: &mut T,
env: &Env,
) {
if !ctx.is_disabled() {
ctx.set_disabled(true);
ctx.request_paint();
}
child.event(ctx, event, data, env)
}
fn lifecycle(
&mut self,
child: &mut Button<T>,
ctx: &mut LifeCycleCtx,
event: &LifeCycle,
data: &T,
env: &Env,
) {
child.lifecycle(ctx, event, data, env)
}
fn update(
&mut self,
child: &mut Button<T>,
ctx: &mut UpdateCtx,
old_data: &T,
data: &T,
env: &Env,
) {
if !ctx.is_disabled() {
ctx.set_disabled(true);
ctx.request_paint();
}
child.update(ctx, old_data, data, env)
}
}

View file

@ -1,3 +1,5 @@
#![feature(let_chains)]
use clap::command; use clap::command;
use color_eyre::Report; use color_eyre::Report;
use color_eyre::Result; use color_eyre::Result;
@ -8,6 +10,7 @@ use tracing_subscriber::EnvFilter;
use crate::state::State; use crate::state::State;
mod controller;
mod main_window; mod main_window;
mod state; mod state;
mod theme; mod theme;

View file

@ -5,19 +5,21 @@ use druid::widget::{
}; };
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, 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;
const TITLE: &str = "Darktide Mod Manager"; const TITLE: &str = "Darktide Mod Manager";
const WINDOW_WIDTH: f64 = 800.0; const WINDOW_SIZE: (f64, f64) = (800.0, 600.0);
const WINDOW_HEIGHT: f64 = 600.0;
const MOD_DETAILS_MIN_WIDTH: f64 = 325.0; const MOD_DETAILS_MIN_WIDTH: f64 = 325.0;
pub(crate) fn new() -> WindowDesc<State> { pub(crate) fn new() -> WindowDesc<State> {
WindowDesc::new(build_window()) WindowDesc::new(build_window())
.title(TITLE) .title(TITLE)
.window_size((WINDOW_WIDTH, WINDOW_HEIGHT)) .window_size(WINDOW_SIZE)
} }
fn build_top_bar() -> impl Widget<State> { fn build_top_bar() -> impl Widget<State> {
@ -33,9 +35,11 @@ fn build_top_bar() -> impl Widget<State> {
) )
.with_default_spacer() .with_default_spacer()
.with_child( .with_child(
Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { Button::new("Settings")
state.set_current_view(View::Settings) .on_click(|_ctx, state: &mut State, _env| {
}), state.set_current_view(View::Settings)
})
.hidden_if(|_, _| true),
) )
.with_default_spacer() .with_default_spacer()
.with_child( .with_child(
@ -68,32 +72,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| {
@ -108,7 +101,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> {
@ -121,23 +119,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(
|| { || {
@ -145,17 +136,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| {
@ -165,7 +157,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(
@ -234,4 +228,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,7 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use druid::im::Vector; use druid::im::Vector;
use druid::{Data, Lens}; use druid::widget::Controller;
use druid::{Data, Env, Event, EventCtx, Lens, Selector, Widget};
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, PartialEq)] #[derive(Copy, Clone, Data, PartialEq)]
pub(crate) enum View { pub(crate) enum View {
@ -48,6 +54,7 @@ pub(crate) struct State {
pub(crate) struct SelectedModLens; pub(crate) struct SelectedModLens;
impl Lens<State, Option<ModInfo>> for 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 { fn with<V, F: FnOnce(&Option<ModInfo>) -> V>(&self, data: &State, f: F) -> V {
let info = data let info = data
.selected_mod_index .selected_mod_index
@ -56,11 +63,25 @@ impl Lens<State, Option<ModInfo>> for SelectedModLens {
f(&info) 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 { fn with_mut<V, F: FnOnce(&mut Option<ModInfo>) -> V>(&self, data: &mut State, f: F) -> V {
let mut info = data match data.selected_mod_index {
.selected_mod_index Some(i) => {
.and_then(|i| data.mods.get_mut(i).cloned()); let mut info = data.mods.get_mut(i).cloned();
f(&mut info) 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),
}
} }
} }
@ -70,21 +91,33 @@ impl Lens<State, Option<ModInfo>> for SelectedModLens {
pub(crate) struct IndexedVectorLens; pub(crate) struct IndexedVectorLens;
impl<T: Data> Lens<Vector<T>, Vector<(usize, T)>> for 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 { #[tracing::instrument(name = "IndexedVectorLens::with", skip_all)]
let data = data fn with<V, F: FnOnce(&Vector<(usize, T)>) -> V>(&self, values: &Vector<T>, f: F) -> V {
let indexed = values
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, val)| (i, val.clone())) .map(|(i, val)| (i, val.clone()))
.collect(); .collect();
f(&data) f(&indexed)
} }
#[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)]
fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>( fn with_mut<V, F: FnOnce(&mut Vector<(usize, T)>) -> V>(
&self, &self,
data: &mut Vector<T>, values: &mut Vector<T>,
f: F, f: F,
) -> V { ) -> V {
todo!() let mut indexed = values
.iter()
.enumerate()
.map(|(i, val)| (i, val.clone()))
.collect();
let ret = f(&mut indexed);
tracing::trace!("with_mut: {}", indexed.len());
*values = indexed.into_iter().map(|(_i, val)| val).collect();
ret
} }
} }
@ -104,16 +137,90 @@ impl State {
self.current_view = view; self.current_view = view;
} }
pub fn delete_selected_mod(&mut self) { pub fn select_mod(&mut self, index: usize) {
let Some(index) = self.selected_mod_index else { self.selected_mod_index = Some(index);
return;
};
self.mods.remove(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(true)
}
pub fn can_move_mod_up(&self) -> bool {
self.selected_mod_index.map(|i| i == 0).unwrap_or(true)
}
}
pub struct StateController {}
impl StateController {
pub fn new() -> Self {
Self {}
}
}
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),
}
}
} }

View file

@ -0,0 +1,60 @@
use druid::widget::prelude::*;
use druid::{Point, WidgetPod};
pub struct HiddenIf<T, W> {
child: WidgetPod<T, W>,
hidden_if: Box<dyn Fn(&T, &Env) -> bool>,
}
impl<T: Data, W: Widget<T>> HiddenIf<T, W> {
pub fn new(child: W, hidden_if: impl Fn(&T, &Env) -> bool + 'static) -> Self {
Self {
hidden_if: Box::new(hidden_if),
child: WidgetPod::new(child),
}
}
}
impl<T: Data, W: Widget<T>> Widget<T> for HiddenIf<T, W> {
#[tracing::instrument(name = "HideContainer", level = "trace", skip_all)]
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
let hidden = (self.hidden_if)(data, env);
ctx.set_disabled(hidden);
self.child.event(ctx, event, data, env);
}
#[tracing::instrument(name = "HideContainer", level = "trace", skip_all)]
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
let hidden = (self.hidden_if)(data, env);
ctx.set_disabled(hidden);
self.child.lifecycle(ctx, event, data, env)
}
#[tracing::instrument(name = "HideContainer", level = "trace", skip_all)]
fn update(&mut self, ctx: &mut UpdateCtx, _: &T, data: &T, env: &Env) {
self.child.update(ctx, data, env);
}
#[tracing::instrument(name = "HideContainer", level = "trace", skip_all)]
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
bc.debug_check("HideContainer");
let hidden = (self.hidden_if)(data, env);
if hidden {
return Size::ZERO;
}
let child_size = self.child.layout(ctx, bc, data, env);
self.child.set_origin(ctx, Point::new(0.0, 0.0));
child_size
}
#[tracing::instrument(name = "HideContainer", level = "trace", skip_all)]
fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
let hidden = (self.hidden_if)(data, env);
if hidden {
return;
}
self.child.paint(ctx, data, env);
}
}

View file

@ -1,14 +1,20 @@
use druid::{Data, Widget}; use druid::{Data, Env, Widget};
use self::fill_container::FillContainer; use self::fill_container::FillContainer;
use self::hidden_if::HiddenIf;
pub mod container; pub mod container;
pub mod fill_container; pub mod fill_container;
pub mod hidden_if;
pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static { pub trait ExtraWidgetExt<T: Data>: Widget<T> + Sized + 'static {
fn content_must_fill(self) -> FillContainer<T> { fn content_must_fill(self) -> FillContainer<T> {
FillContainer::new(self) FillContainer::new(self)
} }
fn hidden_if(self, hidden_if: impl Fn(&T, &Env) -> bool + 'static) -> HiddenIf<T, Self> {
HiddenIf::new(self, hidden_if)
}
} }
impl<T: Data, W: Widget<T> + 'static> ExtraWidgetExt<T> for W {} impl<T: Data, W: Widget<T> + 'static> ExtraWidgetExt<T> for W {}

View file

@ -0,0 +1,73 @@
use druid::widget::{Controller, Flex};
use druid::{Data, Widget};
pub struct TableSelect<T> {
widget: Flex<T>,
controller: TableSelectController<T>,
}
impl<T: Data> TableSelect<T> {
pub fn new(values: impl IntoIterator<Item = (impl Widget<T> + 'static)>) -> Self {
todo!();
}
}
impl<T: Data> Widget<T> for TableSelect<T> {
fn event(
&mut self,
ctx: &mut druid::EventCtx,
event: &druid::Event,
data: &mut T,
env: &druid::Env,
) {
todo!()
}
fn lifecycle(
&mut self,
ctx: &mut druid::LifeCycleCtx,
event: &druid::LifeCycle,
data: &T,
env: &druid::Env,
) {
todo!()
}
fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, env: &druid::Env) {
todo!()
}
fn layout(
&mut self,
ctx: &mut druid::LayoutCtx,
bc: &druid::BoxConstraints,
data: &T,
env: &druid::Env,
) -> druid::Size {
todo!()
}
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, env: &druid::Env) {
todo!()
}
}
struct TableSelectController<T> {
inner: T,
}
impl<T: Data> TableSelectController<T> {}
impl<T: Data> Controller<T, Flex<T>> for TableSelectController<T> {}
pub struct TableItem<T> {
inner: dyn Widget<T>,
}
impl<T: Data> TableItem<T> {
pub fn new(inner: impl Widget<T>) -> Self {
todo!();
}
}
impl<T: Data> Widget<T> for TableItem<T> {}