Resolve merge conflict

This commit is contained in:
Ryan McGrath 2023-08-01 00:20:32 -07:00
commit 33624e9418
41 changed files with 1278 additions and 178 deletions

View file

@ -13,7 +13,7 @@ Have a look at the [issue tracker](https://github.com/ryanmcgrath/cacao/issues).
describing your problem (or a very similar one) there, please open a new issue with describing your problem (or a very similar one) there, please open a new issue with
the following details: the following details:
- Which versions of Rust and cacao (and macOS/iOS build/device) are you using? - Which versions of Rust and Cacao (and macOS/iOS build/device) are you using?
- Which feature flags are you using? - Which feature flags are you using?
- What are you trying to accomplish? - What are you trying to accomplish?
- What is the full error you are seeing? - What is the full error you are seeing?
@ -34,17 +34,17 @@ If you can't find an issue (open or closed) describing your idea on the [issue
tracker], open an issue. Adding answers to the following tracker], open an issue. Adding answers to the following
questions in your description is +1: questions in your description is +1:
- What do you want to do, and how do you expect Alchemy to support you with that? - What do you want to do, and how do you expect Cacao to support you with that?
- How might this be added to Alchemy? - How might this be added to Cacao?
- What are possible alternatives? - What are possible alternatives?
- Are there any disadvantages? - Are there any disadvantages?
Thank you! Thank you!
## Contribute code to Alchemy ## Contribute code to Cacao
### Setting up cacao locally ### Setting up Cacao locally
1. Install Rust. Stable should be fine. 1. Install Rust. Stable should be fine.
2. Clone this repository and open it in your favorite editor. 2. Clone this repository and open it in your favorite editor.
@ -57,8 +57,7 @@ In a few cases, though, it's fine to deviate - a good example is branching match
To run rustfmt tests locally: To run rustfmt tests locally:
1. Use rustup to set rust toolchain to the version specified in the 1. Use rustup to set Rust toolchain to the latest stable version of Rust.
[rust-toolchain file](./rust-toolchain).
2. Install the rustfmt and clippy by running 2. Install the rustfmt and clippy by running
``` ```
@ -66,18 +65,18 @@ To run rustfmt tests locally:
rustup component add clippy-preview rustup component add clippy-preview
``` ```
3. Run clippy using cargo from the root of your alchemy repo. 3. Run clippy nightly using cargo from the root of your Cacao repo.
``` ```
cargo clippy cargo +nightly clippy
``` ```
Each PR needs to compile without warning. Each PR needs to compile without warning.
4. Run rustfmt using cargo from the root of your alchemy repo. 4. Run rustfmt nightly using cargo from the root of your Cacao repo.
To see changes that need to be made, run To see changes that need to be made, run
``` ```
cargo fmt --all -- --check cargo +nightly fmt --all -- --check
``` ```
If all code is properly formatted (e.g. if you have not made any changes), If all code is properly formatted (e.g. if you have not made any changes),
@ -89,15 +88,15 @@ To run rustfmt tests locally:
Once you are ready to apply the formatting changes, run Once you are ready to apply the formatting changes, run
``` ```
cargo fmt --all cargo +nightly fmt --all
``` ```
You won't see any output, but all your files will be corrected. You won't see any output, but all your files will be corrected.
You can also use rustfmt to make corrections or highlight issues in your editor. You can also use rustfmt to make corrections or highlight issues in your editor.
Check out [their README](https://github.com/rust-lang-nursery/rustfmt) for details. Check out [their README](https://github.com/rust-lang/rustfmt) for details.
### Notes ### Notes
This project prefers verbose naming, to a certain degree - UI code is read more often than written, so it's This project prefers verbose naming, to a certain degree - UI code is read more often than written, so it's
worthwhile to ensure that it scans well. It also maps well to existing Cocoa/cacao idioms and is generally preferred. worthwhile to ensure that it scans well. It also maps well to existing Cocoa/Cacao idioms and is generally preferred.

View file

@ -19,11 +19,12 @@ default-target = "x86_64-apple-darwin"
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
bitmask-enum = "2.2.1"
block = "0.1.6" block = "0.1.6"
core-foundation = "0.9" core-foundation = "0.9"
core-graphics = "0.22" core-graphics = "0.23"
dispatch = "0.2.0" dispatch = "0.2.0"
infer = { version = "0.13", optional = true } infer = { version = "0.15", optional = true }
lazy_static = "1.4.0" lazy_static = "1.4.0"
libc = "0.2" libc = "0.2"
objc = "0.2.7" objc = "0.2.7"
@ -102,3 +103,6 @@ required-features = ["appkit"]
[[example]] [[example]]
name = "safe_area" name = "safe_area"
required-features = ["appkit"] required-features = ["appkit"]
[[example]]
name = "popover"
required-features = ["appkit"]

View file

@ -16,7 +16,7 @@ pub fn button(text: &str, msg: Msg) -> Button {
button.set_bordered(false); button.set_bordered(false);
button.set_bezel_style(BezelStyle::SmallSquare); button.set_bezel_style(BezelStyle::SmallSquare);
button.set_focus_ring_type(FocusRingType::None); button.set_focus_ring_type(FocusRingType::None);
button.set_action(move || dispatch(msg.clone())); button.set_action(move |_| dispatch(msg.clone()));
button.set_key_equivalent(&*text.to_lowercase()); button.set_key_equivalent(&*text.to_lowercase());
let font = Font::system(22.); let font = Font::system(22.);

View file

@ -1,5 +1,7 @@
use std::sync::RwLock; use std::sync::RwLock;
use cacao::input::{TextField, TextFieldDelegate};
use cacao::text::{Label, TextAlign};
use cacao::uikit::{App, AppDelegate, Scene, SceneConfig, SceneConnectionOptions, SceneSession, Window, WindowSceneDelegate}; use cacao::uikit::{App, AppDelegate, Scene, SceneConfig, SceneConnectionOptions, SceneSession, Window, WindowSceneDelegate};
use cacao::color::Color; use cacao::color::Color;
@ -15,22 +17,62 @@ impl AppDelegate for TestApp {
SceneConfig::new("Default Configuration", session.role()) SceneConfig::new("Default Configuration", session.role())
} }
} }
#[derive(Debug, Default)]
pub struct ConsoleLogger(String);
impl TextFieldDelegate for ConsoleLogger {
const NAME: &'static str = "ConsoleLogger";
fn text_should_begin_editing(&self, value: &str) -> bool {
println!("{} should begin editing: {}", self.0, value);
true
}
fn text_did_change(&self, value: &str) {
println!("{} text did change to {}", self.0, value);
}
fn text_did_end_editing(&self, value: &str) {
println!("{} did end editing: {}", self.0, value);
}
fn text_should_end_editing(&self, value: &str) -> bool {
println!("{} should end editing: {}", self.0, value);
true
}
}
#[derive(Default)]
pub struct RootView { pub struct RootView {
pub red: View,
pub green: View, pub green: View,
pub blue: View, pub blue: View,
pub image: ImageView pub label: Label,
pub image: ImageView,
pub input: TextField<ConsoleLogger>
}
impl Default for RootView {
fn default() -> Self {
RootView {
green: View::new(),
blue: View::new(),
label: Label::new(),
image: ImageView::new(),
input: TextField::with(ConsoleLogger("input_1".to_string()))
}
}
} }
impl ViewDelegate for RootView { impl ViewDelegate for RootView {
const NAME: &'static str = "RootView"; const NAME: &'static str = "RootView";
fn did_load(&mut self, view: View) { fn did_load(&mut self, view: View) {
self.red.set_background_color(Color::SystemRed); self.label.set_text("my label");
self.red.layer.set_corner_radius(16.); self.label.set_text_color(Color::SystemWhite);
view.add_subview(&self.red); self.label.set_background_color(Color::SystemRed);
self.label.layer.set_corner_radius(16.);
self.label.set_text_alignment(TextAlign::Center);
view.add_subview(&self.label);
self.green.set_background_color(Color::SystemGreen); self.green.set_background_color(Color::SystemGreen);
view.add_subview(&self.green); view.add_subview(&self.green);
@ -43,19 +85,26 @@ impl ViewDelegate for RootView {
self.image.set_image(&Image::with_data(image_bytes)); self.image.set_image(&Image::with_data(image_bytes));
view.add_subview(&self.image); view.add_subview(&self.image);
self.input.set_text("my input box 1");
view.add_subview(&self.input);
LayoutConstraint::activate(&[ LayoutConstraint::activate(&[
self.red.top.constraint_equal_to(&view.top).offset(16.), self.label.leading.constraint_equal_to(&view.leading).offset(16.),
self.red.leading.constraint_equal_to(&view.leading).offset(16.), self.label.top.constraint_equal_to(&view.top).offset(16.),
self.red.trailing.constraint_equal_to(&view.trailing).offset(-16.), self.label.height.constraint_equal_to_constant(100.),
self.red.height.constraint_equal_to_constant(100.), self.label.trailing.constraint_equal_to(&view.trailing).offset(-16.),
self.green.top.constraint_equal_to(&self.red.bottom).offset(16.), self.green.top.constraint_equal_to(&self.label.bottom).offset(16.),
self.green.leading.constraint_equal_to(&view.leading).offset(16.), self.green.leading.constraint_equal_to(&view.leading).offset(16.),
self.green.trailing.constraint_equal_to(&view.trailing).offset(-16.), self.green.trailing.constraint_equal_to(&view.trailing).offset(-16.),
self.green.height.constraint_equal_to_constant(120.), self.green.height.constraint_equal_to_constant(120.),
self.input.center_x.constraint_equal_to(&self.green.center_x),
self.input.center_y.constraint_equal_to(&self.green.center_y),
self.blue.top.constraint_equal_to(&self.green.bottom).offset(16.), self.blue.top.constraint_equal_to(&self.green.bottom).offset(16.),
self.blue.leading.constraint_equal_to(&view.leading).offset(16.), self.blue.leading.constraint_equal_to(&view.leading).offset(16.),
self.blue.trailing.constraint_equal_to(&view.trailing).offset(-16.), self.blue.trailing.constraint_equal_to(&view.trailing).offset(-16.),
self.blue.bottom.constraint_equal_to(&view.bottom).offset(-16.) self.blue.bottom.constraint_equal_to(&view.bottom).offset(-16.),
self.image.center_x.constraint_equal_to(&self.blue.center_x),
self.image.center_y.constraint_equal_to(&self.blue.center_y)
]); ]);
} }
} }

View file

@ -5,13 +5,15 @@
//! - Another Controller / View //! - Another Controller / View
use cacao::appkit::menu::{Menu, MenuItem}; use cacao::appkit::menu::{Menu, MenuItem};
use cacao::appkit::segmentedcontrol::SegmentedControl;
use cacao::appkit::window::{Window, WindowConfig, WindowController, WindowDelegate}; use cacao::appkit::window::{Window, WindowConfig, WindowController, WindowDelegate};
use cacao::appkit::{App, AppDelegate}; use cacao::appkit::{App, AppDelegate};
use cacao::button::Button; use cacao::button::Button;
use cacao::foundation::NSArray;
use cacao::geometry::{Edge, Rect}; use cacao::geometry::{Edge, Rect};
use cacao::image::Image;
use cacao::layout::{Layout, LayoutConstraint}; use cacao::layout::{Layout, LayoutConstraint};
use cacao::notification_center::Dispatcher; use cacao::notification_center::Dispatcher;
use cacao::text::{Font, Label};
use cacao::view::{Popover, PopoverConfig, View, ViewController, ViewDelegate}; use cacao::view::{Popover, PopoverConfig, View, ViewController, ViewDelegate};
struct BasicApp { struct BasicApp {
@ -124,7 +126,7 @@ impl ViewDelegate for PopoverExampleContentView {
fn did_load(&mut self, view: cacao::view::View) { fn did_load(&mut self, view: cacao::view::View) {
let mut button = Button::new("Show"); let mut button = Button::new("Show");
button.set_action(|| dispatch_ui(Msg::Click)); button.set_action(|_| dispatch_ui(Msg::Click));
let controller = PopoverExampleContentViewController::new(); let controller = PopoverExampleContentViewController::new();
let config = PopoverConfig { let config = PopoverConfig {
@ -164,16 +166,21 @@ impl Dispatcher for BasicApp {
#[derive(Debug)] #[derive(Debug)]
struct PopoverExampleContentViewController { struct PopoverExampleContentViewController {
pub label: Label pub control: SegmentedControl
} }
impl PopoverExampleContentViewController { impl PopoverExampleContentViewController {
fn new() -> Self { fn new() -> Self {
let label = Label::new(); let images = NSArray::from(vec![
let font = Font::system(20.); &*Image::symbol(cacao::image::SFSymbol::AtSymbol, "Hello").0,
label.set_font(&font); &*Image::symbol(cacao::image::SFSymbol::PaperPlane, "Hello").0,
label.set_text("Hello"); &*Image::symbol(cacao::image::SFSymbol::PaperPlaneFilled, "Hello").0,
Self { label } ]);
let mut control = SegmentedControl::new(images, cacao::appkit::segmentedcontrol::TrackingMode::SelectOne);
control.set_action(|index| {
println!("Selected Index {index}");
});
Self { control }
} }
} }
@ -181,6 +188,6 @@ impl ViewDelegate for PopoverExampleContentViewController {
const NAME: &'static str = "PopoverExampleContentViewController"; const NAME: &'static str = "PopoverExampleContentViewController";
fn did_load(&mut self, view: View) { fn did_load(&mut self, view: View) {
view.add_subview(&self.label); view.add_subview(&self.control);
} }
} }

View file

@ -50,7 +50,7 @@ impl ViewDelegate for AddNewTodoContentView {
let mut button = Button::new("Add"); let mut button = Button::new("Add");
button.set_key_equivalent("\r"); button.set_key_equivalent("\r");
button.set_action(|| dispatch_ui(Message::ProcessNewTodo)); button.set_action(|_| dispatch_ui(Message::ProcessNewTodo));
view.add_subview(&instructions); view.add_subview(&instructions);
view.add_subview(&input); view.add_subview(&input);

View file

@ -2,6 +2,7 @@ use cacao::layout::{Layout, LayoutConstraint};
use cacao::switch::Switch; use cacao::switch::Switch;
use cacao::text::Label; use cacao::text::Label;
use cacao::view::View; use cacao::view::View;
use objc::runtime::Object;
/// A reusable widget for a toggle; this is effectively a standard checkbox/label combination for /// A reusable widget for a toggle; this is effectively a standard checkbox/label combination for
/// toggling a boolean value. /// toggling a boolean value.
@ -55,7 +56,7 @@ impl ToggleOptionView {
/// can toggle your settings and such there. /// can toggle your settings and such there.
pub fn configure<F>(&mut self, text: &str, subtitle: &str, state: bool, handler: F) pub fn configure<F>(&mut self, text: &str, subtitle: &str, state: bool, handler: F)
where where
F: Fn() + Send + Sync + 'static F: Fn(*const Object) + Send + Sync + 'static
{ {
self.title.set_text(text); self.title.set_text(text);
self.subtitle.set_text(subtitle); self.subtitle.set_text(subtitle);

View file

@ -19,7 +19,7 @@ impl Default for PreferencesToolbar {
let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "General"); let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "General");
item.set_image(icon); item.set_image(icon);
item.set_action(|| { item.set_action(|_| {
dispatch_ui(Message::SwitchPreferencesToGeneralPane); dispatch_ui(Message::SwitchPreferencesToGeneralPane);
}); });
@ -32,7 +32,7 @@ impl Default for PreferencesToolbar {
let icon = Image::toolbar_icon(MacSystemIcon::PreferencesAdvanced, "Advanced"); let icon = Image::toolbar_icon(MacSystemIcon::PreferencesAdvanced, "Advanced");
item.set_image(icon); item.set_image(icon);
item.set_action(|| { item.set_action(|_| {
dispatch_ui(Message::SwitchPreferencesToAdvancedPane); dispatch_ui(Message::SwitchPreferencesToAdvancedPane);
}); });

View file

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use cacao::defaults::{UserDefaults, Value}; use cacao::defaults::{UserDefaults, Value};
use objc::runtime::Object;
const EXAMPLE: &str = "exampleSetting"; const EXAMPLE: &str = "exampleSetting";
@ -25,7 +26,7 @@ impl Defaults {
} }
/// Toggles the example setting. /// Toggles the example setting.
pub fn toggle_should_whatever() { pub fn toggle_should_whatever(_object: *const Object) {
toggle_bool(EXAMPLE); toggle_bool(EXAMPLE);
} }

View file

@ -15,7 +15,7 @@ impl Default for TodosToolbar {
item.set_title("Add Todo"); item.set_title("Add Todo");
item.set_button(Button::new("+ New")); item.set_button(Button::new("+ New"));
item.set_action(|| { item.set_action(|_| {
dispatch_ui(Message::OpenNewTodoSheet); dispatch_ui(Message::OpenNewTodoSheet);
}); });

View file

@ -187,13 +187,12 @@ extern "C" fn accepted_cloudkit_share<T: AppDelegate>(this: &Object, _: Sel, _:
/// Fires when the application receives an `application:openURLs` message. /// Fires when the application receives an `application:openURLs` message.
extern "C" fn open_urls<T: AppDelegate>(this: &Object, _: Sel, _: id, file_urls: id) { extern "C" fn open_urls<T: AppDelegate>(this: &Object, _: Sel, _: id, file_urls: id) {
let urls = NSArray::retain(file_urls) let urls = NSArray::retain(file_urls)
.map(|url| { .iter()
.filter_map(|url| {
let uri = NSString::retain(unsafe { msg_send![url, absoluteString] }); let uri = NSString::retain(unsafe { msg_send![url, absoluteString] });
Url::parse(uri.to_str()) Url::parse(uri.to_str()).ok()
}) })
.into_iter()
.filter_map(|url| url.ok())
.collect(); .collect();
app::<T>(this).open_urls(urls); app::<T>(this).open_urls(urls);
@ -263,7 +262,10 @@ extern "C" fn print_files<T: AppDelegate>(
settings: id, settings: id,
show_print_panels: BOOL show_print_panels: BOOL
) -> NSUInteger { ) -> NSUInteger {
let files = NSArray::retain(files).map(|file| NSString::retain(file).to_str().to_string()); let files = NSArray::retain(files)
.iter()
.map(|file| NSString::retain(file).to_str().to_string())
.collect();
let settings = PrintSettings::with_inner(settings); let settings = PrintSettings::with_inner(settings);

View file

@ -1,15 +1,54 @@
use bitmask_enum::bitmask;
use block::ConcreteBlock; use block::ConcreteBlock;
use objc::runtime::Object; use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id; use objc_id::Id;
use crate::foundation::{id, nil, NSString}; use crate::events::EventType;
use crate::foundation::{id, nil, NSInteger, NSPoint, NSString};
/// An EventMask describes the type of event. /// An EventMask describes the type of event.
#[derive(Debug)] #[bitmask(u64)]
pub enum EventMask { pub enum EventMask {
KeyDown LeftMouseDown = 1 << 1,
LeftMouseUp = 1 << 2,
RightMouseDown = 1 << 3,
RightMouseUp = 1 << 4,
MouseMoved = 1 << 5,
LeftMouseDragged = 1 << 6,
RightMouseDragged = 1 << 7,
MouseEntered = 1 << 8,
MouseExited = 1 << 9,
KeyDown = 1 << 10,
KeyUp = 1 << 11,
FlagsChanged = 1 << 12,
AppKitDefined = 1 << 13,
SystemDefined = 1 << 14,
ApplicationDefined = 1 << 15,
Periodic = 1 << 16,
CursorUpdate = 1 << 17,
ScrollWheel = 1 << 22,
TabletPoint = 1 << 23,
TabletProximity = 1 << 24,
OtherMouseDown = 1 << 25,
OtherMouseUp = 1 << 26,
OtherMouseDragged = 1 << 27,
Gesture = 1 << 29,
Magnify = 1 << 30,
Swipe = 1 << 31,
Rotate = 1 << 18,
BeginGesture = 1 << 19,
EndGesture = 1 << 20,
SmartMagnify = 1 << 32,
QuickLook = 1 << 33,
Pressure = 1 << 34,
DirectTouch = 1 << 37,
ChangeMode = 1 << 38
} }
/// A wrapper over an `NSEvent`. /// A wrapper over an `NSEvent`.
@ -25,6 +64,16 @@ impl Event {
Event(unsafe { Id::from_ptr(objc) }) Event(unsafe { Id::from_ptr(objc) })
} }
/// The event's type.
///
/// Corresponds to the `type` getter.
pub fn kind(&self) -> EventType {
let kind: NSUInteger = unsafe { msg_send![&*self.0, type] };
unsafe { ::std::mem::transmute(kind) }
}
/// The characters associated with a key-up or key-down event.
pub fn characters(&self) -> String { pub fn characters(&self) -> String {
// @TODO: Check here if key event, invalid otherwise. // @TODO: Check here if key event, invalid otherwise.
// @TODO: Figure out if we can just return &str here, since the Objective-C side // @TODO: Figure out if we can just return &str here, since the Objective-C side
@ -34,6 +83,26 @@ impl Event {
characters.to_string() characters.to_string()
} }
/// The indices of the currently pressed mouse buttons.
pub fn pressed_mouse_buttons() -> NSUInteger {
unsafe { msg_send![class!(NSEvent), pressedMouseButtons] }
}
/// Reports the current mouse position in screen coordinates.
pub fn mouse_location() -> NSPoint {
unsafe { msg_send![class!(NSEvent), mouseLocation] }
}
/// The button number for a mouse event.
pub fn button_number(&self) -> NSInteger {
unsafe { msg_send![&*self.0, buttonNumber] }
}
/// The number of mouse clicks associated with a mouse-down or mouse-up event.
pub fn click_count(&self) -> NSInteger {
unsafe { msg_send![&*self.0, clickCount] }
}
/*pub fn contains_modifier_flags(&self, flags: &[EventModifierFlag]) -> bool { /*pub fn contains_modifier_flags(&self, flags: &[EventModifierFlag]) -> bool {
let modifier_flags: NSUInteger = unsafe { let modifier_flags: NSUInteger = unsafe {
msg_send![&*self.0, modifierFlags] msg_send![&*self.0, modifierFlags]
@ -47,13 +116,13 @@ impl Event {
false false
}*/ }*/
/// Register an event handler with the system event stream. This method /// Register an event handler with the local system event stream. This method
/// watches for events that occur _within the application_. Events outside /// watches for events that occur _within the application_. Events outside
/// of the application require installing a `monitor_global_events` handler. /// of the application require installing a `global_monitor` handler.
/// ///
/// Note that in order to monitor all possible events, both local and global /// Note that in order to monitor all possible events, both local and global
/// monitors are required - the streams don't mix. /// monitors are required - the streams don't mix.
pub fn local_monitor<F>(_mask: EventMask, handler: F) -> EventMonitor pub fn local_monitor<F>(mask: EventMask, handler: F) -> EventMonitor
where where
F: Fn(Event) -> Option<Event> + Send + Sync + 'static F: Fn(Event) -> Option<Event> + Send + Sync + 'static
{ {
@ -68,7 +137,33 @@ impl Event {
let block = block.copy(); let block = block.copy();
EventMonitor(unsafe { EventMonitor(unsafe {
msg_send![class!(NSEvent), addLocalMonitorForEventsMatchingMask:1024 msg_send![class!(NSEvent), addLocalMonitorForEventsMatchingMask:mask.bits
handler:block]
})
}
/// Register an event handler with the global system event stream. This method
/// watches for events that occur _outside the application_. Events within
/// the application require installing a `local_monitor` handler.
///
/// Note that in order to monitor all possible events, both local and global
/// monitors are required - the streams don't mix.
pub fn global_monitor<F>(mask: EventMask, handler: F) -> EventMonitor
where
F: Fn(Event) -> Option<Event> + Send + Sync + 'static
{
let block = ConcreteBlock::new(move |event: id| {
let evt = Event::new(event);
match handler(evt) {
Some(mut evt) => &mut *evt.0,
None => nil
}
});
let block = block.copy();
EventMonitor(unsafe {
msg_send![class!(NSEvent), addGlobalMonitorForEventsMatchingMask:mask.bits
handler:block] handler:block]
}) })
} }

79
src/appkit/haptics.rs Normal file
View file

@ -0,0 +1,79 @@
use std::convert::TryFrom;
use objc::{class, msg_send, runtime::Object, sel, sel_impl};
use objc_id::ShareId;
use crate::foundation::NSUInteger;
#[derive(Clone, Debug)]
pub struct HapticFeedbackPerformer(pub ShareId<Object>);
impl HapticFeedbackPerformer {
pub fn perform(&self, pattern: FeedbackPattern, performance_time: PerformanceTime) {
unsafe {
let _: () = msg_send![&*self.0, performFeedbackPattern: pattern performanceTime: performance_time];
}
}
}
impl Default for HapticFeedbackPerformer {
/// Returns the default haptic feedback performer.
fn default() -> Self {
HapticFeedbackPerformer(unsafe {
let manager = msg_send![class!(NSHapticFeedbackManager), defaultPerformer];
ShareId::from_ptr(manager)
})
}
}
#[derive(Clone, Copy, Debug)]
pub enum PerformanceTime {
Default = 0,
Now = 1,
DrawCompleted = 2
}
impl Default for PerformanceTime {
fn default() -> Self {
Self::Default
}
}
impl TryFrom<f64> for PerformanceTime {
type Error = &'static str;
fn try_from(value: f64) -> Result<Self, Self::Error> {
match value as u8 {
0 => Ok(Self::Default),
1 => Ok(Self::Now),
2 => Ok(Self::DrawCompleted),
_ => Err("Invalid performance time")
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum FeedbackPattern {
Generic = 0,
Alignment = 1,
LevelChange = 2
}
impl Default for FeedbackPattern {
fn default() -> Self {
Self::Generic
}
}
impl TryFrom<f64> for FeedbackPattern {
type Error = &'static str;
fn try_from(value: f64) -> Result<Self, Self::Error> {
match value as u8 {
0 => Ok(Self::Generic),
1 => Ok(Self::Alignment),
2 => Ok(Self::LevelChange),
_ => Err("Invalid feedback pattern")
}
}
}

View file

@ -27,3 +27,6 @@ pub mod menu;
pub mod printing; pub mod printing;
pub mod toolbar; pub mod toolbar;
pub mod window; pub mod window;
pub mod haptics;
pub mod segmentedcontrol;

View file

@ -0,0 +1,346 @@
//! Wraps `NSSegmentedControl` on appkit
use std::fmt;
use std::sync::Once;
use std::cell::RefCell;
use std::rc::Rc;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{class, msg_send, sel, sel_impl};
use objc_id::ShareId;
use crate::color::Color;
use crate::control::Control;
use crate::foundation::{id, nil, NSArray, NSString, NSUInteger, BOOL, NO, YES};
use crate::image::Image;
use crate::invoker::TargetActionHandler;
use crate::keys::Key;
use crate::layout::Layout;
use crate::objc_access::ObjcAccess;
use crate::text::{AttributedString, Font};
use crate::utils::{load, properties::ObjcProperty};
#[cfg(feature = "autolayout")]
use crate::layout::{LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY};
#[cfg(feature = "appkit")]
use crate::appkit::FocusRingType;
/// Wraps `NSButton` on appkit, and `UIButton` on iOS and tvOS.
///
/// You'd use this type to create a button that a user can interact with. Buttons can be configured
/// a number of ways, and support setting a callback to fire when they're clicked or tapped.
///
/// Some properties are platform-specific; see the documentation for further information.
///
/// ```rust,no_run
/// use cacao::button::Button;
/// use cacao::view::View;
/// use crate::cacao::layout::Layout;
/// let mut button = Button::new("My button title");
/// button.set_key_equivalent("c");
///
/// button.set_action(|_| {
/// println!("My button was clicked.");
/// });
/// let my_view : View<()> = todo!();
///
/// // Make sure you don't let your Button drop for as long as you need it.
/// my_view.add_subview(&button);
/// ```
#[derive(Debug)]
pub struct SegmentedControl {
/// A handle for the underlying Objective-C object.
pub objc: ObjcProperty,
/// Hold on to the images
images: NSArray,
handler: Option<TargetActionHandler>,
/// A pointer to the Objective-C runtime top layout constraint.
#[cfg(feature = "autolayout")]
pub top: LayoutAnchorY,
/// A pointer to the Objective-C runtime leading layout constraint.
#[cfg(feature = "autolayout")]
pub leading: LayoutAnchorX,
/// A pointer to the Objective-C runtime left layout constraint.
#[cfg(feature = "autolayout")]
pub left: LayoutAnchorX,
/// A pointer to the Objective-C runtime trailing layout constraint.
#[cfg(feature = "autolayout")]
pub trailing: LayoutAnchorX,
/// A pointer to the Objective-C runtime right layout constraint.
#[cfg(feature = "autolayout")]
pub right: LayoutAnchorX,
/// A pointer to the Objective-C runtime bottom layout constraint.
#[cfg(feature = "autolayout")]
pub bottom: LayoutAnchorY,
/// A pointer to the Objective-C runtime width layout constraint.
#[cfg(feature = "autolayout")]
pub width: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime height layout constraint.
#[cfg(feature = "autolayout")]
pub height: LayoutAnchorDimension,
/// A pointer to the Objective-C runtime center X layout constraint.
#[cfg(feature = "autolayout")]
pub center_x: LayoutAnchorX,
/// A pointer to the Objective-C runtime center Y layout constraint.
#[cfg(feature = "autolayout")]
pub center_y: LayoutAnchorY
}
#[derive(Debug)]
#[repr(u8)]
pub enum TrackingMode {
SelectOne = 0,
SelectMany = 1,
SelectMomentary = 2
}
impl SegmentedControl {
/// Creates a new `NSSegmentedControl` instance, configures it appropriately,
/// and retains the necessary Objective-C runtime pointer.
pub fn new(images: NSArray, tracking_mode: TrackingMode) -> Self {
let view: id = unsafe {
let tracking_mode = tracking_mode as u8 as i32;
let control: id = msg_send![register_class(), segmentedControlWithImages:&*images trackingMode:tracking_mode
target:nil
action:nil
];
let _: () = msg_send![control, setWantsLayer: YES];
#[cfg(feature = "autolayout")]
let _: () = msg_send![control, setTranslatesAutoresizingMaskIntoConstraints: NO];
control
};
SegmentedControl {
handler: None,
images,
#[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view),
#[cfg(feature = "autolayout")]
left: LayoutAnchorX::left(view),
#[cfg(feature = "autolayout")]
leading: LayoutAnchorX::leading(view),
#[cfg(feature = "autolayout")]
right: LayoutAnchorX::right(view),
#[cfg(feature = "autolayout")]
trailing: LayoutAnchorX::trailing(view),
#[cfg(feature = "autolayout")]
bottom: LayoutAnchorY::bottom(view),
#[cfg(feature = "autolayout")]
width: LayoutAnchorDimension::width(view),
#[cfg(feature = "autolayout")]
height: LayoutAnchorDimension::height(view),
#[cfg(feature = "autolayout")]
center_x: LayoutAnchorX::center(view),
#[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(view),
objc: ObjcProperty::retain(view)
}
}
/// Select the segment at index
pub fn set_tooltip_segment(&mut self, index: NSUInteger, tooltip: &str) {
self.objc.with_mut(|obj| unsafe {
let converted = NSString::new(tooltip);
let _: () = msg_send![obj, setToolTip: converted forSegment: index];
})
}
/// Select the segment at index
pub fn select_segment(&mut self, index: NSUInteger) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setSelectedSegment: index];
})
}
/// Sets an image on the underlying button.
pub fn set_image_segment(&mut self, image: Image, segment: NSUInteger) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setImage:&*image.0 forSegment: segment];
});
}
/// Attaches a callback for button press events. Don't get too creative now...
/// best just to message pass or something.
pub fn set_action<F: Fn(i32) + Send + Sync + 'static>(&mut self, action: F) {
// @TODO: This probably isn't ideal but gets the job done for now; needs revisiting.
let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) });
let handler = TargetActionHandler::new(&*this, move |obj: *const Object| unsafe {
let selected: i32 = msg_send![obj, selectedSegment];
action(selected)
});
self.handler = Some(handler);
}
/// Call this to set the background color for the backing layer.
pub fn set_background_color<C: AsRef<Color>>(&self, color: C) {
let color: id = color.as_ref().into();
#[cfg(feature = "appkit")]
self.objc.with_mut(|obj| unsafe {
let cell: id = msg_send![obj, cell];
let _: () = msg_send![cell, setBackgroundColor: color];
});
}
/// Set a key to be bound to this button. When the key is pressed, the action coupled to this
/// button will fire.
pub fn set_key_equivalent<'a, K>(&self, key: K)
where
K: Into<Key<'a>>
{
let key: Key<'a> = key.into();
self.objc.with_mut(|obj| {
let keychar = match key {
Key::Char(s) => NSString::new(s),
Key::Delete => NSString::new("\u{08}")
};
unsafe {
let _: () = msg_send![obj, setKeyEquivalent:&*keychar];
}
});
}
/// Sets the text color for this button.
///
/// On appkit, this is done by way of an `AttributedString` under the hood.
pub fn set_text_color<C: AsRef<Color>>(&self, color: C) {
#[cfg(feature = "appkit")]
self.objc.with_mut(move |obj| unsafe {
let text: id = msg_send![obj, attributedTitle];
let len: isize = msg_send![text, length];
let mut attr_str = AttributedString::wrap(text);
attr_str.set_text_color(color.as_ref(), 0..len);
let _: () = msg_send![obj, setAttributedTitle:&*attr_str];
});
}
// @TODO: Figure out how to handle oddities like this.
/// For buttons on appkit, one might need to disable the border. This does that.
#[cfg(feature = "appkit")]
pub fn set_bordered(&self, is_bordered: bool) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setBordered:match is_bordered {
true => YES,
false => NO
}];
});
}
/// Sets the font for this button.
pub fn set_font<F: AsRef<Font>>(&self, font: F) {
let font = font.as_ref().clone();
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setFont:&*font];
});
}
/// Sets how the control should draw a focus ring when a user is focused on it.
///
/// This is an appkit-only method.
#[cfg(feature = "appkit")]
pub fn set_focus_ring_type(&self, focus_ring_type: FocusRingType) {
let ring_type: NSUInteger = focus_ring_type.into();
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setFocusRingType: ring_type];
});
}
/// Toggles the highlighted status of the button.
pub fn set_highlighted(&self, highlight: bool) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, highlight:match highlight {
true => YES,
false => NO
}];
});
}
}
impl ObjcAccess for SegmentedControl {
fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler);
}
fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler)
}
}
impl Layout for SegmentedControl {}
impl Control for SegmentedControl {}
impl ObjcAccess for &SegmentedControl {
fn with_backing_obj_mut<F: Fn(id)>(&self, handler: F) {
self.objc.with_mut(handler);
}
fn get_from_backing_obj<F: Fn(&Object) -> R, R>(&self, handler: F) -> R {
self.objc.get(handler)
}
}
impl Layout for &SegmentedControl {}
impl Control for &SegmentedControl {}
impl Drop for SegmentedControl {
/// Nils out references on the Objective-C side and removes this from the backing view.
// Just to be sure, let's... nil these out. They should be weak references,
// but I'd rather be paranoid and remove them later.
fn drop(&mut self) {
self.objc.with_mut(|obj| unsafe {
let _: () = msg_send![obj, setTarget: nil];
let _: () = msg_send![obj, setAction: nil];
});
}
}
/// Registers an `NSButton` 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::new();
INIT.call_once(|| unsafe {
let superclass = class!(NSSegmentedControl);
let decl = ClassDecl::new("RSTSegmentedControl", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -10,6 +10,7 @@ use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc_id::{Id, ShareId}; use objc_id::{Id, ShareId};
use crate::appkit::segmentedcontrol::SegmentedControl;
use crate::button::{BezelStyle, Button}; use crate::button::{BezelStyle, Button};
use crate::foundation::{id, NSString, NO, YES}; use crate::foundation::{id, NSString, NO, YES};
use crate::image::Image; use crate::image::Image;
@ -21,6 +22,7 @@ pub struct ToolbarItem {
pub identifier: String, pub identifier: String,
pub objc: Id<Object>, pub objc: Id<Object>,
pub button: Option<Button>, pub button: Option<Button>,
pub segmented_control: Option<SegmentedControl>,
pub image: Option<Image>, pub image: Option<Image>,
handler: Option<TargetActionHandler> handler: Option<TargetActionHandler>
} }
@ -42,6 +44,7 @@ impl ToolbarItem {
identifier, identifier,
objc, objc,
button: None, button: None,
segmented_control: None,
image: None, image: None,
handler: None handler: None
} }
@ -52,6 +55,7 @@ impl ToolbarItem {
identifier: "".to_string(), identifier: "".to_string(),
objc: unsafe { Id::from_retained_ptr(item) }, objc: unsafe { Id::from_retained_ptr(item) },
button: None, button: None,
segmented_control: None,
image: None, image: None,
handler: None handler: None
} }
@ -76,6 +80,15 @@ impl ToolbarItem {
self.button = Some(button); self.button = Some(button);
} }
/// Sets and takes ownership of the segmented control for this item.
pub fn set_segmented_control(&mut self, control: SegmentedControl) {
control.objc.with_mut(|obj| unsafe {
let _: () = msg_send![&*self.objc, setView: obj];
});
self.segmented_control = Some(control);
}
/// Sets and takes ownership of the image for this toolbar item. /// Sets and takes ownership of the image for this toolbar item.
pub fn set_image(&mut self, image: Image) { pub fn set_image(&mut self, image: Image) {
unsafe { unsafe {
@ -102,7 +115,7 @@ impl ToolbarItem {
} }
/// Sets an action on this item. /// Sets an action on this item.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) { pub fn set_action<F: Fn(*const Object) + Send + Sync + 'static>(&mut self, action: F) {
let handler = TargetActionHandler::new(&*self.objc, action); let handler = TargetActionHandler::new(&*self.objc, action);
self.handler = Some(handler); self.handler = Some(handler);
} }

View file

@ -295,6 +295,14 @@ impl<T> Window<T> {
} }
} }
/// Used for setting a toolbar on this window.
pub fn toolbar(&self) -> ShareId<Object> {
unsafe {
let o: *mut Object = msg_send![&*self.objc, toolbar];
ShareId::from_ptr(o)
}
}
/// Toggles whether the toolbar is shown for this window. Has no effect if no toolbar exists on /// Toggles whether the toolbar is shown for this window. Has no effect if no toolbar exists on
/// this window. /// this window.
pub fn toggle_toolbar_shown(&self) { pub fn toggle_toolbar_shown(&self) {
@ -303,6 +311,14 @@ impl<T> Window<T> {
} }
} }
/// Set the toolbar style
pub fn set_toolbar_style(&self, style: WindowToolbarStyle) {
let style: NSUInteger = style.into();
unsafe {
let _: () = msg_send![&*self.objc, setToolbarStyle: style];
}
}
/// Set whether the toolbar toggle button is shown. Has no effect if no toolbar exists on this /// Set whether the toolbar toggle button is shown. Has no effect if no toolbar exists on this
/// window. /// window.
pub fn set_shows_toolbar_button(&self, shows: bool) { pub fn set_shows_toolbar_button(&self, shows: bool) {

View file

@ -12,7 +12,7 @@
//! let mut button = Button::new("My button title"); //! let mut button = Button::new("My button title");
//! button.set_key_equivalent("c"); //! button.set_key_equivalent("c");
//! //!
//! button.set_action(|| { //! button.set_action(|_| {
//! println!("My button was clicked."); //! println!("My button was clicked.");
//! }); //! });
//! let my_view : View<()> = todo!(); //! let my_view : View<()> = todo!();
@ -58,7 +58,7 @@ mod enums;
/// let mut button = Button::new("My button title"); /// let mut button = Button::new("My button title");
/// button.set_key_equivalent("c"); /// button.set_key_equivalent("c");
/// ///
/// button.set_action(|| { /// button.set_action(|_| {
/// println!("My button was clicked."); /// println!("My button was clicked.");
/// }); /// });
/// let my_view : View<()> = todo!(); /// let my_view : View<()> = todo!();
@ -212,7 +212,7 @@ impl Button {
/// Attaches a callback for button press events. Don't get too creative now... /// Attaches a callback for button press events. Don't get too creative now...
/// best just to message pass or something. /// best just to message pass or something.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) { pub fn set_action<F: Fn(*const Object) + Send + Sync + 'static>(&mut self, action: F) {
// @TODO: This probably isn't ideal but gets the job done for now; needs revisiting. // @TODO: This probably isn't ideal but gets the job done for now; needs revisiting.
let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) }); let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) });
let handler = TargetActionHandler::new(&*this, action); let handler = TargetActionHandler::new(&*this, action);
@ -353,5 +353,14 @@ impl Drop for Button {
/// Registers an `NSButton` subclass, and configures it to hold some ivars /// Registers an `NSButton` subclass, and configures it to hold some ivars
/// for various things we need to store. /// for various things we need to store.
fn register_class() -> *const Class { fn register_class() -> *const Class {
load_or_register_class("NSButton", "RSTButton", |decl| unsafe {}) #[cfg(feature = "appkit")]
let super_class = "NSButton";
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let super_class = "UIButton";
load_or_register_class(super_class, "RSTButton", |decl| unsafe {})
}
#[test]
fn test_button() {
let button = Button::new("foobar");
} }

View file

@ -48,7 +48,45 @@ impl From<&EventModifierFlag> for NSUInteger {
/// Represents an event type that you can request to be notified about. /// Represents an event type that you can request to be notified about.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[cfg_attr(target_pointer_width = "32", repr(u32))]
#[cfg_attr(target_pointer_width = "64", repr(u64))]
pub enum EventType { pub enum EventType {
/// A keydown event. LeftMouseDown = 1,
KeyDown LeftMouseUp = 2,
RightMouseDown = 3,
RightMouseUp = 4,
MouseMoved = 5,
LeftMouseDragged = 6,
RightMouseDragged = 7,
MouseEntered = 8,
MouseExited = 9,
KeyDown = 10,
KeyUp = 11,
FlagsChanged = 12,
AppKitDefined = 13,
SystemDefined = 14,
ApplicationDefined = 15,
Periodic = 16,
CursorUpdate = 17,
ScrollWheel = 22,
TabletPoint = 23,
TabletProximity = 24,
OtherMouseDown = 25,
OtherMouseUp = 26,
OtherMouseDragged = 27,
Gesture = 29,
Magnify = 30,
Swipe = 31,
Rotate = 18,
BeginGesture = 19,
EndGesture = 20,
SmartMagnify = 32,
QuickLook = 33,
Pressure = 34,
DirectTouch = 37,
ChangeMode = 38
} }

View file

@ -41,21 +41,41 @@ impl NSArray {
unsafe { msg_send![&*self.0, count] } unsafe { msg_send![&*self.0, count] }
} }
/// A helper method for mapping over the backing `NSArray` items and producing a Rust `Vec<T>`. /// Returns an iterator over the `NSArray`
/// Often times we need to map in this framework to convert between Rust types, so isolating pub fn iter<'a>(&'a self) -> NSArrayIterator<'a> {
/// this out makes life much easier. NSArrayIterator {
pub fn map<T, F: Fn(id) -> T>(&self, transform: F) -> Vec<T> { next_index: 0,
let count = self.count(); count: self.count(),
let objc = &*self.0; array: self
}
}
}
#[derive(Debug)]
pub struct NSArrayIterator<'a> {
next_index: usize,
count: usize,
array: &'a NSArray
}
impl Iterator for NSArrayIterator<'_> {
type Item = id;
fn next(&mut self) -> Option<Self::Item> {
// I don't know if it's worth trying to get in with NSFastEnumeration here. I'm content to // I don't know if it's worth trying to get in with NSFastEnumeration here. I'm content to
// just rely on Rust, but someone is free to profile it if they want. // just rely on Rust, but someone is free to profile it if they want.
(0..count) if self.next_index < self.count {
.map(|index| { let objc = &*self.array.0;
let index = self.next_index;
let item: id = unsafe { msg_send![objc, objectAtIndex: index] }; let item: id = unsafe { msg_send![objc, objectAtIndex: index] };
transform(item)
}) self.next_index += 1;
.collect() Some(item)
} else {
None
}
} }
} }

