Quickstart guide for Rust

Back to main page

This guide is available for both Python and Rust.

Overview

To create a plug-in for camlet, you’ll first need to create a new directory for your plug-in in ~/.config/camlet/plugins, e.g.

cd ~/.config/camlet/plugins
mkdir example-plugin-name
cd example-plugin-name

And create a file called about.txt to store the name and description of your plug-in:

Example plug-in name
This is an example plug-in which does something.

Then create a cargo crate for your plug-in.

cargo init
cargo add camlet

And for development, it’s helpful to create a symlink to the executable file so it automatically updates whenever you rebuild the plug-in:

ln -s target/debug/example-plugin-name run

Now, here’s a simple source file that does the bare minimum to get a working plug-in:

struct Plugin {
    // Could store things here...
}

impl camlet::Plugin for Plugin {
    fn init(_command: &mut camlet::CommandInit)
        -> Result<Plugin, Box<dyn std::error::Error>> {
        // You can also report an error with Err(...) if
        // something goes wrong while initializing.
        Ok(Plugin {})
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

Put this in main.rs, then run

cargo build

You now have a plug-in which does nothing! You can try running camlet, and it’ll be the same as before, except you’ll see your plugin’s name listed in the “Plug-ins” section of the menu. You can try enabling and disabling it, not that it’ll do anything.

camlet watches plug-in executable files for changes, so it’ll automatically reload the plug-in whenever you run cargo build.

The general model of camlet’s plug-in system is that camlet sends plug-ins commands when something happens. Certain commands also allow sending information back to camlet about what it should do in response. You can see the full list of commands here. In the following sections, we’ll handle and respond to commands in various ways, to make plug-ins that do something instead of nothing.

Open photos when taken

First, let’s make a plug-in which opens photos whenever they’re taken in the user’s default image viewer.

To do this, we listen for the photo command which indicates that a photo will be taken, and make note of the path where it will be saved. In theory, an error could occur, such as the user’s disk being full or another plug-in cancelling the photo, so then we need to wait for _photo_ok command which indicates that the photo was successfully saved to disk. Here’s the code:

use std::process::{Child, Command};

#[derive(Default)]
struct Plugin {
    // Array of photo viewer processes.
    child_processes: Vec<Child>,
    // Path where the photo will be saved.
    photo_path: String,
}

impl camlet::Plugin for Plugin {
    fn init(
        _: &mut camlet::CommandInit,
    ) -> Result<Plugin, Box<dyn std::error::Error>> {
        Ok(Plugin::default())
    }

    fn photo(&mut self, command: &mut camlet::CommandPhoto) {
        // Keep track of the photo path.
        self.photo_path = command.path().to_owned();
    }

    fn photo_ok(&mut self, _: &mut camlet::CommandPhotoOk) {
        // Check for zombie processes
        self.child_processes.retain_mut(|process| {
            // try_wait() returns Ok(Some(…)) if the process exited
            !matches!(process.try_wait(), Ok(Some(_)))
        });
        // Open the photo with xdg-open (open's the user's
        // default application for the file type).
        let result =
            Command::new("xdg-open").arg(&self.photo_path).spawn();
        match result {
            Ok(process) => self.child_processes.push(process),
            Err(e) => eprintln!("error opening photo viewer: {e}"),
        }
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

Unfortunately we have to keep track of a list of running photo viewer processes, so that we can properly clean up zombie processes, so it’s not as simple as it could be. Every time we get the _photo_ok command, we also look at old photo viewer processes to see if they’re done running. Otherwise Linux will keep around some of their data around indefinitely because it thinks we care what happened to them. To be honest, though, you could leave that part out and it probably wouldn’t matter unless the user decides to take a hundred thousand photos without closing camlet.

User-customized burst capture

Next, let’s look at a more sophisticated example: adding a shortcut to camlet for taking a “burst” of photos. We’ll start by hardcoding the B key to take five photos at ½ second intervals:

use std::time::Instant;

struct Plugin {
    // Number of photos left to take in burst sequence
    burst_photos_left: u32,
    // Time at which last burst photo was taken
    last_photo_time: Instant,
}

impl Plugin {
    fn seconds_since_last_photo(&self) -> f32 {
        Instant::now()
            .duration_since(self.last_photo_time)
            .as_secs_f32()
    }
}

impl camlet::Plugin for Plugin {
    fn init(
        _: &mut camlet::CommandInit,
    ) -> Result<Plugin, Box<dyn std::error::Error>> {
        Ok(Plugin {
            burst_photos_left: 0,
            last_photo_time: Instant::now(),
        })
    }

    fn frame(&mut self, frame: &mut camlet::CommandFrame) {
        if !frame.is_photo_mode() {
            // Stop burst photo if we're in video mode.
            self.burst_photos_left = 0;
        }
        // If there are photos left to take, and it's been
        // long enough,
        if self.burst_photos_left > 0
            && self.seconds_since_last_photo() > 0.5
        {
            // Take a photo now
            frame.take_photo();
            // Decrement number of photos to take
            self.burst_photos_left -= 1;
            // Record current time
            self.last_photo_time = Instant::now();
        }
    }

    fn key_down(&mut self, key_down: &mut camlet::CommandKeyDown) {
        if key_down.name() == "B" {
            // B key was pressed - start burst capture
            self.burst_photos_left = 5;
        }
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

Try it out!

Note the use of the frame command, which runs every frame camlet is open, which is used in this case to check if enough time has passed to take a new photo. Many things can only be done in response to a frame command—it’s kind of the “kitchen sink” command—and taking photos is one of them.

Well, it’s nice, but what if someone wants to use a different key? Or take a different number of photos? That’s the perfect job for plug-in settings. We’ll define the settings in the initializer, then we receive the settings one at a time through the _setting command, first when camlet starts up, then again whenever they’re changed. Everything else is handled by camlet.

use std::time::Instant;

struct Plugin {
    burst_photos_left: u32,
    last_photo_time: Instant,
    // Amount of time in seconds between burst photos.
    photo_interval: f32,
    // Number of photos to take in a burst capture.
    photo_count: u32,
    // Keyboard shortcut for taking a burst capture.
    shortcut: String,
}

impl Plugin {
    fn seconds_since_last_photo(&self) -> f32 {
        Instant::now()
            .duration_since(self.last_photo_time)
            .as_secs_f32()
    }
}

impl camlet::Plugin for Plugin {
    fn init(
        init: &mut camlet::CommandInit,
    ) -> Result<Plugin, Box<dyn std::error::Error>> {
        // Define keyboard shortcut setting with default
        // value of "B".
        init.define_keyboard_setting(
            "shortcut",
            "Keyboard shortcut",
            "B",
        );
        // Define number setting with minimum 2, maximum 30,
        // step 1, default 5.
        init.define_number_setting(
            "count",
            "Number of photos to take",
            2.0,
            30.0,
            1.0,
            5.0,
        );
        // Define number setting with minimum 0.1, maximum 5,
        // step 0.1, default 0.5
        init.define_number_setting(
            "interval",
            "Time between photos",
            0.1,
            5.0,
            0.1,
            0.5,
        );
        Ok(Plugin {
            burst_photos_left: 0,
            last_photo_time: Instant::now(),
            photo_interval: 0.0,
            photo_count: 0,
            shortcut: String::new(),
        })
    }

    fn setting(&mut self, setting: &mut camlet::CommandSetting) {
        match setting.id() {
            "count" => {
                self.photo_count = setting.value().parse().unwrap()
            }
            "interval" => {
                self.photo_interval = setting.value().parse().unwrap()
            }
            "shortcut" => self.shortcut = setting.value().to_owned(),
            _ => panic!(
                "We should only get the settings defined in init()"
            ),
        }
    }

    fn frame(&mut self, frame: &mut camlet::CommandFrame) {
        if !frame.is_photo_mode() {
            self.burst_photos_left = 0;
        }
        if self.burst_photos_left > 0
            && self.seconds_since_last_photo() > self.photo_interval
        {
            frame.take_photo();
            self.burst_photos_left -= 1;
            self.last_photo_time = Instant::now();
        }
    }

    fn key_down(&mut self, key_down: &mut camlet::CommandKeyDown) {
        if key_down.combo() == self.shortcut {
            self.burst_photos_left = self.photo_count;
        }
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

Note that the ID of the setting (e.g. shortcut) is different from its name (e.g. Keyboard shortcut). You can use a shorter name for the ID, and you can change the displayed name of the setting without resetting its value for everyone. We define a “keyboard” setting for the keyboard shortcut, and a number setting for the number of photos and the interval between photos. Note that we are now using the combo value of the _key_down command. This includes both the name of the key and any modifiers (Ctrl, Shift, etc.) that are being held. So, if the user chooses Ctrl+B as the shortcut for example, it’ll be handled automatically.

Try it out by going to the main menu and selecting “Plug-ins”, then finding your plug-in on the list. You should now be able to enter its settings menu and change the settings.

Monochrome shader

Now let’s look at maybe one of the most exciting features of camlet plug-ins: shaders. Shaders describe a function for transforming input data to output data on the GPU. In the case of camlet shaders, we’re transforming the pixels in the picture taken by the camera into pixels that will be displayed to the user. camlet shaders are written in the OpenGL Shading Language (GLSL). One of the simplest things we can do is convert the picture to grayscale, so let’s try that. First, create a file called shader.glsl in your plug-in’s root directory:

// Use GLSL version 130 (aka 3.00). You can use later versions
// to access new features, but they may not work on older devices.
#version 130

// Define a uniform for the picture passed into the shader.
// A uniform is a variable that's the same across all inputs (pixels).
uniform sampler2D camlet_frame;
// This is the input to the shader: the coordinate of the pixel.
// It's a vec2, which is a vector of 2 floating-point numbers.
// (0,0) is the bottom-left of the picture and (1,1) is the top-right.
in vec2 camlet_pos;
// This is the output of the shader: the color to paint the pixel.
// It's a vec3, which is a vector of 3 floating-point numbers,
// the red, green, and blue values of the pixel.
out vec3 camlet_color;

void main() {
    // Access the pixel in the input picture.
    // Note that texture returns a vec4 because OpenGL textures are
    // allowed to have up to 4 components per pixel. But this texture
    // only uses 3 components (RGB), so don't look at the fourth.
    vec4 input_pixel = texture(camlet_frame, camlet_pos);
    // Output a color which is a weighted average of the red (x),
    // green (y), and blue (z) components of the input pixel. The
    // weights roughly match how bright these colors appear to the
    // human eye, for maximal accuracy.
    camlet_color = vec3(0.21 * input_pixel.x
        + 0.72 * input_pixel.y
        + 0.07 * input_pixel.z);
    // ↑ here's a little GLSL trick: vec3(float) will create a
    //   vec3 with all components equal to the float.
}

Note that the names camlet_frame, camlet_pos, camlet_color, and main are all important here — you can’t change them!

(If you’ve never seen GLSL before, there are lots of resources for learning about it online, including the OpenGL cheat sheet—scroll down a bit to get to the GLSL stuff—and this 20-minute video tutorial by Inigo Quilez. Also there are reference pages for all GLSL functions in the OpenGL refpages.)

Now we have to tell camlet to use our shader. We can do this with the following plug-in code

struct Plugin {}

impl camlet::Plugin for Plugin {
    fn init(
        _: &mut camlet::CommandInit,
    ) -> Result<Plugin, Box<dyn std::error::Error>> {
        Ok(Plugin {})
    }

    fn frame(&mut self, frame: &mut camlet::CommandFrame) {
        frame.set_camera_shader(
            camlet::Shader::from_path("shader.glsl")
                .expect("Couldn't load shader file"),
        );
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

We’re setting it as a camera shader. There are also overlay shaders, which apply to the whole window, including letterboxing, and aren’t included in photos or videos (these are useful for displaying textual information, etc.).

Note that since we’re creating the shader from a file path, this means that the shader code will be read from the file every single frame. This is convenient for development, since the shader will automatically update when the file is changed, but for releasing it to the world, you may want to read the file once in the initializer, then create a shader from a string instead when setting it, using Shader::new, to lower the resource usage of the plug-in.

Stop-motion helper shader

Here’s a little example that’s actually the inspiration behind camlet’s whole plug-in system. I wanted a way of superimposing the last photo that was taken over the camera feed, for making stop-motion animations.

This will give us an opportunity to look at some more ways of controlling shaders in camlet plug-ins, namely custom uniforms, texture grabbing, and texture binding.

First, let’s put the shader code in shader.glsl as before:

#version 130

uniform sampler2D camlet_frame;
// Texture of last photo that was taken
uniform sampler2D last_photo;
// User-controlled opacity at which we should display the last photo.
uniform float opacity;

in vec2 camlet_pos;
// You can declare camlet_color as a vec3 or vec4. We use vec4
// here for convenience. The last component is ignored.
out vec4 camlet_color;

void main() {
    // Color of the pixel coming from the camera.
    vec4 input_pixel = texture(camlet_frame, camlet_pos);
    // Color of the same pixel in the last photo that was taken.
    vec4 last_photo_pixel = texture(last_photo, camlet_pos);
    // GLSL "mix" function creates a mix of two values.
    // If the third argument is 0, the first argument is returned.
    // If it's 1, the second argument is returned.
    // If it's something between 0 and 1, a corresponding
    // mix of the first and second arguments is returned.
    camlet_color = mix(input_pixel, last_photo_pixel, opacity);
}

And here’s the plug-in code:

#[derive(Default)]
struct Plugin {
    // Texture ID of the last photo that was taken, or 0 if there is none.
    last_photo: u32,
    // Texture ID of an old photo we can release now, or 0 if there is none.
    old_photo: u32,
    // `true` if a photo was taken this frame.
    just_took_photo: bool,
    // User's chosen opacity for the superimposition
    opacity: f32,
}

impl camlet::Plugin for Plugin {
    fn init(
        init: &mut camlet::CommandInit,
    ) -> Result<Plugin, Box<dyn std::error::Error>> {
        init.define_number_setting(
            "opacity",
            "Superimposition opacity",
            0.1,
            0.9,
            0.1,
            0.5,
        );
        Ok(Plugin::default())
    }

    fn setting(&mut self, setting: &mut camlet::CommandSetting) {
        match setting.id() {
            "opacity" => {
                self.opacity = setting.value().parse().unwrap()
            }
            _ => {
                panic!("We should only get settings defined in init.")
            }
        }
    }

    fn frame(&mut self, frame: &mut camlet::CommandFrame) {
        if self.just_took_photo {
            // If we just took a photo, grab it as an OpenGL texture.
            self.just_took_photo = false;
            frame.grab_camera();
        }
        if self.old_photo != 0 {
            // Free up resources of old photos we don't need anymore.
            frame.release(self.old_photo);
            self.old_photo = 0;
        }
        if self.last_photo != 0 {
            let mut shader = camlet::Shader::from_path("shader.glsl")
                .expect("Couldn't load shader file");
            // Bind the photo texture to texture slot 1.
            // camlet shaders can use up to 31 textures at once,
            // in slots 1-31.
            shader.bind_textures([(1, self.last_photo)]);
            // Set the last_photo uniform to point to texture slot 1
            shader.set_uniform("last_photo", 1);
            // Set opacity uniform according to user's setting.
            shader.set_uniform("opacity", self.opacity);
            frame.set_camera_shader(shader);
        }
    }

    fn photo_ok(&mut self, _: &mut camlet::CommandPhotoOk) {
        // A photo was taken!
        self.just_took_photo = true;
    }

    fn grabbed(&mut self, command: &mut camlet::CommandGrabbed) {
        if self.last_photo != 0 {
            // Don't need this photo anymore - release it next
            // time we get a frame command.
            self.old_photo = self.last_photo;
        }
        // Record the texture we got.
        self.last_photo = command.texture();
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

In the frame command, we ask camlet to grab the camera feed if a photo was taken, so that we can use it in our shader. Then we release old textures we aren’t using anymore — it’s important to do this, otherwise your video memory will keep filling up with old textures until you quit camlet. Next, if a photo has been taken, we set up our shader, by binding the photo texture to a slot, so that we can use it in our shader, then assigning the value of the last_photo uniform to the index of the slot, and lastly assigning the value of the opacity uniform according to the user’s preference.

Cropper

Lastly, we’ll look at something more complicated: a plug-in that lets you crop the camera feed, complete with a crosshair indicator of where the crop will start, a box displaying the area that the frame will be cropped down to, and the coordinates of the crop in the bottom-left corner.

For this, we’ll need a few new features of camlet plug-ins:

To get started, download crosshair.png, and put it in your plug-in’s directory.

We’ll start by looking at the camera shader, which we’ll save to shader.glsl as before. This shader does three things:

#version 130

uniform sampler2D camlet_frame;
// "crosshair" image to display at mouse position
uniform sampler2D crosshair;
// mouse position in 0-1 coordinates.
uniform vec2 mouse_pos;
// first crop corner in 0-1 coordinates.
uniform vec2 crop_pos;
// picture_width / picture_height
uniform float aspect_ratio;
// Scale/offset to use to crop the incoming camera feed
uniform vec2 frame_scale;
uniform vec2 frame_offset;

in vec2 camlet_pos;
out vec4 camlet_color;

// Gives the signed distance to a box centered at p with "radius" b
// Negative = inside the box, positive = outside the box
// https://iquilezles.org/articles/distfunctions2d/
float sd_box(vec2 p, vec2 b)
{
    vec2 d = abs(p)-b;
    return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}

void main() {
    // Compute the cropped texture coordinate
    vec2 texture_pos = camlet_pos * frame_scale + frame_offset;
    vec4 input_pixel = texture(camlet_frame, texture_pos);
    // Vector which you can multiply coordinates by to fix the
    // fact that the width/height of the frame aren't necessarily
    // the same, and we don't want things to be stretched.
    vec2 fix_aspect = vec2(aspect_ratio, 1.);
    vec4 final_color = input_pixel;
    if (crop_pos.x < 0.0) {
        // Draw crosshair image.
        vec2 crosshair_coord =
            (camlet_pos - mouse_pos) * 40.0
            * fix_aspect + 0.5;
        // If we're at a coordinate where we should draw the crosshair,
        if (min(crosshair_coord.x, crosshair_coord.y) >= 0.0
            && max(crosshair_coord.x, crosshair_coord.y) <= 1.0) {
            // Sample the crosshair texture
            vec4 crosshair_pixel = texture(crosshair, crosshair_coord);
            // Mix it with the camera feed according to the
            // alpha (opacity) of the crosshair pixel.
            final_color = mix(final_color, crosshair_pixel, crosshair_pixel.w);
        }
    } else {
        // Draw the crop box.
        vec2 crop_center = (mouse_pos + crop_pos) * 0.5;
        vec2 crop_radius = abs(mouse_pos - crop_pos) * 0.5;
        // Compute signed distance to crop box, to determine
        // whether we're inside, outside, or on the border.
        float box_dist = sd_box(
            (camlet_pos - crop_center) * fix_aspect,
            crop_radius * fix_aspect);
        if (box_dist < 0.0) {
            // (Inside the crop box)
        } else if (box_dist < 0.004) {
            // Draw a border around the crop box.
            final_color = vec4(0.0);
        } else {
            // Darken everything outside of the crop box.
            final_color *= 0.6;
        }
    }
    camlet_color = final_color;
}

Next, let’s look at the overlay shader, which we’ll save to overlay.glsl. This shader’s sole function is to draw the text with the coordinates in the bottom-left corner of the screen.

#version 130

uniform sampler2D camlet_frame;
uniform sampler2D text;
uniform vec2 window_size;
in vec2 camlet_pos;
out vec4 camlet_color;

void main() {
    vec4 input_pixel = texture(camlet_frame, camlet_pos);
    // Compute size of text, in the same coordinate system
    // as camlet_pos.
    vec2 text_size = vec2(textureSize(text, 0)) / window_size;
    // If we're in the bottom-left part of the screen,
    if (camlet_pos.x < text_size.x &&
        camlet_pos.y < text_size.y) {
        // Get the color of the text at this point
        float text_pixel = texture(text,camlet_pos / text_size).x;
        // Blend it with the incoming color. We first multiply
        // the incoming color by 0.5 so that the text gets a
        // semi-transparent black background.
        camlet_color = mix(input_pixel * 0.5,
            vec4(1.0),
            text_pixel);
    } else {
        // Otherwise, don't do anything.
        camlet_color = input_pixel;
    }
}

Lastly, the plug-in code itself, which puts everything together and handles user input:

use camlet::{Shader, TextureSource};
use std::cmp::{max, min};

#[derive(Default)]
struct Plugin {
    /// Texture for the little crosshair icon
    crosshair: u32,
    /// Texture for the informational text
    text: u32,
    /// Position of first point of crop, if we're halfway
    /// through a crop.
    crop_pos: Option<[u32; 2]>,
    /// Current crop (x, y, width, height)
    crop: Option<[u32; 4]>,
    /// picture width/height according to last frame command
    picture_size: [u32; 2],
    /// camera width/height according to last frame command
    camera_size: [u32; 2],
    /// List of textures to release
    release: Vec<u32>,
    /// User preference about whether to display crop coordinates
    show_dimensions: bool,
}

impl Plugin {
    /// Set the resolution of the camera, and adjust stored coordinates
    /// as needed to fit inside.
    fn set_camera_size(&mut self, w: u32, h: u32) {
        self.camera_size = [w, h];
        if let Some(crop) = &mut self.crop {
            if crop[0] + crop[2] > w {
                crop[2] = w - crop[0];
            }
            if crop[1] + crop[3] > h {
                crop[3] = h - crop[1];
            }
        }
        if let Some(crop_pos) = &mut self.crop_pos {
            crop_pos[0] = min(crop_pos[0], w);
            crop_pos[1] = min(crop_pos[1], h);
        }
    }

    /// Get current crop
    fn crop(&self) -> [u32; 4] {
        self.crop.unwrap_or([
            0,
            0,
            self.camera_size[0],
            self.camera_size[1],
        ])
    }

    /// Convert from picture coordinates, where (0,0) is the top-left
    /// and (picture_width,picture_height) is the bottom-right of the
    /// displayed camera feed, to camera coordinates, where (0,0)
    /// is the top-left and (camera_width,camera_height) is the bottom-right
    /// of the uncropped camera feed.
    fn convert_picture_to_camera(&self, x: i32, y: i32) -> [u32; 2] {
        let x01 =
            (x as f64 / self.picture_size[0] as f64).clamp(0.0, 1.0);
        let y01 =
            (y as f64 / self.picture_size[1] as f64).clamp(0.0, 1.0);
        let crop = self.crop();
        [
            (crop[0] as f64 + crop[2] as f64 * x01) as u32,
            (crop[1] as f64 + crop[3] as f64 * y01) as u32,
        ]
    }

    /// Get the second crop corner given the mouse position and the
    /// first crop corner in self.crop_pos. This is to stop the crop
    /// from having a different aspect ratio from the camera and thus
    /// making the picture stretched.
    fn crop_second_point(&self, mouse_pos: [u32; 2]) -> [u32; 2] {
        let mouse_pos = mouse_pos.map(|x| x as f64);
        let crop_pos = self.crop_pos.unwrap().map(|x| x as f64);
        let mut dx = mouse_pos[0] - crop_pos[0];
        let aspect =
            self.camera_size[1] as f64 / self.camera_size[0] as f64;
        let mut dy = dx.abs() * aspect;
        if mouse_pos[1] < crop_pos[1] {
            dy = -dy;
        }
        if crop_pos[1] + dy < 0.0 {
            dy = -crop_pos[1];
            dx = (dy.abs() * dx.signum()) / aspect;
        }
        let camera_height = self.camera_size[1] as f64;
        if crop_pos[1] + dy >= camera_height {
            dy = camera_height - crop_pos[1];
            dx = (dy.abs() * dx.signum()) / aspect;
        }
        let mut dx = dx as i32;
        let mut dy = dy as i32;
        // Prevent division by zero
        if dx == 0 {
            dx = 1;
        }
        if dy == 0 {
            dy = 1;
        }
        [crop_pos[0] as i32 + dx, crop_pos[1] as i32 + dy]
            .map(|x| x as u32)
    }

    /// Convert camera coordinates (described above) to 0-1 OpenGL texture
    /// coordinates - (0,0) = bottom-left, (1,1) = top-right of the displayed
    /// camera feed.
    fn convert_camera_to_01(&self, [x, y]: [u32; 2]) -> [f32; 2] {
        let crop = self.crop().map(|x| x as f32);
        [
            (x as f32 - crop[0]) / crop[2],
            1.0 - (y as f32 - crop[1]) / crop[3],
        ]
    }
}

impl camlet::Plugin for Plugin {
    fn init(
        init: &mut camlet::CommandInit,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        // Define a setting that allows the user to select between
        // "yes" and "no" with a default of "yes"
        init.define_select_setting(
            "show_dimensions",
            "Display crop dimensions",
            ["yes", "no"],
            "yes",
        );
        Ok(Plugin::default())
    }
    fn frame(&mut self, frame: &mut camlet::CommandFrame) {
        // Release old textures we're not using anymore
        for texture in &self.release {
            frame.release(*texture);
        }
        self.release.clear();
        if frame.is_menu_open() {
            // Cancel crop if the menu is currently open
            self.crop_pos = None;
        }
        let picture_width = frame.picture_width();
        let picture_height = frame.picture_height();
        self.picture_size = [picture_width, picture_height];
        self.set_camera_size(
            frame.camera_width(),
            frame.camera_height(),
        );
        let mut mouse_pos = self.convert_picture_to_camera(
            frame.picture_mouse_x(),
            frame.picture_mouse_y(),
        );
        if self.crop_pos.is_some() {
            // Set mouse_pos to the second coordinate of the crop,
            // which may be different from the actual mouse position
            // since we need to adjust for the camera's aspect ratio.
            mouse_pos = self.crop_second_point(mouse_pos);
        }
        if self.crosshair == 0 {
            // Upload texture from image file to OpenGL. This can
            // then be used in shaders.
            frame.upload_texture("crosshair.png");
        } else {
            let mut shader = Shader::from_path("shader.glsl")
                .expect("couldn't load shader file");
            shader.bind_textures([(1, self.crosshair)]);
            shader.set_uniform("crosshair", 1);
            shader.set_uniform(
                "aspect_ratio",
                picture_width as f32 / picture_height as f32,
            );
            let crop = self.crop();
            shader.set_uniform(
                "frame_scale",
                [
                    crop[2] as f32 / self.camera_size[0] as f32,
                    crop[3] as f32 / self.camera_size[1] as f32,
                ],
            );
            let crop_right = (crop[1] + crop[3]) as f32;
            shader.set_uniform(
                "frame_offset",
                [
                    crop[0] as f32 / self.camera_size[0] as f32,
                    1.0 - (crop_right / self.camera_size[1] as f32),
                ],
            );
            shader.set_uniform(
                "crop_pos",
                self.crop_pos.map_or([-100.0, -100.0], |pos| {
                    self.convert_camera_to_01(pos)
                }),
            );
            shader.set_uniform(
                "mouse_pos",
                self.convert_camera_to_01(mouse_pos),
            );
            frame.set_camera_shader(shader);
        }
        let text;
        if let Some(crop_pos) = self.crop_pos {
            let width = mouse_pos[0].abs_diff(crop_pos[0]);
            let height = mouse_pos[1].abs_diff(crop_pos[1]);
            text = format!(
                "x: {}, y: {}, width: {width}, height: {height}",
                mouse_pos[0], mouse_pos[1]
            );
        } else {
            text =
                format!("x: {}, y: {}", mouse_pos[0], mouse_pos[1]);
        }
        // If the show_dimensions setting is set to "yes", display
        // some text with the coordinates of the prospective crop.
        if self.show_dimensions {
            frame.render_text(&text);
            if self.text == 0 {
                return;
            }
            let mut overlay = Shader::from_path("overlay.glsl")
                .expect("couldn't load shader file");
            overlay.bind_textures([(1, self.text)]);
            overlay.set_uniform("text", 1);
            overlay.set_uniform(
                "window_size",
                [
                    frame.window_width() as f32,
                    frame.window_height() as f32,
                ],
            );
            frame.set_overlay_shader(overlay);
        } else {
            frame.clear_overlay_shader();
        }
    }
    fn setting(&mut self, setting: &mut camlet::CommandSetting) {
        match setting.id() {
            "show_dimensions" => {
                self.show_dimensions = setting.value() == "yes";
            }
            _ => {}
        }
    }
    fn grabbed(&mut self, grabbed: &mut camlet::CommandGrabbed) {
        match grabbed.source() {
            TextureSource::Text => {
                // Text was rendered
                if self.text != 0 {
                    self.release.push(self.text);
                }
                self.text = grabbed.texture();
            }
            TextureSource::Upload => {
                // Crosshair image was uploaded
                self.crosshair = grabbed.texture();
            }
            _ => {}
        }
    }
    fn mouse_down(&mut self, command: &mut camlet::CommandMouseDown) {
        if command.button() == 'R' {
            // If the right mouse button is pressed,
            // reset everything.
            self.crop_pos = None;
            self.crop = None;
        }
        if command.button() != 'L' {
            // only handle left clicks from here on
            return;
        }
        let mut mouse_pos = self.convert_picture_to_camera(
            command.picture_x(),
            command.picture_y(),
        );
        match self.crop_pos {
            None => {
                // Set first crop corner.
                self.crop_pos = Some(mouse_pos);
            }
            Some(crop_pos) => {
                // Set second crop corner, and crop.
                mouse_pos = self.crop_second_point(mouse_pos);
                let crop_p0 = [
                    min(mouse_pos[0], crop_pos[0]),
                    min(mouse_pos[1], crop_pos[1]),
                ];
                let crop_p1 = [
                    max(mouse_pos[0], crop_pos[0]),
                    max(mouse_pos[1], crop_pos[1]),
                ];
                self.crop = Some([
                    crop_p0[0],
                    crop_p0[1],
                    crop_p1[0] - crop_p0[0],
                    crop_p1[1] - crop_p0[1],
                ]);
                self.crop_pos = None;
            }
        }
    }
}

fn main() -> std::process::ExitCode {
    camlet::run::<Plugin>()
}

Where to go next…

For more information about what you can do with plug-ins, check out the documentation for the camlet plug-in library here. You can also check out the command reference for more detailed information, or if you want to make bindings for another programming language. Have fun!