From d8bbc7858622b6d9c278469aab701ca0b609cddf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:35:49 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../client/app/src/ui/windows/layout.rs | 436 +++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 toolkit/crashreporter/client/app/src/ui/windows/layout.rs (limited to 'toolkit/crashreporter/client/app/src/ui/windows/layout.rs') diff --git a/toolkit/crashreporter/client/app/src/ui/windows/layout.rs b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs new file mode 100644 index 0000000000..7563b6b2f0 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/ui/windows/layout.rs @@ -0,0 +1,436 @@ +/* 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}, + 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, + 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) -> Self { + Layout { + elements, + 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) { + 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 = inner_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 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 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: 160, + height: 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 + 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 + 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 = width; + } + if let Some(height) = style.vertical_size_request { + ret.height = height; + } + ret + } + + pub fn from_content_size(&mut self, style: &ElementStyle, content_size: &Self) { + if style.horizontal_size_request.is_none() && style.horizontal_alignment != Alignment::Fill + { + self.width = content_size.width; + } + if style.vertical_size_request.is_none() && 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 += margin.start + margin.end; + ret.height += margin.top + margin.bottom; + ret + } + + pub fn less_margin(&self, margin: &Margin) -> Self { + let mut ret = self.clone(); + ret.width = ret.width.saturating_sub(margin.start + margin.end); + ret.height = ret.height.saturating_sub(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(margin.start); + ret.top = ret.top.saturating_sub(margin.top); + ret + } + + pub fn less_margin(&self, margin: &Margin) -> Self { + let mut ret = self.clone(); + ret.start += margin.start; + ret.top += margin.top; + ret + } +} + +fn is_visible(style: &ElementStyle) -> bool { + match &style.visible { + Property::Static(v) => *v, + Property::Binding(s) => *s.borrow(), + _ => true, + } +} -- cgit v1.2.3