ferroui-desktop 0.3.0

The desktop entry point of FerroUI: platform detection with the default render and text shaping backends
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
//! The smallest desktop application: an application class whose main window
//! holds a border with a text block and a button, started through the
//! application builder with the classic desktop lifetime.
//!
//! ```text
//! cargo run -p ferroui-desktop --example hello_window
//! FERROUI_SMOKE_EXIT_MS=2000 cargo run -p ferroui-desktop --example hello_window
//! ```
//!
//! With `FERROUI_SMOKE_EXIT_MS=<n>` the main window is closed after `n`
//! milliseconds, which ends the main loop; the process exits with the exit
//! code the lifetime returns.
//!
//! For runs without a person on Windows: `FERROUI_SMOKE_RENDERING=software`
//! or `angle` asks for one rendering mode without a fallback,
//! `FERROUI_SMOKE_RENDER_ON_UI_THREAD=1` renders on the UI thread,
//! `FERROUI_SMOKE_LOG=1` writes the warnings and errors the framework logs
//! to the error stream, and a fatal exception of the system is reported
//! with its code before the process ends.
//!
//! The window content is painted by the compositing renderer through the
//! compositor the platform registers.

use ferroui_base::layout::{HorizontalAlignment, VerticalAlignment};
use ferroui_base::media::immutable::ImmutableSolidColorBrush;
use ferroui_base::media::Color;
use ferroui_base::threading::{DispatcherPriority, DispatcherTimer};
use ferroui_base::{
    ferro_class, ferro_impl_classes, instantiate, CornerRadius, FerroObjectImpl,
    Ref, Thickness,
};
use ferroui_controls::{
    AppBuilder, Application, ApplicationImpl, ApplicationImplExt, Border, Button, Control, NativeMenu, NativeMenuItem,
    NewApplication, StackPanel, TextBlock, TrayIcon, TrayIcons, Window,
};
use ferroui_desktop::AppBuilderDesktopExtensions;
use std::rc::Rc;
use std::time::Duration;

#[repr(C)]
pub struct App {
    base: Application,
}

ferro_class!(App: Application);
ferro_impl_classes!(App: FerroObjectImpl);

impl NewApplication for App {
    fn new_application() -> Ref<Self> {
        instantiate(Self { base: Application::construct() })
    }
}

impl ApplicationImpl for App {
    fn initialize(this: &Self) {
        Self::parent_initialize(this);
        this.set_name(Some("FerroUI hello window".to_string()));
    }

    fn on_framework_initialization_completed(this: &Self) {
        let lifetime = this.application_lifetime();
        if let Some(desktop) =
            lifetime.as_ref().and_then(|lifetime| lifetime.as_classic_desktop_style_application_lifetime())
        {
            let window = create_main_window();
            if std::env::var_os("FERROUI_SMOKE_MENU").is_some() {
                smoke_menu(&window);
            }
            desktop.set_main_window(Some(window));
        }

        if std::env::var_os("FERROUI_SMOKE_TRAY").is_some() {
            smoke_tray(this);
        }

        Self::parent_on_framework_initialization_completed(this);
    }
}

fn create_main_window() -> Ref<Window> {
    let text = TextBlock::new();
    text.set_text(Some("Hello from FerroUI"));
    text.set_font_size(24.0);

    let button = Button::new();
    button.set_content(Some(Rc::new("Click me".to_string())));
    button.click(|_, _| println!("Button clicked"));

    let panel = StackPanel::new();
    panel.set_spacing(12.0);
    panel.set_horizontal_alignment(HorizontalAlignment::Center);
    panel.set_vertical_alignment(VerticalAlignment::Center);
    panel.children().add(text);
    panel.children().add(button);

    let border = Border::new();
    border.set_background(Some(Rc::new(ImmutableSolidColorBrush::new(Color::from_rgb(0xee, 0xf2, 0xff)))));
    border.set_corner_radius(CornerRadius::uniform(12.0));
    border.set_padding(Thickness::uniform(24.0));
    border.set_margin(Thickness::uniform(16.0));
    border.set_child(panel);

    let window = Window::new();
    window.set_title(Some("FerroUI hello window".to_string()));
    window.set_width(640.0);
    window.set_height(400.0);
    window.set_content(Some(Control::boxed(border)));

    window.opened(|| println!("Window opened"));
    window.closed(|| println!("Window closed"));

    if let Some(ms) = std::env::var("FERROUI_SMOKE_EXIT_MS").ok().and_then(|v| v.parse::<u64>().ok()) {
        println!("Will close the main window after {ms} ms");
        let window = window.clone();
        // The timer stops itself after its only tick.
        let _timer = DispatcherTimer::run_once(
            move || {
                println!("Timer fired: closing the main window");
                window.close();
            },
            Duration::from_millis(ms),
            DispatcherPriority::NORMAL,
        );
    }

    window
}

