Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,22 @@ Default single-key shortcuts:
- <kbd>u</kbd>: Blur tool
- <kbd>g</kbd>: Highlight tool

### Tool Modifiers and Keys
### Pointer Tool <sup>NEXTRELEASE</sup>

Annotations can be selected by click.
Newly created annotation will be autoselected.
It will update the toolbar to the annotation style and allows style changes.

Crop:
- Press <kbd>Esc</kbd> or <kbd>Ctrl</kbd>+right mouse<sup>0.22.0</sup> <sup>experimental</sup> button while editing to reset crop altogether <sup>0.21.0</sup>.
- Press <kbd>Enter</kbd> or <kbd>Ctrl</kbd>+left mouse<sup>0.22.0</sup> <sup>experimental</sup> while editing to finish editing crop and keep the crop area active <sup>0.21.0</sup>.
- Left click crop area when tool is active but not editing to resume editing<sup>0.21.0</sup>.
- Hold <kbd>Alt</kbd> to select between overlapping annotations.
- <kbd>Delete</kbd> deletes the selected annotation.
- Drag the resize handles to change the size.
- Grab at the selection border to move.
- Scroll up/down to raise or lower the annotation.
- Nudge by cursor keys.
- Double-click to edit text annotation.
- Horizontal resize on marker chages its number and vertical the extra ring.

### Tool Modifiers and Keys