View file

@ -86,3 +86,5 @@ pub type NSInteger = libc::c_long;
/// Platform-specific. /// Platform-specific.
#[cfg(target_pointer_width = "64")] #[cfg(target_pointer_width = "64")]
pub type NSUInteger = libc::c_ulong; pub type NSUInteger = libc::c_ulong;
pub type NSPoint = core_graphics::geometry::CGPoint;

View file

@ -74,36 +74,62 @@ impl MacSystemIcon {
#[derive(Debug)] #[derive(Debug)]
pub enum SFSymbol { pub enum SFSymbol {
AtSymbol, AtSymbol,
ArrowClockwise,
Bell,
BellFill,
BellBadge,
BellBadgeFill,
GearShape, GearShape,
FolderFilled, FolderFilled,
ListAndFilm,
PaperPlane, PaperPlane,
PaperPlaneFilled, PaperPlaneFilled,
Plus, Plus,
Minus, Minus,
Message,
MessageFill,
MessageBadge,
MessageBadgeFill,
MessageBadgeFilledFill,
PersonCropCircle,
SliderVertical3, SliderVertical3,
SquareAndArrowUpOnSquare, SquareAndArrowUpOnSquare,
SquareAndArrowUpOnSquareFill, SquareAndArrowUpOnSquareFill,
SquareAndArrowDownOnSquare, SquareAndArrowDownOnSquare,
SquareAndArrowDownOnSquareFill, SquareAndArrowDownOnSquareFill,
SquareDashed SquareDashed,
SquareAndPencil
} }
impl SFSymbol { impl SFSymbol {
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
Self::AtSymbol => "at", Self::AtSymbol => "at",
Self::ArrowClockwise => "arrow.clockwise",
Self::GearShape => "gearshape", Self::GearShape => "gearshape",
Self::FolderFilled => "folder.fill", Self::FolderFilled => "folder.fill",
Self::ListAndFilm => "list.and.film",
Self::Bell => "bell",
Self::BellFill => "bell.fill",
Self::BellBadge => "bell.badge",
Self::BellBadgeFill => "bell.badge.fill",
Self::PaperPlane => "paperplane", Self::PaperPlane => "paperplane",
Self::PaperPlaneFilled => "paperplane.fill", Self::PaperPlaneFilled => "paperplane.fill",
Self::Plus => "plus", Self::Plus => "plus",
Self::Minus => "minus", Self::Minus => "minus",
Self::Message => "message",
Self::MessageFill => "message.fill",
Self::MessageBadge => "message.badge",
Self::MessageBadgeFill => "message.badge.fill",
Self::MessageBadgeFilledFill => "message.badge.filled.fill",
Self::PersonCropCircle => "person.crop.circle",
Self::SliderVertical3 => "slider.vertical.3", Self::SliderVertical3 => "slider.vertical.3",
Self::SquareAndArrowUpOnSquare => "square.and.arrow.up.on.square", Self::SquareAndArrowUpOnSquare => "square.and.arrow.up.on.square",
Self::SquareAndArrowUpOnSquareFill => "square.and.arrow.up.on.square.fill", Self::SquareAndArrowUpOnSquareFill => "square.and.arrow.up.on.square.fill",
Self::SquareAndArrowDownOnSquare => "square.and.arrow.down.on.square", Self::SquareAndArrowDownOnSquare => "square.and.arrow.down.on.square",
Self::SquareAndArrowDownOnSquareFill => "square.and.arrow.down.on.square.fill", Self::SquareAndArrowDownOnSquareFill => "square.and.arrow.down.on.square.fill",
Self::SquareDashed => "square.dashed" Self::SquareDashed => "square.dashed",
Self::SquareAndPencil => "square.and.pencil"
} }
} }
} }

