/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! A UI using the windows API. //! //! This UI contains some edge cases that aren't implemented, for instance: //! * there are a few cases where specific hierarchies are handled differently (e.g. a Button //! containing Label, Scroll behavior, etc). //! * not all controls handle all Property variants (e.g. Checkbox doesn't handle ReadOnly, TextBox //! doesn't handle Binding, etc). //! //! The error handling is also a _little_ fast-and-loose, as many functions return an error value //! that is acceptable to following logic (though it still would be a good idea to improve this). //! //! The rendering treats VBox, HBox, and Scroll as strictly layout-only: they do not create any //! associated windows, and the layout logic handles their behavior. // Our windows-targets doesn't link uxtheme correctly for GetThemeSysFont/GetThemeSysColor. // This was working in windows-sys 0.48. #[link(name = "uxtheme", kind = "static")] extern "C" {} use super::model::{self, Application, Element, ElementStyle, TypedElement}; use crate::data::{Property, Synchronized}; use dpi::Dpi; use font::Fonts; use once_cell::sync::Lazy; use quit_token::QuitToken; use std::cell::RefCell; use std::collections::HashMap; use std::pin::Pin; use std::rc::Rc; use widestring::WideString; use window::{CustomWindowClass, Window, WindowBuilder}; use windows_sys::Win32::{ Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM}, Graphics::Gdi, System::{LibraryLoader::GetModuleHandleW, SystemServices, Threading::GetCurrentThreadId}, UI::{Controls, Input::KeyboardAndMouse, Shell, WindowsAndMessaging as win}, }; macro_rules! success { ( nonzero $e:expr ) => {{ let value = $e; assert_ne!(value, 0); value }}; ( lasterror $e:expr ) => {{ unsafe { windows_sys::Win32::Foundation::SetLastError(0) }; let value = $e; assert!(value != 0 || windows_sys::Win32::Foundation::GetLastError() == 0); value }}; ( hresult $e:expr ) => { assert_eq!($e, windows_sys::Win32::Foundation::S_OK); }; ( pointer $e:expr ) => {{ let ptr = $e; assert_ne!(ptr, 0); ptr }}; } mod dpi; mod font; mod gdi; mod layout; mod quit_token; mod twoway; mod widestring; mod window; /// A Windows API UI implementation. pub struct UI { thread_id: u32, } /// Custom user messages. #[repr(u32)] enum UserMessage { Invoke = win::WM_USER, } fn get_invoke(msg: &win::MSG) -> Option> { if msg.message == UserMessage::Invoke as u32 { Some(unsafe { Box::from_raw(msg.lParam as *mut model::InvokeFn) }) } else { None } } impl UI { pub fn run_loop(&self, app: Application) { // Initialize common controls. { let icc = Controls::INITCOMMONCONTROLSEX { dwSize: std::mem::size_of::() as _, // Buttons, edit controls, and static controls are all included in 'standard'. dwICC: Controls::ICC_STANDARD_CLASSES | Controls::ICC_PROGRESS_CLASS, }; success!(nonzero unsafe { Controls::InitCommonControlsEx(&icc) }); } // Enable font smoothing (per // https://learn.microsoft.com/en-us/windows/win32/gdi/cleartype-antialiasing ). unsafe { // We don't check for failure on these, they are best-effort. win::SystemParametersInfoW( win::SPI_SETFONTSMOOTHING, 1, std::ptr::null_mut(), win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE, ); win::SystemParametersInfoW( win::SPI_SETFONTSMOOTHINGTYPE, 0, win::FE_FONTSMOOTHINGCLEARTYPE as _, win::SPIF_UPDATEINIFILE | win::SPIF_SENDCHANGE, ); } // Enable correct layout direction. if unsafe { win::SetProcessDefaultLayout(if app.rtl { Gdi::LAYOUT_RTL } else { 0 }) } == 0 { log::warn!("failed to set process layout direction"); } let module: HINSTANCE = unsafe { GetModuleHandleW(std::ptr::null()) }; // Register custom classes. AppWindow::register(module).expect("failed to register AppWindow window class"); { // The quit token is cloned for each top-level window and dropped at the end of this // scope. let quit_token = QuitToken::new(); for window in app.windows { let name = WideString::new(window.element_type.title.as_str()); let w = top_level_window( module, AppWindow::new( WindowRenderer::new(module, window.element_type, &window.style), Some(quit_token.clone()), ), &name, &window.style, ); unsafe { win::ShowWindow(w.handle, win::SW_NORMAL) }; unsafe { Gdi::UpdateWindow(w.handle) }; } } // Run the event loop. let mut msg = unsafe { std::mem::zeroed::() }; while unsafe { win::GetMessageW(&mut msg, 0, 0, 0) } > 0 { if let Some(f) = get_invoke(&msg) { f(); continue; } unsafe { // IsDialogMessageW is necessary to handle niceties like tab navigation if win::IsDialogMessageW(win::GetAncestor(msg.hwnd, win::GA_ROOT), &mut msg) == 0 { win::TranslateMessage(&msg); win::DispatchMessageW(&msg); } } } // Flush queue to properly drop late invokes (this is a very unlikely case) while unsafe { win::PeekMessageW(&mut msg, 0, 0, 0, win::PM_REMOVE) } > 0 { if let Some(f) = get_invoke(&msg) { drop(f); } } } pub fn invoke(&self, f: model::InvokeFn) { let ptr: *mut model::InvokeFn = Box::into_raw(Box::new(f)); if unsafe { win::PostThreadMessageW(self.thread_id, UserMessage::Invoke as u32, 0, ptr as _) } == 0 { let _ = unsafe { Box::from_raw(ptr) }; log::warn!("failed to invoke function on thread message queue"); } } } impl Default for UI { fn default() -> Self { UI { thread_id: unsafe { GetCurrentThreadId() }, } } } /// A reference to an Element. #[derive(PartialEq, Eq, Hash, Clone, Copy)] struct ElementRef(*const Element); impl ElementRef { pub fn new(element: &Element) -> Self { ElementRef(element as *const Element) } /// # Safety /// You must ensure the reference is still valid. pub unsafe fn get(&self) -> &Element { &*self.0 } } // Equivalent of win32 HIWORD macro fn hiword(v: u32) -> u16 { (v >> 16) as u16 } // Equivalent of win32 LOWORD macro fn loword(v: u32) -> u16 { v as u16 } // Equivalent of win32 MAKELONG macro fn makelong(low: u16, high: u16) -> u32 { (high as u32) << 16 | low as u32 } fn top_level_window( module: HINSTANCE, class: W, title: &WideString, style: &ElementStyle, ) -> Window { class .builder(module) .name(title) .style(win::WS_OVERLAPPEDWINDOW) .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT) .size( style .horizontal_size_request .and_then(|i| i.try_into().ok()) .unwrap_or(win::CW_USEDEFAULT), style .vertical_size_request .and_then(|i| i.try_into().ok()) .unwrap_or(win::CW_USEDEFAULT), ) .create() } window::basic_window_classes! { /// Static control (text, image, etc) class. struct Static => "STATIC"; /// Button control class. struct Button => "BUTTON"; /// Edit control class. struct Edit => "EDIT"; /// Progress control class. struct Progress => "msctls_progress32"; } /// A top-level application window. /// /// This is used for the main window and modal windows. struct AppWindow { renderer: WindowRenderer, _quit_token: Option, } impl AppWindow { pub fn new(renderer: WindowRenderer, quit_token: Option) -> Self { AppWindow { renderer, _quit_token: quit_token, } } } impl window::WindowClass for AppWindow { fn class_name() -> WideString { WideString::new("App Window") } } impl CustomWindowClass for AppWindow { fn icon() -> win::HICON { static ICON: Lazy = Lazy::new(|| unsafe { // If CreateIconFromResource fails it returns NULL, which is fine (a default icon will be // used). win::CreateIconFromResource( // We take advantage of the fact that since Windows Vista, an RT_ICON resource entry // can simply be a PNG image. super::icon::PNG_DATA.as_ptr(), super::icon::PNG_DATA.len() as u32, true.into(), // The 0x00030000 constant isn't available anywhere; the docs basically say to just // pass it... 0x00030000, ) }); *ICON } fn message( data: &RefCell, hwnd: HWND, umsg: u32, wparam: WPARAM, lparam: LPARAM, ) -> Option { let me = data.borrow(); let model = me.renderer.model(); match umsg { win::WM_CREATE => { me.renderer.set_dpi(Dpi::for_window(hwnd)); if let Some(close) = &model.close { close.subscribe(move |&()| unsafe { win::SendMessageW(hwnd, win::WM_CLOSE, 0, 0); }); } let mut renderer = me.renderer.child_renderer(hwnd); if let Some(child) = &model.content { renderer.render_child(child); } drop(model); let children = std::mem::take(&mut me.renderer.model_mut().children); for child in children { renderer.render_window(child); } // Set the window size to the initial (DPI-aware) min width/height. We don't have // the DPI information until we create the window, so we can't set the initial // window size correctly at that time. unsafe { win::SetWindowPos( hwnd, win::HWND_TOP, 0, 0, me.renderer.min_width().try_into().unwrap(), me.renderer.min_height().try_into().unwrap(), win::SWP_NOZORDER | win::SWP_NOACTIVATE | win::SWP_NOMOVE, ) }; } win::WM_CLOSE => { if model.modal { // Modal windows should hide themselves rather than closing/destroying. unsafe { win::ShowWindow(hwnd, win::SW_HIDE) }; return Some(0); } } win::WM_SHOWWINDOW => { if model.modal { // Modal windows should disable/enable their parent as they are shown/hid, // respectively. let shown = wparam != 0; unsafe { KeyboardAndMouse::EnableWindow( win::GetWindow(hwnd, win::GW_OWNER), (!shown).into(), ) }; return Some(0); } } win::WM_GETMINMAXINFO => { let minmaxinfo = unsafe { (lparam as *mut win::MINMAXINFO).as_mut().unwrap() }; minmaxinfo.ptMinTrackSize.x = me.renderer.min_width().try_into().unwrap(); minmaxinfo.ptMinTrackSize.y = me.renderer.min_height().try_into().unwrap(); return Some(0); } win::WM_SIZE => { // When resized, recompute the layout. let width = loword(lparam as _) as u32; let height = hiword(lparam as _) as u32; if let Some(child) = &model.content { me.renderer.layout(child, width, height); unsafe { Gdi::UpdateWindow(hwnd) }; } return Some(0); } win::WM_DPICHANGED => { // When DPI changes, recompute the layout and move the window. let rect: &RECT = unsafe { (lparam as *const RECT).as_ref() } .expect("null RECT pointer in WM_DPICHANGED"); let dpi = loword(wparam as _) as u32; let width = rect.right - rect.left; let height = rect.bottom - rect.top; me.renderer.set_dpi(Dpi::new(dpi)); // This wil send WM_SIZE, which will take care of the new layout. unsafe { win::SetWindowPos( hwnd, win::HWND_TOP, rect.left, rect.top, width, height, win::SWP_NOZORDER | win::SWP_NOACTIVATE, ) }; return Some(0); } win::WM_GETFONT => return Some(me.renderer.font()), win::WM_COMMAND => { let child = lparam as HWND; let windows = me.renderer.windows.borrow(); if let Some(&element) = windows.reverse().get(&child) { // # Safety // The ElementRefs all pertain to the model stored in the renderer. let element = unsafe { element.get() }; // Handle button presses. use model::ElementType::*; match &element.element_type { Button(model::Button { click, .. }) => { let code = hiword(wparam as _) as u32; if code == win::BN_CLICKED { click.fire(&()); return Some(0); } } Checkbox(model::Checkbox { checked, .. }) => { let code = hiword(wparam as _) as u32; if code == win::BN_CLICKED { let check_state = unsafe { win::SendMessageW(child, win::BM_GETCHECK, 0, 0) }; if let Property::Binding(s) = checked { *s.borrow_mut() = check_state == Controls::BST_CHECKED as isize; } return Some(0); } } _ => (), } } } _ => (), } None } } /// State used while creating and updating windows. struct WindowRenderer { // We wrap with an Rc to get weak references in property callbacks (like that of // `ElementStyle::visible`). inner: Rc, } impl std::ops::Deref for WindowRenderer { type Target = WindowRendererInner; fn deref(&self) -> &Self::Target { &self.inner } } struct WindowRendererInner { pub module: HINSTANCE, /// The model is pinned and boxed to ensure that references in `windows` remain valid. /// /// We need to keep the model around so we can correctly perform layout as the window size /// changes. Unfortunately the win32 API doesn't have any nice ways to automatically perform /// layout. pub model: RefCell>>, min_size: (u32, u32), /// Mapping between model elements and windows. /// /// Element references pertain to elements in `model`. pub windows: RefCell>, pub dpi: Synchronized, pub fonts: Synchronized, } impl WindowRenderer { pub fn new(module: HINSTANCE, model: model::Window, style: &model::ElementStyle) -> Self { let dpi: Synchronized = Default::default(); let scale_factor = font::ScaleFactor::from_registry(); let fonts = dpi.mapped(move |dpi| Fonts::new(*dpi, scale_factor)); WindowRenderer { inner: Rc::new(WindowRendererInner { module, model: RefCell::new(Box::pin(model)), min_size: ( style.horizontal_size_request.unwrap_or(0), style.vertical_size_request.unwrap_or(0), ), windows: Default::default(), dpi, fonts, }), } } pub fn child_renderer(&self, window: HWND) -> WindowChildRenderer<'_> { WindowChildRenderer { renderer: &self.inner, window, child_id: 0, scroll: false, } } pub fn set_dpi(&self, dpi: Dpi) { *self.dpi.borrow_mut() = dpi; } pub fn layout(&self, element: &Element, max_width: u32, max_height: u32) { layout::Layout::new( self.inner.windows.borrow().forward(), *self.inner.dpi.borrow(), ) .layout(element, max_width, max_height); } pub fn model(&self) -> std::cell::Ref<'_, model::Window> { std::cell::Ref::map(self.inner.model.borrow(), |b| &**b) } pub fn model_mut(&self) -> std::cell::RefMut<'_, model::Window> { std::cell::RefMut::map(self.inner.model.borrow_mut(), |b| &mut **b) } pub fn font(&self) -> Gdi::HFONT { *self.inner.fonts.borrow().normal } pub fn min_width(&self) -> u32 { self.inner.dpi.borrow().scale(self.min_size.0) } pub fn min_height(&self) -> u32 { self.inner.dpi.borrow().scale(self.min_size.1) } } struct WindowChildRenderer<'a> { renderer: &'a Rc, window: HWND, child_id: i32, scroll: bool, } impl<'a> WindowChildRenderer<'a> { fn add_child(&mut self, class: W) -> WindowBuilder<'_, W> { let builder = class .builder(self.renderer.module) .style(win::WS_CHILD | win::WS_VISIBLE) .parent(self.window) .child_id(self.child_id); self.child_id += 1; builder } fn add_window(&mut self, class: W) -> WindowBuilder<'_, W> { class .builder(self.renderer.module) .style(win::WS_OVERLAPPEDWINDOW) .pos(win::CW_USEDEFAULT, win::CW_USEDEFAULT) .parent(self.window) } fn render_window(&mut self, model: TypedElement) -> Window { let name = WideString::new(model.element_type.title.as_str()); let style = model.style; let w = self .add_window(AppWindow::new( WindowRenderer::new(self.renderer.module, model.element_type, &style), None, )) .size( style .horizontal_size_request .and_then(|i| i.try_into().ok()) .unwrap_or(win::CW_USEDEFAULT), style .vertical_size_request .and_then(|i| i.try_into().ok()) .unwrap_or(win::CW_USEDEFAULT), ) .name(&name) .create(); enabled_property(&style.enabled, w.handle); let hwnd = w.handle; let set_visible = move |visible| unsafe { win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE }); }; match &style.visible { Property::Static(false) => set_visible(false), Property::Binding(s) => { s.on_change(move |v| set_visible(*v)); if !*s.borrow() { set_visible(false); } } _ => (), } w.generic() } fn render_child(&mut self, element: &Element) { if let Some(mut window) = self.render_element_type(&element.element_type) { window.set_default_font(&self.renderer.fonts, |fonts| &fonts.normal); // Store the element to handle mapping. self.renderer .windows .borrow_mut() .insert(ElementRef::new(element), window.handle); enabled_property(&element.style.enabled, window.handle); } // Handle visibility properties. match &element.style.visible { Property::Static(false) => { set_visibility(element, false, self.renderer.windows.borrow().forward()) } Property::Binding(s) => { let weak_renderer = Rc::downgrade(self.renderer); let element_ref = ElementRef::new(element); let parent = self.window; s.on_change(move |visible| { let Some(renderer) = weak_renderer.upgrade() else { return; }; // # Safety // ElementRefs are valid as long as the renderer is (and we have a strong // reference to it). let element = unsafe { element_ref.get() }; set_visibility(element, *visible, renderer.windows.borrow().forward()); // Send WM_SIZE so that the parent recomputes the layout. unsafe { let mut rect = std::mem::zeroed::(); win::GetClientRect(parent, &mut rect); win::SendMessageW( parent, win::WM_SIZE, 0, makelong( (rect.right - rect.left) as u16, (rect.bottom - rect.top) as u16, ) as isize, ); } }); if !*s.borrow() { set_visibility(element, false, self.renderer.windows.borrow().forward()); } } _ => (), } } fn render_element_type(&mut self, element_type: &model::ElementType) -> Option { use model::ElementType as ET; match element_type { ET::Label(model::Label { text, bold }) => { let mut window = match text { Property::Static(text) => { let text = WideString::new(text.as_str()); self.add_child(Static) .name(&text) .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX) .create() } Property::Binding(b) => { let text = WideString::new(b.borrow().as_str()); let window = self .add_child(Static) .name(&text) .add_style(SystemServices::SS_LEFT | SystemServices::SS_NOPREFIX) .create(); let handle = window.handle; b.on_change(move |text| { let text = WideString::new(text.as_str()); unsafe { win::SetWindowTextW(handle, text.pcwstr()) }; }); window } Property::ReadOnly(_) => { unimplemented!("ReadOnly property not supported for Label::text") } }; if *bold { window.set_font(&self.renderer.fonts, |fonts| &fonts.bold); } Some(window.generic()) } ET::TextBox(model::TextBox { placeholder, content, editable, }) => { let scroll = self.scroll; let window = self .add_child(Edit) .add_style( (win::ES_LEFT | win::ES_MULTILINE | win::ES_WANTRETURN | if *editable { 0 } else { win::ES_READONLY }) as u32 | win::WS_BORDER | win::WS_TABSTOP | if scroll { win::WS_VSCROLL } else { 0 }, ) .create(); fn to_control_text(s: &str) -> String { s.replace("\n", "\r\n") } fn from_control_text(s: &str) -> String { s.replace("\r\n", "\n") } struct SubClassData { placeholder: Option, } // EM_SETCUEBANNER doesn't work with multiline edit controls (for no particular // reason?), so we have to draw it ourselves. unsafe extern "system" fn subclass_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, _uidsubclass: usize, dw_ref_data: usize, ) -> LRESULT { let ret = Shell::DefSubclassProc(hwnd, msg, wparam, lparam); if msg == win::WM_PAINT && KeyboardAndMouse::GetFocus() != hwnd && win::GetWindowTextLengthW(hwnd) == 0 { let data = (dw_ref_data as *const SubClassData).as_ref().unwrap(); if let Some(placeholder) = &data.placeholder { let mut rect = std::mem::zeroed::(); win::GetClientRect(hwnd, &mut rect); Gdi::InflateRect(&mut rect, -2, -2); let dc = gdi::DC::new(hwnd).expect("failed to create GDI DC"); dc.with_object_selected( win::SendMessageW(hwnd, win::WM_GETFONT, 0, 0) as _, |hdc| { Gdi::SetTextColor( hdc, Controls::GetThemeSysColor(0, Gdi::COLOR_GRAYTEXT), ); Gdi::SetBkMode(hdc, Gdi::TRANSPARENT as i32); success!(nonzero Gdi::DrawTextW( hdc, placeholder.pcwstr(), -1, &mut rect, Gdi::DT_LEFT | Gdi::DT_TOP | Gdi::DT_WORDBREAK, )); }, ) .expect("failed to select font gdi object"); } } // Multiline edit controls capture the tab key. We want it to work as usual in // the dialog (focusing the next input control). if msg == win::WM_GETDLGCODE && wparam == KeyboardAndMouse::VK_TAB as usize { return 0; } if msg == win::WM_DESTROY { drop(unsafe { Box::from_raw(dw_ref_data as *mut SubClassData) }); } return ret; } let subclassdata = Box::into_raw(Box::new(SubClassData { placeholder: placeholder .as_ref() .map(|s| WideString::new(to_control_text(s))), })); unsafe { Shell::SetWindowSubclass( window.handle, Some(subclass_proc), 0, subclassdata as _, ); } // Set up content property. match content { Property::ReadOnly(od) => { let handle = window.handle; od.register(move |target| { // GetWindowText requires the buffer be large enough for the terminating // null character (otherwise it truncates the string), but // GetWindowTextLength returns the length without the null character, so we // add 1. let length = unsafe { win::GetWindowTextLengthW(handle) } + 1; let mut buf = vec![0u16; length as usize]; unsafe { win::GetWindowTextW(handle, buf.as_mut_ptr(), length) }; buf.pop(); // null character; `String` doesn't want that *target = from_control_text(&String::from_utf16_lossy(&buf)); }); } Property::Static(s) => { let text = WideString::new(to_control_text(s)); unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) }; } Property::Binding(b) => { let handle = window.handle; b.on_change(move |text| { let text = WideString::new(to_control_text(text.as_str())); unsafe { win::SetWindowTextW(handle, text.pcwstr()) }; }); let text = WideString::new(to_control_text(b.borrow().as_str())); unsafe { win::SetWindowTextW(window.handle, text.pcwstr()) }; } } Some(window.generic()) } ET::Scroll(model::Scroll { content }) => { if let Some(content) = content { // Scrolling is implemented in a cooperative, non-universal way right now. self.scroll = true; self.render_child(content); self.scroll = false; } None } ET::Button(model::Button { content, .. }) => { if let Some(ET::Label(model::Label { text: Property::Static(text), .. })) = content.as_ref().map(|e| &e.element_type) { let text = WideString::new(text); let window = self .add_child(Button) .add_style(win::BS_PUSHBUTTON as u32 | win::WS_TABSTOP) .name(&text) .create(); Some(window.generic()) } else { None } } ET::Checkbox(model::Checkbox { checked, label }) => { let label = label.as_ref().map(WideString::new); let mut builder = self .add_child(Button) .add_style((win::BS_AUTOCHECKBOX | win::BS_MULTILINE) as u32 | win::WS_TABSTOP); if let Some(label) = &label { builder = builder.name(label); } let window = builder.create(); fn set_check(handle: HWND, value: bool) { unsafe { win::SendMessageW( handle, win::BM_SETCHECK, if value { Controls::BST_CHECKED } else { Controls::BST_UNCHECKED } as usize, 0, ); } } match checked { Property::Static(checked) => set_check(window.handle, *checked), Property::Binding(s) => { let handle = window.handle; s.on_change(move |v| { set_check(handle, *v); }); set_check(window.handle, *s.borrow()); } _ => unimplemented!("ReadOnly properties not supported for Checkbox"), } Some(window.generic()) } ET::Progress(model::Progress { amount }) => { let window = self .add_child(Progress) .add_style(Controls::PBS_MARQUEE) .create(); fn set_amount(handle: HWND, value: Option) { match value { None => unsafe { win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 1, 0); }, Some(v) => unsafe { win::SendMessageW(handle, Controls::PBM_SETMARQUEE, 0, 0); win::SendMessageW( handle, Controls::PBM_SETPOS, (v.clamp(0f32, 1f32) * 100f32) as usize, 0, ); }, } } match amount { Property::Static(v) => set_amount(window.handle, *v), Property::Binding(s) => { let handle = window.handle; s.on_change(move |v| set_amount(handle, *v)); set_amount(window.handle, *s.borrow()); } _ => unimplemented!("ReadOnly properties not supported for Progress"), } Some(window.generic()) } // VBox/HBox are virtual, their behaviors are implemented entirely in the renderer layout. // No need for additional windows. ET::VBox(model::VBox { items, .. }) => { for item in items { self.render_child(item); } None } ET::HBox(model::HBox { items, .. }) => { for item in items { self.render_child(item); } None } } } } /// Handle the enabled property. /// /// This function assumes the default state of the window is enabled. fn enabled_property(enabled: &Property, window: HWND) { match enabled { Property::Static(false) => unsafe { KeyboardAndMouse::EnableWindow(window, false.into()); }, Property::Binding(s) => { let handle = window; s.on_change(move |enabled| { unsafe { KeyboardAndMouse::EnableWindow(handle, (*enabled).into()) }; }); if !*s.borrow() { unsafe { KeyboardAndMouse::EnableWindow(window, false.into()) }; } } _ => (), } } /// Set the visibility of the given element. This recurses down the element tree and hides children /// as necessary. fn set_visibility(element: &Element, visible: bool, windows: &HashMap) { if let Some(&hwnd) = windows.get(&ElementRef::new(element)) { unsafe { win::ShowWindow(hwnd, if visible { win::SW_SHOW } else { win::SW_HIDE }); } } else { match &element.element_type { model::ElementType::VBox(model::VBox { items, .. }) => { for item in items { set_visibility(item, visible, windows); } } model::ElementType::HBox(model::HBox { items, .. }) => { for item in items { set_visibility(item, visible, windows); } } model::ElementType::Scroll(model::Scroll { content: Some(content), }) => { set_visibility(&*content, visible, windows); } _ => (), } } }