add support for popovers

This commit is contained in:
Benedikt Terhechte 2023-03-03 12:05:51 +01:00
parent 68da052a8f
commit 7e724934a5
6 changed files with 325 additions and 3 deletions

195
examples/popover.rs Normal file
View file

@ -0,0 +1,195 @@
//! This example showcases how to use a `Popover`.
//! This requires multiple types:
//! - A Window with a Controller / View
//! - A Popover
//! - Another Controller / View
use cacao::appkit::menu::{Menu, MenuItem};
use cacao::appkit::window::{Window, WindowConfig, WindowController, WindowDelegate};
use cacao::appkit::{App, AppDelegate};
use cacao::button::Button;
use cacao::geometry::{Edge, Rect};
use cacao::layout::{Layout, LayoutConstraint};
use cacao::notification_center::Dispatcher;
use cacao::text::{Font, Label};
use cacao::view::{Popover, PopoverConfig, View, ViewController, ViewDelegate};
struct BasicApp {
window: WindowController<MyWindow>,
}
impl AppDelegate for BasicApp {
fn did_finish_launching(&self) {
App::set_menu(vec![
Menu::new(
"",
vec![
MenuItem::Services,
MenuItem::Separator,
MenuItem::Hide,
MenuItem::HideOthers,
MenuItem::ShowAll,
MenuItem::Separator,
MenuItem::Quit,
],
),
Menu::new("File", vec![MenuItem::CloseWindow]),
Menu::new("View", vec![MenuItem::EnterFullScreen]),
Menu::new(
"Window",
vec![
MenuItem::Minimize,
MenuItem::Zoom,
MenuItem::Separator,
MenuItem::new("Bring All to Front"),
],
),
]);
App::activate();
self.window.show();
}
fn should_terminate_after_last_window_closed(&self) -> bool {
true
}
}
#[derive(Default)]
struct MyWindow {
controller: Option<ViewController<PopoverExampleContentView>>,
}
impl WindowDelegate for MyWindow {
const NAME: &'static str = "MyWindow";
fn did_load(&mut self, window: Window) {
window.set_minimum_content_size(400., 400.);
window.set_title("A Basic Window!?");
let view = PopoverExampleContentView::new();
let controller = ViewController::new(view);
window.set_content_view_controller(&controller);
self.controller = Some(controller);
}
fn will_close(&self) {
println!("Closing now!");
}
}
impl MyWindow {
pub fn on_message(&self, message: Msg) {
if let Some(delegate) = self.controller.as_ref().map(|e| &e.view).and_then(|v| v.delegate.as_ref()) {
delegate.on_message(message);
}
}
}
fn main() {
App::new(
"com.test.window-delegate",
BasicApp {
window: WindowController::with(WindowConfig::default(), MyWindow::default()),
},
)
.run();
}
#[derive(Clone, Debug)]
pub enum Msg {
Click,
}
#[derive(Debug, Default)]
struct PopoverExampleContentView {
view: Option<View>,
button: Option<Button>,
popover: Option<Popover<PopoverExampleContentViewController>>,
}
impl PopoverExampleContentView {
pub fn new() -> Self {
Self {
view: None,
button: None,
popover: None,
}
}
pub fn on_message(&self, message: Msg) {
match message {
Msg::Click => {
let Some(ref popover) = self.popover else { return };
let Some(ref button) = self.button else { return };
popover.show_popover(Rect::zero(), button, Edge::MaxY);
},
}
}
}
impl ViewDelegate for PopoverExampleContentView {
const NAME: &'static str = "PopoverExampleContentView";
fn did_load(&mut self, view: cacao::view::View) {
let mut button = Button::new("Show");
button.set_action(|| dispatch_ui(Msg::Click));
let controller = PopoverExampleContentViewController::new();
let config = PopoverConfig {
animates: false,
..Default::default()
};
let popover = Popover::new(controller, config);
self.popover = Some(popover);
view.add_subview(&button);
LayoutConstraint::activate(&[
button.center_x.constraint_equal_to(&view.center_x),
button.center_y.constraint_equal_to(&view.center_y),
]);
self.view = Some(view);
self.button = Some(button);
}
}
pub fn dispatch_ui(message: Msg) {
println!("Dispatching UI message: {:?}", message);
App::<BasicApp, Msg>::dispatch_main(message);
}
impl Dispatcher for BasicApp {
type Message = Msg;
// Handles a message that came over on the main (UI) thread.
fn on_ui_message(&self, message: Self::Message) {
if let Some(d) = &self.window.window.delegate {
d.on_message(message)
}
}
}
#[derive(Debug)]
struct PopoverExampleContentViewController {
pub label: Label,
}
impl PopoverExampleContentViewController {
fn new() -> Self {
let label = Label::new();
let font = Font::system(20.);
label.set_font(&font);
label.set_text("Hello");
Self { label }
}
}
impl ViewDelegate for PopoverExampleContentViewController {
const NAME: &'static str = "PopoverExampleContentViewController";
fn did_load(&mut self, view: View) {
view.add_subview(&self.label);
}
}

