diff options
| author | Max Verevkin <34583604+MaxVerevkin@users.noreply.github.com> | 2022-11-27 21:25:15 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-11-27 21:25:15 +0200 |
| commit | d28693c9d293ea7c348c0e9e1a4e89ccff6dcae4 (patch) | |
| tree | 0794e56251e223388463a84d09616ef47a918bb4 /src | |
| parent | 9ddde2d2ee9833412e39afbd1ed907cdea22f08f (diff) | |
| parent | d45a4ef408c0f5a2071194d4c3285f0b7cb27dd0 (diff) | |
| download | i3bar-mango-d28693c9d293ea7c348c0e9e1a4e89ccff6dcae4.tar.gz | |
Merge pull request #1 from MaxVerevkin/next
Rewrite using latest walayd libs & implement HiDPI
Diffstat (limited to 'src')
| -rw-r--r-- | src/bar.rs | 412 | ||||
| -rw-r--r-- | src/bar_state.rs | 651 | ||||
| -rw-r--r-- | src/config.rs | 14 | ||||
| -rw-r--r-- | src/i3bar_protocol.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 200 | ||||
| -rw-r--r-- | src/river_protocols.rs | 38 | ||||
| -rw-r--r-- | src/river_protocols/control.rs | 112 | ||||
| -rw-r--r-- | src/river_protocols/status.rs | 197 | ||||
| -rw-r--r-- | src/shared_state.rs | 30 | ||||
| -rw-r--r-- | src/state.rs | 479 | ||||
| -rw-r--r-- | src/status_cmd.rs | 71 | ||||
| -rw-r--r-- | src/tags.rs | 20 | ||||
| -rw-r--r-- | src/text.rs | 117 |
13 files changed, 1375 insertions, 969 deletions
diff --git a/src/bar.rs b/src/bar.rs new file mode 100644 index 0000000..dfa63a3 --- /dev/null +++ b/src/bar.rs @@ -0,0 +1,412 @@ +use std::collections::BinaryHeap; + +use pangocairo::cairo; +use smithay_client_toolkit::{ + reexports::client::protocol::{wl_seat, wl_shm}, + shell::layer::LayerSurface, +}; + +use crate::{ + button_manager::ButtonManager, + config::Config, + i3bar_protocol::{self, Block, MinWidth}, + ord_adaptor::DefaultLess, + pointer_btn::PointerBtn, + river_protocols::{control::RiverControlState, status::RiverOutputStatus}, + shared_state::SharedState, + state::ComputedBlock, + tags::{compute_tag_label, TagState, TagsInfo}, + text::{self, ComputedText, RenderOptions}, +}; + +pub struct Bar { + pub configured: bool, + pub width: u32, + pub height: u32, + pub scale: i32, + pub layer: LayerSurface, + pub blocks_btns: ButtonManager<(Option<String>, Option<String>)>, + pub river_output_status: Option<RiverOutputStatus>, + pub river_control: Option<RiverControlState>, + pub tags_btns: ButtonManager, + pub tags_info: TagsInfo, + pub tags_computed: Vec<ComputedText>, +} + +impl Bar { + pub fn configure(&mut self, ss: &mut SharedState, width: u32) { + if self.width != width && width != 0 { + self.width = width; + self.layer.set_exclusive_zone(self.height as i32); + } + + self.configured = true; + self.draw(ss); + } + + pub fn has_tags_provider(&self) -> bool { + // TODO: add more tags providers + self.river_output_status.is_some() + } + + pub fn click( + &mut self, + ss: &mut SharedState, + button: PointerBtn, + seat: &wl_seat::WlSeat, + x: f64, + _y: f64, + ) -> anyhow::Result<()> { + if let Some(tag) = self.tags_btns.click(x) { + if let Some(river_control) = &self.river_control { + let cmd = match button { + PointerBtn::Left => "set-focused-tags", + PointerBtn::Right => "toggle-focused-tags", + _ => return Ok(()), + }; + river_control.run_command(&ss.qh, seat, [cmd.into(), (1u32 << tag).to_string()]); + } + } else if let Some((name, instance)) = self.blocks_btns.click(x) { + if let Some(cmd) = &mut ss.status_cmd { + cmd.send_click_event(&i3bar_protocol::Event { + name: name.as_deref(), + instance: instance.as_deref(), + button, + ..Default::default() + })?; + } + } + Ok(()) + } + + pub fn draw(&mut self, ss: &mut SharedState) { + if !self.configured { + return; + } + + let stride = 4 * self.width as i32; + let width = self.width as i32; + let height = self.height as i32; + let width_f = width as f64; + let height_f = height as f64; + + let (buffer, canvas) = ss + .get_pool(height as usize * stride as usize) + .create_buffer( + width * self.scale, + height * self.scale, + stride * self.scale, + wl_shm::Format::Argb8888, + ) + .expect("create buffer"); + + let cairo_surf = unsafe { + cairo::ImageSurface::create_for_data_unsafe( + canvas.as_mut_ptr(), + cairo::Format::ARgb32, + width * self.scale, + height * self.scale, + stride * self.scale, + ) + .expect("cairo surface") + }; + + let cairo_ctx = cairo::Context::new(&cairo_surf).expect("cairo context"); + cairo_ctx.scale(self.scale as f64, self.scale as f64); + self.layer.wl_surface().set_buffer_scale(self.scale); + + // Background + cairo_ctx.save().unwrap(); + cairo_ctx.set_operator(cairo::Operator::Source); + ss.config.background.apply(&cairo_ctx); + cairo_ctx.paint().unwrap(); + cairo_ctx.restore().unwrap(); + + // Display tags + let mut offset_left = 0.0; + if self.has_tags_provider() { + if self.tags_computed.is_empty() { + // TODO make configurable + for text in ["1", "2", "3", "4", "5", "6", "7", "8", "9"] { + self.tags_computed + .push(compute_tag_label(text, &ss.config.font, &cairo_ctx)); + } + } + self.tags_btns.clear(); + for (i, label) in self.tags_computed.iter().enumerate() { + let state = self.tags_info.get_state(i); + let (bg, fg) = match state { + TagState::Urgent => (ss.config.tag_urgent_bg, ss.config.tag_urgent_fg), + TagState::Focused => (ss.config.tag_focused_bg, ss.config.tag_focused_fg), + TagState::Active => (ss.config.tag_bg, ss.config.tag_fg), + TagState::Inactive => { + if ss.config.hide_inactive_tags { + continue; + } + (ss.config.tag_inactive_bg, ss.config.tag_inactive_fg) + } + }; + label.render( + &cairo_ctx, + RenderOptions { + x_offset: offset_left, + bar_height: height_f, + fg_color: fg, + bg_color: Some(bg), + r_left: if i == 0 || self.tags_info.get_state(i.saturating_sub(1)) != state + { + ss.config.tags_r + } else { + 0.0 + }, + r_right: if i == 8 || self.tags_info.get_state(i + 1) != state { + ss.config.tags_r + } else { + 0.0 + }, + overlap: 0.0, + }, + ); + self.tags_btns.push(offset_left, label.width, i); + offset_left += label.width; + } + } + + // Display the blocks + render_blocks( + &cairo_ctx, + &ss.config, + &ss.blocks, + &mut ss.blocks_cache, + &mut self.blocks_btns, + offset_left, + width_f, + height_f, + ); + + // Attach the buffer to the surface and mark the entire surface as damaged + buffer + .attach_to(self.layer.wl_surface()) + .expect("attaching buffer"); + self.layer + .wl_surface() + .damage_buffer(0, 0, width * self.scale, height * self.scale); + + // Finally, commit the surface + self.layer.wl_surface().commit(); + } +} + +#[allow(clippy::too_many_arguments)] +fn render_blocks( + context: &cairo::Context, + config: &Config, + blocks: &[Block], + blocks_cache: &mut Vec<ComputedBlock>, + buttons: &mut ButtonManager<(Option<String>, Option<String>)>, + offset_left: f64, + full_width: f64, + full_height: f64, +) { + let comp_min_width = |block: &Block| { + let markup = block.markup.as_deref() == Some("pango"); + match &block.min_width { + Some(MinWidth::Pixels(p)) => Some(*p as f64), + Some(MinWidth::Text(t)) => Some(text::width_of(t, context, markup, &config.font.0)), + None => None, + } + }; + let comp_full = |block: &Block, min_width: Option<f64>| { + let markup = block.markup.as_deref() == Some("pango"); + text::ComputedText::new( + &block.full_text, + text::Attributes { + font: &config.font, + padding_left: 0.0, + padding_right: 0.0, + min_width, + align: block.align.unwrap_or_default(), + markup, + }, + context, + ) + }; + let comp_short = |block: &Block, min_width: Option<f64>| { + let markup = block.markup.as_deref() == Some("pango"); + block.short_text.as_ref().map(|short_text| { + text::ComputedText::new( + short_text, + text::Attributes { + font: &config.font, + padding_left: 0.0, + padding_right: 0.0, + min_width, + align: block.align.unwrap_or_default(), + markup, + }, + context, + ) + }) + }; + + // update cashe + if blocks.len() != blocks_cache.len() { + blocks_cache.clear(); + for block in blocks { + let mw = comp_min_width(block); + blocks_cache.push(ComputedBlock { + block: block.clone(), + full: comp_full(block, mw), + short: comp_short(block, mw), + min_width: mw, + }); + } + } else { + for (block, computed) in blocks.iter().zip(blocks_cache.iter_mut()) { + if block.min_width != computed.block.min_width || block.markup != computed.block.markup + { + let mw = comp_min_width(block); + *computed = ComputedBlock { + block: block.clone(), + full: comp_full(block, mw), + short: comp_short(block, mw), + min_width: mw, + }; + } else { + if block.full_text != computed.block.full_text { + computed.block.full_text = block.full_text.clone(); + computed.full = comp_full(block, computed.min_width); + } + if block.short_text != computed.block.short_text { + computed.block.full_text = block.full_text.clone(); + computed.short = comp_short(block, computed.min_width); + } + computed.block.color = block.color; + computed.block.background = block.background; + computed.block.align = block.align; + computed.block.name = block.name.clone(); + computed.block.instance = block.instance.clone(); + computed.block.separator = block.separator; + computed.block.separator_block_width = block.separator_block_width; + } + } + } + + #[derive(Debug)] + struct LogialBlock<'a> { + blocks: Vec<(&'a ComputedBlock, bool)>, + delta: f64, + separator: bool, + separator_block_width: u8, + } + + let mut blocks_computed = Vec::new(); + let mut blocks_width = 0.0; + let mut s_start = 0; + while s_start < blocks.len() { + let mut s_end = s_start + 1; + let series_name = &blocks[s_start].name; + while s_end < blocks.len() + && blocks[s_end - 1].separator_block_width == 0 + && &blocks[s_end].name == series_name + { + s_end += 1; + } + + let mut series = LogialBlock { + blocks: Vec::with_capacity(s_end - s_start), + delta: 0.0, + separator: blocks[s_end - 1].separator, + separator_block_width: blocks[s_end - 1].separator_block_width, + }; + + for comp in &blocks_cache[s_start..s_end] { + blocks_width += comp.full.width; + if let Some(short) = &comp.short { + series.delta += comp.full.width - short.width; + } + series.blocks.push((comp, false)); + } + if s_end != blocks.len() { + blocks_width += series.separator_block_width as f64; + } + blocks_computed.push(series); + s_start = s_end; + } + + // Progressively switch to short mode + if offset_left + blocks_width > full_width { + let mut heap = BinaryHeap::new(); + for (i, b) in blocks_computed.iter().enumerate() { + if b.delta > 0.0 { + heap.push((DefaultLess(b.delta), i)); + } + } + while let Some((DefaultLess(delta), to_switch)) = heap.pop() { + for comp in &mut blocks_computed[to_switch].blocks { + comp.1 = true; + } + blocks_width -= delta; + if offset_left + blocks_width <= full_width { + break; + } + } + } + + // Remove all the empy blocks + for s in &mut blocks_computed { + s.blocks.retain(|(text, is_short)| { + (*is_short + && text + .short + .as_ref() + .map_or(text.full.width > 0.0, |s| s.width > 0.0)) + || (!is_short && text.full.width > 0.0) + }); + } + + // Render blocks + buttons.clear(); + let mut j = 0; + for series in blocks_computed { + let s_len = series.blocks.len(); + for (i, (computed, is_short)) in series.blocks.into_iter().enumerate() { + let block = &computed.block; + let to_render = if is_short { + computed.short.as_ref().unwrap_or(&computed.full) + } else { + &computed.full + }; + j += 1; + to_render.render( + context, + RenderOptions { + x_offset: full_width - blocks_width, + bar_height: full_height, + fg_color: block.color.unwrap_or(config.color), + bg_color: block.background, + r_left: if i == 0 { config.blocks_r } else { 0.0 }, + r_right: if i + 1 == s_len { config.blocks_r } else { 0.0 }, + overlap: config.blocks_overlap, + }, + ); + buttons.push( + full_width - blocks_width, + to_render.width, + (block.name.clone(), block.instance.clone()), + ); + blocks_width -= to_render.width; + } + if j != blocks.len() && series.separator_block_width > 0 { + let w = series.separator_block_width as f64; + if series.separator && config.separator_width > 0.0 { + config.separator.apply(context); + context.set_line_width(config.separator_width); + context.move_to(full_width - blocks_width + w * 0.5, full_height * 0.1); + context.line_to(full_width - blocks_width + w * 0.5, full_height * 0.9); + context.stroke().unwrap(); + } + blocks_width -= w; + } + } +} diff --git a/src/bar_state.rs b/src/bar_state.rs deleted file mode 100644 index 55705da..0000000 --- a/src/bar_state.rs +++ /dev/null @@ -1,651 +0,0 @@ -use std::cell::RefCell; -use std::collections::BinaryHeap; -use std::io; -use std::rc::Rc; - -use smithay_client_toolkit::{ - environment::Environment, - reexports::{ - client::protocol::{wl_output::WlOutput, wl_seat::WlSeat, wl_shm, wl_surface::WlSurface}, - client::{Attached, Main}, - protocols::wlr::unstable::layer_shell::v1::client::{ - zwlr_layer_shell_v1, zwlr_layer_surface_v1, - }, - }, - shm::AutoMemPool, -}; - -use pangocairo::cairo; - -use crate::button_manager::ButtonManager; -use crate::config::Config; -use crate::i3bar_protocol::{self, Block, MinWidth}; -use crate::ord_adaptor::DefaultLess; -use crate::pointer_btn::PointerBtn; -use crate::river_protocols::zriver_command_callback_v1; -use crate::river_protocols::zriver_control_v1; -use crate::river_protocols::zriver_output_status_v1; -use crate::river_protocols::zriver_status_manager_v1; -use crate::status_cmd::StatusCmd; -use crate::tags::{compute_tag_label, TagState, TagsInfo}; -use crate::text; -use crate::text::{ComputedText, RenderOptions}; -use crate::Env; - -pub struct BarState { - pub status_cmd: Option<StatusCmd>, - config: Rc<RefCell<Config>>, - blocks: Vec<Block>, - blocks_cache: Vec<ComputedBlock>, - surfaces: Vec<Surface>, - layer_shell: Attached<zwlr_layer_shell_v1::ZwlrLayerShellV1>, - river_status: Option<Attached<zriver_status_manager_v1::ZriverStatusManagerV1>>, - river_control: Option<Attached<zriver_control_v1::ZriverControlV1>>, -} - -impl BarState { - pub fn new(env: &Environment<Env>) -> Self { - let mut error = Ok(()); - - let config = Config::new() - .map_err(|e| error = Err(e)) - .unwrap_or_default(); - - let status_cmd = match &error { - Err(_) => None, - Ok(()) => config.command.as_deref().and_then(|cmd| { - StatusCmd::new(cmd) - .map_err(|e| error = Err(anyhow!(e))) - .ok() - }), - }; - - let mut s = Self { - status_cmd, - config: Rc::new(RefCell::new(config)), - blocks: Vec::new(), - blocks_cache: Vec::new(), - surfaces: Vec::default(), - layer_shell: env.require_global::<zwlr_layer_shell_v1::ZwlrLayerShellV1>(), - river_status: env.get_global::<zriver_status_manager_v1::ZriverStatusManagerV1>(), - river_control: env.get_global::<zriver_control_v1::ZriverControlV1>(), - }; - - if let Err(e) = error { - s.set_error(e.to_string()); - } - - s - } - - pub fn set_blocks(&mut self, blocks: Vec<Block>) { - self.blocks = blocks; - for s in &mut self.surfaces { - s.dirty = true; - } - } - - pub fn set_error(&mut self, error: impl Into<String>) { - self.set_blocks(vec![Block { - full_text: error.into(), - ..Default::default() - }]); - } - - pub fn remove_surface(&mut self, output_id: u32) { - self.surfaces.retain(|s| s.output_id != output_id); - } - - pub fn notify_available(&mut self) -> io::Result<()> { - if let Some(cmd) = &mut self.status_cmd { - if let Some(blocks) = cmd.notify_available()? { - self.set_blocks(blocks); - } - } - Ok(()) - } - - pub fn handle_click( - &mut self, - surface: &WlSurface, - seat: &WlSeat, - x: f64, - y: f64, - btn: PointerBtn, - ) { - if let Some(s) = self - .surfaces - .iter() - .find(|s| &s.surface.detach() == surface) - { - if let Some(event) = s.click(btn, seat, x, y) { - if let Some(cmd) = &mut self.status_cmd { - if let Err(e) = cmd.send_click_event(&event) { - self.set_error(e.to_string()); - } - } - } - } - } - - pub fn add_surface(&mut self, output: &WlOutput, output_id: u32, env: &Environment<Env>) { - let surface = env.create_surface(); - let pool = env - .create_auto_pool() - .expect("Failed to create a memory pool!"); - - let layer_surface = self.layer_shell.get_layer_surface( - &surface, - Some(output), - zwlr_layer_shell_v1::Layer::Top, - "i3bar-river".to_owned(), - ); - - // Set the height - layer_surface.set_size(0, self.config.borrow().height); - // Anchor to the top - layer_surface.set_anchor( - zwlr_layer_surface_v1::Anchor::Top - | zwlr_layer_surface_v1::Anchor::Left - | zwlr_layer_surface_v1::Anchor::Right, - ); - - layer_surface.quick_assign(move |layer_surface, event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - match event { - zwlr_layer_surface_v1::Event::Closed => { - bar_state.remove_surface(output_id); - } - zwlr_layer_surface_v1::Event::Configure { - serial, - width, - height, - } => { - layer_surface.ack_configure(serial); - if let Some(s) = bar_state - .surfaces - .iter_mut() - .find(|s| s.output_id == output_id) - { - if s.configure(width, height) { - s.draw(&bar_state.blocks, &mut bar_state.blocks_cache); - } - } - } - _ => (), - } - }); - - // Commit so that the server will send a configure event - surface.commit(); - - let (river_output_status, tags_info) = if let Some(river_status) = &self.river_status { - let river_output_status = river_status.get_river_output_status(output); - river_output_status.quick_assign(move |_, event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - if let Some(s) = bar_state - .surfaces - .iter_mut() - .find(|s| s.output_id == output_id) - { - match event { - zriver_output_status_v1::Event::FocusedTags { tags } => { - s.tags_info.as_mut().unwrap().focused = tags; - } - zriver_output_status_v1::Event::UrgentTags { tags } => { - s.tags_info.as_mut().unwrap().urgent = tags; - } - _ => (), - } - s.dirty = true; - } - }); - (Some(river_output_status), Some(TagsInfo::default())) - } else { - (None, None) - }; - - self.surfaces.push(Surface { - output_id, - surface, - layer_surface, - pool, - dimensions: (0, 0), - config: self.config.clone(), - dirty: true, - frame_scheduled: false, - river_output_status, - river_control: self.river_control.clone(), - tags_info, - tags_computed: Vec::with_capacity(9), - tags_btns: Default::default(), - blocks_btns: Default::default(), - }); - } -} - -pub struct Surface { - output_id: u32, - surface: Attached<WlSurface>, - layer_surface: Main<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>, - pool: AutoMemPool, - dimensions: (u32, u32), - config: Rc<RefCell<Config>>, - dirty: bool, - frame_scheduled: bool, - // river stuff - river_output_status: Option<Main<zriver_output_status_v1::ZriverOutputStatusV1>>, - river_control: Option<Attached<zriver_control_v1::ZriverControlV1>>, - // tags - tags_info: Option<TagsInfo>, - tags_computed: Vec<ComputedText>, - // buttons - tags_btns: ButtonManager, - blocks_btns: ButtonManager<(Option<String>, Option<String>)>, -} - -impl Surface { - /// Handle `configure` event. Returns `true` if configuration changed. - fn configure(&mut self, width: u32, height: u32) -> bool { - if self.dimensions != (width, height) { - self.dimensions = (width, height); - self.layer_surface.set_exclusive_zone(height as i32); - self.dirty = true; - true - } else { - false - } - } - - fn click( - &self, - button: PointerBtn, - seat: &WlSeat, - x: f64, - _y: f64, - ) -> Option<i3bar_protocol::Event> { - if let Some((id, river_control)) = self.tags_btns.click(x).zip(self.river_control.as_ref()) - { - let cmd = match button { - PointerBtn::Left => "set-focused-tags", - PointerBtn::Right => "toggle-focused-tags", - _ => return None, - }; - river_control.add_argument(cmd.into()); - river_control.add_argument((1u32 << id).to_string()); - let result = river_control.run_command(seat); - result.quick_assign(|_, event, _| match event { - zriver_command_callback_v1::Event::Success { output } => { - info!("River cmd output: '{output}'"); - } - zriver_command_callback_v1::Event::Failure { failure_message: f } => { - error!("River error: '{f}'"); - } - }); - } else if let Some((name, instance)) = self.blocks_btns.click(x) { - return Some(i3bar_protocol::Event { - name: name.as_deref(), - instance: instance.as_deref(), - button, - ..Default::default() - }); - } - None - } - - fn draw(&mut self, blocks: &[Block], blocks_cache: &mut Vec<ComputedBlock>) { - if self.dirty { - let config = self.config.borrow(); - - let stride = 4 * self.dimensions.0 as i32; - let width = self.dimensions.0 as i32; - let height = self.dimensions.1 as i32; - let width_f = width as f64; - let height_f = height as f64; - - let (canvas, buffer) = self - .pool - .buffer(width, height, stride, wl_shm::Format::Argb8888) - .unwrap(); - - let cairo_surf = unsafe { - cairo::ImageSurface::create_for_data_unsafe( - canvas.as_mut_ptr(), - cairo::Format::ARgb32, - width, - height, - stride, - ) - .expect("cairo surface") - }; - - let cairo_ctx = cairo::Context::new(&cairo_surf).expect("cairo context"); - let pango_layout = pangocairo::create_layout(&cairo_ctx).expect("pango layout"); - pango_layout.set_font_description(Some(&config.font)); - pango_layout.set_height(height); - - // Background - config.background.apply(&cairo_ctx); - cairo_ctx.paint().expect("cairo paint"); - - // Display tags - let mut offset_left = 0.0; - if let Some(tags_info) = &self.tags_info { - if self.tags_computed.is_empty() { - let mut x_offset = 0.0; - // TODO make configurable - for (id, text) in ["1", "2", "3", "4", "5", "6", "7", "8", "9"] - .iter() - .enumerate() - { - let tag = - compute_tag_label(text.to_string(), config.font.clone(), &cairo_ctx); - self.tags_btns.push(x_offset, tag.width, id); - x_offset += tag.width; - self.tags_computed.push(tag); - } - } - for (i, label) in self.tags_computed.iter().enumerate() { - let state = tags_info.get_state(i); - let (bg, fg) = match state { - TagState::Focused => (config.tag_focused_bg, config.tag_focused_fg), - TagState::Inactive => (config.tag_bg, config.tag_fg), - TagState::Urgent => (config.tag_urgent_bg, config.tag_urgent_fg), - }; - label.render( - &cairo_ctx, - RenderOptions { - x_offset: offset_left, - bar_height: height_f, - fg_color: fg, - bg_color: Some(bg), - r_left: if i == 0 || tags_info.get_state(i.saturating_sub(1)) != state { - config.tags_r - } else { - 0.0 - }, - r_right: if i == 8 || tags_info.get_state(i + 1) != state { - config.tags_r - } else { - 0.0 - }, - overlap: 0.0, - }, - ); - offset_left += label.width; - } - } - - // Display the blocks - render_blocks( - &cairo_ctx, - &*config, - blocks, - blocks_cache, - &mut self.blocks_btns, - offset_left, - width_f, - height_f, - ); - - // Attach the buffer to the surface and mark the entire surface as damaged - self.surface.attach(Some(&buffer), 0, 0); - self.surface - .damage_buffer(0, 0, width as i32, height as i32); - } - - // Schedule next frame - if !self.frame_scheduled { - self.frame_scheduled = true; - let output_id = self.output_id; - let frame = self.surface.frame(); - frame.quick_assign(move |_callback, _event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - if let Some(s) = bar_state - .surfaces - .iter_mut() - .find(|s| s.output_id == output_id) - { - s.frame_scheduled = false; - s.draw(&bar_state.blocks, &mut bar_state.blocks_cache); - } - }); - } - - // Finally, commit the surface - self.surface.commit(); - self.dirty = false; - } -} - -impl Drop for Surface { - fn drop(&mut self) { - self.layer_surface.destroy(); - self.surface.destroy(); - if let Some(ros) = &self.river_output_status { - ros.destroy(); - } - } -} - -#[allow(clippy::too_many_arguments)] -fn render_blocks( - context: &cairo::Context, - config: &Config, - blocks: &[Block], - blocks_cache: &mut Vec<ComputedBlock>, - buttons: &mut ButtonManager<(Option<String>, Option<String>)>, - offset_left: f64, - full_width: f64, - full_height: f64, -) { - let comp_min_width = |block: &Block| { - let markup = block.markup.as_deref() == Some("pango"); - match &block.min_width { - Some(MinWidth::Pixels(p)) => Some(*p as f64), - Some(MinWidth::Text(t)) => Some(text::width_of(t, context, markup, &config.font.0)), - None => None, - } - }; - let comp_full = |block: &Block, min_width: Option<f64>| { - let markup = block.markup.as_deref() == Some("pango"); - text::Text { - text: block.full_text.clone(), - attr: text::Attributes { - font: config.font.clone(), - padding_left: 0.0, - padding_right: 0.0, - min_width, - align: block.align.unwrap_or_default(), - markup, - }, - } - .compute(context) - }; - let comp_short = |block: &Block, min_width: Option<f64>| { - let markup = block.markup.as_deref() == Some("pango"); - block.short_text.as_ref().map(|short_text| { - text::Text { - text: short_text.clone(), - attr: text::Attributes { - font: config.font.clone(), - padding_left: 0.0, - padding_right: 0.0, - min_width, - align: block.align.unwrap_or_default(), - markup, - }, - } - .compute(context) - }) - }; - - // update cashe - if blocks.len() != blocks_cache.len() { - blocks_cache.clear(); - for block in blocks { - let mw = comp_min_width(block); - blocks_cache.push(ComputedBlock { - block: block.clone(), - full: comp_full(block, mw), - short: comp_short(block, mw), - min_width: mw, - }); - } - } else { - for (block, computed) in blocks.iter().zip(blocks_cache.iter_mut()) { - if block.min_width != computed.block.min_width || block.markup != computed.block.markup - { - let mw = comp_min_width(block); - *computed = ComputedBlock { - block: block.clone(), - full: comp_full(block, mw), - short: comp_short(block, mw), - min_width: mw, - }; - } else { - if block.full_text != computed.block.full_text { - computed.block.full_text = block.full_text.clone(); - computed.full = comp_full(block, computed.min_width); - } - if block.short_text != computed.block.short_text { - computed.block.full_text = block.full_text.clone(); - computed.short = comp_short(block, computed.min_width); - } - computed.block.color = block.color; - computed.block.background = block.background; - computed.block.align = block.align; - computed.block.name = block.name.clone(); - computed.block.instance = block.instance.clone(); - computed.block.separator = block.separator; - computed.block.separator_block_width = block.separator_block_width; - } - } - } - - #[derive(Debug)] - struct LogialBlock<'a> { - blocks: Vec<(&'a ComputedBlock, bool)>, - delta: f64, - separator: bool, - separator_block_width: u8, - } - - let mut blocks_computed = Vec::new(); - let mut blocks_width = 0.0; - let mut s_start = 0; - while s_start < blocks.len() { - let mut s_end = s_start + 1; - let series_name = &blocks[s_start].name; - while s_end < blocks.len() - && blocks[s_end - 1].separator_block_width == 0 - && &blocks[s_end].name == series_name - { - s_end += 1; - } - - let mut series = LogialBlock { - blocks: Vec::with_capacity(s_end - s_start), - delta: 0.0, - separator: blocks[s_end - 1].separator, - separator_block_width: blocks[s_end - 1].separator_block_width, - }; - - for comp in &blocks_cache[s_start..s_end] { - blocks_width += comp.full.width; - if let Some(short) = &comp.short { - series.delta += comp.full.width - short.width; - } - series.blocks.push((comp, false)); - } - if s_end != blocks.len() { - blocks_width += series.separator_block_width as f64; - } - blocks_computed.push(series); - s_start = s_end; - } - - // Progressively switch to short mode - if offset_left + blocks_width > full_width { - let mut heap = BinaryHeap::new(); - for (i, b) in blocks_computed.iter().enumerate() { - if b.delta > 0.0 { - heap.push((DefaultLess(b.delta), i)); - } - } - while let Some((DefaultLess(delta), to_switch)) = heap.pop() { - for comp in &mut blocks_computed[to_switch].blocks { - comp.1 = true; - } - blocks_width -= delta; - if offset_left + blocks_width <= full_width { - break; - } - } - } - - // Remove all the empy blocks - for s in &mut blocks_computed { - s.blocks.retain(|(text, is_short)| { - (*is_short - && text - .short - .as_ref() - .map_or(text.full.width > 0.0, |s| s.width > 0.0)) - || (!is_short && text.full.width > 0.0) - }); - } - - // Render blocks - buttons.clear(); - let mut j = 0; - for series in blocks_computed { - let s_len = series.blocks.len(); - for (i, (computed, is_short)) in series.blocks.into_iter().enumerate() { - let block = &computed.block; - let to_render = if is_short { - computed.short.as_ref().unwrap_or(&computed.full) - } else { - &computed.full - }; - j += 1; - to_render.render( - context, - RenderOptions { - x_offset: full_width - blocks_width, - bar_height: full_height, - fg_color: block.color.unwrap_or(config.color), - bg_color: block.background, - r_left: if i == 0 { config.blocks_r } else { 0.0 }, - r_right: if i + 1 == s_len { config.blocks_r } else { 0.0 }, - overlap: config.blocks_overlap, - }, - ); - buttons.push( - full_width - blocks_width, - to_render.width, - (block.name.clone(), block.instance.clone()), - ); - blocks_width -= to_render.width; - } - if j != blocks.len() && series.separator_block_width > 0 { - let w = series.separator_block_width as f64; - if series.separator && config.separator_width > 0.0 { - config.separator.apply(context); - context.set_line_width(config.separator_width); - context.move_to(full_width - blocks_width + w * 0.5, full_height * 0.1); - context.line_to(full_width - blocks_width + w * 0.5, full_height * 0.9); - context.stroke().unwrap(); - } - blocks_width -= w; - } - } -} - -#[derive(Debug)] -struct ComputedBlock { - block: Block, - full: ComputedText, - short: Option<ComputedText>, - min_width: Option<f64>, -} diff --git a/src/config.rs b/src/config.rs index 66b8c8a..2af9af0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,8 +7,8 @@ use std::fmt; use std::fs::read_to_string; use std::ops::Deref; -#[derive(Deserialize)] -#[serde(default)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields, default)] pub struct Config { // colors pub background: Color, @@ -20,6 +20,8 @@ pub struct Config { pub tag_focused_bg: Color, pub tag_urgent_fg: Color, pub tag_urgent_bg: Color, + pub tag_inactive_fg: Color, + pub tag_inactive_bg: Color, // font and size pub font: Font, pub height: u32, @@ -29,6 +31,9 @@ pub struct Config { pub blocks_overlap: f64, // command pub command: Option<String>, + // misc + pub hide_inactive_tags: bool, + pub invert_touchpad_scrolling: bool, } impl Default for Config { @@ -44,6 +49,8 @@ impl Default for Config { tag_focused_bg: Color::from_rgba_hex(0x689d68ff), tag_urgent_fg: Color::from_rgba_hex(0x282828ff), tag_urgent_bg: Color::from_rgba_hex(0xcc241dff), + tag_inactive_fg: Color::from_rgba_hex(0xd79921ff), + tag_inactive_bg: Color::from_rgba_hex(0x282828ff), font: Font::new("monospace 10"), height: 24, separator_width: 2.0, @@ -51,6 +58,8 @@ impl Default for Config { blocks_r: 0.0, blocks_overlap: 0.0, command: None, + hide_inactive_tags: true, + invert_touchpad_scrolling: true, } } } @@ -79,6 +88,7 @@ impl Config { } } +#[derive(Debug)] pub struct Font(pub FontDescription); impl Font { diff --git a/src/i3bar_protocol.rs b/src/i3bar_protocol.rs index 54ebd93..67e03f1 100644 --- a/src/i3bar_protocol.rs +++ b/src/i3bar_protocol.rs @@ -64,7 +64,7 @@ pub struct Event<'a> { pub height: u8, } -#[derive(Deserialize, Clone, Copy)] +#[derive(Deserialize, Clone, Copy, Debug)] #[serde(deny_unknown_fields)] pub struct JsonHeader { version: u8, @@ -78,6 +78,7 @@ pub struct JsonHeader { click_events: bool, } +#[derive(Debug)] pub enum Protocol { Unknown, PlainText { diff --git a/src/main.rs b/src/main.rs index 730edf6..8ae31aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,9 @@ -#![allow(clippy::explicit_auto_deref)] // TODO: remove once https://github.com/rust-lang/rust-clippy/issues/9101 is resloved - -#[macro_use] -extern crate log; #[macro_use] extern crate anyhow; +#[macro_use] +extern crate log; -use smithay_client_toolkit::{ - self as sctk, default_environment, - environment::SimpleGlobal, - new_default_environment, - output::{with_output_info, OutputInfo}, - reexports::{ - calloop, - client::protocol::{wl_output::WlOutput, wl_pointer}, - protocols::wlr::unstable::layer_shell::v1::client::zwlr_layer_shell_v1, - }, - seat::pointer, - WaylandSource, -}; - -mod bar_state; +mod bar; mod button_manager; mod color; mod config; @@ -27,163 +11,55 @@ mod i3bar_protocol; mod ord_adaptor; mod pointer_btn; mod river_protocols; +mod shared_state; +mod state; mod status_cmd; mod tags; mod text; mod utils; -use bar_state::BarState; -use pointer_btn::PointerBtn; -use river_protocols::zriver_control_v1; -use river_protocols::zriver_status_manager_v1; +use std::os::unix::io::AsRawFd; -default_environment!(Env, - fields = [ - layer_shell: SimpleGlobal<zwlr_layer_shell_v1::ZwlrLayerShellV1>, - river_status_manager: SimpleGlobal<zriver_status_manager_v1::ZriverStatusManagerV1>, - river_control: SimpleGlobal<zriver_control_v1::ZriverControlV1>, - ], - singles = [ - zwlr_layer_shell_v1::ZwlrLayerShellV1 => layer_shell, - zriver_status_manager_v1::ZriverStatusManagerV1 => river_status_manager, - zriver_control_v1::ZriverControlV1 => river_control, - ], -); +use smithay_client_toolkit::reexports::client::Connection; +use tokio::io::{unix::AsyncFd, Interest}; -fn main() { - env_logger::init(); +use state::State; +use wayland_client::globals::registry_queue_init; - let (env, _display, queue) = new_default_environment!( - Env, - fields = [ - layer_shell: SimpleGlobal::new(), - river_status_manager: SimpleGlobal::new(), - river_control: SimpleGlobal::new(), - ] - ) - .expect("Initial roundtrip failed!"); +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + env_logger::init(); - let mut bar_state = BarState::new(&env); + let conn = Connection::connect_to_env()?; + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let mut state = State::new(&mut event_queue, &globals); - let env_handle = env.clone(); - let output_handler = move |output: WlOutput, info: &OutputInfo, bar_state: &mut BarState| { - if info.obsolete { - info!("Output {} removed", info.id); - output.release(); - } else { - info!("Output {} detected", info.id); - bar_state.add_surface(&output, info.id, &env_handle); - } - }; + let async_fd = AsyncFd::with_interest( + event_queue.prepare_read()?.connection_fd().as_raw_fd(), + Interest::READABLE, + )?; - // Process currently existing outputs - for output in env.get_all_outputs() { - if let Some(info) = with_output_info(&output, Clone::clone) { - output_handler(output, &info, &mut bar_state); - } - } + event_queue.roundtrip(&mut state)?; - // Setup a listener for changes - // The listener will live for as long as we keep this handle alive - let _listner_handle = env.listen_for_outputs(move |output, info, mut data| { - output_handler(output, info, data.get().unwrap()) - }); - - // Right now river only supports one seat - let cursor_theme = pointer::ThemeManager::init( - pointer::ThemeSpec::System, - env.require_global(), - env.require_global(), - ); - for seat in env.get_all_seats() { - sctk::seat::with_seat_data(&seat, |seat_data| { - if seat_data.has_pointer && !seat_data.defunct { - let pointer = seat.get_pointer(); - let themed_pointer = cursor_theme.theme_pointer(pointer.detach()); - let seat = seat.clone(); - let mut pos = (0.0, 0.0); - let mut cur_surface = None; - pointer.quick_assign(move |_, event, mut data| { - let bar_state: &mut BarState = data.get().unwrap(); - match event { - wl_pointer::Event::Enter { - serial, - surface, - surface_x: y, - surface_y: x, - } => { - let _ = themed_pointer.set_cursor("default", Some(serial)); - cur_surface = Some(surface); - pos = (x, y); - } - wl_pointer::Event::Leave { - serial: _, - surface: _, - } => { - cur_surface = None; - } - wl_pointer::Event::Motion { - time: _, - surface_x: x, - surface_y: y, - } => { - pos = (x, y); - } - wl_pointer::Event::Button { - serial: _, - time: _, - button, - state, - } if state == wl_pointer::ButtonState::Pressed => { - if let Some(cur_surface) = &cur_surface { - bar_state.handle_click( - cur_surface, - &seat, - pos.0, - pos.1, - button.into(), - ); - } - } - wl_pointer::Event::Axis { - time: _, - axis, - value, - } if axis == wl_pointer::Axis::VerticalScroll => { - if let Some(cur_surface) = &cur_surface { - bar_state.handle_click( - cur_surface, - &seat, - pos.0, - pos.1, - if value > 0.0 { - PointerBtn::WheelDown - } else { - PointerBtn::WheelUp - }, - ); - } - } - _ => (), + loop { + tokio::select! { + readable = async_fd.readable() => { + readable?.clear_ready(); + event_queue.prepare_read()?.read()?; + event_queue.dispatch_pending(&mut state)?; + event_queue.flush()?; + } + readable = state.wait_for_status_cmd() => { + readable?.clear_ready(); + if let Err(e) = state.notify_available() { + if let Some(mut status_cmd) = state.shared_state.status_cmd.take() { + let _ = status_cmd.child.kill(); } - }); + state.set_error(e.to_string()); + state.draw_all(); + } + event_queue.flush()?; } - }); - } - - let mut event_loop = calloop::EventLoop::<BarState>::try_new().unwrap(); - - WaylandSource::new(queue) - .quick_insert(event_loop.handle()) - .unwrap(); - - if let Some(cmd) = &bar_state.status_cmd { - cmd.quick_insert(event_loop.handle()); - } - - loop { - if let Err(e) = event_loop.dispatch(None, &mut bar_state) { - bar_state.set_error(e.to_string()); } } } diff --git a/src/river_protocols.rs b/src/river_protocols.rs index c81a7f7..0a2cf91 100644 --- a/src/river_protocols.rs +++ b/src/river_protocols.rs @@ -1,36 +1,4 @@ -pub use generated::client1::*; -pub use generated::client2::*; +#![allow(clippy::enum_variant_names)] -mod generated { - // The generated code tends to trigger a lot of warnings - // so we isolate it into a very permissive module - #![allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)] - #![allow(non_upper_case_globals, non_snake_case, unused_imports)] - #![allow(clippy::all)] - - pub mod client1 { - // These imports are used by the generated code - use smithay_client_toolkit::reexports::client as wayland_client; - use wayland_client::protocol::*; - use wayland_client::sys; - use wayland_client::{AnonymousObject, Attached, Main, Proxy, ProxyMap}; - use wayland_commons::map::{Object, ObjectMetadata}; - use wayland_commons::smallvec; - use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; - use wayland_commons::{Interface, MessageGroup}; - include!(concat!(env!("OUT_DIR"), "/river-status-unstable-v1.rs")); - } - - pub mod client2 { - // These imports are used by the generated code - use smithay_client_toolkit::reexports::client as wayland_client; - use wayland_client::protocol::*; - use wayland_client::sys; - use wayland_client::{AnonymousObject, Attached, Main, Proxy, ProxyMap}; - use wayland_commons::map::{Object, ObjectMetadata}; - use wayland_commons::smallvec; - use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; - use wayland_commons::{Interface, MessageGroup}; - include!(concat!(env!("OUT_DIR"), "/river-control-unstable-v1.rs")); - } -} +pub mod control; +pub mod status; diff --git a/src/river_protocols/control.rs b/src/river_protocols/control.rs new file mode 100644 index 0000000..7cce23d --- /dev/null +++ b/src/river_protocols/control.rs @@ -0,0 +1,112 @@ +use wayland_client::globals::{BindError, GlobalList}; +use wayland_client::{protocol::wl_seat, Connection, Dispatch, QueueHandle}; + +use protocol::{zriver_command_callback_v1, zriver_control_v1}; + +#[derive(Debug, Clone)] +pub struct RiverControlState { + control: zriver_control_v1::ZriverControlV1, +} + +impl RiverControlState { + pub fn new<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self, BindError> + where + D: Dispatch<zriver_control_v1::ZriverControlV1, (), D> + 'static, + { + let control = globals.bind(qh, 1..=1, ())?; + Ok(Self { control }) + } + + pub fn run_command<D, Args, Arg>(&self, qh: &QueueHandle<D>, seat: &wl_seat::WlSeat, args: Args) + where + D: Dispatch<zriver_command_callback_v1::ZriverCommandCallbackV1, RiverCommandCallbackData> + + 'static, + Args: IntoIterator<Item = Arg>, + Arg: Into<String>, + { + for arg in args { + self.control.add_argument(arg.into()); + } + self.control + .run_command(seat, qh, RiverCommandCallbackData {}); + } +} + +pub trait RiverControlHandler: Sized { + fn river_control_state(&mut self) -> &mut RiverControlState; + + fn command_failure(&mut self, conn: &Connection, qh: &QueueHandle<Self>, message: String); + + fn command_success(&mut self, conn: &Connection, qh: &QueueHandle<Self>, message: String); +} + +#[derive(Debug)] +pub struct RiverCommandCallbackData { + // This is empty right now, but may be populated in the future. +} + +#[macro_export] +macro_rules! delegate_river_control { + ($ty: ty) => { + ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [ + $crate::river_protocols::control::protocol::zriver_control_v1::ZriverControlV1: () + ] => $crate::river_protocols::control::RiverControlState); + ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [ + $crate::river_protocols::control::protocol::zriver_command_callback_v1::ZriverCommandCallbackV1: $crate::river_protocols::control::RiverCommandCallbackData + ] => $crate::river_protocols::control::RiverControlState); + }; +} + +impl<D> Dispatch<zriver_control_v1::ZriverControlV1, (), D> for RiverControlState +where + D: Dispatch<zriver_control_v1::ZriverControlV1, ()> + RiverControlHandler + 'static, +{ + fn event( + _: &mut D, + _: &zriver_control_v1::ZriverControlV1, + _: zriver_control_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle<D>, + ) { + unreachable!("zriver_control_v1 has no events") + } +} + +impl<D> Dispatch<zriver_command_callback_v1::ZriverCommandCallbackV1, RiverCommandCallbackData, D> + for RiverControlState +where + D: Dispatch<zriver_command_callback_v1::ZriverCommandCallbackV1, RiverCommandCallbackData> + + RiverControlHandler + + 'static, +{ + fn event( + data: &mut D, + _callback: &zriver_command_callback_v1::ZriverCommandCallbackV1, + event: zriver_command_callback_v1::Event, + _udata: &RiverCommandCallbackData, + conn: &Connection, + qh: &QueueHandle<D>, + ) { + use zriver_command_callback_v1::Event; + match event { + Event::Success { output } => data.command_success(conn, qh, output), + Event::Failure { failure_message } => data.command_success(conn, qh, failure_message), + } + } +} + +pub mod protocol { + #![allow(non_upper_case_globals)] + + use smithay_client_toolkit::reexports::client as wayland_client; + use smithay_client_toolkit::reexports::client::protocol::*; + + pub mod __interfaces { + use smithay_client_toolkit::reexports::client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("protocols/river-control-unstable-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("protocols/river-control-unstable-v1.xml"); +} diff --git a/src/river_protocols/status.rs b/src/river_protocols/status.rs new file mode 100644 index 0000000..1779719 --- /dev/null +++ b/src/river_protocols/status.rs @@ -0,0 +1,197 @@ +use std::sync::{Arc, Weak}; + +use wayland_client::{ + globals::{BindError, GlobalList}, + protocol::wl_output, + Connection, Dispatch, QueueHandle, +}; + +use protocol::{zriver_output_status_v1, zriver_status_manager_v1}; + +#[derive(Debug, Clone)] +pub struct RiverStatusState { + status_manager: zriver_status_manager_v1::ZriverStatusManagerV1, + output_statuses: Vec<Weak<OutputStatusInner>>, +} + +impl RiverStatusState { + pub fn new<D>(globals: &GlobalList, qh: &QueueHandle<D>) -> Result<Self, BindError> + where + D: Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, (), D> + 'static, + { + let status_manager = globals.bind(qh, 1..=1, ())?; + Ok(Self { + status_manager, + output_statuses: Vec::new(), + }) + } + + pub fn get_output_status( + &self, + status: &zriver_output_status_v1::ZriverOutputStatusV1, + ) -> Option<RiverOutputStatus> { + self.output_statuses + .iter() + .filter_map(Weak::upgrade) + .find(|inner| &inner.status == status) + .map(RiverOutputStatus) + } + + #[must_use = "The output status is destroyed if dropped"] + pub fn new_output_status<D>( + &mut self, + qh: &QueueHandle<D>, + output: &wl_output::WlOutput, + ) -> RiverOutputStatus + where + D: Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, ()> + + Dispatch<zriver_output_status_v1::ZriverOutputStatusV1, RiverStatusData> + + 'static, + { + let output_status = + self.status_manager + .get_river_output_status(output, qh, RiverStatusData {}); + + let output_status = RiverOutputStatus(Arc::new(OutputStatusInner { + status: output_status, + })); + + self.output_statuses.push(Arc::downgrade(&output_status.0)); + + output_status + } +} + +pub trait RiverStatusHandler: Sized { + fn river_status_state(&mut self) -> &mut RiverStatusState; + + fn focused_tags_updated( + &mut self, + conn: &Connection, + qh: &QueueHandle<Self>, + output_status: &RiverOutputStatus, + focused: u32, + ); + + fn urgent_tags_updated( + &mut self, + conn: &Connection, + qh: &QueueHandle<Self>, + output_status: &RiverOutputStatus, + urgent: u32, + ); + + fn views_tags_updated( + &mut self, + conn: &Connection, + qh: &QueueHandle<Self>, + output_status: &RiverOutputStatus, + tags: Vec<u32>, + ); +} + +#[derive(Debug, Clone)] +pub struct RiverOutputStatus(Arc<OutputStatusInner>); + +impl PartialEq for RiverOutputStatus { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +#[derive(Debug)] +pub struct RiverStatusData { + // This is empty right now, but may be populated in the future. +} + +#[macro_export] +macro_rules! delegate_river_status { + ($ty: ty) => { + ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [ + $crate::river_protocols::status::protocol::zriver_status_manager_v1::ZriverStatusManagerV1: () + ] => $crate::river_protocols::status::RiverStatusState); + ::smithay_client_toolkit::reexports::client::delegate_dispatch!($ty: [ + $crate::river_protocols::status::protocol::zriver_output_status_v1::ZriverOutputStatusV1: $crate::river_protocols::status::RiverStatusData + ] => $crate::river_protocols::status::RiverStatusState); + }; +} + +#[derive(Debug)] +struct OutputStatusInner { + status: zriver_output_status_v1::ZriverOutputStatusV1, +} + +impl Drop for OutputStatusInner { + fn drop(&mut self) { + self.status.destroy(); + } +} + +impl<D> Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, (), D> for RiverStatusState +where + D: Dispatch<zriver_status_manager_v1::ZriverStatusManagerV1, ()> + RiverStatusHandler + 'static, +{ + fn event( + _: &mut D, + _: &zriver_status_manager_v1::ZriverStatusManagerV1, + _: zriver_status_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle<D>, + ) { + unreachable!("zriver_status_manager_v1 has no events") + } +} + +impl<D> Dispatch<zriver_output_status_v1::ZriverOutputStatusV1, RiverStatusData, D> + for RiverStatusState +where + D: Dispatch<zriver_output_status_v1::ZriverOutputStatusV1, RiverStatusData> + + RiverStatusHandler + + 'static, +{ + fn event( + data: &mut D, + status: &zriver_output_status_v1::ZriverOutputStatusV1, + event: zriver_output_status_v1::Event, + _udata: &RiverStatusData, + conn: &Connection, + qh: &QueueHandle<D>, + ) { + use zriver_output_status_v1::Event; + + // Remove any statuses that have been dropped + data.river_status_state() + .output_statuses + .retain(|status| status.upgrade().is_some()); + + if let Some(status) = data.river_status_state().get_output_status(status) { + match event { + Event::FocusedTags { tags } => data.focused_tags_updated(conn, qh, &status, tags), + Event::UrgentTags { tags } => data.urgent_tags_updated(conn, qh, &status, tags), + Event::ViewTags { tags } => { + let tags = tags + .chunks_exact(4) + .map(|bytes| u32::from_ne_bytes(bytes.try_into().unwrap())) + .collect(); + data.views_tags_updated(conn, qh, &status, tags); + } + } + } + } +} + +pub mod protocol { + #![allow(non_upper_case_globals)] + + use smithay_client_toolkit::reexports::client as wayland_client; + use smithay_client_toolkit::reexports::client::protocol::*; + + pub mod __interfaces { + use smithay_client_toolkit::reexports::client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("protocols/river-status-unstable-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("protocols/river-status-unstable-v1.xml"); +} diff --git a/src/shared_state.rs b/src/shared_state.rs new file mode 100644 index 0000000..554d856 --- /dev/null +++ b/src/shared_state.rs @@ -0,0 +1,30 @@ +use smithay_client_toolkit::{ + reexports::client::QueueHandle, + shm::{slot::SlotPool, ShmState}, +}; + +use crate::{ + config::Config, + i3bar_protocol::Block, + state::{ComputedBlock, State}, + status_cmd::StatusCmd, +}; + +#[derive(Debug)] +pub struct SharedState { + pub qh: QueueHandle<State>, + pub shm_state: ShmState, + pub pool: Option<SlotPool>, + pub config: Config, + pub status_cmd: Option<StatusCmd>, + pub blocks: Vec<Block>, + pub blocks_cache: Vec<ComputedBlock>, +} + +impl SharedState { + pub fn get_pool(&mut self, len: usize) -> &mut SlotPool { + self.pool.get_or_insert_with(|| { + SlotPool::new(len, &self.shm_state).expect("Failed to create pool") + }) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..ed6c3d1 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,479 @@ +use std::{future::pending, io}; + +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_layer, delegate_output, delegate_pointer, delegate_registry, + delegate_seat, delegate_shm, + output::{OutputHandler, OutputState}, + reexports::client::{ + protocol::{wl_output, wl_pointer, wl_seat, wl_surface}, + Connection, EventQueue, QueueHandle, + }, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + seat::{ + pointer::{PointerEvent, PointerEventKind, PointerHandler, ThemeSpec, ThemedPointer}, + Capability, SeatHandler, SeatState, + }, + shell::layer::{ + Anchor, Layer, LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure, + }, + shm::{ShmHandler, ShmState}, +}; +use tokio::io::unix::AsyncFdReadyGuard; +use wayland_client::globals::GlobalList; + +use crate::{ + bar::Bar, + config::Config, + delegate_river_control, delegate_river_status, + i3bar_protocol::Block, + pointer_btn::PointerBtn, + river_protocols::{ + control::{RiverControlHandler, RiverControlState}, + status::{RiverOutputStatus, RiverStatusHandler, RiverStatusState}, + }, + shared_state::SharedState, + status_cmd::StatusCmd, + text::ComputedText, +}; + +pub struct State { + registry_state: RegistryState, + seat_state: SeatState, + output_state: OutputState, + compositor_state: CompositorState, + layer_state: LayerShell, + river_status_state: Option<RiverStatusState>, + river_control_state: Option<RiverControlState>, + + seats: Vec<Seat>, + bars: Vec<Bar>, + pub shared_state: SharedState, +} + +struct Seat { + seat: wl_seat::WlSeat, + pointer: wl_pointer::WlPointer, + themed_pointer: ThemedPointer, + pointer_surface: wl_surface::WlSurface, + finger_scroll: f64, +} + +impl State { + pub fn new(event_queue: &mut EventQueue<Self>, globals: &GlobalList) -> Self { + let mut error = Ok(()); + + let qh = event_queue.handle(); + + let config = Config::new() + .map_err(|e| error = Err(e)) + .unwrap_or_default(); + + let status_cmd = match &error { + Err(_) => None, + Ok(()) => config.command.as_ref().and_then(|cmd| { + StatusCmd::new(cmd) + .map_err(|e| error = Err(anyhow!(e))) + .ok() + }), + }; + + let mut this = Self { + registry_state: RegistryState::new(globals), + seat_state: SeatState::new(globals, &qh), + output_state: OutputState::new(globals, &qh), + compositor_state: CompositorState::bind(globals, &qh).unwrap(), + layer_state: LayerShell::bind(globals, &qh).unwrap(), + river_status_state: RiverStatusState::new(globals, &qh).ok(), + river_control_state: RiverControlState::new(globals, &qh).ok(), + + seats: Vec::new(), + bars: Vec::new(), + shared_state: SharedState { + qh: event_queue.handle(), + shm_state: ShmState::bind(globals, &qh).unwrap(), + pool: None, + config, + status_cmd, + blocks: Vec::new(), + blocks_cache: Vec::new(), + }, + }; + + if let Err(e) = error { + this.set_error(e.to_string()); + } + + this + } + + pub fn set_blocks(&mut self, blocks: Vec<Block>) { + self.shared_state.blocks = blocks; + self.draw_all(); + } + + pub fn set_error(&mut self, error: impl Into<String>) { + self.set_blocks(vec![Block { + full_text: error.into(), + ..Default::default() + }]); + } + + pub fn notify_available(&mut self) -> anyhow::Result<()> { + if let Some(cmd) = &mut self.shared_state.status_cmd { + if let Some(blocks) = cmd.notify_available()? { + self.set_blocks(blocks); + } + } + Ok(()) + } + + pub fn draw_all(&mut self) { + for bar in &mut self.bars { + bar.draw(&mut self.shared_state); + } + } + + pub async fn wait_for_status_cmd(&self) -> io::Result<AsyncFdReadyGuard<i32>> { + match &self.shared_state.status_cmd { + Some(cmd) => cmd.async_fd.readable().await, + None => { + pending::<()>().await; + unreachable!() + } + } + } +} + +impl CompositorHandler for State { + fn scale_factor_changed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + surface: &wl_surface::WlSurface, + scale: i32, + ) { + if let Some(bar) = self + .bars + .iter_mut() + .find(|bar| bar.layer.wl_surface() == surface) + { + bar.scale = scale; + bar.draw(&mut self.shared_state); + } + } + + fn frame( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + } +} + +impl OutputHandler for State { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output( + &mut self, + _conn: &Connection, + qh: &QueueHandle<Self>, + output: wl_output::WlOutput, + ) { + let height = self.shared_state.config.height; + let surface = self.compositor_state.create_surface(qh).unwrap(); + let layer = LayerSurface::builder() + .output(&output) + .size((0, height)) + .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT) + .namespace("i3bar-river") + .map(qh, &self.layer_state, surface, Layer::Top) + .expect("layer surface creation"); + let river_output_status = self + .river_status_state + .as_mut() + .map(|s| s.new_output_status(qh, &output)); + self.bars.push(Bar { + configured: false, + width: 0, + height, + scale: 1, + layer, + blocks_btns: Default::default(), + river_output_status, + river_control: self.river_control_state.clone(), + tags_btns: Default::default(), + tags_info: Default::default(), + tags_computed: Vec::new(), + }); + } + + fn update_output( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + _output: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + _output: wl_output::WlOutput, + ) { + } +} + +impl LayerShellHandler for State { + fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, layer: &LayerSurface) { + self.bars.retain(|b| &b.layer != layer) + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + layer: &LayerSurface, + configure: LayerSurfaceConfigure, + _serial: u32, + ) { + self.bars + .iter_mut() + .find(|b| &b.layer == layer) + .unwrap() + .configure(&mut self.shared_state, configure.new_size.0); + } +} + +impl SeatHandler for State { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle<Self>, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Pointer { + let (pointer, themed_pointer) = self + .seat_state + .get_pointer_with_theme(qh, &seat, ThemeSpec::System, 1) + .expect("Failed to create pointer"); + self.seats.push(Seat { + seat, + pointer, + themed_pointer, + pointer_surface: self.compositor_state.create_surface(qh).unwrap(), // TODO: make a PR to remove this unwrap + finger_scroll: 0.0, + }); + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Pointer { + self.seats.retain(|p| p.seat == seat); + } + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {} +} + +impl PointerHandler for State { + fn pointer_frame( + &mut self, + conn: &Connection, + _qh: &QueueHandle<Self>, + pointer: &wl_pointer::WlPointer, + events: &[PointerEvent], + ) { + let seat = self + .seats + .iter_mut() + .find(|p| &p.pointer == pointer) + .unwrap(); + for event in events { + if let Some(bar) = self + .bars + .iter_mut() + .find(|b| b.layer.wl_surface() == &event.surface) + { + match event.kind { + PointerEventKind::Enter { .. } => { + seat.themed_pointer + .set_cursor( + conn, + "default", + self.shared_state.shm_state.wl_shm(), + &seat.pointer_surface, + ) + .unwrap(); + } + PointerEventKind::Press { button, .. } => { + bar.click( + &mut self.shared_state, + button.into(), + &seat.seat, + event.position.0, + event.position.1, + ) + .unwrap(); + } + PointerEventKind::Axis { + vertical, source, .. + } => { + if source == Some(wl_pointer::AxisSource::Finger) { + if self.shared_state.config.invert_touchpad_scrolling { + seat.finger_scroll -= vertical.absolute; + } else { + seat.finger_scroll += vertical.absolute; + } + if vertical.stop { + seat.finger_scroll = 0.0; + } + } + + let btn = if vertical.discrete > 0 { + PointerBtn::WheelDown + } else if vertical.discrete < 0 { + PointerBtn::WheelUp + } else if seat.finger_scroll >= 15.0 { + seat.finger_scroll = 0.0; + PointerBtn::WheelDown + } else if seat.finger_scroll <= -15.0 { + seat.finger_scroll = 0.0; + PointerBtn::WheelUp + } else { + continue; + }; + + bar.click( + &mut self.shared_state, + btn, + &seat.seat, + event.position.0, + event.position.1, + ) + .unwrap(); + } + _ => (), + } + } + } + } +} + +impl ShmHandler for State { + fn shm_state(&mut self) -> &mut ShmState { + &mut self.shared_state.shm_state + } +} + +impl RiverStatusHandler for State { + fn river_status_state(&mut self) -> &mut RiverStatusState { + self.river_status_state.as_mut().unwrap() + } + + fn focused_tags_updated( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + output_status: &RiverOutputStatus, + focused: u32, + ) { + let bar = self + .bars + .iter_mut() + .find(|b| b.river_output_status.as_ref() == Some(output_status)) + .unwrap(); + bar.tags_info.focused = focused; + bar.draw(&mut self.shared_state); + } + + fn urgent_tags_updated( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + output_status: &RiverOutputStatus, + urgent: u32, + ) { + let bar = self + .bars + .iter_mut() + .find(|b| b.river_output_status.as_ref() == Some(output_status)) + .unwrap(); + bar.tags_info.urgent = urgent; + bar.draw(&mut self.shared_state); + } + + fn views_tags_updated( + &mut self, + _conn: &Connection, + _qh: &QueueHandle<Self>, + output_status: &RiverOutputStatus, + tags: Vec<u32>, + ) { + let bar = self + .bars + .iter_mut() + .find(|b| b.river_output_status.as_ref() == Some(output_status)) + .unwrap(); + bar.tags_info.active = tags.into_iter().fold(0, |a, b| a | b); + bar.draw(&mut self.shared_state); + } +} + +impl RiverControlHandler for State { + fn river_control_state(&mut self) -> &mut RiverControlState { + self.river_control_state.as_mut().unwrap() + } + + fn command_failure(&mut self, _: &Connection, _: &QueueHandle<Self>, message: String) { + self.set_error(format!("[river_control] {message}")); + } + + fn command_success(&mut self, _: &Connection, _: &QueueHandle<Self>, message: String) { + info!("river_control: {message}"); + } +} + +delegate_compositor!(State); +delegate_output!(State); +delegate_shm!(State); +delegate_seat!(State); +delegate_pointer!(State); +delegate_layer!(State); +delegate_registry!(State); +delegate_river_status!(State); +delegate_river_control!(State); + +impl ProvidesRegistryState for State { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState,]; +} + +#[derive(Debug)] +pub struct ComputedBlock { + pub block: Block, + pub full: ComputedText, + pub short: Option<ComputedText>, + pub min_width: Option<f64>, +} diff --git a/src/status_cmd.rs b/src/status_cmd.rs index a229200..4d9c453 100644 --- a/src/status_cmd.rs +++ b/src/status_cmd.rs @@ -1,18 +1,22 @@ -use std::io::{BufRead, BufReader, Result, Write}; +use std::io::{BufRead, BufReader, Write}; use std::os::unix::prelude::AsRawFd; use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; -use smithay_client_toolkit::reexports::calloop::{self, LoopHandle}; +use anyhow::Result; + +use tokio::io::unix::AsyncFd; +use tokio::io::Interest; use crate::i3bar_protocol::{Block, Event, Protocol}; -use crate::BarState; +#[derive(Debug)] pub struct StatusCmd { - child: Child, + pub child: Child, output: BufReader<ChildStdout>, input: ChildStdin, protocol: Protocol, buf: Vec<u8>, + pub async_fd: AsyncFd<i32>, } impl StatusCmd { @@ -24,17 +28,22 @@ impl StatusCmd { .spawn()?; let output = child.stdout.take().unwrap(); let input = child.stdin.take().unwrap(); + let async_fd = AsyncFd::with_interest(output.as_raw_fd(), Interest::READABLE)?; Ok(Self { child, output: BufReader::new(output), input, protocol: Protocol::Unknown, buf: Vec::new(), + async_fd, }) } pub fn notify_available(&mut self) -> Result<Option<Vec<Block>>> { let buf = self.output.fill_buf()?; + if buf.is_empty() { + bail!("status command exited"); + } if self.buf.is_empty() { let rem = self.protocol.process_new_bytes(buf)?; if !rem.is_empty() { @@ -57,35 +66,35 @@ impl StatusCmd { pub fn send_click_event(&mut self, event: &Event) -> Result<()> { if self.protocol.supports_clicks() { - writeln!(self.input, "{}", serde_json::to_string(event).unwrap(),)?; + writeln!(self.input, "{}", serde_json::to_string(event).unwrap())?; } Ok(()) } - pub fn quick_insert(&self, handle: LoopHandle<BarState>) { - handle - .insert_source( - calloop::generic::Generic::new( - self.output.get_ref().as_raw_fd(), - calloop::Interest { - readable: true, - writable: false, - }, - calloop::Mode::Level, - ), - move |ready, _, bar_state| { - if ready.readable { - bar_state.notify_available()?; - Ok(calloop::PostAction::Continue) - } else { - bar_state.set_error("error reading from status command"); - if let Some(mut child) = bar_state.status_cmd.take() { - let _ = child.child.kill(); - } - Ok(calloop::PostAction::Remove) - } - }, - ) - .expect("failed to inser calloop source"); - } + // pub fn quick_insert(&self, handle: LoopHandle<BarState>) { + // handle + // .insert_source( + // calloop::generic::Generic::new( + // self.output.get_ref().as_raw_fd(), + // calloop::Interest { + // readable: true, + // writable: false, + // }, + // calloop::Mode::Level, + // ), + // move |ready, _, bar_state| { + // if ready.readable { + // bar_state.notify_available()?; + // Ok(calloop::PostAction::Continue) + // } else { + // bar_state.set_error("error reading from status command"); + // if let Some(mut child) = bar_state.status_cmd.take() { + // let _ = child.child.kill(); + // } + // Ok(calloop::PostAction::Remove) + // } + // }, + // ) + // .expect("failed to inser calloop source"); + // } } diff --git a/src/tags.rs b/src/tags.rs index 8114a74..d22853d 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,19 +1,21 @@ -use crate::text::{Attributes, ComputedText, Text}; +use crate::text::{Attributes, ComputedText}; use cairo::Context; use pango::FontDescription; use pangocairo::{cairo, pango}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TagState { + Urgent, Focused, + Active, Inactive, - Urgent, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct TagsInfo { pub focused: u32, pub urgent: u32, + pub active: u32, } impl TagsInfo { @@ -22,15 +24,18 @@ impl TagsInfo { TagState::Urgent } else if self.focused >> tag & 1 == 1 { TagState::Focused + } else if self.active >> tag & 1 == 1 { + TagState::Active } else { TagState::Inactive } } } -pub fn compute_tag_label(label: String, font: FontDescription, context: &Context) -> ComputedText { - let text = Text { - attr: Attributes { +pub fn compute_tag_label(label: &str, font: &FontDescription, context: &Context) -> ComputedText { + ComputedText::new( + label, + Attributes { font, padding_left: 25.0, padding_right: 25.0, @@ -38,7 +43,6 @@ pub fn compute_tag_label(label: String, font: FontDescription, context: &Context align: Default::default(), markup: false, }, - text: label, - }; - text.compute(context) + context, + ) } diff --git a/src/text.rs b/src/text.rs index 60ed707..9b27a11 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,9 +1,8 @@ use crate::color::Color; -use pango::{EllipsizeMode, FontDescription}; +use pango::FontDescription; use pangocairo::{cairo, pango}; use serde::Deserialize; use std::f64::consts::{FRAC_PI_2, PI, TAU}; -use std::ops::Deref; #[derive(Clone, Debug, PartialEq)] pub struct RenderOptions { @@ -17,8 +16,8 @@ pub struct RenderOptions { } #[derive(Clone, Debug, PartialEq)] -pub struct Attributes { - pub font: FontDescription, +pub struct Attributes<'a> { + pub font: &'a FontDescription, pub padding_left: f64, pub padding_right: f64, pub min_width: Option<f64>, @@ -40,96 +39,55 @@ impl Default for Align { } } -fn create_pango_layout(cairo_context: &cairo::Context) -> pango::Layout { - pangocairo::create_layout(cairo_context).unwrap() -} - -fn show_pango_layout(cairo_context: &cairo::Context, layout: &pango::Layout) { - pangocairo::show_layout(cairo_context, layout); -} - #[derive(Clone, Debug, PartialEq)] -pub struct Text { - pub attr: Attributes, - pub text: String, +pub struct ComputedText { + pub width: f64, + layout: pango::Layout, + height: f64, + padding_left: f64, } -impl Text { - pub fn compute(mut self, context: &cairo::Context) -> ComputedText { - let (mut width, height) = { - let layout = create_pango_layout(context); - layout.set_font_description(Some(&self.attr.font)); - if self.attr.markup { - layout.set_markup(&self.text); - } else { - layout.set_text(&self.text); - } +impl ComputedText { + pub fn new(text: &str, mut attr: Attributes, context: &cairo::Context) -> Self { + let layout = pangocairo::create_layout(context); + layout.set_font_description(Some(attr.font)); + if attr.markup { + layout.set_markup(text); + } else { + layout.set_text(text); + } - let (text_width, text_height) = layout.pixel_size(); - let width = f64::from(text_width) + self.attr.padding_right + self.attr.padding_right; - let height = f64::from(text_height); - (width, height) - }; + let (text_width, text_height) = layout.pixel_size(); + let mut width = f64::from(text_width) + attr.padding_right + attr.padding_right; + let height = f64::from(text_height); - if let Some(min_width) = self.attr.min_width { + if let Some(min_width) = attr.min_width { if width < min_width { let d = min_width - width; width = min_width; - match self.attr.align { - Align::Right => self.attr.padding_left += d, - Align::Left => self.attr.padding_right += d, + match attr.align { + Align::Right => attr.padding_left += d, + Align::Left => attr.padding_right += d, Align::Center => { - self.attr.padding_left += d * 0.5; - self.attr.padding_right += d * 0.5; + attr.padding_left += d * 0.5; + attr.padding_right += d * 0.5; } } } } - ComputedText { - text: self, + Self { width, + layout, height, + padding_left: attr.padding_left, } } -} -#[derive(Clone, Debug, PartialEq)] -pub struct ComputedText { - pub text: Text, - pub width: f64, - pub height: f64, -} - -impl Deref for ComputedText { - type Target = str; - - fn deref(&self) -> &Self::Target { - &self.text.text - } -} - -impl ComputedText { pub fn render(&self, context: &cairo::Context, options: RenderOptions) { - let text = &self.text; - let layout = create_pango_layout(context); - layout.set_font_description(Some(&text.attr.font)); - if text.attr.markup { - layout.set_markup(&text.text); - } else { - layout.set_text(&text.text); - } - context.save().unwrap(); context.translate(options.x_offset - options.overlap, 0.0); - // Set the width/height on the Pango layout so that it word-wraps/ellipises. - let text_width = self.width - text.attr.padding_left - text.attr.padding_right; - let text_height = self.height; - layout.set_ellipsize(EllipsizeMode::End); - layout.set_width(text_width as i32 * pango::SCALE); - layout.set_height(text_height as i32 * pango::SCALE); - // Draw background if let Some(bg) = options.bg_color { bg.apply(context); @@ -147,10 +105,10 @@ impl ComputedText { options.fg_color.apply(context); context.translate( - text.attr.padding_left + options.overlap, + self.padding_left + options.overlap, (options.bar_height - self.height) * 0.5, ); - show_pango_layout(context, &layout); + pangocairo::show_layout(context, &self.layout); context.restore().unwrap(); } } @@ -177,16 +135,17 @@ fn rounded_rectangle( } pub fn width_of(text: &str, context: &cairo::Context, markup: bool, font: &FontDescription) -> f64 { - let text = Text { - text: text.into(), - attr: Attributes { - font: font.clone(), + ComputedText::new( + text, + Attributes { + font, padding_left: 0.0, padding_right: 0.0, min_width: None, align: Default::default(), markup, }, - }; - text.compute(context).width + context, + ) + .width } |