fn logging_item(header: &str) -> Ref<NativeMenuItem> {
    let item = NativeMenuItem::with_header(header);
    let name = header.to_string();
    // The subscription lives as long as the item.
    let _ = item.click(move |_| println!("Menu item clicked: {name}"));
    item
}

/// Gives the window a native menu and, once it is up, inspects and drives
/// the system's main menu.
fn smoke_menu(window: &Ref<Window>) {
    let menu = NativeMenu::new();
    menu.add(logging_item("Smoke _A"));
    menu.add(logging_item("Smoke B"));
    let more = NativeMenuItem::with_header("More");
    let sub_menu = NativeMenu::new();
    sub_menu.add(logging_item("Nested"));
    more.set_menu(Some(sub_menu));
    menu.add(more);
    NativeMenu::set_menu(window, Some(menu));
    println!("Native menu set on the window");

    let _timer = DispatcherTimer::run_once(
        || {
            #[cfg(target_os = "macos")]
            main_menu_probe::dump_and_click(&["Smoke A", "Smoke B"], ("More", "Nested"));
        },
        Duration::from_millis(800),
        DispatcherPriority::NORMAL,
    );
}

/// Creates a tray icon with a menu and disposes it a little later.
fn smoke_tray(application: &Application) {
    let menu = NativeMenu::new();
    menu.add(logging_item("Tray item"));

    let icon = TrayIcon::new();
    icon.set_tool_tip_text(Some("FerroUI hello window".to_string()));
    icon.set_menu(Some(menu));
    let _ = icon.clicked(|_| println!("Tray icon clicked"));

    let icons = TrayIcons::new();
    icons.add(icon.clone());
    TrayIcon::set_icons(application, Some(icons));
    println!("Tray icon created (native menu exporter: {})", icon.native_menu_exporter().is_some());

    // A second item added after the export goes through the queued reset.
    if let Some(menu) = icon.menu() {
        menu.add(logging_item("Tray item added later"));
    }

    let _timer = DispatcherTimer::run_once(
        move || {
            icon.dispose();
            println!("Tray icon disposed");
        },
        Duration::from_millis(1200),
        DispatcherPriority::NORMAL,
    );
}

/// Reads the main menu AppKit holds for this application and performs item
/// actions, all in-process. The native interface of the backend has no
/// query for this, so the probe talks to AppKit directly.
#[cfg(target_os = "macos")]
mod main_menu_probe {
    use std::ffi::{c_char, c_void, CStr};

    type Id = *mut c_void;
    type Sel = *mut c_void;

    #[link(name = "objc")]
    extern "C" {
        fn sel_registerName(name: *const c_char) -> Sel;
        fn objc_getClass(name: *const c_char) -> Id;
        fn objc_msgSend();
    }

    fn sel(name: &CStr) -> Sel {
        // SAFETY: `name` is NUL-terminated.
        unsafe { sel_registerName(name.as_ptr()) }
    }

    // SAFETY (all `send_*`): `objc_msgSend` is called through a pointer
    // with the C signature of the method being sent; receivers are live
    // objects or null (messages to nil return zero).
    unsafe fn send_id(receiver: Id, selector: Sel) -> Id {
        let f: unsafe extern "C" fn(Id, Sel) -> Id = std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
        f(receiver, selector)
    }

    unsafe fn send_isize(receiver: Id, selector: Sel) -> isize {
        let f: unsafe extern "C" fn(Id, Sel) -> isize = std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
        f(receiver, selector)
    }

    unsafe fn send_bool(receiver: Id, selector: Sel) -> bool {
        let f: unsafe extern "C" fn(Id, Sel) -> bool = std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
        f(receiver, selector)
    }