View file

@ -63,13 +63,15 @@ pub use enums::*;
mod traits; mod traits;
pub use traits::AppDelegate; pub use traits::AppDelegate;
use super::window::Window;
pub(crate) static APP_PTR: &str = "rstAppPtr"; pub(crate) static APP_PTR: &str = "rstAppPtr";
/// A handler to make some boilerplate less annoying. /// A handler to make some boilerplate less annoying.
#[inline] #[inline]
fn shared_application<F: Fn(id)>(handler: F) { fn shared_application<T, F: Fn(id) -> T>(handler: F) -> T {
let app: id = unsafe { msg_send![register_app_class(), sharedApplication] }; let app: id = unsafe { msg_send![register_app_class(), sharedApplication] };
handler(app); handler(app)
} }
/// A wrapper for `NSApplication` in AppKit/Cocoa, and `UIApplication` in UIKit/Cocoa Touch. /// A wrapper for `NSApplication` in AppKit/Cocoa, and `UIApplication` in UIKit/Cocoa Touch.
@ -298,6 +300,13 @@ impl App {
}); });
} }
pub fn main_window() -> Window {
shared_application(|app| unsafe {
let window: id = msg_send![app, mainWindow];
Window::existing(window)
})
}
/// Terminates the application, firing the requisite cleanup delegate methods in the process. /// Terminates the application, firing the requisite cleanup delegate methods in the process.
/// ///
/// This is typically called when the user chooses to quit via the App menu. /// This is typically called when the user chooses to quit via the App menu.

View file

