feat(ui): frameless transparent window with drag
This commit is contained in:
@@ -12,16 +12,26 @@
|
|||||||
- GUI: [Slint](https://slint.dev/) (Native-speed, Declarative UI)
|
- GUI: [Slint](https://slint.dev/) (Native-speed, Declarative UI)
|
||||||
- Запуск процессов: std::process::Command
|
- Запуск процессов: std::process::Command
|
||||||
|
|
||||||
## 🏗 План на выходные (MVP)
|
## 📊 Текущий статус
|
||||||
|
|
||||||
- Настройка проекта: Инициализация cargo и установка зависимостей Slint.
|
- Frameless-окно с прозрачным фоном: карточка парит над рабочим столом со скруглёнными углами
|
||||||
- Верстка UI: Создать чистое окно с кнопками установленных браузеров (Firefox, Chrome, Edge и т.д.).
|
- Тёмная и светлая темы (переключение кнопкой, env `BP_THEME=light`)
|
||||||
- Логика запуска: Реализовать передачу URL из аргументов командной строки в выбранный браузер.
|
- Перетаскивание окна за хедер, Esc — закрыть, центрирование при старте
|
||||||
- Регистрация в системе: Написать скрипт/инструкцию, как сделать приложение браузером по умолчанию.
|
- Дизайн-макеты: [refs/dark.html](refs/dark.html) / [refs/light.html](refs/light.html)
|
||||||
|
|
||||||
## 📝 Заметки для реализации
|
## 🔧 Разработка
|
||||||
|
|
||||||
- Использовать no-frame: true в Slint для кастомного дизайна окна.
|
План работ и цикл визуальной разработки с ИИ-агентом — [docs/dev-workflow.md](docs/dev-workflow.md).
|
||||||
- Для иконок браузеров использовать формат SVG (Slint отлично их рендерит).
|
|
||||||
- Конфигурацию путей к браузерам вынести в простой config.toml (в будущем).
|
```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 + страница настроек
|
||||||
|
|||||||
+13
-8
@@ -64,14 +64,19 @@ HTML-макеты → PNG: **headless Edge** (`--headless --screenshot`), он
|
|||||||
Текущее: окно с заголовком (специально, чтобы двигать мышью). Целевое: frameless +
|
Текущее: окно с заголовком (специально, чтобы двигать мышью). Целевое: frameless +
|
||||||
прозрачность + драг за хедер, как в макете.
|
прозрачность + драг за хедер, как в макете.
|
||||||
|
|
||||||
- [ ] `no-frame: true` + `background: transparent` в MainWindow
|
- [x] `no-frame: true` + `background: transparent` в MainWindow (+ `title: "bropicker"`)
|
||||||
- [ ] Прозрачность проверить первой же парой скриншотов (углы карточки vs ref-dark.png)
|
- [x] Прозрачность подтверждена скриншотами: карточка со скруглёнными углами парит
|
||||||
- [ ] Драг: TouchArea в хедере → callback `request-drag` → в Rust через
|
над рабочим столом, тёмная и светлая темы
|
||||||
`i-slint-backend-winit`: `window.with_winit_window(|w| w.drag_window())`
|
- [x] Драг: TouchArea на хедере (`pointer-event` down) → `request-drag` →
|
||||||
(код частично был в старом src/main.rs, восстановить из истории при необходимости)
|
`with_winit_window(drag_window)` через `i-slint-backend-winit`
|
||||||
- [ ] Центрирование окна при старте — вернуть `src/winit.rs` (он в истории main до коммита 464f4f0)
|
- [x] Центрирование окна при старте — `src/winit.rs` восстановлен из истории
|
||||||
- [ ] Esc — закрыть: `FocusScope { key-event }` или `window.close()`
|
(осторожно: git-редирект PowerShell создаёт UTF-16 — файл перезаписан в UTF-8)
|
||||||
- [ ] README привести в соответствие: стек Slint, скриншоты из `tools/out/`
|
- [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`
|
Коммит: `feat(ui): frameless transparent window with drag`
|
||||||
|
|
||||||
|
|||||||
+30
-13
@@ -1,9 +1,9 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
// use slint::BackendSelector;
|
use i_slint_backend_winit::WinitWindowAccessor;
|
||||||
|
|
||||||
// use crate::winit::center_window;
|
use crate::winit::center_window;
|
||||||
// mod winit;
|
mod winit;
|
||||||
|
|
||||||
slint::include_modules!();
|
slint::include_modules!();
|
||||||
|
|
||||||
@@ -41,6 +41,10 @@ fn init() -> State {
|
|||||||
|
|
||||||
let main_window = MainWindow::new().unwrap();
|
let main_window = MainWindow::new().unwrap();
|
||||||
|
|
||||||
|
if std::env::var("BP_THEME").as_deref() == Ok("light") {
|
||||||
|
main_window.global::<Theme>().set_is_dark(false);
|
||||||
|
}
|
||||||
|
|
||||||
main_window.set_current_url("ku6epxboctuk.github.io".into());
|
main_window.set_current_url("ku6epxboctuk.github.io".into());
|
||||||
main_window.set_remember_choice(true);
|
main_window.set_remember_choice(true);
|
||||||
main_window.set_always_ask(true);
|
main_window.set_always_ask(true);
|
||||||
@@ -67,17 +71,21 @@ fn init() -> State {
|
|||||||
println!("Open clicked");
|
println!("Open clicked");
|
||||||
});
|
});
|
||||||
|
|
||||||
// main_window.on_request_drag(move || {
|
main_window.on_request_drag({
|
||||||
// main_window.window().with_winit_window(|winit_window| {
|
let main_window = main_window.clone_strong();
|
||||||
// winit_window.drag_window().ok();
|
move || {
|
||||||
// });
|
main_window.window().with_winit_window(|winit_window| {
|
||||||
// });
|
winit_window.drag_window().ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// center_window(main_window.window());
|
main_window.on_esc_pressed({
|
||||||
|
let main_window = main_window.clone_strong();
|
||||||
main_window
|
move || {
|
||||||
.window()
|
let _ = main_window.window().hide();
|
||||||
.on_close_requested(move || slint::CloseRequestResponse::HideWindow);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
main_window.set_browser_model(browser_model.clone().into());
|
main_window.set_browser_model(browser_model.clone().into());
|
||||||
State {
|
State {
|
||||||
@@ -100,5 +108,14 @@ pub fn main() {
|
|||||||
|
|
||||||
let state = init();
|
let state = init();
|
||||||
let main_window = state.main_window.clone_strong();
|
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();
|
main_window.run().unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ use i_slint_backend_winit::winit::monitor::MonitorHandle;
|
|||||||
use i_slint_backend_winit::winit::window::Window;
|
use i_slint_backend_winit::winit::window::Window;
|
||||||
|
|
||||||
pub fn center_window(window: &slint::Window) {
|
pub fn center_window(window: &slint::Window) {
|
||||||
println!("Centering window");
|
|
||||||
if window.has_winit_window() {
|
if window.has_winit_window() {
|
||||||
println!("Has winit");
|
|
||||||
window.with_winit_window(|window: &Window| {
|
window.with_winit_window(|window: &Window| {
|
||||||
match window.current_monitor() {
|
match window.current_monitor() {
|
||||||
Some(monitor) => set_centered(window, &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_size = monitor.size();
|
||||||
let monitor_position = monitor.position();
|
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 };
|
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)
|
monitor_window_position.x = (monitor_position.x as f32 + (monitor_size.width as f32 * 0.5)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Out = "tools/out/app.png",
|
[string]$Out = "tools/out/app.png",
|
||||||
|
[switch]$Light,
|
||||||
[switch]$NoBuild
|
[switch]$NoBuild
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +17,9 @@ if (-not (Test-Path $exe)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($Light) { $env:BP_THEME = "light" }
|
||||||
$proc = Start-Process -FilePath $exe -WorkingDirectory $PWD -PassThru
|
$proc = Start-Process -FilePath $exe -WorkingDirectory $PWD -PassThru
|
||||||
|
Remove-Item Env:\BP_THEME -ErrorAction SilentlyContinue
|
||||||
Start-Sleep -Milliseconds 1800
|
Start-Sleep -Milliseconds 1800
|
||||||
|
|
||||||
& "$PSScriptRoot\shot.ps1" -ProcId $proc.Id -Out $Out
|
& "$PSScriptRoot\shot.ps1" -ProcId $proc.Id -Out $Out
|
||||||
|
|||||||
+8
-1
@@ -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 uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
||||||
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
[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 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)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
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
|
$rect = New-Object BpWin+RECT
|
||||||
[BpWin]::GetWindowRect($found, [ref]$rect) | Out-Null
|
[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
|
$w = $rect.Right - $rect.Left
|
||||||
$h = $rect.Bottom - $rect.Top
|
$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.Save($outAbs, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
$bmp.Dispose()
|
$bmp.Dispose()
|
||||||
|
|
||||||
Write-Host "OK $OutAbs (${w}x${h})"
|
Write-Host "OK $OutAbs (${w}x${h} @ $($rect.Left),$($rect.Top))"
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ import { GradientIcon } from "./gradient-icon.slint";
|
|||||||
|
|
||||||
export component Header inherits Rectangle {
|
export component Header inherits Rectangle {
|
||||||
in property <string> url;
|
in property <string> url;
|
||||||
|
callback request-drag();
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
mouse-cursor: move;
|
||||||
|
pointer-event(event) => {
|
||||||
|
if (event.kind == PointerEventKind.down) {
|
||||||
|
root.request-drag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+18
-1
@@ -5,7 +5,10 @@ import { Footer } from "./footer.slint";
|
|||||||
import { BrowserConfig } from "./types.slint";
|
import { BrowserConfig } from "./types.slint";
|
||||||
import { Theme } from "theme.slint";
|
import { Theme } from "theme.slint";
|
||||||
|
|
||||||
|
export { Theme }
|
||||||
|
|
||||||
export component MainWindow inherits Window {
|
export component MainWindow inherits Window {
|
||||||
|
title: "bropicker";
|
||||||
in property <[BrowserConfig]> browser-model: [];
|
in property <[BrowserConfig]> browser-model: [];
|
||||||
in property <string> current-url: "";
|
in property <string> current-url: "";
|
||||||
in-out property <bool> remember-choice: false;
|
in-out property <bool> remember-choice: false;
|
||||||
@@ -18,13 +21,26 @@ export component MainWindow inherits Window {
|
|||||||
callback settings-clicked();
|
callback settings-clicked();
|
||||||
callback open-clicked();
|
callback open-clicked();
|
||||||
callback request-drag();
|
callback request-drag();
|
||||||
|
callback esc-pressed();
|
||||||
|
|
||||||
// no-frame: true;
|
no-frame: true;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
width: 488px;
|
width: 488px;
|
||||||
height: 800px;
|
height: 800px;
|
||||||
default-font-family: "Segoe UI";
|
default-font-family: "Segoe UI";
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
init => { self.focus(); }
|
||||||
|
key-pressed(event) => {
|
||||||
|
if (event.text == Key.Escape) {
|
||||||
|
root.esc-pressed();
|
||||||
|
accept
|
||||||
|
} else {
|
||||||
|
reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
alignment: start;
|
alignment: start;
|
||||||
width: 488px;
|
width: 488px;
|
||||||
@@ -46,6 +62,7 @@ export component MainWindow inherits Window {
|
|||||||
|
|
||||||
Header {
|
Header {
|
||||||
url: current-url;
|
url: current-url;
|
||||||
|
request-drag => root.request-drag();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListView {
|
// ListView {
|
||||||
|
|||||||
Reference in New Issue
Block a user