diff --git a/crates/biternet/src/app.rs b/crates/biternet/src/app.rs index 8406e2f..47a4854 100644 --- a/crates/biternet/src/app.rs +++ b/crates/biternet/src/app.rs @@ -50,6 +50,7 @@ pub enum Message { WindowClosed(window::Id), CloseRequested(window::Id), CloseWindow, + DragWindow, Tray(tray::Action), UsernameChanged(String), @@ -165,6 +166,11 @@ impl App { return Task::done(Message::CloseRequested(id)); } } + Message::DragWindow => { + if let Some(id) = self.window_id { + return window::drag(id); + } + } Message::Tray(tray::Action::ShowWindow) => return self.show_window(), Message::Tray(tray::Action::Exit) => return iced::exit(), @@ -386,6 +392,11 @@ impl App { self.overlay.is_none() && matches!(self.session, Session::Online(_)) } + // The borderless window can only be moved by dragging in-app surfaces. + pub fn drag(&self) -> Option { + self.window_id.map(|_| Message::DragWindow) + } + fn login(&mut self, automatic: bool) -> Task { self.session = Session::LoggingIn { automatic }; self.manual_logout = false; @@ -435,12 +446,15 @@ impl App { let icon = image::load_from_memory(APP_ICON).unwrap().into_rgba8(); let icon = window::icon::from_rgba(icon.to_vec(), icon.width(), icon.height()).unwrap(); + // The window is borderless: the in-app header provides the close + // button and dragging, so the native title bar is redundant. let size = Size::new(252.0, 332.0); let (id, open) = window::open(window::Settings { size, min_size: Some(size), max_size: Some(size), resizable: false, + decorations: false, position: window::Position::Centered, icon: Some(icon), exit_on_close_request: false, diff --git a/crates/biternet/src/components/dialog.rs b/crates/biternet/src/components/dialog.rs index 14d77aa..a40e085 100644 --- a/crates/biternet/src/components/dialog.rs +++ b/crates/biternet/src/components/dialog.rs @@ -1,5 +1,5 @@ use iced::widget::{button, container, mouse_area, opaque, row, space, stack, text}; -use iced::{Alignment, Color, Element, Fill}; +use iced::{Alignment, Color, Element, Fill, Padding}; use crate::app::Message; use crate::theme::{self, HEADING}; @@ -33,23 +33,41 @@ pub fn view<'a>(base: Element<'a, Message>, content: Element<'a, Message>) -> El .into() } -pub fn header<'a>(title: &'a str, feedback: Option>) -> Element<'a, Message> { - let mut header = row![text(title).size(18).font(HEADING).width(Fill)] - .spacing(8) +pub fn header<'a>( + title: &'a str, + feedback: Option>, + drag: Option, +) -> Element<'a, Message> { + // Only the title strip starts a drag: wrapping the close button would let + // its release-on-press trigger alongside the window drag. + let mut strip = row![text(title).size(18).font(HEADING)] + .padding(Padding { + right: 8.0, + ..Padding::ZERO + }) + .width(Fill) .align_y(Alignment::Center); if let Some(feedback) = feedback { - header = header.push(feedback); + strip = strip.push(feedback); } - header - .push( - button(text("×").size(20).height(Fill).width(Fill).center()) - .on_press(Message::OverlayOpened(None)) - .style(theme::quiet_button) - .width(28) - .height(28) - .padding(0), - ) - .into() + let mut area = mouse_area(strip); + + if let Some(drag) = drag { + area = area.on_press(drag); + } + + row![ + area, + button(text("×").size(20).height(Fill).width(Fill).center()) + .on_press(Message::OverlayOpened(None)) + .style(theme::quiet_button) + .width(28) + .height(28) + .padding(0), + ] + .spacing(8) + .align_y(Alignment::Center) + .into() } diff --git a/crates/biternet/src/components/feedback.rs b/crates/biternet/src/components/feedback.rs index db21e92..50021c8 100644 --- a/crates/biternet/src/components/feedback.rs +++ b/crates/biternet/src/components/feedback.rs @@ -1,7 +1,7 @@ use iced::widget::{button, column, container, row, scrollable, space, text}; use iced::{Alignment, Element, Fill, Padding}; -use crate::app::{Message, Notice, Overlay}; +use crate::app::{App, Message, Notice, Overlay}; use crate::components::dialog; use crate::theme; @@ -45,14 +45,14 @@ pub fn bar(notice: Option<&Notice>) -> Element<'_, Message> { .into() } -pub fn details(notice: &Notice) -> Element<'_, Message> { +pub fn details<'a>(app: &'a App, notice: &'a Notice) -> Element<'a, Message> { let detail = notice .detail .as_ref() .expect("notice dialog requires details"); column![ - dialog::header(notice.title, None), + dialog::header(notice.title, None, app.drag()), space().height(16), container( scrollable( diff --git a/crates/biternet/src/components/header.rs b/crates/biternet/src/components/header.rs index 522e918..ebae416 100644 --- a/crates/biternet/src/components/header.rs +++ b/crates/biternet/src/components/header.rs @@ -1,4 +1,4 @@ -use iced::widget::{button, container, row, space, text}; +use iced::widget::{button, container, mouse_area, row, space, text}; use iced::{Alignment, Border, Color, Element, Fill}; use crate::app::{App, Message, Overlay}; @@ -14,7 +14,7 @@ pub fn view(app: &App) -> Element<'_, Message> { Session::Offline => ("未连接", MUTED, Color::from_rgb8(238, 241, 247)), }; - row![ + let bar = row![ text("校园网").size(18).font(HEADING), container(text(label).size(11).color(color)) .padding([4, 8]) @@ -45,6 +45,13 @@ pub fn view(app: &App) -> Element<'_, Message> { ] .spacing(8) .align_y(Alignment::Center) - .height(28) - .into() + .height(28); + + let mut area = mouse_area(bar); + + if let Some(drag) = app.drag() { + area = area.on_press(drag); + } + + area.into() } diff --git a/crates/biternet/src/views/mod.rs b/crates/biternet/src/views/mod.rs index 6bf50e0..c2197ac 100644 --- a/crates/biternet/src/views/mod.rs +++ b/crates/biternet/src/views/mod.rs @@ -43,11 +43,13 @@ pub fn view(app: &App) -> Element<'_, Message> { let content = match app.overlay { Some(Overlay::Settings) => settings::view(app), Some(Overlay::Update) => update::view( + app, app.update_info .as_ref() .expect("update dialog requires an available release"), ), Some(Overlay::Notice) => feedback::details( + app, app.notice .as_ref() .expect("notice dialog requires a notice"), diff --git a/crates/biternet/src/views/settings.rs b/crates/biternet/src/views/settings.rs index 5371fa9..dc2e62f 100644 --- a/crates/biternet/src/views/settings.rs +++ b/crates/biternet/src/views/settings.rs @@ -23,7 +23,7 @@ pub fn view(app: &App) -> Element<'_, Message> { }; column![ - dialog::header("设置", Some(saved)), + dialog::header("设置", Some(saved), app.drag()), space().height(16), row![ text("开机启动").size(13).width(Fill), diff --git a/crates/biternet/src/views/update.rs b/crates/biternet/src/views/update.rs index b5e780c..f292440 100644 --- a/crates/biternet/src/views/update.rs +++ b/crates/biternet/src/views/update.rs @@ -1,14 +1,14 @@ use iced::widget::{button, column, row, space, text}; use iced::{Element, Fill}; -use crate::app::Message; +use crate::app::{App, Message}; use crate::components::dialog; use crate::theme::{self, HEADING, MUTED}; use crate::update::UpdateInfo; -pub fn view(info: &UpdateInfo) -> Element<'_, Message> { +pub fn view<'a>(app: &'a App, info: &'a UpdateInfo) -> Element<'a, Message> { column![ - dialog::header("发现新版本", None), + dialog::header("发现新版本", None, app.drag()), space().height(18), text(format!("v{}", info.version)).size(26).font(HEADING), space().height(6),