use windows::Win32::Graphics::Direct2D::Common::{D2D_RECT_F, D2D1_COLOR_F};
use windows::Win32::Graphics::Direct2D::{
D2D1_ANTIALIAS_MODE_ALIASED, D2D1_DRAW_TEXT_OPTIONS_CLIP, D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE,
ID2D1DeviceContext,
};
use windows::Win32::Graphics::DirectWrite::{
DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_WEIGHT_SEMI_BOLD, DWRITE_WORD_WRAPPING_NO_WRAP,
IDWriteFactory, IDWriteTextFormat,
};
use windows_numerics::Vector2;
use super::text_box;
use crate::error::{Call, Result};
use crate::render::{self, Graphics};
use crate::theme::Theme;
use crate::theme::rgba;
use crate::widget::TextStyle;
use crate::widget::tree::{Content, Measure, NAV_ITEM_PADDING, Rect, Tree};
const BUTTON_RADIUS: f32 = 4.0;
const CARD_RADIUS: f32 = 8.0;
const SCROLL_BAR: (f32, f32, f32) = (3.0, 3.0, 24.0);
const NAV_MARK: (f32, f32) = (3.0, 16.0);
const FOCUS_GAP: f32 = 3.0;
const FOCUS_WIDTH: f32 = 2.0;
pub(super) struct Formats {
pub(super) dwrite: IDWriteFactory,
text: Vec<IDWriteTextFormat>,
label: IDWriteTextFormat,
pub(super) line: IDWriteTextFormat,
}
impl Formats {
pub(super) fn new(gfx: &Graphics) -> Result<Self> {
let text = TextStyle::ALL
.iter()
.map(|style| {
let weight = if style.semibold() {
DWRITE_FONT_WEIGHT_SEMI_BOLD
} else {
DWRITE_FONT_WEIGHT_NORMAL
};
gfx.text_format(style.size(), weight, false)
})
.collect::<Result<Vec<_>>>()?;
let label = gfx.text_format(TextStyle::Body.size(), DWRITE_FONT_WEIGHT_NORMAL, true)?;
let line = gfx.text_format(TextStyle::Body.size(), DWRITE_FONT_WEIGHT_NORMAL, false)?;
for format in [&label, &line] {
unsafe {
format
.SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)
.call("SetWordWrapping")?;
}
}
Ok(Self {
dwrite: gfx.dwrite(),
text,
label,
line,
})
}
pub(super) fn text_format(&self, style: TextStyle) -> &IDWriteTextFormat {
let i = TextStyle::ALL.iter().position(|&s| s == style).unwrap_or(1);
&self.text[i]
}
fn size_of(&self, text: &str, format: &IDWriteTextFormat, max_width: f32) -> (f32, f32) {
render::text_size(&self.dwrite, text, format, max_width).unwrap_or((0.0, 0.0))
}
}
impl Measure for Formats {
fn text(&self, text: &str, style: TextStyle, max_width: f32) -> (f32, f32) {
self.size_of(text, self.text_format(style), max_width)
}
fn label(&self, label: &str) -> (f32, f32) {
self.size_of(label, &self.line, f32::MAX / 4.0)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub(super) struct Interaction {
pub hover: Option<usize>,
pub pressed: Option<usize>,
pub focus: Option<usize>,
pub cues: bool,
pub active: bool,
pub caret_off: bool,
}
pub(super) fn paint(
dc: &ID2D1DeviceContext,
tree: &Tree,
theme: &Theme,
formats: &Formats,
now: Interaction,
) -> windows::core::Result<()> {
unsafe {
dc.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
}
for index in tree.draw_order() {
let clip = tree.clip(index);
if clip.is_some_and(|c| c.intersect(tree.nodes[index].rect).is_empty()) {
continue;
}
if let Some(c) = clip {
unsafe { dc.PushAxisAlignedClip(&d2d(c), D2D1_ANTIALIAS_MODE_ALIASED) };
}
let drawn = widget(dc, tree, index, theme, formats, now);
if clip.is_some() {
unsafe { dc.PopAxisAlignedClip() };
}
drawn?;
}
for index in tree.draw_order() {
if let Content::Scroll { offset, extent } = tree.nodes[index].content {
scroll_bar(dc, theme, tree.nodes[index].rect, offset, extent)?;
}
}
Ok(())
}
fn scroll_bar(
dc: &ID2D1DeviceContext,
theme: &Theme,
r: Rect,
offset: f32,
extent: f32,
) -> windows::core::Result<()> {
if extent <= r.h || r.h <= 0.0 {
return Ok(());
}
let (width, inset, min) = SCROLL_BAR;
let length = (r.h * r.h / extent).max(min).min(r.h);
let y = r.y + (r.h - length) * (offset / (extent - r.h));
let bar = Rect::new(r.x + r.w - width - inset, y, width, length);
let mut color = theme.subtle_text;
color.a *= 0.6;
panel(dc, bar, width / 2.0, color, color)
}
fn widget(
dc: &ID2D1DeviceContext,
tree: &Tree,
index: usize,
theme: &Theme,
formats: &Formats,
now: Interaction,
) -> windows::core::Result<()> {
let node = &tree.nodes[index];
let r = node.rect;
match &node.content {
Content::Blank | Content::Scroll { .. } => {}
Content::Card => panel(dc, r, CARD_RADIUS, theme.card, theme.card_border)?,
Content::Text { text, style } => {
let color = if node.enabled {
theme.text
} else {
theme.subtle_text
};
draw_text(dc, formats, text, formats.text_format(*style), r, color)?;
}
Content::TextBox { .. } => text_box::draw(dc, theme, formats, node, index, now)?,
Content::NavItem { label, selected } => {
let hover = now.hover == Some(index);
let pressed = now.pressed == Some(index) && hover;
let fill = match (*selected, hover, pressed) {
(_, _, true) => Some(theme.button_pressed),
(true, true, _) => Some(theme.button_hover),
(true, false, _) | (false, true, _) => Some(theme.button),
(false, false, _) => None,
};
if let Some(fill) = fill {
panel(dc, r, BUTTON_RADIUS, fill, rgba(0.0, 0.0, 0.0, 0.0))?;
}
if *selected {
let (w, h) = NAV_MARK;
let mark = Rect::new(r.x, r.y + (r.h - h) / 2.0, w, h);
panel(dc, mark, w / 2.0, theme.accent, theme.accent)?;
}
let color = if node.enabled {
theme.text
} else {
theme.subtle_text
};
let (_, text_h) = formats.label(label);
let (left, right) = NAV_ITEM_PADDING;
let text = Rect::new(
r.x + left,
r.y + (r.h - text_h) / 2.0,
r.w - left - right,
text_h,
);
draw_text(dc, formats, label, &formats.line, text, color)?;
if now.cues && now.focus == Some(index) {
focus_ring(dc, theme, r)?;
}
}
Content::Button { label, accent } => {
let state = ButtonState {
hover: now.hover == Some(index),
pressed: now.pressed == Some(index) && now.hover == Some(index),
enabled: node.enabled,
};
button(dc, theme, r, *accent, state)?;
let color = match (*accent, node.enabled) {
(_, false) => theme.subtle_text,
(true, true) => theme.on_accent,
(false, true) => theme.text,
};
draw_text(dc, formats, label, &formats.label, r, color)?;
if now.cues && now.focus == Some(index) {
focus_ring(dc, theme, r)?;
}
}
}
Ok(())
}
#[derive(Clone, Copy)]
struct ButtonState {
hover: bool,
pressed: bool,
enabled: bool,
}
fn button(
dc: &ID2D1DeviceContext,
theme: &Theme,
r: Rect,
accent: bool,
s: ButtonState,
) -> windows::core::Result<()> {
let mut fill = if accent {
let mut c = theme.accent;
if s.pressed {
c.a *= 0.8;
} else if s.hover {
c.a *= 0.9;
}
c
} else if s.pressed {
theme.button_pressed
} else if s.hover {
theme.button_hover
} else {
theme.button
};
if !s.enabled {
fill.a *= 0.4;
}
panel(dc, r, BUTTON_RADIUS, fill, theme.button_border)
}
fn panel(
dc: &ID2D1DeviceContext,
r: Rect,
radius: f32,
fill: D2D1_COLOR_F,
border: D2D1_COLOR_F,
) -> windows::core::Result<()> {
render::panel(dc, d2d(r), radius, &fill, &border)
}
fn focus_ring(dc: &ID2D1DeviceContext, theme: &Theme, r: Rect) -> windows::core::Result<()> {
unsafe {
let ring = render::rounded(d2d(r), -FOCUS_GAP, BUTTON_RADIUS + FOCUS_GAP);
dc.DrawRoundedRectangle(
&ring,
&dc.CreateSolidColorBrush(&theme.text, None)?,
FOCUS_WIDTH,
None,
);
}
Ok(())
}
fn draw_text(
dc: &ID2D1DeviceContext,
formats: &Formats,
text: &str,
format: &IDWriteTextFormat,
r: Rect,
color: D2D1_COLOR_F,
) -> windows::core::Result<()> {
let wide: Vec<u16> = text.encode_utf16().collect();
unsafe {
let layout = formats
.dwrite
.CreateTextLayout(&wide, format, r.w.max(1.0), r.h.max(1.0))?;
dc.DrawTextLayout(
Vector2 { X: r.x, Y: r.y },
&layout,
&dc.CreateSolidColorBrush(&color, None)?,
D2D1_DRAW_TEXT_OPTIONS_CLIP,
);
}
Ok(())
}
pub(super) fn d2d(r: Rect) -> D2D_RECT_F {
D2D_RECT_F {
left: r.x,
top: r.y,
right: r.x + r.w,
bottom: r.y + r.h,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_label_has_the_width_of_its_text() {
let _ui = crate::Ui::new().unwrap();
let formats = Formats::new(&Graphics::new().unwrap()).unwrap();
let (short, _) = formats.label("OK");
let (long, height) = formats.label("Open the folder");
assert!(short > 0.0 && height > 0.0);
assert!(long > 3.0 * short, "{long} and {short}");
}
}