Initial, still major WIP
This commit is contained in:
commit
0c503a549a
21 changed files with 1302 additions and 0 deletions
50
src/calendar.rs
Normal file
50
src/calendar.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//! calendar.rs
|
||||
//!
|
||||
//! The actual implementation of the calendar view/data/etc. Fetches
|
||||
//! upcoming tournaments, ensures everything's good, and passes it on
|
||||
//! demand to the rendering view(s).
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use ruikit::view::View;
|
||||
use ruikit::color::Color;
|
||||
use ruikit::tableview::{TableViewData, TableViewRow};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Data {
|
||||
pub color: Color,
|
||||
pub label: String
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn new(msg: &str, r: i32, g: i32, b: i32) -> Self {
|
||||
Data {
|
||||
label: msg.to_string(),
|
||||
color: Color::rgb(r,g,b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Calendar {
|
||||
pub tournaments: Vec<Data>,
|
||||
pub r: Color
|
||||
}
|
||||
|
||||
impl TableViewData for Calendar {
|
||||
fn number_of_items(&self) -> usize {
|
||||
self.tournaments.len()
|
||||
}
|
||||
|
||||
fn configure_item(&mut self, view: &TableViewRow, row: usize) {
|
||||
println!("Hmmm... {}", row);
|
||||
if row == 3 {
|
||||
//view.set_background_color(&Color::system_red());
|
||||
view.set_background_color(&self.r);
|
||||
}/* else if row == 2 {
|
||||
view.set_background_color(&Color::rgb(5,35,229));
|
||||
} else {
|
||||
view.set_background_color(&Color::system_blue());
|
||||
}*/
|
||||
}
|
||||
}
|
||||
38
src/main.rs
Normal file
38
src/main.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
//! App
|
||||
//!
|
||||
//! An "App" in Rust, which is really just wrapping a lot of platform-specific
|
||||
//! logic. Attempts to do as much as possible in Rust, however the GUI story in
|
||||
//! Rust is the worst thing at the moment, so a lot of this is glorified message
|
||||
//! passing to Objective C and co. ObjC is also one of the best languages ever
|
||||
//! created and you can fight me on this if you so choose.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
extern crate cocoa;
|
||||
extern crate objc_id;
|
||||
extern crate core_graphics;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
|
||||
// extern crate shinekit;
|
||||
mod shinekit;
|
||||
use shinekit::*;
|
||||
|
||||
//mod calendar;
|
||||
//use calendar::{Data, Calendar};
|
||||
|
||||
fn main() {
|
||||
shinekit::run(vec![
|
||||
StyleSheet::default(include_str!("styles/default.json"))
|
||||
], App::new("eSports Calendar", View::named("root").subviews(vec![
|
||||
View::named("sidebar"),
|
||||
View::named("content")
|
||||
])));
|
||||
}
|
||||
82
src/shinekit/application/mod.rs
Normal file
82
src/shinekit/application/mod.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
//! application.rs
|
||||
//!
|
||||
//! Wraps application lifetime pieces across platforms. A lot of
|
||||
//! unsafe code in here since it has to interact with a lot of
|
||||
//! outside actors. With that said, if this crashes... there's bigger
|
||||
//! problems with the system, lol.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use cocoa::base::{id, nil};
|
||||
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSAutoreleasePool};
|
||||
use cocoa::appkit::{
|
||||
NSApp, NSApplication, NSApplicationActivationPolicyRegular,
|
||||
NSRunningApplication, NSApplicationActivateIgnoringOtherApps
|
||||
};
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
pub mod window;
|
||||
use shinekit::color::Color;
|
||||
use shinekit::application::window::Window;
|
||||
use shinekit::view::View;
|
||||
|
||||
pub struct App {
|
||||
pub app: id,
|
||||
pub window: Window,
|
||||
}
|
||||
/*
|
||||
pub trait Delegate {
|
||||
// AppDelegate lifecycle callbacks
|
||||
fn did_finish_launching(&self) {}
|
||||
fn will_become_active(&self) {}
|
||||
fn will_enter_foreground(&self) {}
|
||||
fn did_become_active(&self) {}
|
||||
fn will_resign_active(&self) {}
|
||||
fn did_enter_background(&self) {}
|
||||
fn will_terminate(&self) {}
|
||||
|
||||
// Override-able settings
|
||||
fn initial_window_rect(&self) -> (f64, f64, f64, f64) { (0., 0., 800., 600.) }
|
||||
fn window_mask(&self) -> NSWindowStyleMask {
|
||||
}
|
||||
}*/
|
||||
|
||||
impl App {
|
||||
pub fn new(title: &str, view: View) -> Self {
|
||||
App {
|
||||
app: unsafe {
|
||||
let _pool = NSAutoreleasePool::new(nil);
|
||||
let app = NSApp();
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
app
|
||||
},
|
||||
|
||||
window: Window::new(view, title, 0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self) {
|
||||
unsafe {
|
||||
let current_app = NSRunningApplication::currentApplication(nil);
|
||||
current_app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps);
|
||||
self.window.make_key();
|
||||
self.app.run();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_styles(&mut self, styles: &mut Map<String, Value>) {
|
||||
let bg_color = Color::from_json(&styles["window"]["backgroundColor"]);
|
||||
self.window.set_background_color(bg_color);
|
||||
|
||||
let width = &styles["window"]["defaultWidth"].as_f64().unwrap();
|
||||
let height = &styles["window"]["defaultHeight"].as_f64().unwrap();
|
||||
let rect = NSRect::new(NSPoint::new(0., 0.), NSSize::new(*width, *height));
|
||||
self.window.set_frame(rect);
|
||||
|
||||
self.window.content_view.apply_styles(styles);
|
||||
}
|
||||
}
|
||||
|
||||
//impl Delegate for App {}
|
||||
74
src/shinekit/application/window.rs
Normal file
74
src/shinekit/application/window.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
//! window.rs
|
||||
//!
|
||||
//! Handles creating/maintaining/applying styles for the root backing window of
|
||||
//! an app. Very Desktop dependent, as mobile apps... well, they're just in a
|
||||
//! window by default.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use cocoa::base::{id, nil, YES, NO};
|
||||
use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSBackingStoreType};
|
||||
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSAutoreleasePool};
|
||||
|
||||
use shinekit::color::Color;
|
||||
use shinekit::view::View;
|
||||
|
||||
pub struct Window {
|
||||
pub window: id,
|
||||
pub content_view: View
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(view: View, title: &str, top: i32, left: i32, width: i32, height: i32) -> Self {
|
||||
Window {
|
||||
window: unsafe {
|
||||
let style = NSWindowStyleMask::NSResizableWindowMask |
|
||||
NSWindowStyleMask::NSUnifiedTitleAndToolbarWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask |
|
||||
NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask;
|
||||
//NSWindowStyleMask::NSWindowStyleMaskResizable | NSWindowStyleMask::NSWindowStyleMaskMiniaturizable | NSWindowStyleMask::NSWindowStyleMaskClosable | NSWindowStyleMask::NSWindowStyleMaskTitled | NSWindowStyleMask::NSWindowStyleMaskUnifiedTitleAndToolbar
|
||||
|
||||
let window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_(
|
||||
NSRect::new(NSPoint::new(top.into(), left.into()), NSSize::new(width.into(), height.into())),
|
||||
style,
|
||||
NSBackingStoreType::NSBackingStoreBuffered,
|
||||
NO
|
||||
).autorelease();
|
||||
|
||||
let title = NSString::alloc(nil).init_str(title);
|
||||
window.setTitle_(title);
|
||||
view.translates_resizing_mask_into_constraints(true);
|
||||
msg_send![window, setContentView:view.get_root_backing_node()];
|
||||
msg_send![window, setTitlebarAppearsTransparent:YES];
|
||||
msg_send![window, setTitleVisibility:1];
|
||||
window
|
||||
},
|
||||
content_view: view,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_key(&self) {
|
||||
unsafe {
|
||||
self.window.makeKeyAndOrderFront_(nil);
|
||||
}
|
||||
}
|
||||
|
||||
/*pub fn set_content_view(&mut self, view: &T) {
|
||||
unsafe {
|
||||
view.translates_resizing_mask_into_constraints(true);
|
||||
msg_send![self.window, setContentView:&*view.get_root_backing_node()];
|
||||
}
|
||||
}*/
|
||||
|
||||
pub fn set_frame(&self, rect: NSRect) {
|
||||
unsafe {
|
||||
msg_send![self.window, setFrame:rect display:YES];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_background_color(&self, color: Color) {
|
||||
unsafe {
|
||||
msg_send![self.window, setBackgroundColor:color.into_platform_specific_color()];
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/shinekit/color.rs
Normal file
46
src/shinekit/color.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
///
|
||||
/// Color.rs
|
||||
///
|
||||
/// Interface that wraps [NS/UI]Color depending on the platform. Decidedly
|
||||
/// basic as I don't care to get into the whole colorspace issue, and would
|
||||
/// rather just be able to put color on a screen.
|
||||
///
|
||||
/// @author Ryan McGrath <ryan@rymc.io>
|
||||
/// @created 05/23/2018
|
||||
///
|
||||
|
||||
use cocoa::base::{id, class};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Color {
|
||||
pub r: f64,
|
||||
pub g: f64,
|
||||
pub b: f64,
|
||||
pub a: f64
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Color {
|
||||
pub fn rgb(r: i32, g: i32, b: i32) -> Self {
|
||||
Color {
|
||||
r: r as f64 / 255.0,
|
||||
g: g as f64 / 255.0,
|
||||
b: b as f64 / 255.0,
|
||||
a: 1.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_json(obj: &Value) -> Self {
|
||||
Color {
|
||||
r: obj["r"].as_f64().unwrap() / 255.0,
|
||||
g: obj["g"].as_f64().unwrap() / 255.0,
|
||||
b: obj["b"].as_f64().unwrap() / 255.0,
|
||||
a: 1.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_platform_specific_color(&self) -> id {
|
||||
unsafe { msg_send![class("NSColor"), colorWithRed:self.r green:self.g blue:self.b alpha:self.a] }
|
||||
}
|
||||
}
|
||||
26
src/shinekit/layout.rs
Normal file
26
src/shinekit/layout.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! layout.rs
|
||||
//!
|
||||
//! A trait that more or less allows any other types to compose
|
||||
//! a "subclass" for a view. By using the actual backing [UI|NS]View
|
||||
//! for storing certain properties, it becomes much easier to get around
|
||||
//! Rust's (good-for-you) restrictions.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use objc::declare::ClassDecl;
|
||||
use objc::runtime::{Object, BOOL};
|
||||
use cocoa::base::{class, id, nil, YES, NO};
|
||||
use cocoa::foundation::NSArray;
|
||||
|
||||
use shinekit::view::View;
|
||||
|
||||
pub trait Layout {
|
||||
fn get_subviews(&self) -> &Vec<View>;
|
||||
fn get_root_backing_node(&self) -> &Object;
|
||||
fn set_constraint_ivar(&mut self, ivar: &str, constraint: id);
|
||||
|
||||
}
|
||||
|
||||
pub fn add_autolayout_ivars(decl: &mut ClassDecl) {
|
||||
}
|
||||
93
src/shinekit/listview/datasource.rs
Normal file
93
src/shinekit/listview/datasource.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
//! tableview_delegate_internal.rs
|
||||
//!
|
||||
//! This is a struct that handles the nitty gritty implementation of [NS/UI]TableViews.
|
||||
//! These are classes that have a lot of delegate-based implementation, so this is
|
||||
//! essentially bridging that gap for Rust.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use std::sync::{Once, ONCE_INIT};
|
||||
|
||||
use objc::declare::ClassDecl;
|
||||
use objc::runtime::{Class, Object, Sel};
|
||||
use cocoa::base::{id, nil, NO, YES};
|
||||
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSInteger};
|
||||
|
||||
use shinekit::tableview::row::TableViewRow;
|
||||
use shinekit::view::register_view_class;
|
||||
|
||||
pub trait TableViewData {
|
||||
fn number_of_items(&self) -> usize { 0 }
|
||||
fn configure_item(&mut self, view: &TableViewRow, row: usize);
|
||||
}
|
||||
|
||||
extern fn number_of_items<T: TableViewData>(this: &Object, _: Sel, _: id) -> NSInteger {
|
||||
println!("number_of_items");
|
||||
let count: usize;
|
||||
|
||||
unsafe {
|
||||
let state: usize = *this.get_ivar("shinekitDataSourceAndDelegate");
|
||||
let state = state as *mut T;
|
||||
count = (*state).number_of_items();
|
||||
}
|
||||
|
||||
return count as NSInteger;
|
||||
}
|
||||
|
||||
extern fn make_view<T: TableViewData>(this: &Object, _: Sel, table_view: id, _: id, row: NSInteger) -> id {
|
||||
println!("make_view");
|
||||
let mut cell: id;
|
||||
let view: TableViewRow;
|
||||
let mut requires_layout = false;
|
||||
|
||||
unsafe {
|
||||
let state: usize = *this.get_ivar("shinekitgSourceAndDelegate");
|
||||
let state = state as *mut T;
|
||||
|
||||
let title = NSString::alloc(nil).init_str("WHAT");
|
||||
cell = msg_send![table_view, makeViewWithIdentifier:title owner:nil];
|
||||
if cell == nil {
|
||||
println!(" Creating new cell...");
|
||||
requires_layout = true;
|
||||
let default_frame = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
|
||||
let cls: id = msg_send![register_view_class(), alloc];
|
||||
cell = msg_send![cls, initWithFrame:default_frame];
|
||||
msg_send![cell, setIdentifier:title];
|
||||
msg_send![cell, setWantsLayer:YES];
|
||||
msg_send![cell, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
}
|
||||
|
||||
view = TableViewRow {
|
||||
row: row as usize,
|
||||
view: cell
|
||||
};
|
||||
println!("Configuring cell...");
|
||||
(*state).configure_item(&view, row as usize);
|
||||
println!("Configured Cell");
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
pub fn register_listview_delegate_class<T: TableViewData>() -> *const Class {
|
||||
static mut delegate_class: *const Class = 0 as *const Class;
|
||||
static INIT: Once = ONCE_INIT;
|
||||
|
||||
INIT.call_once(|| unsafe {
|
||||
let superclass = Class::get("NSObject").unwrap();
|
||||
let mut decl = ClassDecl::new("shinekitDataSourceAndDelegate", superclass).unwrap();
|
||||
|
||||
// Add callback methods
|
||||
decl.add_method(sel!(numberOfRowsInTableView:), number_of_items::<T> as extern fn(&Object, _, id) -> NSInteger);
|
||||
decl.add_method(sel!(tableView:viewForTableColumn:row:), make_view::<T> as extern fn(&Object, _, id, id, NSInteger) -> id);
|
||||
|
||||
// Store internal state as user data
|
||||
decl.add_ivar::<usize>("shinekitDataSourceAndDelegate");
|
||||
delegate_class = decl.register();
|
||||
});
|
||||
|
||||
unsafe {
|
||||
delegate_class
|
||||
}
|
||||
}
|
||||
97
src/shinekit/listview/mod.rs
Normal file
97
src/shinekit/listview/mod.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
//! table.rs
|
||||
//!
|
||||
//! Wraps TableView class(es) on supported platforms. On macOS (OS X!) it wraps NSTableView,
|
||||
//! and where possible on other platforms it wraps UITableView (Windows, for instance). In
|
||||
//! either case this wraps the underlying APIs to mimic each other and remove nuisances that
|
||||
//! exist.
|
||||
//!
|
||||
//! Many might ask "Why not \*CollectionView?", and to be fair, it's not a bad question. The
|
||||
//! TableView APIs tend to be much better and smoother with regards to auto-calculating view
|
||||
//! heights for dynamic entries, so... I just go with those when possible. Less headache.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/23/2018
|
||||
|
||||
use objc_id::Id;
|
||||
use cocoa::base::{class, id, nil, NO, YES};
|
||||
use cocoa::foundation::NSString;
|
||||
|
||||
pub mod row;
|
||||
pub mod datasource;
|
||||
use shinekit::view::View;
|
||||
use shinekit::scrollview::ScrollView;
|
||||
use shinekit::util::empty_frame;
|
||||
use shinekit::tableview::datasource::register_delegate_class;
|
||||
pub use shinekit::tableview::row::{TableViewRow, TableViewUI};
|
||||
pub use shinekit::tableview::datasource::TableViewData;
|
||||
|
||||
pub struct ListView {
|
||||
pub view: Id<Object>,
|
||||
pub scrollview: ScrollView,
|
||||
delegate: Id<Object>
|
||||
}
|
||||
|
||||
impl ListView {
|
||||
pub fn new<T: TableViewData>(datasource: T) -> Self {
|
||||
let tableview: id;
|
||||
let scrollview = ScrollView::new();
|
||||
let delegate: id;
|
||||
|
||||
unsafe {
|
||||
let cls: id = msg_send![register_list_class(), alloc];
|
||||
tableview = msg_send![cls, initWithFrame:empty_frame()];
|
||||
msg_send![tableview, setWantsLayer:YES];
|
||||
msg_send![tableview, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
msg_send![tableview, setUsesAutomaticRowHeights:YES];
|
||||
msg_send![tableview, setRowHeight:100.];
|
||||
msg_send![tableview, setFloatsGroupRows:YES];
|
||||
msg_send![tableview, setIntercellSpacing:NSSize::new(0., 0.)];
|
||||
msg_send![tableview, setColumnAutoresizingStyle:1];
|
||||
msg_send![tableview, setUsesAlternatingRowBackgroundColors:NO];
|
||||
//msg_send![tableview, setSelectionHighlightStyle:-1];
|
||||
msg_send![tableview, setAllowsEmptySelection:YES];
|
||||
msg_send![tableview, setAllowsMultipleSelection:NO];
|
||||
msg_send![tableview, setHeaderView:nil];
|
||||
|
||||
// NSTableView requires at least one column to be manually added if doing so by code.
|
||||
// A relic of a bygone era, indeed.
|
||||
let default_column_alloc: id = msg_send![class("NSTableColumn"), new];
|
||||
let default_column: id = msg_send![default_column_alloc, initWithIdentifier:NSString::alloc(nil).init_str("Wut")];
|
||||
msg_send![default_column, setResizingMask:(1<<0)];
|
||||
msg_send![tableview, addTableColumn:default_column];
|
||||
|
||||
delegate = msg_send![register_delegate_class::<T>(), new];
|
||||
//msg_send![scrollview, setDocumentView:tableview];
|
||||
}
|
||||
|
||||
let mut state = Box::new(datasource);
|
||||
let state_ptr: *mut T = &mut *state;
|
||||
unsafe {
|
||||
(&mut *delegate).set_ivar("shinekit_tableview_datasource", state_ptr as usize);
|
||||
msg_send![tableview, setDelegate:delegate];
|
||||
msg_send![tableview, setDataSource:delegate];
|
||||
}
|
||||
|
||||
TableView {
|
||||
view: IdRef::new(tableview),
|
||||
scrollview: IdRef::new(scrollview),
|
||||
delegate: IdRef::new(delegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_list_class() -> *const Class {
|
||||
static mut list_class: *const Class = 0 as *const Class;
|
||||
static INIT: Once = ONCE_INIT;
|
||||
|
||||
INIT.call_once(|| unsafe {
|
||||
let superclass = Class::get("NSTableView").unwrap();
|
||||
let mut decl = ClassDecl::new("shinekitListView", superclass).unwrap();
|
||||
add_autolayout_ivars(&mut decl);
|
||||
list_class = decl.register();
|
||||
});
|
||||
|
||||
unsafe {
|
||||
list_class
|
||||
}
|
||||
}
|
||||
23
src/shinekit/listview/row.rs
Normal file
23
src/shinekit/listview/row.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//! row.rs
|
||||
//!
|
||||
//! A default implementation for a TableView row, which... well, depending
|
||||
//! on the platform and/or environment, requires a few different things. This
|
||||
//! ensures that we at least have the following:
|
||||
//!
|
||||
//! - Properly flipped coordinates, because it's {CURRENT_YEAR} and who
|
||||
//! the hell judges from the bottom left. Confuses all newcomers.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use cocoa::base::{id};
|
||||
|
||||
pub trait TableViewUI {
|
||||
fn layout(&self, view: &TableViewRow);
|
||||
fn update(&self, view: &TableViewRow);
|
||||
}
|
||||
|
||||
pub struct TableViewRow {
|
||||
pub row: usize,
|
||||
pub view: id
|
||||
}
|
||||
31
src/shinekit/mod.rs
Normal file
31
src/shinekit/mod.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//! mod.rs
|
||||
//!
|
||||
//! Shinekit main module, which is basically handling various things
|
||||
//! like ensuring styles are registered, the App starts running, and
|
||||
//! everything is properly resolved and installed.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
pub mod application;
|
||||
//pub mod tableview;
|
||||
pub mod color;
|
||||
//pub mod scrollview;
|
||||
//pub mod text;
|
||||
pub mod util;
|
||||
pub mod view;
|
||||
pub mod stylesheet;
|
||||
|
||||
pub use application::App;
|
||||
pub use stylesheet::StyleSheet;
|
||||
pub use view::View;
|
||||
|
||||
use serde_json::Value;
|
||||
use stylesheet::load_styles;
|
||||
|
||||
pub fn run(user_styles: Vec<(String, Value)>, mut application: App) {
|
||||
let mut styles = load_styles(user_styles);
|
||||
let mut current_style = styles["default"].as_object_mut().unwrap();
|
||||
application.apply_styles(&mut current_style);
|
||||
application.run();
|
||||
}
|
||||
61
src/shinekit/scrollview.rs
Normal file
61
src/shinekit/scrollview.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
//! scrollview.rs
|
||||
//!
|
||||
//! Handles making NSScrollView behave more like a UIKit counterpart. Wraps
|
||||
//! up various methods to make it more... enjoyable to use.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use std::sync::{Once, ONCE_INIT};
|
||||
|
||||
use objc_id::Id;
|
||||
use objc::declare::ClassDecl;
|
||||
use objc::runtime::{Class, Object};
|
||||
|
||||
use cocoa::base::{id, YES, NO};
|
||||
|
||||
use shinekit::util::empty_frame;
|
||||
use shinekit::layout::{Layout, add_autolayout_ivars};
|
||||
|
||||
pub struct ScrollView {
|
||||
backing_node: Id<Object>
|
||||
}
|
||||
|
||||
impl ScrollView {
|
||||
pub fn new() -> Self {
|
||||
ScrollView {
|
||||
backing_node: unsafe {
|
||||
let ins: id = msg_send![register_scrollview_class(), alloc];
|
||||
let scrollview: id = msg_send![ins, initWithFrame:empty_frame()];
|
||||
msg_send![scrollview, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
msg_send![scrollview, setDrawsBackground:NO];
|
||||
msg_send![scrollview, setWantsLayer:YES];
|
||||
msg_send![scrollview, setBorderType:0];
|
||||
msg_send![scrollview, setHorizontalScrollElasticity:1];
|
||||
msg_send![scrollview, setHasVerticalScroller:YES];
|
||||
Id::from_ptr(scrollview)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for ScrollView {
|
||||
fn get_root_backing_node(&self) -> &Object { &*self.backing_node }
|
||||
fn set_constraint_ivar(&mut self, ivar: &str, constraint: id) { unsafe { self.backing_node.set_ivar(ivar, constraint); } }
|
||||
}
|
||||
|
||||
fn register_scrollview_class() -> *const Class {
|
||||
static mut scrollview_class: *const Class = 0 as *const Class;
|
||||
static INIT: Once = ONCE_INIT;
|
||||
|
||||
INIT.call_once(|| unsafe {
|
||||
let superclass = Class::get("NSScrollView").unwrap();
|
||||
let mut decl = ClassDecl::new("shinekitScrollView", superclass).unwrap();
|
||||
add_autolayout_ivars(&mut decl);
|
||||
scrollview_class = decl.register();
|
||||
});
|
||||
|
||||
unsafe {
|
||||
scrollview_class
|
||||
}
|
||||
}
|
||||
40
src/shinekit/stylesheet.rs
Normal file
40
src/shinekit/stylesheet.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// stylesheet.rs
|
||||
//
|
||||
// A basic struct for implementing namespaced stylesheets. Useful
|
||||
// so themes are built-in from the beginning. Currently mostly a
|
||||
// fun little JSON based thing, but I could see this being made
|
||||
// better in the future.
|
||||
//
|
||||
// @author Ryan McGrath <ryan@rymc.io>
|
||||
// @created 05/30/2018
|
||||
|
||||
use serde_json::{Map, Value, from_str};
|
||||
use util::merge_json_values;
|
||||
|
||||
pub struct StyleSheet {}
|
||||
|
||||
impl StyleSheet {
|
||||
pub fn default(styles: &str) -> (String, Value) {
|
||||
("default".into(), from_str(styles).expect("Could not parse default JSON stylesheet"))
|
||||
}
|
||||
|
||||
pub fn theme(name: &str, styles: &str) -> (String, Value) {
|
||||
(name.into(), from_str(styles).expect(&format!("Could not parse {} stylesheet", name)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_styles(user_styles: Vec<(String, Value)>) -> Map<String, Value> {
|
||||
let mut styles = Map::new();
|
||||
for (name, value) in user_styles.into_iter() {
|
||||
if styles.contains_key("default") {
|
||||
let mut style = json!({});
|
||||
merge_json_values(&mut style, &styles["default"]);
|
||||
merge_json_values(&mut style, &value);
|
||||
styles.insert(name, style);
|
||||
} else {
|
||||
styles.insert(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
styles
|
||||
}
|
||||
92
src/shinekit/text.rs
Normal file
92
src/shinekit/text.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
//! text.rs
|
||||
//!
|
||||
//! A class that wraps NSTextField and/or UILabel to make them act pretty
|
||||
//! much the same across platforms. Believe it or not... this is a thing.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use std::sync::{Once, ONCE_INIT};
|
||||
|
||||
use objc_id::Id;
|
||||
use objc::declare::ClassDecl;
|
||||
use objc::runtime::{Class, Object};
|
||||
|
||||
use cocoa::base::{id, nil, YES, NO};
|
||||
use cocoa::foundation::NSString;
|
||||
|
||||
use shinekit::color::Color;
|
||||
use shinekit::util::empty_frame;
|
||||
use shinekit::layout::{Layout, add_autolayout_ivars};
|
||||
|
||||
pub struct Text {
|
||||
pub backing_node: Id<Object>
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Text {
|
||||
pub fn new() -> Self {
|
||||
Text {
|
||||
backing_node: unsafe {
|
||||
let alloc: id = msg_send![register_text_class(), alloc];
|
||||
let view: id = msg_send![alloc, initWithFrame:empty_frame()];
|
||||
msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
msg_send![view, setEditable:NO];
|
||||
msg_send![view, setBezeled:NO];
|
||||
msg_send![view, setBordered:NO];
|
||||
msg_send![view, setDrawsBackground:YES];
|
||||
msg_send![view, setAllowsEditingTextAttributes:NO];
|
||||
msg_send![view, setContentCompressionResistancePriority:250 forOrientation:0];
|
||||
|
||||
let cell: id = msg_send![view, cell];
|
||||
msg_send![cell, setUsesSingleLineMode:NO];
|
||||
msg_send![cell, setWraps:YES];
|
||||
msg_send![cell, setLineBreakMode:0];
|
||||
Id::from_ptr(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_background_color(&mut self, color: &Color) {
|
||||
unsafe {
|
||||
self.backing_node.set_ivar("shinekitBackgroundColor", color.into_platform_specific_color());
|
||||
msg_send![&*self.backing_node, setNeedsDisplay:YES];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&self, text: &str) {
|
||||
unsafe {
|
||||
let value = NSString::alloc(nil).init_str(text);
|
||||
msg_send![&*self.backing_node, setStringValue:value];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text_color(&self, color: &Color) {
|
||||
unsafe {
|
||||
msg_send![&*self.backing_node, setTextColor:color.into_platform_specific_color()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for Text {
|
||||
fn get_root_backing_node(&self) -> &Object { &*self.backing_node }
|
||||
fn set_constraint_ivar(&mut self, ivar: &str, constraint: id) { unsafe { self.backing_node.set_ivar(ivar, constraint); } }
|
||||
}
|
||||
|
||||
fn register_text_class() -> *const Class {
|
||||
static mut text_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("shinekitTextLabel", superclass).unwrap();
|
||||
//decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||
//decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _));
|
||||
add_autolayout_ivars(&mut decl);
|
||||
text_class = decl.register();
|
||||
});
|
||||
|
||||
unsafe {
|
||||
text_class
|
||||
}
|
||||
}
|
||||
28
src/shinekit/util.rs
Normal file
28
src/shinekit/util.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//! util.rs
|
||||
//!
|
||||
//! Basic utility functions to handle some boilerplate things littered
|
||||
//! throughout this app.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use serde_json::{Value};
|
||||
use cocoa::foundation::{NSRect, NSPoint, NSSize};
|
||||
|
||||
pub fn empty_frame() -> NSRect {
|
||||
NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
|
||||
}
|
||||
|
||||
pub fn merge_json_values(a: &mut Value, b: &Value) {
|
||||
match (a, b) {
|
||||
(&mut Value::Object(ref mut a), &Value::Object(ref b)) => {
|
||||
for (k, v) in b {
|
||||
merge_json_values(a.entry(k.clone()).or_insert(Value::Null), v);
|
||||
}
|
||||
}
|
||||
|
||||
(a, b) => {
|
||||
*a = b.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
218
src/shinekit/view.rs
Normal file
218
src/shinekit/view.rs
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
//! view.rs
|
||||
//!
|
||||
//! "Fixes" NSView to be a bit more... how does one say, modern. Flips drawing
|
||||
//! and layout coordinates to be fitting for {{CURRENT YEAR}}, layer-backs it all
|
||||
//! by default, and does some ivar trickery to make NSColor less of a headache.
|
||||
//!
|
||||
//! @author Ryan McGrath <ryan@rymc.io>
|
||||
//! @created 05/30/2018
|
||||
|
||||
use std::sync::{Once, ONCE_INIT};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use objc_id::Id;
|
||||
use objc::declare::ClassDecl;
|
||||
use objc::runtime::{Class, Object, Sel, BOOL};
|
||||
use cocoa::foundation::NSArray;
|
||||
use cocoa::base::{class, id, nil, YES, NO};
|
||||
|
||||
use shinekit::color::Color;
|
||||
use shinekit::util::empty_frame;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ViewKind {
|
||||
View,
|
||||
Label
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct View {
|
||||
pub kind: ViewKind,
|
||||
pub name: String,
|
||||
pub backing_node: Id<Object>,
|
||||
pub subviews: Vec<View>
|
||||
}
|
||||
|
||||
unsafe fn create_view_backing_node() -> Id<Object> {
|
||||
let alloc: id = msg_send![register_view_class(), alloc];
|
||||
let view: id = msg_send![alloc, initWithFrame:empty_frame()];
|
||||
msg_send![view, setWantsLayer:YES];
|
||||
msg_send![view, setLayerContentsRedrawPolicy:1];
|
||||
msg_send![view, setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
Id::from_ptr(view)
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn named(name: &str) -> Self {
|
||||
View {
|
||||
name: name.into(),
|
||||
kind: ViewKind::View,
|
||||
backing_node: unsafe { create_view_backing_node() },
|
||||
subviews: vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subviews(self, views: Vec<View>) -> Self {
|
||||
let mut subviews = vec![];
|
||||
unsafe {
|
||||
for view in views.into_iter() {
|
||||
msg_send![&*self.backing_node, addSubview:&*view.backing_node];
|
||||
subviews.push(view);
|
||||
}
|
||||
}
|
||||
|
||||
View {
|
||||
subviews: subviews,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_background_color(&mut self, color: &Color) {
|
||||
unsafe {
|
||||
self.backing_node.set_ivar("shinekitBackgroundColor", color.into_platform_specific_color());
|
||||
msg_send![&*self.backing_node, setNeedsDisplay:YES];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_styles(&mut self, styles: &mut Map<String, Value>) {
|
||||
let bg_color = Color::from_json(&styles[&self.name]["backgroundColor"]);
|
||||
self.set_background_color(&bg_color);
|
||||
|
||||
for view in &mut self.subviews {
|
||||
view.apply_styles(styles);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_root_backing_node(&self) -> &Object { &*self.backing_node }
|
||||
pub fn get_subviews(&self) -> &Vec<View> { &self.subviews }
|
||||
pub fn set_constraint_ivar(&mut self, ivar: &str, constraint: id) { unsafe { self.backing_node.set_ivar(ivar, constraint); } }
|
||||
|
||||
pub fn add_subview(&self, view: &View) {
|
||||
unsafe {
|
||||
msg_send![self.get_root_backing_node(), addSubview:view.get_root_backing_node()];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translates_resizing_mask_into_constraints(&self, translates: bool) {
|
||||
let t: BOOL = if translates { YES } else { NO };
|
||||
unsafe {
|
||||
msg_send![self.get_root_backing_node(), setTranslatesAutoresizingMaskIntoConstraints:t];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&mut self, width: i32) {
|
||||
unsafe {
|
||||
let anchor: id = msg_send![self.get_root_backing_node(), widthAnchor];
|
||||
let constraint: id = msg_send![anchor, constraintEqualToConstant:width as f64];
|
||||
self.set_constraint_ivar("shinekitConstraintWidth", constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&mut self, height: i32) {
|
||||
unsafe {
|
||||
let anchor: id = msg_send![self.get_root_backing_node(), heightAnchor];
|
||||
let constraint: id = msg_send![anchor, constraintEqualToConstant:height as f64];
|
||||
self.set_constraint_ivar("shinekitConstraintHeight", constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn top_relative_to(&mut self, view: &View, margin: i32) {
|
||||
unsafe {
|
||||
let top_anchor: id = msg_send![self.get_root_backing_node(), topAnchor];
|
||||
let view_top_anchor: id = msg_send![view.get_root_backing_node(), topAnchor];
|
||||
let constraint: id = msg_send![top_anchor, constraintEqualToAnchor:view_top_anchor constant:margin as f64];
|
||||
self.set_constraint_ivar("shinekitConstraintTop", constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leading_relative_to(&mut self, view: &View, margin: i32) {
|
||||
unsafe {
|
||||
let leading_anchor: id = msg_send![self.get_root_backing_node(), leadingAnchor];
|
||||
let view_leading_anchor: id = msg_send![view.get_root_backing_node(), leadingAnchor];
|
||||
let constraint: id = msg_send![leading_anchor, constraintEqualToAnchor:view_leading_anchor constant:margin as f64];
|
||||
self.set_constraint_ivar("shinekitConstraintLeading", constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trailing_relative_to(&mut self, view: &View, margin: i32) {
|
||||
let m = margin as f64 * -1.;
|
||||
unsafe {
|
||||
let trailing_anchor: id = msg_send![self.get_root_backing_node(), trailingAnchor];
|
||||
let view_trailing_anchor: id = msg_send![view.get_root_backing_node(), trailingAnchor];
|
||||
let constraint: id = msg_send![trailing_anchor, constraintEqualToAnchor:view_trailing_anchor constant:m];
|
||||
self.set_constraint_ivar("shinekitConstraintTrailing", constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bottom_relative_to(&mut self, view: &View, margin: i32) {
|
||||
let m = margin as f64 * -1.;
|
||||
unsafe {
|
||||
let bottom_anchor: id = msg_send![self.get_root_backing_node(), bottomAnchor];
|
||||
let view_bottom_anchor: id = msg_send![view.get_root_backing_node(), bottomAnchor];
|
||||
let constraint: id = msg_send![bottom_anchor, constraintEqualToAnchor:view_bottom_anchor constant:m];
|
||||
self.set_constraint_ivar("shinekitConstraintBottom", constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_constraints(&self) {
|
||||
unsafe {
|
||||
let mut anchors: Vec<id> = vec![];
|
||||
|
||||
let ivars = [
|
||||
"shinekitConstraintWidth", "shinekitConstraintHeight",
|
||||
"shinekitConstraintTop", "shinekitConstraintLeading",
|
||||
"shinekitConstraintTrailing", "shinekitConstraintBottom"
|
||||
];
|
||||
|
||||
let view = self.get_root_backing_node();
|
||||
for ivar in &ivars {
|
||||
let constraint: id = *view.get_ivar(ivar);
|
||||
if constraint != nil { anchors.push(constraint); }
|
||||
}
|
||||
|
||||
let constraints = NSArray::arrayWithObjects(nil, &anchors);
|
||||
msg_send![class("NSLayoutConstraint"), activateConstraints:constraints];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
|
||||
return YES;
|
||||
}
|
||||
|
||||
extern fn update_layer(this: &Object, _: Sel) {
|
||||
unsafe {
|
||||
let background_color: id = *this.get_ivar("shinekitBackgroundColor");
|
||||
if background_color != nil {
|
||||
let layer: id = msg_send![this, layer];
|
||||
let cg: id = msg_send![background_color, CGColor];
|
||||
msg_send![layer, setBackgroundColor:cg];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_view_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("NSView").unwrap();
|
||||
let mut decl = ClassDecl::new("shinekitView", superclass).unwrap();
|
||||
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||
decl.add_method(sel!(requiresConstraintBasedLayout), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||
decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
|
||||
decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _));
|
||||
decl.add_ivar::<id>("shinekitBackgroundColor");
|
||||
decl.add_ivar::<id>("shinekitConstraintWidth");
|
||||
decl.add_ivar::<id>("shinekitConstraintHeight");
|
||||
decl.add_ivar::<id>("shinekitConstraintTop");
|
||||
decl.add_ivar::<id>("shinekitConstraintLeading");
|
||||
decl.add_ivar::<id>("shinekitConstraintTrailing");
|
||||
decl.add_ivar::<id>("shinekitConstraintBottom");
|
||||
view_class = decl.register();
|
||||
});
|
||||
|
||||
unsafe {
|
||||
view_class
|
||||
}
|
||||
}
|
||||
5
src/styles/dark.json
Normal file
5
src/styles/dark.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"calendar": {
|
||||
"backgroundColor": "blue"
|
||||
}
|
||||
}
|
||||
27
src/styles/default.json
Normal file
27
src/styles/default.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"window": {
|
||||
"backgroundColor": {"r": 35, "g": 108, "b": 218},
|
||||
"defaultWidth": 800,
|
||||
"defaultHeight": 600
|
||||
},
|
||||
|
||||
"root": {
|
||||
"backgroundColor": {"r": 35, "g": 108, "b": 218}
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"backgroundColor": {"r": 5, "g": 5, "b": 5},
|
||||
"width": 200,
|
||||
"top": "root.top",
|
||||
"left": "root.left",
|
||||
"bottom": "root.bottom"
|
||||
},
|
||||
|
||||
"content": {
|
||||
"backgroundColor": {"r": 35, "g": 108, "b": 218},
|
||||
"top": "root.top",
|
||||
"left": "sidebar.right",
|
||||
"right": "root.right",
|
||||
"bottom": "root.bottom"
|
||||
}
|
||||
}
|
||||
Reference in a new issue