Added Copy to Clipboard to theme editor

This also wires up the beginnings of the application type
This commit is contained in:
Jonathan Johnson 2023-12-03 07:38:56 -08:00
parent 0e6796318b
commit 55eea5fad3
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
7 changed files with 155 additions and 40 deletions

View file

@ -1,13 +1,13 @@
use gooey::value::Dynamic; use gooey::value::Dynamic;
use gooey::widget::{MakeWidget, WidgetInstance}; use gooey::widget::{MakeWidget, WidgetInstance};
use gooey::window::ThemeMode; use gooey::window::ThemeMode;
use gooey::Run; use gooey::{Gooey, Run};
fn main() -> gooey::Result { fn main() -> gooey::Result {
let theme_mode = Dynamic::default(); let theme_mode = Dynamic::default();
set_of_containers(3, theme_mode.clone()) set_of_containers(3, theme_mode.clone())
.centered() .centered()
.into_window() .into_window(Gooey::default())
.themed_mode(theme_mode) .themed_mode(theme_mode)
.run() .run()
} }

View file

@ -1,3 +1,5 @@
use std::fmt::Write;
use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::components::{TextColor, WidgetBackground};
use gooey::styles::{ use gooey::styles::{
ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme,
@ -10,7 +12,7 @@ use gooey::widgets::input::InputValue;
use gooey::widgets::slider::Slidable; use gooey::widgets::slider::Slidable;
use gooey::widgets::Space; use gooey::widgets::Space;
use gooey::window::ThemeMode; use gooey::window::ThemeMode;
use gooey::Run; use gooey::{Gooey, Run};
use kludgine::figures::units::Lp; use kludgine::figures::units::Lp;
use kludgine::Color; use kludgine::Color;
use palette::OklabHue; use palette::OklabHue;
@ -75,6 +77,8 @@ impl<Primary, Other> Scheme<Primary, Other> {
} }
fn main() -> gooey::Result { fn main() -> gooey::Result {
let gooey = Gooey::default();
let (theme_mode, theme_switcher) = dark_mode_picker(); let (theme_mode, theme_switcher) = dark_mode_picker();
let scheme = Scheme::from(ColorScheme::default()); let scheme = Scheme::from(ColorScheme::default());
@ -93,7 +97,7 @@ fn main() -> gooey::Result {
(opt_color, editor) (opt_color, editor)
}, },
); );
let color_scheme = ( let color_scheme_builder = (
&sources.primary, &sources.primary,
&editors.secondary.0, &editors.secondary.0,
&editors.tertiary.0, &editors.tertiary.0,
@ -109,9 +113,10 @@ fn main() -> gooey::Result {
scheme.error = error; scheme.error = error;
scheme.neutral = neutral; scheme.neutral = neutral;
scheme.neutral_variant = neutral_variant; scheme.neutral_variant = neutral_variant;
scheme.build() scheme
}, },
); );
let color_scheme = color_scheme_builder.map_each_cloned(|builder| builder.build());
color_scheme color_scheme
.for_each_cloned(move |scheme| { .for_each_cloned(move |scheme| {
sources.primary.set(scheme.primary); sources.primary.set(scheme.primary);
@ -131,6 +136,21 @@ fn main() -> gooey::Result {
.and(editors.error.1) .and(editors.error.1)
.and(editors.neutral.1) .and(editors.neutral.1)
.and(editors.neutral_variant.1) .and(editors.neutral_variant.1)
.and("Copy to Clipboard".into_button().on_click({
let gooey = gooey.clone();
move |()| {
if let Some(mut clipboard) = gooey.clipboard_guard() {
let builder = color_scheme_builder.get();
let mut source = String::default();
builder.format_rust_into(&mut source);
if let Err(err) = clipboard.set_text(&source) {
tracing::error!("Error setting clipboard text: {err}");
println!("{source}");
}
}
}
}))
.into_rows() .into_rows()
.vertical_scroll(); .vertical_scroll();
@ -152,7 +172,7 @@ fn main() -> gooey::Result {
.themed(theme) .themed(theme)
.pad() .pad()
.expand() .expand()
.into_window() .into_window(gooey)
.themed_mode(theme_mode) .themed_mode(theme_mode)
.run() .run()
} }
@ -416,3 +436,54 @@ fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl
.fit_vertically() .fit_vertically()
.expand() .expand()
} }
trait FormatRust {
fn format_rust_into(&self, out: &mut String);
}
impl FormatRust for ColorSource {
fn format_rust_into(&self, out: &mut String) {
write!(
out,
"ColorSource::new({:.1}, {:.1})",
self.hue.into_degrees(),
self.saturation
)
.expect("writing to string")
}
}
impl FormatRust for ColorSchemeBuilder {
fn format_rust_into(&self, source: &mut String) {
if self.secondary.is_none()
&& self.tertiary.is_none()
&& self.error.is_none()
&& self.neutral.is_none()
&& self.neutral_variant.is_none()
{
source.push_str("ColorScheme::from_primary(");
self.primary.format_rust_into(source);
source.push(')');
} else {
source.push_str("ColorSchemeBuilder::new(");
self.primary.format_rust_into(source);
source.push_str(").");
for (label, color) in [
self.secondary.map(|secondary| ("secondary", secondary)),
self.tertiary.map(|color| ("tertiary", color)),
self.error.map(|color| ("error", color)),
self.neutral.map(|color| ("neutral", color)),
self.neutral_variant.map(|color| ("neutral_variant", color)),
]
.into_iter()
.flatten()
{
source.push_str(label);
source.push('(');
color.format_rust_into(source);
source.push_str(").");
}
source.push_str("build()");
}
}
}

31
src/app.rs Normal file
View file

@ -0,0 +1,31 @@
use std::sync::{Arc, Mutex, MutexGuard};
use arboard::Clipboard;
use crate::utils::IgnorePoison;
/// A GUI application.
#[derive(Clone)]
pub struct Gooey {
pub(crate) clipboard: Option<Arc<Mutex<Clipboard>>>,
}
impl Default for Gooey {
fn default() -> Self {
Self {
clipboard: Clipboard::new()
.ok()
.map(|clipboard| Arc::new(Mutex::new(clipboard))),
}
}
}
impl Gooey {
/// Returns a locked mutex guard to the OS's clipboard, if one was able to be
/// initialized when the window opened.
#[must_use]
pub fn clipboard_guard(&self) -> Option<MutexGuard<'_, Clipboard>> {
self.clipboard
.as_ref()
.map(|mutex| mutex.lock().ignore_poison())
}
}

View file

@ -14,6 +14,7 @@ mod graphics;
mod names; mod names;
#[macro_use] #[macro_use]
pub mod styles; pub mod styles;
mod app;
mod tick; mod tick;
mod tree; mod tree;
pub mod value; pub mod value;
@ -22,6 +23,7 @@ pub mod widgets;
pub mod window; pub mod window;
use std::ops::Sub; use std::ops::Sub;
pub use app::Gooey;
pub use kludgine; pub use kludgine;
use kludgine::app::winit::error::EventLoopError; use kludgine::app::winit::error::EventLoopError;
use kludgine::figures::units::UPx; use kludgine::figures::units::UPx;

View file

@ -2039,7 +2039,7 @@ impl RequireInvalidation for ContainerLevel {
} }
/// A builder of [`ColorScheme`]s. /// A builder of [`ColorScheme`]s.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct ColorSchemeBuilder { pub struct ColorSchemeBuilder {
/// The primary color of the scheme. /// The primary color of the scheme.
pub primary: ColorSource, pub primary: ColorSource,
@ -2134,7 +2134,17 @@ impl ColorSchemeBuilder {
/// 33% of the primary saturation will be picked. /// 33% of the primary saturation will be picked.
#[must_use] #[must_use]
pub fn tertiary(mut self, tertiary: impl ProtoColor) -> Self { pub fn tertiary(mut self, tertiary: impl ProtoColor) -> Self {
self.secondary = Some(tertiary.into_source(self.primary.saturation / 3.)); self.tertiary = Some(tertiary.into_source(self.primary.saturation / 3.));
self
}
/// Sets the error color and returns self.
///
/// If `error` doesn't specify a saturation, the primary color's saturation
/// will be used.
#[must_use]
pub fn error(mut self, error: impl ProtoColor) -> Self {
self.error = Some(error.into_source(self.primary.saturation));
self self
} }

View file

@ -17,6 +17,7 @@ use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
use kludgine::Color; use kludgine::Color;
use crate::app::Gooey;
use crate::context::sealed::WindowHandle; use crate::context::sealed::WindowHandle;
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
use crate::styles::components::{ use crate::styles::components::{
@ -680,8 +681,8 @@ pub trait MakeWidget: Sized {
fn make_widget(self) -> WidgetInstance; fn make_widget(self) -> WidgetInstance;
/// Returns a new window containing `self` as the root widget. /// Returns a new window containing `self` as the root widget.
fn into_window(self) -> Window<WidgetInstance> { fn into_window(self, gooey: Gooey) -> Window<WidgetInstance> {
Window::new(self.make_widget()) Window::new(self.make_widget(), gooey.clone())
} }
/// Associates `styles` with this widget. /// Associates `styles` with this widget.
@ -1289,7 +1290,7 @@ impl WidgetInstance {
/// Runs this widget instance as an application. /// Runs this widget instance as an application.
pub fn run(self) -> crate::Result { pub fn run(self) -> crate::Result {
Window::<WidgetInstance>::new(self).run() Window::<WidgetInstance>::new(self, Gooey::default()).run()
} }
/// Returns the id of the widget that should receive focus after this /// Returns the id of the widget that should receive focus after this

View file

@ -6,7 +6,7 @@ use std::ops::{Deref, DerefMut, Not};
use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::path::Path; use std::path::Path;
use std::string::ToString; use std::string::ToString;
use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use std::sync::{MutexGuard, OnceLock};
use ahash::AHashMap; use ahash::AHashMap;
use alot::LotId; use alot::LotId;
@ -27,6 +27,7 @@ use kludgine::Kludgine;
use tracing::Level; use tracing::Level;
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
use crate::app::Gooey;
use crate::context::{ use crate::context::{
AsEventContext, EventContext, Exclusive, GraphicsContext, InvalidationStatus, LayoutContext, AsEventContext, EventContext, Exclusive, GraphicsContext, InvalidationStatus, LayoutContext,
WidgetContext, WidgetContext,
@ -34,7 +35,7 @@ use crate::context::{
use crate::graphics::Graphics; use crate::graphics::Graphics;
use crate::styles::{Edges, FontFamilyList, ThemePair}; use crate::styles::{Edges, FontFamilyList, ThemePair};
use crate::tree::Tree; use crate::tree::Tree;
use crate::utils::{IgnorePoison, ModifiersExt}; use crate::utils::ModifiersExt;
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
use crate::widget::{ use crate::widget::{
EventHandling, ManagedWidget, RootBehavior, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, EventHandling, ManagedWidget, RootBehavior, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
@ -45,7 +46,7 @@ use crate::{initialize_tracing, ConstraintLimit, Run};
/// A currently running Gooey window. /// A currently running Gooey window.
pub struct RunningWindow<'window> { pub struct RunningWindow<'window> {
window: kludgine::app::Window<'window, WindowCommand>, window: kludgine::app::Window<'window, WindowCommand>,
clipboard: Option<Arc<Mutex<Clipboard>>>, gooey: Gooey,
focused: Dynamic<bool>, focused: Dynamic<bool>,
occluded: Dynamic<bool>, occluded: Dynamic<bool>,
} }
@ -53,13 +54,13 @@ pub struct RunningWindow<'window> {
impl<'window> RunningWindow<'window> { impl<'window> RunningWindow<'window> {
pub(crate) fn new( pub(crate) fn new(
window: kludgine::app::Window<'window, WindowCommand>, window: kludgine::app::Window<'window, WindowCommand>,
clipboard: &Option<Arc<Mutex<Clipboard>>>, gooey: &Gooey,
focused: &Dynamic<bool>, focused: &Dynamic<bool>,
occluded: &Dynamic<bool>, occluded: &Dynamic<bool>,
) -> Self { ) -> Self {
Self { Self {
window, window,
clipboard: clipboard.clone(), gooey: gooey.clone(),
focused: focused.clone(), focused: focused.clone(),
occluded: occluded.clone(), occluded: occluded.clone(),
} }
@ -82,10 +83,8 @@ impl<'window> RunningWindow<'window> {
/// Returns a locked mutex guard to the OS's clipboard, if one was able to be /// Returns a locked mutex guard to the OS's clipboard, if one was able to be
/// initialized when the window opened. /// initialized when the window opened.
#[must_use] #[must_use]
pub fn clipboard_guard(&mut self) -> Option<MutexGuard<'_, Clipboard>> { pub fn clipboard_guard(&self) -> Option<MutexGuard<'_, Clipboard>> {
self.clipboard self.gooey.clipboard_guard()
.as_ref()
.map(|mutex| mutex.lock().ignore_poison())
} }
} }
@ -113,6 +112,7 @@ where
Behavior: WindowBehavior, Behavior: WindowBehavior,
{ {
context: Behavior::Context, context: Behavior::Context,
gooey: Gooey,
/// The attributes of this window. /// The attributes of this window.
pub attributes: WindowAttributes, pub attributes: WindowAttributes,
/// The colors to use to theme the user interface. /// The colors to use to theme the user interface.
@ -148,7 +148,7 @@ where
{ {
fn default() -> Self { fn default() -> Self {
let context = Behavior::Context::default(); let context = Behavior::Context::default();
Self::new(context) Self::new(context, Gooey::default())
} }
} }
@ -158,7 +158,7 @@ impl Window<WidgetInstance> {
where where
W: Widget, W: Widget,
{ {
Self::new(WidgetInstance::new(widget)) Self::new(WidgetInstance::new(widget), Gooey::default())
} }
/// Sets `focused` to be the dynamic updated when this window's focus status /// Sets `focused` to be the dynamic updated when this window's focus status
@ -222,7 +222,7 @@ where
{ {
/// Returns a new instance using `context` to initialize the window upon /// Returns a new instance using `context` to initialize the window upon
/// opening. /// opening.
pub fn new(context: Behavior::Context) -> Self { pub fn new(context: Behavior::Context, gooey: Gooey) -> Self {
static EXECUTABLE_NAME: OnceLock<String> = OnceLock::new(); static EXECUTABLE_NAME: OnceLock<String> = OnceLock::new();
let title = EXECUTABLE_NAME let title = EXECUTABLE_NAME
@ -243,6 +243,7 @@ where
title, title,
..WindowAttributes::default() ..WindowAttributes::default()
}, },
gooey,
context, context,
load_system_fonts: true, load_system_fonts: true,
theme: Value::default(), theme: Value::default(),
@ -267,6 +268,7 @@ where
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe(sealed::Context { GooeyWindow::<Behavior>::run_with(AssertUnwindSafe(sealed::Context {
user: self.context, user: self.context,
settings: RefCell::new(sealed::WindowSettings { settings: RefCell::new(sealed::WindowSettings {
gooey: self.gooey,
transparent: self.attributes.transparent, transparent: self.attributes.transparent,
attributes: Some(self.attributes), attributes: Some(self.attributes),
occluded: self.occluded, occluded: self.occluded,
@ -313,7 +315,7 @@ pub trait WindowBehavior: Sized + 'static {
/// Runs this behavior as an application, initialized with `context`. /// Runs this behavior as an application, initialized with `context`.
fn run_with(context: Self::Context) -> crate::Result { fn run_with(context: Self::Context) -> crate::Result {
Window::<Self>::new(context).run() Window::<Self>::new(context, Gooey::default()).run()
} }
} }
@ -335,7 +337,7 @@ struct GooeyWindow<T> {
current_theme: ThemePair, current_theme: ThemePair,
theme_mode: Value<ThemeMode>, theme_mode: Value<ThemeMode>,
transparent: bool, transparent: bool,
clipboard: Option<Arc<Mutex<Clipboard>>>, gooey: Gooey,
} }
impl<T> GooeyWindow<T> impl<T> GooeyWindow<T>
@ -526,6 +528,7 @@ where
AssertUnwindSafe(context): Self::Context, AssertUnwindSafe(context): Self::Context,
) -> Self { ) -> Self {
let mut settings = context.settings.borrow_mut(); let mut settings = context.settings.borrow_mut();
let gooey = settings.gooey.clone();
let occluded = settings.occluded.take().unwrap_or_default(); let occluded = settings.occluded.take().unwrap_or_default();
let focused = settings.focused.take().unwrap_or_default(); let focused = settings.focused.take().unwrap_or_default();
let theme = settings.theme.take().expect("theme always present"); let theme = settings.theme.take().expect("theme always present");
@ -563,10 +566,6 @@ where
fontdb.set_cursive_family(name); fontdb.set_cursive_family(name);
} }
let clipboard = Clipboard::new()
.ok()
.map(|clipboard| Arc::new(Mutex::new(clipboard)));
let theme_mode = match settings.theme_mode.take() { let theme_mode = match settings.theme_mode.take() {
Some(Value::Dynamic(dynamic)) => { Some(Value::Dynamic(dynamic)) => {
dynamic.set(window.theme().into()); dynamic.set(window.theme().into());
@ -577,7 +576,7 @@ where
}; };
let transparent = settings.transparent; let transparent = settings.transparent;
let mut behavior = T::initialize( let mut behavior = T::initialize(
&mut RunningWindow::new(window, &clipboard, &focused, &occluded), &mut RunningWindow::new(window, &gooey, &focused, &occluded),
context.user, context.user,
); );
let root = Tree::default().push_boxed(behavior.make_root(), None); let root = Tree::default().push_boxed(behavior.make_root(), None);
@ -608,7 +607,7 @@ where
theme, theme,
theme_mode, theme_mode,
transparent, transparent,
clipboard, gooey,
} }
} }
@ -633,7 +632,7 @@ where
.new_frame(self.redraw_status.invalidations().drain()); .new_frame(self.redraw_status.invalidations().drain());
let resizable = window.winit().is_resizable(); let resizable = window.winit().is_resizable();
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics); let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics);
let graphics = self.contents.new_frame(graphics); let graphics = self.contents.new_frame(graphics);
@ -754,7 +753,7 @@ where
Self::request_close( Self::request_close(
&mut self.should_close, &mut self.should_close,
&mut self.behavior, &mut self.behavior,
&mut RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded), &mut RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded),
) )
} }
@ -818,7 +817,7 @@ where
let Some(target) = self.root.tree.widget_from_node(target) else { let Some(target) = self.root.tree.widget_from_node(target) else {
return; return;
}; };
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
let mut target = EventContext::new( let mut target = EventContext::new(
WidgetContext::new( WidgetContext::new(
target, target,
@ -928,7 +927,7 @@ where
.expect("missing widget") .expect("missing widget")
}); });
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
let mut widget = EventContext::new( let mut widget = EventContext::new(
WidgetContext::new( WidgetContext::new(
widget, widget,
@ -964,7 +963,7 @@ where
.widget(self.root.id()) .widget(self.root.id())
.expect("missing widget") .expect("missing widget")
}); });
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
let mut target = EventContext::new( let mut target = EventContext::new(
WidgetContext::new( WidgetContext::new(
widget, widget,
@ -991,7 +990,7 @@ where
let location = Point::<Px>::from(position); let location = Point::<Px>::from(position);
self.cursor.location = Some(location); self.cursor.location = Some(location);
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
EventContext::new( EventContext::new(
WidgetContext::new( WidgetContext::new(
@ -1038,8 +1037,7 @@ where
_device_id: DeviceId, _device_id: DeviceId,
) { ) {
if self.cursor.widget.take().is_some() { if self.cursor.widget.take().is_some() {
let mut window = let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded);
let mut context = EventContext::new( let mut context = EventContext::new(
WidgetContext::new( WidgetContext::new(
self.root.clone(), self.root.clone(),
@ -1063,7 +1061,7 @@ where
state: ElementState, state: ElementState,
button: MouseButton, button: MouseButton,
) { ) {
let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut window = RunningWindow::new(window, &self.gooey, &self.focused, &self.occluded);
match state { match state {
ElementState::Pressed => { ElementState::Pressed => {
EventContext::new( EventContext::new(
@ -1198,6 +1196,7 @@ pub(crate) mod sealed {
use crate::styles::{FontFamilyList, ThemePair}; use crate::styles::{FontFamilyList, ThemePair};
use crate::value::{Dynamic, Value}; use crate::value::{Dynamic, Value};
use crate::window::{ThemeMode, WindowAttributes}; use crate::window::{ThemeMode, WindowAttributes};
use crate::Gooey;
pub struct Context<C> { pub struct Context<C> {
pub user: C, pub user: C,
@ -1205,6 +1204,7 @@ pub(crate) mod sealed {
} }
pub struct WindowSettings { pub struct WindowSettings {
pub gooey: Gooey,
pub attributes: Option<WindowAttributes>, pub attributes: Option<WindowAttributes>,
pub occluded: Option<Dynamic<bool>>, pub occluded: Option<Dynamic<bool>>,
pub focused: Option<Dynamic<bool>>, pub focused: Option<Dynamic<bool>>,