# iced-borderless-window-resize-handle-widget
Invisible edge & corner resize handles for borderless [iced](https://iced.rs) windows.
Setting `decorations: false` in iced removes the OS window frame — including its native resize borders, which makes frameless windows impossible to resize with the mouse. This crate restores them with a single lightweight overlay widget.
Pressing a handle hands the drag to the operating system's own resize machinery via `drag_resize`, so resizing looks, feels, and behaves natively:
| Windows | Native size loop (`WM_NCLBUTTONDOWN` + `HT*` hit-test codes). No `WM_NCHITTEST`/`WS_THICKFRAME` hacks, so window snapping is untouched. The resize cursor is also kept visible for the whole drag (a Windows modal-loop quirk — see the crate docs). |
| Linux (X11) | `_NET_WM_MOVERESIZE` — the window manager drives the resize. |
| Linux (Wayland) | `xdg_toplevel.resize` — the compositor drives the resize. |
| macOS | Inert. winit borderless windows already keep `NSWindowStyleMask::Resizable`, so macOS edge-resizes them natively. |
The widget draws nothing and keeps no state — it only watches the borders of its layout bounds, shows the native resize cursors on hover, and captures clicks within its zones.
## Installation
```toml
[dependencies]
iced = "0.14" # any feature set works — the widget is renderer-agnostic
iced_borderless_window_resize_handle_widget = "0.1"
```
The widget only needs iced's `advanced` feature (which many apps already enable) and forces no renderer or executor choices on you. Your app's binary keeps exactly the feature footprint you configured.
## Usage
Layer the handles on top of your UI in a `stack` and forward the message to this crate's `drag_resize` (a drop-in replacement for `iced::window::drag_resize` that also keeps the native resize cursor visible during the whole drag on Windows):
```rust,ignore
use iced::widget::stack;
use iced::window;
use iced_borderless_window_resize_handle_widget::{drag_resize, ResizeHandles};
#[derive(Debug, Clone)]
enum Message {
WindowOpened(Option<window::Id>),
Resized(window::Direction),
}
fn view(state: &App) -> iced::Element<'_, Message> {
stack![
my_ui(state),
ResizeHandles::new(Message::Resized),
]
.width(iced::Fill)
.height(iced::Fill)
.into()
}
fn update(app: &mut App, message: Message) -> iced::Task<Message> {
match message {
Message::WindowOpened(id) => {
app.window_id = id;
iced::Task::none()
}
Message::Resized(direction) => app
.window_id
.map(|id| drag_resize(id, direction))
.unwrap_or_else(iced::Task::none),
}
}
```
iced has no `window::Id::MAIN`; capture the id of your main window once at boot:
```rust,ignore
fn boot() -> (App, iced::Task<Message>) {
(App::default(), window::latest().map(Message::WindowOpened))
}
```
If your message enum implements `From<window::Direction>`, you can skip the closure:
```rust,ignore
impl From<window::Direction> for Message {
fn from(direction: window::Direction) -> Self {
Message::Resized(direction)
}
}
// later, in `view`:
stack![my_ui(state), ResizeHandles::from_direction()]
```
Or wrap any element in one call:
```rust,ignore
with_resize_handles(my_ui(state), Message::Resized)
```
## Options
```rust,ignore
ResizeHandles::new(Message::Resized)
.gutter(6.0) // edge zone thickness (logical px, default 6)
.corner(14.0) // corner zone size — corners take precedence (default 14)
.enabled(true) // e.g. `.enabled(!is_maximized)` to disable while maximized
```
## Notes
* The grab zones consume clicks in the outermost `gutter` pixels of the window — just like the invisible border of a native frame. Don't put interactive widgets flush against the window edges.
* Resizing a maximized window is ignored by the OS; gate the handles with `.enabled(...)` if you prefer.
* In multi-window apps, pass each window's `window::Id` into the message closure.
Run the demo with:
```text
cargo run --example basic
```