Skip to content

Making Your Own Blocks

Michael Palmos edited this page Jun 23, 2021 · 9 revisions

Making your own blocks is designed to be straightforward but flexible. I'm hoping to merge most community blocks, so if you do something cool, make a pull request!

Setup

Before developing, we should fork the repository so we can make commits and then eventually do a pull request later if we want to.

Just hit the fork button at the top right, and then clone the forked repo.

$ git clone https://github.com/<user>/wired-notify.git
$ cd ./wired_notify

And let's try a build to make sure everything's ok. If this fails, either your rust install is outdated or configured, you're missing dependencies, or I've screwed something up.

$ cargo build

Development

First of all, let's create the file for our block. For the purposes of this tutorial, I'm going to make a simplified TextBlock.

$ ls
Cargo.lock  Cargo.toml  LICENSE  README.md  readme_stuff  src  target  wired_derive  wired.ron
$ touch ./src/rendering/blocks/text_block_demo.rs    # Blocks go in /src/rendering/blocks/

Each block should have a Properties struct which defines both config parameters and internal state. This struct must implement Debug, Deserialize, and Clone.

// /src/rendering/blocks/notification_block.rs

use serde::Deserialize;
use crate::config::{Padding, Color};

// A custom struct for these parameters will make things clearer in configuration.
#[derive(Debug, Deserialize, Clone)]
pub struct WidthHeight {
    width: i32,
    height: i32,
}

#[derive(Debug, Deserialize, Clone)]
pub struct TextBlockDemoParameters {
    pub padding: Padding,
    pub text: String,
    pub font: String,
    pub dimensions: WidthHeight,
    pub color: Color,

    // Cached property example.
    #[serde(skip)]
    real_text: String,
}

To add Wired behavior to the block, we need to implement the DrawableLayoutElement trait, which has 2 required methods, and 3 optional methods:

use crate::maths_utility::{Vec2, Rect};
use crate::rendering::layout::{DrawableLayoutElement, Hook};

impl DrawableLayoutElement for TextBlockDemoParameters {
    // Required
    fn predict_rect_and_init(&mut self, hook: &Hook, offset: &Vec2, parent_rect: &Rect, window: &NotifyWindow) -> Rect {
    }
    fn draw(&self, hook: &Hook, offset: &Vec2, parent_rect: &Rect, window: &NotifyWindow) -> Rect {
    }

    // Optional
    fn update(&mut self, delta_time: Duration, window: &NotifyWindow) -> bool {
    }
    fn clicked(&mut self, _window: &NotifyWindow) -> bool {
    }
    fn hovered(&mut self, _entered: bool, _window: &NotifyWindow) -> bool {
    }
}

The first 2 methods, predict_rect_and_init() and draw() are very similar, and are mainly split to enable some optimizations and sequencing. Both of these must be implemented. predict_rect_and_init() is called once at window creation. It is responsible for predicting the size and position of the block so that the window size can be determined, but can also be used to cache some operations; ImageBlock uses this to resize the image once and then use that for successive draws, for instance.

use crate::maths_utility::{Vec2, Rect};
use crate::rendering::layout::{DrawableLayoutElement, LayoutBlock, Hook};
use crate::rendering::text::EllipsizeMode;
use crate::config::Config;

impl DrawableLayoutElement for TextBlockDemoParameters {
    fn predict_rect_and_init(&mut self, hook: &Hook, offset: &Vec2, parent_rect: &Rect, window: &NotifyWindow) -> Rect {
        // Replace our text in the config with notification text.
        // This has a bug, so don't use it in production.
        // See https://github.com/Toqozz/wired-notify/blob/master/src/rendering/blocks/text_block.rs for details.
        let text = self.text.clone().replace("%s", &window.notification.summary).replace("%b", &window.notification.body);

        // Every window has a text render component attached to help with some text operations: https://github.com/Toqozz/wired-notify/blob/master/src/rendering/text.rs
        window.text.set_text(&text, &self.font, self.dimensions.width, self.dimensions.height, &EllipsizeMode::default());
        // Get the rect surrounding this text, plus padding.
        let mut rect = window.text.get_sized_padded_rect(&self.padding, &self.dimensions.width, &self.dimensions.height);
        
        // Cache the text, so we don't have to do string replacement every iteration.
        self.real_text = text;

        // Lastly we need to position our rect correctly.  To do this, just feed `find_anchor_pos()` the provided parameters and
        // your new rect, and it will find the appropriate x, y coordinates.
        let pos = LayoutBlock::find_anchor_pos(hook, offset, parent_rect, &rect);
        rect.set_xy(pos.x, pos.y);
        rect
    }

