diff --git a/examples/containers.rs b/examples/containers.rs index 22eca56..ca44803 100644 --- a/examples/containers.rs +++ b/examples/containers.rs @@ -1,13 +1,13 @@ use gooey::value::Dynamic; use gooey::widget::{MakeWidget, WidgetInstance}; use gooey::window::ThemeMode; -use gooey::Run; +use gooey::{Gooey, Run}; fn main() -> gooey::Result { let theme_mode = Dynamic::default(); set_of_containers(3, theme_mode.clone()) .centered() - .into_window() + .into_window(Gooey::default()) .themed_mode(theme_mode) .run() } diff --git a/examples/theme.rs b/examples/theme.rs index 5677e35..628690f 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -1,3 +1,5 @@ +use std::fmt::Write; + use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, @@ -10,7 +12,7 @@ use gooey::widgets::input::InputValue; use gooey::widgets::slider::Slidable; use gooey::widgets::Space; use gooey::window::ThemeMode; -use gooey::Run; +use gooey::{Gooey, Run}; use kludgine::figures::units::Lp; use kludgine::Color; use palette::OklabHue; @@ -75,6 +77,8 @@ impl Scheme { } fn main() -> gooey::Result { + let gooey = Gooey::default(); + let (theme_mode, theme_switcher) = dark_mode_picker(); let scheme = Scheme::from(ColorScheme::default()); @@ -93,7 +97,7 @@ fn main() -> gooey::Result { (opt_color, editor) }, ); - let color_scheme = ( + let color_scheme_builder = ( &sources.primary, &editors.secondary.0, &editors.tertiary.0, @@ -109,9 +113,10 @@ fn main() -> gooey::Result { scheme.error = error; scheme.neutral = neutral; scheme.neutral_variant = neutral_variant; - scheme.build() + scheme }, ); + let color_scheme = color_scheme_builder.map_each_cloned(|builder| builder.build()); color_scheme .for_each_cloned(move |scheme| { sources.primary.set(scheme.primary); @@ -131,6 +136,21 @@ fn main() -> gooey::Result { .and(editors.error.1) .and(editors.neutral.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() .vertical_scroll(); @@ -152,7 +172,7 @@ fn main() -> gooey::Result { .themed(theme) .pad() .expand() - .into_window() + .into_window(gooey) .themed_mode(theme_mode) .run() } @@ -416,3 +436,54 @@ fn swatch(background: Dynamic, label: &str, text: Dynamic) -> impl .fit_vertically() .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()"); + } + } +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e610d30 --- /dev/null +++ b/src/app.rs @@ -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>>, +} + +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> { + self.clipboard + .as_ref() + .map(|mutex| mutex.lock().ignore_poison()) + } +} diff --git a/src/lib.rs b/src/lib.rs index d7ecef2..bb8936f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ mod graphics; mod names; #[macro_use] pub mod styles; +mod app; mod tick; mod tree; pub mod value; @@ -22,6 +23,7 @@ pub mod widgets; pub mod window; use std::ops::Sub; +pub use app::Gooey; pub use kludgine; use kludgine::app::winit::error::EventLoopError; use kludgine::figures::units::UPx; diff --git a/src/styles.rs b/src/styles.rs index 948c6db..c73c5a2 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -2039,7 +2039,7 @@ impl RequireInvalidation for ContainerLevel { } /// A builder of [`ColorScheme`]s. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct ColorSchemeBuilder { /// The primary color of the scheme. pub primary: ColorSource, @@ -2134,7 +2134,17 @@ impl ColorSchemeBuilder { /// 33% of the primary saturation will be picked. #[must_use] 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 } diff --git a/src/widget.rs b/src/widget.rs index 05753fe..517c765 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -17,6 +17,7 @@ use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use kludgine::Color; +use crate::app::Gooey; use crate::context::sealed::WindowHandle; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::components::{ @@ -680,8 +681,8 @@ pub trait MakeWidget: Sized { fn make_widget(self) -> WidgetInstance; /// Returns a new window containing `self` as the root widget. - fn into_window(self) -> Window { - Window::new(self.make_widget()) + fn into_window(self, gooey: Gooey) -> Window { + Window::new(self.make_widget(), gooey.clone()) } /// Associates `styles` with this widget. @@ -1289,7 +1290,7 @@ impl WidgetInstance { /// Runs this widget instance as an application. pub fn run(self) -> crate::Result { - Window::::new(self).run() + Window::::new(self, Gooey::default()).run() } /// Returns the id of the widget that should receive focus after this diff --git a/src/window.rs b/src/window.rs index 5679547..ce5f1db 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,7 +6,7 @@ use std::ops::{Deref, DerefMut, Not}; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::path::Path; use std::string::ToString; -use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; +use std::sync::{MutexGuard, OnceLock}; use ahash::AHashMap; use alot::LotId; @@ -27,6 +27,7 @@ use kludgine::Kludgine; use tracing::Level; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; +use crate::app::Gooey; use crate::context::{ AsEventContext, EventContext, Exclusive, GraphicsContext, InvalidationStatus, LayoutContext, WidgetContext, @@ -34,7 +35,7 @@ use crate::context::{ use crate::graphics::Graphics; use crate::styles::{Edges, FontFamilyList, ThemePair}; use crate::tree::Tree; -use crate::utils::{IgnorePoison, ModifiersExt}; +use crate::utils::ModifiersExt; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; use crate::widget::{ EventHandling, ManagedWidget, RootBehavior, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, @@ -45,7 +46,7 @@ use crate::{initialize_tracing, ConstraintLimit, Run}; /// A currently running Gooey window. pub struct RunningWindow<'window> { window: kludgine::app::Window<'window, WindowCommand>, - clipboard: Option>>, + gooey: Gooey, focused: Dynamic, occluded: Dynamic, } @@ -53,13 +54,13 @@ pub struct RunningWindow<'window> { impl<'window> RunningWindow<'window> { pub(crate) fn new( window: kludgine::app::Window<'window, WindowCommand>, - clipboard: &Option>>, + gooey: &Gooey, focused: &Dynamic, occluded: &Dynamic, ) -> Self { Self { window, - clipboard: clipboard.clone(), + gooey: gooey.clone(), focused: focused.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 /// initialized when the window opened. #[must_use] - pub fn clipboard_guard(&mut self) -> Option> { - self.clipboard - .as_ref() - .map(|mutex| mutex.lock().ignore_poison()) + pub fn clipboard_guard(&self) -> Option> { + self.gooey.clipboard_guard() } } @@ -113,6 +112,7 @@ where Behavior: WindowBehavior, { context: Behavior::Context, + gooey: Gooey, /// The attributes of this window. pub attributes: WindowAttributes, /// The colors to use to theme the user interface. @@ -148,7 +148,7 @@ where { fn default() -> Self { let context = Behavior::Context::default(); - Self::new(context) + Self::new(context, Gooey::default()) } } @@ -158,7 +158,7 @@ impl Window { where 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 @@ -222,7 +222,7 @@ where { /// Returns a new instance using `context` to initialize the window upon /// opening. - pub fn new(context: Behavior::Context) -> Self { + pub fn new(context: Behavior::Context, gooey: Gooey) -> Self { static EXECUTABLE_NAME: OnceLock = OnceLock::new(); let title = EXECUTABLE_NAME @@ -243,6 +243,7 @@ where title, ..WindowAttributes::default() }, + gooey, context, load_system_fonts: true, theme: Value::default(), @@ -267,6 +268,7 @@ where GooeyWindow::::run_with(AssertUnwindSafe(sealed::Context { user: self.context, settings: RefCell::new(sealed::WindowSettings { + gooey: self.gooey, transparent: self.attributes.transparent, attributes: Some(self.attributes), occluded: self.occluded, @@ -313,7 +315,7 @@ pub trait WindowBehavior: Sized + 'static { /// Runs this behavior as an application, initialized with `context`. fn run_with(context: Self::Context) -> crate::Result { - Window::::new(context).run() + Window::::new(context, Gooey::default()).run() } } @@ -335,7 +337,7 @@ struct GooeyWindow { current_theme: ThemePair, theme_mode: Value, transparent: bool, - clipboard: Option>>, + gooey: Gooey, } impl GooeyWindow @@ -526,6 +528,7 @@ where AssertUnwindSafe(context): Self::Context, ) -> Self { let mut settings = context.settings.borrow_mut(); + let gooey = settings.gooey.clone(); let occluded = settings.occluded.take().unwrap_or_default(); let focused = settings.focused.take().unwrap_or_default(); let theme = settings.theme.take().expect("theme always present"); @@ -563,10 +566,6 @@ where 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() { Some(Value::Dynamic(dynamic)) => { dynamic.set(window.theme().into()); @@ -577,7 +576,7 @@ where }; let transparent = settings.transparent; let mut behavior = T::initialize( - &mut RunningWindow::new(window, &clipboard, &focused, &occluded), + &mut RunningWindow::new(window, &gooey, &focused, &occluded), context.user, ); let root = Tree::default().push_boxed(behavior.make_root(), None); @@ -608,7 +607,7 @@ where theme, theme_mode, transparent, - clipboard, + gooey, } } @@ -633,7 +632,7 @@ where .new_frame(self.redraw_status.invalidations().drain()); 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 graphics = self.contents.new_frame(graphics); @@ -754,7 +753,7 @@ where Self::request_close( &mut self.should_close, &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 { 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( WidgetContext::new( target, @@ -928,7 +927,7 @@ where .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( WidgetContext::new( widget, @@ -964,7 +963,7 @@ where .widget(self.root.id()) .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( WidgetContext::new( widget, @@ -991,7 +990,7 @@ where let location = Point::::from(position); 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( WidgetContext::new( @@ -1038,8 +1037,7 @@ where _device_id: DeviceId, ) { if self.cursor.widget.take().is_some() { - 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 context = EventContext::new( WidgetContext::new( self.root.clone(), @@ -1063,7 +1061,7 @@ where state: ElementState, 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 { ElementState::Pressed => { EventContext::new( @@ -1198,6 +1196,7 @@ pub(crate) mod sealed { use crate::styles::{FontFamilyList, ThemePair}; use crate::value::{Dynamic, Value}; use crate::window::{ThemeMode, WindowAttributes}; + use crate::Gooey; pub struct Context { pub user: C, @@ -1205,6 +1204,7 @@ pub(crate) mod sealed { } pub struct WindowSettings { + pub gooey: Gooey, pub attributes: Option, pub occluded: Option>, pub focused: Option>,