View file

@ -12,7 +12,7 @@ use core_graphics::{
}; };
use super::icons::*; use super::icons::*;
use crate::foundation::{id, NSData, NSString, NO, YES}; use crate::foundation::{id, NSData, NSString, NO, NSURL, YES};
use crate::utils::os; use crate::utils::os;
/// Specifies resizing behavior for image drawing. /// Specifies resizing behavior for image drawing.
@ -146,6 +146,14 @@ impl Image {
}) })
} }
#[cfg(target_os = "macos")]
pub fn with_contents_of_url(url: NSURL) -> Self {
Image(unsafe {
let alloc: id = msg_send![Self::class(), alloc];
ShareId::from_ptr(msg_send![alloc, initWithContentsOfURL: url.objc])
})
}
/// Given a Vec of data, will transform it into an Image by passing it through NSData. /// Given a Vec of data, will transform it into an Image by passing it through NSData.
/// This can be useful for when you need to include_bytes!() something into your binary. /// This can be useful for when you need to include_bytes!() something into your binary.
pub fn with_data(data: &[u8]) -> Self { pub fn with_data(data: &[u8]) -> Self {

View file

@ -8,6 +8,7 @@ use crate::layout::Layout;
use crate::objc_access::ObjcAccess; use crate::objc_access::ObjcAccess;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
use crate::layer::Layer;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
use crate::layout::{LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY}; use crate::layout::{LayoutAnchorDimension, LayoutAnchorX, LayoutAnchorY};
@ -52,6 +53,10 @@ pub struct ImageView {
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
pub objc: ObjcProperty, pub objc: ObjcProperty,
/// References the underlying layer. This is consistent across AppKit & UIKit - in AppKit
/// we explicitly opt in to layer backed views.
pub layer: Layer,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C runtime top layout constraint.
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub top: LayoutAnchorY, pub top: LayoutAnchorY,
@ -135,6 +140,8 @@ impl ImageView {
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(view), center_y: LayoutAnchorY::center(view),
layer: Layer::wrap(unsafe { msg_send![view, layer] }),
objc: ObjcProperty::retain(view) objc: ObjcProperty::retain(view)
} }
} }

