From c38909db22d163d9a8a1539286de560c2d63a9a6 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 15 Mar 2023 16:24:08 +0100 Subject: [PATCH] feat(dtmm): Add section borders This implements a new container widget that allows separate widths and colors for each border side. --- crates/dtmm/src/ui/theme/mod.rs | 3 + crates/dtmm/src/ui/widget/border.rs | 197 ++++++++++++++++++++++++++++ crates/dtmm/src/ui/widget/mod.rs | 1 + crates/dtmm/src/ui/window/main.rs | 42 ++++-- 4 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 crates/dtmm/src/ui/widget/border.rs diff --git a/crates/dtmm/src/ui/theme/mod.rs b/crates/dtmm/src/ui/theme/mod.rs index 45a7c3a..7f93524 100644 --- a/crates/dtmm/src/ui/theme/mod.rs +++ b/crates/dtmm/src/ui/theme/mod.rs @@ -13,7 +13,10 @@ pub const DISABLED_ALPHA: f64 = 0.65; pub(crate) fn set_theme_env(env: &mut Env, _: &State) { env.set(druid::theme::TEXT_COLOR, COLOR_FG); + env.set(druid::theme::SCROLLBAR_COLOR, COLOR_FG); + env.set(druid::theme::BORDER_LIGHT, COLOR_FG); env.set(druid::theme::BUTTON_BORDER_RADIUS, 2.); + env.set(keys::KEY_BUTTON_BG, COLOR_ACCENT); env.set(keys::KEY_BUTTON_BG_HOT, COLOR_ACCENT.darken(0.03)); env.set(keys::KEY_BUTTON_BG_ACTIVE, COLOR_ACCENT.darken(0.1)); diff --git a/crates/dtmm/src/ui/widget/border.rs b/crates/dtmm/src/ui/widget/border.rs new file mode 100644 index 0000000..2ca7cdb --- /dev/null +++ b/crates/dtmm/src/ui/widget/border.rs @@ -0,0 +1,197 @@ +use druid::kurbo::Line; +use druid::widget::prelude::*; +use druid::{Color, KeyOrValue, Point, WidgetPod}; + +pub struct Border { + inner: WidgetPod>>, + color: BorderColor, + width: BorderWidths, + // corner_radius: KeyOrValue, +} + +impl Border { + pub fn new(inner: impl Widget + 'static) -> Self { + let inner = WidgetPod::new(inner).boxed(); + Self { + inner, + color: Color::TRANSPARENT.into(), + width: 0f64.into(), + } + } + + pub fn set_color(&mut self, color: impl Into>) { + self.color = BorderColor::Uniform(color.into()); + } + + pub fn with_color(mut self, color: impl Into>) -> Self { + self.set_color(color); + self + } + + pub fn set_bottom_border(&mut self, width: impl Into>) { + self.width.bottom = width.into(); + } + + pub fn with_bottom_border(mut self, width: impl Into>) -> Self { + self.set_bottom_border(width); + self + } + + pub fn set_top_border(&mut self, width: impl Into>) { + self.width.top = width.into(); + } + + pub fn with_top_border(mut self, width: impl Into>) -> Self { + self.set_top_border(width); + self + } +} + +impl Widget for Border { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + self.inner.event(ctx, event, data, env) + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.inner.lifecycle(ctx, event, data, env); + } + + fn update(&mut self, ctx: &mut UpdateCtx, _: &T, data: &T, env: &Env) { + self.inner.update(ctx, data, env); + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + bc.debug_check("Border"); + + let (left, top, right, bottom) = self.width.resolve(env); + + let inner_bc = bc.shrink((left + right, top + bottom)); + let inner_size = self.inner.layout(ctx, &inner_bc, data, env); + + let origin = Point::new(left, top); + self.inner.set_origin(ctx, origin); + + let size = Size::new( + inner_size.width + left + right, + inner_size.height + top + bottom, + ); + + let insets = self.inner.compute_parent_paint_insets(size); + ctx.set_paint_insets(insets); + + let baseline_offset = self.inner.baseline_offset(); + if baseline_offset > 0. { + ctx.set_baseline_offset(baseline_offset + bottom); + } + + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + let size = ctx.size(); + let (left, top, right, bottom) = self.width.resolve(env); + let (col_left, col_top, col_right, col_bottom) = self.color.resolve(env); + + self.inner.paint(ctx, data, env); + + // There's probably a more elegant way to create the various `Line`s, but this works for now. + // The important bit is to move each line inwards by half each side's border width. Otherwise + // it would draw hald of the border outside of the widget's boundary. + + if left > 0. { + ctx.stroke( + Line::new((left / 2., top / 2.), (left / 2., size.height)), + &col_left, + left, + ); + } + + if top > 0. { + ctx.stroke( + Line::new((left / 2., top / 2.), (size.width - (right / 2.), top / 2.)), + &col_top, + top, + ); + } + + if right > 0. { + ctx.stroke( + Line::new( + (size.width - (right / 2.), top / 2.), + (size.width - (right / 2.), size.height - (bottom / 2.)), + ), + &col_right, + right, + ); + } + + if bottom > 0. { + ctx.stroke( + Line::new( + (left / 2., size.height - (bottom / 2.)), + (size.width - (right / 2.), size.height - (bottom / 2.)), + ), + &col_bottom, + bottom, + ); + } + } +} + +#[derive(Clone, Debug)] +pub enum BorderColor { + Uniform(KeyOrValue), + // Individual { + // left: KeyOrValue, + // top: KeyOrValue, + // right: KeyOrValue, + // bottom: KeyOrValue, + // }, +} + +impl BorderColor { + pub fn resolve(&self, env: &Env) -> (Color, Color, Color, Color) { + match self { + Self::Uniform(val) => { + let color = val.resolve(env); + (color, color, color, color) + } + } + } +} + +impl From for BorderColor { + fn from(value: Color) -> Self { + Self::Uniform(value.into()) + } +} + +#[derive(Clone, Debug)] +pub struct BorderWidths { + pub left: KeyOrValue, + pub top: KeyOrValue, + pub right: KeyOrValue, + pub bottom: KeyOrValue, +} + +impl From for BorderWidths { + fn from(value: f64) -> Self { + Self { + left: value.into(), + top: value.into(), + right: value.into(), + bottom: value.into(), + } + } +} + +impl BorderWidths { + pub fn resolve(&self, env: &Env) -> (f64, f64, f64, f64) { + ( + self.left.resolve(env), + self.top.resolve(env), + self.right.resolve(env), + self.bottom.resolve(env), + ) + } +} diff --git a/crates/dtmm/src/ui/widget/mod.rs b/crates/dtmm/src/ui/widget/mod.rs index 3c8c08c..05c91c7 100644 --- a/crates/dtmm/src/ui/widget/mod.rs +++ b/crates/dtmm/src/ui/widget/mod.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use druid::text::Formatter; use druid::{Data, Widget}; +pub mod border; pub mod button; pub mod controller; diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index 4a72f02..8fe62d4 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -18,7 +18,8 @@ use crate::state::{ ACTION_SELECT_MOD, ACTION_SET_WINDOW_HANDLE, ACTION_START_DELETE_SELECTED_MOD, ACTION_START_DEPLOY, ACTION_START_RESET_DEPLOYMENT, }; -use crate::ui::theme; +use crate::ui::theme::{self, ColorExt}; +use crate::ui::widget::border::Border; use crate::ui::widget::button::Button; use crate::ui::widget::controller::{ AutoScrollController, DirtyStateController, ImageLensController, @@ -73,7 +74,7 @@ fn build_top_bar() -> impl Widget { }) .disabled_if(|data, _| data.is_deployment_in_progress || data.is_reset_in_progress); - Flex::row() + let bar = Flex::row() .must_fill_main_axis(true) .main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_child( @@ -89,14 +90,29 @@ fn build_top_bar() -> impl Widget { .with_child(reset_button), ) .padding(theme::TOP_BAR_INSETS) - .background(theme::TOP_BAR_BACKGROUND_COLOR) - // TODO: Add bottom border. Need a custom widget for that, as the built-in only provides - // uniform borders on all sides + .background(theme::TOP_BAR_BACKGROUND_COLOR); + + Border::new(bar) + .with_color(theme::COLOR_FG2) + .with_bottom_border(1.) } fn build_mod_list() -> impl Widget { let list = List::new(|| { let checkbox = Checkbox::new("") + .env_scope(|env, selected| { + env.set(druid::theme::BORDER_DARK, theme::COLOR_BG3); + env.set(druid::theme::BORDER_LIGHT, theme::COLOR_BG3); + env.set(druid::theme::TEXT_COLOR, theme::COLOR_ACCENT_FG); + + if *selected { + env.set(druid::theme::BACKGROUND_DARK, theme::COLOR_ACCENT); + env.set(druid::theme::BACKGROUND_LIGHT, theme::COLOR_ACCENT); + } else { + env.set(druid::theme::BACKGROUND_DARK, Color::TRANSPARENT); + env.set(druid::theme::BACKGROUND_LIGHT, Color::TRANSPARENT); + } + }) .lens(lens!((usize, Arc, bool), 1).then(ModInfo::enabled.in_arc())); let name = @@ -112,13 +128,17 @@ fn build_mod_list() -> impl Widget { .env_scope(|env, (i, _, selected)| { if *selected { env.set(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR, theme::COLOR_ACCENT); - env.set(druid::theme::TEXT_COLOR, theme::COLOR_ACCENT_FG); + env.set( + druid::theme::TEXT_COLOR, + theme::COLOR_ACCENT_FG.darken(0.05), + ); } else { env.set(druid::theme::TEXT_COLOR, theme::COLOR_FG); + if (i % 2) == 1 { env.set(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR, theme::COLOR_BG1); } else { - env.set(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR, Color::TRANSPARENT); + env.set(theme::keys::KEY_MOD_LIST_ITEM_BG_COLOR, theme::COLOR_BG); } } }) @@ -298,7 +318,7 @@ fn build_mod_details() -> impl Widget { .cross_axis_alignment(CrossAxisAlignment::Start) .main_axis_alignment(MainAxisAlignment::SpaceBetween) .with_flex_child(build_mod_details_info(), 1.0) - .with_child(build_mod_details_buttons().padding(4.)) + .with_child(build_mod_details_buttons().padding((4., 4., 4., 8.))) } fn build_view_mods() -> impl Widget { @@ -373,7 +393,11 @@ fn build_log_view() -> impl Widget { .vertical() .controller(AutoScrollController); - SizedBox::new(label).expand_width().height(128.0) + let inner = Border::new(label) + .with_color(theme::COLOR_FG2) + .with_top_border(1.); + + SizedBox::new(inner).expand_width().height(128.0) } fn build_window() -> impl Widget {