#![allow(unsafe_op_in_unsafe_fn)]
use crate::ui::controls::apply_button_theme;
use crate::ui::builder::ControlBuilder;
use crate::ui::framework::{get_window_state, WindowHandler, WindowBuilder, WindowAlignment};
use crate::utils::to_wstring;
use crate::logger::LogEntry;
use crate::ui::state::AppState;
use crate::w;
use crate::types::*;
use std::collections::VecDeque;
const CONSOLE_TITLE: &str = "Debug Console";
const IDC_EDIT_CONSOLE: i32 = 1001;
const IDC_BTN_COPY: i32 = 1002;
const IDC_BTN_CLEAR: i32 = 1003;
const BUTTON_HEIGHT: i32 = 30;
const TIMER_ID: usize = 1;
const UPDATE_INTERVAL_MS: u32 = 150;
const MAX_HISTORY: usize = 1000;
const EDIT_LIMIT: usize = 1024 * 1024;
struct ConsoleState {
parent_state: *mut AppState, edit_hwnd: Option<HWND>,
btn_copy_hwnd: Option<HWND>,
btn_clear_hwnd: Option<HWND>,
is_dark: bool,
history: VecDeque<LogEntry>,
pending: Vec<LogEntry>,
}
pub unsafe fn show_console_window(app_state: &mut AppState, parent: HWND, is_dark: bool) {
if let Some(hwnd) = app_state.console_hwnd {
if let Some(state) = get_window_state::<ConsoleState>(hwnd) {
state.is_dark = is_dark;
state.update_theme(hwnd);
}
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
BringWindowToTop(hwnd);
return;
}
let bg_brush = (COLOR_WINDOW + 1) as HBRUSH;
let mut history = VecDeque::with_capacity(MAX_HISTORY);
history.extend(app_state.logs.iter().cloned());
let state = Box::new(ConsoleState {
parent_state: app_state as *mut _,
edit_hwnd: None,
btn_copy_hwnd: None,
btn_clear_hwnd: None,
is_dark,
history,
pending: Vec::new(), });
let state_ref = Box::leak(state);
let hwnd_res = WindowBuilder::new(state_ref, "CompactRS_Console", CONSOLE_TITLE)
.style(WS_OVERLAPPEDWINDOW | WS_VISIBLE)
.size(600, 400)
.align(WindowAlignment::Manual(CW_USEDEFAULT, CW_USEDEFAULT))
.background(bg_brush)
.build(parent);
if let Ok(hwnd) = hwnd_res {
if hwnd != std::ptr::null_mut() {
app_state.console_hwnd = Some(hwnd);
}
}
}
impl ConsoleState {
unsafe fn update_theme(&self, hwnd: HWND) {
crate::ui::theme::set_window_frame_theme(hwnd, self.is_dark);
if let Some(edit) = self.edit_hwnd {
if self.is_dark {
let dark_mode = w!("DarkMode_Explorer");
SetWindowTheme(edit, dark_mode.as_ptr(), std::ptr::null_mut());
} else {
let explorer = w!("Explorer");
SetWindowTheme(edit, explorer.as_ptr(), std::ptr::null_mut());
}
InvalidateRect(edit, std::ptr::null_mut(), 1);
}
if let Some(btn) = self.btn_copy_hwnd {
apply_button_theme(btn, self.is_dark);
}
if let Some(btn) = self.btn_clear_hwnd {
apply_button_theme(btn, self.is_dark);
}
InvalidateRect(hwnd, std::ptr::null_mut(), 1);
}
unsafe fn refresh_text(&self) {
if let Some(edit) = self.edit_hwnd {
let total_len = self.history.len() * 100; let mut combined = crate::utils::PathBuffer::with_capacity(total_len);
for entry in &self.history {
let line = format_log_entry(entry);
combined.push_u16_slice(&line);
}
let mut buf = Vec::with_capacity(total_len);
for entry in &self.history {
let line = format_log_entry(entry);
let len = if line.last() == Some(&0) { line.len() - 1 } else { line.len() };
buf.extend_from_slice(&line[..len]);
}
buf.push(0);
SetWindowTextW(edit, buf.as_ptr());
let len = GetWindowTextLengthW(edit);
SendMessageW(edit, EM_SETSEL, len as WPARAM, len as LPARAM);
SendMessageW(edit, EM_REPLACESEL, 0, w!("").as_ptr() as LPARAM);
}
}
unsafe fn flush_pending(&mut self) {
if self.pending.is_empty() { return; }
if let Some(edit) = self.edit_hwnd {
let mut chunk_wide = Vec::with_capacity(self.pending.len() * 100);
for entry in &self.pending {
if self.history.len() >= MAX_HISTORY {
self.history.pop_front();
}
self.history.push_back(entry.clone());
let line = format_log_entry(entry);
let len = if line.last() == Some(&0) { line.len() - 1 } else { line.len() };
chunk_wide.extend_from_slice(&line[..len]);
}
chunk_wide.push(0);
self.pending.clear();
let len = GetWindowTextLengthW(edit);
SendMessageW(edit, EM_SETSEL, len as WPARAM, len as LPARAM);
SendMessageW(edit, EM_REPLACESEL, 0, chunk_wide.as_ptr() as LPARAM);
if self.history.len() >= MAX_HISTORY {
if len > (EDIT_LIMIT as i32) - 10000 {
self.refresh_text();
}
}
}
}
}
unsafe fn format_log_entry(entry: &LogEntry) -> Vec<u16> {
let ts_w = crate::utils::fmt_timestamp(entry.timestamp);
let level_str = match entry.level {
crate::logger::LogLevel::Error => w!(" [ERROR] "),
crate::logger::LogLevel::Warning => w!(" [WARN] "),
crate::logger::LogLevel::Info => w!(" [INFO] "),
crate::logger::LogLevel::Trace => w!(" [TRACE] "),
};
let msg_w = to_wstring(&entry.message);
let newline = w!("\r\n");
crate::utils::concat_wstrings(&[&ts_w, level_str, &msg_w, newline])
}
impl WindowHandler for ConsoleState {
fn is_dark_mode(&self) -> bool {
self.is_dark
}
fn is_modal(&self) -> bool {
false
}
fn on_create(&mut self, hwnd: HWND) -> LRESULT {
unsafe {
let instance = GetModuleHandleW(std::ptr::null_mut());
let edit_cls = to_wstring("EDIT");
let edit = CreateWindowExW(
0,
edit_cls.as_ptr(),
std::ptr::null_mut(),
WS_CHILD | WS_VISIBLE | WS_VSCROLL |
(ES_MULTILINE as u32) | (ES_READONLY as u32) | (ES_AUTOVSCROLL as u32),
0, 0, 0, 0,
hwnd,
IDC_EDIT_CONSOLE as isize as HMENU,
instance,
std::ptr::null_mut()
);
SendMessageW(edit, EM_SETLIMITTEXT, EDIT_LIMIT, 0);
self.edit_hwnd = Some(edit);
let btn_copy = ControlBuilder::new(hwnd, IDC_BTN_COPY as u16)
.text_w(w!("Copy")).pos(0, 0).size(80, BUTTON_HEIGHT).dark_mode(self.is_dark).build();
self.btn_copy_hwnd = Some(btn_copy);
let btn_clear = ControlBuilder::new(hwnd, IDC_BTN_CLEAR as u16)
.text_w(w!("Clear")).pos(90, 0).size(80, BUTTON_HEIGHT).dark_mode(self.is_dark).build();
self.btn_clear_hwnd = Some(btn_clear);
self.update_theme(hwnd);
self.refresh_text();
SetTimer(hwnd, TIMER_ID, UPDATE_INTERVAL_MS, None);
}
0
}
fn on_message(&mut self, hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> Option<LRESULT> {
unsafe {
match msg {
WM_TIMER => {
if wparam == TIMER_ID {
self.flush_pending();
}
Some(0)
},
WM_SIZE => {
let width = (lparam & 0xFFFF) as i32;
let height = ((lparam >> 16) & 0xFFFF) as i32;
if let Some(edit) = self.edit_hwnd {
SetWindowPos(edit, std::ptr::null_mut(), 0, 0, width, height - BUTTON_HEIGHT - 5, SWP_NOZORDER);
}
let btn_y = height - BUTTON_HEIGHT;
if let Some(btn) = self.btn_copy_hwnd {
SetWindowPos(btn, std::ptr::null_mut(), 5, btn_y, 80, BUTTON_HEIGHT - 5, SWP_NOZORDER);
}
if let Some(btn) = self.btn_clear_hwnd {
SetWindowPos(btn, std::ptr::null_mut(), 90, btn_y, 80, BUTTON_HEIGHT - 5, SWP_NOZORDER);
}
Some(0)
},
WM_COMMAND => {
let id = (wparam & 0xFFFF) as i32;
match id {
IDC_BTN_COPY => {
if let Some(edit) = self.edit_hwnd {
let len = GetWindowTextLengthW(edit);
if len > 0 {
let mut buffer: Vec<u16> = vec![0; (len + 1) as usize];
GetWindowTextW(edit, buffer.as_mut_ptr(), len + 1);
if OpenClipboard(hwnd) != 0 {
let _ = EmptyClipboard();
let size = (buffer.len() * 2) as usize;
let hmem = GlobalAlloc(GMEM_MOVEABLE, size);
if hmem != std::ptr::null_mut() {
let ptr = GlobalLock(hmem);
if !ptr.is_null() {
std::ptr::copy_nonoverlapping(buffer.as_ptr(), ptr as *mut u16, buffer.len());
GlobalUnlock(hmem);
SetClipboardData(13, hmem);
}
}
CloseClipboard();
}
}
}
},
IDC_BTN_CLEAR => {
self.history.clear();
self.pending.clear();
let app_state = &mut *self.parent_state;
app_state.logs.clear();
if let Some(edit) = self.edit_hwnd {
let empty = w!("");
SetWindowTextW(edit, empty.as_ptr());
}
},
_ => {}
}
Some(0)
},
WM_DESTROY => {
KillTimer(hwnd, TIMER_ID);
let app_state = &mut *self.parent_state;
app_state.console_hwnd = None;
Some(0)
},
WM_CTLCOLOREDIT => None,
0x8002 => {
let new_is_dark = wparam == 1;
self.is_dark = new_is_dark;
self.update_theme(hwnd);
Some(0)
},
_ => None,
}
}
}
}
pub unsafe fn append_log_entry(console_hwnd: Option<HWND>, entry: LogEntry) {
if let Some(hwnd) = console_hwnd {
if let Some(state) = get_window_state::<ConsoleState>(hwnd) {
state.pending.push(entry);
}
}
}
pub unsafe fn close_console(hwnd: HWND) {
DestroyWindow(hwnd);
}