use crate::types::*;
use super::base::Component;
use crate::engine::wof::{WofAlgorithm, CompressionState};
use crate::ui::state::BatchItem;
use crate::utils::to_wstring;
use crate::w;
const DEFAULT_PATH_WIDTH: i32 = 250;
const COLUMN_DEFS: &[(&str, i32)] = &[
("Path", DEFAULT_PATH_WIDTH),
("Current", 70),
("Algorithm", 70),
("Action", 70),
("Size", 75),
("Est. Size", 75),
("On Disk", 75),
("Ratio", 60),
("Progress", 70),
("Status", 80),
("▶ Start", 110),
];
pub mod columns {
pub const PATH: i32 = 0;
pub const CURRENT: i32 = 1;
pub const ALGORITHM: i32 = 2;
pub const ACTION: i32 = 3;
pub const SIZE: i32 = 4;
pub const EST_SIZE: i32 = 5;
pub const ON_DISK: i32 = 6;
pub const RATIO: i32 = 7;
pub const PROGRESS: i32 = 8;
pub const STATUS: i32 = 9;
pub const START: i32 = 10;
}
pub struct FileListView {
hwnd: HWND,
}
impl FileListView {
pub unsafe fn new(parent: HWND, x: i32, y: i32, w: i32, h: i32, id: u16) -> Self { unsafe {
let instance = GetModuleHandleW(std::ptr::null_mut());
let class_name = w!("SysListView32");
let empty_str = w!("");
let hwnd = CreateWindowExW(
0,
class_name.as_ptr(),
empty_str.as_ptr(),
WS_VISIBLE | WS_CHILD | WS_BORDER | LVS_REPORT | LVS_SHOWSELALWAYS,
x,
y,
w,
h,
parent,
id as usize as HMENU,
instance,
std::ptr::null_mut(),
);
if hwnd == std::ptr::null_mut() {
}
SendMessageW(
hwnd,
LVM_SETEXTENDEDLISTVIEWSTYLE,
0,
(LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER) as isize,
);
let file_list = Self { hwnd };
file_list.setup_columns();
file_list
}}
#[inline]
pub fn hwnd(&self) -> HWND {
self.hwnd
}
fn setup_columns(&self) {
for (i, (name, width)) in COLUMN_DEFS.iter().enumerate() {
let name_wide = to_wstring(name); let mut fmt = LVCFMT_LEFT;
if i == columns::START as usize {
fmt = LVCFMT_LEFT;
}
let col = LVCOLUMNW {
mask: LVCF_WIDTH | LVCF_TEXT | LVCF_FMT,
fmt,
cx: *width,
pszText: name_wide.as_ptr() as *mut _,
..Default::default()
};
unsafe {
SendMessageW(
self.hwnd,
LVM_INSERTCOLUMNW,
i as usize,
&col as *const _ as isize,
);
}
}
}
pub fn add_item(
&self,
id: u32,
item: &BatchItem,
size_logical: &[u16],
size_disk: &[u16],
size_estimated: &[u16],
state: CompressionState,
) -> i32 {
let path_wide = to_wstring(&item.path);
let algo_wide = match item.algorithm {
WofAlgorithm::Xpress4K => w!("XPRESS4K"),
WofAlgorithm::Xpress8K => w!("XPRESS8K"),
WofAlgorithm::Xpress16K => w!("XPRESS16K"),
WofAlgorithm::Lzx => w!("LZX"),
};
let action_wide = if item.action == crate::ui::state::BatchAction::Compress {
w!("Compress")
} else {
w!("Decompress")
};
let size_wide = size_logical;
let disk_wide = size_disk;
let est_wide = size_estimated;
let current_wide: &[u16] = match state {
CompressionState::None => w!("-"),
CompressionState::Specific(algo) => match algo {
WofAlgorithm::Xpress4K => w!("XPRESS4K"),
WofAlgorithm::Xpress8K => w!("XPRESS8K"),
WofAlgorithm::Xpress16K => w!("XPRESS16K"),
WofAlgorithm::Lzx => w!("LZX"),
},
CompressionState::Mixed => w!("Mixed"),
};
let status_wide = w!("Pending");
let start_wide_vec = crate::utils::to_wstring("\u{1F441} ▶");
let start_wide = &start_wide_vec;
unsafe {
let mut lvi = LVITEMW {
mask: LVIF_TEXT | LVIF_PARAM,
iItem: std::i32::MAX, iSubItem: 0,
pszText: path_wide.as_ptr() as *mut _,
lParam: id as isize,
..Default::default()
};
let idx = SendMessageW(
self.hwnd,
LVM_INSERTITEMW,
0,
&lvi as *const _ as isize,
);
let row = idx as i32;
lvi.mask = LVIF_TEXT;
lvi.iItem = row;
lvi.iSubItem = columns::CURRENT;
lvi.pszText = current_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::ALGORITHM;
lvi.pszText = algo_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::ACTION;
lvi.pszText = action_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::SIZE;
lvi.pszText = size_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::EST_SIZE;
lvi.pszText = est_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::ON_DISK;
lvi.pszText = disk_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
let ratio_wide = w!("-");
lvi.iSubItem = columns::RATIO;
lvi.pszText = ratio_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::PROGRESS;
lvi.iSubItem = columns::STATUS; lvi.pszText = status_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
lvi.iSubItem = columns::START;
lvi.pszText = start_wide.as_ptr() as *mut _;
SendMessageW(self.hwnd, LVM_SETITEMW, 0, &lvi as *const _ as isize);
row
}
}
pub fn find_item_by_id(&self, id: u32) -> Option<i32> {
let mut find_info = LVFINDINFOW {
flags: LVFI_PARAM,
psz: std::ptr::null_mut(),
lParam: id as isize,
pt: POINT { x: 0, y: 0 },
vkDirection: 0,
};
unsafe {
let index = SendMessageW(
self.hwnd,
LVM_FINDITEMW,
-1isize as usize, &mut find_info as *mut _ as isize,
);
if index >= 0 {
Some(index as i32)
} else {
None
}
}
}
pub fn update_item_text(&self, row: i32, col: i32, text: &[u16]) {
let text_wide = if text.last() == Some(&0) {
std::borrow::Cow::Borrowed(text)
} else {
let mut t = text.to_vec();
t.push(0);
std::borrow::Cow::Owned(t)
};
let item = LVITEMW {
mask: LVIF_TEXT,
iItem: row,
iSubItem: col,
pszText: text_wide.as_ptr() as *mut _,
..Default::default()
};
unsafe {
SendMessageW(
self.hwnd,
LVM_SETITEMW,
0,
&item as *const _ as isize,
);
}
}
pub fn get_selected_indices(&self) -> Vec<usize> {
let mut selected = Vec::new();
let mut item_idx: i32 = -1;
unsafe {
loop {
let start_param = item_idx as usize;
let next = SendMessageW(
self.hwnd,
LVM_GETNEXTITEM,
if item_idx < 0 { usize::MAX } else { start_param },
LVNI_SELECTED as isize,
);
if (next as i32) < 0 {
break;
}
item_idx = next as i32;
selected.push(item_idx as usize);
}
}
selected
}
pub fn remove_item(&self, index: i32) {
unsafe {
SendMessageW(self.hwnd, LVM_DELETEITEM, index as usize, 0);
}
}
pub fn set_theme(&self, is_dark: bool) {
unsafe {
crate::ui::theme::allow_dark_mode_for_window(self.hwnd, is_dark);
if is_dark {
crate::ui::theme::apply_theme(self.hwnd, crate::ui::theme::ControlType::ItemsView, true);
} else {
crate::ui::theme::apply_theme(self.hwnd, crate::ui::theme::ControlType::List, false);
}
let (bg_color, text_color) = if is_dark {
(crate::ui::theme::COLOR_LIST_BG_DARK, crate::ui::theme::COLOR_LIST_TEXT_DARK)
} else {
(crate::ui::theme::COLOR_LIST_BG_LIGHT, crate::ui::theme::COLOR_LIST_TEXT_LIGHT)
};
SendMessageW(self.hwnd, LVM_SETBKCOLOR, 0, bg_color as isize);
SendMessageW(self.hwnd, LVM_SETTEXTBKCOLOR, 0, bg_color as isize);
SendMessageW(self.hwnd, LVM_SETTEXTCOLOR, 0, text_color as isize);
let header = SendMessageW(self.hwnd, LVM_GETHEADER, 0, 0) as HWND;
if header != std::ptr::null_mut() {
crate::ui::theme::allow_dark_mode_for_window(header, is_dark);
crate::ui::theme::apply_theme(header, crate::ui::theme::ControlType::Header, is_dark);
}
let _ = InvalidateRect(self.hwnd, std::ptr::null_mut(), 1);
}
}
pub fn apply_subclass(&self, main_hwnd: HWND) {
unsafe {
let _ = SetWindowSubclass(
self.hwnd,
Some(listview_subclass_proc),
1, main_hwnd as usize,
);
}
}
pub fn get_selection_count(&self) -> usize {
self.get_selected_indices().len()
}
pub fn get_item_count(&self) -> i32 {
unsafe { SendMessageW(self.hwnd, LVM_GETITEMCOUNT, 0, 0) as i32 }
}
pub fn deselect_all(&self) {
let selected = self.get_selected_indices();
for idx in selected {
self.set_selected(idx as i32, false);
}
}
pub fn set_selected(&self, index: i32, selected: bool) {
let state = if selected { LVIS_SELECTED } else { 0 };
let mask = LVIS_SELECTED;
let mut item = LVITEMW {
state,
stateMask: mask,
..Default::default()
};
unsafe {
SendMessageW(self.hwnd, LVM_SETITEMSTATE, index as usize, &mut item as *mut _ as isize);
}
}
pub fn sort_items(&self, callback: unsafe extern "system" fn(isize, isize, isize) -> i32, context: isize) {
unsafe {
SendMessageW(self.hwnd, LVM_SORTITEMS, context as usize, callback as isize);
}
}
pub fn set_sort_indicator(&self, column_index: i32, ascending: bool) {
const LVM_GETHEADER_MSG: u32 = 0x1000 + 31;
const HDM_GETITEMW: u32 = 0x1200 + 11;
const HDM_SETITEMW: u32 = 0x1200 + 12;
const HDI_FORMAT: u32 = 0x0004;
const HDF_SORTUP: i32 = 0x0400;
const HDF_SORTDOWN: i32 = 0x0200;
#[repr(C)]
struct HDITEMW {
mask: u32,
cxy: i32,
psz_text: *mut u16,
hbm: isize,
cch_text_max: i32,
fmt: i32,
l_param: isize,
i_image: i32,
i_order: i32,
type_: u32,
pv_filter: *mut std::ffi::c_void,
state: u32,
}
const COLUMN_COUNT: i32 = 11;
unsafe {
let header = SendMessageW(self.hwnd, LVM_GETHEADER_MSG, 0, 0) as HWND;
if header.is_null() {
return;
}
for i in 0..COLUMN_COUNT {
let mut hd_item: HDITEMW = std::mem::zeroed();
hd_item.mask = HDI_FORMAT;
let result = SendMessageW(
header,
HDM_GETITEMW,
i as usize,
&mut hd_item as *mut _ as isize,
);
if result == 0 {
continue; }
hd_item.fmt &= !(HDF_SORTUP | HDF_SORTDOWN);
if i == column_index {
hd_item.fmt |= if ascending { HDF_SORTUP } else { HDF_SORTDOWN };
}
SendMessageW(
header,
HDM_SETITEMW,
i as usize,
&hd_item as *const _ as isize,
);
}
}
}
pub fn update_playback_controls(&self, row: i32, state: crate::ui::state::ProcessingState, is_complete: bool) {
let text_wide = if is_complete {
crate::utils::to_wstring("\u{1F441} ▶")
} else {
match state {
crate::ui::state::ProcessingState::Idle | crate::ui::state::ProcessingState::Stopped => crate::utils::to_wstring("\u{1F441} ▶"),
crate::ui::state::ProcessingState::Running => crate::utils::to_wstring("\u{1F441} \u{23F8} \u{23F9}"),
crate::ui::state::ProcessingState::Paused => crate::utils::to_wstring("\u{1F441} \u{25B6} \u{23F9}"),
}
};
self.update_item_text(row, columns::START, &text_wide);
}
pub fn update_status_text(&self, row: i32, text: &str) {
self.update_item_text(row, columns::STATUS, &to_wstring(text));
}
pub fn update_algorithm(&self, row: i32, algo: WofAlgorithm) {
let name = match algo {
WofAlgorithm::Xpress4K => w!("XPRESS4K"),
WofAlgorithm::Xpress8K => w!("XPRESS8K"),
WofAlgorithm::Xpress16K => w!("XPRESS16K"),
WofAlgorithm::Lzx => w!("LZX"),
};
self.update_item_text(row, columns::ALGORITHM, name);
}
pub fn update_action(&self, row: i32, action: crate::ui::state::BatchAction) {
let name = match action {
crate::ui::state::BatchAction::Compress => w!("Compress"),
crate::ui::state::BatchAction::Decompress => w!("Decompress"),
};
self.update_item_text(row, columns::ACTION, name);
}
pub fn get_subitem_rect(&self, row: i32, col: i32) -> RECT {
let mut rect: RECT = unsafe { std::mem::zeroed() };
rect.top = col; rect.left = LVIR_BOUNDS as i32;
unsafe {
SendMessageW(self.hwnd, LVM_GETSUBITEMRECT, row as usize, &mut rect as *mut _ as isize);
}
rect
}
pub fn set_font(&self, hfont: crate::types::HFONT) {
unsafe {
SendMessageW(self.hwnd, WM_SETFONT, hfont as usize, 1);
}
}
pub fn clear_all(&self) {
unsafe {
SendMessageW(self.hwnd, LVM_DELETEALLITEMS, 0, 0);
}
}
}
impl Component for FileListView {
unsafe fn create(&mut self, _parent: HWND) -> Result<(), String> {
Ok(())
}
fn hwnd(&self) -> Option<HWND> {
Some(self.hwnd)
}
unsafe fn on_resize(&mut self, rect: &RECT) {
unsafe {
let width = rect.right - rect.left;
let height = rect.bottom - rect.top;
SetWindowPos(
self.hwnd,
std::ptr::null_mut(),
rect.left,
rect.top,
width,
height,
SWP_NOZORDER,
);
let mut fixed_width = 0;
for i in 1..COLUMN_DEFS.len() {
fixed_width += COLUMN_DEFS[i].1;
}
let mut client_rect: RECT = std::mem::zeroed();
GetClientRect(self.hwnd, &mut client_rect);
let list_inner_width = client_rect.right - client_rect.left;
let mut new_path_width = list_inner_width - fixed_width;
if new_path_width < 100 {
new_path_width = 100;
}
SendMessageW(
self.hwnd,
LVM_SETCOLUMNWIDTH,
0, new_path_width as isize,
);
}
}
unsafe fn on_theme_change(&mut self, is_dark: bool) {
self.set_theme(is_dark);
}
}
#[derive(Clone, Copy)]
enum RatioTier {
Ultra,
Good,
Moderate,
Negligible,
}
#[inline]
fn parse_ratio_from_utf16(buffer: &[u16]) -> Option<f32> {
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
if len == 0 {
return None;
}
let mut ascii_buf = [0u8; 32];
let copy_len = len.min(31);
for (i, &wc) in buffer[..copy_len].iter().enumerate() {
if wc > 127 {
return None;
}
ascii_buf[i] = wc as u8;
}
let s = std::str::from_utf8(&ascii_buf[..copy_len]).ok()?;
let s = s.trim();
if s == "-" || s.is_empty() || s == "N/A" {
return None;
}
let s = s.strip_suffix('%').unwrap_or(s);
s.trim().parse::<f32>().ok()
}
#[inline]
fn get_ratio_tier(ratio: f32) -> RatioTier {
if ratio > 50.0 {
RatioTier::Ultra
} else if ratio >= 20.0 {
RatioTier::Good
} else if ratio >= 5.0 {
RatioTier::Moderate
} else {
RatioTier::Negligible
}
}
#[inline]
fn tier_to_color(tier: RatioTier, is_dark: bool) -> COLORREF {
match (tier, is_dark) {
(RatioTier::Ultra, false) => 0x0000A000, (RatioTier::Ultra, true) => 0x0080FF00,
(RatioTier::Good, false) => 0x00C86400, (RatioTier::Good, true) => 0x00FFC850,
(RatioTier::Moderate, false) => 0x000064C8, (RatioTier::Moderate, true) => 0x0000D7FF,
(RatioTier::Negligible, false) => 0x00808080, (RatioTier::Negligible, true) => 0x00A0A0A0, }
}
#[inline]
unsafe fn get_listview_item_text(hwnd: HWND, row: i32, col: i32, buffer: &mut [u16]) -> usize {
let mut lvi = LVITEMW {
mask: LVIF_TEXT,
iItem: row,
iSubItem: col,
pszText: buffer.as_mut_ptr(),
cchTextMax: buffer.len() as i32,
..Default::default()
};
let result = unsafe { SendMessageW(hwnd, LVM_GETITEMTEXTW, row as usize, &mut lvi as *mut _ as isize) };
result as usize
}
pub unsafe fn handle_listview_customdraw(
_list_hwnd: HWND,
lparam: LPARAM,
is_dark: bool,
items: &[BatchItem]
) -> Option<LRESULT> {
unsafe {
let nmlvcd = &mut *(lparam as *mut NMLVCUSTOMDRAW);
let draw_stage = nmlvcd.nmcd.dwDrawStage;
if draw_stage == CDDS_PREPAINT {
return Some(CDRF_NOTIFYITEMDRAW as LRESULT);
}
if draw_stage == CDDS_ITEMPREPAINT {
return Some(CDRF_NOTIFYSUBITEMDRAW as LRESULT);
}
if (draw_stage & (CDDS_ITEMPREPAINT | CDDS_SUBITEM)) == (CDDS_ITEMPREPAINT | CDDS_SUBITEM) {
let sub_item = nmlvcd.i_sub_item;
if sub_item == columns::RATIO {
let id = nmlvcd.nmcd.lItemlParam as u32;
let item_opt = items.binary_search_by_key(&id, |i| i.id)
.ok()
.map(|idx| &items[idx])
.or_else(|| items.get(nmlvcd.nmcd.dwItemSpec as usize));
if let Some(item) = item_opt {
let ratio = crate::utils::calculate_saved_percentage(item.logical_size, item.disk_size) as f32;
let tier = get_ratio_tier(ratio);
let color = tier_to_color(tier, is_dark);
SetTextColor(nmlvcd.nmcd.hdc, color);
SetBkMode(nmlvcd.nmcd.hdc, TRANSPARENT as i32);
nmlvcd.clr_text = color;
return Some(CDRF_NEWFONT as LRESULT);
}
}
let default_color = if is_dark {
crate::ui::theme::COLOR_LIST_TEXT_DARK
} else {
crate::ui::theme::COLOR_LIST_TEXT_LIGHT
};
SetTextColor(nmlvcd.nmcd.hdc, default_color);
nmlvcd.clr_text = default_color;
return Some(CDRF_NEWFONT as LRESULT);
}
None
}
}
unsafe extern "system" fn listview_subclass_proc(
hwnd: HWND,
umsg: u32,
wparam: WPARAM,
lparam: LPARAM,
_uidsubclass: usize,
dwrefdata: usize,
) -> LRESULT { unsafe {
let main_hwnd = dwrefdata as HWND;
let is_dark = crate::ui::theme::is_app_dark_mode(main_hwnd);
if umsg == WM_NOTIFY {
let nmhdr = &*(lparam as *const NMHDR);
if nmhdr.code == HDN_BEGINTRACKW || nmhdr.code == HDN_DIVIDERDBLCLICKW {
return 1; }
if nmhdr.code == NM_CUSTOMDRAW {
let header_hwnd = SendMessageW(hwnd, 0x101F, 0, 0) as HWND; let is_from_header = nmhdr.hwndFrom == header_hwnd;
if is_from_header {
if is_dark {
let nmcd = &mut *(lparam as *mut NMCUSTOMDRAW);
if nmcd.dwDrawStage == CDDS_PREPAINT {
return CDRF_NOTIFYITEMDRAW as LRESULT;
}
if nmcd.dwDrawStage == CDDS_ITEMPREPAINT {
SetTextColor(nmcd.hdc, crate::ui::theme::COLOR_HEADER_TEXT_DARK);
SetBkMode(nmcd.hdc, TRANSPARENT as i32);
return CDRF_NEWFONT as LRESULT;
}
}
} else if nmhdr.hwndFrom == hwnd {
let nmlvcd = &mut *(lparam as *mut NMLVCUSTOMDRAW);
let draw_stage = nmlvcd.nmcd.dwDrawStage;
if draw_stage == CDDS_PREPAINT {
return CDRF_NOTIFYITEMDRAW as LRESULT;
}
if draw_stage == CDDS_ITEMPREPAINT {
return CDRF_NOTIFYSUBITEMDRAW as LRESULT;
}
if draw_stage == (CDDS_ITEMPREPAINT | CDDS_SUBITEM) {
let sub_item = nmlvcd.i_sub_item;
if sub_item == columns::RATIO {
let row = nmlvcd.nmcd.dwItemSpec as i32;
let mut text_buffer = [0u16; 16];
let _len = get_listview_item_text(hwnd, row, columns::RATIO, &mut text_buffer);
if let Some(ratio) = parse_ratio_from_utf16(&text_buffer) {
let tier = get_ratio_tier(ratio);
let color = tier_to_color(tier, is_dark);
SetTextColor(nmlvcd.nmcd.hdc, color);
SetBkMode(nmlvcd.nmcd.hdc, TRANSPARENT as i32);
return CDRF_NEWFONT as LRESULT;
}
}
return CDRF_NEWFONT as LRESULT;
}
}
}
}
DefSubclassProc(hwnd, umsg, wparam, lparam)
}}