diff --git a/crates/dtmm/src/controller.rs b/crates/dtmm/src/controller.rs new file mode 100644 index 0000000..a2bf429 --- /dev/null +++ b/crates/dtmm/src/controller.rs @@ -0,0 +1,47 @@ +use druid::widget::{Button, Controller}; +use druid::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, UpdateCtx, Widget}; + +pub struct DisabledButtonController; + +impl Controller> for DisabledButtonController { + fn event( + &mut self, + child: &mut Button, + 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, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &T, + env: &Env, + ) { + child.lifecycle(ctx, event, data, env) + } + + fn update( + &mut self, + child: &mut Button, + 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) + } +} diff --git a/crates/dtmm/src/main.rs b/crates/dtmm/src/main.rs index d73b440..8bf57c1 100644 --- a/crates/dtmm/src/main.rs +++ b/crates/dtmm/src/main.rs @@ -1,3 +1,5 @@ +#![feature(let_chains)] + use clap::command; use color_eyre::Report; use color_eyre::Result; @@ -8,6 +10,7 @@ use tracing_subscriber::EnvFilter; use crate::state::State; +mod controller; mod main_window; mod state; mod theme; diff --git a/crates/dtmm/src/main_window.rs b/crates/dtmm/src/main_window.rs index a8b1e71..385894b 100644 --- a/crates/dtmm/src/main_window.rs +++ b/crates/dtmm/src/main_window.rs @@ -5,19 +5,21 @@ use druid::widget::{ }; 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::widget::ExtraWidgetExt; const TITLE: &str = "Darktide Mod Manager"; -const WINDOW_WIDTH: f64 = 800.0; -const WINDOW_HEIGHT: f64 = 600.0; +const WINDOW_SIZE: (f64, f64) = (800.0, 600.0); const MOD_DETAILS_MIN_WIDTH: f64 = 325.0; pub(crate) fn new() -> WindowDesc { WindowDesc::new(build_window()) .title(TITLE) - .window_size((WINDOW_WIDTH, WINDOW_HEIGHT)) + .window_size(WINDOW_SIZE) } fn build_top_bar() -> impl Widget { @@ -33,9 +35,11 @@ fn build_top_bar() -> impl Widget { ) .with_default_spacer() .with_child( - Button::new("Settings").on_click(|_ctx, state: &mut State, _env| { - state.set_current_view(View::Settings) - }), + Button::new("Settings") + .on_click(|_ctx, state: &mut State, _env| { + state.set_current_view(View::Settings) + }) + .hidden_if(|_, _| true), ) .with_default_spacer() .with_child( @@ -68,32 +72,21 @@ fn build_mod_list() -> impl Widget { 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| { @@ -108,7 +101,12 @@ fn build_mod_list() -> impl Widget { }); }, )) - .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 { @@ -121,23 +119,16 @@ fn build_mod_details() -> impl Widget { }, 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, _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, _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( || { @@ -145,17 +136,18 @@ fn build_mod_details() -> impl Widget { 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, _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| { @@ -165,7 +157,9 @@ fn build_mod_details() -> impl Widget { }); 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, _env: &druid::Env| info.is_none()) + .lens(State::selected_mod); let buttons = Flex::column() .with_child( @@ -234,4 +228,5 @@ fn build_window() -> impl Widget { .must_fill_main_axis(true) .with_child(build_top_bar()) .with_flex_child(build_main(), 1.0) + .controller(StateController::new()) } diff --git a/crates/dtmm/src/state.rs b/crates/dtmm/src/state.rs index 0639818..51f0d40 100644 --- a/crates/dtmm/src/state.rs +++ b/crates/dtmm/src/state.rs @@ -1,7 +1,13 @@ use std::sync::Arc; 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 = 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)] pub(crate) enum View { @@ -48,6 +54,7 @@ pub(crate) struct State { pub(crate) struct SelectedModLens; impl Lens> for SelectedModLens { + #[tracing::instrument(name = "SelectedModLens::with", skip_all)] fn with) -> V>(&self, data: &State, f: F) -> V { let info = data .selected_mod_index @@ -56,11 +63,25 @@ impl Lens> for SelectedModLens { f(&info) } + #[tracing::instrument(name = "SelectedModLens::with_mut", skip_all)] fn with_mut) -> 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) + 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), + } } } @@ -70,21 +91,33 @@ impl Lens> for SelectedModLens { pub(crate) struct IndexedVectorLens; impl Lens, Vector<(usize, T)>> for IndexedVectorLens { - fn with) -> V>(&self, data: &Vector, f: F) -> V { - let data = data + #[tracing::instrument(name = "IndexedVectorLens::with", skip_all)] + fn with) -> V>(&self, values: &Vector, f: F) -> V { + let indexed = values .iter() .enumerate() .map(|(i, val)| (i, val.clone())) .collect(); - f(&data) + f(&indexed) } + #[tracing::instrument(name = "IndexedVectorLens::with_mut", skip_all)] fn with_mut) -> V>( &self, - data: &mut Vector, + values: &mut Vector, f: F, ) -> 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; } - pub fn delete_selected_mod(&mut self) { - let Some(index) = self.selected_mod_index else { - 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) { 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(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> Controller 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), + } + } } diff --git a/crates/dtmm/src/widget/hidden_if.rs b/crates/dtmm/src/widget/hidden_if.rs new file mode 100644 index 0000000..122eb40 --- /dev/null +++ b/crates/dtmm/src/widget/hidden_if.rs @@ -0,0 +1,60 @@ +use druid::widget::prelude::*; +use druid::{Point, WidgetPod}; + +pub struct HiddenIf { + child: WidgetPod, + hidden_if: Box bool>, +} + +impl> HiddenIf { + 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> Widget for HiddenIf { + #[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); + } +} diff --git a/crates/dtmm/src/widget/mod.rs b/crates/dtmm/src/widget/mod.rs index 9262d0a..61a8778 100644 --- a/crates/dtmm/src/widget/mod.rs +++ b/crates/dtmm/src/widget/mod.rs @@ -1,14 +1,20 @@ -use druid::{Data, Widget}; +use druid::{Data, Env, Widget}; use self::fill_container::FillContainer; +use self::hidden_if::HiddenIf; pub mod container; pub mod fill_container; +pub mod hidden_if; pub trait ExtraWidgetExt: Widget + Sized + 'static { fn content_must_fill(self) -> FillContainer { FillContainer::new(self) } + + fn hidden_if(self, hidden_if: impl Fn(&T, &Env) -> bool + 'static) -> HiddenIf { + HiddenIf::new(self, hidden_if) + } } impl + 'static> ExtraWidgetExt for W {} diff --git a/crates/dtmm/src/widget/table_select.rs b/crates/dtmm/src/widget/table_select.rs new file mode 100644 index 0000000..00321f8 --- /dev/null +++ b/crates/dtmm/src/widget/table_select.rs @@ -0,0 +1,73 @@ +use druid::widget::{Controller, Flex}; +use druid::{Data, Widget}; + +pub struct TableSelect { + widget: Flex, + controller: TableSelectController, +} + +impl TableSelect { + pub fn new(values: impl IntoIterator + 'static)>) -> Self { + todo!(); + } +} + +impl Widget for TableSelect { + 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 { + inner: T, +} + +impl TableSelectController {} + +impl Controller> for TableSelectController {} + +pub struct TableItem { + inner: dyn Widget, +} + +impl TableItem { + pub fn new(inner: impl Widget) -> Self { + todo!(); + } +} + +impl Widget for TableItem {}