Skip to content

Commit

Permalink
WIP status-line-applet
Browse files Browse the repository at this point in the history
status line: Custom widget for mouse input handling
  • Loading branch information
ids1024 committed Aug 25, 2023
1 parent 4678e95 commit 0e62b3d
Show file tree
Hide file tree
Showing 8 changed files with 841 additions and 55 deletions.
382 changes: 328 additions & 54 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"cosmic-applet-notifications",
"cosmic-applet-power",
"cosmic-applet-status-area",
"cosmic-applet-status-line",
"cosmic-applet-time",
"cosmic-applet-workspaces",
"cosmic-panel-button",
Expand Down
12 changes: 12 additions & 0 deletions cosmic-applet-status-line/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "cosmic-applet-status-line"
version = "0.1.0"
edition = "2021"

[dependencies]
delegate = "0.9"
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.27", features = ["io-util", "process", "sync"] }
tokio-stream = "0.1"
145 changes: 145 additions & 0 deletions cosmic-applet-status-line/src/bar_widget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use cosmic::{
iced::{self, widget, Length, Rectangle},
iced_core::{
clipboard::Clipboard,
event::{self, Event},
layout::{Layout, Limits, Node},
mouse,
renderer::Style,
touch,
widget::{
operation::{Operation, OperationOutputWrapper},
Tree, Widget,
},
Shell,
},
};

use crate::protocol::ClickEvent;

const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
const BTN_MIDDLE: u32 = 0x112;

/// Wraps a `Row` widget, handling mouse input
pub struct BarWidget<'a, Msg> {
pub row: widget::Row<'a, Msg, cosmic::Renderer>,
pub name_instance: Vec<(Option<&'a str>, Option<&'a str>)>,
pub on_press: fn(ClickEvent) -> Msg,
}

impl<'a, Msg> Widget<Msg, cosmic::Renderer> for BarWidget<'a, Msg> {
delegate::delegate! {
to self.row {
fn children(&self) -> Vec<Tree>;
fn diff(&mut self, tree: &mut Tree);
fn layout(&self, renderer: &cosmic::Renderer, limits: &Limits) -> Node;
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &cosmic::Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Msg>>,
);
fn draw(
&self,
state: &Tree,
renderer: &mut cosmic::Renderer,
theme: &cosmic::Theme,
style: &Style,
layout: Layout,
cursor: iced::mouse::Cursor,
viewport: &Rectangle,
);
}
}

fn width(&self) -> Length {
Widget::width(&self.row)
}

fn height(&self) -> Length {
Widget::height(&self.row)
}

fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: iced::mouse::Cursor,
renderer: &cosmic::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Msg>,
viewport: &Rectangle,
) -> event::Status {
if self.update(&event, layout, cursor, shell) == event::Status::Captured {
return event::Status::Captured;
}
self.row.on_event(
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
)
}
}

impl<'a, Msg> From<BarWidget<'a, Msg>> for cosmic::Element<'a, Msg>
where
Msg: 'a,
{
fn from(widget: BarWidget<'a, Msg>) -> cosmic::Element<'a, Msg> {
cosmic::Element::new(widget)
}
}

impl<'a, Msg> BarWidget<'a, Msg> {
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: iced::mouse::Cursor,
shell: &mut Shell<'_, Msg>,
) -> event::Status {
let Some(cursor_position) = cursor.position() else {
return event::Status::Ignored;
};

let (button, event_code) = match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => (1, BTN_LEFT),
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => (2, BTN_MIDDLE),
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => (3, BTN_RIGHT),
Event::Touch(touch::Event::FingerPressed { .. }) => (1, BTN_LEFT),
_ => {
return event::Status::Ignored;
}
};

let Some((n, bounds)) = layout.children().map(|x| x.bounds()).enumerate().find(|(_, bounds)| bounds.contains(cursor_position)) else {
return event::Status::Ignored;
};

let (name, instance) = self.name_instance.get(n).cloned().unwrap_or((None, None));

// TODO coordinate space? int conversion?
let x = cursor_position.x as u32;
let y = cursor_position.y as u32;
let relative_x = (cursor_position.x - bounds.x) as u32;
let relative_y = (cursor_position.y - bounds.y) as u32;
let width = bounds.width as u32;
let height = bounds.height as u32;

shell.publish((self.on_press)(ClickEvent {
name: name.map(str::to_owned),
instance: instance.map(str::to_owned),
x,
y,
button,
event: event_code,
relative_x,
relative_y,
width,
height,
}));

event::Status::Captured
}
}
100 changes: 100 additions & 0 deletions cosmic-applet-status-line/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// TODO: work vertically

use cosmic::{app, iced, iced_style::application, Theme};

mod bar_widget;
use bar_widget::BarWidget;
mod protocol;

#[derive(Clone, Debug)]
enum Msg {
Protocol(protocol::StatusLine),
ClickEvent(protocol::ClickEvent),
}

