dialog.rs

  1use std::{cell::RefCell, sync::mpsc::Receiver};
  2
  3use anyhow::{Context as _, Result};
  4use windows::{
  5    Win32::{
  6        Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM},
  7        Graphics::Gdi::{
  8            BeginPaint, CLEARTYPE_QUALITY, CLIP_DEFAULT_PRECIS, CreateFontW, DEFAULT_CHARSET,
  9            DeleteObject, EndPaint, FW_NORMAL, LOGFONTW, OUT_TT_ONLY_PRECIS, PAINTSTRUCT,
 10            ReleaseDC, SelectObject, TextOutW,
 11        },
 12        System::LibraryLoader::GetModuleHandleW,
 13        UI::{
 14            Controls::{PBM_SETRANGE, PBM_SETSTEP, PBM_STEPIT, PROGRESS_CLASS},
 15            WindowsAndMessaging::{
 16                CREATESTRUCTW, CS_HREDRAW, CS_VREDRAW, CreateWindowExW, DefWindowProcW,
 17                GWLP_USERDATA, GetDesktopWindow, GetWindowLongPtrW, GetWindowRect, HICON,
 18                IMAGE_ICON, LR_DEFAULTSIZE, LR_SHARED, LoadImageW, PostQuitMessage, RegisterClassW,
 19                SPI_GETICONTITLELOGFONT, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SendMessageW,
 20                SetWindowLongPtrW, SystemParametersInfoW, WINDOW_EX_STYLE, WM_CLOSE, WM_CREATE,
 21                WM_DESTROY, WM_NCCREATE, WM_PAINT, WNDCLASSW, WS_CAPTION, WS_CHILD, WS_EX_TOPMOST,
 22                WS_POPUP, WS_VISIBLE,
 23            },
 24        },
 25    },
 26    core::HSTRING,
 27};
 28
 29use crate::{
 30    updater::JOBS,
 31    windows_impl::{WM_JOB_UPDATED, WM_TERMINATE, show_error},
 32};
 33
 34#[repr(C)]
 35#[derive(Debug)]
 36struct DialogInfo {
 37    rx: Receiver<Result<()>>,
 38    progress_bar: isize,
 39}
 40
 41pub(crate) fn create_dialog_window(receiver: Receiver<Result<()>>) -> Result<HWND> {
 42    unsafe {
 43        let class_name = windows::core::w!("Zed-Auto-Updater-Dialog-Class");
 44        let module = GetModuleHandleW(None).context("unable to get module handle")?;
 45        let handle = LoadImageW(
 46            Some(module.into()),
 47            windows::core::PCWSTR(1 as _),
 48            IMAGE_ICON,
 49            0,
 50            0,
 51            LR_DEFAULTSIZE | LR_SHARED,
 52        )
 53        .context("unable to load icon file")?;
 54        let wc = WNDCLASSW {
 55            lpfnWndProc: Some(wnd_proc),
 56            lpszClassName: class_name,
 57            style: CS_HREDRAW | CS_VREDRAW,
 58            hIcon: HICON(handle.0),
 59            ..Default::default()
 60        };
 61        RegisterClassW(&wc);
 62        let mut rect = RECT::default();
 63        GetWindowRect(GetDesktopWindow(), &mut rect)
 64            .context("unable to get desktop window rect")?;
 65        let width = 400;
 66        let height = 150;
 67        let info = Box::new(RefCell::new(DialogInfo {
 68            rx: receiver,
 69            progress_bar: 0,
 70        }));
 71
 72        let hwnd = CreateWindowExW(
 73            WS_EX_TOPMOST,
 74            class_name,
 75            windows::core::w!("Zed"),
 76            WS_VISIBLE | WS_POPUP | WS_CAPTION,
 77            rect.right / 2 - width / 2,
 78            rect.bottom / 2 - height / 2,
 79            width,
 80            height,
 81            None,
 82            None,
 83            None,
 84            Some(Box::into_raw(info) as _),
 85        )
 86        .context("unable to create dialog window")?;
 87        Ok(hwnd)
 88    }
 89}
 90
 91macro_rules! return_if_failed {
 92    ($e:expr) => {
 93        match $e {
 94            Ok(v) => v,
 95            Err(e) => {
 96                return LRESULT(e.code().0 as _);
 97            }
 98        }
 99    };
100}
101
102macro_rules! make_lparam {
103    ($l:expr, $h:expr) => {
104        LPARAM(($l as u32 | ($h as u32) << 16) as isize)
105    };
106}
107
108unsafe extern "system" fn wnd_proc(
109    hwnd: HWND,
110    msg: u32,
111    wparam: WPARAM,
112    lparam: LPARAM,
113) -> LRESULT {
114    match msg {
115        WM_NCCREATE => unsafe {
116            let create_struct = lparam.0 as *const CREATESTRUCTW;
117            let info = (*create_struct).lpCreateParams as *mut RefCell<DialogInfo>;
118            let info = Box::from_raw(info);
119            SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(info) as _);
120            DefWindowProcW(hwnd, msg, wparam, lparam)
121        },
122        WM_CREATE => unsafe {
123            // Create progress bar
124            let mut rect = RECT::default();
125            return_if_failed!(GetWindowRect(hwnd, &mut rect));
126            let progress_bar = return_if_failed!(CreateWindowExW(
127                WINDOW_EX_STYLE(0),
128                PROGRESS_CLASS,
129                None,
130                WS_CHILD | WS_VISIBLE,
131                20,
132                50,
133                340,
134                35,
135                Some(hwnd),
136                None,
137                None,
138                None,
139            ));
140            SendMessageW(
141                progress_bar,
142                PBM_SETRANGE,
143                None,
144                Some(make_lparam!(0, JOBS.len() * 10)),
145            );
146            SendMessageW(progress_bar, PBM_SETSTEP, Some(WPARAM(10)), None);
147            with_dialog_data(hwnd, |data| {
148                data.borrow_mut().progress_bar = progress_bar.0 as isize
149            });
150            LRESULT(0)
151        },
152        WM_PAINT => unsafe {
153            let mut ps = PAINTSTRUCT::default();
154            let hdc = BeginPaint(hwnd, &mut ps);
155
156            let font_name = get_system_ui_font_name();
157            let font = CreateFontW(
158                24,
159                0,
160                0,
161                0,
162                FW_NORMAL.0 as _,
163                0,
164                0,
165                0,
166                DEFAULT_CHARSET,
167                OUT_TT_ONLY_PRECIS,
168                CLIP_DEFAULT_PRECIS,
169                CLEARTYPE_QUALITY,
170                0,
171                &HSTRING::from(font_name),
172            );
173            let temp = SelectObject(hdc, font.into());
174            let string = HSTRING::from("Updating Zed...");
175            return_if_failed!(TextOutW(hdc, 20, 15, &string).ok());
176            return_if_failed!(DeleteObject(temp).ok());
177
178            return_if_failed!(EndPaint(hwnd, &ps).ok());
179            ReleaseDC(Some(hwnd), hdc);
180
181            LRESULT(0)
182        },
183        WM_JOB_UPDATED => with_dialog_data(hwnd, |data| {
184            let progress_bar = data.borrow().progress_bar;
185            unsafe { SendMessageW(HWND(progress_bar as _), PBM_STEPIT, None, None) }
186        }),
187        WM_TERMINATE => {
188            with_dialog_data(hwnd, |data| {
189                if let Ok(result) = data.borrow_mut().rx.recv()
190                    && let Err(e) = result
191                {
192                    log::error!("Failed to update Zed: {:?}", e);
193                    show_error(format!("Error: {:?}", e));
194                }
195            });
196            unsafe { PostQuitMessage(0) };
197            LRESULT(0)
198        }
199        WM_CLOSE => LRESULT(0), // Prevent user occasionally closing the window
200        WM_DESTROY => {
201            unsafe { PostQuitMessage(0) };
202            LRESULT(0)
203        }
204        _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
205    }
206}
207
208fn with_dialog_data<F, T>(hwnd: HWND, f: F) -> T
209where
210    F: FnOnce(&RefCell<DialogInfo>) -> T,
211{
212    let raw = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut RefCell<DialogInfo> };
213    let data = unsafe { Box::from_raw(raw) };
214    let result = f(data.as_ref());
215    unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(data) as _) };
216    result
217}
218
219fn get_system_ui_font_name() -> String {
220    unsafe {
221        let mut info: LOGFONTW = std::mem::zeroed();
222        if SystemParametersInfoW(
223            SPI_GETICONTITLELOGFONT,
224            std::mem::size_of::<LOGFONTW>() as u32,
225            Some(&mut info as *mut _ as _),
226            SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
227        )
228        .is_ok()
229        {
230            let font_name = String::from_utf16_lossy(&info.lfFaceName);
231            font_name.trim_matches(char::from(0)).to_owned()
232        } else {
233            "MS Shell Dlg".to_owned()
234        }
235    }
236}