View file

@ -44,7 +44,7 @@
//! For more information on Autolayout, view the module or check out the examples folder. //! For more information on Autolayout, view the module or check out the examples folder.
use objc::runtime::{Class, Object}; use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc_id::ShareId; use objc_id::ShareId;
use crate::color::Color; use crate::color::Color;
@ -64,11 +64,11 @@ mod appkit;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
use appkit::{register_view_class, register_view_class_with_delegate}; use appkit::{register_view_class, register_view_class_with_delegate};
//#[cfg(feature = "uikit")] #[cfg(feature = "uikit")]
//mod uikit; mod uikit;
//#[cfg(feature = "uikit")] #[cfg(all(feature = "uikit", not(feature = "appkit")))]
//use uikit::{register_view_class, register_view_class_with_delegate}; use uikit::{register_view_class, register_view_class_with_delegate};
mod traits; mod traits;
pub use traits::TextFieldDelegate; pub use traits::TextFieldDelegate;
@ -200,50 +200,52 @@ where
let class = register_view_class_with_delegate(&delegate); let class = register_view_class_with_delegate(&delegate);
let mut delegate = Box::new(delegate); let mut delegate = Box::new(delegate);
let label = common_init(class); let input = common_init(class);
unsafe { unsafe {
let ptr: *const T = &*delegate; let ptr: *const T = &*delegate;
(&mut *label).set_ivar(TEXTFIELD_DELEGATE_PTR, ptr as usize); (&mut *input).set_ivar(TEXTFIELD_DELEGATE_PTR, ptr as usize);
}; };
#[cfg(feature = "uikit")]
let _: () = unsafe { msg_send![input, setDelegate: input] };
let mut label = TextField { let mut input = TextField {
delegate: None, delegate: None,
objc: ObjcProperty::retain(label), objc: ObjcProperty::retain(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(label), top: LayoutAnchorY::top(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
left: LayoutAnchorX::left(label), left: LayoutAnchorX::left(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
leading: LayoutAnchorX::leading(label), leading: LayoutAnchorX::leading(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
right: LayoutAnchorX::right(label), right: LayoutAnchorX::right(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
trailing: LayoutAnchorX::trailing(label), trailing: LayoutAnchorX::trailing(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
bottom: LayoutAnchorY::bottom(label), bottom: LayoutAnchorY::bottom(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
width: LayoutAnchorDimension::width(label), width: LayoutAnchorDimension::width(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
height: LayoutAnchorDimension::height(label), height: LayoutAnchorDimension::height(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
center_x: LayoutAnchorX::center(label), center_x: LayoutAnchorX::center(input),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(label) center_y: LayoutAnchorY::center(input)
}; };
(&mut delegate).did_load(label.clone_as_handle()); (&mut delegate).did_load(input.clone_as_handle());
label.delegate = Some(delegate); input.delegate = Some(delegate);
label input
} }
} }
@ -290,10 +292,16 @@ impl<T> TextField<T> {
} }
/// Grabs the value from the textfield and returns it as an owned String. /// Grabs the value from the textfield and returns it as an owned String.
#[cfg(feature = "appkit")]
pub fn get_value(&self) -> String { pub fn get_value(&self) -> String {
self.objc self.objc
.get(|obj| unsafe { NSString::retain(msg_send![obj, stringValue]).to_string() }) .get(|obj| unsafe { NSString::retain(msg_send![obj, stringValue]).to_string() })
} }
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
pub fn get_value(&self) -> String {
self.objc
.get(|obj| unsafe { NSString::retain(msg_send![obj, text]).to_string() })
}
/// Call this to set the background color for the backing layer. /// Call this to set the background color for the backing layer.
pub fn set_background_color<C: AsRef<Color>>(&self, color: C) { pub fn set_background_color<C: AsRef<Color>>(&self, color: C) {
@ -309,7 +317,10 @@ impl<T> TextField<T> {
let s = NSString::new(text); let s = NSString::new(text);
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setStringValue:&*s]; let _: () = msg_send![obj, setStringValue:&*s];
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let _: () = msg_send![obj, setText:&*s];
}); });
} }
@ -318,7 +329,10 @@ impl<T> TextField<T> {
let s = NSString::new(text); let s = NSString::new(text);
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setPlaceholderString:&*s]; let _: () = msg_send![obj, setPlaceholderString:&*s];
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let _: () = msg_send![obj, setPlaceholder:&*s];
}); });
} }
@ -326,7 +340,10 @@ impl<T> TextField<T> {
pub fn set_text_alignment(&self, alignment: TextAlign) { pub fn set_text_alignment(&self, alignment: TextAlign) {
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
let alignment: NSInteger = alignment.into(); let alignment: NSInteger = alignment.into();
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setAlignment: alignment]; let _: () = msg_send![obj, setAlignment: alignment];
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let _: () = msg_send![obj, setTextAlignment: alignment];
}); });
} }
@ -401,3 +418,16 @@ impl<T> Drop for TextField<T> {
}*/ }*/
} }
} }
#[test]
fn test_text_view() {
let text_field = TextField::new();
let value = text_field.get_value();
assert!(value.is_empty());
text_field.set_background_color(Color::SystemBlue);
text_field.set_text("foobar");
let value = text_field.get_value();
assert_eq!(value, "foobar".to_string());
text_field.set_text_alignment(TextAlign::Left);
text_field.set_font(Font::default());
}

