use std::collections::BTreeMap;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct WndClass {
pub name: String,
pub wndproc_sel: u16,
pub wndproc_off: u16,
pub style: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct Window {
pub hwnd: u16,
pub class: String,
pub title: String,
pub parent: u16,
pub id: u16,
pub style: u32,
pub wndproc_sel: u16,
pub wndproc_off: u16,
pub visible: bool,
pub children: Vec<u16>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DialogControl {
pub id: u16,
pub class: String,
pub text: String,
pub style: u32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum GuiEvent {
RegisterClass {
name: String,
},
CreateWindow {
hwnd: u16,
class: String,
title: String,
parent: u16,
id: u16,
},
ShowWindow {
hwnd: u16,
cmd: u16,
},
MessageBox {
caption: String,
text: String,
flags: u16,
result: u16,
},
DialogStart {
title: String,
controls: Vec<DialogControl>,
},
DialogEnd {
result: i16,
},
SetWindowText {
hwnd: u16,
text: String,
},
}
#[derive(Debug, Default, Clone, Serialize)]
pub struct GuiState {
pub classes: BTreeMap<String, WndClass>,
pub windows: BTreeMap<u16, Window>,
#[serde(skip)]
pub next_hwnd: u16,
#[serde(skip)]
pub next_obj_handle: u16,
pub events: Vec<GuiEvent>,
}
const HWND_BASE: u16 = 0x1000;
const OBJ_HANDLE_BASE: u16 = 0x8000;
impl GuiState {
pub fn alloc_obj_handle(&mut self) -> u16 {
if self.next_obj_handle < OBJ_HANDLE_BASE {
self.next_obj_handle = OBJ_HANDLE_BASE;
}
let h = self.next_obj_handle;
self.next_obj_handle = self.next_obj_handle.wrapping_add(4);
h
}
pub fn alloc_hwnd(&mut self) -> u16 {
if self.next_hwnd < HWND_BASE {
self.next_hwnd = HWND_BASE;
}
let h = self.next_hwnd;
self.next_hwnd = self.next_hwnd.wrapping_add(4);
h
}
pub fn register_class(&mut self, name: &str, wndproc_sel: u16, wndproc_off: u16, style: u32) {
self.classes.insert(
name.to_string(),
WndClass {
name: name.to_string(),
wndproc_sel,
wndproc_off,
style,
},
);
self.events.push(GuiEvent::RegisterClass {
name: name.to_string(),
});
}
#[allow(clippy::too_many_arguments)]
pub fn create_window(
&mut self,
class: &str,
title: &str,
parent: u16,
id: u16,
style: u32,
) -> u16 {
let (sel, off) = self
.classes
.get(class)
.map_or((0, 0), |c| (c.wndproc_sel, c.wndproc_off));
let hwnd = self.alloc_hwnd();
self.windows.insert(
hwnd,
Window {
hwnd,
class: class.to_string(),
title: title.to_string(),
parent,
id,
style,
wndproc_sel: sel,
wndproc_off: off,
visible: false,
children: Vec::new(),
},
);
if parent != 0 {
if let Some(p) = self.windows.get_mut(&parent) {
p.children.push(hwnd);
}
}
self.events.push(GuiEvent::CreateWindow {
hwnd,
class: class.to_string(),
title: title.to_string(),
parent,
id,
});
hwnd
}
pub fn wndproc_of(&self, hwnd: u16) -> Option<(u16, u16)> {
self.windows.get(&hwnd).and_then(|w| {
if w.wndproc_off != 0 || w.wndproc_sel != 0 {
Some((w.wndproc_sel, w.wndproc_off))
} else {
self.classes
.get(&w.class)
.map(|c| (c.wndproc_sel, c.wndproc_off))
}
})
}
}
#[must_use]
pub fn parse_dialog_template(data: &[u8]) -> (String, Vec<DialogControl>) {
let rd16 = |d: &[u8], o: usize| -> u16 {
d.get(o..o + 2)
.map_or(0, |b| u16::from_le_bytes([b[0], b[1]]))
};
let style = u32::from(rd16(data, 0)) | (u32::from(rd16(data, 2)) << 16);
let cdit = *data.get(4).unwrap_or(&0);
let mut p = 13usize;
p = skip_sz_or_ord(data, p);
p = skip_sz_or_ord(data, p);
let (title, np) = read_sz(data, p);
p = np;
if style & 0x40 != 0 {
p += 2;
p = read_sz(data, p).1;
}
let mut controls = Vec::new();
for _ in 0..cdit {
if p + 14 > data.len() {
break;
}
let id = rd16(data, p + 8);
let cstyle = u32::from(rd16(data, p + 10)) | (u32::from(rd16(data, p + 12)) << 16);
p += 14;
let (class, np) = read_class(data, p);
p = np;
let (text, np) = read_text(data, p);
p = np;
let extra = *data.get(p).unwrap_or(&0) as usize;
p += 1 + extra;
controls.push(DialogControl {
id,
class,
text,
style: cstyle,
});
}
(title, controls)
}
fn read_sz(data: &[u8], p: usize) -> (String, usize) {
let mut end = p;
while end < data.len() && data[end] != 0 {
end += 1;
}
let s = String::from_utf8_lossy(data.get(p..end).unwrap_or(&[])).into_owned();
(s, (end + 1).min(data.len().max(p)))
}
fn skip_sz_or_ord(data: &[u8], p: usize) -> usize {
match data.get(p) {
Some(0) => p + 1,
Some(0xFF) => p + 3,
Some(_) => read_sz(data, p).1,
None => p,
}
}
fn read_class(data: &[u8], p: usize) -> (String, usize) {
match data.get(p) {
Some(&b) if b & 0x80 != 0 => {
let name = match b {
0x80 => "Button",
0x81 => "Edit",
0x82 => "Static",
0x83 => "ListBox",
0x84 => "ScrollBar",
0x85 => "ComboBox",
_ => "?",
};
(name.to_string(), p + 1)
}
Some(_) => read_sz(data, p),
None => (String::new(), p),
}
}
fn read_text(data: &[u8], p: usize) -> (String, usize) {
match data.get(p) {
Some(0xFF) => {
let ord = data
.get(p + 1..p + 3)
.map_or(0, |b| u16::from_le_bytes([b[0], b[1]]));
(format!("#{ord}"), p + 3)
}
Some(_) => read_sz(data, p),
None => (String::new(), p),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_window_links_to_parent_and_logs() {
let mut g = GuiState::default();
g.register_class("AfxFrameOrView", 1, 0x100, 0);
let frame = g.create_window("AfxFrameOrView", "Setup", 0, 0, 0);
let btn = g.create_window("BUTTON", "Install", frame, 1, 0);
assert_eq!(g.windows[&frame].children, vec![btn]);
assert_eq!(g.wndproc_of(frame), Some((1, 0x100)));
assert_eq!(g.events.len(), 3);
}
}