    fn draw(&self, _hook: &Hook, _offset: &Vec2, parent_rect: &Rect, window: &NotifyWindow) -> Rect {
        // For drawing, wired uses rust bindings for the Cairo 2D graphics library.
        // Check out the documentation (https://www.cairographics.org/documentation/) and the rust bindings (https://docs.rs/cairo-rs/0.9.1/cairo/struct.Context.html) to learn more.
        // All drawing from this point should draw over the background.
        window.context.set_operator(cairo::Operator::Over);

        window.text.set_text(&self.real_text, &self.font, &self.dimensions.width, &self.dimensions.height as i32, &EllipsizeMode::default());

        // Since our rect doesn't update or move, it would be more efficient to cache this rect and just use that, but we're lazy.
        let mut rect = window.text.get_sized_padded_rect(&self.padding, &self.dimensions.width, &self.dimensions.height);
        let mut pos = LayoutBlock::find_anchor_pos(hook, offset, parent_rect, &rect);

        // Text is rendered starting from the top left hand corner of the provided position.  Since our rect has padding applied, we need
        // to add that padding to actually draw with padding.
        // `paint_padded()` does this work for you.
        window.text.paint_padded(&window.context, &pos, &self.color, &self.padding);

        rect.set_xy(pos.x, pos.y);
        rect
    }
}

Using It

To be able to use our block, we first have to tell Wired about it. This is pretty straightforward, and should hopefully be automated in the future.

Firstly, in src/rendering/blocks/mod.rs, add we add exports for our new block so our other source can access it:

pub mod notification_block;
pub mod text_block;
pub mod scrolling_text_block;
pub mod image_block;
pub mod button_block;

pub mod text_block_demo;  // same as the name of our file: text_block_demo.rs

pub use notification_block::*;
pub use text_block::*;
pub use scrolling_text_block::*;
pub use image_block::*;
pub use button_block::*;

pub use text_block_demo::*;  // as above

Then, in layout.rs, we find the LayoutElement enum and add a new entry:

pub enum LayoutElement {
    NotificationBlock(NotificationBlockParameters),
    TextBlock(TextBlockParameters),
    ScrollingTextBlock(ScrollingTextBlockParameters),
    ImageBlock(ImageBlockParameters),

    TextBlockDemo(TextBlockDemoParameters),
}

Lastly, compile and run so we can use our new block:

$ cargo run
   Compiling wired v0.8.0 (/home/toqoz/code/rust/wired)
    Finished dev [unoptimized + debuginfo] target(s) in 4.51s
     Running `target/debug/wired`
In queue for notification bus name -- is another notification daemon running?

If you get In queue for notification bus name -- is another notification daemon running?, you need to close the existing notification daemon:

In queue for notification bus name -- is another notification daemon running?
^C
$ pkill wired
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/wired`
Acquired notification bus name.
DBus Init Success.

Now you can use your new block just like any other!

// ~/.config/wired/wired.ron

...
layout_blocks: [
    (
        name: "root",
        parent: "",
        hook: Hook(parent_anchor: TL, self_anchor: TL),
        offset: Vec2(x: 7.0, y: 7.0),
        params: NotificationBlock((
            ...
        )),
    ),

    (
        name: "summary",
        parent: "root",
        hook: Hook(parent_anchor: TL, self_anchor: TL),
        offset: Vec2(x: 0.0, y: 0.0),
        params: TextBlockDemo((
            text: "%s",
            font: "Arial Bold 10",
            color: Color(hex: "#ebdbb2"),
            padding: Padding(left: 20.0, right: 20.0, top: 20.0, bottom: 20.0),
            dimensions: (width: 150.0, height: 50.0),   // struct names are optionally omitted in config.
        )),
    ),
    ...
],
...

We're done!

$ notify-send "Just the summary."

Demo Reveal


Pull Requests

I'm looking to merge most block pull requests; if you make something cool, share it! You might be asked to create a documentation page similar to the ones seen in the wiki.