95
src/input/uikit.rs Normal file
View file

@ -0,0 +1,95 @@
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, load_or_register_class, NSString, NSUInteger, NO, YES};
use crate::input::{TextFieldDelegate, TEXTFIELD_DELEGATE_PTR};
use crate::utils::load;
/// Called when editing this text field has ended (e.g. user pressed enter).
extern "C" fn text_did_end_editing<T: TextFieldDelegate>(this: &mut Object, _: Sel, _info: id) {
let view = load::<T>(this, TEXTFIELD_DELEGATE_PTR);
let s = NSString::retain(unsafe { msg_send![this, text] });
view.text_did_end_editing(s.to_str());
}
extern "C" fn text_did_begin_editing<T: TextFieldDelegate>(this: &mut Object, _: Sel, _info: id) {
let view = load::<T>(this, TEXTFIELD_DELEGATE_PTR);
let s = NSString::retain(unsafe { msg_send![this, text] });
view.text_did_begin_editing(s.to_str());
}
extern "C" fn text_did_change<T: TextFieldDelegate>(this: &mut Object, _: Sel, _info: id) {
let view = load::<T>(this, TEXTFIELD_DELEGATE_PTR);
let s = NSString::retain(unsafe { msg_send![this, text] });
view.text_did_change(s.to_str());
}
extern "C" fn text_should_begin_editing<T: TextFieldDelegate>(this: &mut Object, _: Sel, _info: id) -> BOOL {
let view = load::<T>(this, TEXTFIELD_DELEGATE_PTR);
let s = NSString::retain(unsafe { msg_send![this, text] });
match view.text_should_begin_editing(s.to_str()) {
true => YES,
false => NO
}
}
extern "C" fn text_should_end_editing<T: TextFieldDelegate>(this: &mut Object, _: Sel, _info: id) -> BOOL {
let view = load::<T>(this, TEXTFIELD_DELEGATE_PTR);
let s = NSString::retain(unsafe { msg_send![this, text] });
match view.text_should_end_editing(s.to_str()) {
true => YES,
false => NO
}
}
/// Injects an `UITextField` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UITextField);
let decl = ClassDecl::new("RSTTextInputField", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `UITextField` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: TextFieldDelegate>(instance: &T) -> *const Class {
load_or_register_class("UITextField", instance.subclass_name(), |decl| unsafe {
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(TEXTFIELD_DELEGATE_PTR);
decl.add_method(
sel!(textFieldDidEndEditing:),
text_did_end_editing::<T> as extern "C" fn(&mut Object, _, _)
);
decl.add_method(
sel!(textFieldDidBeginEditing:),
text_did_begin_editing::<T> as extern "C" fn(&mut Object, _, _)
);
decl.add_method(
sel!(textFieldDidChangeSelection:),
text_did_change::<T> as extern "C" fn(&mut Object, _, _)
);
decl.add_method(
sel!(textFieldShouldBeginEditing:),
text_should_begin_editing::<T> as extern "C" fn(&mut Object, Sel, id) -> BOOL
);
decl.add_method(
sel!(textFieldShouldEndEditing:),
text_should_end_editing::<T> as extern "C" fn(&mut Object, Sel, id) -> BOOL
);
})
}

View file

@ -27,7 +27,7 @@ pub static ACTION_CALLBACK_PTR: &str = "rstTargetActionPtr";
/// Point is, Button aren't created that much in the grand scheme of things, /// Point is, Button aren't created that much in the grand scheme of things,
/// and the heap isn't our enemy in a GUI framework anyway. If someone knows /// and the heap isn't our enemy in a GUI framework anyway. If someone knows
/// a better way to do this that doesn't require double-boxing, I'm all ears. /// a better way to do this that doesn't require double-boxing, I'm all ears.
pub struct Action(Box<dyn Fn() + Send + Sync + 'static>); pub struct Action(Box<dyn Fn(*const Object) + Send + Sync + 'static>);
impl fmt::Debug for Action { impl fmt::Debug for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -51,7 +51,7 @@ pub struct TargetActionHandler {
impl TargetActionHandler { impl TargetActionHandler {
/// Returns a new TargetEventHandler. /// Returns a new TargetEventHandler.
pub fn new<F: Fn() + Send + Sync + 'static>(control: &Object, action: F) -> Self { pub fn new<F: Fn(*const Object) + Send + Sync + 'static>(control: &Object, action: F) -> Self {
let block = Box::new(Action(Box::new(action))); let block = Box::new(Action(Box::new(action)));
let ptr = Box::into_raw(block); let ptr = Box::into_raw(block);
@ -74,9 +74,9 @@ impl TargetActionHandler {
} }
/// This will fire for an NSButton callback. /// This will fire for an NSButton callback.
extern "C" fn perform<F: Fn() + 'static>(this: &mut Object, _: Sel, _sender: id) { extern "C" fn perform<F: Fn(*const Object) + 'static>(this: &mut Object, _: Sel, sender: id) {
let action = load::<Action>(this, ACTION_CALLBACK_PTR); let action = load::<Action>(this, ACTION_CALLBACK_PTR);
(action.0)(); (action.0)(sender.cast_const());
} }
/// Due to the way that Rust and Objective-C live... very different lifestyles, /// Due to the way that Rust and Objective-C live... very different lifestyles,
@ -91,7 +91,7 @@ extern "C" fn perform<F: Fn() + 'static>(this: &mut Object, _: Sel, _sender: id)
/// The `NSButton` owns this object on instantiation, and will release it /// The `NSButton` owns this object on instantiation, and will release it
/// on drop. We handle the heap copy on the Rust side, so setting the block /// on drop. We handle the heap copy on the Rust side, so setting the block
/// is just an ivar. /// is just an ivar.
pub(crate) fn register_invoker_class<F: Fn() + 'static>() -> *const Class { pub(crate) fn register_invoker_class<F: Fn(*const Object) + 'static>() -> *const Class {
load_or_register_class("NSObject", "RSTTargetActionHandler", |decl| unsafe { load_or_register_class("NSObject", "RSTTargetActionHandler", |decl| unsafe {
decl.add_ivar::<usize>(ACTION_CALLBACK_PTR); decl.add_ivar::<usize>(ACTION_CALLBACK_PTR);
decl.add_method(sel!(perform:), perform::<F> as extern "C" fn(&mut Object, _, id)); decl.add_method(sel!(perform:), perform::<F> as extern "C" fn(&mut Object, _, id));

View file

@ -118,7 +118,7 @@ pub mod cloudkit;
pub mod color; pub mod color;
#[cfg(feature = "appkit")] #[cfg(any(feature = "appkit", feature = "uikit"))]
pub mod control; pub mod control;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
@ -140,7 +140,7 @@ pub mod geometry;
#[cfg(any(feature = "appkit", feature = "uikit"))] #[cfg(any(feature = "appkit", feature = "uikit"))]
pub mod image; pub mod image;
#[cfg(feature = "appkit")] #[cfg(any(feature = "appkit", feature = "uikit"))]
pub mod input; pub mod input;
pub(crate) mod invoker; pub(crate) mod invoker;
@ -161,7 +161,7 @@ pub mod pasteboard;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod progress; pub mod progress;
#[cfg(feature = "appkit")] #[cfg(any(feature = "appkit", feature = "uikit"))]
pub mod scrollview; pub mod scrollview;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
@ -170,7 +170,6 @@ pub mod switch;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub mod select; pub mod select;
#[cfg(feature = "appkit")]
pub mod text; pub mod text;
#[cfg(feature = "quicklook")] #[cfg(feature = "quicklook")]

View file

@ -55,6 +55,7 @@ use crate::layer::Layer;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess; use crate::objc_access::ObjcAccess;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(all(feature = "appkit", target_os = "macos"))]
use crate::view::{ViewAnimatorProxy, ViewDelegate}; use crate::view::{ViewAnimatorProxy, ViewDelegate};
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -96,6 +97,7 @@ fn allocate_view(registration_fn: fn() -> *const Class) -> id {
#[derive(Debug)] #[derive(Debug)]
pub struct ListViewRow<T = ()> { pub struct ListViewRow<T = ()> {
/// An object that supports limited animations. Can be cloned into animation closures. /// An object that supports limited animations. Can be cloned into animation closures.
#[cfg(all(feature = "appkit", target_os = "macos"))]
pub animator: ViewAnimatorProxy, pub animator: ViewAnimatorProxy,
/// A pointer to the Objective-C runtime view controller. /// A pointer to the Objective-C runtime view controller.
@ -163,6 +165,7 @@ impl ListViewRow {
ListViewRow { ListViewRow {
delegate: None, delegate: None,
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
#[cfg(all(feature = "appkit", target_os = "macos"))]
animator: ViewAnimatorProxy::new(view), animator: ViewAnimatorProxy::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -227,6 +230,7 @@ where
let view = ListViewRow { let view = ListViewRow {
delegate: Some(delegate), delegate: Some(delegate),
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
#[cfg(all(feature = "appkit", target_os = "macos"))]
animator: ViewAnimatorProxy::new(view), animator: ViewAnimatorProxy::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -283,6 +287,7 @@ where
let mut view = ListViewRow { let mut view = ListViewRow {
delegate: None, delegate: None,
objc: ObjcProperty::retain(view), objc: ObjcProperty::retain(view),
#[cfg(all(feature = "appkit", target_os = "macos"))]
animator: ViewAnimatorProxy::new(view), animator: ViewAnimatorProxy::new(view),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -335,6 +340,7 @@ where
ListViewRow { ListViewRow {
delegate: None, delegate: None,
objc: self.objc.clone(), objc: self.objc.clone(),
#[cfg(all(feature = "appkit", target_os = "macos"))]
animator: self.animator.clone(), animator: self.animator.clone(),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -384,6 +390,7 @@ impl<T> ListViewRow<T> {
is_handle: true, is_handle: true,
layer: Layer::new(), // @TODO: Fix & return cloned true layer for this row. layer: Layer::new(), // @TODO: Fix & return cloned true layer for this row.
objc: self.objc.clone(), objc: self.objc.clone(),
#[cfg(all(feature = "appkit", target_os = "macos"))]
animator: self.animator.clone(), animator: self.animator.clone(),
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]

View file

@ -106,7 +106,7 @@ impl Pasteboard {
})); }));
} }
let urls = NSArray::retain(contents).map(|url| NSURL::retain(url)).into_iter().collect(); let urls = NSArray::retain(contents).iter().map(|url| NSURL::retain(url)).collect();
Ok(urls) Ok(urls)
} }

View file

@ -50,7 +50,6 @@ use crate::color::Color;
use crate::foundation::{id, nil, NSArray, NSString, NO, YES}; use crate::foundation::{id, nil, NSArray, NSString, NO, YES};
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess; use crate::objc_access::ObjcAccess;
use crate::pasteboard::PasteboardType;
use crate::utils::properties::ObjcProperty; use crate::utils::properties::ObjcProperty;
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
@ -62,11 +61,11 @@ mod appkit;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
use appkit::{register_scrollview_class, register_scrollview_class_with_delegate}; use appkit::{register_scrollview_class, register_scrollview_class_with_delegate};
//#[cfg(feature = "uikit")] #[cfg(feature = "uikit")]
//mod ios; mod uikit;
//#[cfg(feature = "uikit")] #[cfg(all(feature = "uikit", not(feature = "appkit")))]
//use ios::{register_view_class, register_view_class_with_delegate}; use uikit::{register_scrollview_class, register_scrollview_class_with_delegate};
mod traits; mod traits;
pub use traits::ScrollViewDelegate; pub use traits::ScrollViewDelegate;
@ -334,3 +333,8 @@ impl<T> Drop for ScrollView<T> {
}*/ }*/
} }
} }
#[test]
fn test_scrollview() {
let view = ScrollView::new();
}

View file

@ -1,3 +1,4 @@
#[cfg(feature = "appkit")]
use crate::dragdrop::{DragInfo, DragOperation}; use crate::dragdrop::{DragInfo, DragOperation};
use crate::scrollview::ScrollView; use crate::scrollview::ScrollView;
@ -22,24 +23,29 @@ pub trait ScrollViewDelegate {
/// Called when this has been removed from the view heirarchy. /// Called when this has been removed from the view heirarchy.
fn did_disappear(&self, _animated: bool) {} fn did_disappear(&self, _animated: bool) {}
#[cfg(feature = "appkit")]
/// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform. /// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform.
fn dragging_entered(&self, _info: DragInfo) -> DragOperation { fn dragging_entered(&self, _info: DragInfo) -> DragOperation {
DragOperation::None DragOperation::None
} }
#[cfg(feature = "appkit")]
/// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation. /// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation.
fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool {
false false
} }
#[cfg(feature = "appkit")]
/// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data. /// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data.
fn perform_drag_operation(&self, _info: DragInfo) -> bool { fn perform_drag_operation(&self, _info: DragInfo) -> bool {
false false
} }
#[cfg(feature = "appkit")]
/// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up. /// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up.
fn conclude_drag_operation(&self, _info: DragInfo) {} fn conclude_drag_operation(&self, _info: DragInfo) {}
#[cfg(feature = "appkit")]
/// Invoked when the dragged image exits the destinations bounds rectangle (in the case of a view) or its frame /// Invoked when the dragged image exits the destinations bounds rectangle (in the case of a view) or its frame
/// rectangle (in the case of a window object). /// rectangle (in the case of a window object).
fn dragging_exited(&self, _info: DragInfo) {} fn dragging_exited(&self, _info: DragInfo) {}

138
src/scrollview/uikit.rs Normal file
View file

@ -0,0 +1,138 @@
//! This module does one specific thing: register a custom `NSView` class that's... brought to the
//! modern era.
//!
//! I kid, I kid.
//!
//! It just enforces that coordinates are judged from the top-left, which is what most people look
//! for in the modern era. It also implements a few helpers for things like setting a background
//! color, and enforcing layer backing by default.
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, NSUInteger, NO, YES};
use crate::scrollview::{ScrollViewDelegate, SCROLLVIEW_DELEGATE_PTR};
use crate::utils::load;
/// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though.
extern "C" fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
return YES;
}
/*
use crate::dragdrop::DragInfo;
/// Called when a drag/drop operation has entered this view.
extern "C" fn dragging_entered<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) -> NSUInteger {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
view.dragging_entered(DragInfo {
info: unsafe { Id::from_ptr(info) }
})
.into()
}
/// Called when a drag/drop operation has entered this view.
extern "C" fn prepare_for_drag_operation<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
match view.prepare_for_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern "C" fn perform_drag_operation<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) -> BOOL {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
match view.perform_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
}) {
true => YES,
false => NO
}
}
/// Called when a drag/drop operation has entered this view.
extern "C" fn conclude_drag_operation<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
view.conclude_drag_operation(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
/// Called when a drag/drop operation has entered this view.
extern "C" fn dragging_exited<T: ScrollViewDelegate>(this: &mut Object, _: Sel, info: id) {
let view = load::<T>(this, SCROLLVIEW_DELEGATE_PTR);
view.dragging_exited(DragInfo {
info: unsafe { Id::from_ptr(info) }
});
}
*/
/// Injects an `UIScrollView` subclass.
pub(crate) fn register_scrollview_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIScrollView);
let decl = ClassDecl::new("RSTScrollView", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `NSView` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_scrollview_class_with_delegate<T: ScrollViewDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIScrollView);
let mut decl = ClassDecl::new("RSTScrollViewWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(SCROLLVIEW_DELEGATE_PTR);
decl.add_method(sel!(isFlipped), enforce_normalcy as extern "C" fn(&Object, _) -> BOOL);
/*
// Drag and drop operations (e.g, accepting files)
decl.add_method(
sel!(draggingEntered:),
dragging_entered::<T> as extern "C" fn(&mut Object, _, _) -> NSUInteger
);
decl.add_method(
sel!(prepareForDragOperation:),
prepare_for_drag_operation::<T> as extern "C" fn(&mut Object, _, _) -> BOOL
);
decl.add_method(
sel!(performDragOperation:),
perform_drag_operation::<T> as extern "C" fn(&mut Object, _, _) -> BOOL
);
decl.add_method(
sel!(concludeDragOperation:),
conclude_drag_operation::<T> as extern "C" fn(&mut Object, _, _)
);
decl.add_method(
sel!(draggingExited:),
dragging_exited::<T> as extern "C" fn(&mut Object, _, _)
);
*/
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -140,7 +140,7 @@ impl Select {
/// Really, this is not ideal. /// Really, this is not ideal.
/// ///
/// I cannot stress this enough. /// I cannot stress this enough.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) { pub fn set_action<F: Fn(*const Object) + Send + Sync + 'static>(&mut self, action: F) {
// @TODO: This probably isn't ideal but gets the job done for now; needs revisiting. // @TODO: This probably isn't ideal but gets the job done for now; needs revisiting.
let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) }); let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) });
let handler = TargetActionHandler::new(&*this, action); let handler = TargetActionHandler::new(&*this, action);

