#[cfg(not(target_os = "macos"))]
fn main() {
eprintln!("this example needs macOS");
}
#[cfg(target_os = "macos")]
fn main() {
app::run();
}
#[cfg(target_os = "macos")]
mod app {
use std::time::Instant;
use denise::{InputEvent, Radius, Rect, Role, Size, Surface, Theme};
use denise_macos::{DeniseView, ViewDelegate, ViewSurface};
use denise_ui::widgets::{Align, Button, Label, Panel, TextInput};
use denise_ui::{NodeId, Ui};
use objc2::rc::Retained;
use objc2::{MainThreadOnly, sel};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSBackingStoreType, NSWindow,
NSWindowStyleMask,
};
use objc2_foundation::{MainThreadMarker, NSPoint, NSRect, NSSize, NSString, NSTimer};
const WIDTH: f64 = 520.0;
const HEIGHT: f64 = 360.0;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Msg {
Save,
Reset,
}
struct Demo {
ui: Ui<Msg>,
status: NodeId,
name: NodeId,
started: Instant,
saves: u32,
}
impl Demo {
fn new(size: Size) -> Self {
let mut ui: Ui<Msg> = Ui::new(size, Theme::DARK);
ui.show_cursor(false);
let root = ui.root();
let scale = (size.height as f32 / HEIGHT as f32).max(1.0);
let px = |points: f32| (points * scale) as i32;
let pt = |points: f32| (points * scale) as u16;
let card = ui
.add(
root,
Panel::default().with_radius(Radius::Box),
Rect::new(px(28.0), px(28.0), px(464.0), px(304.0)),
)
.expect("card");
ui.add(
card,
Label::new("Operatørpanel").with_size(pt(22.0)),
Rect::new(px(24.0), px(20.0), px(416.0), px(30.0)),
)
.expect("title");
ui.add(
card,
Label::new("Navn").with_size(pt(13.0)),
Rect::new(px(24.0), px(70.0), px(416.0), px(22.0)),
)
.expect("name label");
let name = ui
.add(
card,
TextInput::<Msg>::new()
.with_placeholder("Ola Nordmann")
.with_size(pt(15.0))
.with_submit(Msg::Save),
Rect::new(px(24.0), px(96.0), px(416.0), px(40.0)),
)
.expect("name");
ui.add(
card,
Button::new("Lagre", Msg::Save).with_size(pt(15.0)),
Rect::new(px(24.0), px(160.0), px(200.0), px(44.0)),
)
.expect("save");
ui.add(
card,
Button::new("Nullstill", Msg::Reset)
.with_role(Role::Neutral)
.with_size(pt(15.0)),
Rect::new(px(240.0), px(160.0), px(200.0), px(44.0)),
)
.expect("reset");
let status = ui
.add(
card,
Label::new("Tab, Enter, click. Try æ ø å.")
.with_role(Role::Base300)
.with_size(pt(13.0))
.with_align(Align::Center, Align::Center),
Rect::new(px(24.0), px(220.0), px(416.0), px(24.0)),
)
.expect("status");
ui.focus(Some(name));
Self {
ui,
status,
name,
started: Instant::now(),
saves: 0,
}
}
fn set_status(&mut self, text: &str) {
if let Some(label) = self.ui.widget_mut::<Label>(self.status) {
label.set_text(text);
}
}
fn now_ms(&self) -> u64 {
self.started.elapsed().as_millis() as u64
}
}
impl ViewDelegate for Demo {
fn update(
&mut self,
surface: &mut ViewSurface,
events: &[InputEvent],
damage: &mut Vec<Rect>,
) {
if surface.size() != self.ui.size() {
self.ui = Demo::new(surface.size()).ui;
self.ui.invalidate_all();
}
self.ui.handle(events);
self.ui.tick(self.now_ms());
let messages: Vec<Msg> = self.ui.drain_messages().collect();
for message in messages {
match message {
Msg::Save => {
self.saves += 1;
let who = self
.ui
.widget::<TextInput<Msg>>(self.name)
.map(|f| f.text().to_owned())
.unwrap_or_default();
let who = if who.is_empty() { "ingen" } else { &who };
let text = format!("Lagret {} ({who})", self.saves);
self.set_status(&text);
}
Msg::Reset => {
if let Some(field) = self.ui.widget_mut::<TextInput<Msg>>(self.name) {
field.clear();
}
self.set_status("Nullstilt.");
}
}
}
if !self.ui.needs_paint() {
return;
}
match surface.acquire() {
Ok(mut frame) => {
self.ui.paint(&mut frame);
drop(frame);
damage.extend_from_slice(self.ui.damage());
self.ui.presented();
}
Err(e) => eprintln!("acquire: {e}"),
}
}
fn next_wake_ms(&self) -> Option<u64> {
self.ui.next_wake_ms()
}
}
pub fn run() {
let mut args = std::env::args().skip(1);
if args.next().as_deref() == Some("snapshot") {
let path = args.next().unwrap_or_else(|| "embed.ppm".to_owned());
snapshot(&path);
return;
}
let mtm = MainThreadMarker::new().expect("main thread");
let app = NSApplication::sharedApplication(mtm);
app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
let frame = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(WIDTH, HEIGHT));
let view = DeniseView::new(
mtm,
frame,
1.0,
Box::new(Demo::new(Size::new(WIDTH as u32, HEIGHT as u32))),
)
.expect("view");
let window = new_window(mtm, frame);
window.setContentView(Some(&view));
window.setTitle(&NSString::from_str("Denise — embedded NSView"));
window.makeKeyAndOrderFront(None);
window.makeFirstResponder(Some(&view));
view.sync_surface_size();
view.update();
unsafe {
NSTimer::scheduledTimerWithTimeInterval_target_selector_userInfo_repeats(
0.02,
&view,
sel!(deniseTick:),
None,
true,
);
}
eprintln!("running — close the window to quit");
app.run();
}
fn snapshot(path: &str) {
let mtm = MainThreadMarker::new().expect("main thread");
let frame = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(WIDTH, HEIGHT));
let size = Size::new(WIDTH as u32, HEIGHT as u32);
let view = DeniseView::new(mtm, frame, 1.0, Box::new(Demo::new(size))).expect("view");
view.update();
let rep = view
.bitmapImageRepForCachingDisplayInRect(frame)
.expect("bitmap rep");
view.cacheDisplayInRect_toBitmapImageRep(frame, &rep);
let data = rep.bitmapData();
assert!(!data.is_null(), "no bitmap data");
let width = rep.pixelsWide() as usize;
let height = rep.pixelsHigh() as usize;
let stride = rep.bytesPerRow() as usize;
let bytes_per_pixel = (rep.bitsPerPixel() / 8) as usize;
let pixels = unsafe { std::slice::from_raw_parts(data, stride * height) };
let mut out = format!("P6\n{width} {height}\n255\n").into_bytes();
for y in 0..height {
for x in 0..width {
let at = y * stride + x * bytes_per_pixel;
out.extend_from_slice(&pixels[at..at + 3]);
}
}
std::fs::write(path, out).expect("write ppm");
eprintln!("wrote {path} ({width}x{height})");
}
fn new_window(mtm: MainThreadMarker, frame: NSRect) -> Retained<NSWindow> {
let style = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable
| NSWindowStyleMask::Resizable;
unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
NSWindow::alloc(mtm),
frame,
style,
NSBackingStoreType::Buffered,
false,
)
}
}
}