Arrow and line:
- <kbd>Shift</kbd> to make tool snap to 15° steps.
Expand Down Expand Up @@ -170,6 +180,8 @@ resize = { mode = "smart" }
floating-hack = true
# Change to true to automatically copy to clipboard after every annotation change (0.21.0)
auto-copy = false
# Change to true to automatically select newly created annotation (NEXTRELEASE)
auto-select-new = false
# Exit directly after copy/save action. 0.21.0: change to list of triggers
# Note that exit-early-save-as was removed with 0.21.0.
early-exit = ["all"]
Expand Down
2 changes: 2 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ resize = { mode = "smart" }
floating-hack = true
# Change to true to automatically copy to clipboard after every annotation change (0.21.0)
auto-copy = false
# Change to true to automatically select newly created annotation (NEXTRELEASE)
auto-select-new = false
# Exit directly after copy/save action. 0.21.0: change to list of triggers
# Note that exit-early-save-as was removed with 0.21.0.
early-exit = ["all"]
Expand Down
10 changes: 10 additions & 0 deletions src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub struct Configuration {
annotation_size_factor: f32,
save_after_copy: bool,
auto_copy: bool,
auto_select: bool,
actions_on_enter: Vec<Action>,
actions_on_escape: Vec<Action>,
actions_on_right_click: Vec<Action>,
Expand Down Expand Up @@ -305,6 +306,9 @@ impl Configuration {
if let Some(v) = general.auto_copy {
self.auto_copy = v;
}
if let Some(v) = general.auto_select_new {
self.auto_select = v;
}
if let Some(v) = general.actions_on_enter {
self.actions_on_enter = v;
}
Expand Down Expand Up @@ -577,6 +581,10 @@ impl Configuration {
self.auto_copy
}

pub fn auto_select(&self) -> bool {
self.auto_select
}

pub fn actions_on_enter(&self) -> Vec<Action> {
self.actions_on_enter.clone()
}
Expand Down Expand Up @@ -683,6 +691,7 @@ impl Default for Configuration {
annotation_size_factor: 1.0,
save_after_copy: false,
auto_copy: false,
auto_select: false,
actions_on_enter: vec![],
actions_on_escape: vec![Action::Exit],
actions_on_right_click: vec![],
Expand Down Expand Up @@ -756,6 +765,7 @@ struct ConfigurationFileGeneral {
annotation_size_factor: Option<f32>,
save_after_copy: Option<bool>,
auto_copy: Option<bool>,
auto_select_new: Option<bool>,
output_filename: Option<String>,
actions_on_enter: Option<Vec<Action>>,
actions_on_escape: Option<Vec<Action>>,
Expand Down
153 changes: 130 additions & 23 deletions src/femtovg_area/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::{
configuration::Action,
math::{Vec2D, rect_ensure_in_bounds, rect_round},
sketch_board::SketchBoardInput,
tools::{CropTool, Drawable, Tool},
tools::{Drawable, Tool, Tools},
};

use super::{font_stack, set_font_stack};
Expand All @@ -38,6 +38,7 @@ pub struct FemtoVGArea {
font: RefCell<Option<FontId>>,
inner: RefCell<Option<FemtoVgAreaMut>>,
request_render: RefCell<Option<Vec<Action>>>,
post_render_refresh_selection: RefCell<Option<usize>>,
sender: RefCell<Option<Sender<SketchBoardInput>>>,
}

Expand All @@ -46,7 +47,6 @@ pub struct FemtoVgAreaMut {
background_image_id: Option<femtovg::ImageId>,
transparent_background_id: Option<femtovg::ImageId>,
active_tool: Rc<RefCell<dyn Tool>>,
crop_tool: Rc<RefCell<CropTool>>,
scale_factor: f32,
offset: Vec2D,
drawables: Vec<Box<dyn Drawable>>,
Expand All @@ -59,6 +59,7 @@ pub struct FemtoVgAreaMut {
drag_offset: Vec2D,
is_drag: bool,
is_reset: bool,
hidden_drawable_index: Option<usize>,
}

enum HistoryEntry {
Expand Down Expand Up @@ -115,6 +116,7 @@ impl GLAreaImpl for FemtoVGArea {
.expect("Did you call init before using FemtoVgArea?")
.update_transformation(canvas);
}

fn render(&self, _context: &gtk::gdk::GLContext) -> glib::Propagation {
self.ensure_canvas();

Expand Down Expand Up @@ -157,14 +159,22 @@ impl GLAreaImpl for FemtoVGArea {
{
eprintln!("Error rendering to framebuffer: {e}");
}

if let Some(index) = self.post_render_refresh_selection.borrow_mut().take() {
self.sender
.borrow()
.as_ref()
.expect("Did you call init before using FemtoVgArea?")
.emit(SketchBoardInput::RefreshSelectionBounds(index));
}
glib::Propagation::Stop
}
}

impl FemtoVGArea {
pub fn init(
&self,
sender: Sender<SketchBoardInput>,
crop_tool: Rc<RefCell<CropTool>>,
active_tool: Rc<RefCell<dyn Tool>>,
background_image: Pixbuf,
) {
Expand All @@ -174,7 +184,6 @@ impl FemtoVGArea {
background_image_id: None,
transparent_background_id: None,
active_tool,
crop_tool,
scale_factor: 1.0,
offset: Vec2D::zero(),
drawables: Vec::new(),
Expand All @@ -187,9 +196,11 @@ impl FemtoVGArea {
last_scale: initial_scale,
is_drag: false,
is_reset: false,
hidden_drawable_index: None,
});
self.sender.borrow_mut().replace(sender);
}

fn ensure_canvas(&self) {
if self.canvas.borrow().is_none() {
let c = self
Expand Down Expand Up @@ -321,19 +332,86 @@ impl FemtoVGArea {
self.request_render.borrow_mut().replace(actions.into());
self.obj().queue_render();
}

pub fn schedule_refresh_selection_after_render(&self, index: usize) {
self.post_render_refresh_selection
.borrow_mut()
.replace(index);
self.obj().queue_render();
}

pub fn set_parent_sender(&self, sender: Sender<SketchBoardInput>) {
self.sender.borrow_mut().replace(sender);
}
}

impl FemtoVgAreaMut {
pub fn commit(&mut self, drawable: Box<dyn Drawable>) {
// Keep at most one crop drawable
if drawable.is_crop() {
self.drawables.retain(|d| !d.is_crop());
}
self.undo_stack
.push(HistoryEntry::Drawable(drawable.clone_box()));
self.drawables.push(drawable);
self.redo_stack.clear();
}

pub fn last_drawable_index(&self) -> Option<usize> {
self.drawables.len().checked_sub(1)
}

pub fn crop_drawable_index(&self) -> Option<usize> {
self.drawables.iter().position(|d| d.is_crop())
}

// Hit-test all drawables and return all indices whose bounds contain `pos`, in order from topmost to bottommost.
pub fn hit_test(&self, pos: Vec2D) -> Vec<usize> {
let mut results = Vec::new();
for (i, d) in self.drawables.iter().enumerate().rev() {
if d.hit_test(pos, crate::tools::HIT_BORDER_TOLERANCE) {
results.push(i);
}
}
results
}

pub fn get_drawable_bounds(&self, index: usize) -> Option<(Vec2D, Vec2D)> {
self.drawables.get(index).and_then(|d| d.bounds())
}

pub fn get_drawable_clone(&self, index: usize) -> Option<Box<dyn Drawable>> {
self.drawables.get(index).map(|d| d.clone_box())
}

pub fn replace_drawable(&mut self, index: usize, drawable: Box<dyn Drawable>) {
if index < self.drawables.len() {
self.drawables[index] = drawable;
}
}

pub fn move_drawable_index(&mut self, index: usize, offset: isize) -> Option<usize> {
if index >= self.drawables.len() {
return None;
}
let new_index =
(index as isize + offset).clamp(0, self.drawables.len() as isize - 1) as usize;
let drawable = self.drawables.remove(index);
self.drawables.insert(new_index, drawable);
Some(new_index)
}

pub fn remove_drawable(&mut self, index: usize) {
if index < self.drawables.len() {
self.drawables.remove(index);
}
}

// Set (or clear) the drawable index to skip during rendering (used while drag-previewing).
pub fn set_hidden_drawable_index(&mut self, index: Option<usize>) {
self.hidden_drawable_index = index;
}

pub fn undo(&mut self) -> bool {
match self.undo_stack.pop() {
Some(HistoryEntry::Drawable(history_drawable)) => {
Expand All @@ -354,6 +432,12 @@ impl FemtoVgAreaMut {
match self.redo_stack.pop() {
Some(HistoryEntry::Drawable(mut drawable)) => {
drawable.handle_redo();

// Keep at most one crop drawable
if drawable.is_crop() {
self.drawables.retain(|d| !d.is_crop());
}

self.undo_stack
.push(HistoryEntry::Drawable(drawable.clone_box()));
self.drawables.push(drawable);
Expand Down Expand Up @@ -422,15 +506,18 @@ impl FemtoVgAreaMut {
self.background_image.height() as f32,
),
);
// get offset and size of the area in question

// get offset and size of the crop if there is one
let (pos, size) = self
.crop_tool
.borrow()
.get_crop()
.map(|c| c.get_rectangle())
.map(|rect| rect_ensure_in_bounds(rect, bounds))
.map(rect_round)
.filter(|(_, size)| !size.is_zero())
.drawables
.iter()
.find(|d| d.is_crop())
.and_then(|d| {
d.bounds().map(|(tl, br)| {
let rect = (tl, br - tl);
rect_ensure_in_bounds(rect_round(rect), bounds)
})
})
.unwrap_or(bounds);

// create render-target
Expand All @@ -451,7 +538,6 @@ impl FemtoVgAreaMut {
self.render(
canvas,
font,
false,
femtovg::Color::rgbaf(0.0, 0.0, 0.0, 0.0),
false,
)?;
Expand Down Expand Up @@ -484,7 +570,6 @@ impl FemtoVgAreaMut {
self.render(
canvas,
font,
true,
femtovg::Color::rgbaf(0.0, 0.0, 0.0, 0.0),
true,
)?;
Expand All @@ -496,7 +581,6 @@ impl FemtoVgAreaMut {
&mut self,
canvas: &mut femtovg::Canvas<femtovg::renderer::OpenGl>,
font: FontId,
render_crop: bool,
outside_bg_color: femtovg::Color,
onscreen: bool,
) -> Result<()> {
Expand All @@ -514,19 +598,42 @@ impl FemtoVgAreaMut {
self.background_image.height() as f32,
),
);
// Offscreen export should not include pointer selection handles.
let draw_active_tool =
onscreen || self.active_tool.borrow().get_tool_type() != Tools::Pointer;
let mut active_tool_drawn_in_stack = false;

// render the whole stack
for d in &mut self.drawables {
d.draw(canvas, font, bounds)?;
for (i, d) in self.drawables.iter().enumerate() {
if self.hidden_drawable_index == Some(i) {
// Draw the active tool preview in the original z position.
if draw_active_tool
&& let Some(preview) = self.active_tool.borrow().get_drawable()
&& !preview.is_crop()
{
preview.draw(canvas, font, bounds)?;
active_tool_drawn_in_stack = true;
}
continue;
}
if !d.is_crop() {
d.draw(canvas, font, bounds)?;
}
}

// render active tool
if let Some(d) = self.active_tool.borrow().get_drawable() {
d.draw(canvas, font, bounds)?;
// draw crop bounds on top
for (i, d) in self.drawables.iter().enumerate() {
if self.hidden_drawable_index != Some(i) && d.is_crop() {
d.draw(canvas, font, bounds)?;
}
}

// render crop tool
if render_crop && let Some(c) = self.crop_tool.borrow().get_crop() {
c.draw(canvas, font, bounds)?;
// render active (pointer) tool (default: on top) when not already drawn in stack order
if draw_active_tool
&& !active_tool_drawn_in_stack
&& let Some(d) = self.active_tool.borrow().get_drawable()
{
d.draw(canvas, font, bounds)?;
}

canvas.flush();
Expand Down
Loading
Loading