1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 //! A UI using the windows API.
7 //! This UI contains some edge cases that aren't implemented, for instance:
8 //! * there are a few cases where specific hierarchies are handled differently (e.g. a Button
9 //! containing Label, Scroll behavior, etc).
10 //! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, TextBox
11 //! doesn't handle Binding, etc).
13 //! The error handling is also a _little_ fast-and-loose, as many functions return an error value
14 //! that is acceptable to following logic (though it still would be a good idea to improve this).
16 //! The rendering treats VBox, HBox, and Scroll as strictly layout-only: they do not create any
17 //! associated windows, and the layout logic handles their behavior.
19 // Our windows-targets doesn't link uxtheme correctly for GetThemeSysFont/GetThemeSysColor.
20 // This was working in windows-sys 0.48.
21 #[link(name = "uxtheme", kind = "static")]
24 use super::model::{self, Application, Element, ElementStyle, TypedElement};
25 use crate::data::Property;
27 use quit_token::QuitToken;
28 use std::cell::RefCell;
29 use std::collections::HashMap;
32 use widestring::WideString;
33 use window::{CustomWindowClass, Window, WindowBuilder};
34 use windows_sys::Win32::{
35 Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM},
37 System::{LibraryLoader::GetModuleHandleW, SystemServices, Threading::GetCurrentThreadId},
38 UI::{Controls, Input::KeyboardAndMouse, Shell, WindowsAndMessaging as win},
41 macro_rules! success {
42 ( nonzero $e:expr ) => {{
47 ( lasterror $e:expr ) => {{
48 unsafe { windows_sys::Win32::Foundation::SetLastError(0) };
50 assert!(value != 0 || windows_sys::Win32::Foundation::GetLastError() == 0);
53 ( hresult $e:expr ) => {
54 assert_eq!($e, windows_sys::Win32::Foundation::S_OK);
56 ( pointer $e:expr ) => {{
71 /// A Windows API UI implementation.
76 /// Custom user messages.
79 Invoke = win::WM_USER,
82 fn get_invoke(msg: &win::MSG) -> Option<Box<model::InvokeFn>> {
83 if msg.message == UserMessage::Invoke as u32 {
84 Some(unsafe { Box::from_raw(msg.lParam as *mut model::InvokeFn) })
91 pub fn run_loop(&self, app: Application) {
92 // Initialize common controls.
94 let icc = Controls::INITCOMMONCONTROLSEX {
95 dwSize: std::mem::size_of::<Controls::INITCOMMONCONTROLSEX>() as _,
96 // Buttons, edit controls, and static controls are all included in 'standard'.
97 dwICC: Controls::ICC_STANDARD_CLASSES | Controls::ICC_PROGRESS_CLASS,
99 success!(nonzero unsafe { Controls::InitCommonControlsEx(&icc) });
102 // Enable font smoothing (per
103 // https://learn.microsoft.com/en-us/windows/win32/gdi/cleartype-antialiasing ).
105 // We don't check for failure on these, they are best-effort.
106 win::SystemParametersInfoW(
107 win::SPI_SETFONTSMOOTHING,
109 std::ptr::null_mut(),
110 win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE,
112 win::SystemParametersInfoW(
113 win::SPI_SETFONTSMOOTHINGTYPE,
115 win::FE_FONTSMOOTHINGCLEARTYPE as _,
116 win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE,
120 // Enable correct layout direction.
121 if unsafe { win::SetProcessDefaultLayout(if app.rtl { Gdi::LAYOUT_RTL } else { 0 }) } == 0 {
122 log::warn!("failed to set process layout direction");
125 let module: HINSTANCE = unsafe { GetModuleHandleW(std::ptr::null()) };
127 // Register custom classes.
128 AppWindow::register(module).expect("failed to register AppWindow window class");
131 // The quit token is cloned for each top-level window and dropped at the end of this
133 let quit_token = QuitToken::new();
135 for window in app.windows {
136 let name = WideString::new(window.element_type.title.as_str());
137 let w = top_level_window(
140 WindowRenderer::new(module, window.element_type),
141 Some(quit_token.clone()),
147 unsafe { win::ShowWindow(w.handle, win::SW_NORMAL) };
148 unsafe { Gdi::UpdateWindow(w.handle) };
152 // Run the event loop.
153 let mut msg = unsafe { std::mem::zeroed::<win::MSG>() };
154 while unsafe { win::GetMessageW(&mut msg, 0, 0, 0) } > 0 {
155 if let Some(f) = get_invoke(&msg) {
161 // IsDialogMessageW is necessary to handle niceties like tab navigation
162 if win::IsDialogMessageW(win::GetAncestor(msg.hwnd, win::GA_ROOT), &mut msg) == 0 {
163 win::TranslateMessage(&msg);
164 win::DispatchMessageW(&msg);
169 // Flush queue to properly drop late invokes (this is a very unlikely case)
170 while unsafe { win::PeekMessageW(&mut msg, 0, 0, 0, win::PM_REMOVE) } > 0 {
171 if let Some(f) = get_invoke(&msg) {
177 pub fn invoke(&self, f: model::InvokeFn) {
178 let ptr: *mut model::InvokeFn = Box::into_raw(Box::new(f));
180 win::PostThreadMessageW(self.thread_id, UserMessage::Invoke as u32, 0, ptr as _)
183 let _ = unsafe { Box::from_raw(ptr) };
184 log::warn!("failed to invoke function on thread message queue");
189 impl Default for UI {
190 fn default() -> Self {
192 thread_id: unsafe { GetCurrentThreadId() },
197 /// A reference to an Element.
198 #[derive(PartialEq, Eq, Hash, Clone, Copy)]
199 struct ElementRef(*const Element);
202 pub fn new(element: &Element) -> Self {
203 ElementRef(element as *const Element)
207 /// You must ensure the reference is still valid.
208 pub unsafe fn get(&self) -> &Element {
213 // Equivalent of win32 HIWORD macro
214 fn hiword(v: u32) -> u16 {
218 // Equivalent of win32 LOWORD macro
219 fn loword(v: u32) -> u16 {
223 // Equivalent of win32 MAKELONG macro
224 fn makelong(low: u16, high: u16) -> u32 {
225 (high as u32) << 16 | low as u32
228 fn top_level_window<W: window::WindowClass + window::WindowData>(
232 style: &ElementStyle,
237 .style(win::WS_OVERLAPPEDWINDOW)
238 .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT)
241 .horizontal_size_request
242 .and_then(|i| i.try_into().ok())
243 .unwrap_or(win::CW_USEDEFAULT),
245 .vertical_size_request
246 .and_then(|i| i.try_into().ok())
247 .unwrap_or(win::CW_USEDEFAULT),
252 window::basic_window_classes! {
253 /// Static control (text, image, etc) class.
254 struct Static => "STATIC";
256 /// Button control class.
257 struct Button => "BUTTON";
259 /// Edit control class.
260 struct Edit => "EDIT";
262 /// Progress control class.
263 struct Progress => "msctls_progress32";
266 /// A top-level application window.
268 /// This is used for the main window and modal windows.
270 renderer: WindowRenderer,
271 _quit_token: Option<QuitToken>,
275 pub fn new(renderer: WindowRenderer, quit_token: Option<QuitToken>) -> Self {
278 _quit_token: quit_token,
283 impl window::WindowClass for AppWindow {
284 fn class_name() -> WideString {
285 WideString::new("App Window")
289 impl CustomWindowClass for AppWindow {
291 data: &RefCell<Self>,
296 ) -> Option<LRESULT> {
297 let me = data.borrow();
298 let model = me.renderer.model();
301 if let Some(close) = &model.close {
302 close.subscribe(move |&()| unsafe {
303 win::SendMessageW(hwnd, win::WM_CLOSE, 0, 0);
307 let mut renderer = me.renderer.child_renderer(hwnd);
308 if let Some(child) = &model.content {
309 renderer.render_child(child);
313 let children = std::mem::take(&mut me.renderer.model_mut().children);
314 for child in children {
315 renderer.render_window(child);
320 // Modal windows should hide themselves rather than closing/destroying.
321 unsafe { win::ShowWindow(hwnd, win::SW_HIDE) };
325 win::WM_SHOWWINDOW => {
327 // Modal windows should disable/enable their parent as they are shown/hid,
329 let shown = wparam != 0;
331 KeyboardAndMouse::EnableWindow(
332 win::GetWindow(hwnd, win::GW_OWNER),
340 // When resized, recompute the layout.
341 let width = loword(lparam as _) as u32;
342 let height = hiword(lparam as _) as u32;
344 if let Some(child) = &model.content {
345 me.renderer.layout(child, width, height);
346 unsafe { Gdi::UpdateWindow(hwnd) };
350 win::WM_GETFONT => return Some(**me.renderer.font() as _),
352 let child = lparam as HWND;
353 let windows = me.renderer.windows.borrow();
354 if let Some(&element) = windows.reverse().get(&child) {
356 // The ElementRefs all pertain to the model stored in the renderer.
357 let element = unsafe { element.get() };
358 // Handle button presses.
359 use model::ElementType::*;
360 match &element.element_type {
361 Button(model::Button { click, .. }) => {
362 let code = hiword(wparam as _) as u32;
363 if code == win::BN_CLICKED {
368 Checkbox(model::Checkbox { checked, .. }) => {
369 let code = hiword(wparam as _) as u32;
370 if code == win::BN_CLICKED {
372 unsafe { win::SendMessageW(child, win::BM_GETCHECK, 0, 0) };
373 if let Property::Binding(s) = checked {
374 *s.borrow_mut() = check_state == Controls::BST_CHECKED as isize;
389 /// State used while creating and updating windows.
390 struct WindowRenderer {
391 // We wrap with an Rc to get weak references in property callbacks (like that of
392 // `ElementStyle::visible`).
393 inner: Rc<WindowRendererInner>,
396 impl std::ops::Deref for WindowRenderer {
397 type Target = WindowRendererInner;
399 fn deref(&self) -> &Self::Target {
404 struct WindowRendererInner {
405 pub module: HINSTANCE,
406 /// The model is pinned and boxed to ensure that references in `windows` remain valid.
408 /// We need to keep the model around so we can correctly perform layout as the window size
409 /// changes. Unfortunately the win32 API doesn't have any nice ways to automatically perform
411 pub model: RefCell<Pin<Box<model::Window>>>,
412 /// Mapping between model elements and windows.
414 /// Element references pertain to elements in `model`.
415 pub windows: RefCell<twoway::TwoWay<ElementRef, HWND>>,
420 impl WindowRenderer {
421 pub fn new(module: HINSTANCE, model: model::Window) -> Self {
423 inner: Rc::new(WindowRendererInner {
425 model: RefCell::new(Box::pin(model)),
426 windows: Default::default(),
427 font: Font::caption(),
428 bold_font: Font::caption_bold().unwrap_or_else(Font::caption),
433 pub fn child_renderer(&self, window: HWND) -> WindowChildRenderer {
434 WindowChildRenderer {
435 renderer: &self.inner,
442 pub fn layout(&self, element: &Element, max_width: u32, max_height: u32) {
443 layout::Layout::new(self.inner.windows.borrow().forward())
444 .layout(element, max_width, max_height);
447 pub fn model(&self) -> std::cell::Ref<'_, model::Window> {
448 std::cell::Ref::map(self.inner.model.borrow(), |b| &**b)
451 pub fn model_mut(&self) -> std::cell::RefMut<'_, model::Window> {
452 std::cell::RefMut::map(self.inner.model.borrow_mut(), |b| &mut **b)
455 pub fn font(&self) -> &Font {
460 struct WindowChildRenderer<'a> {
461 renderer: &'a Rc<WindowRendererInner>,
467 impl<'a> WindowChildRenderer<'a> {
468 fn add_child<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> {
470 .builder(self.renderer.module)
471 .style(win::WS_CHILD | win::WS_VISIBLE)
473 .child_id(self.child_id);
478 fn add_window<W: window::WindowClass>(&mut self, class: W) -> WindowBuilder<W> {
480 .builder(self.renderer.module)
481 .style(win::WS_OVERLAPPEDWINDOW)
482 .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT)
486 fn render_window(&mut self, model: TypedElement<model::Window>) -> Window {
487 let name = WideString::new(model.element_type.title.as_str());
488 let style = model.style;
490 .add_window(AppWindow::new(
491 WindowRenderer::new(self.renderer.module, model.element_type),
496 .horizontal_size_request
497 .and_then(|i| i.try_into().ok())
498 .unwrap_or(win::CW_USEDEFAULT),
500 .vertical_size_request
501 .and_then(|i| i.try_into().ok())
502 .unwrap_or(win::CW_USEDEFAULT),
507 enabled_property(&style.enabled, w.handle);
510 let set_visible = move |visible| unsafe {
511 win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE });
514 match &style.visible {
515 Property::Static(false) => set_visible(false),
516 Property::Binding(s) => {
517 s.on_change(move |v| set_visible(*v));
528 fn render_child(&mut self, element: &Element) {
529 if let Some(mut window) = self.render_element_type(&element.element_type) {
530 window.set_default_font(&self.renderer.font);
532 // Store the element to handle mapping.
536 .insert(ElementRef::new(element), window.handle);
538 enabled_property(&element.style.enabled, window.handle);
541 // Handle visibility properties.
542 match &element.style.visible {
543 Property::Static(false) => {
544 set_visibility(element, false, self.renderer.windows.borrow().forward())
546 Property::Binding(s) => {
547 let weak_renderer = Rc::downgrade(self.renderer);
548 let element_ref = ElementRef::new(element);
549 let parent = self.window;
550 s.on_change(move |visible| {
551 let Some(renderer) = weak_renderer.upgrade() else {
555 // ElementRefs are valid as long as the renderer is (and we have a strong
557 let element = unsafe { element_ref.get() };
558 set_visibility(element, *visible, renderer.windows.borrow().forward());
559 // Send WM_SIZE so that the parent recomputes the layout.
561 let mut rect = std::mem::zeroed::<RECT>();
562 win::GetClientRect(parent, &mut rect);
568 (rect.right - rect.left) as u16,
569 (rect.bottom - rect.top) as u16,
575 set_visibility(element, false, self.renderer.windows.borrow().forward());
582 fn render_element_type(&mut self, element_type: &model::ElementType) -> Option<Window> {
583 use model::ElementType as ET;
585 ET::Label(model::Label { text, bold }) => {
586 let mut window = match text {
587 Property::Static(text) => {
588 let text = WideString::new(text.as_str());
589 self.add_child(Static)
591 .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX)
594 Property::Binding(b) => {
595 let text = WideString::new(b.borrow().as_str());
599 .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX)
601 let handle = window.handle;
602 b.on_change(move |text| {
603 let text = WideString::new(text.as_str());
604 unsafe { win::SetWindowTextW(handle, text.pcwstr()) };
608 Property::ReadOnly(_) => {
609 unimplemented!("ReadOnly property not supported for Label::text")
613 window.set_font(&self.renderer.bold_font);
615 Some(window.generic())
617 ET::TextBox(model::TextBox {
622 let scroll = self.scroll;
629 | if *editable { 0 } else { win::ES_READONLY })
633 | if scroll { win::WS_VSCROLL } else { 0 },
637 fn to_control_text(s: &str) -> String {
638 s.replace("\n", "\r\n")
641 fn from_control_text(s: &str) -> String {
642 s.replace("\r\n", "\n")
645 struct SubClassData {
646 placeholder: Option<WideString>,
649 // EM_SETCUEBANNER doesn't work with multiline edit controls (for no particular
650 // reason?), so we have to draw it ourselves.
651 unsafe extern "system" fn subclass_proc(
659 let ret = Shell::DefSubclassProc(hwnd, msg, wparam, lparam);
660 if msg == win::WM_PAINT
661 && KeyboardAndMouse::GetFocus() != hwnd
662 && win::GetWindowTextLengthW(hwnd) == 0
664 let data = (dw_ref_data as *const SubClassData).as_ref().unwrap();
665 if let Some(placeholder) = &data.placeholder {
666 let mut rect = std::mem::zeroed::<RECT>();
667 win::GetClientRect(hwnd, &mut rect);
668 Gdi::InflateRect(&mut rect, -2, -2);
670 let dc = gdi::DC::new(hwnd).expect("failed to create GDI DC");
671 dc.with_object_selected(
672 win::SendMessageW(hwnd, win::WM_GETFONT, 0, 0) as _,
676 Controls::GetThemeSysColor(0, Gdi::COLOR_GRAYTEXT),
678 success!(nonzero Gdi::DrawTextW(
680 placeholder.pcwstr(),
683 Gdi::DT_LEFT | Gdi::DT_TOP | Gdi::DT_WORDBREAK,
687 .expect("failed to select font gdi object");
690 if msg == win::WM_DESTROY {
691 drop(unsafe { Box::from_raw(dw_ref_data as *mut SubClassData) });
696 let subclassdata = Box::into_raw(Box::new(SubClassData {
697 placeholder: placeholder
699 .map(|s| WideString::new(to_control_text(s))),
703 Shell::SetWindowSubclass(
711 // Set up content property.
713 Property::ReadOnly(od) => {
714 let handle = window.handle;
715 od.register(move |target| {
716 // GetWindowText requires the buffer be large enough for the terminating
717 // null character (otherwise it truncates the string), but
718 // GetWindowTextLength returns the length without the null character, so we
720 let length = unsafe { win::GetWindowTextLengthW(handle) } + 1;
721 let mut buf = vec![0u16; length as usize];
722 unsafe { win::GetWindowTextW(handle, buf.as_mut_ptr(), length) };
723 buf.pop(); // null character; `String` doesn't want that
724 *target = from_control_text(&String::from_utf16_lossy(&buf));
727 Property::Static(s) => {
728 let text = WideString::new(to_control_text(s));
729 unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) };
731 Property::Binding(b) => {
732 let handle = window.handle;
733 b.on_change(move |text| {
734 let text = WideString::new(to_control_text(text.as_str()));
735 unsafe { win::SetWindowTextW(handle, text.pcwstr()) };
737 let text = WideString::new(to_control_text(b.borrow().as_str()));
738 unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) };
741 Some(window.generic())
743 ET::Scroll(model::Scroll { content }) => {
744 if let Some(content) = content {
745 // Scrolling is implemented in a cooperative, non-universal way right now.
747 self.render_child(content);
752 ET::Button(model::Button { content, .. }) => {
753 if let Some(ET::Label(model::Label {
754 text: Property::Static(text),
756 })) = content.as_ref().map(|e| &e.element_type)
758 let text = WideString::new(text);
762 .add_style(win::BS_PUSHBUTTON as u32 | win::WS_TABSTOP)
765 Some(window.generic())
770 ET::Checkbox(model::Checkbox { checked, label }) => {
771 let label = label.as_ref().map(WideString::new);
772 let mut builder = self
774 .add_style((win::BS_AUTOCHECKBOX | win::BS_MULTILINE) as u32 | win::WS_TABSTOP);
775 if let Some(label) = &label {
776 builder = builder.name(label);
778 let window = builder.create();
780 fn set_check(handle: HWND, value: bool) {
786 Controls::BST_CHECKED
788 Controls::BST_UNCHECKED
796 Property::Static(checked) => set_check(window.handle, *checked),
797 Property::Binding(s) => {
798 let handle = window.handle;
799 s.on_change(move |v| {
800 set_check(handle, *v);
802 set_check(window.handle, *s.borrow());
804 _ => unimplemented!("ReadOnly properties not supported for Checkbox"),
807 Some(window.generic())
809 ET::Progress(model::Progress { amount }) => {
812 .add_style(Controls::PBS_MARQUEE)
815 fn set_amount(handle: HWND, value: Option<f32>) {
818 win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 1, 0);
821 win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 0, 0);
824 Controls::PBM_SETPOS,
825 (v.clamp(0f32, 1f32) * 100f32) as usize,
833 Property::Static(v) => set_amount(window.handle, *v),
834 Property::Binding(s) => {
835 let handle = window.handle;
836 s.on_change(move |v| set_amount(handle, *v));
837 set_amount(window.handle, *s.borrow());
839 _ => unimplemented!("ReadOnly properties not supported for Progress"),
842 Some(window.generic())
844 // VBox/HBox are virtual, their behaviors are implemented entirely in the renderer layout.
845 // No need for additional windows.
846 ET::VBox(model::VBox { items, .. }) => {
848 self.render_child(item);
852 ET::HBox(model::HBox { items, .. }) => {
854 self.render_child(item);
862 /// Handle the enabled property.
864 /// This function assumes the default state of the window is enabled.
865 fn enabled_property(enabled: &Property<bool>, window: HWND) {
867 Property::Static(false) => unsafe {
868 KeyboardAndMouse::EnableWindow(window, false.into());
870 Property::Binding(s) => {
872 s.on_change(move |enabled| {
873 unsafe { KeyboardAndMouse::EnableWindow(handle, (*enabled).into()) };
876 unsafe { KeyboardAndMouse::EnableWindow(window, false.into()) };
883 /// Set the visibility of the given element. This recurses down the element tree and hides children
885 fn set_visibility(element: &Element, visible: bool, windows: &HashMap<ElementRef, HWND>) {
886 if let Some(&hwnd) = windows.get(&ElementRef::new(element)) {
888 win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE });
891 match &element.element_type {
892 model::ElementType::VBox(model::VBox { items, .. }) => {
894 set_visibility(item, visible, windows);
897 model::ElementType::HBox(model::HBox { items, .. }) => {
899 set_visibility(item, visible, windows);
902 model::ElementType::Scroll(model::Scroll {
903 content: Some(content),
905 set_visibility(&*content, visible, windows);