#![allow(clippy::new_ret_no_self)] #![allow(clippy::too_many_arguments)]
use crate::error::{Error, Result};
use crate::string::WideString;
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, WPARAM};
use windows::Win32::Graphics::Gdi::InvalidateRect;
use windows::Win32::UI::Controls::{
InitCommonControlsEx, ICC_STANDARD_CLASSES, ICC_WIN95_CLASSES, INITCOMMONCONTROLSEX,
PBM_DELTAPOS, PBM_GETPOS, PBM_SETMARQUEE, PBM_SETPOS, PBM_SETRANGE32, PBM_SETSTEP, PBM_STEPIT,
PBS_MARQUEE, PBS_SMOOTH, PROGRESS_CLASSW,
};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DestroyWindow, GetWindowLongPtrW, SendMessageW, SetWindowLongPtrW,
SetWindowTextW, ShowWindow, HMENU, SW_HIDE, SW_SHOW, WINDOW_EX_STYLE, WINDOW_STYLE, WM_GETTEXT,
WM_GETTEXTLENGTH, WS_BORDER, WS_CHILD, WS_DISABLED, WS_EX_CLIENTEDGE, WS_TABSTOP, WS_VISIBLE,
};
const BS_PUSHBUTTON: i32 = 0x0000;
const BS_DEFPUSHBUTTON: i32 = 0x0001;
const BS_CHECKBOX: i32 = 0x0002;
const BS_AUTOCHECKBOX: i32 = 0x0003;
const BS_RADIOBUTTON: i32 = 0x0004;
const BS_AUTORADIOBUTTON: i32 = 0x0009;
const BS_GROUPBOX: i32 = 0x0007;
const BST_UNCHECKED: usize = 0x0000;
const BST_CHECKED: usize = 0x0001;
const BM_GETCHECK: u32 = 0x00F0;
const BM_SETCHECK: u32 = 0x00F1;
const ES_LEFT: i32 = 0x0000;
const ES_CENTER: i32 = 0x0001;
const ES_RIGHT: i32 = 0x0002;
const ES_MULTILINE: i32 = 0x0004;
const ES_PASSWORD: i32 = 0x0020;
const ES_AUTOVSCROLL: i32 = 0x0040;
const ES_AUTOHSCROLL: i32 = 0x0080;
const ES_READONLY: i32 = 0x0800;
const ES_NUMBER: i32 = 0x2000;
const EM_GETSEL: u32 = 0x00B0;
const EM_SETSEL: u32 = 0x00B1;
const EM_LIMITTEXT: u32 = 0x00C5;
const EM_REPLACESEL: u32 = 0x00C2;
const EM_SETREADONLY: u32 = 0x00CF;
const LB_ADDSTRING: u32 = 0x0180;
const LB_INSERTSTRING: u32 = 0x0181;
const LB_DELETESTRING: u32 = 0x0182;
const LB_RESETCONTENT: u32 = 0x0184;
const LB_GETCOUNT: u32 = 0x018B;
const LB_GETCURSEL: u32 = 0x0188;
const LB_SETCURSEL: u32 = 0x0186;
const CB_ADDSTRING: u32 = 0x0143;
const CB_RESETCONTENT: u32 = 0x014B;
const CB_GETCOUNT: u32 = 0x0146;
const CB_GETCURSEL: u32 = 0x0147;
const CB_SETCURSEL: u32 = 0x014E;
pub fn init_common_controls() -> Result<()> {
let icc = INITCOMMONCONTROLSEX {
dwSize: std::mem::size_of::<INITCOMMONCONTROLSEX>() as u32,
dwICC: ICC_STANDARD_CLASSES | ICC_WIN95_CLASSES,
};
let result = unsafe { InitCommonControlsEx(&icc) };
if result.as_bool() {
Ok(())
} else {
Err(Error::last_os_error())
}
}
#[derive(Debug)]
pub struct Control {
hwnd: HWND,
owned: bool,
}
impl Control {
pub unsafe fn from_raw(hwnd: HWND, owned: bool) -> Self {
Self { hwnd, owned }
}
pub fn hwnd(&self) -> HWND {
self.hwnd
}
pub fn show(&self) {
unsafe {
let _ = ShowWindow(self.hwnd, SW_SHOW);
}
}
pub fn hide(&self) {
unsafe {
let _ = ShowWindow(self.hwnd, SW_HIDE);
}
}
pub fn enable(&self) {
unsafe {
let style = GetWindowLongPtrW(
self.hwnd,
windows::Win32::UI::WindowsAndMessaging::GWL_STYLE,
);
SetWindowLongPtrW(
self.hwnd,
windows::Win32::UI::WindowsAndMessaging::GWL_STYLE,
style & !(WS_DISABLED.0 as isize),
);
let _ = InvalidateRect(self.hwnd, None, true);
}
}
pub fn disable(&self) {
unsafe {
let style = GetWindowLongPtrW(
self.hwnd,
windows::Win32::UI::WindowsAndMessaging::GWL_STYLE,
);
SetWindowLongPtrW(
self.hwnd,
windows::Win32::UI::WindowsAndMessaging::GWL_STYLE,
style | (WS_DISABLED.0 as isize),
);
let _ = InvalidateRect(self.hwnd, None, true);
}
}
pub fn text(&self) -> String {
unsafe {
let len = SendMessageW(self.hwnd, WM_GETTEXTLENGTH, WPARAM(0), LPARAM(0)).0 as usize;
if len == 0 {
return String::new();
}
let mut buffer = vec![0u16; len + 1];
SendMessageW(
self.hwnd,
WM_GETTEXT,
WPARAM(buffer.len()),
LPARAM(buffer.as_mut_ptr() as isize),
);
String::from_utf16_lossy(&buffer[..len])
}
}
pub fn set_text(&self, text: &str) {
let wide = WideString::new(text);
unsafe {
let _ = SetWindowTextW(self.hwnd, wide.as_pcwstr());
}
}
pub fn set_user_data(&self, data: isize) {
unsafe {
SetWindowLongPtrW(
self.hwnd,
windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA,
data,
);
}
}
pub fn user_data(&self) -> isize {
unsafe {
GetWindowLongPtrW(
self.hwnd,
windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA,
)
}
}
}
impl Drop for Control {
fn drop(&mut self) {
if self.owned && !self.hwnd.is_invalid() {
unsafe {
let _ = DestroyWindow(self.hwnd);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonStyle {
Push,
Default,
Checkbox,
AutoCheckbox,
Radio,
AutoRadio,
GroupBox,
}
impl ButtonStyle {
fn to_style(self) -> u32 {
match self {
ButtonStyle::Push => BS_PUSHBUTTON as u32,
ButtonStyle::Default => BS_DEFPUSHBUTTON as u32,
ButtonStyle::Checkbox => BS_CHECKBOX as u32,
ButtonStyle::AutoCheckbox => BS_AUTOCHECKBOX as u32,
ButtonStyle::Radio => BS_RADIOBUTTON as u32,
ButtonStyle::AutoRadio => BS_AUTORADIOBUTTON as u32,
ButtonStyle::GroupBox => BS_GROUPBOX as u32,
}
}
}
pub struct Button;
impl Button {
pub fn new(
parent: HWND,
text: &str,
x: i32,
y: i32,
width: i32,
height: i32,
id: u16,
style: ButtonStyle,
) -> Result<Control> {
init_common_controls()?;
let text_wide = WideString::new(text);
let class_wide = WideString::new("BUTTON");
let base_style = WS_CHILD | WS_VISIBLE | WS_TABSTOP;
let button_style = WINDOW_STYLE(style.to_style());
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
class_wide.as_pcwstr(),
text_wide.as_pcwstr(),
base_style | button_style,
x,
y,
width,
height,
parent,
HMENU(id as isize as *mut _),
HINSTANCE::default(),
None,
)?
};
Ok(unsafe { Control::from_raw(hwnd, true) })
}
pub fn is_checked(control: &Control) -> bool {
let result = unsafe { SendMessageW(control.hwnd(), BM_GETCHECK, WPARAM(0), LPARAM(0)) };
result.0 == BST_CHECKED as isize
}
pub fn set_checked(control: &Control, checked: bool) {
let state = if checked { BST_CHECKED } else { BST_UNCHECKED };
unsafe {
SendMessageW(control.hwnd(), BM_SETCHECK, WPARAM(state), LPARAM(0));
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EditStyle {
pub multiline: bool,
pub password: bool,
pub readonly: bool,
pub number: bool,
pub align: TextAlign,
pub auto_hscroll: bool,
pub auto_vscroll: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
pub struct Edit;
impl Edit {
pub fn new(
parent: HWND,
text: &str,
x: i32,
y: i32,
width: i32,
height: i32,
id: u16,
style: EditStyle,
) -> Result<Control> {
init_common_controls()?;
let text_wide = WideString::new(text);
let class_wide = WideString::new("EDIT");
let mut win_style = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER;
if style.multiline {
win_style |= WINDOW_STYLE(ES_MULTILINE as u32);
}
if style.password {
win_style |= WINDOW_STYLE(ES_PASSWORD as u32);
}
if style.readonly {
win_style |= WINDOW_STYLE(ES_READONLY as u32);
}
if style.number {
win_style |= WINDOW_STYLE(ES_NUMBER as u32);
}
if style.auto_hscroll {
win_style |= WINDOW_STYLE(ES_AUTOHSCROLL as u32);
}
if style.auto_vscroll {
win_style |= WINDOW_STYLE(ES_AUTOVSCROLL as u32);
}
win_style |= WINDOW_STYLE(match style.align {
TextAlign::Left => ES_LEFT as u32,
TextAlign::Center => ES_CENTER as u32,
TextAlign::Right => ES_RIGHT as u32,
});
let hwnd = unsafe {
CreateWindowExW(
WS_EX_CLIENTEDGE,
class_wide.as_pcwstr(),
text_wide.as_pcwstr(),
win_style,
x,
y,
width,
height,
parent,
HMENU(id as isize as *mut _),
HINSTANCE::default(),
None,
)?
};
Ok(unsafe { Control::from_raw(hwnd, true) })
}
pub fn set_limit(control: &Control, max_chars: usize) {
unsafe {
SendMessageW(control.hwnd(), EM_LIMITTEXT, WPARAM(max_chars), LPARAM(0));
}
}
pub fn set_readonly(control: &Control, readonly: bool) {
unsafe {
SendMessageW(
control.hwnd(),
EM_SETREADONLY,
WPARAM(readonly as usize),
LPARAM(0),
);
}
}
pub fn select_all(control: &Control) {
unsafe {
SendMessageW(control.hwnd(), EM_SETSEL, WPARAM(0), LPARAM(-1));
}
}
pub fn selection(control: &Control) -> (u32, u32) {
let mut start = 0u32;
let mut end = 0u32;
unsafe {
SendMessageW(
control.hwnd(),
EM_GETSEL,
WPARAM(&mut start as *mut _ as usize),
LPARAM(&mut end as *mut _ as isize),
);
}
(start, end)
}
pub fn replace_selection(control: &Control, text: &str) {
let wide = WideString::new(text);
unsafe {
SendMessageW(
control.hwnd(),
EM_REPLACESEL,
WPARAM(1), LPARAM(wide.as_ptr() as isize),
);
}
}
}
pub struct Label;
impl Label {
pub fn new(
parent: HWND,
text: &str,
x: i32,
y: i32,
width: i32,
height: i32,
id: u16,
) -> Result<Control> {
init_common_controls()?;
let text_wide = WideString::new(text);
let class_wide = WideString::new("STATIC");
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
class_wide.as_pcwstr(),
text_wide.as_pcwstr(),
WS_CHILD | WS_VISIBLE,
x,
y,
width,
height,
parent,
HMENU(id as isize as *mut _),
HINSTANCE::default(),
None,
)?
};
Ok(unsafe { Control::from_raw(hwnd, true) })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProgressStyle {
#[default]
Standard,
Smooth,
Marquee,
}
pub struct ProgressBar;
impl ProgressBar {
pub fn new(
parent: HWND,
x: i32,
y: i32,
width: i32,
height: i32,
id: u16,
style: ProgressStyle,
) -> Result<Control> {
init_common_controls()?;
let mut win_style = WS_CHILD | WS_VISIBLE;
match style {
ProgressStyle::Standard => {}
ProgressStyle::Smooth => win_style |= WINDOW_STYLE(PBS_SMOOTH),
ProgressStyle::Marquee => win_style |= WINDOW_STYLE(PBS_MARQUEE),
}
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
PROGRESS_CLASSW,
None,
win_style,
x,
y,
width,
height,
parent,
HMENU(id as isize as *mut _),
HINSTANCE::default(),
None,
)?
};
Ok(unsafe { Control::from_raw(hwnd, true) })
}
pub fn set_range(control: &Control, min: i32, max: i32) {
unsafe {
SendMessageW(
control.hwnd(),
PBM_SETRANGE32,
WPARAM(min as usize),
LPARAM(max as isize),
);
}
}
pub fn set_pos(control: &Control, pos: i32) {
unsafe {
SendMessageW(control.hwnd(), PBM_SETPOS, WPARAM(pos as usize), LPARAM(0));
}
}
pub fn pos(control: &Control) -> i32 {
unsafe { SendMessageW(control.hwnd(), PBM_GETPOS, WPARAM(0), LPARAM(0)).0 as i32 }
}
pub fn step(control: &Control) {
unsafe {
SendMessageW(control.hwnd(), PBM_STEPIT, WPARAM(0), LPARAM(0));
}
}
pub fn set_step(control: &Control, step: i32) {
unsafe {
SendMessageW(
control.hwnd(),
PBM_SETSTEP,
WPARAM(step as usize),
LPARAM(0),
);
}
}
pub fn advance(control: &Control, delta: i32) {
unsafe {
SendMessageW(
control.hwnd(),
PBM_DELTAPOS,
WPARAM(delta as usize),
LPARAM(0),
);
}
}
pub fn set_marquee(control: &Control, enable: bool, interval_ms: u32) {
unsafe {
SendMessageW(
control.hwnd(),
PBM_SETMARQUEE,
WPARAM(enable as usize),
LPARAM(interval_ms as isize),
);
}
}
}
pub struct ListBox;
impl ListBox {
pub fn new(
parent: HWND,
x: i32,
y: i32,
width: i32,
height: i32,
id: u16,
multi_select: bool,
) -> Result<Control> {
init_common_controls()?;
let class_wide = WideString::new("LISTBOX");
let mut win_style = WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP;
win_style |= WINDOW_STYLE(0x00200000); win_style |= WINDOW_STYLE(0x00000001);
if multi_select {
win_style |= WINDOW_STYLE(0x00000008); }
let hwnd = unsafe {
CreateWindowExW(
WS_EX_CLIENTEDGE,
class_wide.as_pcwstr(),
None,
win_style,
x,
y,
width,
height,
parent,
HMENU(id as isize as *mut _),
HINSTANCE::default(),
None,
)?
};
Ok(unsafe { Control::from_raw(hwnd, true) })
}
pub fn add_string(control: &Control, text: &str) -> i32 {
let wide = WideString::new(text);
unsafe {
SendMessageW(
control.hwnd(),
LB_ADDSTRING,
WPARAM(0),
LPARAM(wide.as_ptr() as isize),
)
.0 as i32
}
}
pub fn insert_string(control: &Control, index: i32, text: &str) -> i32 {
let wide = WideString::new(text);
unsafe {
SendMessageW(
control.hwnd(),
LB_INSERTSTRING,
WPARAM(index as usize),
LPARAM(wide.as_ptr() as isize),
)
.0 as i32
}
}
pub fn delete_string(control: &Control, index: i32) {
unsafe {
SendMessageW(
control.hwnd(),
LB_DELETESTRING,
WPARAM(index as usize),
LPARAM(0),
);
}
}
pub fn count(control: &Control) -> i32 {
unsafe { SendMessageW(control.hwnd(), LB_GETCOUNT, WPARAM(0), LPARAM(0)).0 as i32 }
}
pub fn selected_index(control: &Control) -> i32 {
unsafe { SendMessageW(control.hwnd(), LB_GETCURSEL, WPARAM(0), LPARAM(0)).0 as i32 }
}
pub fn set_selected_index(control: &Control, index: i32) {
unsafe {
SendMessageW(
control.hwnd(),
LB_SETCURSEL,
WPARAM(index as usize),
LPARAM(0),
);
}
}
pub fn clear(control: &Control) {
unsafe {
SendMessageW(control.hwnd(), LB_RESETCONTENT, WPARAM(0), LPARAM(0));
}
}
}
pub struct ComboBox;
impl ComboBox {
pub fn new(
parent: HWND,
x: i32,
y: i32,
width: i32,
height: i32,
id: u16,
dropdown: bool,
) -> Result<Control> {
init_common_controls()?;
let class_wide = WideString::new("COMBOBOX");
let mut win_style = WS_CHILD | WS_VISIBLE | WS_TABSTOP;
win_style |= WINDOW_STYLE(0x00000200);
if dropdown {
win_style |= WINDOW_STYLE(0x0003); } else {
win_style |= WINDOW_STYLE(0x0002); }
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
class_wide.as_pcwstr(),
None,
win_style,
x,
y,
width,
height,
parent,
HMENU(id as isize as *mut _),
HINSTANCE::default(),
None,
)?
};
Ok(unsafe { Control::from_raw(hwnd, true) })
}
pub fn add_string(control: &Control, text: &str) -> i32 {
let wide = WideString::new(text);
unsafe {
SendMessageW(
control.hwnd(),
CB_ADDSTRING,
WPARAM(0),
LPARAM(wide.as_ptr() as isize),
)
.0 as i32
}
}
pub fn count(control: &Control) -> i32 {
unsafe { SendMessageW(control.hwnd(), CB_GETCOUNT, WPARAM(0), LPARAM(0)).0 as i32 }
}
pub fn selected_index(control: &Control) -> i32 {
unsafe { SendMessageW(control.hwnd(), CB_GETCURSEL, WPARAM(0), LPARAM(0)).0 as i32 }
}
pub fn set_selected_index(control: &Control, index: i32) {
unsafe {
SendMessageW(
control.hwnd(),
CB_SETCURSEL,
WPARAM(index as usize),
LPARAM(0),
);
}
}
pub fn clear(control: &Control) {
unsafe {
SendMessageW(control.hwnd(), CB_RESETCONTENT, WPARAM(0), LPARAM(0));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_common_controls() {
let result = init_common_controls();
if result.is_err() {
eprintln!(
"init_common_controls failed (expected in headless CI): {:?}",
result
);
}
}
#[test]
fn test_button_style() {
assert_eq!(ButtonStyle::Push.to_style(), BS_PUSHBUTTON as u32);
assert_eq!(ButtonStyle::Checkbox.to_style(), BS_CHECKBOX as u32);
}
#[test]
fn test_edit_style_default() {
let style = EditStyle::default();
assert!(!style.multiline);
assert!(!style.password);
assert_eq!(style.align, TextAlign::Left);
}
}