denise_win32/lib.rs
1//! A Denise panel in a Win32 child window.
2//!
3//! The oldest of the reasons this project exists. CoreCanvas shipped inside
4//! Windows applications that were not going to be rewritten — MFC, WinForms, VB6
5//! through the ActiveX shim — and the thing they all need is a control they can
6//! put in a dialog next to the ones they already have.
7//!
8//! So: [`DeniseControl`] registers a window class and creates a child `HWND`. The
9//! host owns the window, the message loop and the parent; Denise owns the pixels
10//! inside one rectangle and nothing else.
11//!
12//! # What is different from the bare-metal backends
13//!
14//! - **The host owns the message loop.** There is no `run` function here. Windows
15//! decides when to paint and Denise answers, which is the opposite of the DRM
16//! backend where Denise decides and the display follows.
17//! - **Damage is real bandwidth.** `BitBlt` moves only the rectangles it is given,
18//! unlike a DRM page flip where the whole buffer goes regardless. The tree's
19//! damage is worth passing on rather than rounding up to the client area.
20//! - **The pixel format already matches.** A 32-bit `BI_RGB` DIB section is
21//! `0xAARRGGBB` in a little-endian `DWORD`, which is exactly what the rasteriser
22//! writes. No conversion pass anywhere.
23//! - **There is already a cursor.** Windows draws one, so the composited sprite
24//! stays off: `Ui::show_cursor(false)`, which since M5 is a decision that sticks
25//! rather than one the next mouse move overrides.
26//!
27//! # No console window
28//!
29//! A Rust binary defaults to the **console** subsystem, so Windows allocates a
30//! console and it sits behind the app looking like a mistake. The `.exe` that
31//! hosts the control — not this library — decides that, with a crate-level
32//! attribute:
33//!
34//! ```ignore
35//! #![windows_subsystem = "windows"]
36//! ```
37//!
38//! The cost is that `stdout` and `stderr` go nowhere, including panic messages
39//! and anything printed before a window exists. So the usual form keeps the
40//! console while developing and drops it for the build that ships:
41//!
42//! ```ignore
43//! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
44//! ```
45//!
46//! It is ignored on non-Windows targets, so it needs no `cfg(windows)` of its
47//! own. A host that wants both — no console, and somewhere for diagnostics to go
48//! — should log to a file or `OutputDebugString` rather than to a stream nothing
49//! is reading.
50//!
51//! # Testing
52//!
53//! [`keymap`] is platform-independent and its tests run everywhere. That is not
54//! tidiness: it is a table of a hundred numbers, it is the part that breaks, and
55//! discovering that on a CI runner rather than locally is a slow way to discover
56//! it — which is exactly what happened the first time. Only the window and the
57//! DIB section are gated to Windows, the same split `denise-drm` and
58//! `denise-evdev` make.
59//!
60//! # Status
61//!
62//! Run on real hardware — Windows 11 ARM64, where Tab reaches the control and
63//! AltGr and the dead keys compose — and built and tested by CI on every push.
64//! Still unverified: focus behaviour inside a real *dialog*, and DPI changes.
65//! For the latter, the toolkit's answer exists and is documented in
66//! `docs/design.md`: the host rebuilds with `theme.scaled(factor)` (or
67//! `denise_ui_new_scaled` over the C ABI) when `WM_DPICHANGED` arrives — what
68//! is missing is a host that has actually done it.
69
70// `keymap` is deliberately outside the gate: it is a table of a hundred numbers
71// mapping `u16` to `KeyCode`, which is exactly the sort of thing that goes wrong
72// and exactly the sort of thing that should not need a Windows machine to test.
73// Only the window and the DIB section are platform code. The same split
74// `denise-drm` and `denise-evdev` make, for the same reason.
75pub mod keymap;
76
77#[cfg(windows)]
78mod control;
79#[cfg(windows)]
80mod surface;
81
82pub use keymap::key_code;
83
84#[cfg(windows)]
85pub use control::{ControlDelegate, DeniseControl};
86#[cfg(windows)]
87pub use surface::DibSurface;
88
89/// Failures from this backend.
90#[cfg(windows)]
91#[derive(Debug)]
92pub enum Error {
93 /// A surface was asked for with no pixels in it.
94 EmptySurface,
95
96 /// `CreateDIBSection` failed, or handed back no pixels.
97 DibSection,
98
99 /// `CreateCompatibleDC` failed.
100 MemoryDc,
101
102 /// The window class could not be registered.
103 RegisterClass,
104
105 /// `CreateWindowEx` failed.
106 CreateWindow,
107
108 /// A surface operation failed.
109 Surface(denise::SurfaceError),
110}
111
112#[cfg(windows)]
113impl core::fmt::Display for Error {
114 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115 match self {
116 Self::EmptySurface => f.write_str("a surface needs a non-zero width and height"),
117 Self::DibSection => f.write_str("could not create a DIB section"),
118 Self::MemoryDc => f.write_str("could not create a memory device context"),
119 Self::RegisterClass => f.write_str("could not register the window class"),
120 Self::CreateWindow => f.write_str("could not create the control window"),
121 Self::Surface(err) => core::fmt::Display::fmt(err, f),
122 }
123 }
124}
125
126#[cfg(windows)]
127impl core::error::Error for Error {
128 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
129 match self {
130 Self::Surface(err) => core::error::Error::source(err),
131 _ => None,
132 }
133 }
134}
135
136#[cfg(windows)]
137impl From<denise::SurfaceError> for Error {
138 fn from(err: denise::SurfaceError) -> Self {
139 Self::Surface(err)
140 }
141}
142
143/// Compiles the examples in this crate's README, so they cannot drift from the API
144/// they claim to demonstrate. Never built except under `cargo test --doc`.
145#[cfg(doctest)]
146#[doc = include_str!("../README.md")]
147struct Readme;