View file

@ -130,7 +130,7 @@ impl Switch {
/// Attaches a callback for button press events. Don't get too creative now... /// Attaches a callback for button press events. Don't get too creative now...
/// best just to message pass or something. /// best just to message pass or something.
pub fn set_action<F: Fn() + Send + Sync + 'static>(&mut self, action: F) { pub fn set_action<F: Fn(*const Object) + Send + Sync + 'static>(&mut self, action: F) {
// @TODO: This probably isn't ideal but gets the job done for now; needs revisiting. // @TODO: This probably isn't ideal but gets the job done for now; needs revisiting.
let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) }); let this = self.objc.get(|obj| unsafe { ShareId::from_ptr(msg_send![obj, self]) });
let handler = TargetActionHandler::new(&*this, action); let handler = TargetActionHandler::new(&*this, action);

View file

@ -19,27 +19,39 @@ pub struct Font(pub ShareId<Object>);
impl Default for Font { impl Default for Font {
/// Returns the default `labelFont` on macOS. /// Returns the default `labelFont` on macOS.
fn default() -> Self { fn default() -> Self {
Font(unsafe { let cls = Self::class();
let cls = class!(NSFont); let default_size: id = unsafe { msg_send![cls, labelFontSize] };
let default_size: id = msg_send![cls, labelFontSize];
ShareId::from_ptr(msg_send![cls, labelFontOfSize: default_size]) #[cfg(feature = "appkit")]
}) let font = Font(unsafe { ShareId::from_ptr(msg_send![cls, labelFontOfSize: default_size]) });
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let font = Font(unsafe { ShareId::from_ptr(msg_send![cls, systemFontOfSize: default_size]) });
font
} }
} }
impl Font { impl Font {
fn class() -> &'static Class {
#[cfg(feature = "appkit")]
let class = class!(NSFont);
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let class = class!(UIFont);
class
}
/// Creates and returns a default system font at the specified size. /// Creates and returns a default system font at the specified size.
pub fn system(size: f64) -> Self { pub fn system(size: f64) -> Self {
let size = size as CGFloat; let size = size as CGFloat;
Font(unsafe { ShareId::from_ptr(msg_send![class!(NSFont), systemFontOfSize: size]) }) Font(unsafe { ShareId::from_ptr(msg_send![Self::class(), systemFontOfSize: size]) })
} }
/// Creates and returns a default bold system font at the specified size. /// Creates and returns a default bold system font at the specified size.
pub fn bold_system(size: f64) -> Self { pub fn bold_system(size: f64) -> Self {
let size = size as CGFloat; let size = size as CGFloat;
Font(unsafe { ShareId::from_ptr(msg_send![class!(NSFont), boldSystemFontOfSize: size]) }) Font(unsafe { ShareId::from_ptr(msg_send![Self::class(), boldSystemFontOfSize: size]) })
} }
/// Creates and returns a monospace system font at the specified size and weight /// Creates and returns a monospace system font at the specified size and weight
@ -78,3 +90,10 @@ impl AsRef<Font> for Font {
self self
} }
} }
#[test]
fn font_test() {
let default_font = Font::default();
let system_font = Font::system(100.0);
let bold_system_font = Font::bold_system(100.0);
}