    unsafe fn send_index(receiver: Id, selector: Sel, index: isize) -> Id {
        let f: unsafe extern "C" fn(Id, Sel, isize) -> Id =
            std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
        f(receiver, selector, index)
    }

    unsafe fn title(item: Id) -> String {
        if send_bool(item, sel(c"isSeparatorItem")) {
            return "-".to_string();
        }
        let ns_string = send_id(item, sel(c"title"));
        let utf8 = send_id(ns_string, sel(c"UTF8String")) as *const c_char;
        if utf8.is_null() {
            String::new()
        } else {
            CStr::from_ptr(utf8).to_string_lossy().into_owned()
        }
    }

    unsafe fn items(menu: Id) -> Vec<(String, Id)> {
        let count = send_isize(menu, sel(c"numberOfItems"));
        (0..count)
            .map(|index| {
                let item = send_index(menu, sel(c"itemAtIndex:"), index);
                (title(item), item)
            })
            .collect()
    }

    unsafe fn perform(menu: Id, wanted: &str) {
        match items(menu).iter().position(|(title, _)| title == wanted) {
            Some(index) => {
                println!("Performing the action of {wanted:?}");
                send_index(menu, sel(c"performActionForItemAtIndex:"), index as isize);
            }
            None => println!("Main menu has no item {wanted:?}"),
        }
    }

    /// Prints the menu bar and its submenus, then performs the actions of
    /// the named menu bar items and of one nested item.
    pub fn dump_and_click(top_level: &[&str], nested: (&str, &str)) {
        // SAFETY: plain AppKit calls on the main thread (see `send_*`).
        unsafe {
            let app = send_id(objc_getClass(c"NSApplication".as_ptr()), sel(c"sharedApplication"));
            let main_menu = send_id(app, sel(c"mainMenu"));
            if main_menu.is_null() {
                println!("Main menu: none");
                return;
            }

            let menu_bar = items(main_menu);
            for (index, (title, item)) in menu_bar.iter().enumerate() {
                let sub_menu = send_id(*item, sel(c"submenu"));
                let children: Vec<String> = items(sub_menu).into_iter().map(|(title, _)| title).collect();
                println!("Main menu [{index}] {title:?} -> {children:?}");
            }

            for wanted in top_level {
                perform(main_menu, wanted);
            }

            if let Some((_, parent)) = menu_bar.iter().find(|(title, _)| title == nested.0) {
                perform(send_id(*parent, sel(c"submenu")), nested.1);
            } else {
                println!("Main menu has no item {:?}", nested.0);
            }
        }
    }
}

/// On Windows, a fatal exception of the system (an access violation in a
/// library, for one) ends the process without a word, where a panic says
/// what failed. This prints the code and the address of such an exception
/// before the system goes on to end the process, so a run without a person
/// leaves something to read.
#[cfg(all(windows, not(target_arch = "x86")))]
mod fatal_exceptions {
    use std::ffi::c_void;

    #[repr(C)]
    struct ExceptionRecord {
        code: u32,
        flags: u32,
        record: *mut ExceptionRecord,
        address: *mut c_void,
    }

    #[repr(C)]
    struct ExceptionPointers {
        record: *mut ExceptionRecord,
        context: *mut c_void,
    }

    #[link(name = "kernel32", kind = "raw-dylib")]
    extern "system" {
        fn AddVectoredExceptionHandler(
            first: u32,
            handler: unsafe extern "system" fn(info: *mut ExceptionPointers) -> i32,
        ) -> *mut c_void;
    }

    /// `EXCEPTION_CONTINUE_SEARCH`: the exception goes on to the handlers
    /// it would have reached without this one.
    const CONTINUE_SEARCH: i32 = 0;

    unsafe extern "system" fn handler(info: *mut ExceptionPointers) -> i32 {
        // SAFETY: the system passes the record of the exception that is
        // being dispatched, valid for the length of this call.
        let (code, address) = unsafe {
            let record = (*info).record;
            ((*record).code, (*record).address)
        };
        // The errors of the system (the severity bits 11); the exceptions a
        // language runtime throws and catches, and debug output, are not.
        if code >= 0xC000_0000 {
            let thread = std::thread::current();
            eprintln!(
                "Fatal exception {code:#010x} at {address:p} on thread {:?} ({:?})",
                thread.name().unwrap_or("unnamed"),
                thread.id()
            );
        }
        CONTINUE_SEARCH
    }

