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::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()
}

View file

@ -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<Primary, Other> Scheme<Primary, Other> {
}
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<Color>, label: &str, text: Dynamic<Color>) -> 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()");
}
}
}

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;
#[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;

View file

@ -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
}

View file

@ -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<WidgetInstance> {
Window::new(self.make_widget())
fn into_window(self, gooey: Gooey) -> Window<WidgetInstance> {
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::<WidgetInstance>::new(self).run()
Window::<WidgetInstance>::new(self, Gooey::default()).run()
}
/// 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::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<Arc<Mutex<Clipboard>>>,
gooey: Gooey,
focused: Dynamic<bool>,
occluded: Dynamic<bool>,
}
@ -53,13 +54,13 @@ pub struct RunningWindow<'window> {
impl<'window> RunningWindow<'window> {
pub(crate) fn new(
window: kludgine::app::Window<'window, WindowCommand>,
clipboard: &Option<Arc<Mutex<Clipboard>>>,
gooey: &Gooey,
focused: &Dynamic<bool>,
occluded: &Dynamic<bool>,
) -> 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<MutexGuard<'_, Clipboard>> {
self.clipboard
.as_ref()
.map(|mutex| mutex.lock().ignore_poison())
pub fn clipboard_guard(&self) -> Option<MutexGuard<'_, Clipboard>> {
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<WidgetInstance> {
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<String> = 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::<Behavior>::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::<Self>::new(context).run()
Window::<Self>::new(context, Gooey::default()).run()
}
}
@ -335,7 +337,7 @@ struct GooeyWindow<T> {
current_theme: ThemePair,
theme_mode: Value<ThemeMode>,
transparent: bool,
clipboard: Option<Arc<Mutex<Clipboard>>>,
gooey: Gooey,
}
impl<T> GooeyWindow<T>
@ -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::<Px>::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<C> {
pub user: C,
@ -1205,6 +1204,7 @@ pub(crate) mod sealed {
}
pub struct WindowSettings {
pub gooey: Gooey,
pub attributes: Option<WindowAttributes>,
pub occluded: Option<Dynamic<bool>>,
pub focused: Option<Dynamic<bool>>,