From f425145e81037413da1ab7f7f7c3747898ce450c Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Thu, 7 May 2026 19:49:45 +0500 Subject: [PATCH] feat: impl design header --- Cargo.lock | 4 + Cargo.toml | 4 +- logos/external_link.svg | 6 + logos/globe.svg | 6 + refs/dark.html | 390 ++++++++++++++++++++++++++++------------ refs/light.html | 390 ++++++++++++++++++++++++++++------------ src/main.rs | 91 ++++++++-- src/winit.rs | 39 ++++ ui/browser_item.slint | 104 ++++++++--- ui/footer.slint | 41 +++++ ui/header.slint | 92 ++++++++++ ui/main.slint | 108 +++++++---- ui/types.slint | 7 +- 13 files changed, 978 insertions(+), 304 deletions(-) create mode 100644 logos/external_link.svg create mode 100644 logos/globe.svg create mode 100644 src/winit.rs create mode 100644 ui/footer.slint create mode 100644 ui/header.slint diff --git a/Cargo.lock b/Cargo.lock index d6c3cb4..647d586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,10 +606,12 @@ dependencies = [ name = "bropicker" version = "0.0.1" dependencies = [ + "i-slint-backend-winit", "serde", "serde_json", "slint", "slint-build", + "winit", ] [[package]] @@ -1815,6 +1817,7 @@ dependencies = [ "i-slint-common", "i-slint-core", "i-slint-renderer-femtovg", + "i-slint-renderer-skia", "i-slint-renderer-software", "input", "memmap2", @@ -4328,6 +4331,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "as-raw-xcb-connection", "bytemuck", + "drm", "fastrand", "js-sys", "memmap2", diff --git a/Cargo.toml b/Cargo.toml index 9268f0e..8539610 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,11 @@ edition = "2024" build = "build.rs" [dependencies] +i-slint-backend-winit = "1.16.1" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -slint = { version = "1.16.1", features = ["serde"] } +slint = { version = "1.16.1", features = ["renderer-skia", "serde"] } +winit = "0.30" [build-dependencies] slint-build = "1.16.1" diff --git a/logos/external_link.svg b/logos/external_link.svg new file mode 100644 index 0000000..68856d7 --- /dev/null +++ b/logos/external_link.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/logos/globe.svg b/logos/globe.svg new file mode 100644 index 0000000..0b383b3 --- /dev/null +++ b/logos/globe.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/refs/dark.html b/refs/dark.html index 74d9525..db6ae34 100644 --- a/refs/dark.html +++ b/refs/dark.html @@ -1,129 +1,293 @@ - + - - - - Choose Browser - Dark - - - - - + + + + Choose Browser - Dark + + + + + -
-
-
-
-
-
- - - - +
+
+
+
+
+
+ + + + + +
+
+

Выберите браузер

+

Для открытия ссылки

+
+
+
+ + + + -
-
-

Выберите браузер

-

Для открытия ссылки

+ ku6epxboctuk.github.io
-
- - - - - - ku6epxboctuk.github.io -
-
-
-
-
-
- -
- +
+
+ Work browser + По умолчанию +
+
+
+ + + +
+ - + - + - -
-
- -
- - -
+
-
-
- - + +
+
+

Esc - закрыть

+
+
-
-

Нажмите Esc для отмены

-
-
- - \ No newline at end of file + + + diff --git a/refs/light.html b/refs/light.html index d5e3c5e..013cc54 100644 --- a/refs/light.html +++ b/refs/light.html @@ -1,129 +1,293 @@ - + - - - - Choose Browser - Light - - - - - + + + + Choose Browser - Light + + + + + -
-
-
-
-
-
- - - - +
+
+
+
+
+
+ + + + + +
+
+

Выберите браузер

+

Для открытия ссылки

+
+
+
+ + + + -
-
-

Выберите браузер

-

Для открытия ссылки

+ ku6epxboctuk.github.io
-
- - - - - - ku6epxboctuk.github.io -
-
-
-
-
-
- -
- +
+
+ Work browser + По умолчанию +
+
+
+ + + +
+ - + - + - -
-
- -
- - -
+
-
-
- - + +
+
+

Esc - закрыть

+
+
-
-

Нажмите Esc для отмены

-
-
- - \ No newline at end of file + + + diff --git a/src/main.rs b/src/main.rs index 083c976..1fe84a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,92 @@ use std::rc::Rc; +// use slint::BackendSelector; + +// use crate::winit::center_window; +// mod winit; + slint::include_modules!(); fn init() -> State { let browser_model = Rc::new(slint::VecModel::::from(vec![ BrowserConfig { - icon: "chrome".into(), - path: "chrome.exe".into(), - flags: "profile 1".into(), + icon: "🦊".into(), + name: "Work browser".into(), + path: "firefox.exe".into(), + flags: "--profile work".into(), + is_default: true, }, BrowserConfig { - icon: "chrome".into(), + icon: "🔥".into(), + name: "Ku6epXBOCTuK".into(), path: "chrome.exe".into(), - flags: "profile 2".into(), + flags: "--profile personal".into(), + is_default: false, }, BrowserConfig { - icon: "chrome".into(), + icon: "🌐".into(), + name: "Chrome".into(), + path: "chrome.exe".into(), + flags: "".into(), + is_default: false, + }, + BrowserConfig { + icon: "🧘".into(), + name: "Zen Browser".into(), path: "zen.exe".into(), flags: "".into(), - }, - BrowserConfig { - icon: "chrome".into(), - path: "edge.exe".into(), - flags: "".into(), + is_default: false, }, ])); let main_window = MainWindow::new().unwrap(); - { - main_window.window().on_close_requested(move || { - todo!(); - }); - } + main_window.set_current_url("ku6epxboctuk.github.io".into()); + main_window.set_remember_choice(true); + main_window.set_always_ask(true); + + main_window.on_browser_selected({ + move |browser: BrowserConfig| { + println!("Selected browser: {:?} {:?}", browser.path, browser.flags); + } + }); + + main_window.on_toggle_remember(move |checked| { + println!("Remember choice: {}", checked); + }); + + main_window.on_toggle_always_ask(move |checked| { + println!("Always ask: {}", checked); + }); + + main_window.on_settings_clicked(move || { + println!("Settings clicked"); + }); + + main_window.on_feedback_clicked(move || { + println!("Feedback clicked"); + }); + + main_window.on_open_clicked(move || { + println!("Open clicked"); + }); + + // main_window.on_request_drag(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.set_browser_model(browser_model.clone().into()); State { main_window, - browser_model: browser_model, + browser_model, } } @@ -47,9 +96,13 @@ pub struct State { } pub fn main() { + // TODO: how to enable skia? not panics + // let _selector = BackendSelector::new() + // .backend_name("renderer_skia".into()) + // .select() + // .unwrap(); + let state = init(); let main_window = state.main_window.clone_strong(); - #[cfg(target_os = "android")] - STATE.with(|ui| *ui.borrow_mut() = Some(state)); main_window.run().unwrap(); } diff --git a/src/winit.rs b/src/winit.rs new file mode 100644 index 0000000..69211f5 --- /dev/null +++ b/src/winit.rs @@ -0,0 +1,39 @@ +use i_slint_backend_winit::WinitWindowAccessor; +use i_slint_backend_winit::winit::dpi::PhysicalPosition; +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), + None => (), + }; + + None as Option<()> + }); + } +} + +fn set_centered(window: &Window, monitor: &MonitorHandle) { + let window_size = window.outer_size(); + + 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) + - (window_size.width as f32 * 0.5)) as i32; + + monitor_window_position.y = (monitor_position.y as f32 + (monitor_size.height as f32 * 0.5) + - (window_size.height as f32 * 0.5)) as i32; + + window.set_outer_position(monitor_window_position); +} diff --git a/ui/browser_item.slint b/ui/browser_item.slint index e3e669f..668da24 100644 --- a/ui/browser_item.slint +++ b/ui/browser_item.slint @@ -1,44 +1,98 @@ import { - Button, HorizontalBox, + VerticalBox, } from "std-widgets.slint"; - import { BrowserConfig } from "./types.slint"; export component BrowserItem { - height: 80px; in property browser; + in property selected; + + height: 88px; + width: 100%; Rectangle { - height: 100%; width: 100%; - background: rgba(30, 41, 59, 0.3); + height: 100%; + border-radius: 16px; + background: selected ? #1e293b : #0f172a; + HorizontalBox { - height: 80px; - padding: 8px; - alignment: space-between; - Image { - height: 48px; + padding: 16px; + spacing: 16px; + + Rectangle { width: 48px; - source: @image-url("../logos/firefox_48x48.png"); + height: 48px; + border-radius: 12px; + background: #1e293b; + + Text { + width: 100%; + height: 100%; + horizontal-alignment: center; + vertical-alignment: center; + text: browser.icon; + font-size: 24px; + } } - Text { - text: browser.path; - vertical_alignment: center; - } - - Text { - text: browser.flags; - vertical_alignment: center; - } - - VerticalLayout { + VerticalBox { alignment: center; - Button { - text: "use"; + spacing: 4px; + + Text { + text: browser.name; + color: #f8fafc; + font-size: 16px; + font-weight: 500; + } + + if (browser.is-default): Rectangle { + height: 22px; + border-radius: 4px; + background: #3b82f6; + + Text { + text: "По умолчанию"; + color: #60a5fa; + font-size: 12px; + } + } + } + + VerticalBox { + alignment: center; + width: 24px; + height: 24px; + + if (selected): Rectangle { + width: 24px; + height: 24px; + border-radius: 12px; + background: #3b82f6; + + Text { + width: 100%; + height: 100%; + horizontal-alignment: center; + vertical-alignment: center; + text: "✓"; + color: #ffffff; + font-size: 14px; + } + } + + if (!selected): Text { + width: 100%; + height: 100%; + horizontal-alignment: center; + vertical-alignment: center; + text: ">"; + color: #64748b; + font-size: 18px; } } } } -} +} \ No newline at end of file diff --git a/ui/footer.slint b/ui/footer.slint new file mode 100644 index 0000000..8c23b50 --- /dev/null +++ b/ui/footer.slint @@ -0,0 +1,41 @@ +import { + Button, + HorizontalBox, + VerticalBox, +} from "std-widgets.slint"; + +export component Footer inherits Rectangle { + callback settings-clicked(); + callback feedback-clicked(); + callback open-clicked(); + + HorizontalBox { + spacing: 16px; + padding-top: 16px; + + HorizontalBox { + spacing: 16px; + + Button { + text: "⚙️ Настроить"; + clicked => { + root.settings-clicked(); + } + } + + Button { + text: "💬 Обратная связь"; + clicked => { + root.feedback-clicked(); + } + } + } + + Button { + text: "Открыть"; + clicked => { + root.open-clicked(); + } + } + } +} diff --git a/ui/header.slint b/ui/header.slint new file mode 100644 index 0000000..9aed392 --- /dev/null +++ b/ui/header.slint @@ -0,0 +1,92 @@ +import { BrowserConfig } from "./types.slint"; + + +export component Header inherits Rectangle { + in property url; + property icon-gradient: @linear-gradient(135deg, rgb(59, 130, 246), rgb(6, 182, 212)); + + VerticalLayout { + // width: 100%; + width: 448px; + padding: 12px; + padding-top: 20px; + spacing: 12px; + + HorizontalLayout { + spacing: 12px; + + Rectangle { + width: 48px; + height: 48px; + border-radius: 12px; + background: icon-gradient; + + Image { + source: @image-url("../logos/globe.svg"); + colorize: white; + } + } + + VerticalLayout { + alignment: center; + spacing: 4px; + + Text { + font-family: "Segpoe UI"; + text: "Выберите браузер"; + color: #ffffff; + font-size: 18px; + font-weight: 600; + } + + Text { + font-family: "Segpoe UI"; + text: "Для открытия ссылки"; + color: #94a3b8; + font-size: 14px; + } + } + } + + Rectangle { + height: 44px; + border-radius: 12px; + background: #1e293b; + border-width: 1px; + border-color: #334155; + + HorizontalLayout { + padding: 12px; + padding-left: 16px; + padding-right: 16px; + spacing: 8px; + alignment: start; + + Image { + source: @image-url("../logos/external_link.svg"); + width: 16px; + colorize: #64748b; + height: 16px; + } + + VerticalLayout { + padding-top: 2px; + + Text { + text: "ku6epxboctuk.github.io"; + color: #cbd5e1; + font-size: 14px; + font-family: "Consolas"; + } + } + } + } + } + + Rectangle { + height: 1px; + width: 100%; + y: parent.height - self.height; + background: #334155; + } +} diff --git a/ui/main.slint b/ui/main.slint index 32aa706..1d1e34c 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -1,43 +1,91 @@ -import { - SpinBox, - Button, - CheckBox, - Slider, - LineEdit, - ScrollView, - ListView, - HorizontalBox, - VerticalBox, - GridBox, - StandardButton, - Palette, -} from "std-widgets.slint"; +import { Button, CheckBox, ListView } from "std-widgets.slint"; import { BrowserItem } from "./browser_item.slint"; - +import { Header } from "./header.slint"; +import { Footer } from "./footer.slint"; import { BrowserConfig } from "./types.slint"; export component MainWindow inherits Window { in property <[BrowserConfig]> browser-model: []; + in property current-url: ""; + in-out property remember-choice: false; + in-out property always-ask: true; + in property selected-index: 0; - callback todo-added(string); - callback remove-done(); - callback update_todo(BrowserConfig); + callback browser-selected(BrowserConfig); + callback toggle-remember(bool); + callback toggle-always-ask(bool); + callback settings-clicked(); + callback feedback-clicked(); + callback open-clicked(); + callback request-drag(); - preferred-width: 400px; - preferred-height: 600px; + // no-frame: true; + background: transparent; + width: 488px; + height: 800px; + default-font-family: "Segoe UI"; - VerticalBox { - x: root.safe-area-insets.left; - y: root.safe-area-insets.top; - width: root.width - root.safe-area-insets.right - root.safe-area-insets.left; - height: root.height - root.safe-area-insets.bottom - root.safe-area-insets.top; + VerticalLayout { + alignment: start; + width: 488px; + height: 800px; + + padding: 20px; + + Rectangle { + max-width: 448px; + width: 100%; + height: 760px; + border-radius: 16px; + clip: true; + background: #0f172a; + border-width: 1px; + border-color: #334155; - list-view := ScrollView { VerticalLayout { - spacing: 10px; - padding: 5px; - for browser in root.browser-model: BrowserItem { - browser: browser; + padding: 12px; + + Header { + url: current-url; + } + + // ListView { + // max-height: 400px; + + // for browser[i] in root.browser-model: BrowserItem { + // browser: browser; + // selected: i == root.selected-index; + // } + // } + + // spacing: 12px; + // padding-left: 4px; + + // CheckBox { + // text: "Запомнить выбор для " + root.current-url; + // checked <=> root.remember-choice; + // toggled => { + // root.toggle-remember(root.remember-choice); + // } + // } + + // CheckBox { + // text: "Всегда спрашивать"; + // checked <=> root.always-ask; + // toggled => { + // root.toggle-always-ask(root.always-ask); + // } + // } + // } + + Rectangle { + vertical-stretch: 1; + } + + Footer { + settings-clicked => settings-clicked(); + feedback-clicked => feedback-clicked(); + open-clicked => open-clicked(); } } } diff --git a/ui/types.slint b/ui/types.slint index 098c26f..cecb4f2 100644 --- a/ui/types.slint +++ b/ui/types.slint @@ -1,7 +1,8 @@ - @rust-attr(derive(serde::Serialize, serde::Deserialize)) -export struct BrowserConfig { +export struct BrowserConfig { icon: string, + name: string, path: string, flags: string, -} + is-default: bool, +} \ No newline at end of file