Skip to content

Commit 0e62b3d

Browse files
committed
WIP status-line-applet
status line: Custom widget for mouse input handling
1 parent 4678e95 commit 0e62b3d

File tree

8 files changed

+841
-55
lines changed

8 files changed

+841
-55
lines changed

Cargo.lock

Lines changed: 328 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"cosmic-applet-notifications",
1111
"cosmic-applet-power",
1212
"cosmic-applet-status-area",
13+
"cosmic-applet-status-line",
1314
"cosmic-applet-time",
1415
"cosmic-applet-workspaces",
1516
"cosmic-panel-button",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "cosmic-applet-status-line"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
delegate = "0.9"
8+
libcosmic = { git = "https://github.com/pop-os/libcosmic/", branch = "master", default-features = false, features = ["tokio", "wayland", "applet"] }
9+
serde = { version = "1.0", features = ["derive"] }
10+
serde_json = "1.0"
11+
tokio = { version = "1.27", features = ["io-util", "process", "sync"] }
12+
tokio-stream = "0.1"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use cosmic::{
2+
iced::{self, widget, Length, Rectangle},
3+
iced_core::{
4+
clipboard::Clipboard,
5+
event::{self, Event},
6+
layout::{Layout, Limits, Node},
7+
mouse,
8+
renderer::Style,
9+
touch,
10+
widget::{
11+
operation::{Operation, OperationOutputWrapper},
12+
Tree, Widget,
13+
},
14+
Shell,
15+
},
16+
};
17+
18+
use crate::protocol::ClickEvent;
19+
20+
const BTN_LEFT: u32 = 0x110;
21+
const BTN_RIGHT: u32 = 0x111;
22+
const BTN_MIDDLE: u32 = 0x112;
23+
24+
/// Wraps a `Row` widget, handling mouse input
25+
pub struct BarWidget<'a, Msg> {
26+
pub row: widget::Row<'a, Msg, cosmic::Renderer>,
27+
pub name_instance: Vec<(Option<&'a str>, Option<&'a str>)>,
28+
pub on_press: fn(ClickEvent) -> Msg,
29+
}
30+
31+
impl<'a, Msg> Widget<Msg, cosmic::Renderer> for BarWidget<'a, Msg> {
32+
delegate::delegate! {
33+
to self.row {
34+
fn children(&self) -> Vec<Tree>;
35+
fn diff(&mut self, tree: &mut Tree);
36+
fn layout(&self, renderer: &cosmic::Renderer, limits: &Limits) -> Node;
37+
fn operate(
38+
&self,
39+
tree: &mut Tree,
40+
layout: Layout<'_>,
41+
renderer: &cosmic::Renderer,
42+
operation: &mut dyn Operation<OperationOutputWrapper<Msg>>,
43+
);
44+
fn draw(
45+
&self,
46+
state: &Tree,
47+
renderer: &mut cosmic::Renderer,
48+
theme: &cosmic::Theme,
49+
style: &Style,
50+
layout: Layout,
51+
cursor: iced::mouse::Cursor,
52+
viewport: &Rectangle,
53+
);
54+
}
55+
}
56+
57+
fn width(&self) -> Length {
58+
Widget::width(&self.row)
59+
}
60+
61+
fn height(&self) -> Length {
62+
Widget::height(&self.row)
63+
}
64+
65+
fn on_event(
66+
&mut self,
67+
tree: &mut Tree,
68+
event: Event,
69+
layout: Layout<'_>,
70+
cursor: iced::mouse::Cursor,
71+
renderer: &cosmic::Renderer,
72+
clipboard: &mut dyn Clipboard,
73+
shell: &mut Shell<'_, Msg>,
74+
viewport: &Rectangle,
75+
) -> event::Status {
76+
if self.update(&event, layout, cursor, shell) == event::Status::Captured {
77+
return event::Status::Captured;
78+
}
79+
self.row.on_event(
80+
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
81+
)
82+
}
83+
}
84+
85+
impl<'a, Msg> From<BarWidget<'a, Msg>> for cosmic::Element<'a, Msg>
86+
where
87+
Msg: 'a,
88+
{
89+
fn from(widget: BarWidget<'a, Msg>) -> cosmic::Element<'a, Msg> {
90+
cosmic::Element::new(widget)
91+
}
92+
}
93+
94+
impl<'a, Msg> BarWidget<'a, Msg> {
95+
fn update(
96+
&mut self,
97+
event: &Event,
98+
layout: Layout<'_>,
99+
cursor: iced::mouse::Cursor,
100+
shell: &mut Shell<'_, Msg>,
101+
) -> event::Status {
102+
let Some(cursor_position) = cursor.position() else {
103+
return event::Status::Ignored;
104+
};
105+
106+
let (button, event_code) = match event {
107+
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => (1, BTN_LEFT),
108+
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => (2, BTN_MIDDLE),
109+
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => (3, BTN_RIGHT),
110+
Event::Touch(touch::Event::FingerPressed { .. }) => (1, BTN_LEFT),
111+
_ => {
112+
return event::Status::Ignored;
113+
}
114+
};
115+
116+
let Some((n, bounds)) = layout.children().map(|x| x.bounds()).enumerate().find(|(_, bounds)| bounds.contains(cursor_position)) else {
117+
return event::Status::Ignored;
118+
};
119+
120+
let (name, instance) = self.name_instance.get(n).cloned().unwrap_or((None, None));
121+
122+
// TODO coordinate space? int conversion?
123+
let x = cursor_position.x as u32;
124+
let y = cursor_position.y as u32;
125+
let relative_x = (cursor_position.x - bounds.x) as u32;
126+
let relative_y = (cursor_position.y - bounds.y) as u32;
127+
let width = bounds.width as u32;
128+
let height = bounds.height as u32;
129+
130+
shell.publish((self.on_press)(ClickEvent {
131+
name: name.map(str::to_owned),
132+
instance: instance.map(str::to_owned),
133+
x,
134+
y,
135+
button,
136+
event: event_code,
137+
relative_x,
138+
relative_y,
139+
width,
140+
height,
141+
}));
142+
143+
event::Status::Captured
144+
}
145+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// TODO: work vertically
2+
3+
use cosmic::{app, iced, iced_style::application, Theme};
4+
5+
mod bar_widget;
6+
use bar_widget::BarWidget;
7+
mod protocol;
8+
9+
#[derive(Clone, Debug)]
10+
enum Msg {
11+
Protocol(protocol::StatusLine),
12+
ClickEvent(protocol::ClickEvent),
13+
}
14+
15+
struct App {
16+
core: app::Core,
17+
status_line: protocol::StatusLine,
18+
}
19+
20+
impl cosmic::Application for App {
21+
type Message = Msg;
22+
type Executor = cosmic::SingleThreadExecutor;
23+
type Flags = ();
24+
const APP_ID: &'static str = "com.system76.CosmicAppletStatusLine";
25+
26+
fn init(core: app::Core, _flags: ()) -> (Self, app::Command<Msg>) {
27+
(
28+
App {
29+
core,
30+
status_line: Default::default(),
31+
},
32+
iced::Command::none(),
33+
)
34+
}
35+
36+
fn core(&self) -> &app::Core {
37+
&self.core
38+
}
39+
40+
fn core_mut(&mut self) -> &mut app::Core {
41+
&mut self.core
42+
}
43+
44+
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
45+
Some(app::applet::style())
46+
}
47+
48+
fn subscription(&self) -> iced::Subscription<Msg> {
49+
protocol::subscription().map(Msg::Protocol)
50+
}
51+
52+
fn update(&mut self, message: Msg) -> app::Command<Msg> {
53+
match message {
54+
Msg::Protocol(status_line) => {
55+
println!("{:?}", status_line);
56+
self.status_line = status_line;
57+
}
58+
Msg::ClickEvent(click_event) => {
59+
println!("{:?}", click_event);
60+
if self.status_line.click_events {
61+
// TODO: pass click event to backend
62+
}
63+
}
64+
}
65+
iced::Command::none()
66+
}
67+
68+
fn view(&self) -> cosmic::Element<Msg> {
69+
let (block_views, name_instance): (Vec<_>, Vec<_>) = self
70+
.status_line
71+
.blocks
72+
.iter()
73+
.map(|block| {
74+
(
75+
block_view(block),
76+
(block.name.as_deref(), block.instance.as_deref()),
77+
)
78+
})
79+
.unzip();
80+
BarWidget {
81+
row: iced::widget::row(block_views),
82+
name_instance,
83+
on_press: Msg::ClickEvent,
84+
}
85+
.into()
86+
}
87+
}
88+
89+
// TODO seperator
90+
fn block_view(block: &protocol::Block) -> cosmic::Element<Msg> {
91+
let theme = block
92+
.color
93+
.map(cosmic::theme::Text::Color)
94+
.unwrap_or(cosmic::theme::Text::Default);
95+
cosmic::widget::text(&block.full_text).style(theme).into()
96+
}
97+
98+
fn main() -> iced::Result {
99+
app::applet::run::<App>(true, ())
100+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/// TODO: if we get an error, terminate process with exit code 1. Let cosmic-panel restart us.
2+
/// TODO: configuration for command? Use cosmic config system.
3+
use cosmic::iced::{self, futures::FutureExt};
4+
use std::{
5+
fmt,
6+
io::{BufRead, BufReader},
7+
process::{self, Stdio},
8+
thread,
9+
};
10+
use tokio::sync::mpsc;
11+
12+
mod serialization;
13+
use serialization::Header;
14+
pub use serialization::{Block, ClickEvent};
15+
16+
#[derive(Clone, Debug, Default)]
17+
pub struct StatusLine {
18+
pub blocks: Vec<Block>,
19+
pub click_events: bool,
20+
}
21+
22+
pub fn subscription() -> iced::Subscription<StatusLine> {
23+
iced::subscription::run_with_id(
24+
"status-cmd",
25+
async {
26+
let (sender, reciever) = mpsc::channel(20);
27+
thread::spawn(move || {
28+
let mut status_cmd = StatusCmd::spawn();
29+
let mut deserializer =
30+
serde_json::Deserializer::from_reader(&mut status_cmd.stdout);
31+
deserialize_status_lines(&mut deserializer, |blocks| {
32+
sender
33+
.blocking_send(StatusLine {
34+
blocks,
35+
click_events: status_cmd.header.click_events,
36+
})
37+
.unwrap();
38+
})
39+
.unwrap();
40+
status_cmd.wait();
41+
});
42+
tokio_stream::wrappers::ReceiverStream::new(reciever)
43+
}
44+
.flatten_stream(),
45+
)
46+
}
47+
48+
pub struct StatusCmd {
49+
header: Header,
50+
stdin: process::ChildStdin,
51+
stdout: BufReader<process::ChildStdout>,
52+
child: process::Child,
53+
}
54+
55+
impl StatusCmd {
56+
fn spawn() -> StatusCmd {
57+
// XXX command
58+
// XXX unwrap
59+
let mut child = process::Command::new("i3status")
60+
.stdin(Stdio::piped())
61+
.stdout(Stdio::piped())
62+
.spawn()
63+
.unwrap();
64+
65+
let mut stdout = BufReader::new(child.stdout.take().unwrap());
66+
let mut header = String::new();
67+
stdout.read_line(&mut header).unwrap();
68+
69+
StatusCmd {
70+
header: serde_json::from_str(&header).unwrap(),
71+
stdin: child.stdin.take().unwrap(),
72+
stdout,
73+
child,
74+
}
75+
}
76+
77+
fn wait(mut self) {
78+
drop(self.stdin);
79+
drop(self.stdout);
80+
self.child.wait();
81+
}
82+
}
83+
84+
/// Deserialize a sequence of `Vec<Block>`s, executing a callback for each one.
85+
/// Blocks thread until end of status line sequence.
86+
fn deserialize_status_lines<'de, D: serde::Deserializer<'de>, F: FnMut(Vec<Block>)>(
87+
deserializer: D,
88+
cb: F,
89+
) -> Result<(), D::Error> {
90+
struct Visitor<F: FnMut(Vec<Block>)> {
91+
cb: F,
92+
}
93+
94+
impl<'de, F: FnMut(Vec<Block>)> serde::de::Visitor<'de> for Visitor<F> {
95+
type Value = ();
96+
97+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
98+
formatter.write_str("a sequence of status lines")
99+
}
100+
101+
fn visit_seq<S: serde::de::SeqAccess<'de>>(mut self, mut seq: S) -> Result<(), S::Error> {
102+
while let Some(blocks) = seq.next_element()? {
103+
(self.cb)(blocks);
104+
}
105+
Ok(())
106+
}
107+
}
108+
109+
let visitor = Visitor { cb };
110+
deserializer.deserialize_seq(visitor)
111+
}

0 commit comments

Comments
 (0)