Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: widget support #1771

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,458 changes: 721 additions & 737 deletions backend/Cargo.lock

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions backend/nyanpasu-egui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ name = "nyanpasu-network-statistic-widget-small"
path = "./src/small.rs"

[dependencies]
eframe = "0.29.1"
egui_extras = { version = "*", features = ["all_loaders"] }
eframe = "0.30.0"
egui_extras = { version = "0.30.0", features = ["all_loaders"] }
parking_lot = "0.12"
image = { version = "0.25.5", features = ["jpeg", "png"] }
humansize = "2.1.3"
# for svg currentColor replacement
resvg = "0.44.0" # for svg rendering
usvg = "0.44.0" # for svg parsing
csscolorparser = "0.7.0" # for color conversion
resvg = "0.44.0" # for svg rendering
usvg = "0.44.0" # for svg parsing
csscolorparser = "0.7.0" # for color conversion
ipc-channel = "0.19.0" # for IPC between the Widget process and the GUI process
serde = { version = "1.0.216", features = ["derive"] }
anyhow = "1"
66 changes: 66 additions & 0 deletions backend/nyanpasu-egui/src/ipc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};

use crate::widget::network_statistic_large::LogoPreset;

#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct StatisticMessage {
pub download_total: u64,
pub upload_total: u64,
pub download_speed: u64,
pub upload_speed: u64,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum Message {
Stop,
UpdateStatistic(StatisticMessage),
UpdateLogo(LogoPreset),
}

pub struct IPCServer {
oneshot_server: Option<ipc::IpcOneShotServer<IpcSender<Message>>>,
tx: Option<IpcSender<Message>>,
}

impl IPCServer {
pub fn is_connected(&self) -> bool {
self.tx.is_some()
}

pub fn connect(&mut self) -> anyhow::Result<()> {
if self.oneshot_server.is_none() {
anyhow::bail!("IPC server is already initialized");
}

let (_, tx) = self.oneshot_server.take().unwrap().accept()?;
self.tx = Some(tx);
Ok(())
}

pub fn into_tx(self) -> Option<IpcSender<Message>> {
self.tx
}
}

pub fn create_ipc_server() -> anyhow::Result<(IPCServer, String)> {
let (oneshot_server, oneshot_server_name) = ipc::IpcOneShotServer::new()?;
Ok((
IPCServer {
oneshot_server: Some(oneshot_server),
tx: None,
},
oneshot_server_name,
))
}

pub(crate) fn setup_ipc_receiver(name: &str) -> anyhow::Result<IpcReceiver<Message>> {
let oneshot_sender: IpcSender<IpcSender<Message>> = ipc::IpcSender::connect(name.to_string())?;
let (tx, rx) = ipc::channel()?;
oneshot_sender.send(tx)?;
Ok(rx)
}

pub(crate) fn setup_ipc_receiver_with_env() -> anyhow::Result<IpcReceiver<Message>> {
let name = std::env::var("NYANPASU_EGUI_IPC_SERVER")?;
setup_ipc_receiver(&name)
}
5 changes: 5 additions & 0 deletions backend/nyanpasu-egui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
#![feature(trait_alias)]

mod utils;
pub mod widget;
pub mod ipc;


37 changes: 37 additions & 0 deletions backend/nyanpasu-egui/src/widget/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
use eframe::{App, AppCreator, CreationContext};
use std::error::Error;
use std::sync::mpsc::Receiver;
use std::sync::Arc;

trait EframeAppCreator<'app, T: Send + Sync + Sized> =
FnOnce(
&CreationContext<'_>,
)
-> Result<Box<dyn 'app + AppWithListener<T>>, Box<dyn std::error::Error + Send + Sync>>;

pub trait AppWithListener<T: Send + Sync + Sized>: App + Listener<T> {}

pub mod network_statistic_large;
pub mod network_statistic_small;

pub use network_statistic_large::NyanpasuNetworkStatisticLargeWidget;
pub use network_statistic_small::NyanpasuNetworkStatisticSmallWidget;

trait Listener<T: Send + Sync + Sized> {
fn listen(&self, event: WidgetEvent<T>);
}

pub struct WidgetEvent<T: Send + Sync + Sized> {
pub id: u32,
pub payload: T,
}

pub enum WidgetEventPayload<T: Send + Sync + Sized> {
/// Terminate the egui window
Terminate,
/// User defined event
Custom(T),
}

// pub fn launch_widget<'app, T: Send + Sync + Sized, A: EframeAppCreator<'app, T>>(
// name: &str,
// opts: eframe::NativeOptions,
// creator: A,
// ) -> std::io::Result<Receiver<WidgetEvent<T>>> {
// let (tx, rx) = mpsc::channel();
// }
116 changes: 85 additions & 31 deletions backend/nyanpasu-egui/src/widget/network_statistic_large.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::OnceLock;

use crate::ipc::Message;
use crate::utils::svg::{render_svg_with_current_color_replace, SvgExt};
use eframe::egui::{
self, style::Selection, Color32, Id, Image, Layout, Margin, Rounding, Sense, Stroke, Style,
TextureOptions, Theme, Vec2, ViewportCommand, Visuals,
};
use parking_lot::RwLock;

// Presets
const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;
Expand Down Expand Up @@ -42,7 +46,9 @@ fn setup_fonts(ctx: &egui::Context) {

fonts.font_data.insert(
"Inter".to_owned(),
egui::FontData::from_static(include_bytes!("../../assets/Inter-Regular.ttf")),
Arc::new(egui::FontData::from_static(include_bytes!(
"../../assets/Inter-Regular.ttf"
))),
);

fonts
Expand Down Expand Up @@ -85,45 +91,52 @@ fn use_dark_purple_accent(style: &mut Style) {
};
}