@ -23,6 +23,7 @@ use crate::foundation::{id, nil, to_bool, NSInteger, NSString, NSUInteger, NO, Y
use crate::layout::Layout; use crate::layout::Layout;
use crate::objc_access::ObjcAccess; use crate::objc_access::ObjcAccess;
use crate::utils::{os, Controller}; use crate::utils::{os, Controller};
use crate::view::View;
mod class; mod class;
use class::register_window_class_with_delegate; use class::register_window_class_with_delegate;
@ -109,7 +110,14 @@ impl Window {
Window { Window {
objc: objc, objc: objc,
delegate: None delegate: None,
}
}
pub(crate) unsafe fn existing(window: *mut Object) -> Window {
Window {
objc: ShareId::from_ptr(window),
delegate: None,
} }
} }
} }
@ -291,6 +299,11 @@ impl<T> Window<T> {
} }
} }
/// Return the objc ContentView from the window
pub(crate) unsafe fn content_view(&self) -> id {
let id: *mut Object = msg_send![&*self.objc, contentView];
id
}
/// Given a view, sets it as the content view for this window. /// Given a view, sets it as the content view for this window.
pub fn set_content_view<L: Layout + 'static>(&self, view: &L) { pub fn set_content_view<L: Layout + 'static>(&self, view: &L) {
view.with_backing_obj_mut(|backing_node| unsafe { view.with_backing_obj_mut(|backing_node| unsafe {

View file

@ -41,6 +41,14 @@ impl Rect {
} }
} }
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[repr(u32)]
pub enum Edge {
MinX = 0,
MinY = 1,
MaxX = 2,
MaxY = 3,
}
impl From<Rect> for CGRect { impl From<Rect> for CGRect {
fn from(rect: Rect) -> CGRect { fn from(rect: Rect) -> CGRect {
CGRect::new(&CGPoint::new(rect.left, rect.top), &CGSize::new(rect.width, rect.height)) CGRect::new(&CGPoint::new(rect.left, rect.top), &CGSize::new(rect.width, rect.height))

View file

@ -77,6 +77,8 @@ mod splitviewcontroller;
#[cfg(feature = "appkit")] #[cfg(feature = "appkit")]
pub use splitviewcontroller::SplitViewController; pub use splitviewcontroller::SplitViewController;
mod popover;
pub use popover::*;
mod traits; mod traits;
pub use traits::ViewDelegate; pub use traits::ViewDelegate;

95
src/view/popover/mod.rs Normal file
View file

@ -0,0 +1,95 @@
use core_graphics::geometry::CGRect;
use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl};
use objc_id::ShareId;
use crate::appkit::toolbar::ToolbarItem;
use crate::appkit::window::Window;
use crate::appkit::App;
use crate::foundation::{id, nil, NSString};
use crate::geometry::{Edge, Rect};
use crate::layout::Layout;
use crate::utils::{os, CGSize, Controller};
use crate::view::{View, ViewController, ViewDelegate};
#[derive(Debug, Eq, PartialEq)]
#[repr(i64)]
pub enum PopoverBehaviour {
/// Your application assumes responsibility for closing the popover.
ApplicationDefined = 0,
/// The system will close the popover when the user interacts with a user interface element outside the popover.
Transient = 1,
/// The system will close the popover when the user interacts with user interface elements in the window containing the popover's positioning view.
Semitransient = 2,
}
#[derive(Debug)]
pub struct PopoverConfig {
pub content_size: CGSize,
pub animates: bool,
pub behaviour: PopoverBehaviour,
}
impl Default for PopoverConfig {
fn default() -> Self {
Self {
content_size: CGSize {
width: 320.0,
height: 320.0,
},
animates: true,
behaviour: PopoverBehaviour::Transient,
}
}
}
#[derive(Debug)]
pub struct Popover<Content> {
/// A reference to the underlying Objective-C NSPopover
pub objc: ShareId<Object>,
/// The wrapped ViewController.
pub view_controller: ViewController<Content>,
}
impl<Content> Popover<Content>
where
Content: ViewDelegate + 'static,
{
pub fn new(content: Content, config: PopoverConfig) -> Self {
let view_controller = ViewController::new(content);
let objc = unsafe {
let pop: id = msg_send![class!(NSPopover), new];
let _: () = msg_send![pop, setContentSize: config.content_size];
let _: () = msg_send![pop, setBehavior: config.behaviour as i64];
let _: () = msg_send![pop, setAnimates: config.animates];
let _: () = msg_send![pop, setContentViewController: &*view_controller.objc];
ShareId::from_ptr(pop)
};
Popover { objc, view_controller }
}
}
impl<Content> Popover<Content> {
/// Show a popover relative to a view
pub fn show_popover<V: Layout>(&self, relative_to: Rect, view: &V, edge: Edge) {
let rect: CGRect = relative_to.into();
unsafe {
view.with_backing_obj_mut(|obj| {
let _: () = msg_send![&*self.objc, showRelativeToRect:rect ofView: &*obj preferredEdge: edge as u32];
});
}
}
/// Show the popover relative to the content view of the main window
pub fn show_popover_main(&self, rect: Rect, edge: Edge) {
let window = App::main_window();
unsafe {
let content_view = window.content_view();
let rect: CGRect = rect.into();
let _: () = msg_send![&*self.objc, showRelativeToRect:rect ofView: content_view preferredEdge: edge as u32];
}
}
}