    pub fn install() {
        // SAFETY: the handler is a function of this program with the
        // signature the system calls, and it stays for the life of the
        // process.
        unsafe {
            AddVectoredExceptionHandler(1, handler);
        }
    }
}

/// The options of a smoke run on Windows, from the environment:
/// `FERROUI_SMOKE_RENDERING=software|angle|wgl` asks for that rendering mode
/// alone (no fallback, so a mode that does not work fails the run), and
/// `FERROUI_SMOKE_RENDER_ON_UI_THREAD=1` renders on the UI thread.
#[cfg(windows)]
fn smoke_platform_options(builder: AppBuilder) -> AppBuilder {
    use ferroui_win32::{Win32PlatformOptions, Win32RenderingMode};

    let rendering = std::env::var("FERROUI_SMOKE_RENDERING").ok();
    let on_ui_thread = std::env::var_os("FERROUI_SMOKE_RENDER_ON_UI_THREAD").is_some();
    if rendering.is_none() && !on_ui_thread {
        return builder;
    }
    let mut options = Win32PlatformOptions { should_render_on_ui_thread: on_ui_thread, ..Default::default() };
    match rendering.as_deref() {
        Some("software") => options.rendering_mode = vec![Win32RenderingMode::Software],
        Some("angle") => {
            options.rendering_mode = vec![Win32RenderingMode::AngleEgl];
            options.composition_mode = vec![smoke_composition_mode()];
        }
        // The OpenGL of the system, with software rendering behind it: a system without a
        // driver for it (the generic implementation is OpenGL 1.1) passes the mode over.
        Some("wgl") => options.rendering_mode = vec![Win32RenderingMode::Wgl, Win32RenderingMode::Software],
        Some(other) => println!("FERROUI_SMOKE_RENDERING: unknown mode {other:?} (software, angle, wgl); the default order is used"),
        None => {}
    }
    println!(
        "Rendering modes: {:?}; composition modes: {:?}; render on the UI thread: {on_ui_thread}",
        options.rendering_mode, options.composition_mode
    );
    builder.with(Rc::new(options))
}

/// The composition mode of a smoke run through ANGLE, without a fallback:
/// `FERROUI_SMOKE_COMPOSITION=redirection|dcomp|winui|dxgi`; the redirection surface
/// of the window when the variable is not set.
#[cfg(windows)]
fn smoke_composition_mode() -> ferroui_win32::Win32CompositionMode {
    use ferroui_win32::Win32CompositionMode;

    match std::env::var("FERROUI_SMOKE_COMPOSITION").ok().as_deref() {
        None | Some("redirection") => Win32CompositionMode::RedirectionSurface,
        Some("dcomp") => Win32CompositionMode::DirectComposition,
        Some("winui") => Win32CompositionMode::WinUIComposition,
        Some("dxgi") => Win32CompositionMode::LowLatencyDxgiSwapChain,
        Some(other) => panic!("FERROUI_SMOKE_COMPOSITION: unknown mode {other:?} (redirection, dcomp, winui, dxgi)"),
    }
}

#[cfg(not(windows))]
fn smoke_platform_options(builder: AppBuilder) -> AppBuilder {
    builder
}

fn main() -> std::process::ExitCode {
    #[cfg(all(windows, not(target_arch = "x86")))]
    fatal_exceptions::install();

    let args: Vec<String> = std::env::args().skip(1).collect();
    let mut builder = smoke_platform_options(AppBuilder::configure::<App>());
    // `FERROUI_SMOKE_LOG=1`: what the framework logs at the level of
    // warnings and above, to the error stream.
    if std::env::var_os("FERROUI_SMOKE_LOG").is_some() {
        builder = builder.log_to_text_writer(std::io::stderr(), ferroui_base::logging::LogEventLevel::Warning, &[]);
    }
    let exit_code = builder.use_platform_detect().start_with_classic_desktop_lifetime(&args);
    println!("start_with_classic_desktop_lifetime returned {exit_code}");
    std::process::ExitCode::from(exit_code as u8)
}