-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
status line: Custom widget for mouse input handling
- Loading branch information
Showing
8 changed files
with
841 additions
and
55 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.