From e336cf7e861bc24f4d8354224c807764e36389bf Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Tue, 25 Aug 2026 18:05:12 +0500 Subject: [PATCH] feat(ui): frameless transparent window with drag --- README.md | 30 +++++++++++++++++++---------- docs/dev-workflow.md | 21 +++++++++++++-------- src/main.rs | 43 +++++++++++++++++++++++++++++------------- src/winit.rs | 5 ----- tools/run-and-shot.ps1 | 3 +++ tools/shot.ps1 | 9 ++++++++- ui/header.slint | 12 ++++++++++++ ui/main.slint | 19 ++++++++++++++++++- 8 files changed, 104 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 907bd31..a035382 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,26 @@ - GUI: [Slint](https://slint.dev/) (Native-speed, Declarative UI) - Запуск процессов: std::process::Command -## 🏗 План на выходные (MVP) +## 📊 Текущий статус -- Настройка проекта: Инициализация cargo и установка зависимостей Slint. -- Верстка UI: Создать чистое окно с кнопками установленных браузеров (Firefox, Chrome, Edge и т.д.). -- Логика запуска: Реализовать передачу URL из аргументов командной строки в выбранный браузер. -- Регистрация в системе: Написать скрипт/инструкцию, как сделать приложение браузером по умолчанию. +- Frameless-окно с прозрачным фоном: карточка парит над рабочим столом со скруглёнными углами +- Тёмная и светлая темы (переключение кнопкой, env `BP_THEME=light`) +- Перетаскивание окна за хедер, Esc — закрыть, центрирование при старте +- Дизайн-макеты: [refs/dark.html](refs/dark.html) / [refs/light.html](refs/light.html) -## 📝 Заметки для реализации +## 🔧 Разработка -- Использовать no-frame: true в Slint для кастомного дизайна окна. -- Для иконок браузеров использовать формат SVG (Slint отлично их рендерит). -- Конфигурацию путей к браузерам вынести в простой config.toml (в будущем). -- загружать конфигурацию браузера из ярлыка +План работ и цикл визуальной разработки с ИИ-агентом — [docs/dev-workflow.md](docs/dev-workflow.md). + +```powershell +cargo run # запуск +tools/render-refs.ps1 # PNG-референсы из макетов +tools/run-and-shot.ps1 # собрать, снять скриншот окна +tools/run-and-shot.ps1 -Light # то же в светлой теме +``` + +## 🗺 Дорожная карта + +1. Список браузеров с логотипами, запуск выбранного браузера с URL (MVP) +2. Регистрация в системе как браузер по умолчанию +3. config.toml + страница настроек diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md index d6e01d9..f121dba 100644 --- a/docs/dev-workflow.md +++ b/docs/dev-workflow.md @@ -64,14 +64,19 @@ HTML-макеты → PNG: **headless Edge** (`--headless --screenshot`), он Текущее: окно с заголовком (специально, чтобы двигать мышью). Целевое: frameless + прозрачность + драг за хедер, как в макете. -- [ ] `no-frame: true` + `background: transparent` в MainWindow -- [ ] Прозрачность проверить первой же парой скриншотов (углы карточки vs ref-dark.png) -- [ ] Драг: TouchArea в хедере → callback `request-drag` → в Rust через - `i-slint-backend-winit`: `window.with_winit_window(|w| w.drag_window())` - (код частично был в старом src/main.rs, восстановить из истории при необходимости) -- [ ] Центрирование окна при старте — вернуть `src/winit.rs` (он в истории main до коммита 464f4f0) -- [ ] Esc — закрыть: `FocusScope { key-event }` или `window.close()` -- [ ] README привести в соответствие: стек Slint, скриншоты из `tools/out/` +- [x] `no-frame: true` + `background: transparent` в MainWindow (+ `title: "bropicker"`) +- [x] Прозрачность подтверждена скриншотами: карточка со скруглёнными углами парит + над рабочим столом, тёмная и светлая темы +- [x] Драг: TouchArea на хедере (`pointer-event` down) → `request-drag` → + `with_winit_window(drag_window)` через `i-slint-backend-winit` +- [x] Центрирование окна при старте — `src/winit.rs` восстановлен из истории + (осторожно: git-редирект PowerShell создаёт UTF-16 — файл перезаписан в UTF-8) +- [x] Esc — закрыть: `FocusScope.key-pressed` (в 1.16 нет `key-event`; обработчик + обязан вернуть `accept`/`reject` во всех ветках) +- [x] `BP_THEME=light` env-флаг + `run-and-shot.ps1 -Light` +- [x] README обновлён (стек, статус, команды dev-цикла, дорожная карта) + +Требует ручной проверки: перетаскивание мышью, Esc, центрирование на активном мониторе. Коммит: `feat(ui): frameless transparent window with drag` diff --git a/src/main.rs b/src/main.rs index cc0483d..d5ffd92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use std::rc::Rc; -// use slint::BackendSelector; +use i_slint_backend_winit::WinitWindowAccessor; -// use crate::winit::center_window; -// mod winit; +use crate::winit::center_window; +mod winit; slint::include_modules!(); @@ -41,6 +41,10 @@ fn init() -> State { let main_window = MainWindow::new().unwrap(); + if std::env::var("BP_THEME").as_deref() == Ok("light") { + main_window.global::().set_is_dark(false); + } + main_window.set_current_url("ku6epxboctuk.github.io".into()); main_window.set_remember_choice(true); main_window.set_always_ask(true); @@ -67,17 +71,21 @@ fn init() -> State { println!("Open clicked"); }); - // main_window.on_request_drag(move || { - // main_window.window().with_winit_window(|winit_window| { - // winit_window.drag_window().ok(); - // }); - // }); + main_window.on_request_drag({ + let main_window = main_window.clone_strong(); + move || { + main_window.window().with_winit_window(|winit_window| { + winit_window.drag_window().ok(); + }); + } + }); - // center_window(main_window.window()); - - main_window - .window() - .on_close_requested(move || slint::CloseRequestResponse::HideWindow); + main_window.on_esc_pressed({ + let main_window = main_window.clone_strong(); + move || { + let _ = main_window.window().hide(); + } + }); main_window.set_browser_model(browser_model.clone().into()); State { @@ -100,5 +108,14 @@ pub fn main() { let state = init(); let main_window = state.main_window.clone_strong(); + main_window.show().unwrap(); + + let weak = main_window.as_weak(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(mw) = weak.upgrade() { + center_window(mw.window()); + } + }); + main_window.run().unwrap(); } diff --git a/src/winit.rs b/src/winit.rs index 69211f5..23a27d1 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -4,9 +4,7 @@ use i_slint_backend_winit::winit::monitor::MonitorHandle; use i_slint_backend_winit::winit::window::Window; pub fn center_window(window: &slint::Window) { - println!("Centering window"); if window.has_winit_window() { - println!("Has winit"); window.with_winit_window(|window: &Window| { match window.current_monitor() { Some(monitor) => set_centered(window, &monitor), @@ -24,9 +22,6 @@ fn set_centered(window: &Window, monitor: &MonitorHandle) { let monitor_size = monitor.size(); let monitor_position = monitor.position(); - println!("Monitor size: {:?}", monitor_size); - println!("Window size: {:?}", window_size); - let mut monitor_window_position = PhysicalPosition { x: 0, y: 0 }; monitor_window_position.x = (monitor_position.x as f32 + (monitor_size.width as f32 * 0.5) diff --git a/tools/run-and-shot.ps1 b/tools/run-and-shot.ps1 index 88486d0..8000333 100644 --- a/tools/run-and-shot.ps1 +++ b/tools/run-and-shot.ps1 @@ -1,5 +1,6 @@ param( [string]$Out = "tools/out/app.png", + [switch]$Light, [switch]$NoBuild ) @@ -16,7 +17,9 @@ if (-not (Test-Path $exe)) { exit 1 } +if ($Light) { $env:BP_THEME = "light" } $proc = Start-Process -FilePath $exe -WorkingDirectory $PWD -PassThru +Remove-Item Env:\BP_THEME -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 1800 & "$PSScriptRoot\shot.ps1" -ProcId $proc.Id -Out $Out diff --git a/tools/shot.ps1 b/tools/shot.ps1 index 6c1f0d2..dd80878 100644 --- a/tools/shot.ps1 +++ b/tools/shot.ps1 @@ -16,6 +16,8 @@ public class BpWin { [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } } @@ -48,6 +50,11 @@ if ($found -eq [IntPtr]::Zero) { $rect = New-Object BpWin+RECT [BpWin]::GetWindowRect($found, [ref]$rect) | Out-Null + +[BpWin]::SetWindowPos($found, [IntPtr](-1), 0, 0, 0, 0, 0x0003) | Out-Null +[BpWin]::SetForegroundWindow($found) | Out-Null +Start-Sleep -Milliseconds 250 + $w = $rect.Right - $rect.Left $h = $rect.Bottom - $rect.Top @@ -68,4 +75,4 @@ $outAbs = [System.IO.Path]::GetFullPath((Join-Path $PWD $Out)) $bmp.Save($outAbs, [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() -Write-Host "OK $OutAbs (${w}x${h})" +Write-Host "OK $OutAbs (${w}x${h} @ $($rect.Left),$($rect.Top))" diff --git a/ui/header.slint b/ui/header.slint index 7f4f164..19a50f4 100644 --- a/ui/header.slint +++ b/ui/header.slint @@ -4,6 +4,18 @@ import { GradientIcon } from "./gradient-icon.slint"; export component Header inherits Rectangle { in property url; + callback request-drag(); + + TouchArea { + width: 100%; + height: 100%; + mouse-cursor: move; + pointer-event(event) => { + if (event.kind == PointerEventKind.down) { + root.request-drag(); + } + } + } VerticalLayout { width: 100%; diff --git a/ui/main.slint b/ui/main.slint index a93bc11..e4a9ad4 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -5,7 +5,10 @@ import { Footer } from "./footer.slint"; import { BrowserConfig } from "./types.slint"; import { Theme } from "theme.slint"; +export { Theme } + export component MainWindow inherits Window { + title: "bropicker"; in property <[BrowserConfig]> browser-model: []; in property current-url: ""; in-out property remember-choice: false; @@ -18,13 +21,26 @@ export component MainWindow inherits Window { callback settings-clicked(); callback open-clicked(); callback request-drag(); + callback esc-pressed(); - // no-frame: true; + no-frame: true; background: transparent; width: 488px; height: 800px; default-font-family: "Segoe UI"; + FocusScope { + init => { self.focus(); } + key-pressed(event) => { + if (event.text == Key.Escape) { + root.esc-pressed(); + accept + } else { + reject + } + } + } + VerticalLayout { alignment: start; width: 488px; @@ -46,6 +62,7 @@ export component MainWindow inherits Window { Header { url: current-url; + request-drag => root.request-drag(); } // ListView {