struct App {
core: app::Core,
status_line: protocol::StatusLine,
}

impl cosmic::Application for App {
type Message = Msg;
type Executor = cosmic::SingleThreadExecutor;
type Flags = ();
const APP_ID: &'static str = "com.system76.CosmicAppletStatusLine";

fn init(core: app::Core, _flags: ()) -> (Self, app::Command<Msg>) {
(
App {
core,
status_line: Default::default(),
},
iced::Command::none(),
)
}

fn core(&self) -> &app::Core {
&self.core
}

fn core_mut(&mut self) -> &mut app::Core {
&mut self.core
}

fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
Some(app::applet::style())
}

fn subscription(&self) -> iced::Subscription<Msg> {
protocol::subscription().map(Msg::Protocol)
}

fn update(&mut self, message: Msg) -> app::Command<Msg> {
match message {
Msg::Protocol(status_line) => {
println!("{:?}", status_line);
self.status_line = status_line;
}
Msg::ClickEvent(click_event) => {
println!("{:?}", click_event);
if self.status_line.click_events {
// TODO: pass click event to backend
}
}
}
iced::Command::none()
}

fn view(&self) -> cosmic::Element<Msg> {
let (block_views, name_instance): (Vec<_>, Vec<_>) = self
.status_line
.blocks
.iter()
.map(|block| {
(
block_view(block),
(block.name.as_deref(), block.instance.as_deref()),
)
})
.unzip();
BarWidget {
row: iced::widget::row(block_views),
name_instance,
on_press: Msg::ClickEvent,
}
.into()
}
}

// TODO seperator
fn block_view(block: &protocol::Block) -> cosmic::Element<Msg> {
let theme = block
.color
.map(cosmic::theme::Text::Color)
.unwrap_or(cosmic::theme::Text::Default);
cosmic::widget::text(&block.full_text).style(theme).into()
}

fn main() -> iced::Result {
app::applet::run::<App>(true, ())
}
111 changes: 111 additions & 0 deletions cosmic-applet-status-line/src/protocol/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/// TODO: if we get an error, terminate process with exit code 1. Let cosmic-panel restart us.
/// TODO: configuration for command? Use cosmic config system.
use cosmic::iced::{self, futures::FutureExt};
use std::{
fmt,
io::{BufRead, BufReader},
process::{self, Stdio},
thread,
};
use tokio::sync::mpsc;

mod serialization;
use serialization::Header;
pub use serialization::{Block, ClickEvent};

#[derive(Clone, Debug, Default)]
pub struct StatusLine {
pub blocks: Vec<Block>,
pub click_events: bool,
}

pub fn subscription() -> iced::Subscription<StatusLine> {
iced::subscription::run_with_id(
"status-cmd",
async {
let (sender, reciever) = mpsc::channel(20);
thread::spawn(move || {
let mut status_cmd = StatusCmd::spawn();
let mut deserializer =
serde_json::Deserializer::from_reader(&mut status_cmd.stdout);
deserialize_status_lines(&mut deserializer, |blocks| {
sender
.blocking_send(StatusLine {
blocks,
click_events: status_cmd.header.click_events,
})
.unwrap();
})
.unwrap();
status_cmd.wait();
});
tokio_stream::wrappers::ReceiverStream::new(reciever)
}
.flatten_stream(),
)
}

pub struct StatusCmd {
header: Header,
stdin: process::ChildStdin,
stdout: BufReader<process::ChildStdout>,
child: process::Child,
}

impl StatusCmd {
fn spawn() -> StatusCmd {
// XXX command
// XXX unwrap
let mut child = process::Command::new("i3status")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();

let mut stdout = BufReader::new(child.stdout.take().unwrap());
let mut header = String::new();
stdout.read_line(&mut header).unwrap();

StatusCmd {
header: serde_json::from_str(&header).unwrap(),
stdin: child.stdin.take().unwrap(),
stdout,
child,
}
}

fn wait(mut self) {
drop(self.stdin);
drop(self.stdout);
self.child.wait();
}
}

/// Deserialize a sequence of `Vec<Block>`s, executing a callback for each one.
/// Blocks thread until end of status line sequence.
fn deserialize_status_lines<'de, D: serde::Deserializer<'de>, F: FnMut(Vec<Block>)>(
deserializer: D,
cb: F,
) -> Result<(), D::Error> {
struct Visitor<F: FnMut(Vec<Block>)> {
cb: F,
}

impl<'de, F: FnMut(Vec<Block>)> serde::de::Visitor<'de> for Visitor<F> {
type Value = ();

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence of status lines")
}

fn visit_seq<S: serde::de::SeqAccess<'de>>(mut self, mut seq: S) -> Result<(), S::Error> {
while let Some(blocks) = seq.next_element()? {
(self.cb)(blocks);
}
Ok(())
}
}

let visitor = Visitor { cb };
deserializer.deserialize_seq(visitor)
}
Loading

0 comments on commit 0e62b3d

Please sign in to comment.