mod countdown;
mod glass;
mod image;
mod input;
mod layout;
mod markup;
mod prompt;
mod state;
use std::ffi::c_void;
use std::rc::Rc;
use std::time::SystemTime;
use windows::Win32::Foundation::{HINSTANCE, HWND, RECT};
use windows::Win32::Graphics::Dwm::{
DWMWA_USE_HOSTBACKDROPBRUSH, DWMWA_USE_IMMERSIVE_DARK_MODE, DWMWA_WINDOW_CORNER_PREFERENCE,
DWMWCP_DONOTROUND,
};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, IDC_ARROW, SPI_GETWORKAREA, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
SystemParametersInfoW, WS_EX_NOACTIVATE, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOOLWINDOW,
WS_EX_TOPMOST, WS_POPUP,
};
use windows::core::{PCWSTR, w};
use crate::Ui;
use crate::error::{Call, Error, Result};
use crate::events::PopupId;
use crate::native::{self, NativeWindow};
use crate::placement::{self, Area, Placement, Rect, SCREEN_MARGIN};
use crate::render::Graphics;
use crate::theme::Theme;
use crate::ui::PopupSlot;
use countdown::{label, whole_seconds};
use glass::Scene;
pub use image::Image;
pub use prompt::{Mark, PopupStyle, Prompt};
use state::{WindowState, wndproc};
const CLASS_NAME: PCWSTR = w!("GlasskitChoicePopup");
const STACK_GAP: f32 = 8.0;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Response {
Choice(usize),
Text(String),
Dismissed,
TimedOut,
Quit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reply {
pub response: Response,
pub at: SystemTime,
}
pub struct ChoicePopup {
ui: Ui,
id: PopupId,
placement: Placement,
style: PopupStyle,
active: Option<Active>,
}
struct Active {
window: NativeWindow<WindowState>,
_slot: PopupSlot,
_gfx: Graphics,
}
impl ChoicePopup {
pub fn set_style(&mut self, style: PopupStyle) {
self.style = style;
}
pub fn set_placement(&mut self, placement: Placement) {
self.placement = placement;
}
pub fn new(ui: &Ui, placement: Placement) -> Result<Self> {
native::register_class(CLASS_NAME, Some(wndproc), ui.hinstance(), IDC_ARROW)?;
Ok(Self {
ui: ui.clone(),
id: PopupId(ui.new_id()),
placement,
style: PopupStyle::default(),
active: None,
})
}
pub fn id(&self) -> PopupId {
self.id
}
pub fn is_showing(&self) -> bool {
self.active.is_some()
}
pub fn show(&mut self, request: &Prompt) -> Result<()> {
if self.active.is_some() {
return Err(Error::PopupShowing);
}
let scale = primary_monitor_scale();
let gfx = Graphics::new()?;
let fonts = layout::Fonts::new(&gfx, scale, &self.style)?;
let dismiss_label = match request.auto_dismiss {
Some(after) => label(&request.dismiss, whole_seconds(after)),
None => request.dismiss.to_string(),
};
let content = layout::Content::of(request, dismiss_label);
let layout = layout::build(scale, self.style.width, &gfx, &fonts, &content)?;
let theme = Theme::current();
let others = self.ui.popup_rects();
let (hwnd, rect) = create_window(
self.ui.hinstance(),
&layout,
self.placement,
scale,
&theme,
&others,
)?;
let guard = native::DestroyOnError::new(hwnd);
let scene = Scene::new(
&gfx,
hwnd,
layout.width,
layout.height,
layout.window_radius,
&theme,
)?;
let state = WindowState::new(self.id.0, self.style, layout, scene, theme, fonts, request);
let handle = NativeWindow::attach(hwnd, Rc::new(state));
guard.disarm();
handle.state().open(hwnd)?;
self.active = Some(Active {
window: handle,
_slot: self.ui.claim_popup_rect(self.id.0, rect),
_gfx: gfx,
});
Ok(())
}
pub fn take_reply(&mut self) -> Option<Reply> {
let reply = self.active.as_ref()?.window.state().reply()?;
self.close();
Some(reply)
}
pub fn close(&mut self) {
self.active = None;
}
pub fn ask(&mut self, request: &Prompt) -> Result<Reply> {
self.show(request)?;
loop {
if let Some(reply) = self.take_reply() {
return Ok(reply);
}
match self.ui.pump_wait() {
Ok(true) => {}
Ok(false) => {
let at = SystemTime::now();
self.close();
return Ok(Reply {
response: Response::Quit,
at,
});
}
Err(err) => {
self.close();
return Err(err);
}
}
}
}
}
impl Drop for ChoicePopup {
fn drop(&mut self) {
self.close();
}
}
fn primary_monitor_scale() -> f32 {
crate::primary_monitor_dpi() as f32 / 96.0
}
fn create_window(
hinstance: HINSTANCE,
layout: &layout::Layout,
placement: Placement,
scale: f32,
theme: &Theme,
others: &[Rect],
) -> Result<(HWND, Rect)> {
unsafe {
let mut work = RECT::default();
SystemParametersInfoW(
SPI_GETWORKAREA,
0,
Some(&mut work as *mut _ as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
)
.call("SystemParametersInfoW(SPI_GETWORKAREA)")?;
let margin = (SCREEN_MARGIN * scale).round() as i32;
let (win_w, win_h) = (layout.width as i32, layout.height.ceil() as i32);
let (x, y) = placement.place(
Area::from((work.left, work.top, work.right, work.bottom)),
win_w,
win_h,
margin,
);
let rect = placement::stack(
Rect {
x,
y,
width: win_w,
height: win_h,
},
others,
placement.y() > 0.5,
(STACK_GAP * scale).round() as i32,
work.top,
work.bottom,
);
let (x, y) = (rect.x, rect.y);
let mut ex_style = WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOREDIRECTIONBITMAP;
if layout.input.is_none() {
ex_style |= WS_EX_NOACTIVATE;
}
let hwnd = CreateWindowExW(
ex_style,
CLASS_NAME,
CLASS_NAME,
WS_POPUP,
x,
y,
win_w,
win_h,
None,
None,
Some(hinstance),
None,
)
.call("CreateWindowExW")?;
native::set_dwm_attribute(
hwnd,
DWMWA_WINDOW_CORNER_PREFERENCE,
DWMWCP_DONOTROUND.0 as u32,
);
native::set_dwm_attribute(hwnd, DWMWA_USE_HOSTBACKDROPBRUSH, 1);
native::set_dwm_attribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, u32::from(theme.dark));
Ok((hwnd, rect))
}
}