Implement a basic <Text> component in Cocoa - it's cool because it really just uses the same Component lifecycle. Neat!
This commit is contained in:
parent
22e9628b27
commit
e17f05dec5
11 changed files with 205 additions and 42 deletions
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
pub mod fragment;
|
pub mod fragment;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
//pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
pub use fragment::Fragment;
|
pub use fragment::Fragment;
|
||||||
pub use view::View;
|
pub use view::View;
|
||||||
//pub use text::*;
|
pub use text::Text;
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,59 @@
|
||||||
//! components/label.rs
|
//! Handles hoisting per-platform specific Text components.
|
||||||
//!
|
//! Each platform needs the freedom to do some specific things,
|
||||||
//! Implements a Label Component struct. Used for TextNode
|
//! hence why they're all (somewhat annoyingly, but lovingly) re-implemented
|
||||||
//! behind the scenes on most platforms.
|
//! as bridges.
|
||||||
//!
|
|
||||||
//! @author Ryan McGrath <ryan@rymc.io>
|
|
||||||
//! @created 03/29/2019
|
|
||||||
|
|
||||||
use crate::prelude::RSX;
|
use std::sync::{Mutex};
|
||||||
use crate::components::Component;
|
|
||||||
use crate::dom::elements::FlowContent;
|
|
||||||
|
|
||||||
#[derive(RSX, Debug, Default)]
|
use alchemy_styles::styles::{Layout, Style};
|
||||||
pub struct Text {}
|
|
||||||
|
use alchemy_lifecycle::error::Error;
|
||||||
|
use alchemy_lifecycle::rsx::{Props, RSX};
|
||||||
|
use alchemy_lifecycle::traits::{Component, PlatformSpecificNodeType};
|
||||||
|
|
||||||
|
#[cfg(feature = "cocoa")]
|
||||||
|
use alchemy_cocoa::text::{Text as PlatformTextBridge};
|
||||||
|
|
||||||
|
/// 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:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// <Text styles=["styleKey1", "styleKey2"] />
|
||||||
|
/// ```
|
||||||
|
pub struct Text(Mutex<PlatformTextBridge>);
|
||||||
|
|
||||||
|
impl Default for Text {
|
||||||
|
fn default() -> Text {
|
||||||
|
Text(Mutex::new(PlatformTextBridge::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
impl Component for Text {
|
impl Component for Text {
|
||||||
fn create_native_backing_node(&self) -> cocoa::base::id {
|
fn has_native_backing_node(&self) -> bool { true }
|
||||||
use objc::{msg_send, sel, sel_impl};
|
|
||||||
use cocoa::foundation::{NSRect, NSPoint, NSSize};
|
|
||||||
use cocoa::base::id;
|
|
||||||
use crate::components::macos::objc_classes::label;
|
|
||||||
|
|
||||||
let view: cocoa::base::id;
|
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> {
|
||||||
|
let bridge = self.0.lock().unwrap();
|
||||||
unsafe {
|
Some(bridge.borrow_native_backing_node())
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view
|
// Shouldn't be allowed to have child <Text> 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<RSX, Error> {
|
||||||
|
Ok(RSX::None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,3 @@ impl Component for View {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ mod app;
|
||||||
use app::App;
|
use app::App;
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub use components::{Fragment, View};
|
pub use components::{Fragment, Text, View};
|
||||||
|
|
||||||
pub(crate) mod reconciler;
|
pub(crate) mod reconciler;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ fn reduce_styles_into_style(styles: &Vec<Styles>, layout: &mut Style) {
|
||||||
},
|
},
|
||||||
|
|
||||||
Styles::TextAlignment(val) => { },
|
Styles::TextAlignment(val) => { },
|
||||||
Styles::TextColor(val) => { },
|
Styles::TextColor(val) => { layout.text_color = *val; },
|
||||||
Styles::TextDecorationColor(val) => { },
|
Styles::TextDecorationColor(val) => { },
|
||||||
Styles::TextShadowColor(val) => { },
|
Styles::TextShadowColor(val) => { },
|
||||||
Styles::TintColor(val) => { },
|
Styles::TintColor(val) => { },
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,6 @@
|
||||||
|
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod text;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
|
||||||
140
cocoa/src/text.rs
Normal file
140
cocoa/src/text.rs
Normal file
|
|
@ -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<Object>,
|
||||||
|
inner_share: ShareId<Object>,
|
||||||
|
background_color: Id<Object>,
|
||||||
|
text_color: Id<Object>,
|
||||||
|
text: Id<Object>
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<usize>(ALCHEMY_DELEGATE);
|
||||||
|
|
||||||
|
VIEW_CLASS = decl.register();
|
||||||
|
});
|
||||||
|
|
||||||
|
unsafe { VIEW_CLASS }
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
//! components/view/mod.rs
|
|
||||||
//!
|
|
||||||
//! Implements a View Component struct. The most common
|
//! Implements a View Component struct. The most common
|
||||||
//! basic building block of any app. Maps back to native
|
//! basic building block of any app. Wraps NSView on macOS.
|
||||||
//! layer per-platform.
|
|
||||||
//!
|
|
||||||
//! @author Ryan McGrath <ryan@rymc.io>
|
|
||||||
//! @created 03/29/2019
|
|
||||||
|
|
||||||
use std::sync::{Once, ONCE_INIT};
|
use std::sync::{Once, ONCE_INIT};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ impl Window {
|
||||||
|
|
||||||
let title = NSString::alloc(nil).init_str(title);
|
let title = NSString::alloc(nil).init_str(title);
|
||||||
window.setTitle_(title);
|
window.setTitle_(title);
|
||||||
msg_send![window, setTitlebarAppearsTransparent:YES];
|
//msg_send![window, setTitlebarAppearsTransparent:YES];
|
||||||
msg_send![window, setTitleVisibility:1];
|
msg_send![window, setTitleVisibility:1];
|
||||||
|
|
||||||
// This is very important! NSWindow is an old class and has some behavior that we need
|
// This is very important! NSWindow is an old class and has some behavior that we need
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
use alchemy::{
|
use alchemy::{
|
||||||
AppDelegate, Component, Fragment, Props, Error, rsx, RSX, styles,
|
AppDelegate, Component, Fragment, Props, Error, rsx, RSX, styles,
|
||||||
View, Window, WindowDelegate
|
Text, View, Window, WindowDelegate
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
|
@ -48,6 +48,7 @@ impl WindowDelegate for WindowState {
|
||||||
|
|
||||||
Ok(rsx! {
|
Ok(rsx! {
|
||||||
<View styles={messages}>
|
<View styles={messages}>
|
||||||
|
<Text styles=["message"]>"Hello there, my name is Bert"</Text>
|
||||||
<View styles=["boxxx"] />
|
<View styles=["boxxx"] />
|
||||||
/*{messages.iter().map(|message| rsx! {
|
/*{messages.iter().map(|message| rsx! {
|
||||||
<View>{text!("{}", message)}</View>
|
<View>{text!("{}", message)}</View>
|
||||||
|
|
@ -67,6 +68,7 @@ fn main() {
|
||||||
let app = alchemy::shared_app();
|
let app = alchemy::shared_app();
|
||||||
|
|
||||||
app.register_styles("default", styles! {
|
app.register_styles("default", styles! {
|
||||||
|
message { width: 500; height: 100; background-color: yellow; color: black; }
|
||||||
LOL {
|
LOL {
|
||||||
background-color: #307ace;
|
background-color: #307ace;
|
||||||
width: 500;
|
width: 500;
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,8 @@ pub struct Style {
|
||||||
pub aspect_ratio: Number,
|
pub aspect_ratio: Number,
|
||||||
|
|
||||||
// Appearance-based styles
|
// Appearance-based styles
|
||||||
pub background_color: Color
|
pub background_color: Color,
|
||||||
|
pub text_color: Color
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Style {
|
impl Default for Style {
|
||||||
|
|
@ -363,7 +364,8 @@ impl Default for Style {
|
||||||
min_size: Default::default(),
|
min_size: Default::default(),
|
||||||
max_size: Default::default(),
|
max_size: Default::default(),
|
||||||
aspect_ratio: Default::default(),
|
aspect_ratio: Default::default(),
|
||||||
background_color: Color::transparent()
|
background_color: Color::transparent(),
|
||||||
|
text_color: Color::transparent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue