/* 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/. */ //! Helpers for window layout. use super::{ model::{self, Alignment, Element, ElementStyle, Margin}, Dpi, ElementRef, WideString, }; use crate::data::Property; use std::collections::HashMap; use windows_sys::Win32::{ Foundation::{HWND, SIZE}, Graphics::Gdi, UI::WindowsAndMessaging as win, }; pub(super) type ElementMapping = HashMap; /// Handles the layout of windows. /// /// This is done in two passes. The first pass calculates the sizes for all elements in the tree. /// Once sizes are known, the second pass can appropriately position the elements (taking alignment /// into account). /// /// Currently, the resize/reposition logic is tied into the methods here, which is an inconvenient /// design because when adding support for a new element type you have to add new information in /// disparate locations. pub struct Layout<'a> { elements: &'a ElementMapping, dpi: Dpi, sizes: HashMap, last_positioned: Option, } // Unfortunately, there's no good way to get these margins. I just guessed and the first guesses // seemed to be close enough. const BUTTON_MARGIN: Margin = Margin { start: 5, end: 5, top: 5, bottom: 5, }; const CHECKBOX_MARGIN: Margin = Margin { start: 15, end: 0, top: 0, bottom: 0, }; impl<'a> Layout<'a> { pub(super) fn new(elements: &'a ElementMapping, dpi: Dpi) -> Self { Layout { elements, dpi, sizes: Default::default(), last_positioned: None, } } /// Perform a layout of the element and all child elements. pub fn layout(mut self, element: &Element, max_width: u32, max_height: u32) { self.dpi.clone().with_context(|| { let max_size = Size { width: max_width, height: max_height, }; self.resize(element, &max_size); self.reposition(element, &Position::default(), &max_size); }) } fn resize(&mut self, element: &Element, max_size: &Size) -> Size { let style = &element.style; let mut inner_size = max_size.inner_size(style); let mut content_size = None; if !is_visible(style) { self.sizes .insert(ElementRef::new(element), Default::default()); return Default::default(); } // Resize inner content. // // These cases should result in `content_size` being set, if relevant. use model::ElementType::*; match &element.element_type { Button(model::Button { content: Some(content), .. }) => { // Special case for buttons with a label. if let Label(model::Label { text: Property::Static(text), .. }) = &content.element_type { let mut size = Size { // We prefer button text to not wrap, so ignore inner size for width to get // an unwrapped content_size. width: max_size.width, height: inner_size.height, }; size = size.less_margin(&BUTTON_MARGIN); self.measure_text(text.as_str(), element, &mut size); content_size = Some(size.plus_margin(&BUTTON_MARGIN)); } } Checkbox(model::Checkbox { label: Some(label), .. }) => { let mut size = inner_size.less_margin(&CHECKBOX_MARGIN); self.measure_text(label.as_str(), element, &mut size); content_size = Some(size.plus_margin(&CHECKBOX_MARGIN)); } Label(model::Label { text, bold: _ }) => { let mut size = inner_size.clone(); match text { Property::Static(text) => self.measure_text(text.as_str(), element, &mut size), Property::Binding(b) => { self.measure_text(b.borrow().as_str(), element, &mut size) } Property::ReadOnly(_) => { unimplemented!("Label::text does not support ReadOnly") } } content_size = Some(size); } VBox(model::VBox { items, spacing }) => { let spacing = Dpi::context_scale(*spacing); let mut height = 0; let mut max_width = 0; let mut remaining_size = inner_size.clone(); let mut resize_child = |c| { let child_size = self.resize(c, &remaining_size); height += child_size.height; max_width = std::cmp::max(child_size.width, max_width); remaining_size.height = remaining_size .height .saturating_sub(child_size.height + spacing); }; // First resize all non-Fill items; Fill items get the remaining space. for item in items .iter() .filter(|i| i.style.vertical_alignment != Alignment::Fill) { resize_child(item); } for item in items .iter() .filter(|i| i.style.vertical_alignment == Alignment::Fill) { resize_child(item); } content_size = Some(Size { width: max_width, height: height + spacing * (items.len().saturating_sub(1) as u32), }); } HBox(model::HBox { items, spacing, affirmative_order: _, }) => { let spacing = Dpi::context_scale(*spacing); let mut width = 0; let mut max_height = 0; let mut remaining_size = inner_size.clone(); let mut resize_child = |c| { let child_size = self.resize(c, &remaining_size); width += child_size.width; max_height = std::cmp::max(child_size.height, max_height); remaining_size.width = remaining_size .width .saturating_sub(child_size.width + spacing); }; // First resize all non-Fill items; Fill items get the remaining space. for item in items .iter() .filter(|i| i.style.horizontal_alignment != Alignment::Fill) { resize_child(item); } for item in items .iter() .filter(|i| i.style.horizontal_alignment == Alignment::Fill) { resize_child(item); } content_size = Some(Size { width: width + spacing * (items.len().saturating_sub(1) as u32), height: max_height, }); } Scroll(model::Scroll { content: Some(content), }) => { content_size = Some(self.resize(content, &inner_size)); } Progress(model::Progress { .. }) => { // Min size recommended by windows uxguide content_size = Some(Size { width: Dpi::context_scale(160), height: Dpi::context_scale(15), }); } // We don't support sizing by textbox content yet (need to read from the HWND due to // Property::ReadOnly). TextBox(_) => (), _ => (), } // Adjust from content size. if let Some(content_size) = content_size { inner_size.from_content_size(style, &content_size); } // Compute/store (outer) size and return. let size = inner_size.plus_margin(&style.margin); self.sizes.insert(ElementRef::new(element), size); size } fn get_size(&self, element: &Element) -> &Size { self.sizes .get(&ElementRef::new(element)) .expect("element not resized") } fn reposition(&mut self, element: &Element, position: &Position, parent_size: &Size) { let style = &element.style; if !is_visible(style) { return; } let size = self.get_size(element); let start_offset = match style.horizontal_alignment { Alignment::Fill | Alignment::Start => 0, Alignment::Center => parent_size.width.saturating_sub(size.width) / 2, Alignment::End => parent_size.width.saturating_sub(size.width), }; let top_offset = match style.vertical_alignment { Alignment::Fill | Alignment::Start => 0, Alignment::Center => parent_size.height.saturating_sub(size.height) / 2, Alignment::End => parent_size.height.saturating_sub(size.height), }; let inner_position = Position { start: position.start + start_offset, top: position.top + top_offset, } .less_margin(&style.margin); let inner_size = size.less_margin(&style.margin); // Set the window size/position if there is a handle associated with the element. if let Some(&hwnd) = self.elements.get(&ElementRef::new(element)) { unsafe { win::SetWindowPos( hwnd, self.last_positioned.unwrap_or(win::HWND_TOP), inner_position.start.try_into().unwrap(), inner_position.top.try_into().unwrap(), inner_size.width.try_into().unwrap(), inner_size.height.try_into().unwrap(), 0, ); Gdi::InvalidateRect(hwnd, std::ptr::null(), 1); } self.last_positioned = Some(hwnd); } // Reposition content. match &element.element_type { model::ElementType::VBox(model::VBox { items, spacing }) => { let mut position = inner_position; let mut size = inner_size; for item in items { self.reposition(item, &position, &size); let consumed = self.get_size(item).height + Dpi::context_scale(*spacing); if item.style.vertical_alignment != Alignment::End { position.top += consumed; } size.height = size.height.saturating_sub(consumed); } } model::ElementType::HBox(model::HBox { items, spacing, // The default ordering matches the windows platform order affirmative_order: _, }) => { let mut position = inner_position; let mut size = inner_size; for item in items { self.reposition(item, &position, &inner_size); let consumed = self.get_size(item).width + Dpi::context_scale(*spacing); if item.style.horizontal_alignment != Alignment::End { position.start += consumed; } size.width = size.width.saturating_sub(consumed); } } model::ElementType::Scroll(model::Scroll { content: Some(content), }) => { self.reposition(content, &inner_position, &inner_size); } _ => (), } } /// The `size` represents the maximum size permitted for the text (which is used for word /// breaking), and it will be set to the precise width and height of the text. The width should /// not exceed the input `size` width, but the height may. fn measure_text(&mut self, text: &str, element: &Element, size: &mut Size) { let Some(&window) = self.elements.get(&ElementRef::new(element)) else { return; }; let hdc = unsafe { Gdi::GetDC(window) }; unsafe { Gdi::SelectObject(hdc, win::SendMessageW(window, win::WM_GETFONT, 0, 0) as _) }; let mut height: u32 = 0; let mut max_width: u32 = 0; let mut char_fit = 0i32; let mut win_size = unsafe { std::mem::zeroed::() }; for mut line in text.lines() { if line.is_empty() { line = " "; } let text = WideString::new(line); let mut text = text.as_slice(); let mut extents = vec![0i32; text.len()]; while !text.is_empty() { unsafe { Gdi::GetTextExtentExPointW( hdc, text.as_ptr(), text.len() as i32, size.width.try_into().unwrap(), &mut char_fit, extents.as_mut_ptr(), &mut win_size, ); } if char_fit == 0 { return; } let mut split = char_fit as usize; let mut split_end = split.saturating_sub(1); if (char_fit as usize) < text.len() { for i in (0..char_fit as usize).rev() { // FIXME safer utf16 handling? if text[i] == b' ' as u16 { split = i + 1; split_end = i.saturating_sub(1); break; } } } text = &text[split..]; max_width = std::cmp::max(max_width, extents[split_end].try_into().unwrap()); let measured_height: u32 = win_size.cy.try_into().unwrap(); height += measured_height; } } unsafe { Gdi::ReleaseDC(window, hdc) }; assert!(max_width <= size.width); size.width = max_width; size.height = height; } } #[derive(Debug, Default, Clone, Copy)] struct Size { pub width: u32, pub height: u32, } impl Size { pub fn inner_size(&self, style: &ElementStyle) -> Self { let mut ret = self.less_margin(&style.margin); if let Some(width) = style.horizontal_size_request { ret.width = Dpi::context_scale(width); } if let Some(height) = style.vertical_size_request { ret.height = Dpi::context_scale(height); } ret } pub fn from_content_size(&mut self, style: &ElementStyle, content_size: &Self) { if style.horizontal_size_request.map(Dpi::context_scale) < Some(content_size.width) && style.horizontal_alignment != Alignment::Fill { self.width = content_size.width; } if style.vertical_size_request.map(Dpi::context_scale) < Some(content_size.height) && style.vertical_alignment != Alignment::Fill { self.height = content_size.height; } } pub fn plus_margin(&self, margin: &Margin) -> Self { let mut ret = self.clone(); ret.width += Dpi::context_scale(margin.start + margin.end); ret.height += Dpi::context_scale(margin.top + margin.bottom); ret } pub fn less_margin(&self, margin: &Margin) -> Self { let mut ret = self.clone(); ret.width = ret .width .saturating_sub(Dpi::context_scale(margin.start + margin.end)); ret.height = ret .height .saturating_sub(Dpi::context_scale(margin.top + margin.bottom)); ret } } #[derive(Debug, Default, Clone, Copy)] struct Position { pub start: u32, pub top: u32, } impl Position { #[allow(dead_code)] pub fn plus_margin(&self, margin: &Margin) -> Self { let mut ret = self.clone(); ret.start = ret.start.saturating_sub(Dpi::context_scale(margin.start)); ret.top = ret.top.saturating_sub(Dpi::context_scale(margin.top)); ret } pub fn less_margin(&self, margin: &Margin) -> Self { let mut ret = self.clone(); ret.start += Dpi::context_scale(margin.start); ret.top += Dpi::context_scale(margin.top); ret } } fn is_visible(style: &ElementStyle) -> bool { match &style.visible { Property::Static(v) => *v, Property::Binding(s) => *s.borrow(), _ => true, } }