Repository: tarkah/grout Branch: master Commit: 9f90d27be040 Files: 16 Total size: 63.2 KB Directory structure: gitextract_bmii00ec/ ├── .github/ │ └── workflows/ │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src/ ├── autostart.rs ├── common.rs ├── config.rs ├── event.rs ├── grid.rs ├── hotkey.rs ├── main.rs ├── tray.rs ├── window/ │ ├── grid.rs │ └── preview.rs └── window.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/rust.yml ================================================ name: Rust on: push: branches: [ master ] tags: - '*' pull_request: branches: [ master ] jobs: test: if: startsWith(github.ref, 'refs/tags/') != true runs-on: windows-latest steps: - uses: actions/checkout@v2 - name: Cache cargo registry uses: actions/cache@v1 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo index uses: actions/cache@v1 with: path: ~/.cargo/git key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo build uses: actions/cache@v1 with: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - name: Test run: cargo test lint: if: startsWith(github.ref, 'refs/tags/') != true runs-on: windows-latest steps: - uses: actions/checkout@v2 - name: Cache cargo registry uses: actions/cache@v1 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo index uses: actions/cache@v1 with: path: ~/.cargo/git key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo build uses: actions/cache@v1 with: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - name: Fmt run: cargo fmt --all -- --check - name: Clippy run: cargo clippy -- -D warnings release: if: startsWith(github.ref, 'refs/tags/') runs-on: windows-latest steps: - uses: actions/checkout@v2 - name: Build run: cargo build --release - name: Release uses: softprops/action-gh-release@v1 with: files: target/release/grout.exe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ /target .vscode/ ================================================ FILE: Cargo.toml ================================================ [package] name = "grout" version = "0.7.0" authors = ["tarkah "] edition = "2018" [dependencies] anyhow = "1.0" crossbeam-channel = "0.4" config = { version = "0.10", default-features=false, features = ['yaml'] } dirs = "2.0" lazy_static = "1.4" regex = "1.3" ron = "0.5" serde = { version = "1.0", features = ['derive'] } [dependencies.winapi] version = "0.3" features = ["winuser", "wingdi", "libloaderapi", "errhandlingapi", "shellapi", "winreg"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 tarkah Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # grout ![Rust](https://github.com/tarkah/grout/workflows/Rust/badge.svg) A simple tiling window manager for Windows, written in Rust. Inspired by Budgie's Window Shuffler grid functionality. - [Demo](#demo) - [Download](#download) - [Usage](#usage) - [Config](#config) ## Demo Click for full video [![Demo](https://i.imgur.com/bErviBc.gif)](https://i.imgur.com/ugPMvlA.mp4) ## Download - Download executable from [latest release](https://github.com/tarkah/grout/releases/latest) ## Usage - Run `grout.exe` or `cargo run`. Program will run in the background and options can be accessed by right clicking the system tray icon. - Activate the windowing grid with hotkey `CRTL + ALT + S`. - Increase / decrease grid rows / columns with `CTRL + arrows`. - Hovering cursor over the grid will show a preview of that zone in the window. - Select a window you want resized, then click on a tile in the grid. Window will resize to that zone. - Hold `SHIFT` down while hovering after a selection, zone will increase in size across all tiles. Select again to resize to larger zone. - Resizing can also be achieved by click-drag-release. Click & hold cursor down, drag cursor across multiple tiles and release to make selection. - F1 - F6 can be used to toggle between saved profiles. F1 is the default profile loaded when program is first started. ## Config See [example config](https://github.com/tarkah/grout/wiki/Example-Config) in the wiki for a full list of all options. - A configuration file will be created at `%APPDATA%\grout\config.yml` that can be customized. You can also open the config file from the system tray icon. ================================================ FILE: src/autostart.rs ================================================ use std::env; use std::fs; use std::mem; use std::ptr; use anyhow::format_err; use winapi::shared::minwindef::HKEY; use winapi::um::winnt::{KEY_SET_VALUE, REG_OPTION_NON_VOLATILE, REG_SZ}; use winapi::um::winreg::{RegCreateKeyExW, RegDeleteKeyValueW, RegSetValueExW, HKEY_CURRENT_USER}; use crate::{str_to_wide, Result}; pub unsafe fn toggle_autostart_registry_key(enabled: bool) -> Result<()> { let mut app_path = dirs::config_dir().ok_or_else(|| format_err!("Failed to get config directory"))?; app_path.push("grout"); app_path.push("grout.exe"); let current_path = env::current_exe()?; if current_path != app_path && enabled { fs::copy(current_path, &app_path)?; } let app_path = str_to_wide!(app_path.to_str().unwrap_or_default()); let mut key_name = str_to_wide!("Software\\Microsoft\\Windows\\CurrentVersion\\Run"); let mut value_name = str_to_wide!("grout"); let mut key: HKEY = mem::zeroed(); if enabled { if RegCreateKeyExW( HKEY_CURRENT_USER, key_name.as_mut_ptr(), 0, ptr::null_mut(), REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, ptr::null_mut(), &mut key, ptr::null_mut(), ) == 0 { RegSetValueExW( key, value_name.as_mut_ptr(), 0, REG_SZ, app_path.as_ptr() as _, app_path.len() as u32 * 2, ); } } else { RegDeleteKeyValueW( HKEY_CURRENT_USER, key_name.as_mut_ptr(), value_name.as_mut_ptr(), ); } Ok(()) } ================================================ FILE: src/common.rs ================================================ use std::fmt::{Display, Error, Formatter}; use std::mem; use std::process; use std::ptr; use winapi::shared::windef::{POINT, RECT}; use winapi::um::winuser::{ GetCursorPos, GetForegroundWindow, GetMonitorInfoW, MessageBoxW, MonitorFromPoint, MB_OK, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, }; use crate::str_to_wide; use crate::window::Window; /// x & y coordinates are relative to top left of screen #[derive(Debug, Clone, Copy, PartialEq)] pub struct Rect { pub x: i32, pub y: i32, pub width: i32, pub height: i32, } impl Rect { pub fn contains_point(self, point: (i32, i32)) -> bool { point.0 >= self.x && point.0 <= self.x + self.width && point.1 >= self.y && point.1 <= self.y + self.height } pub fn zero() -> Self { Rect { x: 0, y: 0, width: 0, height: 0, } } pub fn adjust_for_border(&mut self, border: (i32, i32)) { self.x -= border.0; self.width += border.0 * 2; self.height += border.1; } } impl Display for Rect { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { writeln!(f, "x: {}", self.x)?; writeln!(f, "y: {}", self.y)?; writeln!(f, "width: {}", self.width)?; writeln!(f, "height: {}", self.height)?; Ok(()) } } impl From for Rect { fn from(rect: RECT) -> Self { Rect { x: rect.left, y: rect.top, width: rect.right - rect.left, height: rect.bottom - rect.top, } } } impl From for RECT { fn from(rect: Rect) -> Self { RECT { left: rect.x, top: rect.y, right: rect.x + rect.width, bottom: rect.y + rect.height, } } } pub fn get_foreground_window() -> Window { let hwnd = unsafe { GetForegroundWindow() }; Window(hwnd) } pub unsafe fn get_work_area() -> Rect { let active_monitor = { let mut cursor_pos: POINT = mem::zeroed(); GetCursorPos(&mut cursor_pos); MonitorFromPoint(cursor_pos, MONITOR_DEFAULTTONEAREST) }; let work_area: Rect = { let mut info: MONITORINFOEXW = mem::zeroed(); info.cbSize = mem::size_of::() as u32; GetMonitorInfoW(active_monitor, &mut info as *mut MONITORINFOEXW as *mut _); info.rcWork.into() }; work_area } pub unsafe fn get_active_monitor_name() -> String { let active_monitor = { let mut cursor_pos: POINT = mem::zeroed(); GetCursorPos(&mut cursor_pos); MonitorFromPoint(cursor_pos, MONITOR_DEFAULTTONEAREST) }; let mut info: MONITORINFOEXW = mem::zeroed(); info.cbSize = mem::size_of::() as u32; GetMonitorInfoW(active_monitor, &mut info as *mut MONITORINFOEXW as *mut _); String::from_utf16_lossy(&info.szDevice) } pub fn report_and_exit(error_msg: &str) -> ! { show_msg_box(error_msg); process::exit(1) } pub fn show_msg_box(message: &str) { let mut message = str_to_wide!(message); unsafe { MessageBoxW( ptr::null_mut(), message.as_mut_ptr(), ptr::null_mut(), MB_OK, ); } } ================================================ FILE: src/config.rs ================================================ use std::fs::{create_dir_all, write, File}; use std::io::Read; use anyhow::format_err; use regex::{Captures, Regex}; use serde::{Deserialize, Serialize}; use crate::Result; static EXAMPLE_CONFIG: &str = "--- # Example config file for Grout # Margin between windows, in pixels margins: 10 # Padding between edge of monitor and windows, in pixels window_padding: 10 # Hotkey to activate grid. Valid modifiers are CTRL, ALT, SHIFT, WIN hotkey: CTRL+ALT+S # Hotkey to activate grid for a quick resize. Grid will automatically close after resize operation. #hotkey_quick_resize: CTRL+ALT+Q # Hotkey to maximize / restore the active window #hotkey_maximize_toggle: CTRL+ALT+X # Automatically launch program on startup auto_start: false "; pub fn load_config() -> Result { let mut config_path = dirs::config_dir().ok_or_else(|| format_err!("Failed to get config directory"))?; config_path.push("grout"); if !config_path.exists() { create_dir_all(&config_path)?; } config_path.push("config.yml"); if !config_path.exists() { write(&config_path, EXAMPLE_CONFIG)?; } let mut config = config::Config::default(); config.merge(config::Config::try_from(&Config::default())?)?; let file_config = config::File::from(config_path).format(config::FileFormat::Yaml); let config = config.merge(file_config)?; Ok(config.clone().try_into()?) } pub fn toggle_autostart() -> Result<()> { let mut config_path = dirs::config_dir().ok_or_else(|| format_err!("Failed to get config directory"))?; config_path.push("grout"); config_path.push("config.yml"); let mut config = File::open(&config_path)?; let mut config_str = String::new(); config.read_to_string(&mut config_str)?; let re_line = Regex::new(r"(?m)^(auto_start:)(.*)$")?; let updated_config = if let Some(cap) = re_line.captures_iter(&config_str).next() { if re_line.captures_len() == 3 { let re_cap = Regex::new(r"(?m)^(y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON)$")?; let enabled = re_cap.find(&cap[2].trim()); let updated_config = re_line.replace(&config_str, |caps: &Captures| { format!("{} {}", &caps[1], !enabled.is_some()) }); Some(updated_config.as_ref().to_owned()) } else { None } } else { None }; let updated_config = if let Some(updated_config) = updated_config { updated_config } else { format!("{}\n\nauto_start: true", config_str) }; write(&config_path, updated_config)?; Ok(()) } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Config { pub margins: u8, pub window_padding: u8, pub hotkey: String, pub hotkey_quick_resize: Option, pub hotkey_maximize_toggle: Option, pub auto_start: bool, } impl Default for Config { fn default() -> Self { Config { margins: 10, window_padding: 10, hotkey: "CTRL+ALT+S".to_string(), hotkey_quick_resize: None, hotkey_maximize_toggle: None, auto_start: false, } } } ================================================ FILE: src/event.rs ================================================ use std::mem; use std::ptr; use std::thread; use std::time::Duration; use crossbeam_channel::{select, Receiver}; use winapi::shared::{ minwindef::DWORD, windef::{HWINEVENTHOOK, HWND}, }; use winapi::um::winnt::LONG; use winapi::um::winuser::{ DispatchMessageW, PeekMessageW, SetWinEventHook, TranslateMessage, EVENT_SYSTEM_FOREGROUND, WINEVENT_OUTOFCONTEXT, }; use crate::common::get_active_monitor_name; use crate::window::Window; use crate::Message; use crate::CHANNEL; pub fn spawn_foreground_hook(close_msg: Receiver<()>) { thread::spawn(move || unsafe { SetWinEventHook( EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, ptr::null_mut(), Some(callback), 0, 0, WINEVENT_OUTOFCONTEXT, ); let mut msg = mem::zeroed(); loop { if PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 1) > 0 { TranslateMessage(&msg); DispatchMessageW(&msg); }; select! { recv(close_msg) -> _ => break, default(Duration::from_millis(10)) => {} } } }); } pub fn spawn_track_monitor_thread(close_msg: Receiver<()>) { thread::spawn(move || unsafe { let sender = &CHANNEL.0.clone(); let mut previous_monitor = get_active_monitor_name(); loop { let current_monitor = get_active_monitor_name(); if current_monitor != previous_monitor { previous_monitor = current_monitor.clone(); let _ = sender.send(Message::MonitorChange); } select! { recv(close_msg) -> _ => { break; } default(Duration::from_millis(10)) => {} } } }); } unsafe extern "system" fn callback( _hWinEventHook: HWINEVENTHOOK, _event: DWORD, hwnd: HWND, _idObject: LONG, _idChild: LONG, _idEventThread: DWORD, _dwmsEventTime: DWORD, ) { let sender = &CHANNEL.0.clone(); let _ = sender.send(Message::ActiveWindowChange(Window(hwnd))); } ================================================ FILE: src/grid.rs ================================================ use std::collections::HashMap; use std::fs; use std::mem; use serde::{Deserialize, Serialize}; use winapi::shared::windef::{HBRUSH, HDC}; use winapi::um::wingdi::{CreateSolidBrush, DeleteObject, RGB}; use winapi::um::winuser::{BeginPaint, EndPaint, FillRect, FrameRect, PAINTSTRUCT}; use crate::common::{get_active_monitor_name, get_work_area, Rect}; use crate::config::Config; use crate::window::Window; use crate::ACTIVE_PROFILE; const TILE_WIDTH: u32 = 48; const TILE_HEIGHT: u32 = 48; pub struct Grid { pub shift_down: bool, pub control_down: bool, pub cursor_down: bool, pub selected_tile: Option<(usize, usize)>, pub hovered_tile: Option<(usize, usize)>, pub active_window: Option, pub grid_window: Option, pub previous_resize: Option<(Window, Rect)>, pub quick_resize: bool, grid_margins: u8, zone_margins: u8, border_margins: u8, tiles: Vec>, // tiles[row][column] active_config: GridConfigKey, configs: GridConfigs, } #[derive(Serialize, Deserialize, Clone, Copy, Debug)] pub struct GridConfig { rows: usize, columns: usize, } impl Default for GridConfig { fn default() -> Self { GridConfig { rows: 2, columns: 2, } } } #[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Debug)] pub struct GridConfigKey { monitor: String, profile: String, } impl Default for GridConfigKey { fn default() -> Self { let monitor = unsafe { get_active_monitor_name() }; let profile = ACTIVE_PROFILE.lock().unwrap().clone(); GridConfigKey { monitor, profile } } } pub type GridConfigs = HashMap; pub trait GridCache { fn load() -> GridConfigs; fn save(&self); } impl GridCache for GridConfigs { fn load() -> GridConfigs { if let Some(mut config_path) = dirs::config_dir() { config_path.push("grout"); config_path.push("cache"); if !config_path.exists() { let _ = fs::create_dir_all(&config_path); } config_path.push("grid.ron"); if let Ok(file) = fs::File::open(config_path) { if let Ok(config) = ron::de::from_reader(file) { return config; } } } let mut config = HashMap::new(); config.insert(GridConfigKey::default(), GridConfig::default()); config } fn save(&self) { if let Some(mut config_path) = dirs::config_dir() { config_path.push("grout"); config_path.push("cache"); config_path.push("grid.ron"); if let Ok(serialized) = ron::ser::to_string(&self) { let _ = fs::write(config_path, serialized); } } } } impl From<&Config> for Grid { fn from(config: &Config) -> Self { Grid { zone_margins: config.margins, border_margins: config.window_padding, ..Default::default() } } } impl Default for Grid { fn default() -> Self { let configs = GridConfigs::load(); let active_config = GridConfigKey::default(); let default_config = configs.get(&active_config).cloned().unwrap_or_default(); let rows = default_config.rows; let columns = default_config.columns; Grid { shift_down: false, control_down: false, cursor_down: false, selected_tile: None, hovered_tile: None, active_window: None, grid_window: None, previous_resize: None, quick_resize: false, grid_margins: 3, zone_margins: 10, border_margins: 10, tiles: vec![vec![Tile::default(); columns]; rows], active_config, configs, } } } impl Grid { pub fn reset(&mut self) { self.shift_down = false; self.control_down = false; self.cursor_down = false; self.selected_tile = None; self.hovered_tile = None; self.grid_window = None; self.quick_resize = false; self.tiles.iter_mut().for_each(|row| { row.iter_mut().for_each(|tile| { tile.selected = false; tile.hovered = false; }) }); } fn save_config(&mut self) { let rows = self.rows(); let columns = self.columns(); if let Some(grid_config) = self.configs.get_mut(&self.active_config) { grid_config.rows = rows; grid_config.columns = columns; } else { self.configs .insert(self.active_config.clone(), GridConfig { rows, columns }); } self.configs.save(); } pub fn dimensions(&self) -> (u32, u32) { let width = self.columns() as u32 * TILE_WIDTH + (self.columns() as u32 + 1) * self.grid_margins as u32; let height = self.rows() as u32 * TILE_HEIGHT + (self.rows() as u32 + 1) * self.grid_margins as u32; (width, height) } fn zone_area(&self, row: usize, column: usize) -> Rect { let work_area = unsafe { get_work_area() }; let zone_width = (work_area.width - self.border_margins as i32 * 2 - (self.columns() - 1) as i32 * self.zone_margins as i32) / self.columns() as i32; let zone_height = (work_area.height - self.border_margins as i32 * 2 - (self.rows() - 1) as i32 * self.zone_margins as i32) / self.rows() as i32; let x = column as i32 * zone_width + self.border_margins as i32 + column as i32 * self.zone_margins as i32 + work_area.x; let y = row as i32 * zone_height + self.border_margins as i32 + row as i32 * self.zone_margins as i32 + work_area.y; Rect { x, y, width: zone_width, height: zone_height, } } fn rows(&self) -> usize { self.tiles.len() } fn columns(&self) -> usize { self.tiles[0].len() } pub fn add_row(&mut self) { self.tiles.push(vec![Tile::default(); self.columns()]); self.save_config(); } pub fn add_column(&mut self) { for row in self.tiles.iter_mut() { row.push(Tile::default()); } self.save_config(); } pub fn remove_row(&mut self) { if self.rows() > 1 { self.tiles.pop(); } self.save_config(); } pub fn remove_column(&mut self) { if self.columns() > 1 { for row in self.tiles.iter_mut() { row.pop(); } } self.save_config(); } fn tile_area(&self, row: usize, column: usize) -> Rect { let x = column as i32 * TILE_WIDTH as i32 + (column as i32 + 1) * self.grid_margins as i32; let y = row as i32 * TILE_HEIGHT as i32 + (row as i32 + 1) * self.grid_margins as i32; Rect { x, y, width: TILE_WIDTH as i32, height: TILE_HEIGHT as i32, } } pub fn reposition(&mut self) { let work_area = unsafe { get_work_area() }; let dimensions = self.dimensions(); let rect = Rect { x: work_area.width / 2 - dimensions.0 as i32 / 2 + work_area.x, y: work_area.height / 2 - dimensions.1 as i32 / 2 + work_area.y, width: dimensions.0 as i32, height: dimensions.1 as i32, }; self.grid_window.as_mut().unwrap().set_pos(rect, None); } /// Returns true if a change in highlighting occured pub unsafe fn highlight_tiles(&mut self, point: (i32, i32)) -> Option { let original_tiles = self.tiles.clone(); let mut hovered_rect = None; for row in 0..self.rows() { for column in 0..self.columns() { let tile_area = self.tile_area(row, column); if tile_area.contains_point(point) { self.tiles[row][column].hovered = true; self.hovered_tile = Some((row, column)); hovered_rect = Some(self.zone_area(row, column)); } else { self.tiles[row][column].hovered = false; } } } if let Some(rect) = self.shift_hover_and_calc_rect(true) { hovered_rect = Some(rect); } if original_tiles == self.tiles { None } else { hovered_rect } } unsafe fn shift_hover_and_calc_rect(&mut self, highlight: bool) -> Option { if self.shift_down || self.cursor_down { if let Some(selected_tile) = self.selected_tile { if let Some(hovered_tile) = self.hovered_tile { let selected_zone = self.zone_area(selected_tile.0, selected_tile.1); let hovered_zone = self.zone_area(hovered_tile.0, hovered_tile.1); let from_tile; let to_tile; let hovered_rect = if hovered_zone.x < selected_zone.x && hovered_zone.y > selected_zone.y { from_tile = (selected_tile.0, hovered_tile.1); to_tile = (hovered_tile.0, selected_tile.1); let from_zone = self.zone_area(from_tile.0, from_tile.1); let to_zone = self.zone_area(to_tile.0, to_tile.1); Rect { x: from_zone.x, y: from_zone.y, width: (to_zone.x + to_zone.width) - from_zone.x, height: (to_zone.y + to_zone.height) - from_zone.y, } } else if hovered_zone.y < selected_zone.y && hovered_zone.x > selected_zone.x { from_tile = (hovered_tile.0, selected_tile.1); to_tile = (selected_tile.0, hovered_tile.1); let from_zone = self.zone_area(from_tile.0, from_tile.1); let to_zone = self.zone_area(to_tile.0, to_tile.1); Rect { x: from_zone.x, y: from_zone.y, width: (to_zone.x + to_zone.width) - from_zone.x, height: (to_zone.y + to_zone.height) - from_zone.y, } } else if hovered_zone.x > selected_zone.x || hovered_zone.y > selected_zone.y { from_tile = selected_tile; to_tile = hovered_tile; Rect { x: selected_zone.x, y: selected_zone.y, width: (hovered_zone.x + hovered_zone.width) - selected_zone.x, height: (hovered_zone.y + hovered_zone.height) - selected_zone.y, } } else { from_tile = hovered_tile; to_tile = selected_tile; Rect { x: hovered_zone.x, y: hovered_zone.y, width: (selected_zone.x + selected_zone.width) - hovered_zone.x, height: (selected_zone.y + selected_zone.height) - hovered_zone.y, } }; if highlight { for row in from_tile.0..=to_tile.0 { for column in from_tile.1..=to_tile.1 { self.tiles[row][column].hovered = true; } } } return Some(hovered_rect); } } } None } pub unsafe fn select_tile(&mut self, point: (i32, i32)) -> bool { if self.cursor_down || self.shift_down { return false; } let previously_selected = self.selected_tile; for row in 0..self.rows() { for column in 0..self.columns() { let tile_area = self.tile_area(row, column); if tile_area.contains_point(point) { self.tiles[row][column].selected = true; self.selected_tile = Some((row, column)); } else { self.tiles[row][column].selected = false; } } } self.selected_tile != previously_selected } pub fn get_max_area(&self) -> Rect { let from_zone = self.zone_area(0, 0); let to_zone = self.zone_area(self.rows() - 1, self.columns() - 1); Rect { x: from_zone.x, y: from_zone.y, width: (to_zone.x + to_zone.width) - from_zone.x, height: (to_zone.y + to_zone.height) - from_zone.y, } } pub unsafe fn selected_area(&mut self) -> Option { if let Some(shift_rect) = self.shift_hover_and_calc_rect(false) { return Some(shift_rect); } if let Some(selected_tile) = self.selected_tile { Some(self.zone_area(selected_tile.0, selected_tile.1)) } else { None } } pub fn unhighlight_all_tiles(&mut self) { self.tiles .iter_mut() .for_each(|row| row.iter_mut().for_each(|tile| tile.hovered = false)); } pub fn unselect_all_tiles(&mut self) { self.tiles .iter_mut() .for_each(|row| row.iter_mut().for_each(|tile| tile.selected = false)); } pub unsafe fn draw(&self, window: Window) { let mut paint: PAINTSTRUCT = mem::zeroed(); //paint.fErase = 1; let hdc = BeginPaint(window.0, &mut paint); for row in 0..self.rows() { for column in 0..self.columns() { self.tiles[row][column].draw(hdc, self.tile_area(row, column)); } } EndPaint(window.0, &paint); } } #[derive(Default, Clone, Copy, PartialEq)] struct Tile { selected: bool, hovered: bool, } impl Tile { unsafe fn draw(self, hdc: HDC, area: Rect) { let fill_brush = self.fill_brush(); let frame_brush = CreateSolidBrush(RGB(0, 0, 0)); FillRect(hdc, &area.into(), fill_brush); FrameRect(hdc, &area.into(), frame_brush); DeleteObject(fill_brush as *mut _); DeleteObject(frame_brush as *mut _); } unsafe fn fill_brush(self) -> HBRUSH { let color = if self.selected { RGB(0, 77, 128) } else if self.hovered { RGB(0, 100, 148) } else { RGB( (255.0 * (70.0 / 100.0)) as u8, (255.0 * (70.0 / 100.0)) as u8, (255.0 * (70.0 / 100.0)) as u8, ) }; CreateSolidBrush(color) } } ================================================ FILE: src/hotkey.rs ================================================ use std::mem; use std::ptr; use std::thread; use winapi::um::winuser::{ DispatchMessageW, GetKeyboardLayout, GetMessageW, RegisterHotKey, TranslateMessage, VkKeyScanExW, MOD_ALT, MOD_CONTROL, MOD_NOREPEAT, MOD_SHIFT, MOD_WIN, WM_HOTKEY, }; use crate::common::report_and_exit; use crate::Message; use crate::CHANNEL; #[derive(PartialEq, Clone, Copy, Debug)] pub enum HotkeyType { Main, QuickResize, Maximize, } pub fn spawn_hotkey_thread(hotkey_str: &str, hotkey_type: HotkeyType) { let mut hotkey: Vec = hotkey_str .split('+') .map(|s| s.trim().to_string()) .collect(); if hotkey.len() < 2 || hotkey.len() > 5 { report_and_exit(&format!( "Invalid hotkey <{}>: Combination must be between 2 to 5 keys long.", hotkey_str )); } let virtual_key_char = hotkey.pop().unwrap().chars().next().unwrap(); let hotkey_str = hotkey_str.to_owned(); thread::spawn(move || unsafe { let sender = &CHANNEL.0.clone(); let result = RegisterHotKey( ptr::null_mut(), 0, compile_modifiers(&hotkey, &hotkey_str) | MOD_NOREPEAT as u32, get_vkcode(virtual_key_char), ); if result == 0 { report_and_exit(&format!("Failed to assign hot key <{}>. Either program is already running or hotkey is already assigned in another program.", hotkey_str)); } let mut msg = mem::zeroed(); while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) != 0 { TranslateMessage(&msg); DispatchMessageW(&msg); if msg.message == WM_HOTKEY { let _ = sender.send(Message::HotkeyPressed(hotkey_type)); } } }); } fn compile_modifiers(activators: &[String], hotkey_str: &str) -> u32 { let mut code: u32 = 0; for key in activators { match key.as_str() { "ALT" => code |= MOD_ALT as u32, "CTRL" => code |= MOD_CONTROL as u32, "SHIFT" => code |= MOD_SHIFT as u32, "WIN" => code |= MOD_WIN as u32, _ => report_and_exit(&format!("Invalid hotkey <{}>: Unidentified modifier in hotkey combination. Valid modifiers are CTRL, ALT, SHIFT, WIN.", hotkey_str)) } } code } unsafe fn get_vkcode(key_char: char) -> u32 { let keyboard_layout = GetKeyboardLayout(0); let vk_code = VkKeyScanExW(key_char as u16, keyboard_layout); if vk_code == -1 { report_and_exit(&format!("Invalid key {} in hotkey combination.", key_char)); } vk_code.to_be_bytes()[1] as u32 } ================================================ FILE: src/main.rs ================================================ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![allow(non_snake_case)] use std::{ mem, result, sync::{Arc, Mutex}, }; use anyhow::Error; use crossbeam_channel::{bounded, select, unbounded, Receiver, Sender}; use lazy_static::lazy_static; use winapi::um::winuser::{ SetForegroundWindow, ShowWindow, TrackMouseEvent, SW_SHOW, TME_LEAVE, TRACKMOUSEEVENT, }; use crate::common::{get_foreground_window, report_and_exit, show_msg_box, Rect}; use crate::event::{spawn_foreground_hook, spawn_track_monitor_thread}; use crate::grid::Grid; use crate::hotkey::{spawn_hotkey_thread, HotkeyType}; use crate::tray::spawn_sys_tray; use crate::window::{spawn_grid_window, spawn_preview_window, Window}; mod autostart; mod common; mod config; mod event; mod grid; mod hotkey; mod tray; mod window; lazy_static! { static ref CHANNEL: (Sender, Receiver) = unbounded(); static ref CONFIG: Arc> = { match config::load_config() { Ok(config) => Arc::new(Mutex::new(config)), Err(e) => report_and_exit(&format!("Could not load config. Check config file for formatting errors and relaunch program.\n\nErr: {}", e)), } }; static ref GRID: Arc> = Arc::new(Mutex::new(Grid::from(&*CONFIG.lock().unwrap()))); static ref ACTIVE_PROFILE: Arc> = Arc::new(Mutex::new("Default".to_owned())); } pub enum Message { PreviewWindow(Window), GridWindow(Window), HighlightZone(Rect), HotkeyPressed(HotkeyType), TrackMouse(Window), ActiveWindowChange(Window), ProfileChange(&'static str), MonitorChange, MouseLeft, InitializeWindows, CloseWindows, Exit, } #[macro_export] macro_rules! str_to_wide { ($str:expr) => {{ $str.encode_utf16() .chain(std::iter::once(0)) .collect::>() }}; } pub type Result = result::Result; fn main() { let receiver = &CHANNEL.1.clone(); let sender = &CHANNEL.0.clone(); let close_channel = bounded::<()>(3); let config = CONFIG.lock().unwrap().clone(); unsafe { if let Err(e) = autostart::toggle_autostart_registry_key(config.auto_start) { show_msg_box(&format!( "Error updating registry while toggling autostart from system tray.\n\nErr: {}", e )) }; } spawn_hotkey_thread(&config.hotkey, HotkeyType::Main); if let Some(hotkey) = &config.hotkey_quick_resize { spawn_hotkey_thread(hotkey, HotkeyType::QuickResize); } if let Some(hotkey_maximize) = &config.hotkey_maximize_toggle { spawn_hotkey_thread(hotkey_maximize, HotkeyType::Maximize); } unsafe { spawn_sys_tray(); } let mut preview_window: Option = None; let mut grid_window: Option = None; let mut track_mouse = false; loop { select! { recv(receiver) -> msg => { match msg.unwrap() { Message::PreviewWindow(window) => unsafe { preview_window = Some(window); spawn_foreground_hook(close_channel.1.clone()); ShowWindow(grid_window.as_ref().unwrap().0, SW_SHOW); SetForegroundWindow(grid_window.as_ref().unwrap().0); } Message::GridWindow(window) => { grid_window = Some(window); let mut grid = GRID.lock().unwrap(); grid.grid_window = Some(window); grid.active_window = Some(get_foreground_window()); spawn_track_monitor_thread(close_channel.1.clone()); spawn_preview_window(close_channel.1.clone()); } Message::HighlightZone(rect) => { let mut preview_window = preview_window.unwrap_or_default(); let grid_window = grid_window.unwrap_or_default(); preview_window.set_pos(rect, Some(grid_window)); } Message::HotkeyPressed(hotkey_type) => { if hotkey_type == HotkeyType::Maximize { let mut grid = GRID.lock().unwrap(); let mut active_window = if grid_window.is_some() { grid.active_window.unwrap() } else { let active_window = get_foreground_window(); grid.active_window = Some(active_window); active_window }; let active_rect = active_window.rect(); active_window.restore(); let mut max_rect = grid.get_max_area(); max_rect.adjust_for_border(active_window.transparent_border()); if let Some((_, previous_rect)) = grid.previous_resize { if active_rect == max_rect { active_window.set_pos(previous_rect, None); } else { active_window.set_pos(max_rect, None); } } else { active_window.set_pos(max_rect, None); } grid.previous_resize = Some((active_window, active_rect)); } else if preview_window.is_some() && grid_window.is_some() { let _ = sender.send(Message::CloseWindows); } else { let _ = sender.send(Message::InitializeWindows); if hotkey_type == HotkeyType::QuickResize { GRID.lock().unwrap().quick_resize = true; } } } Message::TrackMouse(window) => unsafe { if !track_mouse { let mut event_track: TRACKMOUSEEVENT = mem::zeroed(); event_track.cbSize = mem::size_of::() as u32; event_track.dwFlags = TME_LEAVE; event_track.hwndTrack = window.0; TrackMouseEvent(&mut event_track); track_mouse = true; } } Message::MouseLeft => { track_mouse = false; } Message::ActiveWindowChange(window) => { let mut grid = GRID.lock().unwrap(); if grid.grid_window != Some(window) && grid.active_window != Some(window) { grid.active_window = Some(window); } } Message::MonitorChange => { let mut grid = GRID.lock().unwrap(); let active_window = grid.active_window; let previous_resize = grid.previous_resize; let quick_resize = grid.quick_resize; *grid = Grid::from(&*CONFIG.lock().unwrap()); grid.grid_window = grid_window; grid.active_window = active_window; grid.previous_resize = previous_resize; grid.quick_resize = quick_resize; grid.reposition(); } Message::ProfileChange(profile) => { { let mut active_profile = ACTIVE_PROFILE.lock().unwrap(); *active_profile = profile.to_owned(); } let mut grid = GRID.lock().unwrap(); let active_window = grid.active_window; let previous_resize = grid.previous_resize; let quick_resize = grid.quick_resize; *grid = Grid::from(&*CONFIG.lock().unwrap()); grid.grid_window = grid_window; grid.active_window = active_window; grid.previous_resize = previous_resize; grid.quick_resize = quick_resize; grid.reposition(); } Message::InitializeWindows => { let mut grid = GRID.lock().unwrap(); let quick_resize = grid.quick_resize; let previous_resize = grid.previous_resize; *grid = Grid::from(&*CONFIG.lock().unwrap()); grid.quick_resize = quick_resize; grid.previous_resize = previous_resize; spawn_grid_window(close_channel.1.clone()); } Message::CloseWindows => { preview_window.take(); grid_window.take(); for _ in 0..4 { let _ = close_channel.0.send(()); } let mut grid = GRID.lock().unwrap(); grid.reset(); track_mouse = false; } Message::Exit => { break; } } }, } } } ================================================ FILE: src/tray.rs ================================================ use std::mem; use std::ptr; use std::thread; use winapi::shared::{ minwindef::{LOWORD, LPARAM, LRESULT, UINT, WPARAM}, windef::{HWND, POINT}, }; use winapi::um::libloaderapi::GetModuleHandleW; use winapi::um::shellapi::{ ShellExecuteW, Shell_NotifyIconW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NOTIFYICONDATAW, }; use winapi::um::wingdi::{CreateSolidBrush, RGB}; use winapi::um::winuser::{ CheckMenuItem, CreateIconFromResourceEx, CreatePopupMenu, CreateWindowExW, DefWindowProcW, DestroyMenu, DispatchMessageW, GetCursorPos, GetMessageW, InsertMenuW, MessageBoxW, PostMessageW, PostQuitMessage, RegisterClassExW, SendMessageW, SetFocus, SetForegroundWindow, SetMenuDefaultItem, SetMenuItemBitmaps, TrackPopupMenu, TranslateMessage, LR_DEFAULTCOLOR, MB_ICONINFORMATION, MB_OK, MF_BYPOSITION, MF_CHECKED, MF_STRING, MF_UNCHECKED, SW_SHOW, TPM_LEFTALIGN, TPM_NONOTIFY, TPM_RETURNCMD, TPM_RIGHTBUTTON, WM_APP, WM_CLOSE, WM_COMMAND, WM_CREATE, WM_INITMENUPOPUP, WM_LBUTTONDBLCLK, WM_RBUTTONUP, WNDCLASSEXW, WS_EX_NOACTIVATE, }; use crate::autostart; use crate::common::show_msg_box; use crate::config; use crate::str_to_wide; use crate::Message; use crate::CHANNEL; use crate::CONFIG; const ID_ABOUT: u16 = 2000; const ID_EXIT: u16 = 2001; const ID_CONFIG: u16 = 2002; const ID_AUTOSTART: u16 = 2003; static mut MODAL_SHOWN: bool = false; pub unsafe fn spawn_sys_tray() { thread::spawn(|| { let hInstance = GetModuleHandleW(ptr::null()); let class_name = str_to_wide!("Grout Tray"); let mut class = mem::zeroed::(); class.cbSize = mem::size_of::() as u32; class.lpfnWndProc = Some(callback); class.hInstance = hInstance; class.lpszClassName = class_name.as_ptr(); class.hbrBackground = CreateSolidBrush(RGB(0, 77, 128)); RegisterClassExW(&class); CreateWindowExW( WS_EX_NOACTIVATE, class_name.as_ptr(), ptr::null(), 0, 0, 0, 0, 0, ptr::null_mut(), ptr::null_mut(), hInstance, ptr::null_mut(), ); let mut msg = mem::zeroed(); while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) != 0 { TranslateMessage(&msg); DispatchMessageW(&msg); } }); } unsafe fn add_icon(hwnd: HWND) { let icon_bytes = include_bytes!("../assets/icon_32.png"); let icon_handle = CreateIconFromResourceEx( icon_bytes.as_ptr() as *mut _, icon_bytes.len() as u32, 1, 0x0003_0000, 32, 32, LR_DEFAULTCOLOR, ); let mut tooltip_array = [0u16; 128]; let tooltip = "Grout"; let mut tooltip = tooltip.encode_utf16().collect::>(); tooltip.extend(vec![0; 128 - tooltip.len()]); tooltip_array.swap_with_slice(&mut tooltip[..]); let mut icon_data: NOTIFYICONDATAW = mem::zeroed(); icon_data.cbSize = mem::size_of::() as u32; icon_data.hWnd = hwnd; icon_data.uID = 1; icon_data.uCallbackMessage = WM_APP; icon_data.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; icon_data.hIcon = icon_handle; icon_data.szTip = tooltip_array; Shell_NotifyIconW(NIM_ADD, &mut icon_data); } unsafe fn remove_icon(hwnd: HWND) { let mut icon_data: NOTIFYICONDATAW = mem::zeroed(); icon_data.hWnd = hwnd; icon_data.uID = 1; Shell_NotifyIconW(NIM_DELETE, &mut icon_data); } unsafe fn show_popup_menu(hwnd: HWND) { if MODAL_SHOWN { return; } let menu = CreatePopupMenu(); let mut about = str_to_wide!("About..."); let mut auto_start = str_to_wide!("Launch at startup"); let mut open_config = str_to_wide!("Open Config"); let mut exit = str_to_wide!("Exit"); InsertMenuW( menu, 0, MF_BYPOSITION | MF_STRING, ID_ABOUT as usize, about.as_mut_ptr(), ); InsertMenuW( menu, 1, MF_BYPOSITION | MF_STRING, ID_AUTOSTART as usize, auto_start.as_mut_ptr(), ); SetMenuItemBitmaps(menu, 1, MF_BYPOSITION, ptr::null_mut(), ptr::null_mut()); let checked = if CONFIG.lock().unwrap().auto_start { MF_CHECKED } else { MF_UNCHECKED }; CheckMenuItem(menu, 1, MF_BYPOSITION | checked); InsertMenuW( menu, 2, MF_BYPOSITION | MF_STRING, ID_CONFIG as usize, open_config.as_mut_ptr(), ); InsertMenuW( menu, 3, MF_BYPOSITION | MF_STRING, ID_EXIT as usize, exit.as_mut_ptr(), ); SetMenuDefaultItem(menu, ID_ABOUT as u32, 0); SetFocus(hwnd); SendMessageW(hwnd, WM_INITMENUPOPUP, menu as usize, 0); let mut point: POINT = mem::zeroed(); GetCursorPos(&mut point); let cmd = TrackPopupMenu( menu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, point.x, point.y, 0, hwnd, ptr::null_mut(), ); SendMessageW(hwnd, WM_COMMAND, cmd as usize, 0); DestroyMenu(menu); } unsafe fn show_about() { let mut title = str_to_wide!("About"); let msg = format!( "Grout - v{}\n\nCopyright © 2020 Cory Forsstrom", env!("CARGO_PKG_VERSION") ); let mut msg = str_to_wide!(msg); MessageBoxW( ptr::null_mut(), msg.as_mut_ptr(), title.as_mut_ptr(), MB_ICONINFORMATION | MB_OK, ); } unsafe extern "system" fn callback( hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM, ) -> LRESULT { match Msg { WM_CREATE => { add_icon(hWnd); return 0; } WM_CLOSE => { remove_icon(hWnd); PostQuitMessage(0); let _ = &CHANNEL.0.clone().send(Message::Exit); } WM_COMMAND => { if MODAL_SHOWN { return 1; } match LOWORD(wParam as u32) { ID_ABOUT => { MODAL_SHOWN = true; show_about(); MODAL_SHOWN = false; } ID_AUTOSTART => { if let Err(e) = config::toggle_autostart() { show_msg_box(&format!( "Error while toggling autostart from system tray.\n\nErr: {}", e )) }; let mut config = CONFIG.lock().unwrap(); match config::load_config() { Ok(_config) => *config = _config, Err(e) => show_msg_box(&format!("Error loading config while toggling autostart from system tray. Check config file for formatting errors.\n\nErr: {}", e)), } if let Err(e) = autostart::toggle_autostart_registry_key(config.auto_start) { show_msg_box(&format!( "Error updating registry while toggling autostart from system tray.\n\nErr: {}", e )) }; } ID_CONFIG => { if let Some(mut config_path) = dirs::config_dir() { config_path.push("grout"); config_path.push("config.yml"); if config_path.exists() { let mut operation = str_to_wide!("open"); let mut config_path = str_to_wide!(config_path.to_str().unwrap()); ShellExecuteW( hWnd, operation.as_mut_ptr(), config_path.as_mut_ptr(), ptr::null_mut(), ptr::null_mut(), SW_SHOW, ); } } } ID_EXIT => { PostMessageW(hWnd, WM_CLOSE, 0, 0); } _ => {} } return 0; } WM_APP => { match lParam as u32 { WM_LBUTTONDBLCLK => show_about(), WM_RBUTTONUP => { SetForegroundWindow(hWnd); show_popup_menu(hWnd); PostMessageW(hWnd, WM_APP + 1, 0, 0); } _ => {} } return 0; } _ => {} } DefWindowProcW(hWnd, Msg, wParam, lParam) } ================================================ FILE: src/window/grid.rs ================================================ use std::mem; use std::ptr; use std::thread; use std::time::Duration; use crossbeam_channel::{select, Receiver}; use winapi::shared::{ minwindef::{HIWORD, LOWORD, LPARAM, LRESULT, UINT, WPARAM}, windef::HWND, }; use winapi::um::libloaderapi::GetModuleHandleW; use winapi::um::wingdi::{CreateSolidBrush, RGB}; use winapi::um::winuser::{ CreateWindowExW, DefWindowProcW, DispatchMessageW, InvalidateRect, LoadCursorW, PeekMessageW, RegisterClassExW, SendMessageW, TranslateMessage, IDC_ARROW, VK_CONTROL, VK_DOWN, VK_ESCAPE, VK_F1, VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_LEFT, VK_RIGHT, VK_SHIFT, VK_UP, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSELEAVE, WM_MOUSEMOVE, WM_PAINT, WNDCLASSEXW, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_POPUP, }; use crate::common::{get_work_area, Rect}; use crate::str_to_wide; use crate::window::Window; use crate::Message; use crate::{CHANNEL, GRID}; pub fn spawn_grid_window(close_msg: Receiver<()>) { thread::spawn(move || unsafe { let hInstance = GetModuleHandleW(ptr::null()); let class_name = str_to_wide!("Grout Zone Grid"); let mut class = mem::zeroed::(); class.cbSize = mem::size_of::() as u32; class.lpfnWndProc = Some(callback); class.hInstance = hInstance; class.lpszClassName = class_name.as_ptr(); class.hbrBackground = CreateSolidBrush(RGB(44, 44, 44)); class.hCursor = LoadCursorW(ptr::null_mut(), IDC_ARROW); RegisterClassExW(&class); let work_area = get_work_area(); let dimensions = GRID.lock().unwrap().dimensions(); let hwnd = CreateWindowExW( WS_EX_TOPMOST | WS_EX_TOOLWINDOW, class_name.as_ptr(), ptr::null(), WS_POPUP, work_area.width / 2 - dimensions.0 as i32 / 2 + work_area.x, work_area.height / 2 - dimensions.1 as i32 / 2 + work_area.y, dimensions.0 as i32, dimensions.1 as i32, ptr::null_mut(), ptr::null_mut(), hInstance, ptr::null_mut(), ); let _ = &CHANNEL.0.clone().send(Message::GridWindow(Window(hwnd))); let mut msg = mem::zeroed(); loop { if PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 1) > 0 { TranslateMessage(&msg); DispatchMessageW(&msg); }; select! { recv(close_msg) -> _ => { break; } default(Duration::from_millis(10)) => {} } } }); } unsafe extern "system" fn callback( hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM, ) -> LRESULT { let sender = &CHANNEL.0.clone(); let repaint = match Msg { WM_PAINT => { GRID.lock().unwrap().draw(Window(hWnd)); false } WM_KEYDOWN => match wParam as i32 { VK_ESCAPE => { let _ = sender.send(Message::CloseWindows); false } VK_CONTROL => { GRID.lock().unwrap().control_down = true; false } VK_SHIFT => { GRID.lock().unwrap().shift_down = true; false } VK_RIGHT => { if GRID.lock().unwrap().control_down { GRID.lock().unwrap().add_column(); GRID.lock().unwrap().reposition(); } false } VK_LEFT => { if GRID.lock().unwrap().control_down { GRID.lock().unwrap().remove_column(); GRID.lock().unwrap().reposition(); } false } VK_UP => { if GRID.lock().unwrap().control_down { GRID.lock().unwrap().add_row(); GRID.lock().unwrap().reposition(); } false } VK_DOWN => { if GRID.lock().unwrap().control_down { GRID.lock().unwrap().remove_row(); GRID.lock().unwrap().reposition(); } false } _ => false, }, WM_KEYUP => match wParam as i32 { VK_CONTROL => { GRID.lock().unwrap().control_down = false; false } VK_SHIFT => { GRID.lock().unwrap().shift_down = false; false } VK_F1 => { let _ = sender.send(Message::ProfileChange("Default")); false } VK_F2 => { let _ = sender.send(Message::ProfileChange("Profile2")); false } VK_F3 => { let _ = sender.send(Message::ProfileChange("Profile3")); false } VK_F4 => { let _ = sender.send(Message::ProfileChange("Profile4")); false } VK_F5 => { let _ = sender.send(Message::ProfileChange("Profile5")); false } VK_F6 => { let _ = sender.send(Message::ProfileChange("Profile6")); false } _ => false, }, WM_MOUSEMOVE => { let x = LOWORD(lParam as u32) as i32; let y = HIWORD(lParam as u32) as i32; let _ = sender.send(Message::TrackMouse(Window(hWnd))); if let Some(rect) = GRID.lock().unwrap().highlight_tiles((x, y)) { let _ = sender.send(Message::HighlightZone(rect)); true } else { false } } WM_LBUTTONDOWN => { let x = LOWORD(lParam as u32) as i32; let y = HIWORD(lParam as u32) as i32; let mut grid = GRID.lock().unwrap(); let repaint = grid.select_tile((x, y)); grid.cursor_down = true; repaint } WM_LBUTTONUP => { let mut grid = GRID.lock().unwrap(); let repaint = if let Some(mut rect) = grid.selected_area() { if let Some(mut active_window) = grid.active_window { if grid.previous_resize != Some((active_window, rect)) { active_window.restore(); rect.adjust_for_border(active_window.transparent_border()); active_window.set_pos(rect, None); grid.previous_resize = Some((active_window, rect)); if grid.quick_resize { let _ = sender.send(Message::CloseWindows); } } grid.unselect_all_tiles(); } true } else { false }; grid.cursor_down = false; repaint } WM_MOUSELEAVE => { GRID.lock().unwrap().unhighlight_all_tiles(); let _ = sender.send(Message::MouseLeft); let _ = sender.send(Message::HighlightZone(Rect::zero())); true } _ => false, }; if repaint { let dimensions = GRID.lock().unwrap().dimensions(); let rect = Rect { x: 0, y: 0, width: dimensions.0 as i32, height: dimensions.1 as i32, }; InvalidateRect(hWnd, &rect.into(), 0); SendMessageW(hWnd, WM_PAINT, 0, 0); } DefWindowProcW(hWnd, Msg, wParam, lParam) } ================================================ FILE: src/window/preview.rs ================================================ use std::mem; use std::ptr; use std::thread; use std::time::Duration; use crossbeam_channel::{select, Receiver}; use winapi::shared::{ minwindef::{LPARAM, LRESULT, UINT, WPARAM}, windef::HWND, }; use winapi::um::libloaderapi::GetModuleHandleW; use winapi::um::wingdi::{CreateSolidBrush, RGB}; use winapi::um::winuser::{ CreateWindowExW, DefWindowProcW, DispatchMessageW, PeekMessageW, RegisterClassExW, SetLayeredWindowAttributes, TranslateMessage, LWA_ALPHA, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_POPUP, WS_SYSMENU, WS_VISIBLE, }; use crate::str_to_wide; use crate::window::Window; use crate::Message; use crate::CHANNEL; pub fn spawn_preview_window(close_msg: Receiver<()>) { thread::spawn(move || unsafe { let hInstance = GetModuleHandleW(ptr::null()); let class_name = str_to_wide!("Grout Zone Preview"); let mut class = mem::zeroed::(); class.cbSize = mem::size_of::() as u32; class.lpfnWndProc = Some(callback); class.hInstance = hInstance; class.lpszClassName = class_name.as_ptr(); class.hbrBackground = CreateSolidBrush(RGB(0, 77, 128)); RegisterClassExW(&class); let hwnd = CreateWindowExW( WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_NOACTIVATE, class_name.as_ptr(), ptr::null(), WS_POPUP | WS_VISIBLE | WS_SYSMENU, 0, 0, 0, 0, ptr::null_mut(), ptr::null_mut(), hInstance, ptr::null_mut(), ); SetLayeredWindowAttributes(hwnd, 0, 107, LWA_ALPHA); let _ = &CHANNEL.0.clone().send(Message::PreviewWindow(Window(hwnd))); let mut msg = mem::zeroed(); loop { if PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 1) > 0 { TranslateMessage(&msg); DispatchMessageW(&msg); }; select! { recv(close_msg) -> _ => { break; } default(Duration::from_millis(10)) => {} } } }); } unsafe extern "system" fn callback( hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM, ) -> LRESULT { DefWindowProcW(hWnd, Msg, wParam, lParam) } ================================================ FILE: src/window.rs ================================================ use std::mem; use std::ptr; use winapi::shared::windef::HWND; use winapi::um::winuser::{ GetWindowInfo, GetWindowRect, SetWindowPos, ShowWindow, SWP_NOACTIVATE, SW_RESTORE, WINDOWINFO, }; use crate::common::Rect; mod grid; pub use grid::spawn_grid_window; mod preview; pub use preview::spawn_preview_window; #[derive(Clone, Copy, Debug)] pub struct Window(pub HWND); unsafe impl Send for Window {} impl Window { pub fn rect(self) -> Rect { unsafe { let mut rect = mem::zeroed(); GetWindowRect(self.0, &mut rect); rect.into() } } pub fn set_pos(&mut self, rect: Rect, insert_after: Option) { unsafe { SetWindowPos( self.0, insert_after.unwrap_or_default().0, rect.x, rect.y, rect.width, rect.height, SWP_NOACTIVATE, ); } } pub unsafe fn info(self) -> WindowInfo { let mut info: WINDOWINFO = mem::zeroed(); info.cbSize = mem::size_of::() as u32; GetWindowInfo(self.0, &mut info); info.into() } pub fn transparent_border(self) -> (i32, i32) { let info = unsafe { self.info() }; let x = { (info.window_rect.x - info.client_rect.x) + (info.window_rect.width - info.client_rect.width) }; let y = { (info.window_rect.y - info.client_rect.y) + (info.window_rect.height - info.client_rect.height) }; (x, y) } pub fn restore(&mut self) { unsafe { ShowWindow(self.0, SW_RESTORE); }; } } impl Default for Window { fn default() -> Self { Window(ptr::null_mut()) } } impl PartialEq for Window { fn eq(&self, other: &Window) -> bool { self.0 == other.0 } } #[derive(Debug)] pub struct WindowInfo { pub window_rect: Rect, pub client_rect: Rect, pub styles: u32, pub extended_styles: u32, pub x_borders: u32, pub y_borders: u32, } impl From for WindowInfo { fn from(info: WINDOWINFO) -> Self { WindowInfo { window_rect: info.rcWindow.into(), client_rect: info.rcClient.into(), styles: info.dwStyle, extended_styles: info.dwExStyle, x_borders: info.cxWindowBorders, y_borders: info.cxWindowBorders, } } }