View file

@ -49,6 +49,7 @@ use objc_id::ShareId;
use crate::color::Color; use crate::color::Color;
use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NSUInteger, NO, YES}; use crate::foundation::{id, nil, NSArray, NSInteger, NSString, NSUInteger, NO, YES};
use crate::layer::Layer;
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess; use crate::objc_access::ObjcAccess;
use crate::text::{AttributedString, Font, LineBreakMode, TextAlign}; use crate::text::{AttributedString, Font, LineBreakMode, TextAlign};
@ -63,11 +64,11 @@ mod appkit;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
use appkit::{register_view_class, register_view_class_with_delegate}; use appkit::{register_view_class, register_view_class_with_delegate};
//#[cfg(feature = "uikit")] #[cfg(feature = "uikit")]
//mod uikit; mod uikit;
//#[cfg(feature = "uikit")] #[cfg(all(feature = "uikit", not(feature = "appkit")))]
//use uikit::{register_view_class, register_view_class_with_delegate}; use uikit::{register_view_class, register_view_class_with_delegate};
mod traits; mod traits;
pub use traits::LabelDelegate; pub use traits::LabelDelegate;
@ -156,6 +157,10 @@ pub struct Label<T = ()> {
/// A pointer to the delegate for this view. /// A pointer to the delegate for this view.
pub delegate: Option<Box<T>>, pub delegate: Option<Box<T>>,
/// References the underlying layer. This is consistent across AppKit & UIKit - in AppKit
/// we explicitly opt in to layer backed views.
pub layer: Layer,
/// A pointer to the Objective-C runtime top layout constraint. /// A pointer to the Objective-C runtime top layout constraint.
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
pub top: LayoutAnchorY, pub top: LayoutAnchorY,
@ -207,9 +212,12 @@ impl Label {
/// Returns a default `Label`, suitable for /// Returns a default `Label`, suitable for
pub fn new() -> Self { pub fn new() -> Self {
let view = allocate_view(register_view_class); let view = allocate_view(register_view_class);
Self::init(view, None)
}
pub(crate) fn init<T>(view: id, delegate: Option<Box<T>>) -> Label<T> {
Label { Label {
delegate: None, delegate,
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(view), top: LayoutAnchorY::top(view),
@ -241,6 +249,8 @@ impl Label {
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(view), center_y: LayoutAnchorY::center(view),
layer: Layer::wrap(unsafe { msg_send![view, layer] }),
objc: ObjcProperty::retain(view) objc: ObjcProperty::retain(view)
} }
} }
@ -255,51 +265,12 @@ where
pub fn with(delegate: T) -> Label<T> { pub fn with(delegate: T) -> Label<T> {
let delegate = Box::new(delegate); let delegate = Box::new(delegate);
let label = allocate_view(register_view_class_with_delegate::<T>); let view = allocate_view(register_view_class_with_delegate::<T>);
unsafe { unsafe {
let ptr: *const T = &*delegate; let ptr: *const T = &*delegate;
(&mut *label).set_ivar(LABEL_DELEGATE_PTR, ptr as usize); (&mut *view).set_ivar(LABEL_DELEGATE_PTR, ptr as usize);
}; };
Label::init(view, Some(delegate))
let mut label = Label {
delegate: None,
#[cfg(feature = "autolayout")]
top: LayoutAnchorY::top(label),
#[cfg(feature = "autolayout")]
left: LayoutAnchorX::left(label),
#[cfg(feature = "autolayout")]
leading: LayoutAnchorX::leading(label),
#[cfg(feature = "autolayout")]
right: LayoutAnchorX::right(label),
#[cfg(feature = "autolayout")]
trailing: LayoutAnchorX::trailing(label),
#[cfg(feature = "autolayout")]
bottom: LayoutAnchorY::bottom(label),
#[cfg(feature = "autolayout")]
width: LayoutAnchorDimension::width(label),
#[cfg(feature = "autolayout")]
height: LayoutAnchorDimension::height(label),
#[cfg(feature = "autolayout")]
center_x: LayoutAnchorX::center(label),
#[cfg(feature = "autolayout")]
center_y: LayoutAnchorY::center(label),
objc: ObjcProperty::retain(label)
};
//(&mut delegate).did_load(label.clone_as_handle());
label.delegate = Some(delegate);
label
} }
} }
@ -342,6 +313,8 @@ impl<T> Label<T> {
#[cfg(feature = "autolayout")] #[cfg(feature = "autolayout")]
center_y: self.center_y.clone(), center_y: self.center_y.clone(),
layer: self.layer.clone(),
objc: self.objc.clone() objc: self.objc.clone()
} }
} }
@ -372,28 +345,51 @@ impl<T> Label<T> {
let s = NSString::new(text); let s = NSString::new(text);
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setStringValue:&*s]; let _: () = msg_send![obj, setStringValue:&*s];
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let _: () = msg_send![obj, setText:&*s];
}); });
} }
/// Sets the attributed string to be the attributed string value on this label. /// Sets the attributed string to be the attributed string value on this label.
pub fn set_attributed_text(&self, text: AttributedString) { pub fn set_attributed_text(&self, text: AttributedString) {
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setAttributedStringValue:&*text]; let _: () = msg_send![obj, setAttributedStringValue:&*text];
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let _: () = msg_send![obj, setAttributedText:&*text];
}); });
} }
/// Retrieve the text currently held in the label. /// Retrieve the text currently held in the label.
#[cfg(feature = "appkit")]
pub fn get_text(&self) -> String { pub fn get_text(&self) -> String {
self.objc self.objc
.get(|obj| unsafe { NSString::retain(msg_send![obj, stringValue]).to_string() }) .get(|obj| unsafe { NSString::retain(msg_send![obj, stringValue]).to_string() })
} }
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
pub fn get_text(&self) -> String {
self.objc.get(|obj| {
let val: id = unsafe { msg_send![obj, text] };
// Through trial and error, this seems to return a null pointer when there's no
// text.
if val.is_null() {
String::new()
} else {
NSString::retain(val).to_string()
}
})
}
/// Sets the text alignment for this label. /// Sets the text alignment for this label.
pub fn set_text_alignment(&self, alignment: TextAlign) { pub fn set_text_alignment(&self, alignment: TextAlign) {
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
let alignment: NSInteger = alignment.into(); let alignment: NSInteger = alignment.into();
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setAlignment: alignment]; let _: () = msg_send![obj, setAlignment: alignment];
#[cfg(all(feature = "uikit", not(feature = "appkit")))]
let _: () = msg_send![obj, setTextAlignment: alignment];
}); });
} }
@ -421,7 +417,10 @@ impl<T> Label<T> {
/// Sets the maximum number of lines. /// Sets the maximum number of lines.
pub fn set_max_number_of_lines(&self, num: NSInteger) { pub fn set_max_number_of_lines(&self, num: NSInteger) {
self.objc.with_mut(|obj| unsafe { self.objc.with_mut(|obj| unsafe {
#[cfg(feature = "appkit")]
let _: () = msg_send![obj, setMaximumNumberOfLines: num]; let _: () = msg_send![obj, setMaximumNumberOfLines: num];
#[cfg(feature = "uikit")]
let _: () = msg_send![obj, setNumberOfLines: num];
}); });
} }
@ -467,3 +466,18 @@ impl<T> Drop for Label<T> {
}*/ }*/
} }
} }
#[test]
fn test_label() {
let label = Label::new();
let text = label.get_text();
assert!(text.is_empty());
label.set_background_color(Color::SystemOrange);
label.set_text_color(Color::SystemRed);
label.set_text_alignment(TextAlign::Right);
label.set_text("foobar");
let text = label.get_text();
assert_eq!(text, "foobar".to_string());
label.set_font(Font::system(10.0));
label.set_attributed_text(AttributedString::new("foobar"));
}

