use std::cell::{Cell, RefCell};
use std::time::{Duration, Instant, SystemTime};
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::Graphics::Direct2D::Common::{
D2D_RECT_F, D2D_SIZE_U, D2D1_ALPHA_MODE_PREMULTIPLIED, D2D1_PIXEL_FORMAT,
};
use windows::Win32::Graphics::Direct2D::{
D2D1_BITMAP_OPTIONS_NONE, D2D1_BITMAP_PROPERTIES1, D2D1_DRAW_TEXT_OPTIONS_NONE,
D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC, D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE,
ID2D1DeviceContext,
};
use windows::Win32::Graphics::DirectWrite::DWRITE_MEASURING_MODE_NATURAL;
use windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_B8G8R8A8_UNORM;
use windows::Win32::UI::Controls::WM_MOUSELEAVE;
use windows::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture};
use windows::Win32::UI::WindowsAndMessaging::{
DefWindowProcW, HWND_TOPMOST, IDC_HAND, KillTimer, LoadCursorW, MA_ACTIVATE, MA_NOACTIVATE,
SW_SHOWNOACTIVATE, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SetCursor, SetTimer, SetWindowPos,
ShowWindow, WM_CHAR, WM_DESTROY, WM_KEYDOWN, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP,
WM_MOUSEACTIVATE, WM_MOUSEMOVE, WM_SETCURSOR, WM_SETFOCUS, WM_TIMER,
};
use windows_numerics::Vector2;
use super::countdown::{self, Countdown};
use super::glass::Scene;
use super::input::{Frame, TextBox};
use super::layout::{Fonts, Layout};
use super::{Image, Mark, PopupStyle, Prompt, Reply, Response};
use crate::error::{Call, Result};
use crate::native::{self, state_of};
use crate::render;
use crate::theme::Theme;
use crate::ui::Notice;
const FADE_OUT_EXTRA: Duration = Duration::from_millis(50);
const TIMER_COUNTDOWN: usize = 1;
const TIMER_FADE_OUT: usize = 2;
const TIMER_FEEDBACK: usize = 3;
const BACKSPACE: u16 = 0x08;
const ENTER: u16 = 0x0D;
const PASTE: u16 = 0x16;
const ESCAPE: u16 = 0x1B;
pub(super) struct WindowState {
pub(super) id: u32,
pub(super) style: PopupStyle,
pub(super) layout: RefCell<Layout>,
pub(super) scene: Scene,
pub(super) theme: Theme,
pub(super) fonts: Fonts,
hover: Cell<Option<usize>>,
pressed: Cell<Option<usize>>,
pointer_inside: Cell<bool>,
dismiss: String,
countdown: RefCell<Option<Countdown>>,
marks: Option<Vec<Vec<(usize, Mark)>>>,
image: Option<Image>,
input: Option<RefCell<TextBox>>,
selected: Cell<Option<usize>>,
closing: Cell<bool>,
pending: RefCell<Option<Response>>,
decided_at: Cell<Option<SystemTime>>,
response: RefCell<Option<Response>>,
}
impl WindowState {
pub(super) fn new(
id: u32,
style: PopupStyle,
layout: Layout,
scene: Scene,
theme: Theme,
fonts: Fonts,
request: &Prompt,
) -> Self {
Self {
id,
style,
layout: RefCell::new(layout),
scene,
theme,
fonts,
hover: Cell::new(None),
pressed: Cell::new(None),
pointer_inside: Cell::new(false),
dismiss: request.dismiss.to_string(),
countdown: RefCell::new(request.auto_dismiss.map(Countdown::new)),
marks: request.marks.clone(),
image: request.image.clone(),
input: request.input.clone().map(|p| RefCell::new(TextBox::new(p))),
selected: Cell::new(None),
closing: Cell::new(false),
pending: RefCell::new(None),
decided_at: Cell::new(None),
response: RefCell::new(None),
}
}
pub(super) fn reply(&self) -> Option<Reply> {
let response = self.response.borrow().clone()?;
let at = self.decided_at.get().unwrap_or_else(SystemTime::now);
Some(Reply { response, at })
}
pub(super) fn open(&self, hwnd: HWND) -> Result<()> {
self.draw()?;
unsafe {
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
SetWindowPos(
hwnd,
Some(HWND_TOPMOST),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
)
.call("SetWindowPos")?;
}
self.scene.fade(0.0, 1.0, self.style.fade)?;
self.resume_countdown(hwnd);
Ok(())
}
fn finish(&self, response: Response) {
if self.response.borrow().is_none() {
*self.response.borrow_mut() = Some(response);
crate::ui::notify(Notice::Popup(self.id));
}
}
fn tick(&self, hwnd: HWND) {
let (left, label, running) = {
let mut countdown = self.countdown.borrow_mut();
let Some(c) = countdown.as_mut() else {
return;
};
let left = c.remaining(Instant::now());
let seconds = countdown::whole_seconds(left);
let mut label = None;
if seconds != c.shown {
c.shown = seconds;
label = Some(countdown::label(&self.dismiss, seconds));
}
(left, label, c.is_running())
};
if left.is_zero() {
self.decide();
self.fade_out(hwnd, Response::TimedOut);
return;
}
if let Some(label) = label {
if let Some(b) = self.layout.borrow_mut().buttons.last_mut() {
b.label = label.encode_utf16().collect();
}
self.redraw();
}
if running {
self.arm_countdown(hwnd, left);
}
}
fn resume_countdown(&self, hwnd: HWND) {
let now = Instant::now();
let left = self.countdown.borrow_mut().as_mut().map(|c| {
c.resume(now);
c.remaining(now)
});
if let Some(left) = left {
self.arm_countdown(hwnd, left);
}
}
fn pause_countdown(&self, hwnd: HWND) {
if let Some(c) = self.countdown.borrow_mut().as_mut() {
c.pause(Instant::now());
}
unsafe {
let _ = KillTimer(Some(hwnd), TIMER_COUNTDOWN);
}
}
fn arm_countdown(&self, hwnd: HWND, left: Duration) {
let delay = countdown::until_next_second(left);
unsafe {
SetTimer(Some(hwnd), TIMER_COUNTDOWN, delay.as_millis() as u32, None);
}
}
fn decide(&self) {
if self.decided_at.get().is_none() {
self.decided_at.set(Some(SystemTime::now()));
}
}
fn close_input(&self, hwnd: HWND) {
self.countdown.replace(None);
self.closing.set(true);
self.hover.set(None);
unsafe {
let _ = KillTimer(Some(hwnd), TIMER_COUNTDOWN);
if self.pressed.take().is_some() {
let _ = ReleaseCapture();
}
}
}
fn show_feedback(&self, hwnd: HWND, selected: usize) {
self.close_input(hwnd);
self.selected.set(Some(selected));
self.redraw();
let held = unsafe {
SetTimer(
Some(hwnd),
TIMER_FEEDBACK,
self.style.hold.as_millis() as u32,
None,
) != 0
};
if !held {
self.fade_out(hwnd, Response::Choice(selected));
}
}
fn fade_out(&self, hwnd: HWND, response: Response) {
self.close_input(hwnd);
*self.pending.borrow_mut() = Some(response.clone());
unsafe {
let started = match self.scene.fade(1.0, 0.0, self.style.fade) {
Ok(()) => {
SetTimer(
Some(hwnd),
TIMER_FADE_OUT,
(self.style.fade + FADE_OUT_EXTRA).as_millis() as u32,
None,
) != 0
}
Err(_) => false,
};
if !started {
self.finish(response);
}
}
}
fn stop_countdown(&self, hwnd: HWND) {
if self.countdown.replace(None).is_some() {
unsafe {
let _ = KillTimer(Some(hwnd), TIMER_COUNTDOWN);
}
if let Some(b) = self.layout.borrow_mut().buttons.last_mut() {
b.label = self.dismiss.encode_utf16().collect();
}
}
}
fn type_char(&self, hwnd: HWND, unit: u16) {
let Some(input) = &self.input else {
return;
};
let changed = match unit {
ENTER => {
let text = input.borrow().text().trim().to_string();
if !text.is_empty() {
self.decide();
self.close_input(hwnd);
self.finish(Response::Text(text));
}
return;
}
ESCAPE => {
self.decide();
self.close_input(hwnd);
self.finish(Response::Dismissed);
return;
}
BACKSPACE => input.borrow_mut().backspace(),
PASTE => match crate::clipboard::text(hwnd) {
Some(text) => input.borrow_mut().insert(&text),
None => false,
},
unit if unit < 0x20 || unit == 0x7F => false,
unit => input.borrow_mut().type_unit(unit),
};
if changed {
self.stop_countdown(hwnd);
self.redraw();
}
}
fn move_caret(&self, key: u16) -> bool {
let Some(input) = &self.input else {
return false;
};
let Some(changed) = input.borrow_mut().press(key) else {
return false;
};
if changed {
self.redraw();
}
true
}
fn hit(&self, lparam: LPARAM) -> Option<usize> {
let (x, y) = native::point(lparam);
let (x, y) = (x as f32, y as f32);
self.layout.borrow().buttons.iter().position(|b| {
x >= b.rect.left && x < b.rect.right && y >= b.rect.top && y < b.rect.bottom
})
}
fn draw(&self) -> Result<()> {
let t = &self.theme;
let l = &*self.layout.borrow();
let (hover, pressed) = (self.hover.get(), self.pressed.get());
let shown: &[(usize, Mark)] = match (self.selected.get(), &self.marks) {
(Some(clicked), Some(marks)) => &marks[clicked],
_ => &[],
};
self.scene.draw(|dc: &ID2D1DeviceContext| unsafe {
dc.SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
let frame = render::rounded(
render::rect(0.0, 0.0, l.width, l.height),
0.5,
l.window_radius,
);
let window_border = dc.CreateSolidColorBrush(&t.window_border, None)?;
dc.DrawRoundedRectangle(&frame, &window_border, 1.0, None);
let text = dc.CreateSolidColorBrush(&t.text, None)?;
dc.DrawTextLayout(
Vector2 { X: l.pad, Y: l.pad },
&l.prompt,
&text,
D2D1_DRAW_TEXT_OPTIONS_NONE,
);
if let (Some(rect), Some(image)) = (l.image, &self.image) {
draw_image(dc, image, rect)?;
}
if let (Some(rect), Some(input)) = (l.input, &self.input) {
let frame = Frame {
rect,
radius: l.radius,
padding: l.input_padding,
};
input.borrow().draw(dc, frame, &self.theme, &self.fonts)?;
}
for (i, b) in l.buttons.iter().enumerate() {
let mark = shown
.iter()
.find(|&&(index, _)| index == i)
.map(|&(_, m)| m);
let fill = match mark {
Some(Mark::Good) => t.good,
Some(Mark::Bad) => t.bad,
_ if pressed == Some(i) && hover == Some(i) => t.button_pressed,
_ if hover == Some(i) => t.button_hover,
_ => t.button,
};
render::panel(dc, b.rect, l.radius, &fill, &t.button_border)?;
dc.DrawText(
&b.label,
&self.fonts.button,
&b.rect,
&text,
D2D1_DRAW_TEXT_OPTIONS_NONE,
DWRITE_MEASURING_MODE_NATURAL,
);
}
Ok(())
})
}
fn redraw(&self) {
let _ = self.draw();
}
}
fn draw_image(
dc: &ID2D1DeviceContext,
image: &Image,
rect: D2D_RECT_F,
) -> windows::core::Result<()> {
unsafe {
let bitmap = dc.CreateBitmap(
D2D_SIZE_U {
width: image.width,
height: image.height,
},
Some(image.pixels.as_ptr().cast()),
image.width * 4,
&D2D1_BITMAP_PROPERTIES1 {
pixelFormat: D2D1_PIXEL_FORMAT {
format: DXGI_FORMAT_B8G8R8A8_UNORM,
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
},
dpiX: 96.0,
dpiY: 96.0,
bitmapOptions: D2D1_BITMAP_OPTIONS_NONE,
colorContext: std::mem::ManuallyDrop::new(None),
},
)?;
dc.DrawBitmap(
&bitmap,
Some(&rect),
1.0,
D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC,
None,
None,
);
}
Ok(())
}
pub(super) extern "system" fn wndproc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
let Some(state) = state_of::<WindowState>(hwnd) else {
return DefWindowProcW(hwnd, msg, wparam, lparam);
};
match msg {
WM_MOUSEACTIVATE if state.input.is_some() && !state.closing.get() => {
LRESULT(MA_ACTIVATE as isize)
}
WM_MOUSEACTIVATE => LRESULT(MA_NOACTIVATE as isize),
WM_SETFOCUS | WM_KILLFOCUS => {
if let Some(input) = &state.input {
input.borrow_mut().focused = msg == WM_SETFOCUS;
state.redraw();
}
LRESULT(0)
}
WM_CHAR if state.input.is_some() && !state.closing.get() => {
state.type_char(hwnd, wparam.0 as u16);
LRESULT(0)
}
WM_KEYDOWN if state.input.is_some() && !state.closing.get() => {
if state.move_caret(wparam.0 as u16) {
LRESULT(0)
} else {
DefWindowProcW(hwnd, msg, wparam, lparam)
}
}
WM_TIMER => {
match wparam.0 {
TIMER_COUNTDOWN => state.tick(hwnd),
TIMER_FEEDBACK => {
let _ = KillTimer(Some(hwnd), TIMER_FEEDBACK);
if let Some(selected) = state.selected.get() {
state.fade_out(hwnd, Response::Choice(selected));
}
}
TIMER_FADE_OUT => {
let pending = state.pending.borrow_mut().take();
if let Some(response) = pending {
state.finish(response);
}
}
_ => {}
}
LRESULT(0)
}
WM_MOUSEMOVE | WM_LBUTTONDOWN | WM_LBUTTONUP if state.closing.get() => LRESULT(0),
WM_MOUSEMOVE => {
if !state.pointer_inside.get() && native::track_leave(hwnd) {
state.pointer_inside.set(true);
state.pause_countdown(hwnd);
}
let hit = state.hit(lparam);
if hit != state.hover.get() {
state.hover.set(hit);
state.redraw();
}
LRESULT(0)
}
WM_MOUSELEAVE => {
state.pointer_inside.set(false);
state.resume_countdown(hwnd);
state.hover.set(None);
state.redraw();
LRESULT(0)
}
WM_SETCURSOR if state.hover.get().is_some() => {
if let Ok(hand) = LoadCursorW(None, IDC_HAND) {
SetCursor(Some(hand));
}
LRESULT(1)
}
WM_LBUTTONDOWN => {
let hit = state.hit(lparam);
state.pressed.set(hit);
if hit.is_some() {
SetCapture(hwnd);
state.redraw();
}
LRESULT(0)
}
WM_LBUTTONUP => {
let pressed = state.pressed.take();
let _ = ReleaseCapture();
let hit = state.hit(lparam);
match pressed {
Some(i) if hit == Some(i) => {
state.decide();
let response = state.layout.borrow().buttons[i].response.clone();
match response {
Response::Choice(choice) if state.marks.is_some() => {
state.show_feedback(hwnd, choice);
}
_ => state.finish(response),
}
}
Some(_) => {
state.hover.set(hit);
state.redraw();
}
None => {}
}
LRESULT(0)
}
WM_DESTROY => {
state.finish(Response::Dismissed);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}