#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum LogoPreset {
#[default]
Default,
System,
Tun,
}

#[derive(Debug, Default)]
pub struct StatisticMessage {
download_total: u64,
upload_total: u64,
download_speed: u64,
upload_speed: u64,
}

#[derive(Debug)]
pub enum Message {
UpdateStatistic(StatisticMessage),
UpdateLogo(LogoPreset),
}

#[derive(Debug)]
pub struct NyanpasuNetworkStatisticLargeWidget {
pub struct NyanpasuNetworkStatisticLargeWidgetInner {
// data fields
logo_preset: LogoPreset,
download_total: u64,
upload_total: u64,
download_speed: u64,
upload_speed: u64,

// eframe ctx
egui_ctx: OnceLock<egui::Context>,
}

impl Default for NyanpasuNetworkStatisticLargeWidget {
impl Default for NyanpasuNetworkStatisticLargeWidgetInner {
fn default() -> Self {
Self {
logo_preset: LogoPreset::Default,
download_total: 0,
upload_total: 0,
download_speed: 0,
upload_speed: 0,
egui_ctx: OnceLock::new(),
}
}
}

#[derive(Debug, Clone)]
pub struct NyanpasuNetworkStatisticLargeWidget {
inner: Arc<RwLock<NyanpasuNetworkStatisticLargeWidgetInner>>,
}

impl Default for NyanpasuNetworkStatisticLargeWidget {
fn default() -> Self {
Self {
inner: Arc::new(RwLock::new(
NyanpasuNetworkStatisticLargeWidgetInner::default(),
)),
}
}
}
Expand All @@ -134,23 +147,59 @@ impl NyanpasuNetworkStatisticLargeWidget {
setup_fonts(&cc.egui_ctx);
setup_custom_style(&cc.egui_ctx);
egui_extras::install_image_loaders(&cc.egui_ctx);
Self::default()
let rx = crate::ipc::setup_ipc_receiver_with_env().unwrap();
let widget = Self::default();
let this = widget.clone();
std::thread::spawn(move || {
while let Ok(msg) = rx.recv() {
let _ = this.handle_message(msg);
}
});
widget
}

pub fn run() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([206.0, 60.0])
.with_decorations(false)
.with_transparent(true)
.with_always_on_top()
.with_drag_and_drop(true)
.with_resizable(false)
.with_taskbar(false),
..Default::default()
};
eframe::run_native(
"Nyanpasu Network Statistic Widget",
options,
Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticLargeWidget::new(cc)))),
)
}

pub fn handle_message(&mut self, msg: Message) -> bool {
pub fn handle_message(&self, msg: Message) -> anyhow::Result<()> {
let mut this = self.inner.write();
match msg {
Message::UpdateStatistic(statistic) => {
self.download_total = statistic.download_total;
self.upload_total = statistic.upload_total;
self.download_speed = statistic.download_speed;
self.upload_speed = statistic.upload_speed;
true
this.download_total = statistic.download_total;
this.upload_total = statistic.upload_total;
this.download_speed = statistic.download_speed;
this.upload_speed = statistic.upload_speed;
}
Message::UpdateLogo(logo_preset) => {
self.logo_preset = logo_preset;
true
this.logo_preset = logo_preset;
}
Message::Stop => match this.egui_ctx.get() {
Some(ctx) => {
ctx.send_viewport_cmd(ViewportCommand::Close);
}
None => {
eprintln!("Failed to close the widget: eframe context is not initialized");
std::process::exit(1);
}
},
}
Ok(())
}
}

Expand All @@ -160,6 +209,11 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
}

fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// setup ctx
let egui_ctx = ctx.clone();
let this = self.inner.read();
let _ = this.egui_ctx.get_or_init(move || egui_ctx);

let visuals = &ctx.style().visuals;

egui::CentralPanel::default()
Expand Down Expand Up @@ -229,7 +283,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
let width = ui.available_width();
let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
ui.label(humansize::format_size(self.download_total, humansize::DECIMAL));
ui.label(humansize::format_size(this.download_total, humansize::DECIMAL));
});
});
});
Expand Down Expand Up @@ -262,7 +316,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
let width = ui.available_width();
let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
ui.label(format!("{}/s", humansize::format_size(self.download_speed, humansize::DECIMAL)));
ui.label(format!("{}/s", humansize::format_size(this.download_speed, humansize::DECIMAL)));
});
});
})
Expand Down Expand Up @@ -300,7 +354,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
let width = ui.available_width();
let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
ui.label(humansize::format_size(self.upload_total, humansize::DECIMAL));
ui.label(humansize::format_size(this.upload_total, humansize::DECIMAL));
});
});
});
Expand Down Expand Up @@ -333,7 +387,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
let width = ui.available_width();
let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
ui.label(format!("{}/s", humansize::format_size(self.upload_speed, humansize::DECIMAL)));
ui.label(format!("{}/s", humansize::format_size(this.upload_speed, humansize::DECIMAL)));
});
});
})
Expand Down
5 changes: 4 additions & 1 deletion backend/nyanpasu-egui/src/widget/network_statistic_small.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::sync::Arc;
use std::sync::LazyLock;

use crate::utils::svg::{render_svg_with_current_color_replace, SvgExt};
Expand Down Expand Up @@ -37,7 +38,9 @@ fn setup_fonts(ctx: &egui::Context) {

fonts.font_data.insert(
"Inter".to_owned(),
egui::FontData::from_static(include_bytes!("../../assets/Inter-Regular.ttf")),
Arc::new(egui::FontData::from_static(include_bytes!(
"../../assets/Inter-Regular.ttf"
))),
);

fonts
Expand Down
Loading