45
src/text/label/uikit.rs Normal file
View file

@ -0,0 +1,45 @@
use std::sync::Once;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use objc::{class, sel, sel_impl};
use objc_id::Id;
use crate::foundation::{id, NSUInteger, NO, YES};
use crate::text::label::{LabelDelegate, LABEL_DELEGATE_PTR};
/// Injects an `UILabel` subclass. This is used for the default views that don't use delegates - we
/// have separate classes here since we don't want to waste cycles on methods that will never be
/// used if there's no delegates.
pub(crate) fn register_view_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UILabel);
let decl = ClassDecl::new("RSTTextField", superclass).unwrap();
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}
/// Injects an `UILabel` subclass, with some callback and pointer ivars for what we
/// need to do.
pub(crate) fn register_view_class_with_delegate<T: LabelDelegate>() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = Once::new();
INIT.call_once(|| unsafe {
let superclass = class!(UIView);
let mut decl = ClassDecl::new("RSTTextFieldWithDelegate", superclass).unwrap();
// A pointer to the "view controller" on the Rust side. It's expected that this doesn't
// move.
decl.add_ivar::<usize>(LABEL_DELEGATE_PTR);
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -139,7 +139,10 @@ where
} }
} }
impl<T, W, F> App<T, W, F> { impl<T, W, F> App<T, W, F>
where
T: AppDelegate + 'static
{
/// Handles calling through to `UIApplicationMain()`, ensuring that it's using our custom /// Handles calling through to `UIApplicationMain()`, ensuring that it's using our custom
/// `UIApplication` and `UIApplicationDelegate` classes. /// `UIApplication` and `UIApplicationDelegate` classes.
pub fn run(&self) { pub fn run(&self) {
@ -149,12 +152,14 @@ impl<T, W, F> App<T, W, F> {
let c_args = args.iter().map(|arg| arg.as_ptr()).collect::<Vec<*const c_char>>(); let c_args = args.iter().map(|arg| arg.as_ptr()).collect::<Vec<*const c_char>>();
let mut s = NSString::new("RSTApplication_UIApplication"); let cls = register_app_class();
let mut s2 = NSString::new("RSTAppDelegate_NSObject"); let dl = register_app_delegate_class::<T>();
let cls_name: id = unsafe { msg_send![cls, className] };
let dl_name: id = unsafe { msg_send![dl, className] };
unsafe { unsafe {
println!("RUNNING?!"); UIApplicationMain(c_args.len() as c_int, c_args.as_ptr(), cls_name, dl_name);
UIApplicationMain(c_args.len() as c_int, c_args.as_ptr(), s.into(), s2.into());
} }
//self.pool.drain(); //self.pool.drain();

View file

@ -2,7 +2,8 @@ use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use objc_id::Id; use objc_id::Id;
use crate::foundation::{id, ClassMap, NSString}; use crate::foundation::{id, load_or_register_class, ClassMap, NSString};
use crate::uikit::scene::SessionRole; use crate::uikit::scene::SessionRole;
/// A wrapper for UISceneConfiguration. /// A wrapper for UISceneConfiguration.
@ -26,7 +27,10 @@ impl SceneConfig {
let config: id = msg_send![cls, configurationWithName:name sessionRole:role]; let config: id = msg_send![cls, configurationWithName:name sessionRole:role];
let _: () = msg_send![config, setSceneClass: class!(UIWindowScene)]; let _: () = msg_send![config, setSceneClass: class!(UIWindowScene)];
let _: () = msg_send![config, setDelegateClass: delegate_class];
// TODO: use register_window_scene_delegate_class rather than load_or_register_class.
let window_delegate = load_or_register_class("UIResponder", "RSTWindowSceneDelegate", |decl| unsafe {});
let _: () = msg_send![config, setDelegateClass: window_delegate];
Id::from_ptr(config) Id::from_ptr(config)
}) })

View file

@ -77,7 +77,9 @@ mod splitviewcontroller;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub use splitviewcontroller::SplitViewController; pub use splitviewcontroller::SplitViewController;
#[cfg(feature = "appkit")]
mod popover; mod popover;
#[cfg(feature = "appkit")]
pub use popover::*; pub use popover::*;
mod traits; mod traits;
pub use traits::ViewDelegate; pub use traits::ViewDelegate;
@ -398,3 +400,10 @@ impl LayerContentsRedrawPolicy {
} }
} }
} }
#[test]
fn test_view() {
let view = View::new();
let _clone = view.clone_as_handle();
view.set_background_color(Color::SystemGreen);
}