use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use std::time::Duration;
use crate::backend::{
Backend, BackendError, EditorPaintResult, PlatformServices, PointerShape, ServiceResult,
WindowControl,
};
use crate::dispatch::{DoubleClickDetector, DragState, TextRegion};
use crate::event::{Point, Rect, UiEvent, Viewport};
#[cfg(target_os = "windows")]
use crate::generic_font::GenericFamily;
use crate::modal_stack::ModalStack;
use crate::paint_surface::PaintSurface;
use crate::primitives::activity_bar::ActivityBarRowHit;
use crate::primitives::command_center::{CommandCenter, CommandCenterHit, CommandCenterLayout};
use crate::primitives::completions::{Completions, CompletionsLayout};
use crate::primitives::context_menu::{ContextMenu, ContextMenuLayout};
use crate::primitives::dialog::{Dialog, DialogLayout};
use crate::primitives::editor::Editor;
use crate::primitives::find_replace::FindReplacePanel;
use crate::primitives::form::{Form, FormLayout};
use crate::primitives::menu_bar::{MenuBar, MenuBarLayout};
use crate::primitives::message_list::MessageList;
use crate::primitives::multi_section_view::{
MsvLayoutMetrics, MultiSectionView, MultiSectionViewLayout,
};
use crate::primitives::panel::{Panel, PanelLayout};
use crate::primitives::progress::{ProgressBar, ProgressBarLayout};
use crate::primitives::rich_text_popup::{RichTextPopup, RichTextPopupLayout};
use crate::primitives::scrollbar::Scrollbar;
use crate::primitives::spinner::{Spinner, SpinnerLayout};
use crate::primitives::split::{Split, SplitLayout};
use crate::primitives::status_bar::{StatusBarHit, StatusBarLayout};
#[cfg(target_os = "windows")]
use crate::primitives::toolbar::ToolbarButton;
#[allow(deprecated)]
use crate::primitives::tab_bar::TabBarHits;
use crate::primitives::text_display::{TextDisplayLayout, TextDisplayLineMeasure};
use crate::primitives::toast::{ToastOverlay, ToastStackLayout};
use crate::primitives::tooltip::{Tooltip, TooltipLayout};
use crate::primitives::tree::TreeViewLayout;
use crate::types::WidgetId;
use crate::{
Accelerator, AcceleratorId, ActivityBar, Key, ListView, Modifiers, Palette, StatusBar, TabBar,
TabBarLayout, Terminal, TextDisplay, TooltipChrome, TreeView, UserPayload,
};
#[cfg(target_os = "windows")]
use crate::{FieldKind, Theme};
#[cfg(test)]
use crate::AcceleratorScope;
use super::services::WinPlatformServices;
fn nominal_text_width(text: &str, char_width: f32) -> f32 {
text.chars().count() as f32 * char_width
}
struct NominalTextMeasure {
char_width: f32,
}
impl crate::primitives::layout_metrics::TextMeasure for NominalTextMeasure {
fn width_of(&self, text: &str) -> f32 {
nominal_text_width(text, self.char_width)
}
}
impl super::status_bar::StatusMeasure for NominalTextMeasure {
fn width_of(&self, text: &str, _bold: bool) -> f32 {
nominal_text_width(text, self.char_width)
}
}
struct NominalSpinnerMeasure {
char_width: f32,
line_height: f32,
}
impl super::spinner::SpinnerMeasureSource for NominalSpinnerMeasure {
fn measure(&self, text: &str) -> (f32, f32) {
(nominal_text_width(text, self.char_width), self.line_height)
}
}
#[cfg(target_os = "windows")]
use windows::core::Result as WinResult;
#[cfg(target_os = "windows")]
use windows::Win32::Foundation::{HWND, LPARAM, RECT, WPARAM};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct2D::Common::{
D2D1_ALPHA_MODE_UNKNOWN, D2D1_PIXEL_FORMAT, D2D_SIZE_U,
};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct2D::{
D2D1CreateFactory, ID2D1DCRenderTarget, ID2D1Factory, ID2D1HwndRenderTarget, ID2D1RenderTarget,
D2D1_FACTORY_TYPE_SINGLE_THREADED, D2D1_HWND_RENDER_TARGET_PROPERTIES,
D2D1_RENDER_TARGET_PROPERTIES,
};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::DirectWrite::{IDWriteFontCollection1, IDWriteFontFallback};
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_B8G8R8A8_UNORM;
#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Gdi::{
GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST,
};
#[cfg(target_os = "windows")]
use windows::Win32::UI::HiDpi::GetDpiForWindow;
#[cfg(target_os = "windows")]
use windows::Win32::UI::WindowsAndMessaging::{
GetClientRect, GetWindowLongPtrW, GetWindowRect, IsZoomed, LoadCursorW, PostMessageW,
SetCursor, SetForegroundWindow, SetTimer, SetWindowLongPtrW, SetWindowPos, SetWindowTextW,
ShowWindow, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, IDC_ARROW, IDC_SIZENESW, IDC_SIZENS,
IDC_SIZENWSE, IDC_SIZEWE, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
SWP_NOZORDER, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, WS_CAPTION,
WS_OVERLAPPEDWINDOW,
};
#[cfg(target_os = "windows")]
use super::text::{new_brush_cache, BrushCache, DWrite};
#[cfg(target_os = "windows")]
const DEFAULT_EDITOR_FONT_FAMILY: &str = "Consolas";
#[cfg(target_os = "windows")]
const DEFAULT_UI_FONT_FAMILY: &str = "Segoe UI";
#[cfg(target_os = "windows")]
const DEFAULT_UI_FONT_SIZE_PT: f32 = 11.0;
#[cfg(target_os = "windows")]
fn parse_ui_font_desc(desc: &str) -> (String, f32) {
let desc = desc.trim();
let (family_part, size_pt) = match desc.rfind(' ') {
Some(idx) if desc[idx + 1..].trim().parse::<f32>().is_ok() => {
let (family, size_str) = desc.split_at(idx);
(
family.trim(),
size_str
.trim()
.parse::<f32>()
.expect("just matched by the guard above"),
)
}
_ => (desc, DEFAULT_UI_FONT_SIZE_PT),
};
let first = family_part
.split(',')
.map(str::trim)
.find(|f| !f.is_empty());
match first {
None => (DEFAULT_UI_FONT_FAMILY.to_string(), DEFAULT_UI_FONT_SIZE_PT),
Some(candidate) => match GenericFamily::parse(candidate) {
Some(GenericFamily::Monospace) => (DEFAULT_EDITOR_FONT_FAMILY.to_string(), size_pt),
Some(GenericFamily::SansSerif | GenericFamily::SystemUi) => {
(DEFAULT_UI_FONT_FAMILY.to_string(), size_pt)
}
None => (candidate.to_string(), size_pt),
},
}
}
#[cfg(target_os = "windows")]
enum RenderTarget {
Hwnd(ID2D1HwndRenderTarget),
Dc(ID2D1DCRenderTarget),
}
#[cfg(target_os = "windows")]
impl std::ops::Deref for RenderTarget {
type Target = ID2D1RenderTarget;
fn deref(&self) -> &ID2D1RenderTarget {
match self {
RenderTarget::Hwnd(target) => target,
RenderTarget::Dc(target) => target,
}
}
}
#[cfg(target_os = "windows")]
struct Surface {
#[allow(dead_code)]
factory: Option<ID2D1Factory>,
target: RenderTarget,
brushes: BrushCache,
}
const WIN_DOUBLE_CLICK_RADIUS: f32 = 4.0;
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) const WM_QUADRAUI_USER_EVENT: u32 = 0x8000 + 1;
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) const WM_QUADRAUI_TRAY_CALLBACK: u32 = 0x8000 + 2;
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) const FRAME_TIMER_ID: usize = 3;
pub struct WinBackend {
viewport: Viewport,
modal_stack: Rc<RefCell<ModalStack>>,
drag_state: Rc<RefCell<DragState>>,
double_click: DoubleClickDetector,
core: crate::backend_core::BackendCore,
focused_activity_bar: Option<WidgetId>,
events: Rc<RefCell<VecDeque<UiEvent>>>,
services: WinPlatformServices,
current_line_height: f32,
current_char_width: f32,
minimap_scale: crate::primitives::minimap::MinimapScale,
#[cfg(target_os = "windows")]
dpi_scale: f32,
#[cfg(target_os = "windows")]
surface: Option<Surface>,
#[cfg(target_os = "windows")]
headless_target: Option<ID2D1DCRenderTarget>,
#[cfg(target_os = "windows")]
hwnd: Option<HWND>,
#[cfg(target_os = "windows")]
tray: super::tray::WinTrayState,
#[cfg(target_os = "windows")]
fullscreen_saved: Option<(RECT, isize)>,
#[cfg(target_os = "windows")]
hwnd_raw: std::sync::Arc<std::sync::atomic::AtomicIsize>,
#[cfg(target_os = "windows")]
dwrite: Option<DWrite>,
#[cfg(target_os = "windows")]
editor_font_family: String,
#[cfg(target_os = "windows")]
editor_font_size_pt: f32,
#[cfg(target_os = "windows")]
chrome_dwrite: Option<DWrite>,
#[cfg(target_os = "windows")]
ui_font_family: String,
#[cfg(target_os = "windows")]
ui_font_size_pt: f32,
#[cfg(target_os = "windows")]
nerd_font_fallback: Option<IDWriteFontFallback>,
#[cfg(target_os = "windows")]
registered_font_collection: Option<IDWriteFontCollection1>,
#[cfg(target_os = "windows")]
registered_font_bytes: Vec<Vec<u8>>,
current_theme: crate::theme::Theme,
current_style: crate::Style,
current_pointer_shape: PointerShape,
painted_text_recording: bool,
text_runs: Vec<crate::testing::TextRun>,
nerd_fonts_enabled: bool,
last_error: Option<BackendError>,
focus: crate::focus::FocusManager,
user_events: std::sync::Arc<crate::runtime::UserEventQueue>,
pub(crate) zones: Vec<crate::testing::ZoneRec>,
frame_scheduler: crate::runtime::FrameScheduler,
}
impl WinBackend {
pub fn new() -> Self {
Self {
viewport: Viewport::new(0.0, 0.0, 1.0),
modal_stack: Rc::new(RefCell::new(ModalStack::new())),
drag_state: Rc::new(RefCell::new(DragState::new())),
double_click: DoubleClickDetector::with_radius(WIN_DOUBLE_CLICK_RADIUS),
core: crate::backend_core::BackendCore::default(),
focused_activity_bar: None,
events: Rc::new(RefCell::new(VecDeque::new())),
services: WinPlatformServices::new(),
current_line_height: 16.0,
current_char_width: 8.0,
minimap_scale: crate::primitives::minimap::MinimapScale::default(),
#[cfg(target_os = "windows")]
dpi_scale: 1.0,
#[cfg(target_os = "windows")]
surface: None,
#[cfg(target_os = "windows")]
headless_target: None,
#[cfg(target_os = "windows")]
hwnd: None,
#[cfg(target_os = "windows")]
tray: super::tray::WinTrayState::default(),
#[cfg(target_os = "windows")]
fullscreen_saved: None,
#[cfg(target_os = "windows")]
hwnd_raw: std::sync::Arc::new(std::sync::atomic::AtomicIsize::new(0)),
#[cfg(target_os = "windows")]
dwrite: None,
#[cfg(target_os = "windows")]
editor_font_family: DEFAULT_EDITOR_FONT_FAMILY.to_string(),
#[cfg(target_os = "windows")]
editor_font_size_pt: 11.0,
#[cfg(target_os = "windows")]
chrome_dwrite: None,
#[cfg(target_os = "windows")]
ui_font_family: DEFAULT_UI_FONT_FAMILY.to_string(),
#[cfg(target_os = "windows")]
ui_font_size_pt: DEFAULT_UI_FONT_SIZE_PT,
#[cfg(target_os = "windows")]
nerd_font_fallback: None,
#[cfg(target_os = "windows")]
registered_font_collection: None,
#[cfg(target_os = "windows")]
registered_font_bytes: Vec::new(),
current_theme: crate::theme::Theme::default(),
current_style: crate::Style::default(),
current_pointer_shape: PointerShape::Default,
painted_text_recording: false,
text_runs: Vec::new(),
nerd_fonts_enabled: false,
last_error: None,
focus: crate::focus::FocusManager::new(),
user_events: crate::runtime::UserEventQueue::new(),
zones: Vec::new(),
frame_scheduler: crate::runtime::FrameScheduler::new(),
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn drain_user_events(&mut self) -> Vec<UiEvent> {
let mut out = Vec::new();
self.user_events.drain_into(&mut out);
out
}
pub fn set_current_line_height(&mut self, line_height: f32) {
self.current_line_height = line_height;
}
pub fn set_current_char_width(&mut self, char_width: f32) {
self.current_char_width = char_width;
}
pub fn set_current_theme(&mut self, theme: crate::theme::Theme) {
self.current_theme = theme;
}
pub fn current_theme(&self) -> &crate::theme::Theme {
&self.current_theme
}
pub fn set_current_style(&mut self, style: crate::Style) {
self.current_style = style;
}
pub fn current_style(&self) -> &crate::Style {
&self.current_style
}
#[cfg(target_os = "windows")]
pub fn chrome_dwrite(&self) -> Option<&DWrite> {
self.chrome_dwrite.as_ref()
}
#[cfg(target_os = "windows")]
pub(crate) fn attach_surface(&mut self, hwnd: HWND) -> WinResult<()> {
let factory: ID2D1Factory =
unsafe { D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, None)? };
let mut rect = RECT::default();
unsafe { GetClientRect(hwnd, &mut rect)? };
let width = (rect.right - rect.left).max(1) as u32;
let height = (rect.bottom - rect.top).max(1) as u32;
let render_props = D2D1_RENDER_TARGET_PROPERTIES {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_UNKNOWN,
},
..Default::default()
};
let hwnd_props = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd,
pixelSize: D2D_SIZE_U { width, height },
..Default::default()
};
let target = unsafe { factory.CreateHwndRenderTarget(&render_props, &hwnd_props)? };
self.dpi_scale = dpi_scale_for_window(hwnd);
self.viewport = Viewport::new(width as f32, height as f32, self.dpi_scale);
self.surface = Some(Surface {
factory: Some(factory),
target: RenderTarget::Hwnd(target),
brushes: new_brush_cache(),
});
self.hwnd = Some(hwnd);
self.hwnd_raw
.store(hwnd.0 as isize, std::sync::atomic::Ordering::Release);
self.services.set_window(hwnd);
let (dwrite, line_height, char_width) = DWrite::new(
&self.editor_font_family,
self.editor_font_size_pt,
self.nerd_font_fallback.as_ref(),
)?;
self.dwrite = Some(dwrite);
self.current_line_height = line_height;
self.current_char_width = char_width;
let (chrome_dwrite, _, _) = DWrite::new(
&self.ui_font_family,
self.ui_font_size_pt,
self.nerd_font_fallback.as_ref(),
)?;
self.chrome_dwrite = Some(chrome_dwrite);
Ok(())
}
#[cfg(target_os = "windows")]
pub(crate) fn attach_headless(
&mut self,
target: ID2D1DCRenderTarget,
width: u32,
height: u32,
) -> WinResult<()> {
self.dpi_scale = 1.0;
self.viewport = Viewport::new(width as f32, height as f32, self.dpi_scale);
self.headless_target = Some(target.clone());
self.surface = Some(Surface {
factory: None,
target: RenderTarget::Dc(target),
brushes: new_brush_cache(),
});
let (dwrite, line_height, char_width) = DWrite::new(
&self.editor_font_family,
self.editor_font_size_pt,
self.nerd_font_fallback.as_ref(),
)?;
self.dwrite = Some(dwrite);
self.current_line_height = line_height;
self.current_char_width = char_width;
let (chrome_dwrite, _, _) = DWrite::new(
&self.ui_font_family,
self.ui_font_size_pt,
self.nerd_font_fallback.as_ref(),
)?;
self.chrome_dwrite = Some(chrome_dwrite);
Ok(())
}
#[cfg(target_os = "windows")]
pub fn measure_text(&self, text: &str) -> (f32, f32) {
self.dwrite
.as_ref()
.and_then(|d| d.measure_text(text).ok())
.unwrap_or((0.0, 0.0))
}
#[cfg(target_os = "windows")]
pub fn draw_text(&self, text: &str, rect: Rect, color: crate::Color) {
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let _ = dwrite.draw_text(&surface.target, text, rect, color);
}
}
#[cfg(target_os = "windows")]
pub(crate) fn ensure_surface(&mut self) -> WinResult<()> {
if self.surface.is_some() {
return Ok(());
}
if let Some(target) = self.headless_target.clone() {
self.surface = Some(Surface {
factory: None,
target: RenderTarget::Dc(target),
brushes: new_brush_cache(),
});
return Ok(());
}
match self.hwnd {
Some(hwnd) => self.attach_surface(hwnd),
None => Ok(()),
}
}
#[cfg(target_os = "windows")]
pub(crate) fn drop_surface_for_testing(&mut self) {
self.surface = None;
}
#[cfg(target_os = "windows")]
pub(crate) fn resize_surface(&mut self, width: u32, height: u32) -> WinResult<()> {
let width = width.max(1);
let height = height.max(1);
if let Some(surface) = &self.surface {
if let RenderTarget::Hwnd(target) = &surface.target {
let size = D2D_SIZE_U { width, height };
unsafe { target.Resize(&size)? };
}
}
self.viewport = Viewport::new(width as f32, height as f32, self.dpi_scale);
Ok(())
}
#[cfg(target_os = "windows")]
pub(crate) fn set_dpi_scale(&mut self, scale: f32) {
self.dpi_scale = scale;
self.viewport.scale = scale;
}
pub(crate) fn focused_activity_bar_id(&self) -> Option<&WidgetId> {
self.focused_activity_bar.as_ref()
}
pub(crate) fn fold_double_click(&mut self, ev: UiEvent) -> UiEvent {
let mut events = [ev];
self.double_click.process(&mut events);
let [ev] = events;
ev
}
pub(crate) fn match_keypress(&self, key: &Key, modifiers: Modifiers) -> Option<AcceleratorId> {
self.core.match_keypress(key, modifiers)
}
pub(crate) fn apply_accelerators(&self, events: &mut [UiEvent]) {
self.core.apply_accelerators(events);
}
pub fn events_handle(&self) -> Rc<RefCell<VecDeque<UiEvent>>> {
self.events.clone()
}
pub fn push_event(&self, ev: UiEvent) {
self.events.borrow_mut().push_back(ev);
}
#[cfg(target_os = "windows")]
pub(crate) fn apply_current_cursor(&self) {
unsafe {
let _ = SetCursor(
LoadCursorW(
None,
pointer_shape_to_win32_cursor(self.current_pointer_shape),
)
.ok(),
);
}
}
pub const TITLE_BAR_DRAG_ZONE: &'static str = "app-shell:title-bar";
pub fn nc_hit_test(&self, x: f32, y: f32) -> Option<bool> {
let point = Point::new(x, y);
let band = self
.zones
.iter()
.find(|z| z.id.as_str() == Self::TITLE_BAR_DRAG_ZONE)?;
if !band.bounds.contains(point) {
return None;
}
let band_area = band.bounds.width * band.bounds.height;
let excluded = self.zones.iter().any(|z| {
z.id.as_str() != Self::TITLE_BAR_DRAG_ZONE
&& z.bounds.contains(point)
&& z.bounds.width * z.bounds.height < band_area
});
Some(!excluded)
}
fn register_menu_bar_item_zones(&mut self, layout: &MenuBarLayout) {
for item in &layout.visible_items {
if item.clickable {
self.register_zone(item.id.clone(), item.bounds);
}
}
}
fn register_status_bar_segment_zones(&mut self, rect: Rect, layout: &StatusBarLayout) {
for (bounds, hit) in &layout.hit_regions {
if let StatusBarHit::Segment(id) = hit {
self.register_zone(
id.clone(),
Rect::new(
rect.x + bounds.x,
rect.y + bounds.y,
bounds.width,
bounds.height,
),
);
}
}
}
fn register_command_center_zones(&mut self, cc_id: &WidgetId, layout: &CommandCenterLayout) {
for (bounds, hit) in &layout.hit_regions {
let suffix = match hit {
CommandCenterHit::Back => "back",
CommandCenterHit::Forward => "forward",
CommandCenterHit::SearchBox => "search",
CommandCenterHit::Bar | CommandCenterHit::Outside => continue,
};
self.register_zone(
WidgetId::new(format!("{}:{}", cc_id.as_str(), suffix)),
*bounds,
);
}
}
pub(crate) fn set_painted_text_recording(&mut self, enabled: bool) {
self.painted_text_recording = enabled;
}
pub(crate) fn text_runs(&self) -> &[crate::testing::TextRun] {
&self.text_runs
}
pub fn frame_requests(&self) -> u64 {
self.frame_scheduler.requests()
}
pub fn pending_frame_delay(&self) -> Option<Duration> {
self.frame_scheduler.pending_delay()
}
pub(crate) fn text_selection_state_mut(
&mut self,
) -> &mut crate::text_selection::TextSelectionState {
&mut self.core.text_selection
}
pub(crate) fn active_text_selection(
&self,
) -> Option<&crate::text_selection::ActiveTextSelection> {
self.core.active_text_selection()
}
pub(crate) fn set_active_text_selection(
&mut self,
region: WidgetId,
anchor: crate::event::Point,
focus: crate::event::Point,
) {
self.core.set_active_text_selection(region, anchor, focus);
}
pub(crate) fn clear_selection_display(&mut self) {
self.core.clear_selection_display();
}
pub(crate) fn clear_text_selection(&mut self) {
let mut drag = self.drag_state.borrow_mut();
self.core.clear_text_selection(&mut drag);
}
fn cancel_text_selection_drag_impl(&mut self) {
let mut drag = self.drag_state.borrow_mut();
self.core.cancel_text_selection_drag(&mut drag);
}
pub(crate) fn select_all_text_region(&mut self) -> bool {
self.core.select_all_text_region()
}
pub(crate) fn extract_selection_text(&self) -> String {
self.core
.extract_selection_text_pixel(self.current_line_height, self.current_char_width)
}
pub(crate) fn apply_selection_highlight(&self) {
let Some(rects) = self
.core
.selection_highlight_rects(self.current_line_height, self.current_char_width)
else {
return;
};
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let highlight = self.current_theme.text_selection_highlight();
for rect in rects {
let _ = super::text::fill_rect(&surface.target, rect, highlight);
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = rects;
}
}
}
#[cfg(target_os = "windows")]
fn dpi_scale_for_window(hwnd: HWND) -> f32 {
crate::win::msg::dpi_ratio(unsafe { GetDpiForWindow(hwnd) })
}
#[cfg(target_os = "windows")]
fn live_hwnd(hwnd_raw: &std::sync::atomic::AtomicIsize) -> Option<HWND> {
let raw = hwnd_raw.load(std::sync::atomic::Ordering::Acquire);
if raw == 0 {
None
} else {
Some(HWND(raw as *mut core::ffi::c_void))
}
}
#[cfg(target_os = "windows")]
fn pointer_shape_to_win32_cursor(shape: PointerShape) -> windows::core::PCWSTR {
use crate::backend::ResizeEdge;
match shape {
PointerShape::Default => IDC_ARROW,
PointerShape::Resize(ResizeEdge::North) | PointerShape::Resize(ResizeEdge::South) => {
IDC_SIZENS
}
PointerShape::Resize(ResizeEdge::East) | PointerShape::Resize(ResizeEdge::West) => {
IDC_SIZEWE
}
PointerShape::Resize(ResizeEdge::NorthEast)
| PointerShape::Resize(ResizeEdge::SouthWest) => IDC_SIZENESW,
PointerShape::Resize(ResizeEdge::NorthWest)
| PointerShape::Resize(ResizeEdge::SouthEast) => IDC_SIZENWSE,
}
}
impl Default for WinBackend {
fn default() -> Self {
Self::new()
}
}
impl crate::runtime::PreprocessBackend for WinBackend {
fn active_text_selection(&self) -> Option<&crate::text_selection::ActiveTextSelection> {
self.active_text_selection()
}
fn set_active_text_selection(&mut self, region: WidgetId, anchor: Point, focus: Point) {
self.set_active_text_selection(region, anchor, focus)
}
fn clear_text_selection(&mut self) {
self.clear_text_selection()
}
fn clear_selection_display(&mut self) {
self.clear_selection_display()
}
fn select_all_text_region(&mut self) -> bool {
self.select_all_text_region()
}
fn selection_text_for_copy(&self) -> String {
self.extract_selection_text()
}
fn focused_activity_bar_id(&self) -> Option<&WidgetId> {
self.focused_activity_bar_id()
}
fn focus_manager_mut(&mut self) -> &mut crate::focus::FocusManager {
&mut self.focus
}
fn match_keypress(
&self,
key: &crate::Key,
modifiers: crate::Modifiers,
) -> Option<AcceleratorId> {
self.match_keypress(key, modifiers)
}
fn fold_double_click(&mut self, ev: UiEvent) -> UiEvent {
self.fold_double_click(ev)
}
}
impl crate::backend::sealed::Sealed for WinBackend {}
impl Backend for WinBackend {
fn viewport(&self) -> Viewport {
self.viewport
}
fn begin_frame(&mut self, viewport: Viewport) {
self.viewport = viewport;
self.focused_activity_bar = None;
self.core.begin_frame();
self.zones.clear();
if self.painted_text_recording {
let _ = crate::testing::install_text_run_sink();
}
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
unsafe {
surface.target.BeginDraw();
}
super::text::activate_brush_cache(&surface.target, &surface.brushes);
let clear_color = super::text::color_to_d2d(self.current_theme.background);
unsafe {
surface.target.Clear(Some(&clear_color));
}
}
#[cfg(not(target_os = "windows"))]
{}
}
fn end_frame(&mut self) {
if self.painted_text_recording {
self.text_runs = crate::testing::take_text_run_sink(None);
}
#[cfg(target_os = "windows")]
super::text::deactivate_brush_cache();
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
if unsafe { surface.target.EndDraw(None, None) }.is_err() {
self.surface = None;
self.last_error = Some(BackendError::SurfaceLost);
crate::diagnostics::emit(
"quadraui win backend: EndDraw failed (device lost / \
D2DERR_RECREATE_TARGET) — render target dropped, will \
recreate via ensure_surface on the next frame",
);
}
}
#[cfg(not(target_os = "windows"))]
{}
}
fn set_theme(&mut self, theme: crate::Theme) {
self.set_current_theme(theme);
}
fn theme(&self) -> crate::Theme {
self.current_theme
}
fn set_style(&mut self, style: crate::Style) {
self.set_current_style(style);
}
fn style(&self) -> crate::Style {
self.current_style
}
fn set_nerd_fonts(&mut self, enabled: bool) {
self.nerd_fonts_enabled = enabled;
}
fn nerd_fonts_enabled(&self) -> bool {
self.nerd_fonts_enabled
}
fn set_ui_font(&mut self, font_desc: &str) {
#[cfg(target_os = "windows")]
{
let (family, size_pt) = parse_ui_font_desc(font_desc);
self.ui_font_family = family;
self.ui_font_size_pt = size_pt;
}
#[cfg(not(target_os = "windows"))]
{
let _ = font_desc;
}
}
fn default_fonts(&self) -> crate::backend::PlatformFontDefaults {
crate::backend::PlatformFontDefaults {
editor_family: "Consolas".to_string(),
editor_size_pt: 14.0,
ui_family: "Segoe UI".to_string(),
ui_size_pt: 13.0,
}
}
fn register_font_from_memory(&mut self, bytes: &[u8]) -> Option<Vec<String>> {
#[cfg(target_os = "windows")]
{
self.registered_font_bytes.push(bytes.to_vec());
let owned: &[u8] = self
.registered_font_bytes
.last()
.expect("just pushed a value above");
match crate::win::text::register_font_from_memory(owned) {
Ok((collection, names)) => {
self.registered_font_collection = Some(collection);
Some(names)
}
Err(_) => {
None
}
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = bytes;
None
}
}
fn has_font_family(&self, family: &str) -> Option<bool> {
#[cfg(target_os = "windows")]
{
crate::win::text::has_font_family(family).ok()
}
#[cfg(not(target_os = "windows"))]
{
let _ = family;
None
}
}
fn set_nerd_font_fallback(&mut self, family: &str) {
#[cfg(target_os = "windows")]
{
let built = crate::win::text::build_nerd_font_fallback(
family,
self.registered_font_collection.as_ref(),
);
match built {
Ok(fallback) => self.nerd_font_fallback = Some(fallback),
Err(err) => {
crate::diagnostics::emit(format!(
"quadraui: set_nerd_font_fallback({family:?}) failed to build a font \
fallback ({err:?}); icon glyphs may render as tofu"
));
}
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = family;
}
}
fn poll_events(&mut self) -> Vec<UiEvent> {
let mut out: Vec<UiEvent> = self.events.borrow_mut().drain(..).collect();
self.apply_accelerators(&mut out);
self.user_events.drain_into(&mut out);
out
}
fn wait_events(&mut self, _timeout: Duration) -> Vec<UiEvent> {
self.poll_events()
}
fn waker(&self) -> std::sync::Arc<dyn Fn(UserPayload) + Send + Sync> {
let queue = std::sync::Arc::clone(&self.user_events);
#[cfg(target_os = "windows")]
{
let hwnd_raw = std::sync::Arc::clone(&self.hwnd_raw);
std::sync::Arc::new(move |payload: UserPayload| {
queue.push(payload.into_arc());
if let Some(hwnd) = live_hwnd(&hwnd_raw) {
unsafe {
let _ =
PostMessageW(Some(hwnd), WM_QUADRAUI_USER_EVENT, WPARAM(0), LPARAM(0));
}
}
})
}
#[cfg(not(target_os = "windows"))]
{
std::sync::Arc::new(move |payload: UserPayload| {
queue.push(payload.into_arc());
})
}
}
fn request_frame_in(&self, delay: Duration) {
self.frame_scheduler.request(delay);
#[cfg(target_os = "windows")]
{
if let Some(hwnd) = self.hwnd {
unsafe {
SetTimer(
Some(hwnd),
FRAME_TIMER_ID,
delay.as_millis().min(u32::MAX as u128) as u32,
None,
);
}
}
}
}
fn register_accelerator(&mut self, acc: &Accelerator) {
self.core.register_accelerator(acc);
}
fn unregister_accelerator(&mut self, id: &AcceleratorId) {
self.core.unregister_accelerator(id);
}
fn menu_style(&self) -> crate::backend::MenuStyle {
self.core.menu_style()
}
fn set_menu_style(&mut self, style: crate::backend::MenuStyle) {
self.core.set_menu_style(style);
}
fn register_text_region(&mut self, region: TextRegion) {
self.core.register_text_region(region);
}
fn cancel_text_selection_drag(&mut self) {
self.cancel_text_selection_drag_impl();
}
fn register_zone(&mut self, id: WidgetId, bounds: Rect) {
self.zones.push(crate::testing::ZoneRec { id, bounds });
}
fn modal_stack_handle(&self) -> Rc<RefCell<ModalStack>> {
self.modal_stack.clone()
}
fn drag_state_handle(&self) -> Rc<RefCell<DragState>> {
self.drag_state.clone()
}
fn focus_manager(&self) -> &crate::focus::FocusManager {
&self.focus
}
fn draw_focus_ring(&mut self, rect: Rect) {
let theme = self.current_theme;
let stroke_width = self.current_style.focus_ring_width;
self.surface_stroke_rect(rect, theme.accent_fg, stroke_width);
self.register_zone(WidgetId::new("chrome:focus-ring"), rect);
}
fn last_error(&mut self) -> Option<BackendError> {
self.last_error.take()
}
fn services(&self) -> &dyn PlatformServices {
&self.services
}
fn paint_surface(&mut self) -> Option<&mut dyn PaintSurface> {
Some(self)
}
fn set_cursor(&mut self, shape: PointerShape) -> bool {
self.current_pointer_shape = shape;
#[cfg(target_os = "windows")]
{
if self.hwnd.is_none() {
return false;
}
self.apply_current_cursor();
true
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
fn window(&mut self) -> Option<&mut dyn crate::backend::WindowControl> {
#[cfg(target_os = "windows")]
{
self.hwnd?;
Some(self)
}
#[cfg(not(target_os = "windows"))]
{
None
}
}
fn tray(&mut self) -> Option<&mut dyn crate::backend::TrayService> {
#[cfg(target_os = "windows")]
{
self.hwnd?;
Some(self)
}
#[cfg(not(target_os = "windows"))]
{
None
}
}
fn backend_caps(&self) -> crate::backend::BackendCaps {
#[cfg(target_os = "windows")]
{
crate::backend::BackendCaps {
file_dialogs: true,
folder_dialogs: true,
native_dialogs: true,
notifications: true,
pointer_cursor: true,
mouse: true,
scroll: true,
drag: true,
text_selection: true,
app_font_registration: true,
generic_font_families: true,
window_control: true,
tray: true,
window_chrome: true,
..crate::backend::BackendCaps::empty()
}
}
#[cfg(not(target_os = "windows"))]
{
crate::backend::BackendCaps::empty()
}
}
fn toggle_window_maximize(&mut self) -> bool {
#[cfg(target_os = "windows")]
{
let Some(hwnd) = self.hwnd else {
return false;
};
let target = if unsafe { IsZoomed(hwnd) }.as_bool() {
SW_RESTORE
} else {
SW_MAXIMIZE
};
unsafe {
let _ = ShowWindow(hwnd, target);
}
true
}
#[cfg(not(target_os = "windows"))]
{
false
}
}
fn line_height(&self) -> f32 {
self.current_line_height
}
fn char_width(&self) -> f32 {
self.current_char_width
}
fn set_current_line_height(&mut self, line_height: f32) {
WinBackend::set_current_line_height(self, line_height);
}
fn set_current_char_width(&mut self, char_width: f32) {
WinBackend::set_current_char_width(self, char_width);
}
fn list_char_width(&self) -> f32 {
self.current_char_width
}
fn measure_text(&self, text: &str, role: crate::FontRole) -> (f32, f32) {
#[cfg(target_os = "windows")]
{
let dwrite = match role {
crate::FontRole::Chrome => self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()),
crate::FontRole::Editor => self.dwrite.as_ref(),
};
if let Some(dwrite) = dwrite {
if let Ok((w, h)) = dwrite.measure_text(text) {
return (w, h);
}
}
}
let _ = role;
(
text.chars().count() as f32 * self.current_char_width,
self.current_line_height,
)
}
fn set_editor_font(&mut self, family: &str, size_pt: f32) {
#[cfg(target_os = "windows")]
{
self.editor_font_family = match GenericFamily::parse(family) {
Some(GenericFamily::Monospace) => DEFAULT_EDITOR_FONT_FAMILY.to_string(),
Some(GenericFamily::SansSerif | GenericFamily::SystemUi) => {
DEFAULT_UI_FONT_FAMILY.to_string()
}
None => family.to_string(),
};
self.editor_font_size_pt = size_pt;
}
#[cfg(not(target_os = "windows"))]
{
let _ = (family, size_pt);
}
}
fn draw_tree(&mut self, rect: Rect, tree: &TreeView) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
super::tree::draw_tree(
&surface.target,
dwrite,
rect,
tree,
self.current_line_height,
self.nerd_fonts_enabled,
&self.current_theme,
);
return;
}
let _ = (rect, tree);
}
fn draw_list(&mut self, rect: Rect, list: &ListView) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
super::list::draw_list(
&surface.target,
dwrite,
rect,
list,
self.current_line_height,
);
return;
}
let _ = (rect, list);
}
fn draw_data_table(
&mut self,
rect: Rect,
table: &crate::DataTable,
hovered_idx: Option<usize>,
) -> crate::DataTableLayout {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
return super::data_table::draw_data_table(
&surface.target,
dwrite,
rect,
table,
self.current_line_height,
hovered_idx,
);
}
let _ = hovered_idx;
super::data_table::win_data_table_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
table,
self.current_line_height,
)
}
fn data_table_layout(&self, rect: Rect, table: &crate::DataTable) -> crate::DataTableLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = &self.dwrite {
return super::data_table::win_data_table_layout(
dwrite,
rect,
table,
self.current_line_height,
);
}
super::data_table::win_data_table_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
table,
self.current_line_height,
)
}
fn list_hscrollbar(&self, rect: Rect, list: &ListView) -> Option<crate::Scrollbar> {
let char_w = self.current_char_width;
let max_w_chars = list.max_content_width? as f32;
let content_px = max_w_chars * char_w;
let border_inset = if list.bordered { char_w } else { 0.0 };
let visible_px = (rect.width - 2.0 * border_inset).max(0.0);
if content_px <= visible_px {
return None;
}
let row_h = self.current_line_height;
let track = crate::primitives::list::hscrollbar_track(rect, list.bordered, char_w, row_h);
Some(crate::Scrollbar::horizontal(
list.id.clone(),
track,
list.h_scroll as f32 * char_w,
content_px,
visible_px,
row_h,
))
}
fn list_vscrollbar(&self, rect: Rect, list: &ListView) -> Option<crate::Scrollbar> {
list.vscrollbar(rect, self.current_line_height)
}
fn list_layout(&self, rect: Rect, list: &ListView) -> crate::ListViewLayout {
super::list::win_list_layout(
list,
rect,
self.current_line_height,
self.current_char_width,
)
}
fn draw_form(&mut self, rect: Rect, form: &Form) {
#[cfg(target_os = "windows")]
if self.dwrite.is_some() && self.surface.is_some() {
let dwrite = self
.dwrite
.as_ref()
.expect("checked Some by the `if` guard above");
let flayout =
super::form::win_form_layout(dwrite, rect, form, self.current_line_height);
let theme = Theme::default();
let origin = Point::new(rect.x, rect.y);
crate::primitives::form::paint(form, &flayout, self, &theme, origin);
for vf in &flayout.visible_fields {
let Some(field) = form.fields.get(vf.field_idx) else {
continue;
};
let FieldKind::Toolbar(toolbar) = &field.kind else {
continue;
};
let field_fg = if field.disabled {
theme.muted_fg
} else {
theme.foreground
};
for (item_id, item_rect) in &vf.item_bounds {
let btn = toolbar.buttons.iter().find_map(|b| {
super::form::toolbar_item(&field.id, b)
.filter(|(id, _)| id == item_id)
.map(|_| b)
});
let r = Rect::new(
origin.x + item_rect.x,
origin.y + item_rect.y,
item_rect.width,
item_rect.height,
);
match btn {
Some(ToolbarButton::Action { label, enabled, .. }) => {
let fg = if *enabled { field_fg } else { theme.muted_fg };
let (tw, th) = self.measure_text(label);
let ty = r.y + (r.height - th) / 2.0;
self.draw_text(label, Rect::new(r.x, ty, tw, th), fg);
}
Some(ToolbarButton::Label { text, fg }) => {
let color = fg.unwrap_or(field_fg);
let (tw, th) = self.measure_text(text);
let ty = r.y + (r.height - th) / 2.0;
self.draw_text(text, Rect::new(r.x, ty, tw, th), color);
}
_ => {}
}
}
}
return;
}
let _ = (rect, form);
}
fn draw_palette(&mut self, rect: Rect, palette: &Palette) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
super::palette::draw_palette(
&surface.target,
dwrite,
rect,
palette,
self.current_line_height,
self.nerd_fonts_enabled,
);
return;
}
let _ = (rect, palette);
}
fn palette_layout(&self, rect: Rect, palette: &Palette) -> crate::PaletteLayout {
crate::primitives::palette::native_surface_paint::layout(
rect.width,
rect.height,
palette,
self.current_line_height,
)
.0
}
fn draw_settings_chrome(
&mut self,
rect: Rect,
header_text: &str,
query: &str,
placeholder: &str,
active: bool,
) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
super::form::draw_settings_chrome(
&surface.target,
dwrite,
rect,
self.current_line_height,
header_text,
query,
placeholder,
active,
);
return;
}
let _ = (rect, header_text, query, placeholder, active);
}
fn draw_status_bar_interactive(
&mut self,
rect: Rect,
bar: &StatusBar,
interaction: &crate::interaction::InteractionState,
) -> StatusBarLayout {
let (hovered_id, pressed_id) = (interaction.hovered(), interaction.pressed());
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let theme = self.current_theme;
let mut surface = ChromeSurface { backend: self };
let layout = crate::primitives::status_bar::native_surface_paint::paint(
bar,
&mut surface,
&theme,
rect.x,
rect.y,
rect.width,
rect.height,
hovered_id,
pressed_id,
);
self.register_status_bar_segment_zones(rect, &layout);
return layout;
}
let _ = (hovered_id, pressed_id);
let layout = super::status_bar::win_status_bar_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
);
self.register_status_bar_segment_zones(rect, &layout);
layout
}
#[allow(deprecated)] fn draw_tab_bar(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
) -> TabBarHits {
self.draw_tab_bar_icons(rect, bar, &[], hovered_close_tab)
}
fn draw_tab_bar_layout(
&mut self,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
) -> TabBarLayout {
self.draw_tab_bar_icons_layout(rect, bar, &[], hovered_close_tab)
}
#[allow(deprecated)] fn draw_tab_bar_icons(
&mut self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
hovered_close_tab: Option<usize>,
) -> TabBarHits {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
return super::tab_bar::draw_tab_bar_icons(
&surface.target,
dwrite,
rect,
bar,
icons,
hovered_close_tab,
&self.current_theme,
);
}
let _ = hovered_close_tab;
super::tab_bar::win_tab_bar_layout_icons(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
icons,
)
}
fn draw_tab_bar_icons_layout(
&mut self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
hovered_close_tab: Option<usize>,
) -> TabBarLayout {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
return super::tab_bar::draw_tab_bar_icons_layout(
&surface.target,
dwrite,
rect,
bar,
icons,
hovered_close_tab,
&self.current_theme,
);
}
let _ = hovered_close_tab;
super::tab_bar::win_tab_bar_native_layout_icons(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
icons,
)
}
fn draw_activity_bar(
&mut self,
rect: Rect,
bar: &ActivityBar,
hovered_idx: Option<usize>,
) -> Vec<ActivityBarRowHit> {
if bar.is_keyboard_focused {
self.focused_activity_bar = Some(bar.id.clone());
}
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
return super::activity_bar::draw_activity_bar(
&surface.target,
dwrite,
rect,
bar,
hovered_idx,
&self.current_theme,
);
}
let _ = hovered_idx;
self.activity_bar_layout(rect, bar)
}
fn status_bar_layout(&self, rect: Rect, bar: &StatusBar) -> StatusBarLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()) {
return super::status_bar::win_status_bar_layout(dwrite, rect, bar);
}
super::status_bar::win_status_bar_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
)
}
#[allow(deprecated)] fn tab_bar_layout(&self, rect: Rect, bar: &TabBar) -> TabBarHits {
self.tab_bar_layout_icons(rect, bar, &[])
}
fn resolve_tab_bar_layout(&self, rect: Rect, bar: &TabBar) -> TabBarLayout {
self.resolve_tab_bar_layout_icons(rect, bar, &[])
}
#[allow(deprecated)] fn tab_bar_layout_icons(
&self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
) -> TabBarHits {
#[cfg(target_os = "windows")]
if let Some(dwrite) = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()) {
return super::tab_bar::win_tab_bar_layout_icons(dwrite, rect, bar, icons);
}
super::tab_bar::win_tab_bar_layout_icons(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
icons,
)
}
fn resolve_tab_bar_layout_icons(
&self,
rect: Rect,
bar: &TabBar,
icons: &[Option<crate::TabIcon>],
) -> TabBarLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()) {
return super::tab_bar::win_tab_bar_native_layout_icons(dwrite, rect, bar, icons);
}
super::tab_bar::win_tab_bar_native_layout_icons(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
icons,
)
}
fn activity_bar_layout(&self, rect: Rect, bar: &ActivityBar) -> Vec<ActivityBarRowHit> {
const ACTIVITY_ROW_DIP: f32 = 48.0;
bar.layout(rect.width, rect.height, ACTIVITY_ROW_DIP)
.visible_items
.into_iter()
.map(|vi| {
let item = match vi.side {
crate::primitives::activity_bar::ActivitySide::Top => {
&bar.top_items[vi.item_idx]
}
crate::primitives::activity_bar::ActivitySide::Bottom => {
&bar.bottom_items[vi.item_idx]
}
};
ActivityBarRowHit {
y_start: vi.bounds.y,
y_end: vi.bounds.y + vi.bounds.height,
id: item.id.clone(),
tooltip: item.tooltip.clone(),
}
})
.collect()
}
fn draw_terminal(&mut self, rect: Rect, term: &Terminal) {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let lh = self.current_line_height;
let cw = self.current_char_width;
let theme = self.current_theme;
let sb_width = match &term.scrollbar {
Some(sb) => sb.width.map(|w| w as f32).unwrap_or(8.0),
None => 0.0,
};
let cell_area_w = (rect.width - sb_width).max(0.0);
crate::primitives::terminal::paint(
term,
self,
&theme,
rect.x,
rect.y,
cell_area_w,
rect.height,
lh,
cw,
None,
);
if let Some(ref sb_state) = term.scrollbar {
let sb = crate::primitives::scrollbar::Scrollbar::vertical(
term.id.clone(),
Rect::new(rect.x + cell_area_w, rect.y, sb_width, rect.height),
sb_state.effective_scroll_offset() as f32,
sb_state.total_lines as f32,
sb_state.visible_lines as f32,
lh,
);
crate::primitives::scrollbar::native_surface_paint::paint(&sb, self, &theme);
}
return;
}
let _ = (rect, term);
}
fn draw_terminal_divider(&mut self, rect: Rect) {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let theme = self.current_theme;
crate::primitives::terminal::paint_divider(self, rect.x, rect.y, rect.height, &theme);
return;
}
let _ = rect;
}
fn draw_solid_fill(&mut self, rect: Rect, color: crate::Color) {
self.surface_fill_rect(rect, color);
self.register_zone(WidgetId::new("chrome:solid-fill"), rect);
}
fn draw_text_display(&mut self, rect: Rect, td: &TextDisplay) {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let theme = self.current_theme;
let line_height = self.current_line_height;
let char_width = self.current_char_width;
crate::primitives::text_display::paint(td, rect, self, &theme, line_height, char_width);
return;
}
let _ = (rect, td);
}
fn draw_command_line(
&mut self,
rect: Rect,
cmd: &crate::primitives::command_line::CommandLine,
) {
self.draw_command_line_selection(rect, cmd, None);
}
fn draw_command_line_selection(
&mut self,
rect: Rect,
cmd: &crate::primitives::command_line::CommandLine,
selection: Option<(usize, usize)>,
) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
super::command_line::draw_command_line_selection(
&surface.target,
dwrite,
rect,
cmd,
&self.current_theme,
self.current_char_width,
selection,
);
return;
}
let _ = (rect, cmd, selection);
}
fn command_line_layout(
&self,
rect: Rect,
cmd: &crate::primitives::command_line::CommandLine,
) -> crate::primitives::command_line::CommandLineLayout {
cmd.layout(
rect,
crate::primitives::command_line::CommandLineMeasure::new(self.current_char_width),
)
}
fn text_display_layout(&self, rect: Rect, td: &TextDisplay) -> TextDisplayLayout {
const SCROLLBAR_GUTTER_DIP: f32 = 12.0;
const SCROLLBAR_MIN_THUMB_DIP: f32 = 8.0;
let line_height = self.current_line_height;
let char_width = self.current_char_width;
let body_h = if td.title.is_some() {
(rect.height - line_height).max(0.0)
} else {
rect.height
};
if body_h <= 0.0 {
return td.layout(0.0, 0.0, |_| TextDisplayLineMeasure::new(line_height));
}
let body_width_px = if td.show_scrollbar {
(rect.width - SCROLLBAR_GUTTER_DIP).max(0.0)
} else {
rect.width
};
let col_budget = crate::primitives::text_display::px_to_cols(body_width_px, char_width);
let measure = |i: usize| {
let rows = crate::primitives::text_display::wrap_row_count(&td.lines[i], col_budget);
TextDisplayLineMeasure::new(rows as f32 * line_height)
};
if td.show_scrollbar {
td.layout_with_scrollbar(
rect.width,
body_h,
SCROLLBAR_GUTTER_DIP,
SCROLLBAR_MIN_THUMB_DIP,
measure,
)
} else {
td.layout(rect.width, body_h, measure)
}
}
fn draw_text_input(
&mut self,
rect: Rect,
ti: &crate::primitives::text_input::TextInput,
) -> crate::primitives::text_input::TextInputLayout {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
return super::text_input::draw_text_input(
&surface.target,
dwrite,
rect,
ti,
&self.current_theme,
self.current_line_height,
self.current_char_width,
);
}
ti.layout(
rect,
crate::primitives::text_input::TextInputMeasure::new(
self.current_line_height,
self.current_char_width,
),
)
}
fn text_input_layout(
&self,
rect: Rect,
ti: &crate::primitives::text_input::TextInput,
) -> crate::primitives::text_input::TextInputLayout {
ti.layout(
rect,
crate::primitives::text_input::TextInputMeasure::new(
self.current_line_height,
self.current_char_width,
),
)
}
fn draw_tooltip(&mut self, tooltip: &Tooltip, layout: &TooltipLayout) {
self.draw_tooltip_with_chrome(tooltip, layout, &TooltipChrome::default());
}
fn draw_tooltip_with_chrome(
&mut self,
tooltip: &Tooltip,
layout: &TooltipLayout,
chrome: &TooltipChrome,
) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
super::tooltip::draw_tooltip_with_chrome(
&surface.target,
dwrite,
tooltip,
layout,
chrome,
self.current_line_height,
self.current_char_width,
);
return;
}
let _ = (tooltip, layout, chrome);
}
fn draw_context_menu(
&mut self,
menu: &ContextMenu,
layout: &ContextMenuLayout,
) -> Vec<(Rect, WidgetId)> {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
return super::context_menu::draw_context_menu(
&surface.target,
dwrite,
menu,
layout,
&self.current_theme,
);
}
layout
.visible_items
.iter()
.filter(|vis| vis.clickable)
.filter_map(|vis| {
menu.items[vis.item_idx]
.id
.clone()
.map(|id| (vis.bounds, id))
})
.collect()
}
fn draw_dialog(&mut self, dialog: &Dialog, layout: &DialogLayout) -> Vec<Rect> {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let dwrite = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref());
if let Some(dwrite) = dwrite {
return super::dialog::draw_dialog(
&surface.target,
dwrite,
dialog,
layout,
self.current_line_height,
);
}
}
let _ = dialog;
layout
.visible_buttons
.iter()
.map(|vis| vis.bounds)
.collect()
}
fn draw_float(
&mut self,
float: &crate::primitives::float::Float,
layout: &crate::primitives::float::FloatLayout,
) {
self.modal_stack.borrow_mut().mark_painted(&float.id);
let theme = self.current_theme;
crate::primitives::float::native_surface_paint::paint(float, layout, self, &theme);
self.register_zone(float.id.clone(), layout.bounds);
}
fn draw_multi_section_view(&mut self, rect: Rect, view: &MultiSectionView) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
super::multi_section_view::draw_multi_section_view(
&surface.target,
dwrite,
rect,
view,
self.current_line_height,
self.current_char_width,
);
return;
}
let _ = (rect, view);
}
fn msv_layout(&self, rect: Rect, view: &MultiSectionView) -> MultiSectionViewLayout {
crate::primitives::layout_metrics::msv_layout(view, rect, self.current_line_height as f64)
}
fn msv_metrics(&self) -> MsvLayoutMetrics {
crate::primitives::layout_metrics::msv_metrics(self.current_line_height as f64, false)
}
fn tree_layout(&self, rect: Rect, tree: &TreeView) -> TreeViewLayout {
crate::primitives::layout_metrics::tree_layout(tree, rect, self.current_line_height as f64)
}
fn tree_vscrollbar(&self, rect: Rect, tree: &TreeView) -> Option<crate::Scrollbar> {
let row_h = crate::primitives::layout_metrics::tree_row_pitch(
tree,
self.current_line_height as f64,
);
tree.vscrollbar(rect, row_h as f32)
}
fn form_layout(&self, rect: Rect, form: &Form) -> FormLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = &self.dwrite {
return super::form::win_form_layout(dwrite, rect, form, self.current_line_height);
}
let row_h =
crate::primitives::layout_metrics::form_row_height(self.current_line_height as f64);
let measure = NominalTextMeasure {
char_width: self.current_char_width,
};
form.layout(rect.width, rect.height, |i| {
crate::primitives::layout_metrics::form_field_measure(&form.fields[i], row_h, &measure)
})
}
fn draw_editor(&mut self, rect: Rect, editor: &Editor) -> EditorPaintResult {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let _ = rect;
return super::editor::draw_editor(
&surface.target,
dwrite,
editor,
self.current_char_width,
self.current_line_height,
&self.current_theme,
);
}
let _ = (rect, editor);
EditorPaintResult::default()
}
fn draw_message_list(&mut self, rect: Rect, list: &MessageList) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
super::message_list::draw_message_list(
&surface.target,
dwrite,
list,
rect.x,
rect.y,
rect.y + rect.height,
self.current_line_height,
);
return;
}
let _ = (rect, list);
}
fn draw_rich_text_popup(&mut self, popup: &RichTextPopup, layout: &RichTextPopupLayout) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let dwrite = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref());
if let Some(dwrite) = dwrite {
let _ = super::rich_text_popup::draw_rich_text_popup(
&surface.target,
dwrite,
popup,
layout,
);
return;
}
}
let _ = (popup, layout);
}
fn draw_rich_text_popup_with_font_role(
&mut self,
popup: &RichTextPopup,
layout: &RichTextPopupLayout,
role: crate::FontRole,
) {
if role == crate::FontRole::Chrome {
self.draw_rich_text_popup(popup, layout);
return;
}
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
if let Some(dwrite) = self.dwrite.as_ref() {
let _ = super::rich_text_popup::draw_rich_text_popup(
&surface.target,
dwrite,
popup,
layout,
);
return;
}
}
let _ = (popup, layout);
}
fn draw_find_replace(&mut self, rect: Rect, panel: &FindReplacePanel) {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let _ = rect;
let theme = self.current_theme;
crate::primitives::find_replace::paint(panel, self, &theme);
return;
}
let _ = (rect, panel);
}
fn draw_completions(&mut self, completions: &Completions, layout: &CompletionsLayout) {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
super::completions::draw_completions(
&surface.target,
dwrite,
completions,
layout,
&self.current_theme,
);
return;
}
let _ = (completions, layout);
}
fn draw_scrollbar(&mut self, rect: Rect, scrollbar: &Scrollbar) {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let _ = rect;
let theme = self.current_theme;
crate::primitives::scrollbar::native_surface_paint::paint(scrollbar, self, &theme);
return;
}
let _ = (rect, scrollbar);
}
fn draw_drop_overlay(&mut self, overlay: &crate::primitives::drop_zone::DropOverlay) {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let theme = self.current_theme;
crate::primitives::drop_zone::native_surface_paint::paint(overlay, self, &theme);
return;
}
let _ = overlay;
}
fn draw_menu_bar(&mut self, rect: Rect, bar: &MenuBar) -> MenuBarLayout {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
let layout = super::menu_bar::draw_menu_bar(
&surface.target,
dwrite,
rect,
bar,
&self.current_theme,
);
self.register_menu_bar_item_zones(&layout);
return layout;
}
let layout = super::menu_bar::win_menu_bar_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
);
self.register_menu_bar_item_zones(&layout);
layout
}
fn menu_bar_layout(&self, rect: Rect, bar: &MenuBar) -> MenuBarLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()) {
return super::menu_bar::win_menu_bar_layout(dwrite, rect, bar);
}
super::menu_bar::win_menu_bar_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
bar,
)
}
fn draw_split(&mut self, rect: Rect, split: &Split) -> SplitLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let layout = super::split::win_split_layout(rect, split);
let theme = self.current_theme;
crate::primitives::split::native_surface_paint::paint(&layout, self, &theme);
return layout;
}
self.split_layout(rect, split)
}
fn split_layout(&self, rect: Rect, split: &Split) -> SplitLayout {
crate::primitives::layout_metrics::pixel_split_layout(split, rect)
}
fn draw_split_tree(
&mut self,
rect: Rect,
tree: &crate::primitives::split_tree::SplitTree,
) -> crate::primitives::split_tree::SplitTreeLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let layout = super::split_tree::win_split_tree_layout(rect, tree);
let theme = self.current_theme;
crate::primitives::split_tree::native_surface_paint::paint(&layout, self, &theme);
return layout;
}
self.split_tree_layout(rect, tree)
}
fn split_tree_layout(
&self,
rect: Rect,
tree: &crate::primitives::split_tree::SplitTree,
) -> crate::primitives::split_tree::SplitTreeLayout {
crate::primitives::layout_metrics::pixel_split_tree_layout(tree, rect)
}
fn draw_board(&mut self, rect: Rect, model: &crate::BoardModel) -> crate::BoardLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let theme = self.current_theme;
return crate::primitives::board::native_surface_paint::paint(
model, self, &theme, rect,
);
}
self.board_layout(rect, model)
}
fn board_layout(&self, rect: Rect, model: &crate::BoardModel) -> crate::BoardLayout {
crate::primitives::layout_metrics::pixel_board_layout(
model,
rect.x,
rect.y,
rect.width,
rect.height,
)
}
fn draw_minimap(
&mut self,
rect: Rect,
minimap: &crate::primitives::minimap::Minimap,
) -> crate::backend::MinimapPaintResult {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let layout = super::minimap::draw_minimap_scaled(
&surface.target,
dwrite,
rect,
minimap,
&self.current_theme,
self.minimap_scale,
);
return crate::backend::MinimapPaintResult {
layout,
painted: true,
};
}
crate::backend::MinimapPaintResult {
layout: self.minimap_layout(rect, minimap),
painted: false,
}
}
fn minimap_layout(
&self,
rect: Rect,
minimap: &crate::primitives::minimap::Minimap,
) -> crate::primitives::minimap::MinimapLayout {
minimap.layout_with_sizing(
rect,
1,
crate::primitives::minimap::MinimapSizing::FixedPitch(
self.minimap_scale.row_pitch_px() as f32,
),
)
}
fn minimap_scale(&self) -> crate::primitives::minimap::MinimapScale {
self.minimap_scale
}
fn set_minimap_scale(&mut self, scale: crate::primitives::minimap::MinimapScale) {
self.minimap_scale = scale;
}
fn draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
return super::image::draw_image(&surface.target, rect, image);
}
let _ = (rect, image);
crate::backend::ImagePaintResult::Unsupported
}
fn draw_panel(&mut self, rect: Rect, panel: &Panel) -> PanelLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let line_height = self.current_line_height;
let layout = super::panel::win_panel_layout(rect, panel, line_height);
let theme = self.current_theme;
crate::primitives::panel::native_surface_paint::paint(panel, &layout, self, &theme);
return layout;
}
self.panel_layout(rect, panel)
}
fn panel_layout(&self, rect: Rect, panel: &Panel) -> PanelLayout {
let line_height = self.current_line_height;
let measure = crate::primitives::panel::PanelMeasure::new(if panel.title.is_some() {
line_height
} else {
0.0
});
panel.layout(rect, measure)
}
fn draw_canvas(
&mut self,
rect: Rect,
canvas: &crate::primitives::canvas::Canvas,
) -> crate::primitives::canvas::CanvasLayout {
let layout = canvas.layout(rect);
crate::primitives::canvas::native_surface_paint::paint(canvas, &layout, self);
layout
}
fn draw_toast_overlay(&mut self, rect: Rect, stack: &ToastOverlay) -> ToastStackLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let theme = self.current_theme;
let line_height = self.current_line_height;
return crate::primitives::toast::native_surface_paint::paint(
stack,
self,
&theme,
rect.x,
rect.y,
rect.width,
rect.height,
line_height,
);
}
super::toast::win_toast_stack_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
stack,
self.current_line_height,
)
}
fn toast_stack_layout(&self, rect: Rect, stack: &ToastOverlay) -> ToastStackLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = &self.dwrite {
return super::toast::win_toast_stack_layout(
dwrite,
rect,
stack,
self.current_line_height,
);
}
super::toast::win_toast_stack_layout(
&NominalTextMeasure {
char_width: self.current_char_width,
},
rect,
stack,
self.current_line_height,
)
}
fn draw_pipeline_view(
&mut self,
rect: Rect,
view: &crate::primitives::pipeline_view::PipelineView,
) -> crate::primitives::pipeline_view::PipelineViewLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let theme = self.current_theme;
return crate::primitives::pipeline_view::native_surface_paint::paint(
view, self, &theme, rect,
);
}
self.pipeline_view_layout(rect, view)
}
fn pipeline_view_layout(
&self,
rect: Rect,
view: &crate::primitives::pipeline_view::PipelineView,
) -> crate::primitives::pipeline_view::PipelineViewLayout {
crate::primitives::layout_metrics::pixel_pipeline_view_layout(
view,
rect.x,
rect.y,
rect.width,
rect.height,
)
}
fn draw_progress(&mut self, rect: Rect, bar: &ProgressBar) -> ProgressBarLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let theme = self.current_theme;
return crate::primitives::progress::native_surface_paint::paint(
bar, self, &theme, rect,
);
}
self.progress_layout(rect, bar)
}
fn progress_layout(&self, rect: Rect, bar: &ProgressBar) -> ProgressBarLayout {
crate::primitives::layout_metrics::pixel_progress_layout(
bar,
rect.x,
rect.y,
rect.width,
rect.height,
)
}
fn draw_spinner(&mut self, rect: Rect, spinner: &Spinner) -> SpinnerLayout {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
return super::spinner::draw_spinner(&surface.target, dwrite, rect, spinner);
}
super::spinner::win_spinner_layout(
&NominalSpinnerMeasure {
char_width: self.current_char_width,
line_height: self.current_line_height,
},
rect,
spinner,
)
}
fn spinner_layout(&self, rect: Rect, spinner: &Spinner) -> SpinnerLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = &self.dwrite {
return super::spinner::win_spinner_layout(dwrite, rect, spinner);
}
super::spinner::win_spinner_layout(
&NominalSpinnerMeasure {
char_width: self.current_char_width,
line_height: self.current_line_height,
},
rect,
spinner,
)
}
fn draw_command_center(&mut self, rect: Rect, cc: &CommandCenter) -> CommandCenterLayout {
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
let layout = super::command_center::draw_command_center(
&surface.target,
dwrite,
self.current_char_width,
self.current_line_height,
rect,
cc,
&self.current_theme,
);
self.register_command_center_zones(&cc.id, &layout);
return layout;
}
let layout = self.command_center_layout(rect, cc);
self.register_command_center_zones(&cc.id, &layout);
layout
}
fn command_center_layout(&self, rect: Rect, cc: &CommandCenter) -> CommandCenterLayout {
cc.layout(
rect,
crate::primitives::command_center::CommandCenterMeasure::from_char_width(
&cc.search_label,
self.current_char_width,
rect.height,
),
)
}
fn draw_toolbar_with_options(
&mut self,
rect: Rect,
bar: &crate::primitives::toolbar::Toolbar,
interaction: &crate::interaction::InteractionState,
options: crate::primitives::toolbar::ToolbarPaintOptions,
) -> crate::primitives::toolbar::ToolbarLayout {
let (hovered_id, pressed_id) = (interaction.hovered(), interaction.pressed());
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
let dwrite = self.chrome_dwrite.as_ref().unwrap_or(dwrite);
return super::toolbar::draw_toolbar_with_options(
&surface.target,
dwrite,
rect,
bar,
hovered_id,
pressed_id,
options,
);
}
let _ = (hovered_id, pressed_id, options);
self.toolbar_layout(rect, bar)
}
fn toolbar_layout(
&self,
rect: Rect,
bar: &crate::primitives::toolbar::Toolbar,
) -> crate::primitives::toolbar::ToolbarLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()) {
return super::toolbar::win_toolbar_layout(dwrite, rect, bar);
}
let measure = NominalTextMeasure {
char_width: self.current_char_width,
};
bar.layout(rect.x, rect.y, rect.width, rect.height, |btn| {
crate::primitives::toolbar::ToolbarItemMeasure::new(
crate::primitives::toolbar::measure_button(&measure, btn),
)
})
}
fn draw_sidebar_panel_interactive(
&mut self,
rect: Rect,
panel: &crate::primitives::sidebar_panel::SidebarPanel,
interaction: &crate::interaction::InteractionState,
) -> crate::primitives::sidebar_panel::SidebarPanelLayout {
let (hovered_toolbar_id, pressed_toolbar_id) =
(interaction.hovered(), interaction.pressed());
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let line_height = self.current_line_height;
let theme = self.current_theme;
let mut surface = ChromeSurface { backend: self };
return crate::primitives::sidebar_panel::native_surface_paint::paint(
panel,
&mut surface,
&theme,
rect,
line_height,
hovered_toolbar_id,
pressed_toolbar_id,
);
}
let _ = (hovered_toolbar_id, pressed_toolbar_id);
self.sidebar_panel_layout(rect, panel)
}
fn sidebar_panel_layout(
&self,
rect: Rect,
panel: &crate::primitives::sidebar_panel::SidebarPanel,
) -> crate::primitives::sidebar_panel::SidebarPanelLayout {
#[cfg(target_os = "windows")]
if let Some(dwrite) = self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()) {
return super::sidebar_panel::win_sidebar_panel_layout(
dwrite,
self.current_line_height,
rect,
panel,
);
}
let measure = NominalTextMeasure {
char_width: self.current_char_width,
};
panel.layout(
rect,
crate::primitives::sidebar_panel::SidebarPanelMeasure::new(
self.current_line_height,
0.0,
),
|btn| {
crate::primitives::toolbar::ToolbarItemMeasure::new(
crate::primitives::toolbar::measure_button(&measure, btn),
)
},
)
}
fn draw_diff_view(
&mut self,
rect: Rect,
view: &crate::primitives::diff_view::DiffView,
) -> crate::primitives::diff_view::DiffViewLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() && self.dwrite.is_some() {
let theme = self.current_theme;
let line_height = self.current_line_height;
return crate::primitives::diff_view::native_surface_paint::paint(
view,
self,
&theme,
rect,
line_height,
);
}
Backend::diff_view_layout(self, rect, view)
}
fn draw_chart(
&mut self,
rect: Rect,
chart: &crate::primitives::chart::Chart,
hovered_point: Option<(usize, usize)>,
crosshair_x: Option<f64>,
) -> crate::primitives::chart::ChartLayout {
#[cfg(target_os = "windows")]
if self.surface.is_some() {
let layout = self.chart_layout(rect, chart);
let theme = Theme::default();
crate::primitives::chart::paint(
chart,
&layout,
self,
&theme,
hovered_point,
crosshair_x,
);
return layout;
}
let _ = (hovered_point, crosshair_x);
self.chart_layout(rect, chart)
}
fn chart_layout(
&self,
rect: Rect,
chart: &crate::primitives::chart::Chart,
) -> crate::primitives::chart::ChartLayout {
crate::primitives::layout_metrics::pixel_chart_layout(
chart,
rect.x,
rect.y,
rect.width,
rect.height,
self.current_line_height,
self.current_char_width,
)
}
}
impl WindowControl for WinBackend {
fn set_title(&mut self, title: &str) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let wide = win_wide_nul_terminated(title);
unsafe { SetWindowTextW(hwnd, windows::core::PCWSTR::from_raw(wide.as_ptr())) }.map_err(
|e| BackendError::PlatformFailure {
context: format!("SetWindowTextW: {e}"),
},
)
}
#[cfg(not(target_os = "windows"))]
{
let _ = title;
Err(BackendError::Unsupported)
}
}
fn set_size(&mut self, width: f32, height: f32) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let scale = self.dpi_scale;
unsafe {
SetWindowPos(
hwnd,
None,
0,
0,
(width * scale).round() as i32,
(height * scale).round() as i32,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
}
#[cfg(not(target_os = "windows"))]
{
let _ = (width, height);
Err(BackendError::Unsupported)
}
}
fn set_min_size(&mut self, _width: f32, _height: f32) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn set_max_size(&mut self, _width: f32, _height: f32) -> ServiceResult<()> {
Err(BackendError::Unsupported)
}
fn bounds(&self) -> ServiceResult<Rect> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let mut rect = RECT::default();
unsafe { GetWindowRect(hwnd, &mut rect) }.map_err(|e| {
BackendError::PlatformFailure {
context: format!("GetWindowRect: {e}"),
}
})?;
let scale = self.dpi_scale.max(0.01);
Ok(Rect::new(
rect.left as f32 / scale,
rect.top as f32 / scale,
(rect.right - rect.left) as f32 / scale,
(rect.bottom - rect.top) as f32 / scale,
))
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn set_bounds(&mut self, bounds: Rect) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let scale = self.dpi_scale;
unsafe {
SetWindowPos(
hwnd,
None,
(bounds.x * scale).round() as i32,
(bounds.y * scale).round() as i32,
(bounds.width * scale).round() as i32,
(bounds.height * scale).round() as i32,
SWP_NOZORDER | SWP_NOACTIVATE,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
}
#[cfg(not(target_os = "windows"))]
{
let _ = bounds;
Err(BackendError::Unsupported)
}
}
fn center(&mut self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let mut window_rect = RECT::default();
unsafe { GetWindowRect(hwnd, &mut window_rect) }.map_err(|e| {
BackendError::PlatformFailure {
context: format!("GetWindowRect: {e}"),
}
})?;
let monitor_rect = win_monitor_rect(hwnd)?;
let w = window_rect.right - window_rect.left;
let h = window_rect.bottom - window_rect.top;
let mw = monitor_rect.right - monitor_rect.left;
let mh = monitor_rect.bottom - monitor_rect.top;
unsafe {
SetWindowPos(
hwnd,
None,
monitor_rect.left + (mw - w) / 2,
monitor_rect.top + (mh - h) / 2,
0,
0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn set_fullscreen(&mut self, fullscreen: bool) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
if fullscreen {
if self.fullscreen_saved.is_some() {
return Ok(());
}
let mut rect = RECT::default();
unsafe { GetWindowRect(hwnd, &mut rect) }.map_err(|e| {
BackendError::PlatformFailure {
context: format!("GetWindowRect: {e}"),
}
})?;
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) };
self.fullscreen_saved = Some((rect, style));
let stripped = style & !(WS_OVERLAPPEDWINDOW.0 as isize);
unsafe { SetWindowLongPtrW(hwnd, GWL_STYLE, stripped) };
let monitor_rect = win_monitor_rect(hwnd)?;
unsafe {
SetWindowPos(
hwnd,
None,
monitor_rect.left,
monitor_rect.top,
monitor_rect.right - monitor_rect.left,
monitor_rect.bottom - monitor_rect.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
} else {
let Some((rect, style)) = self.fullscreen_saved.take() else {
return Ok(());
};
unsafe { SetWindowLongPtrW(hwnd, GWL_STYLE, style) };
unsafe {
SetWindowPos(
hwnd,
None,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
}
}
#[cfg(not(target_os = "windows"))]
{
let _ = fullscreen;
Err(BackendError::Unsupported)
}
}
fn is_maximized(&self) -> ServiceResult<bool> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
Ok(unsafe { IsZoomed(hwnd) }.as_bool())
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn set_always_on_top(&mut self, on_top: bool) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let insert_after = if on_top { HWND_TOPMOST } else { HWND_NOTOPMOST };
unsafe {
SetWindowPos(
hwnd,
Some(insert_after),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
}
#[cfg(not(target_os = "windows"))]
{
let _ = on_top;
Err(BackendError::Unsupported)
}
}
fn set_decorated(&mut self, decorated: bool) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) };
let new_style = if decorated {
style | (WS_CAPTION.0 as isize)
} else {
style & !(WS_CAPTION.0 as isize)
};
unsafe { SetWindowLongPtrW(hwnd, GWL_STYLE, new_style) };
unsafe {
SetWindowPos(
hwnd,
None,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
)
}
.map_err(|e| BackendError::PlatformFailure {
context: format!("SetWindowPos: {e}"),
})
}
#[cfg(not(target_os = "windows"))]
{
let _ = decorated;
Err(BackendError::Unsupported)
}
}
fn minimize(&mut self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
unsafe {
let _ = ShowWindow(hwnd, SW_MINIMIZE);
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn restore(&mut self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
unsafe {
let _ = ShowWindow(hwnd, SW_RESTORE);
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn hide(&mut self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
unsafe {
let _ = ShowWindow(hwnd, SW_HIDE);
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn show(&mut self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
unsafe {
let _ = ShowWindow(hwnd, SW_SHOW);
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
fn focus(&mut self) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
unsafe {
let _ = SetForegroundWindow(hwnd);
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
Err(BackendError::Unsupported)
}
}
}
impl crate::backend::TrayService for WinBackend {
fn set_icon(&mut self, icon: crate::primitives::image::ImageSource) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
super::tray::set_icon(&mut self.tray, hwnd, icon)
}
#[cfg(not(target_os = "windows"))]
{
let _ = icon;
Err(BackendError::Unsupported)
}
}
fn set_tooltip(&mut self, tooltip: &str) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
super::tray::set_tooltip(&mut self.tray, hwnd, tooltip)
}
#[cfg(not(target_os = "windows"))]
{
let _ = tooltip;
Err(BackendError::Unsupported)
}
}
fn set_menu(
&mut self,
menu: &crate::primitives::context_menu::ContextMenu,
) -> ServiceResult<()> {
#[cfg(target_os = "windows")]
{
let hwnd = self.hwnd.ok_or(BackendError::Unsupported)?;
super::tray::set_menu(&mut self.tray, hwnd, menu)
}
#[cfg(not(target_os = "windows"))]
{
let _ = menu;
Err(BackendError::Unsupported)
}
}
}
impl WinBackend {
#[cfg(target_os = "windows")]
pub(crate) fn tray_menu(&self) -> Option<crate::primitives::context_menu::ContextMenu> {
super::tray::attached_menu(&self.tray)
}
}
#[cfg(target_os = "windows")]
use crate::desktop::wide_nul_terminated as win_wide_nul_terminated;
#[cfg(target_os = "windows")]
fn win_monitor_rect(hwnd: HWND) -> ServiceResult<RECT> {
let hmonitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) };
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
unsafe { GetMonitorInfoW(hmonitor, &mut info) }
.ok()
.map_err(|e| BackendError::PlatformFailure {
context: format!("GetMonitorInfoW: {e}"),
})?;
Ok(info.rcMonitor)
}
impl PaintSurface for WinBackend {
fn surface_begin_frame(&mut self, viewport: Viewport) {
Backend::begin_frame(self, viewport);
}
fn surface_end_frame(&mut self) {
Backend::end_frame(self);
}
fn surface_viewport(&self) -> Viewport {
Backend::viewport(self)
}
fn surface_line_height(&self) -> f32 {
Backend::line_height(self)
}
fn surface_char_width(&self) -> f32 {
Backend::char_width(self)
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
#[cfg(target_os = "windows")]
if self.dwrite.is_some() {
return self.measure_text(text);
}
(
nominal_text_width(text, self.current_char_width),
self.current_line_height,
)
}
fn surface_measure_text_styled(&self, text: &str, bold: bool) -> (f32, f32) {
let _ = bold;
#[cfg(target_os = "windows")]
if let Some(d) = self.dwrite.as_ref() {
if let Ok(m) = d.measure_text_styled(text, bold) {
return m;
}
}
(
nominal_text_width(text, self.current_char_width),
self.current_line_height,
)
}
fn surface_fill_rect(&mut self, rect: Rect, color: crate::Color) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let _ = super::text::fill_rect(&surface.target, rect, color);
return;
}
let _ = (rect, color);
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: crate::Color) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let _ = super::text::fill_rounded_rect(&surface.target, rect, radius, color);
return;
}
let _ = (rect, radius, color);
}
fn surface_stroke_rect(&mut self, rect: Rect, color: crate::Color, stroke_width: f32) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let _ = super::text::stroke_rect(&surface.target, rect, color, stroke_width);
return;
}
let _ = (rect, color, stroke_width);
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: crate::Color) {
#[cfg(target_os = "windows")]
{
self.draw_text(text, rect, color);
}
#[cfg(not(target_os = "windows"))]
{
let _ = (rect, text, color);
}
}
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: crate::Color,
bold: bool,
italic: bool,
underline: bool,
scale_x: f32,
) {
let _ = (italic, underline);
#[cfg(target_os = "windows")]
if let (Some(surface), Some(dwrite)) = (&self.surface, &self.dwrite) {
if (scale_x - 1.0).abs() > f32::EPSILON {
super::text::with_horizontal_scale(&surface.target, scale_x, rect.x, || {
let _ = dwrite.draw_text_styled(&surface.target, text, rect, color, bold);
});
} else {
let _ = dwrite.draw_text_styled(&surface.target, text, rect, color, bold);
}
return;
}
let _ = (rect, text, color, bold, scale_x);
}
fn surface_draw_text_run_with_role(
&mut self,
rect: Rect,
text: &str,
color: crate::Color,
role: crate::FontRole,
italic: bool,
) {
let _ = italic;
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let dwrite = match role {
crate::FontRole::Chrome => self.chrome_dwrite.as_ref().or(self.dwrite.as_ref()),
crate::FontRole::Editor => self.dwrite.as_ref(),
};
if let Some(dwrite) = dwrite {
let _ = dwrite.draw_text(&surface.target, text, rect, color);
return;
}
}
let _ = (rect, text, color, role);
}
fn surface_draw_icon_glyph(&mut self, rect: Rect, text: &str, color: crate::Color) {
self.surface_draw_text_run(rect, text, color)
}
fn surface_draw_line(
&mut self,
from: Point,
to: Point,
color: crate::Color,
stroke_width: f32,
) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
let _ = super::text::draw_line(
&surface.target,
from.x,
from.y,
to.x,
to.y,
color,
stroke_width,
);
return;
}
let _ = (from, to, color, stroke_width);
}
fn surface_push_clip(&mut self, rect: Rect) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
super::text::push_clip(&surface.target, rect);
return;
}
let _ = rect;
}
fn surface_pop_clip(&mut self) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.surface {
super::text::pop_clip(&surface.target);
}
}
fn surface_draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
Backend::draw_image(self, rect, image)
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
struct ChromeSurface<'a> {
backend: &'a mut WinBackend,
}
impl PaintSurface for ChromeSurface<'_> {
fn surface_begin_frame(&mut self, viewport: Viewport) {
self.backend.surface_begin_frame(viewport)
}
fn surface_end_frame(&mut self) {
self.backend.surface_end_frame()
}
fn surface_viewport(&self) -> Viewport {
self.backend.surface_viewport()
}
fn surface_line_height(&self) -> f32 {
self.backend.surface_line_height()
}
fn surface_char_width(&self) -> f32 {
self.backend.surface_char_width()
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
#[cfg(target_os = "windows")]
if let Some(d) = self
.backend
.chrome_dwrite
.as_ref()
.or(self.backend.dwrite.as_ref())
{
if let Ok(m) = d.measure_text(text) {
return m;
}
}
self.backend.surface_measure_text(text)
}
fn surface_measure_text_styled(&self, text: &str, bold: bool) -> (f32, f32) {
#[cfg(target_os = "windows")]
if let Some(d) = self
.backend
.chrome_dwrite
.as_ref()
.or(self.backend.dwrite.as_ref())
{
if let Ok(m) = d.measure_text_styled(text, bold) {
return m;
}
}
self.backend.surface_measure_text_styled(text, bold)
}
fn surface_fill_rect(&mut self, rect: Rect, color: crate::Color) {
self.backend.surface_fill_rect(rect, color)
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: crate::Color) {
self.backend.surface_fill_rounded_rect(rect, radius, color)
}
fn surface_stroke_rect(&mut self, rect: Rect, color: crate::Color, stroke_width: f32) {
self.backend.surface_stroke_rect(rect, color, stroke_width)
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: crate::Color) {
#[cfg(target_os = "windows")]
if let Some(surface) = &self.backend.surface {
let dwrite = self
.backend
.chrome_dwrite
.as_ref()
.or(self.backend.dwrite.as_ref());
if let Some(dwrite) = dwrite {
let _ = dwrite.draw_text(&surface.target, text, rect, color);
return;
}
}
let _ = (rect, text, color);
}
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: crate::Color,
bold: bool,
italic: bool,
underline: bool,
scale_x: f32,
) {
let _ = (italic, underline);
#[cfg(target_os = "windows")]
if let Some(surface) = &self.backend.surface {
let dwrite = self
.backend
.chrome_dwrite
.as_ref()
.or(self.backend.dwrite.as_ref());
if let Some(dwrite) = dwrite {
if (scale_x - 1.0).abs() > f32::EPSILON {
super::text::with_horizontal_scale(&surface.target, scale_x, rect.x, || {
let _ = dwrite.draw_text_styled(&surface.target, text, rect, color, bold);
});
} else {
let _ = dwrite.draw_text_styled(&surface.target, text, rect, color, bold);
}
return;
}
}
let _ = (rect, text, color, bold, scale_x);
}
fn surface_draw_line(
&mut self,
from: Point,
to: Point,
color: crate::Color,
stroke_width: f32,
) {
self.backend
.surface_draw_line(from, to, color, stroke_width)
}
fn surface_push_clip(&mut self, rect: Rect) {
self.backend.surface_push_clip(rect)
}
fn surface_pop_clip(&mut self) {
self.backend.surface_pop_clip()
}
fn surface_draw_image(
&mut self,
rect: Rect,
image: &crate::primitives::image::Image,
) -> crate::backend::ImagePaintResult {
self.backend.surface_draw_image(rect, image)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_tree(n_rows: usize) -> TreeView {
TreeView {
id: WidgetId::new("test:tree:vscrollbar"),
rows: (0..n_rows)
.map(|i| crate::primitives::tree::TreeRow {
path: vec![i as u16],
indent: 0,
icon: None,
text: crate::types::StyledText::plain(format!("row{i}")),
badge: None,
is_expanded: None,
decoration: crate::types::Decoration::Normal,
edit: None,
})
.collect(),
selection_mode: crate::types::SelectionMode::Single,
selected_path: None,
scroll_offset: 0,
style: crate::types::TreeStyle::default(),
has_focus: false,
}
}
#[test]
fn win_backend_tree_vscrollbar_uses_tree_layout_row_pitch_not_raw_line_height() {
let backend = WinBackend::new();
let tree = flat_tree(15);
let rect = Rect::new(0.0, 0.0, 20.0, 300.0);
let sb = Backend::tree_vscrollbar(&backend, rect, &tree)
.expect("15 rows at the real 22px row pitch overflow a 300px viewport");
let expected_row_h = ((backend.current_line_height as f64 * 1.4).round()) as f32;
assert_eq!(
sb.track.width, expected_row_h,
"track width (and row pitch) must match tree_layout's non-header \
item_height, not the raw line_height"
);
}
#[test]
fn win_backend_measure_text_falls_back_to_char_width_before_a_surface_attaches() {
let mut backend = WinBackend::new();
backend.current_char_width = 9.0;
backend.current_line_height = 18.0;
let (w, h) = Backend::measure_text(&backend, "abcd", crate::FontRole::Chrome);
assert_eq!(w, 4.0 * 9.0);
assert_eq!(h, 18.0);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_measure_text_resolves_a_different_font_per_role() {
use crate::win::testing::HeadlessSurface;
let surface = HeadlessSurface::new(100, 100).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_editor_font(DEFAULT_EDITOR_FONT_FAMILY, 10.0);
backend.set_ui_font(&format!("{DEFAULT_UI_FONT_FAMILY} 40"));
backend
.attach_headless(surface.target().clone(), 100, 100)
.expect("attach headless surface");
let (chrome_w, _) = Backend::measure_text(&backend, "quadraui", crate::FontRole::Chrome);
let (editor_w, _) = Backend::measure_text(&backend, "quadraui", crate::FontRole::Editor);
assert!(
chrome_w > editor_w * 2.0,
"a 40pt chrome font should measure \"quadraui\" much wider than a \
10pt editor font: chrome_w={chrome_w}, editor_w={editor_w}"
);
}
#[cfg(target_os = "windows")]
#[test]
fn repaint_reuses_cached_brushes_creating_no_new_ones() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 64;
const H: u32 = 64;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let colors = [
crate::Color::rgb(10, 20, 30),
crate::Color::rgb(200, 50, 50),
crate::Color::rgb(0, 128, 255),
];
const FILLS_PER_COLOR: usize = 4;
let paint_frame = |backend: &mut WinBackend| {
backend.surface_begin_frame(Viewport::new(W as f32, H as f32, 1.0));
for (i, &color) in colors.iter().enumerate() {
for j in 0..FILLS_PER_COLOR {
let x = (i * FILLS_PER_COLOR + j) as f32 * 2.0;
backend.surface_fill_rect(Rect::new(x, 0.0, 2.0, 2.0), color);
}
}
backend.surface_end_frame();
};
crate::win::text::reset_brush_creation_count();
paint_frame(&mut backend);
let created_frame_1 = crate::win::text::brush_creation_count();
assert!(
created_frame_1 > 0 && created_frame_1 as usize <= colors.len(),
"painting {} distinct colors ({} fills total) should create at most \
{} brushes (one per distinct color), got {created_frame_1}",
colors.len(),
colors.len() * FILLS_PER_COLOR,
colors.len()
);
paint_frame(&mut backend);
let created_frame_2 = crate::win::text::brush_creation_count() - created_frame_1;
assert_eq!(
created_frame_2,
0,
"repainting the exact same colors in frame 2 must create zero new brushes \
(cache miss count went from {created_frame_1} to \
{})",
crate::win::text::brush_creation_count()
);
backend.drop_surface_for_testing();
backend.ensure_surface().expect("recreate headless surface");
crate::win::text::reset_brush_creation_count();
paint_frame(&mut backend);
assert_eq!(
crate::win::text::brush_creation_count() as usize,
colors.len(),
"a recreated render target must not reuse the old target's brushes"
);
paint_frame(&mut backend);
assert_eq!(
crate::win::text::brush_creation_count() as usize,
colors.len(),
"repainting after a recreate must hit the new surface's cache"
);
crate::win::text::reset_brush_creation_count();
surface
.paint(|target| {
crate::win::text::fill_rect(target, Rect::new(0.0, 0.0, 2.0, 2.0), colors[0])
.expect("fill_rect");
crate::win::text::fill_rect(target, Rect::new(2.0, 0.0, 2.0, 2.0), colors[0])
.expect("fill_rect");
})
.expect("headless paint");
assert_eq!(crate::win::text::brush_creation_count(), 2);
}
#[test]
fn win_backend_default_fonts_reports_platform_convention() {
let backend = WinBackend::new();
let defaults = Backend::default_fonts(&backend);
assert_eq!(defaults.editor_family, "Consolas");
assert_eq!(defaults.editor_size_pt, 14.0);
assert_eq!(defaults.ui_family, "Segoe UI");
assert_eq!(defaults.ui_size_pt, 13.0);
}
#[test]
fn win_backend_default_fonts_is_unaffected_by_set_editor_font_and_set_ui_font() {
let mut backend = WinBackend::new();
Backend::set_editor_font(&mut backend, "Fira Code", 40.0);
Backend::set_ui_font(&mut backend, "Cantarell 30");
let defaults = Backend::default_fonts(&backend);
assert_eq!(defaults.editor_family, "Consolas");
assert_eq!(defaults.editor_size_pt, 14.0);
assert_eq!(defaults.ui_family, "Segoe UI");
assert_eq!(defaults.ui_size_pt, 13.0);
}
#[test]
fn win_backend_tree_vscrollbar_honors_tree_style_row_height_override() {
let backend = WinBackend::new();
let mut tree = flat_tree(15);
tree.style.row_height = Some(50);
let rect = Rect::new(0.0, 0.0, 20.0, 300.0);
let sb = Backend::tree_vscrollbar(&backend, rect, &tree)
.expect("15 rows at a 50px override overflow a 300px viewport (6 visible)");
assert_eq!(sb.track.width, 50.0);
}
#[cfg(target_os = "windows")]
#[test]
fn parse_ui_font_desc_splits_a_comma_separated_fallback_list() {
assert_eq!(
parse_ui_font_desc("Segoe UI, Tahoma, Sans 12"),
("Segoe UI".to_string(), 12.0)
);
assert_eq!(
parse_ui_font_desc("Segoe UI, Tahoma, Sans"),
("Segoe UI".to_string(), DEFAULT_UI_FONT_SIZE_PT)
);
}
#[cfg(target_os = "windows")]
#[test]
fn parse_ui_font_desc_resolves_generic_tokens() {
assert_eq!(
parse_ui_font_desc("monospace 13"),
(DEFAULT_EDITOR_FONT_FAMILY.to_string(), 13.0)
);
assert_eq!(
parse_ui_font_desc("sans-serif 13"),
(DEFAULT_UI_FONT_FAMILY.to_string(), 13.0)
);
assert_eq!(
parse_ui_font_desc("system-ui 13"),
(DEFAULT_UI_FONT_FAMILY.to_string(), 13.0)
);
assert_eq!(
parse_ui_font_desc("Monospace, Sans 13"),
(DEFAULT_EDITOR_FONT_FAMILY.to_string(), 13.0)
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_editor_font_resolves_the_monospace_generic_token() {
let mut b = WinBackend::new();
Backend::set_editor_font(&mut b, "monospace", 14.0);
assert_eq!(b.editor_font_family, DEFAULT_EDITOR_FONT_FAMILY);
assert_eq!(b.editor_font_size_pt, 14.0);
}
#[test]
fn win_backend_modal_stack_handle_shares_state() {
let backend = WinBackend::new();
let h1 = backend.modal_stack_handle();
let h2 = backend.modal_stack_handle();
h1.borrow_mut()
.push(WidgetId::new("test:popup"), Rect::new(0.0, 0.0, 10.0, 5.0));
assert_eq!(h2.borrow().len(), 1);
}
#[test]
fn win_backend_is_maximized_err_without_window() {
let b = WinBackend::new();
assert!(matches!(
WindowControl::is_maximized(&b),
Err(BackendError::Unsupported)
));
}
#[test]
fn win_backend_set_decorated_err_without_window() {
let mut b = WinBackend::new();
assert!(matches!(
WindowControl::set_decorated(&mut b, false),
Err(BackendError::Unsupported)
));
}
#[test]
fn win_backend_list_hscrollbar_insets_track_for_bordered_list() {
let mut b = WinBackend::new();
b.set_current_char_width(8.0);
b.set_current_line_height(16.0);
let rect = Rect::new(0.0, 0.0, 200.0, 100.0);
let list = |bordered: bool| -> ListView {
ListView {
id: WidgetId::new("l"),
title: None,
items: vec![crate::ListItem {
text: crate::types::StyledText::plain(&"x".repeat(40)),
detail: None,
icon: None,
decoration: crate::types::Decoration::default(),
}],
selected_idx: 0,
scroll_offset: 0,
has_focus: true,
bordered,
h_scroll: 0,
max_content_width: Some(40),
show_v_scrollbar: false,
}
};
let flat_sb = b
.list_hscrollbar(rect, &list(false))
.expect("overflowing content should yield a scrollbar");
assert_eq!(flat_sb.track.x, 0.0);
assert_eq!(flat_sb.track.width, 200.0);
let bordered_sb = b
.list_hscrollbar(rect, &list(true))
.expect("overflowing content should yield a scrollbar");
assert_eq!(bordered_sb.track.x, 8.0);
assert_eq!(bordered_sb.track.width, 184.0);
assert_eq!(bordered_sb.track.y, 100.0 - 2.0 * 16.0);
assert!(bordered_sb.track.x >= rect.x + 8.0);
assert!(bordered_sb.track.x + bordered_sb.track.width <= rect.x + rect.width - 8.0);
}
#[test]
fn modal_stack_handle_outlives_the_backend_borrow_through_the_trait() {
let mut backend = WinBackend::new();
let stack_rc = {
let dyn_backend: &mut dyn Backend = &mut backend;
dyn_backend.modal_stack_handle()
};
stack_rc
.borrow_mut()
.push(WidgetId::new("test:popup"), Rect::new(0.0, 0.0, 10.0, 5.0));
assert_eq!(backend.modal_stack_handle().borrow().len(), 1);
}
#[test]
fn win_backend_drag_state_handle_shares_state() {
let backend = WinBackend::new();
let h1 = backend.drag_state_handle();
let h2 = backend.drag_state_handle();
h1.borrow_mut()
.begin(crate::dispatch::DragTarget::TextSelection {
region: WidgetId::new("r"),
anchor: crate::event::Point::new(0.0, 0.0),
});
assert!(h2.borrow().is_active());
}
fn acc(id: &str, key: &str) -> Accelerator {
Accelerator {
id: AcceleratorId::new(id),
binding: crate::KeyBinding::Literal(key.to_string()),
scope: AcceleratorScope::Global,
label: None,
}
}
#[test]
fn register_and_unregister_accelerator_round_trip() {
let mut b = WinBackend::new();
let a = acc("save", "<C-s>");
b.register_accelerator(&a);
let ctrl_s = Modifiers {
ctrl: true,
..Default::default()
};
assert_eq!(
b.match_keypress(&Key::Char('s'), ctrl_s),
Some(AcceleratorId::new("save"))
);
b.unregister_accelerator(&AcceleratorId::new("save"));
assert_eq!(b.match_keypress(&Key::Char('s'), ctrl_s), None);
}
#[test]
fn match_keypress_finds_registered_global_binding() {
let mut b = WinBackend::new();
b.register_accelerator(&acc("save", "<C-s>"));
let id = b.match_keypress(
&Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
},
);
assert_eq!(id, Some(AcceleratorId::new("save")));
}
#[test]
fn match_keypress_modifier_mismatch_no_match() {
let mut b = WinBackend::new();
b.register_accelerator(&acc("save", "<C-s>"));
let id = b.match_keypress(
&Key::Char('s'),
Modifiers {
alt: true,
..Default::default()
},
);
assert_eq!(id, None);
}
#[test]
fn match_keypress_skips_non_global_scope() {
let mut b = WinBackend::new();
b.register_accelerator(&Accelerator {
id: AcceleratorId::new("find-in-tree"),
binding: crate::KeyBinding::Literal("<C-f>".to_string()),
scope: AcceleratorScope::Widget(WidgetId::new("tree")),
label: None,
});
let id = b.match_keypress(
&Key::Char('f'),
Modifiers {
ctrl: true,
..Default::default()
},
);
assert_eq!(id, None);
}
#[test]
fn match_keypress_re_register_replaces_binding() {
let mut b = WinBackend::new();
b.register_accelerator(&acc("save", "<C-s>"));
b.register_accelerator(&acc("save", "<C-S-s>"));
assert_eq!(
b.match_keypress(
&Key::Char('s'),
Modifiers {
ctrl: true,
..Default::default()
}
),
None,
"old binding must no longer match after re-registration",
);
assert_eq!(
b.match_keypress(
&Key::Char('s'),
Modifiers {
ctrl: true,
shift: true,
..Default::default()
}
),
Some(AcceleratorId::new("save")),
);
}
fn win_mouse_down(x: f32, y: f32) -> UiEvent {
UiEvent::MouseDown {
widget: None,
button: crate::MouseButton::Left,
position: crate::event::Point::new(x, y),
modifiers: Modifiers::default(),
}
}
#[test]
fn fold_double_click_second_click_same_position_becomes_double_click() {
let mut b = WinBackend::new();
let first = b.fold_double_click(win_mouse_down(5.0, 3.0));
assert!(matches!(first, UiEvent::MouseDown { .. }));
let second = b.fold_double_click(win_mouse_down(5.0, 3.0));
assert!(
matches!(second, UiEvent::DoubleClick { .. }),
"second click at the same position should fold to DoubleClick"
);
}
#[test]
fn fold_double_click_different_position_stays_mouse_down() {
let mut b = WinBackend::new();
let _ = b.fold_double_click(win_mouse_down(5.0, 3.0));
let second = b.fold_double_click(win_mouse_down(50.0, 30.0));
assert!(matches!(second, UiEvent::MouseDown { .. }));
}
#[test]
fn fold_double_click_passes_non_mouse_down_events_through() {
let mut b = WinBackend::new();
let ev = b.fold_double_click(UiEvent::WindowFocused(true));
assert_eq!(ev, UiEvent::WindowFocused(true));
}
#[test]
fn poll_events_drains_queue_fifo() {
let b = WinBackend::new();
b.push_event(win_mouse_down(1.0, 2.0));
b.push_event(UiEvent::WindowFocused(true));
let mut b = b;
let evs = b.poll_events();
assert_eq!(evs.len(), 2);
assert!(matches!(evs[0], UiEvent::MouseDown { .. }));
assert!(matches!(evs[1], UiEvent::WindowFocused(true)));
assert!(b.poll_events().is_empty());
}
#[test]
fn waker_payload_reaches_poll_events_as_user_event() {
let backend = WinBackend::new();
let waker = Backend::waker(&backend);
let handle = std::thread::spawn(move || {
waker(crate::UserPayload::new("from background".to_string()));
});
handle.join().expect("background thread must not panic");
let mut backend = backend;
let events = backend.poll_events();
assert_eq!(events.len(), 1);
match &events[0] {
UiEvent::User(payload) => {
assert_eq!(
payload.downcast_ref::<String>().map(String::as_str),
Some("from background")
);
}
other => panic!("expected UiEvent::User, got {other:?}"),
}
}
#[test]
fn request_frame_in_before_a_window_is_attached_does_not_panic() {
let backend = WinBackend::new();
Backend::request_frame_in(&backend, Duration::from_millis(50));
}
#[test]
fn frame_requests_and_pending_delay_observe_calls_with_no_hwnd() {
let backend = WinBackend::new();
assert_eq!(backend.frame_requests(), 0);
assert!(backend.pending_frame_delay().is_none());
Backend::request_frame_in(&backend, Duration::from_millis(50));
assert_eq!(backend.frame_requests(), 1);
assert!(backend.pending_frame_delay().is_some());
Backend::request_frame_in(&backend, Duration::from_millis(50));
assert_eq!(backend.frame_requests(), 2);
}
#[cfg(target_os = "windows")]
#[test]
fn waker_resolves_hwnd_set_after_the_waker_was_obtained() {
let backend = WinBackend::new();
assert!(
live_hwnd(&backend.hwnd_raw).is_none(),
"no window attached yet"
);
let fake_hwnd_value: isize = 0xDEAD_BEEFusize as isize;
backend
.hwnd_raw
.store(fake_hwnd_value, std::sync::atomic::Ordering::Release);
let resolved = live_hwnd(&backend.hwnd_raw).expect("window is now attached");
assert_eq!(resolved.0 as isize, fake_hwnd_value);
}
#[test]
fn poll_events_does_not_panic_on_empty_queue() {
let mut b = WinBackend::new();
assert!(b.poll_events().is_empty());
}
#[test]
fn wait_events_does_not_panic_and_drains_like_poll_events() {
let mut b = WinBackend::new();
assert!(b.wait_events(Duration::from_millis(50)).is_empty());
b.push_event(win_mouse_down(1.0, 2.0));
let evs = b.wait_events(Duration::from_millis(50));
assert_eq!(evs.len(), 1);
assert!(matches!(evs[0], UiEvent::MouseDown { .. }));
}
#[test]
fn poll_events_and_wait_events_agree_for_the_same_sequence() {
let seq = |mut b: WinBackend| -> WinBackend {
b.register_accelerator(&acc("save", "<C-s>"));
b.push_event(UiEvent::KeyPressed {
key: Key::Char('s'),
modifiers: Modifiers {
ctrl: true,
..Default::default()
},
repeat: false,
});
b.push_event(win_mouse_down(1.0, 2.0));
b
};
let mut by_poll = seq(WinBackend::new());
let via_poll = by_poll.poll_events();
let mut by_wait = seq(WinBackend::new());
let via_wait = by_wait.wait_events(Duration::from_millis(0));
assert_eq!(via_poll, via_wait);
assert_eq!(
via_poll[0],
UiEvent::Accelerator(
AcceleratorId::new("save"),
Modifiers {
ctrl: true,
..Default::default()
}
),
"registered KeyPressed must rewrite to Accelerator, matching \
GtkBackend::apply_accelerators",
);
assert!(matches!(via_poll[1], UiEvent::MouseDown { .. }));
}
#[test]
fn focused_activity_bar_id_starts_none() {
let b = WinBackend::new();
assert!(b.focused_activity_bar_id().is_none());
}
#[test]
fn draw_activity_bar_tracks_keyboard_focus() {
let mut b = WinBackend::new();
let bar = ActivityBar {
id: WidgetId::new("demo:bar"),
top_items: Vec::new(),
bottom_items: Vec::new(),
active_accent: None,
selection_bg: None,
is_keyboard_focused: true,
};
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
b.draw_activity_bar(Rect::new(0.0, 0.0, 40.0, 200.0), &bar, None);
}));
assert_eq!(
b.focused_activity_bar_id(),
Some(&WidgetId::new("demo:bar"))
);
}
#[test]
fn begin_frame_clears_focused_activity_bar() {
let mut b = WinBackend::new();
b.focused_activity_bar = Some(WidgetId::new("demo:bar"));
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
b.begin_frame(Viewport::new(80.0, 24.0, 1.0));
}));
assert!(
b.focused_activity_bar_id().is_none(),
"focused_activity_bar must be cleared by begin_frame"
);
}
#[test]
fn toggle_window_maximize_false_without_window() {
let mut b = WinBackend::new();
assert!(!Backend::toggle_window_maximize(&mut b));
}
#[test]
fn backend_caps_declares_window_chrome() {
let b = WinBackend::new();
#[cfg(target_os = "windows")]
assert!(
b.backend_caps().window_chrome,
"issue #1199: WinBackend must declare window_chrome now that \
toggle_window_maximize is overridden and win::run handles \
WM_NCCALCSIZE/WM_NCHITTEST"
);
#[cfg(not(target_os = "windows"))]
assert!(
!b.backend_caps().window_chrome,
"off-Windows backend_caps() is BackendCaps::empty() — see that method's doc"
);
}
#[test]
fn backend_caps_declares_no_native_menu_but_keeps_window_chrome() {
let b = WinBackend::new();
#[cfg(target_os = "windows")]
{
assert!(
!b.backend_caps().native_menu,
"issue #1228: a native HMENU sits in the non-client area that the drawn \
caption (#1199) permanently covers, so Win-GUI must not claim native_menu \
— install_menu_bar is the trait's no-op default now, matching GtkBackend/\
TuiBackend"
);
assert!(
b.backend_caps().window_chrome,
"issue #1228 keeps the drawn caption from #1199 — only the native menu bar \
half of #1200 was reverted"
);
}
#[cfg(not(target_os = "windows"))]
assert!(
!b.backend_caps().native_menu && !b.backend_caps().window_chrome,
"off-Windows backend_caps() is BackendCaps::empty() — see that method's doc"
);
}
#[cfg(target_os = "windows")]
#[test]
fn install_menu_bar_never_attaches_a_native_menu_to_a_live_window() {
use windows::core::PCWSTR;
use windows::Win32::Foundation::HINSTANCE;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, GetMenu, RegisterClassExW,
CW_USEDEFAULT, WINDOW_EX_STYLE, WNDCLASSEXW,
};
unsafe extern "system" fn test_wndproc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> windows::Win32::Foundation::LRESULT {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
let hinstance: HINSTANCE = unsafe { GetModuleHandleW(PCWSTR::null()) }
.expect("GetModuleHandleW(None) always succeeds for the current process")
.into();
let class_name = win_wide_nul_terminated("quadraui_test_1228_no_native_menu_wndclass");
let wc = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(test_wndproc),
hInstance: hinstance,
lpszClassName: PCWSTR::from_raw(class_name.as_ptr()),
..Default::default()
};
unsafe {
let _ = RegisterClassExW(&wc);
}
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
PCWSTR::from_raw(class_name.as_ptr()),
PCWSTR::null(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
None,
None,
Some(hinstance),
None,
)
}
.expect(
"CreateWindowExW for an unshown top-level test window must succeed on real \
Windows hardware",
);
let mut backend = WinBackend::new();
backend.hwnd = Some(hwnd);
let menu_before = unsafe { GetMenu(hwnd) };
assert!(
menu_before.is_invalid(),
"a freshly created window must start with no menu at all"
);
{
use crate::primitives::menu_bar::MenuBarItem;
Backend::install_menu_bar(
&mut backend,
&MenuBar {
id: WidgetId::new("menubar"),
items: vec![MenuBarItem {
id: WidgetId::new("file"),
label: "&File".to_string(),
disabled: false,
submenu: None,
}],
open_item: None,
focused_item: None,
},
);
}
let menu_after = unsafe { GetMenu(hwnd) };
assert!(
menu_after.is_invalid(),
"issue #1228: install_menu_bar is the trait's no-op default on Win-GUI now — \
GetMenu(hwnd) must stay null even after a caller invokes it"
);
unsafe {
let _ = DestroyWindow(hwnd);
}
}
#[test]
fn register_zone_records_the_zone() {
let mut b = WinBackend::new();
Backend::register_zone(
&mut b,
WidgetId::new("demo:zone"),
Rect::new(1.0, 2.0, 3.0, 4.0),
);
assert_eq!(b.zones.len(), 1);
assert_eq!(b.zones[0].id, WidgetId::new("demo:zone"));
assert_eq!(b.zones[0].bounds, Rect::new(1.0, 2.0, 3.0, 4.0));
}
#[test]
fn begin_frame_clears_zones() {
let mut b = WinBackend::new();
Backend::register_zone(
&mut b,
WidgetId::new("demo:zone"),
Rect::new(0.0, 0.0, 1.0, 1.0),
);
assert_eq!(b.zones.len(), 1);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
b.begin_frame(Viewport::new(80.0, 24.0, 1.0));
}));
assert!(
b.zones.is_empty(),
"begin_frame must clear last frame's zones"
);
}
#[test]
fn nc_hit_test_none_with_no_band_registered() {
let b = WinBackend::new();
assert_eq!(b.nc_hit_test(10.0, 10.0), None);
}
#[test]
fn nc_hit_test_none_outside_the_band() {
let mut b = WinBackend::new();
Backend::register_zone(
&mut b,
WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE),
Rect::new(0.0, 0.0, 800.0, 32.0),
);
assert_eq!(b.nc_hit_test(10.0, 100.0), None);
}
#[test]
fn nc_hit_test_caption_in_the_empty_band() {
let mut b = WinBackend::new();
Backend::register_zone(
&mut b,
WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE),
Rect::new(0.0, 0.0, 800.0, 32.0),
);
assert_eq!(b.nc_hit_test(400.0, 16.0), Some(true));
}
#[test]
fn nc_hit_test_client_on_an_inline_button() {
let mut b = WinBackend::new();
Backend::register_zone(
&mut b,
WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE),
Rect::new(0.0, 0.0, 800.0, 32.0),
);
Backend::register_zone(
&mut b,
WidgetId::new("titlebar:minimize-button"),
Rect::new(700.0, 4.0, 24.0, 24.0),
);
assert_eq!(b.nc_hit_test(712.0, 16.0), Some(false));
assert_eq!(b.nc_hit_test(100.0, 16.0), Some(true));
}
#[test]
fn draw_menu_bar_registers_its_own_item_zones_inside_the_band() {
let mut b = WinBackend::new();
let band = Rect::new(0.0, 0.0, 800.0, 32.0);
Backend::register_zone(&mut b, WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE), band);
let bar = MenuBar {
id: WidgetId::new("bar"),
items: vec![
crate::primitives::menu_bar::MenuBarItem {
id: WidgetId::new("bar:file"),
label: "&File".into(),
disabled: false,
submenu: None,
},
crate::primitives::menu_bar::MenuBarItem {
id: WidgetId::new("bar:edit"),
label: "&Edit".into(),
disabled: false,
submenu: None,
},
],
open_item: None,
focused_item: None,
};
let layout = Backend::draw_menu_bar(&mut b, Rect::new(0.0, 0.0, 300.0, 32.0), &bar);
let file_item = layout
.visible_items
.iter()
.find(|v| v.id == WidgetId::new("bar:file"))
.expect("File item must be visible at this width");
let (cx, cy) = (
file_item.bounds.x + file_item.bounds.width / 2.0,
file_item.bounds.y + file_item.bounds.height / 2.0,
);
assert_eq!(b.nc_hit_test(cx, cy), Some(false));
assert_eq!(b.nc_hit_test(790.0, 16.0), Some(true));
}
#[test]
fn draw_status_bar_interactive_registers_its_own_segment_zones_inside_the_band() {
let mut b = WinBackend::new();
let band = Rect::new(0.0, 0.0, 800.0, 32.0);
Backend::register_zone(&mut b, WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE), band);
let bar = StatusBar {
id: WidgetId::new("titlebar:controls"),
left_segments: vec![],
right_segments: vec![
crate::primitives::status_bar::StatusBarSegment {
text: "_".into(),
fg: crate::types::Color::rgb(255, 255, 255),
bg: crate::types::Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new("titlebar:minimize")),
},
crate::primitives::status_bar::StatusBarSegment {
text: "X".into(),
fg: crate::types::Color::rgb(255, 255, 255),
bg: crate::types::Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new("titlebar:close")),
},
],
};
let interaction = crate::interaction::InteractionState::new();
let status_rect = Rect::new(600.0, 0.0, 200.0, 32.0);
let layout = Backend::draw_status_bar_interactive(&mut b, status_rect, &bar, &interaction);
let (close_rect, hit) = layout
.hit_regions
.iter()
.find(|(_, hit)| {
matches!(hit, StatusBarHit::Segment(id) if *id == WidgetId::new("titlebar:close"))
})
.expect("close segment must be a registered hit region");
let _ = hit;
let (cx, cy) = (
status_rect.x + close_rect.x + close_rect.width / 2.0,
status_rect.y + close_rect.y + close_rect.height / 2.0,
);
assert_eq!(b.nc_hit_test(cx, cy), Some(false));
assert_eq!(b.nc_hit_test(10.0, 16.0), Some(true));
}
#[test]
fn register_status_bar_segment_zones_translates_bar_local_bounds_by_rect_origin() {
let mut b = WinBackend::new();
let bar = StatusBar {
id: WidgetId::new("titlebar:controls"),
left_segments: vec![],
right_segments: vec![crate::primitives::status_bar::StatusBarSegment {
text: "X".into(),
fg: crate::types::Color::rgb(255, 255, 255),
bg: crate::types::Color::rgb(0, 0, 0),
bold: false,
action_id: Some(WidgetId::new("titlebar:close")),
}],
};
let interaction = crate::interaction::InteractionState::new();
let status_rect = Rect::new(850.0, 4.0, 150.0, 28.0);
let layout = Backend::draw_status_bar_interactive(&mut b, status_rect, &bar, &interaction);
let (close_rect, _) = layout
.hit_regions
.iter()
.find(|(_, hit)| {
matches!(hit, StatusBarHit::Segment(id) if *id == WidgetId::new("titlebar:close"))
})
.expect("close segment must be a registered hit region");
let registered = b
.zones
.iter()
.find(|z| z.id == WidgetId::new("titlebar:close"))
.expect("close segment zone must be registered")
.bounds;
assert_eq!(registered.x, status_rect.x + close_rect.x);
assert_eq!(registered.y, status_rect.y + close_rect.y);
assert_eq!(registered.width, close_rect.width);
assert_eq!(registered.height, close_rect.height);
assert!(
registered.x > 0.0,
"regression guard: bar-local bounds alone would be well under \
status_rect.x's 850.0, not reflecting the real absolute position"
);
}
#[test]
fn draw_command_center_registers_its_own_zones_inside_the_band() {
let mut b = WinBackend::new();
let band = Rect::new(0.0, 0.0, 800.0, 32.0);
Backend::register_zone(&mut b, WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE), band);
let cc = CommandCenter {
id: WidgetId::new("titlebar:cc"),
back_enabled: true,
forward_enabled: true,
search_label: "project-name".into(),
};
let layout = Backend::draw_command_center(&mut b, Rect::new(300.0, 0.0, 400.0, 32.0), &cc);
let search = layout.search_bounds.expect("search box must be laid out");
let (cx, cy) = (
search.x + search.width / 2.0,
search.y + search.height / 2.0,
);
assert_eq!(b.nc_hit_test(cx, cy), Some(false));
assert_eq!(b.nc_hit_test(1.0, 16.0), Some(true));
}
#[test]
fn draw_command_center_leaves_its_own_container_padding_as_caption() {
let mut b = WinBackend::new();
let band = Rect::new(0.0, 0.0, 900.0, 32.0);
Backend::register_zone(&mut b, WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE), band);
let cc = CommandCenter {
id: WidgetId::new("titlebar:cc"),
back_enabled: true,
forward_enabled: true,
search_label: "x".into(),
};
let container = Rect::new(300.0, 0.0, 400.0, 32.0);
let layout = Backend::draw_command_center(&mut b, container, &cc);
let back = layout.back_bounds.expect("back arrow must be laid out");
assert!(
back.x > container.x,
"fixture must actually leave left padding to probe (back.x={}, container.x={})",
back.x,
container.x
);
let pad_x = (container.x + back.x) / 2.0;
assert_eq!(b.nc_hit_test(pad_x, 16.0), Some(true));
}
#[test]
fn nc_hit_test_ignores_zones_at_least_as_large_as_the_band() {
let mut b = WinBackend::new();
Backend::register_zone(
&mut b,
WidgetId::new("app-shell:window"),
Rect::new(0.0, 0.0, 800.0, 600.0),
);
Backend::register_zone(
&mut b,
WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE),
Rect::new(0.0, 0.0, 800.0, 32.0),
);
assert_eq!(b.nc_hit_test(400.0, 16.0), Some(true));
}
#[cfg(target_os = "windows")]
#[test]
fn pointer_shape_to_win32_cursor_maps_every_variant() {
let expected = [
IDC_ARROW,
IDC_SIZENS, IDC_SIZENS, IDC_SIZEWE, IDC_SIZEWE, IDC_SIZENESW, IDC_SIZENWSE, IDC_SIZENWSE, IDC_SIZENESW, ];
for (shape, expected) in crate::desktop::all_pointer_shapes()
.iter()
.zip(expected.iter())
{
assert_eq!(pointer_shape_to_win32_cursor(*shape), *expected);
}
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_rasterisers() {
use crate::theme::Theme;
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 32;
const H: u32 = 32;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_separator = Color::rgb(0x12, 0x34, 0x56);
assert_ne!(
custom_separator,
Theme::default().separator,
"test fixture bug: the probe colour must differ from the default theme's, or a \
hardcoded `Theme::default()` would pass this test too"
);
backend.set_theme(Theme {
separator: custom_separator,
..Theme::default()
});
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_terminal_divider(Rect::new(0.0, 0.0, 1.0, H as f32));
backend.end_frame();
let px = surface.pixel_at(0, H / 2);
assert_eq!(
(px.r, px.g, px.b),
(custom_separator.r, custom_separator.g, custom_separator.b),
"painted pixel must reflect the theme set via `set_theme`, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_activity_bar() {
use crate::theme::Theme;
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 40;
const H: u32 = 40;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0x11, 0x22, 0x33);
assert_ne!(
custom_bg,
Theme::default().tab_bar_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
tab_bar_bg: custom_bg,
..Theme::default()
});
let bar = ActivityBar {
id: WidgetId::new("bar"),
top_items: Vec::new(),
bottom_items: Vec::new(),
active_accent: None,
selection_bg: None,
is_keyboard_focused: false,
};
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_activity_bar(Rect::new(0.0, 0.0, W as f32, H as f32), &bar, None);
backend.end_frame();
let px = surface.pixel_at(W / 2, H / 2);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"activity bar background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_menu_bar() {
use crate::theme::Theme;
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 24;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0x44, 0x55, 0x66);
assert_ne!(
custom_bg,
Theme::default().tab_bar_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
tab_bar_bg: custom_bg,
..Theme::default()
});
let bar = MenuBar {
id: WidgetId::new("bar"),
items: Vec::new(),
open_item: None,
focused_item: None,
};
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_menu_bar(Rect::new(0.0, 0.0, W as f32, H as f32), &bar);
backend.end_frame();
let px = surface.pixel_at(W / 2, H / 2);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"menu bar background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_context_menu() {
use crate::primitives::context_menu::{
ContextMenuItem, ContextMenuItemMeasure, ContextMenuPlacement,
};
use crate::theme::Theme;
use crate::types::{Color, StyledText};
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 100;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0x77, 0x88, 0x99);
assert_ne!(
custom_bg,
Theme::default().hover_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
hover_bg: custom_bg,
..Theme::default()
});
let item = |id: &str, label: &str| ContextMenuItem {
id: Some(WidgetId::new(id)),
label: StyledText::plain(label),
..Default::default()
};
let menu = ContextMenu {
id: WidgetId::new("ctx"),
items: vec![item("a", "A"), item("b", "B")],
selected_idx: 0,
bg: None,
placement: ContextMenuPlacement::AnchorPoint,
};
let viewport = Rect::new(0.0, 0.0, W as f32, H as f32);
let layout = menu.layout(4.0, 4.0, viewport, 60.0, |_| {
ContextMenuItemMeasure::new(20.0)
});
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_context_menu(&menu, &layout);
backend.end_frame();
assert!(
layout.bounds.width > 0.0 && layout.bounds.height > 0.0,
"test fixture bug: empty menu bounds"
);
let row = layout
.visible_items
.get(1)
.expect("test fixture bug: second menu row not visible")
.bounds;
let px = surface.pixel_at(
(row.x + row.width * 0.75) as u32,
(row.y + row.height / 2.0) as u32,
);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"context menu background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_completions() {
use crate::primitives::completions::{CompletionItem, CompletionItemMeasure};
use crate::theme::Theme;
use crate::types::{Color, StyledText};
use crate::win::testing::HeadlessSurface;
const W: u32 = 200;
const H: u32 = 100;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0xaa, 0xbb, 0xcc);
assert_ne!(
custom_bg,
Theme::default().completion_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
completion_bg: custom_bg,
..Theme::default()
});
let item = |label: &str| CompletionItem {
label: StyledText::plain(label),
detail: None,
documentation: None,
kind: Default::default(),
icon: None,
};
let completions = Completions {
id: WidgetId::new("comp"),
items: vec![item("alpha"), item("beta")],
selected_idx: 0,
scroll_offset: 0,
has_focus: true,
};
let viewport = Rect::new(0.0, 0.0, W as f32, H as f32);
let layout = completions.layout(10.0, 10.0, 16.0, viewport, 120.0, 60.0, |_| {
CompletionItemMeasure::new(16.0)
});
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_completions(&completions, &layout);
backend.end_frame();
assert!(
layout.bounds.width > 0.0 && layout.bounds.height > 0.0,
"test fixture bug: empty popup bounds"
);
let row = layout
.visible_items
.get(1)
.expect("test fixture bug: second completion row not visible")
.bounds;
let px = surface.pixel_at(
(row.x + row.width * 0.75) as u32,
(row.y + row.height / 2.0) as u32,
);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"completions popup background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_find_replace() {
use crate::primitives::find_replace::compute_hit_regions;
use crate::theme::Theme;
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 600;
const H: u32 = 200;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0x22, 0x44, 0x66);
assert_ne!(
custom_bg,
Theme::default().surface_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
surface_bg: custom_bg,
..Theme::default()
});
let (hit_regions, _input_width) = compute_hit_regions(50, false, "1 of 3", 2, 2);
let panel = FindReplacePanel {
query: "needle".into(),
replacement: String::new(),
show_replace: false,
focus: 0,
cursor: 3,
sel_anchor: None,
match_info: "1 of 3".into(),
case_sensitive: false,
whole_word: false,
use_regex: false,
preserve_case: false,
in_selection: false,
group_bounds: Rect::new(0.0, 0.0, W as f32, H as f32),
panel_width: 50,
replace_one_glyph: "R1".into(),
replace_all_glyph: "R*".into(),
hit_regions,
};
let cw = backend.char_width().max(1.0);
let lh = backend.line_height().max(1.0);
let popup_w = panel.panel_width as f32 * cw;
let popup_h = 3.0 * lh;
let popup_x = (panel.group_bounds.x + panel.group_bounds.width - popup_w - 10.0)
.max(panel.group_bounds.x);
let popup_y = panel.group_bounds.y + 2.0;
assert!(
popup_w > 8.0
&& popup_h > 8.0
&& popup_x + popup_w <= W as f32
&& popup_y + popup_h <= H as f32,
"test fixture bug: popup {popup_w}x{popup_h} at ({popup_x}, {popup_y}) (cell \
{cw}x{lh}) does not fit the {W}x{H} probe surface",
);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_find_replace(Rect::new(0.0, 0.0, W as f32, H as f32), &panel);
backend.end_frame();
let px = surface.pixel_at(
(popup_x + popup_w - 4.0) as u32,
(popup_y + popup_h - 4.0) as u32,
);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"find/replace panel background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_editor() {
use crate::primitives::editor::Editor;
use crate::theme::Theme;
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 80;
const H: u32 = 40;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0x77, 0x88, 0x99);
assert_ne!(
custom_bg,
Theme::default().background,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
background: custom_bg,
..Theme::default()
});
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let editor = Editor::new(WidgetId::new("editor"), rect);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let _ = backend.draw_editor(rect, &editor);
backend.end_frame();
let px = surface.pixel_at(W / 2, H / 2);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"editor background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_tree() {
use crate::theme::Theme;
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 80;
const H: u32 = 40;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0xaa, 0xbb, 0xcc);
assert_ne!(
custom_bg,
Theme::default().tab_bar_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
tab_bar_bg: custom_bg,
..Theme::default()
});
let tree = flat_tree(1);
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_tree(rect, &tree);
backend.end_frame();
let px = surface.pixel_at(W / 2, H - 2);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"Explorer sidebar (tree) background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_tab_bar() {
use crate::theme::Theme;
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 24;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_bg = Color::rgb(0x33, 0x66, 0x99);
assert_ne!(
custom_bg,
Theme::default().tab_bar_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
tab_bar_bg: custom_bg,
..Theme::default()
});
#[allow(deprecated)]
let tab_bar = TabBar {
id: WidgetId::new("tab-bar"),
tabs: Vec::new(),
scroll_offset: 0,
right_segments: Vec::new(),
active_accent: None,
show_tab_close: false,
compact: false,
};
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
#[allow(deprecated)]
let _ = backend.draw_tab_bar(rect, &tab_bar, None);
backend.end_frame();
let px = surface.pixel_at(W / 2, H / 2);
assert_eq!(
(px.r, px.g, px.b),
(custom_bg.r, custom_bg.g, custom_bg.b),
"tab bar background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_panel() {
use crate::primitives::panel::Panel;
use crate::theme::Theme;
use crate::types::{Color, StyledText, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 60;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_separator = Color::rgb(0x12, 0x99, 0x34);
assert_ne!(
custom_separator,
Theme::default().separator,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
separator: custom_separator,
..Theme::default()
});
let panel = Panel {
id: WidgetId::new("panel"),
title: Some(StyledText::plain("Terminal")),
actions: Vec::new(),
accent: None,
collapsed: false,
};
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = backend.draw_panel(rect, &panel);
backend.end_frame();
let tb = layout
.title_bar_bounds
.expect("panel has a title, so a title bar is reserved");
let px = surface.pixel_at(
(tb.x + tb.width - 2.0) as u32,
(tb.y + tb.height / 2.0) as u32,
);
assert_eq!(
(px.r, px.g, px.b),
(custom_separator.r, custom_separator.g, custom_separator.b),
"panel title-bar background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_sidebar_panel_toolbar() {
use crate::theme::Theme;
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 60;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_header_bg = Color::rgb(0x55, 0x11, 0x77);
assert_ne!(
custom_header_bg,
Theme::default().header_bg,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
header_bg: custom_header_bg,
..Theme::default()
});
let panel = crate::primitives::sidebar_panel::SidebarPanel {
id: WidgetId::new("sidebar-panel"),
toolbar: Some(crate::primitives::toolbar::Toolbar {
id: WidgetId::new("toolbar"),
buttons: Vec::new(),
bg: None,
focused_index: None,
}),
toolbar_height: Some(24.0),
};
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let interaction = crate::interaction::InteractionState::default();
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = backend.draw_sidebar_panel_interactive(rect, &panel, &interaction);
backend.end_frame();
let tb = layout
.toolbar_bounds
.expect("panel has a toolbar, so a toolbar slot is reserved");
let px = surface.pixel_at(
(tb.x + tb.width / 2.0) as u32,
(tb.y + tb.height / 2.0) as u32,
);
assert_eq!(
(px.r, px.g, px.b),
(custom_header_bg.r, custom_header_bg.g, custom_header_bg.b),
"sidebar panel toolbar background must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_split() {
use crate::primitives::split::{Split, SplitDirection};
use crate::theme::Theme;
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 60;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_separator = Color::rgb(0x44, 0x22, 0x88);
assert_ne!(
custom_separator,
Theme::default().separator,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
separator: custom_separator,
..Theme::default()
});
let split = Split {
id: WidgetId::new("split"),
direction: SplitDirection::Horizontal,
ratio: 0.5,
first_min: 0.0,
second_min: 0.0,
};
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = backend.draw_split(rect, &split);
backend.end_frame();
let db = layout.divider_bounds;
let px = surface.pixel_at(
(db.x + db.width / 2.0) as u32,
(db.y + db.height / 2.0) as u32,
);
assert_eq!(
(px.r, px.g, px.b),
(custom_separator.r, custom_separator.g, custom_separator.b),
"split divider must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_theme_reaches_the_split_tree() {
use crate::primitives::split::SplitDirection;
use crate::primitives::split_tree::SplitTree;
use crate::theme::Theme;
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 60;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let custom_separator = Color::rgb(0x66, 0x33, 0x99);
assert_ne!(
custom_separator,
Theme::default().separator,
"test fixture bug: the probe colour must differ from the default theme's"
);
backend.set_theme(Theme {
separator: custom_separator,
..Theme::default()
});
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(WidgetId::new("left")),
SplitTree::leaf(WidgetId::new("right")),
);
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = backend.draw_split_tree(rect, &tree);
backend.end_frame();
let div = layout
.dividers
.first()
.expect("one divider between two leaves");
let (cx, cy) = match div.direction {
SplitDirection::Horizontal => (
div.position + div.thickness / 2.0,
div.cross_start + div.cross_size / 2.0,
),
SplitDirection::Vertical => (
div.cross_start + div.cross_size / 2.0,
div.position + div.thickness / 2.0,
),
};
let px = surface.pixel_at(cx as u32, cy as u32);
assert_eq!(
(px.r, px.g, px.b),
(custom_separator.r, custom_separator.g, custom_separator.b),
"split-tree divider must reflect the live theme, not `Theme::default()`",
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_paint_surface_fill_rect_paints_solid_color() {
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 32;
const H: u32 = 32;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let red = Color::rgb(200, 20, 20);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.surface_fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), red);
backend.end_frame();
let px = surface.pixel_at(5, 5);
assert_eq!(
(px.r, px.g, px.b),
(red.r, red.g, red.b),
"surface_fill_rect must paint the solid color it was given"
);
}
#[cfg(target_os = "windows")]
fn cleared_frame_background(width: u32, height: u32) -> (u8, u8, u8) {
use crate::win::testing::HeadlessSurface;
let surface = HeadlessSurface::new(width, height).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), width, height)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(width as f32, height as f32, 1.0));
backend.end_frame();
let px = surface.pixel_at(0, 0);
(px.r, px.g, px.b)
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_begin_frame_clears_the_whole_surface_uniformly() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 16;
const H: u32 = 16;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.end_frame();
let expected = cleared_frame_background(W, H);
for y in 0..H {
for x in 0..W {
let px = surface.pixel_at(x, y);
assert_eq!(
(px.r, px.g, px.b),
expected,
"begin_frame's Clear must cover every pixel — ({x}, {y}) differs, so \
sampling one pixel as 'the background' would be wrong"
);
}
}
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_paint_surface_fill_rounded_rect_clips_the_corners() {
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 40;
const H: u32 = 40;
let background = cleared_frame_background(W, H);
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let red = Color::rgb(200, 20, 20);
assert_ne!(
background,
(red.r, red.g, red.b),
"the fill colour must differ from the frame's clear colour, or \
'this corner stayed untouched' is unprovable"
);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.surface_fill_rounded_rect(Rect::new(0.0, 0.0, W as f32, H as f32), 15.0, red);
backend.end_frame();
let centre = surface.pixel_at(20, 20);
assert_eq!(
(centre.r, centre.g, centre.b),
(red.r, red.g, red.b),
"surface_fill_rounded_rect must paint the solid color at the box's centre"
);
let corner = surface.pixel_at(1, 1);
assert_eq!(
(corner.r, corner.g, corner.b),
background,
"a corner pixel well inside a radius-15 fillet on a 40x40 box must stay at \
the frame's clear colour — otherwise this is just `surface_fill_rect` \
under a new name"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_paint_surface_fill_rect_alpha_blends_with_the_background() {
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 40;
const H: u32 = 40;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let white = Color::rgb(255, 255, 255);
let red = Color::rgb(200, 20, 20);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.surface_fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), white);
backend.surface_fill_rect_alpha(Rect::new(0.0, 0.0, W as f32, H as f32), red, 0.5);
backend.end_frame();
let px = surface.pixel_at(20, 20);
assert!(
(px.r, px.g, px.b) != (white.r, white.g, white.b)
&& (px.r, px.g, px.b) != (red.r, red.g, red.b),
"a 50%-alpha fill over an opaque background must land a real blend, not the \
background or the fill colour verbatim: got ({}, {}, {})",
px.r,
px.g,
px.b
);
assert!(
(200..=245).contains(&px.r),
"red channel should sit between the fill's 200 and white's 255: got {}",
px.r
);
assert!(
(100..=200).contains(&px.g) && (100..=200).contains(&px.b),
"green/blue channels should sit between the fill's 20 and white's 255: \
got ({}, {})",
px.g,
px.b
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_paint_surface_draw_text_run_with_role_uses_the_requested_fonts_size() {
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 80;
const H: u32 = 80;
fn ink_pixel_count(surface: &HeadlessSurface, background: (u8, u8, u8)) -> u32 {
let mut count = 0;
for y in 0..H {
for x in 0..W {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != background {
count += 1;
}
}
}
count
}
let background = cleared_frame_background(W, H);
let white = Color::rgb(255, 255, 255);
let chrome_surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_editor_font(DEFAULT_UI_FONT_FAMILY, 6.0);
backend.set_ui_font(&format!("{DEFAULT_UI_FONT_FAMILY} 60"));
backend
.attach_headless(chrome_surface.target().clone(), W, H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.surface_draw_text_run_with_role(
Rect::new(2.0, 2.0, 76.0, 76.0),
"A",
white,
crate::FontRole::Chrome,
false,
);
backend.end_frame();
let chrome_ink = ink_pixel_count(&chrome_surface, background);
let editor_surface = HeadlessSurface::new(W, H).expect("create headless surface");
backend
.attach_headless(editor_surface.target().clone(), W, H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.surface_draw_text_run_with_role(
Rect::new(2.0, 2.0, 76.0, 76.0),
"A",
white,
crate::FontRole::Editor,
false,
);
backend.end_frame();
let editor_ink = ink_pixel_count(&editor_surface, background);
assert!(
chrome_ink > editor_ink * 4,
"FontRole::Chrome (60pt) must paint far more ink than FontRole::Editor \
(6pt) for the same glyph: chrome={chrome_ink}, editor={editor_ink}"
);
assert!(editor_ink > 0, "FontRole::Editor must still paint real ink");
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_dialog_paints_body_text_in_the_chrome_fonts_size() {
use crate::primitives::dialog::DialogMeasure;
use crate::primitives::toolbar::ToolbarItemMeasure;
use crate::theme::Theme;
use crate::types::{StyledText, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 100;
const H: u32 = 100;
const EDITOR_SIZE_PT: f32 = 14.0;
fn body_only_dialog() -> Dialog {
Dialog {
id: WidgetId::new("d"),
title: StyledText::plain(""),
body: vec![StyledText::plain("A")],
buttons: vec![],
severity: None,
vertical_buttons: false,
table: None,
input: None,
}
}
fn body_layout() -> DialogLayout {
let dialog = body_only_dialog();
let viewport = Rect::new(0.0, 0.0, W as f32, H as f32);
let measure = DialogMeasure {
width: W as f32,
title_height: 0.0,
body_height: 90.0,
table_height: 0.0,
input_height: 0.0,
button_row_height: 0.0,
button_width: 0.0,
button_gap: 0.0,
padding: 4.0,
};
dialog.layout(viewport, measure, |_| ToolbarItemMeasure::new(0.0))
}
fn body_ink_right_edge(surface: &HeadlessSurface, body_bounds: Rect) -> i32 {
let bg = Theme::default().surface_bg;
let mut last = 0i32;
for y in body_bounds.y as u32..(body_bounds.y + body_bounds.height) as u32 {
for x in body_bounds.x as u32..(body_bounds.x + body_bounds.width) as u32 {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != (bg.r, bg.g, bg.b) {
last = last.max(x as i32 - body_bounds.x as i32);
}
}
}
last
}
fn paint_with_chrome_size(chrome_size_pt: f32, layout: &DialogLayout) -> i32 {
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_editor_font(DEFAULT_UI_FONT_FAMILY, EDITOR_SIZE_PT);
backend.set_ui_font(&format!("{DEFAULT_UI_FONT_FAMILY} {chrome_size_pt}"));
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let _ = Backend::draw_dialog(&mut backend, &body_only_dialog(), layout);
backend.end_frame();
body_ink_right_edge(&surface, layout.body_bounds)
}
let layout = body_layout();
let right_tiny_chrome = paint_with_chrome_size(6.0, &layout);
let right_huge_chrome = paint_with_chrome_size(40.0, &layout);
assert!(
right_huge_chrome > right_tiny_chrome * 2,
"with the editor font size held fixed at {EDITOR_SIZE_PT}pt, only varying \
the chrome font size (6pt vs 40pt), the dialog body's glyph should paint \
far wider at the larger chrome size — proves draw_dialog paints body text \
via chrome_dwrite, not the (unchanged) editor dwrite handle \
(right_tiny_chrome={right_tiny_chrome}, right_huge_chrome={right_huge_chrome})"
);
assert!(
right_tiny_chrome > 0,
"the tiny-chrome run must still paint some real ink, or this comparison \
proves nothing"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_rich_text_popup_honours_font_role() {
use crate::primitives::rich_text_popup::{
PopupPlacement, RichTextPopup, RichTextPopupMeasure,
};
use crate::types::{Color, StyledText, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 120;
const H: u32 = 120;
const EDITOR_SIZE_PT: f32 = 6.0;
const CHROME_SIZE_PT: f32 = 40.0;
let bg = Color::rgb(0, 0, 0);
let fg = Color::rgb(255, 255, 255);
fn popup(bg: Color, fg: Color) -> RichTextPopup {
RichTextPopup {
id: WidgetId::new("rtp:font-role"),
lines: vec![StyledText::plain("M")],
line_text: vec!["M".to_string()],
line_scales: vec![],
scroll_top: 0,
max_visible_rows: 4,
has_focus: false,
selection: None,
links: vec![],
focused_link: None,
placement: PopupPlacement::Below,
padding: 0.0,
fg: Some(fg),
bg: Some(bg),
}
}
fn ink_right_edge(surface: &HeadlessSurface, bounds: Rect, bg: (u8, u8, u8)) -> i32 {
let mut last = 0i32;
for y in bounds.y as u32..(bounds.y + bounds.height) as u32 {
for x in bounds.x as u32..(bounds.x + bounds.width) as u32 {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != bg {
last = last.max(x as i32 - bounds.x as i32);
}
}
}
last
}
fn paint_with_role(role: Option<crate::FontRole>, bg: Color, fg: Color) -> (i32, Rect) {
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_editor_font(DEFAULT_UI_FONT_FAMILY, EDITOR_SIZE_PT);
backend.set_ui_font(&format!("{DEFAULT_UI_FONT_FAMILY} {CHROME_SIZE_PT}"));
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let p = popup(bg, fg);
let measure = RichTextPopupMeasure::new(100.0, 90.0);
let layout = p.layout(
2.0,
2.0,
Rect::new(0.0, 0.0, W as f32, H as f32),
measure,
|_, s, e| (e - s) as f32,
);
let content = layout.content_bounds;
match role {
Some(role) => {
Backend::draw_rich_text_popup_with_font_role(&mut backend, &p, &layout, role)
}
None => Backend::draw_rich_text_popup(&mut backend, &p, &layout),
}
backend.end_frame();
(
ink_right_edge(&surface, content, (bg.r, bg.g, bg.b)),
content,
)
}
let (chrome_right, _) = paint_with_role(None, bg, fg);
let (editor_right, _) = paint_with_role(Some(crate::FontRole::Editor), bg, fg);
assert!(
editor_right > 0,
"FontRole::Editor must still paint real ink, got {editor_right}"
);
assert!(
chrome_right > editor_right * 2,
"with the editor font size held fixed at {EDITOR_SIZE_PT}pt, plain \
draw_rich_text_popup (no role argument) painting the {CHROME_SIZE_PT}pt \
chrome font should paint a far wider glyph than FontRole::Editor: \
chrome_right={chrome_right}, editor_right={editor_right}"
);
}
#[cfg(target_os = "windows")]
fn chrome_font_diff_pixel_count(
w: u32,
h: u32,
ui_font_a: &str,
ui_font_b: &str,
paint: impl Fn(&mut WinBackend),
) -> u32 {
use crate::win::testing::HeadlessSurface;
fn render(
w: u32,
h: u32,
ui_font: &str,
paint: &dyn Fn(&mut WinBackend),
) -> HeadlessSurface {
let surface = HeadlessSurface::new(w, h).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_editor_font(DEFAULT_UI_FONT_FAMILY, 10.0);
backend.set_ui_font(ui_font);
backend
.attach_headless(surface.target().clone(), w, h)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(w as f32, h as f32, 1.0));
paint(&mut backend);
backend.end_frame();
surface
}
let a = render(w, h, ui_font_a, &paint);
let b = render(w, h, ui_font_b, &paint);
let mut diff = 0u32;
for y in 0..h {
for x in 0..w {
if a.pixel_at(x, y) != b.pixel_at(x, y) {
diff += 1;
}
}
}
diff
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_tree_paints_row_text_in_the_chrome_fonts_size() {
let tree = flat_tree(3);
let diff = chrome_font_diff_pixel_count(
200,
120,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
backend.draw_tree(Rect::new(0.0, 0.0, 200.0, 120.0), &tree);
},
);
assert!(
diff > 0,
"#1266: varying only the chrome font size must repaint different pixels — \
draw_tree must paint row text through chrome_dwrite, not the (unchanged) \
editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_list_paints_row_text_in_the_chrome_fonts_size() {
let list = ListView {
id: WidgetId::new("list"),
title: None,
items: vec![
crate::primitives::list::ListItem {
text: crate::types::StyledText::plain("alpha"),
icon: None,
detail: None,
decoration: crate::types::Decoration::Normal,
},
crate::primitives::list::ListItem {
text: crate::types::StyledText::plain("beta"),
icon: None,
detail: None,
decoration: crate::types::Decoration::Normal,
},
],
selected_idx: 0,
scroll_offset: 0,
has_focus: false,
bordered: false,
h_scroll: 0,
max_content_width: None,
show_v_scrollbar: false,
};
let diff = chrome_font_diff_pixel_count(
200,
120,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
backend.draw_list(Rect::new(0.0, 0.0, 200.0, 120.0), &list);
},
);
assert!(
diff > 0,
"#1266: draw_list must paint row text through chrome_dwrite, not the \
(unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_menu_bar_paints_item_labels_in_the_chrome_fonts_size() {
let bar = MenuBar {
id: WidgetId::new("bar"),
items: vec![crate::primitives::menu_bar::MenuBarItem {
id: WidgetId::new("bar:file"),
label: "&File".into(),
disabled: false,
submenu: None,
}],
open_item: None,
focused_item: None,
};
let diff = chrome_font_diff_pixel_count(
300,
32,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_menu_bar(Rect::new(0.0, 0.0, 300.0, 32.0), &bar);
},
);
assert!(
diff > 0,
"#1266: draw_menu_bar must paint item labels through chrome_dwrite, not \
the (unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_context_menu_paints_item_labels_in_the_chrome_fonts_size() {
let menu = ContextMenu {
id: WidgetId::new("ctx"),
items: vec![crate::primitives::context_menu::ContextMenuItem {
id: Some(WidgetId::new("ctx:copy")),
label: crate::types::StyledText::plain("copy"),
..Default::default()
}],
selected_idx: 0,
bg: None,
placement: crate::primitives::context_menu::ContextMenuPlacement::AnchorPoint,
};
let viewport = Rect::new(0.0, 0.0, 200.0, 100.0);
let layout = menu.layout(10.0, 10.0, viewport, 120.0, |_| {
crate::primitives::context_menu::ContextMenuItemMeasure::new(20.0)
});
let diff = chrome_font_diff_pixel_count(
200,
100,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_context_menu(&menu, &layout);
},
);
assert!(
diff > 0,
"#1266: draw_context_menu must paint item labels through chrome_dwrite, \
not the (unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_command_center_paints_the_search_label_in_the_chrome_fonts_size() {
let cc = CommandCenter {
id: WidgetId::new("cc"),
back_enabled: true,
forward_enabled: true,
search_label: "project-name".into(),
};
let diff = chrome_font_diff_pixel_count(
300,
32,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_command_center(Rect::new(0.0, 0.0, 300.0, 32.0), &cc);
},
);
assert!(
diff > 0,
"#1266: draw_command_center must paint the search label through \
chrome_dwrite, not the (unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_multi_section_view_paints_header_titles_in_the_chrome_fonts_size() {
let view = MultiSectionView {
id: WidgetId::new("msv"),
sections: vec![crate::primitives::multi_section_view::Section {
id: "alpha".into(),
header: crate::primitives::multi_section_view::SectionHeader {
icon: None,
title: crate::types::StyledText::plain("alpha"),
badge: None,
actions: vec![],
show_chevron: true,
},
body: crate::primitives::multi_section_view::SectionBody::Text(vec![
crate::types::StyledText::plain("line one"),
]),
aux: None,
size: crate::primitives::multi_section_view::SectionSize::EqualShare,
collapsed: false,
min_size: None,
max_size: None,
}],
active_section: Some(0),
axis: crate::primitives::multi_section_view::Axis::Vertical,
allow_resize: false,
allow_collapse: true,
scroll_mode: crate::primitives::multi_section_view::ScrollMode::PerSection,
has_focus: true,
panel_scroll: 0.0,
};
let diff = chrome_font_diff_pixel_count(
200,
150,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
backend.draw_multi_section_view(Rect::new(0.0, 0.0, 200.0, 150.0), &view);
},
);
assert!(
diff > 0,
"#1266: draw_multi_section_view must paint section header titles through \
chrome_dwrite, not the (unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_sidebar_panel_interactive_paints_the_toolbar_label_in_the_chrome_fonts_size(
) {
let panel = crate::primitives::sidebar_panel::SidebarPanel {
id: WidgetId::new("sb"),
toolbar: Some(crate::primitives::toolbar::Toolbar {
id: WidgetId::new("sb:toolbar"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("sb:refine"),
label: "Refine".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: false,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
}),
toolbar_height: None,
};
let interaction = crate::interaction::InteractionState::new();
let diff = chrome_font_diff_pixel_count(
200,
100,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_sidebar_panel_interactive(
Rect::new(0.0, 0.0, 200.0, 100.0),
&panel,
&interaction,
);
},
);
assert!(
diff > 0,
"#1266: draw_sidebar_panel_interactive must paint the embedded toolbar's \
label through ChromeSurface (chrome_dwrite), not WinBackend's own \
PaintSurface impl (the editor dwrite handle)"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_status_bar_interactive_paints_segment_text_in_the_chrome_fonts_size() {
let bar = StatusBar {
id: WidgetId::new("status"),
left_segments: vec![crate::primitives::status_bar::StatusBarSegment {
text: "main.rs".into(),
fg: crate::types::Color::rgb(255, 255, 255),
bg: crate::types::Color::rgb(0, 0, 0),
bold: false,
action_id: None,
}],
right_segments: vec![],
};
let interaction = crate::interaction::InteractionState::new();
let diff = chrome_font_diff_pixel_count(
300,
24,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, 300.0, 24.0),
&bar,
&interaction,
);
},
);
assert!(
diff > 0,
"#1266: draw_status_bar_interactive must paint segment text through \
ChromeSurface (chrome_dwrite), not WinBackend's own PaintSurface impl \
(the editor dwrite handle)"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_activity_bar_paints_icon_glyphs_in_the_chrome_fonts_size() {
let bar = ActivityBar {
id: WidgetId::new("activity"),
top_items: vec![crate::primitives::activity_bar::ActivityItem {
id: WidgetId::new("activity:explorer"),
icon: crate::types::Icon::new("", "E"),
tooltip: String::new(),
is_active: false,
is_keyboard_selected: false,
}],
bottom_items: Vec::new(),
active_accent: None,
selection_bg: None,
is_keyboard_focused: false,
};
let diff =
chrome_font_diff_pixel_count(60, 200, "Segoe UI 12", "Courier New 12", |backend| {
let _ = backend.draw_activity_bar(Rect::new(0.0, 0.0, 60.0, 200.0), &bar, None);
});
assert!(
diff > 0,
"#1266: draw_activity_bar must paint icon glyphs through chrome_dwrite (whose \
family — not its point size, fixed by #1157's icon-size override — this test \
varies), not the (unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_toolbar_interactive_paints_button_labels_in_the_chrome_fonts_size() {
let bar = crate::primitives::toolbar::Toolbar {
id: WidgetId::new("tb"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("tb:on"),
label: "On".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: false,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
};
let interaction = crate::interaction::InteractionState::new();
let diff = chrome_font_diff_pixel_count(
100,
32,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_toolbar_interactive(
Rect::new(0.0, 0.0, 100.0, 32.0),
&bar,
&interaction,
);
},
);
assert!(
diff > 0,
"#1266: draw_toolbar_interactive must paint button labels through \
chrome_dwrite, not the (unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
#[allow(deprecated)] fn win_backend_draw_tab_bar_paints_tab_labels_in_the_chrome_fonts_size() {
let bar = TabBar {
id: WidgetId::new("tabs"),
tabs: vec![crate::primitives::tab_bar::TabItem {
label: "main.rs".into(),
is_active: true,
is_dirty: false,
is_preview: false,
is_closable: true,
}],
scroll_offset: 0,
right_segments: Vec::new(),
active_accent: None,
show_tab_close: true,
compact: false,
};
let diff = chrome_font_diff_pixel_count(
300,
30,
&format!("{DEFAULT_UI_FONT_FAMILY} 6.0"),
&format!("{DEFAULT_UI_FONT_FAMILY} 40.0"),
|backend| {
let _ = backend.draw_tab_bar(Rect::new(0.0, 0.0, 300.0, 30.0), &bar, None);
},
);
assert!(
diff > 0,
"#1266: draw_tab_bar must paint tab labels through chrome_dwrite, not the \
(unchanged) editor dwrite handle"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_paint_surface_draw_icon_glyph_paints_real_ink() {
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 40;
const H: u32 = 40;
let background = cleared_frame_background(W, H);
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let white = Color::rgb(255, 255, 255);
assert_ne!(
background,
(white.r, white.g, white.b),
"the glyph colour must differ from the frame's clear colour, or \
'it painted ink' is unprovable"
);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.surface_draw_icon_glyph(Rect::new(2.0, 2.0, 30.0, 30.0), "i", white);
backend.end_frame();
let mut ink = 0u32;
for y in 0..H {
for x in 0..W {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != background {
ink += 1;
}
}
}
assert!(
ink > 0,
"surface_draw_icon_glyph must paint real ink through the fallback-baked \
DirectWrite text format, not silently no-op"
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_paint_surface_verbs_do_not_panic() {
use crate::types::Color;
use crate::win::testing::HeadlessSurface;
const W: u32 = 64;
const H: u32 = 64;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let viewport = Viewport::new(W as f32, H as f32, 1.0);
backend.surface_begin_frame(viewport);
assert_eq!(
backend.surface_viewport(),
viewport,
"surface_viewport must forward to Backend::begin_frame's stored value"
);
assert_eq!(
backend.surface_line_height(),
Backend::line_height(&backend)
);
assert_eq!(backend.surface_char_width(), Backend::char_width(&backend));
let blue = Color::rgb(20, 20, 200);
let white = Color::rgb(255, 255, 255);
backend.surface_stroke_rect(Rect::new(0.0, 0.0, 40.0, 40.0), blue, 2.0);
backend.surface_draw_line(Point::new(0.0, 0.0), Point::new(40.0, 40.0), blue, 1.0);
backend.surface_push_clip(Rect::new(0.0, 0.0, 40.0, 40.0));
backend.surface_draw_text_run(Rect::new(2.0, 2.0, 30.0, 10.0), "hi", white);
backend.surface_pop_clip();
backend.surface_fill_rounded_rect(Rect::new(44.0, 0.0, 16.0, 16.0), 4.0, blue);
backend.surface_fill_rect_alpha(Rect::new(44.0, 20.0, 16.0, 16.0), blue, 0.5);
backend.surface_draw_text_run_with_role(
Rect::new(2.0, 14.0, 30.0, 10.0),
"hi",
white,
crate::FontRole::Chrome,
false,
);
backend.surface_draw_icon_glyph(Rect::new(2.0, 26.0, 10.0, 10.0), "i", white);
let (w, h) = backend.surface_measure_text("hi");
assert!(
w > 0.0 && h > 0.0,
"surface_measure_text must report a nonzero footprint for non-empty \
text: got ({w}, {h})"
);
let image = crate::primitives::image::Image {
id: WidgetId::new("test:native-surface-image"),
source: crate::primitives::image::ImageSource::Bytes(Vec::new()),
intrinsic_size: Some((8, 8)),
fit: crate::primitives::image::ImageFit::Contain,
fallback_text: "[i]".to_string(),
};
let _ = backend.surface_draw_image(Rect::new(0.0, 40.0, 8.0, 8.0), &image);
backend.surface_end_frame();
}
#[cfg(target_os = "windows")]
fn chart_line_fixture(data: Vec<f64>) -> crate::primitives::chart::Chart {
use crate::primitives::chart::{ChartKind, Series};
crate::primitives::chart::Chart {
id: WidgetId::new("chart"),
kind: ChartKind::Line,
series: vec![Series {
label: "A".into(),
data,
color: None,
fill: false,
}],
x_label: None,
y_label: None,
y_range: None,
x_range: None,
show_legend: false,
y_ticks: Some(0),
x_ticks: Some(0),
show_grid: false,
}
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_chart_paint_and_nearest_point_round_trip() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 200;
const H: u32 = 100;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let chart = chart_line_fixture(vec![1.0, 5.0, 2.0, 8.0, 3.0]);
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = backend.draw_chart(rect, &chart, None, None);
backend.end_frame();
assert_eq!(layout.data_point_positions.len(), 5);
for &(si, di, x, y) in &layout.data_point_positions {
let nearest = layout.nearest_point(x, y, 1.0);
assert_eq!(nearest, Some((si, di)));
}
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_chart(rect, &chart, Some((0, 0)), Some(1.0));
backend.end_frame();
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_chart_no_paint_layout_matches_paint_layout() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 200;
const H: u32 = 100;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let chart = chart_line_fixture(vec![1.0, 4.0, 2.0]);
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let painted = backend.draw_chart(rect, &chart, None, None);
backend.end_frame();
let no_paint = backend.chart_layout(rect, &chart);
assert_eq!(painted, no_paint);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_chart_hover_marker_does_not_escape_chart_rect() {
use crate::primitives::chart::{ChartKind, Series};
use crate::win::testing::HeadlessSurface;
use crate::Color;
let chart = crate::primitives::chart::Chart {
id: WidgetId::new("chart"),
kind: ChartKind::Sparkline,
series: vec![Series {
label: "a".into(),
data: vec![10.0, 0.0],
color: None,
fill: false,
}],
x_label: None,
y_label: None,
y_range: Some((0.0, 10.0)),
x_range: None,
show_legend: false,
y_ticks: Some(0),
x_ticks: Some(0),
show_grid: false,
};
const CANVAS_W: u32 = 40;
const CANVAS_H: u32 = 40;
let sentinel = Color::rgb(0, 0, 0);
let (chart_x, chart_y, chart_w, chart_h) = (10.0_f32, 10.0_f32, 20.0_f32, 20.0_f32);
let surface = HeadlessSurface::new(CANVAS_W, CANVAS_H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), CANVAS_W, CANVAS_H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(CANVAS_W as f32, CANVAS_H as f32, 1.0));
backend.surface_fill_rect(
Rect::new(0.0, 0.0, CANVAS_W as f32, CANVAS_H as f32),
sentinel,
);
backend.draw_chart(
Rect::new(chart_x, chart_y, chart_w, chart_h),
&chart,
Some((0, 0)),
None,
);
backend.end_frame();
let px = surface.pixel_at(5, 5);
assert_eq!(
(px.r, px.g, px.b),
(sentinel.r, sentinel.g, sentinel.b),
"hover marker must not paint outside the chart's own rect",
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_status_bar_bold_segment_measures_wider() {
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::win::testing::HeadlessSurface;
use crate::Color;
const W: u32 = 400;
const H: u32 = 20;
const TEXT: &str = "READY";
const FONT: &str = DEFAULT_UI_FONT_FAMILY;
const SIZE_PT: f32 = 20.0;
let (probe, _, _) = DWrite::new(FONT, SIZE_PT, None).expect("create probe DWrite");
let (probe_regular, _) = probe
.measure_text_styled(TEXT, false)
.expect("measure regular");
let (probe_bold, _) = probe.measure_text_styled(TEXT, true).expect("measure bold");
assert!(
probe_bold > probe_regular,
"test fixture bug: `{FONT}` must measure bold text wider than regular for this \
assertion to mean anything (regular={probe_regular}, bold={probe_bold}) — pick a \
proportional font that distinguishes weights"
);
let width_for = |bold: bool| {
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_editor_font(FONT, SIZE_PT);
backend.set_ui_font(&format!("{FONT} {SIZE_PT}"));
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = backend.draw_status_bar_interactive(
Rect::new(0.0, 0.0, W as f32, H as f32),
&StatusBar {
id: WidgetId::new("test:status-bar"),
left_segments: vec![StatusBarSegment {
text: TEXT.into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(0, 0, 0),
bold,
action_id: None,
}],
right_segments: vec![],
},
&crate::InteractionState::new(),
);
backend.end_frame();
layout.visible_segments[0].bounds.width
};
let regular_width = width_for(false);
let bold_width = width_for(true);
assert!(
bold_width > regular_width,
"a bold segment must measure (and paint) wider than the same text non-bold: \
regular={regular_width}, bold={bold_width}"
);
const PAD: f32 = 2.0 * crate::primitives::status_bar::PIXEL_SEGMENT_PADDING;
assert_eq!(
(regular_width, bold_width),
(probe_regular + PAD, probe_bold + PAD),
"resolved segment widths must match DirectWrite's own per-weight measurement \
plus #1155's per-segment padding"
);
}
#[cfg(target_os = "windows")]
#[test]
fn register_font_from_memory_rejects_bytes_that_are_not_a_font() {
let mut backend = WinBackend::new();
let garbage = [0u8; 64];
assert!(
backend.register_font_from_memory(&garbage).is_none(),
"64 zero bytes are not a parseable font — must report None, not a fabricated family"
);
}
#[cfg(target_os = "windows")]
#[test]
fn register_font_from_memory_copies_bytes_before_the_callers_buffer_can_be_dropped() {
let mut backend = WinBackend::new();
{
let transient = vec![0xABu8; 64];
let _ = backend.register_font_from_memory(&transient);
}
assert_eq!(
backend.registered_font_bytes.len(),
1,
"the call must copy bytes into backend-owned storage regardless of whether \
registration itself succeeds"
);
assert_eq!(
backend.registered_font_bytes[0],
vec![0xABu8; 64],
"the stored copy must match what was passed in, read back from backend-owned \
memory rather than the (by now dropped) caller buffer"
);
}
#[cfg(target_os = "windows")]
#[test]
fn set_nerd_font_fallback_against_the_system_collection_does_not_break_normal_paint() {
use crate::win::testing::HeadlessSurface;
use crate::Color;
const W: u32 = 200;
const H: u32 = 30;
const TEXT: &str = "OK";
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend.set_nerd_font_fallback("Segoe UI Symbol");
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach_headless must still succeed with a fallback wired in");
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let sentinel = Color::rgb(0, 0, 0);
backend.surface_fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), sentinel);
backend.draw_text(
TEXT,
Rect::new(4.0, 4.0, W as f32 - 8.0, H as f32 - 8.0),
Color::rgb(255, 255, 255),
);
backend.end_frame();
let (w, h) = backend.measure_text(TEXT);
assert!(
w > 0.0 && h > 0.0,
"ordinary text must still measure to a real, positive size with a fallback wired \
in (w={w}, h={h})"
);
let mut painted_something = false;
'outer: for y in 0..H {
for x in 0..W {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != (sentinel.r, sentinel.g, sentinel.b) {
painted_something = true;
break 'outer;
}
}
}
assert!(
painted_something,
"draw_text must paint something other than the sentinel background"
);
}
#[cfg(target_os = "windows")]
#[test]
fn has_font_family_finds_a_real_system_family() {
let backend = WinBackend::new();
assert_eq!(
backend.has_font_family("Segoe UI"),
Some(true),
"Segoe UI ships on every supported Windows version"
);
}
#[cfg(target_os = "windows")]
#[test]
fn has_font_family_is_some_false_for_a_name_nothing_is_installed_under() {
let backend = WinBackend::new();
assert_eq!(
backend.has_font_family("Definitely Not A Real Font Family Quadraui 1024"),
Some(false)
);
}
#[cfg(target_os = "windows")]
fn td_line(text: &str) -> crate::primitives::text_display::TextDisplayLine {
crate::primitives::text_display::TextDisplayLine {
spans: vec![crate::types::StyledSpan::plain(text)],
decoration: crate::types::Decoration::Normal,
timestamp: None,
}
}
#[cfg(target_os = "windows")]
fn make_td(lines: usize, show_scrollbar: bool) -> crate::primitives::text_display::TextDisplay {
crate::primitives::text_display::TextDisplay {
id: WidgetId::new("td"),
lines: (0..lines).map(|i| td_line(&format!("ln{i}"))).collect(),
scroll_offset: 0,
auto_scroll: false,
max_lines: 0,
has_focus: false,
title: None,
show_scrollbar,
}
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_text_display_background_fills_theme_background() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 240;
const H: u32 = 160;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let td = make_td(0, false);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_text_display(Rect::new(0.0, 0.0, W as f32, H as f32), &td);
backend.end_frame();
let theme = Theme::default();
let px = surface.pixel_at(W / 2, H / 2);
assert_eq!(
(px.r, px.g, px.b),
(theme.background.r, theme.background.g, theme.background.b)
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_text_display_scrollbar_gutter_and_thumb_paint() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 240;
const H: u32 = 160;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let td = make_td(100, true);
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_text_display(rect, &td);
let layout = backend.text_display_layout(rect, &td);
backend.end_frame();
let theme = Theme::default();
let gutter = layout.scrollbar_bounds.expect("gutter present");
let probe_x = (gutter.x + gutter.width / 2.0) as u32;
let probe_y = (gutter.y + gutter.height - 2.0) as u32;
let px = surface.pixel_at(probe_x, probe_y);
assert_eq!(
(px.r, px.g, px.b),
(
theme.scrollbar_track.r,
theme.scrollbar_track.g,
theme.scrollbar_track.b
)
);
let thumb = layout.thumb_bounds.expect("thumb present");
let probe_x = (thumb.x + thumb.width / 2.0) as u32;
let probe_y = (thumb.y + thumb.height / 2.0) as u32;
let px = surface.pixel_at(probe_x, probe_y);
assert_eq!(
(px.r, px.g, px.b),
(
theme.scrollbar_thumb.r,
theme.scrollbar_thumb.g,
theme.scrollbar_thumb.b
)
);
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_text_display_layout_hit_test_resolves_lines_at_nonzero_origin() {
use crate::primitives::text_display::TextDisplayHit;
use crate::win::testing::HeadlessSurface;
const W: u32 = 240;
const H: u32 = 160;
const LINE_HEIGHT: f32 = 16.0;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let mut td = make_td(20, false);
td.title = Some(crate::types::StyledText::plain("Logs"));
let rect_x = 9.0_f32;
let rect_y = 17.0_f32;
let rect = Rect::new(rect_x, rect_y, W as f32 - rect_x, H as f32 - rect_y);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_text_display(rect, &td);
let layout = backend.text_display_layout(rect, &td);
backend.end_frame();
let body_y = rect_y + LINE_HEIGHT;
let vis = &layout.visible_lines[0];
assert_eq!(
vis.bounds.y, 0.0,
"visible_lines bounds.y must be body-local"
);
let abs_x = rect_x + vis.bounds.x + vis.bounds.width * 0.5;
let abs_y = body_y + vis.bounds.y + vis.bounds.height * 0.5;
let local_x = abs_x - rect_x;
let local_y = abs_y - body_y;
match layout.hit_test(local_x, local_y) {
TextDisplayHit::Line(idx) => assert_eq!(idx, vis.line_idx),
other => panic!("expected Line, got {:?}", other),
}
}
#[cfg(target_os = "windows")]
#[test]
fn win_backend_draw_text_display_no_paint_layout_matches_paint_layout() {
use crate::win::testing::HeadlessSurface;
const W: u32 = 240;
const H: u32 = 160;
let surface = HeadlessSurface::new(W, H).expect("create headless surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W, H)
.expect("attach headless surface");
let td = make_td(30, true);
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.draw_text_display(rect, &td);
let painted = backend.text_display_layout(rect, &td);
backend.end_frame();
let no_paint = backend.text_display_layout(rect, &td);
assert_eq!(painted, no_paint);
}
fn sample_minimap() -> crate::primitives::minimap::Minimap {
crate::primitives::minimap::Minimap {
id: WidgetId::new("minimap"),
lines: (0..40)
.map(|i| crate::primitives::minimap::MinimapLine {
text: format!("line {i}"),
line_idx: i,
})
.collect(),
syntax_spans: Vec::new(),
visible_row_start: 0,
visible_row_count: 10,
total_buffer_lines: 40,
}
}
fn sample_panel() -> Panel {
Panel {
id: WidgetId::new("panel"),
title: None,
actions: Vec::new(),
accent: None,
collapsed: false,
}
}
#[test]
fn win_backend_does_not_panic_with_no_surface_attached_across_a_full_frame() {
let mut backend = WinBackend::new();
let viewport = Viewport::new(400.0, 300.0, 1.0);
Backend::begin_frame(&mut backend, viewport);
let rect = Rect::new(0.0, 0.0, 200.0, 100.0);
let msv = MultiSectionView {
id: WidgetId::new("msv"),
sections: Vec::new(),
active_section: None,
axis: crate::primitives::multi_section_view::Axis::Vertical,
allow_resize: false,
allow_collapse: false,
scroll_mode: crate::primitives::multi_section_view::ScrollMode::PerSection,
has_focus: false,
panel_scroll: 0.0,
};
backend.draw_multi_section_view(rect, &msv);
let activity_bar = ActivityBar {
id: WidgetId::new("activity-bar"),
top_items: Vec::new(),
bottom_items: Vec::new(),
active_accent: None,
selection_bg: None,
is_keyboard_focused: false,
};
let _ = backend.draw_activity_bar(rect, &activity_bar, None);
#[allow(deprecated)]
{
let tab_bar = TabBar {
id: WidgetId::new("tab-bar"),
tabs: Vec::new(),
scroll_offset: 0,
right_segments: Vec::new(),
active_accent: None,
show_tab_close: false,
compact: false,
};
let _ = backend.draw_tab_bar(rect, &tab_bar, None);
}
let toolbar = crate::primitives::toolbar::Toolbar {
id: WidgetId::new("toolbar"),
buttons: Vec::new(),
bg: None,
focused_index: None,
};
let interaction = crate::interaction::InteractionState::default();
let _ = backend.draw_toolbar_interactive(rect, &toolbar, &interaction);
let panel = sample_panel();
let _ = backend.draw_panel(rect, &panel);
let minimap = sample_minimap();
let _ = backend.draw_minimap(rect, &minimap);
let _ = PaintSurface::surface_measure_text(&backend, "hello");
let _ = PaintSurface::surface_measure_text_styled(&backend, "hello", true);
Backend::end_frame(&mut backend);
}
#[test]
fn draw_minimap_with_no_surface_reports_unpainted_and_agrees_with_minimap_layout() {
let mut backend = WinBackend::new();
let minimap = sample_minimap();
let rect = Rect::new(0.0, 0.0, 20.0, 100.0);
let result = backend.draw_minimap(rect, &minimap);
assert!(
!result.painted,
"no surface attached — `painted` must honestly report `false`, not panic"
);
assert!(
!result.layout.visible_lines.is_empty(),
"the layout must still be real geometry, not an empty stub"
);
let no_paint = backend.minimap_layout(rect, &minimap);
assert_eq!(
result.layout, no_paint,
"draw_minimap's returned layout must agree with minimap_layout's \
standalone query"
);
}
#[test]
fn set_minimap_scale_changes_the_resolved_row_pitch() {
use crate::primitives::minimap::MinimapScale;
let mut backend = WinBackend::new();
let minimap = sample_minimap();
let rect = Rect::new(0.0, 0.0, 20.0, 100.0);
assert_eq!(backend.minimap_scale(), MinimapScale::One);
let default_layout = backend.minimap_layout(rect, &minimap);
assert_eq!(
default_layout.visible_lines[0].bounds.height,
MinimapScale::One.row_pitch_px() as f32
);
backend.set_minimap_scale(MinimapScale::Two);
assert_eq!(backend.minimap_scale(), MinimapScale::Two);
let scaled_layout = backend.minimap_layout(rect, &minimap);
assert_eq!(
scaled_layout.visible_lines[0].bounds.height,
MinimapScale::Two.row_pitch_px() as f32
);
let result = backend.draw_minimap(rect, &minimap);
assert_eq!(
result.layout, scaled_layout,
"draw_minimap's own (unpainted, no-surface) layout must also honor the \
backend's minimap_scale"
);
}
#[test]
fn draw_panel_with_no_surface_agrees_with_panel_layout() {
let mut backend = WinBackend::new();
let panel = sample_panel();
let rect = Rect::new(0.0, 0.0, 200.0, 100.0);
let painted = backend.draw_panel(rect, &panel);
let no_paint = backend.panel_layout(rect, &panel);
assert_eq!(
painted, no_paint,
"draw_panel's returned layout must agree with panel_layout's \
standalone query"
);
}
#[test]
fn draw_image_with_no_surface_reports_unsupported() {
let mut backend = WinBackend::new();
let image = crate::primitives::image::Image {
id: WidgetId::new("img"),
source: crate::primitives::image::ImageSource::Bytes(Vec::new()),
intrinsic_size: None,
fit: crate::primitives::image::ImageFit::Contain,
fallback_text: "img".to_string(),
};
let rect = Rect::new(0.0, 0.0, 50.0, 50.0);
let result = backend.draw_image(rect, &image);
assert_eq!(result, crate::backend::ImagePaintResult::Unsupported);
}
#[test]
fn surface_measure_text_with_no_surface_returns_a_nominal_estimate() {
let backend = WinBackend::new();
let (w, h) = PaintSurface::surface_measure_text(&backend, "hello");
assert_eq!(w, 5.0 * backend.current_char_width);
assert_eq!(h, backend.current_line_height);
let (w2, h2) = PaintSurface::surface_measure_text_styled(&backend, "hello", true);
assert_eq!(w2, 5.0 * backend.current_char_width);
assert_eq!(h2, backend.current_line_height);
}
}