From e17f05dec53314874826d4c3f44243ce828a14cc Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 24 May 2019 23:32:40 -0700 Subject: [PATCH] Implement a basic component in Cocoa - it's cool because it really just uses the same Component lifecycle. Neat! --- alchemy/src/components/mod.rs | 4 +- alchemy/src/components/text.rs | 77 ++++++++++++------ alchemy/src/components/view.rs | 1 - alchemy/src/lib.rs | 2 +- alchemy/src/theme/stylesheet.rs | 2 +- cocoa/src/lib.rs | 1 + cocoa/src/text.rs | 140 ++++++++++++++++++++++++++++++++ cocoa/src/view.rs | 8 +- cocoa/src/window.rs | 2 +- examples/layout/src/main.rs | 4 +- styles/src/styles.rs | 6 +- 11 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 cocoa/src/text.rs diff --git a/alchemy/src/components/mod.rs b/alchemy/src/components/mod.rs index 97c370f..4015a88 100644 --- a/alchemy/src/components/mod.rs +++ b/alchemy/src/components/mod.rs @@ -5,8 +5,8 @@ pub mod fragment; pub mod view; -//pub mod text; +pub mod text; pub use fragment::Fragment; pub use view::View; -//pub use text::*; +pub use text::Text; diff --git a/alchemy/src/components/text.rs b/alchemy/src/components/text.rs index 258b4f3..1b6f86a 100644 --- a/alchemy/src/components/text.rs +++ b/alchemy/src/components/text.rs @@ -1,34 +1,59 @@ -//! components/label.rs -//! -//! Implements a Label Component struct. Used for TextNode -//! behind the scenes on most platforms. -//! -//! @author Ryan McGrath -//! @created 03/29/2019 +//! Handles hoisting per-platform specific Text components. +//! Each platform needs the freedom to do some specific things, +//! hence why they're all (somewhat annoyingly, but lovingly) re-implemented +//! as bridges. -use crate::prelude::RSX; -use crate::components::Component; -use crate::dom::elements::FlowContent; +use std::sync::{Mutex}; -#[derive(RSX, Debug, Default)] -pub struct Text {} +use alchemy_styles::styles::{Layout, Style}; -#[cfg(target_os = "macos")] -impl Component for Text { - fn create_native_backing_node(&self) -> cocoa::base::id { - use objc::{msg_send, sel, sel_impl}; - use cocoa::foundation::{NSRect, NSPoint, NSSize}; - use cocoa::base::id; - use crate::components::macos::objc_classes::label; +use alchemy_lifecycle::error::Error; +use alchemy_lifecycle::rsx::{Props, RSX}; +use alchemy_lifecycle::traits::{Component, PlatformSpecificNodeType}; - let view: cocoa::base::id; +#[cfg(feature = "cocoa")] +use alchemy_cocoa::text::{Text as PlatformTextBridge}; - unsafe { - let rect_zero = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)); - let alloc: id = msg_send![label::register_class(), alloc]; - view = msg_send![alloc, initWithFrame:rect_zero]; - } +/// Text rendering is a complicated mess, and being able to defer to the +/// backing platform for this is amazing. This is a very common Component. +/// +/// Views accept styles and event callbacks as props. For example: +/// +/// ``` +/// +/// ``` +pub struct Text(Mutex); - view +impl Default for Text { + fn default() -> Text { + Text(Mutex::new(PlatformTextBridge::new())) } } + +impl Component for Text { + fn has_native_backing_node(&self) -> bool { true } + + fn borrow_native_backing_node(&self) -> Option { + let bridge = self.0.lock().unwrap(); + Some(bridge.borrow_native_backing_node()) + } + + // Shouldn't be allowed to have child elements... or, should it? + // Panic might not be right here, but eh, should probably do something. + fn append_child_component(&self, component: &Component) {} + + fn apply_styles(&self, layout: &Layout, style: &Style) { + let mut bridge = self.0.lock().unwrap(); + bridge.apply_styles(layout, style); + } + + fn component_did_mount(&mut self, props: &Props) { + let mut bridge = self.0.lock().unwrap(); + bridge.set_text("LOL"); + } + + fn render(&self, props: &Props) -> Result { + Ok(RSX::None) + } +} + diff --git a/alchemy/src/components/view.rs b/alchemy/src/components/view.rs index e969467..ff99dfc 100644 --- a/alchemy/src/components/view.rs +++ b/alchemy/src/components/view.rs @@ -61,4 +61,3 @@ impl Component for View { })) } } - diff --git a/alchemy/src/lib.rs b/alchemy/src/lib.rs index df9f39a..48f20bb 100644 --- a/alchemy/src/lib.rs +++ b/alchemy/src/lib.rs @@ -29,7 +29,7 @@ mod app; use app::App; pub mod components; -pub use components::{Fragment, View}; +pub use components::{Fragment, Text, View}; pub(crate) mod reconciler; pub mod theme; diff --git a/alchemy/src/theme/stylesheet.rs b/alchemy/src/theme/stylesheet.rs index 4a09289..25993d5 100644 --- a/alchemy/src/theme/stylesheet.rs +++ b/alchemy/src/theme/stylesheet.rs @@ -279,7 +279,7 @@ fn reduce_styles_into_style(styles: &Vec, layout: &mut Style) { }, Styles::TextAlignment(val) => { }, - Styles::TextColor(val) => { }, + Styles::TextColor(val) => { layout.text_color = *val; }, Styles::TextDecorationColor(val) => { }, Styles::TextShadowColor(val) => { }, Styles::TintColor(val) => { }, diff --git a/cocoa/src/lib.rs b/cocoa/src/lib.rs index b717846..1900be3 100644 --- a/cocoa/src/lib.rs +++ b/cocoa/src/lib.rs @@ -19,5 +19,6 @@ pub mod color; pub mod app; +pub mod text; pub mod view; pub mod window; diff --git a/cocoa/src/text.rs b/cocoa/src/text.rs new file mode 100644 index 0000000..76095d0 --- /dev/null +++ b/cocoa/src/text.rs @@ -0,0 +1,140 @@ +//! This wraps NTextField on macOS, and configures it to act like a label +//! with standard behavior that most users would expect. + +use std::sync::{Once, ONCE_INIT}; + +use objc_id::{Id, ShareId}; +use objc::{msg_send, sel, sel_impl}; +use objc::declare::ClassDecl; +use objc::runtime::{Class, Object, Sel, BOOL}; + +use cocoa::base::{id, nil, YES}; +use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString}; + +use crate::color::IntoNSColor; + +use alchemy_styles::color::Color; +use alchemy_styles::styles::Style; +use alchemy_styles::result::Layout; + +use alchemy_lifecycle::traits::PlatformSpecificNodeType; + +static ALCHEMY_DELEGATE: &str = "alchemyDelegate"; + +/// A wrapper for `NSText`. This holds retained pointers for the Objective-C +/// runtime - namely, the view itself, and associated things such as background +/// colors and so forth. +#[derive(Debug)] +pub struct Text { + inner_mut: Id, + inner_share: ShareId, + background_color: Id, + text_color: Id, + text: Id +} + +impl Text { + /// Allocates a new `NSTextField` on the Objective-C side, ensuring that things like coordinate + /// flipping occur (macOS still uses (0,0) as lower-left by default), and opting in to layer + /// backed views for smoother scrolling. + pub fn new() -> Text { + let (inner_mut, inner_share, s) = unsafe { + let initial_string = NSString::alloc(nil).init_str("wut wut"); + let view: id = msg_send![register_class(), labelWithString:initial_string]; + msg_send![view, setSelectable:YES]; + msg_send![view, setDrawsBackground:YES]; + msg_send![view, setWantsLayer:YES]; + msg_send![view, setLayerContentsRedrawPolicy:1]; + let x = view.clone(); + (Id::from_ptr(view), ShareId::from_ptr(x), Id::from_ptr(initial_string)) + }; + + Text { + inner_mut: inner_mut, + inner_share: inner_share, + background_color: Color::transparent().into_nscolor(), + text_color: Color::transparent().into_nscolor(), + text: s + } + } + + /// Returns a pointer to the underlying Objective-C view. The pointer is not mutable; however, + /// you can send messages to it (unsafely). + pub fn borrow_native_backing_node(&self) -> PlatformSpecificNodeType { + self.inner_share.clone() + } + + /// Appends a child NSText (or subclassed type) to this view. + pub fn append_child(&mut self, child: PlatformSpecificNodeType) { + unsafe { + msg_send![&*self.inner_mut, addSubview:child]; + } + } + + /// Given a `&Style`, will set the frame, background color, borders and so forth. It then + /// calls `setNeedsDisplay:YES` on the Objective-C side, so that Cocoa will re-render this + /// view. + pub fn apply_styles(&mut self, layout: &Layout, style: &Style) { + unsafe { + let rect = NSRect::new( + NSPoint::new(layout.location.x.into(), layout.location.y.into()), + NSSize::new(layout.size.width.into(), layout.size.height.into()) + ); + + self.background_color = style.background_color.into_nscolor(); + self.text_color = style.text_color.into_nscolor(); + + msg_send![&*self.inner_mut, setFrame:rect]; + msg_send![&*self.inner_mut, setBackgroundColor:&*self.background_color]; + msg_send![&*self.inner_mut, setTextColor:&*self.text_color]; + } + } + + pub fn set_text(&mut self, text: &str) { + unsafe { + let string_value = NSString::alloc(nil).init_str(text); + msg_send![&*self.inner_mut, setStringValue:string_value]; + } + } +} + +/// This is used for some specific calls, where macOS NSText needs to be +/// forcefully dragged into the modern age (e.g, position coordinates from top left...). +extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL { + return YES; +} + +/// Registers an `NSText` subclass, and configures it to hold some ivars for various things we need +/// to store. +fn register_class() -> *const Class { + static mut VIEW_CLASS: *const Class = 0 as *const Class; + static INIT: Once = ONCE_INIT; + + INIT.call_once(|| unsafe { + let superclass = Class::get("NSTextField").unwrap(); + let mut decl = ClassDecl::new("AlchemyTextField", superclass).unwrap(); + + // Force NSText to render from the top-left, not bottom-left + //decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL); + + // Opt-in to AutoLayout + decl.add_method(sel!(isSelectable), enforce_normalcy as extern fn(&Object, _) -> BOOL); + decl.add_method(sel!(drawsBackground), enforce_normalcy as extern fn(&Object, _) -> BOOL); + + // Request optimized backing layers + //decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _)); + //decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL); + + // Ensure mouse events and so on work + //decl.add_method(sel!(acceptsFirstResponder), update_layer as extern fn(&Object, _)); + + // A pointer back to our Text, for forwarding mouse + etc events. + // Note that NSText's don't really have a "delegate", I'm just using it here + // for common terminology sake. + decl.add_ivar::(ALCHEMY_DELEGATE); + + VIEW_CLASS = decl.register(); + }); + + unsafe { VIEW_CLASS } +} diff --git a/cocoa/src/view.rs b/cocoa/src/view.rs index 57ead0f..58899e8 100644 --- a/cocoa/src/view.rs +++ b/cocoa/src/view.rs @@ -1,11 +1,5 @@ -//! components/view/mod.rs -//! //! Implements a View Component struct. The most common -//! basic building block of any app. Maps back to native -//! layer per-platform. -//! -//! @author Ryan McGrath -//! @created 03/29/2019 +//! basic building block of any app. Wraps NSView on macOS. use std::sync::{Once, ONCE_INIT}; diff --git a/cocoa/src/window.rs b/cocoa/src/window.rs index c132bc4..d1c8586 100644 --- a/cocoa/src/window.rs +++ b/cocoa/src/window.rs @@ -47,7 +47,7 @@ impl Window { let title = NSString::alloc(nil).init_str(title); window.setTitle_(title); - msg_send![window, setTitlebarAppearsTransparent:YES]; + //msg_send![window, setTitlebarAppearsTransparent:YES]; msg_send![window, setTitleVisibility:1]; // This is very important! NSWindow is an old class and has some behavior that we need diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 4e12edc..96a5ec1 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -9,7 +9,7 @@ use alchemy::{ AppDelegate, Component, Fragment, Props, Error, rsx, RSX, styles, - View, Window, WindowDelegate + Text, View, Window, WindowDelegate }; pub struct AppState { @@ -48,6 +48,7 @@ impl WindowDelegate for WindowState { Ok(rsx! { + "Hello there, my name is Bert" /*{messages.iter().map(|message| rsx! { {text!("{}", message)} @@ -67,6 +68,7 @@ fn main() { let app = alchemy::shared_app(); app.register_styles("default", styles! { + message { width: 500; height: 100; background-color: yellow; color: black; } LOL { background-color: #307ace; width: 500; diff --git a/styles/src/styles.rs b/styles/src/styles.rs index 5578de6..e8b621d 100644 --- a/styles/src/styles.rs +++ b/styles/src/styles.rs @@ -336,7 +336,8 @@ pub struct Style { pub aspect_ratio: Number, // Appearance-based styles - pub background_color: Color + pub background_color: Color, + pub text_color: Color } impl Default for Style { @@ -363,7 +364,8 @@ impl Default for Style { min_size: Default::default(), max_size: Default::default(), aspect_ratio: Default::default(), - background_color: Color::transparent() + background_color: Color::transparent(), + text_color: Color::transparent() } } }