Add cropping
This commit is contained in:
parent
4c2b72e0f9
commit
6867e0b13f
19 changed files with 1414 additions and 52 deletions
48
data/resources/edit_crop.css
Normal file
48
data/resources/edit_crop.css
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
lpcrop {
|
||||
--discarded-background: rgba(50, 50, 50, 0.7);
|
||||
--selection-border-color: rgba(255, 255, 255, 0.7);
|
||||
--handle-border-color: rgba(255, 255, 255, 0.9);
|
||||
--handle-size: 20px;
|
||||
--handle-border-width: 3px;
|
||||
--handle-offset: -4px;
|
||||
}
|
||||
|
||||
.selection {
|
||||
outline: 1px solid var(--selection-border-color);
|
||||
}
|
||||
|
||||
.handle {
|
||||
padding: var(--handle-size) var(--handle-size) 0 0;
|
||||
}
|
||||
|
||||
.handle-top-left {
|
||||
border-top: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
border-left: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
margin-top: var(--handle-offset);
|
||||
margin-left: var(--handle-offset);
|
||||
}
|
||||
|
||||
.handle-top-right {
|
||||
border-top: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
border-right: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
margin-top: var(--handle-offset);
|
||||
margin-right: var(--handle-offset);
|
||||
}
|
||||
|
||||
.handle-bottom-left {
|
||||
border-left: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
border-bottom: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
margin-left: var(--handle-offset);
|
||||
margin-bottom: var(--handle-offset);
|
||||
}
|
||||
|
||||
.handle-bottom-right {
|
||||
border-bottom: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
border-right: var(--handle-border-width) var(--handle-border-color) solid;
|
||||
margin-bottom: var(--handle-offset);
|
||||
margin-right: var(--handle-offset);
|
||||
}
|
||||
|
||||
.discarded-area {
|
||||
background: var(--discarded-background);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 0 3 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m -14 2 v 1 h 1 v -1 z m 14 0 v 1 h 1 v -1 z m -11 1 v 5 h 9 v -5 z m -3 1 v 1 h 1 v -1 z m 14 0 v 1 h 1 v -1 z m -14 2 v 1 h 1 v -1 z m 14 0 v 1 h 1 v -1 z m -14 2 v 1 h 1 v -1 z m 14 0 v 1 h 1 v -1 z m -14 2 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 2 0 v 1 h 1 v -1 z m 0 0" fill="#222222"/></svg>
|
||||
|
After Width: | Height: | Size: 705 B |
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#2e3436">
|
||||
<path d="m 2 0 c -0.859375 0 -1.429688 0.320312 -1.710938 0.699219 c -0.28515575 0.378906 -0.289062 0.800781 -0.289062 0.800781 v 13 s 0.00390625 0.421875 0.289062 0.800781 c 0.28125 0.378907 0.851563 0.699219 1.710938 0.699219 c 0.109375 0 0.21875 -0.035156 0.304688 -0.105469 l 5.5 -4.25 c 0.125 -0.09375 0.195312 -0.238281 0.195312 -0.394531 v -6.75 c 0 -0.160156 -0.074219 -0.308594 -0.207031 -0.40625 l -5.5 -4 c -0.082031 -0.0585938 -0.1875 -0.09375 -0.292969 -0.09375 z m 0 1.117188 l 5 3.640624 v 6.246094 l -5 3.863282 z m 0 0"/>
|
||||
<path d="m 14 0 c -0.105469 0 -0.210938 0.0351562 -0.296875 0.09375 l -5.5 4 c -0.128906 0.097656 -0.203125 0.246094 -0.203125 0.40625 v 6.75 c 0 0.15625 0.070312 0.300781 0.191406 0.394531 l 5.5 4.25 c 0.089844 0.070313 0.199219 0.105469 0.308594 0.105469 c 0.859375 0 1.429688 -0.320312 1.714844 -0.699219 c 0.28125 -0.378906 0.285156 -0.800781 0.285156 -0.800781 v -13 s -0.003906 -0.421875 -0.285156 -0.800781 c -0.285156 -0.378907 -0.855469 -0.699219 -1.714844 -0.699219 z m 0 1.117188 v 13.75 l -5 -3.863282 v -6.246094 z m 0 0" fill-opacity="0.34902"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,3 +1,5 @@
|
|||
@import 'edit_crop.css';
|
||||
|
||||
:root {
|
||||
--osd-background-color: rgba(240, 240, 240, 0.75);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pub mod edit;
|
||||
|
||||
mod drag_overlay;
|
||||
mod edit_window;
|
||||
mod error_details;
|
||||
|
|
@ -33,7 +35,7 @@ mod window;
|
|||
mod window_content;
|
||||
|
||||
pub use drag_overlay::LpDragOverlay;
|
||||
pub use edit_window::LpImageEdit;
|
||||
pub use edit_window::LpEditWindow;
|
||||
pub use error_details::LpErrorDetails;
|
||||
pub use image::LpImage;
|
||||
pub use image_page::LpImagePage;
|
||||
|
|
|
|||
5
src/widgets/edit.rs
Normal file
5
src/widgets/edit.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod crop;
|
||||
pub mod crop_selection;
|
||||
|
||||
pub use crop::LpEditCrop;
|
||||
pub use crop_selection::LpEditCropSelection;
|
||||
137
src/widgets/edit/crop.rs
Normal file
137
src/widgets/edit/crop.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use std::cell::{Cell, OnceCell};
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{glib, gtk};
|
||||
|
||||
use crate::widgets::edit::LpEditCropSelection;
|
||||
use crate::widgets::LpImage;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, glib::Enum)]
|
||||
#[enum_type(name = "LpAspectRatio")]
|
||||
pub enum LpAspectRatio {
|
||||
#[default]
|
||||
Free,
|
||||
Original,
|
||||
/// 1.0
|
||||
Square,
|
||||
/// 1.25
|
||||
R5to4,
|
||||
/// 1.33
|
||||
R4to3,
|
||||
/// 1.5
|
||||
R3to2,
|
||||
/// 1.6
|
||||
R16to10,
|
||||
/// 1.77
|
||||
R16to9,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, glib::Enum)]
|
||||
#[enum_type(name = "LpOrientation")]
|
||||
pub enum LpOrientation {
|
||||
#[default]
|
||||
Landscape,
|
||||
Portrait,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
use crate::widgets::LpImage;
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
|
||||
#[properties(wrapper_type = super::LpEditCrop)]
|
||||
#[template(file = "crop.ui")]
|
||||
pub struct LpEditCrop {
|
||||
#[template_child]
|
||||
image: TemplateChild<LpImage>,
|
||||
#[template_child]
|
||||
pub(super) selection: TemplateChild<LpEditCropSelection>,
|
||||
|
||||
#[property(get, set, builder(LpAspectRatio::default()))]
|
||||
aspect_ratio: Cell<LpAspectRatio>,
|
||||
#[property(get, set, builder(LpOrientation::default()))]
|
||||
orientation: Cell<LpOrientation>,
|
||||
|
||||
#[property(get, construct_only)]
|
||||
original_image: OnceCell<LpImage>,
|
||||
|
||||
#[property(get, set)]
|
||||
child: OnceCell<gtk::Widget>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LpEditCrop {
|
||||
const NAME: &'static str = "LpEditCrop";
|
||||
type Type = super::LpEditCrop;
|
||||
type ParentType = gtk::Widget;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
|
||||
klass.install_property_action("edit-crop.aspect-ratio", "aspect_ratio");
|
||||
klass.install_property_action("edit-crop.orientation", "orientation");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for LpEditCrop {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = &*self.obj();
|
||||
|
||||
obj.child().set_parent(obj);
|
||||
|
||||
obj.set_layout_manager(None::<gtk::LayoutManager>);
|
||||
self.image.duplicate_from(&obj.original_image());
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
self.obj().child().unparent();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for LpEditCrop {
|
||||
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
|
||||
let obj = self.obj();
|
||||
|
||||
let image: &TemplateChild<LpImage> = &self.image;
|
||||
|
||||
obj.child().allocate(width, height, baseline, None);
|
||||
|
||||
// TODO: Round wrong
|
||||
self.selection.set_size(
|
||||
image.image_rendering_x() as i32,
|
||||
image.image_rendering_y() as i32,
|
||||
image.image_displayed_width().round() as i32,
|
||||
image.image_displayed_height().round() as i32,
|
||||
);
|
||||
}
|
||||
|
||||
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
|
||||
self.obj().child().measure(orientation, for_size)
|
||||
}
|
||||
}
|
||||
impl BinImpl for LpEditCrop {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LpEditCrop(ObjectSubclass<imp::LpEditCrop>)
|
||||
@extends gtk::Widget, adw::Bin;
|
||||
}
|
||||
|
||||
impl LpEditCrop {
|
||||
pub fn new(original_image: LpImage) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("original_image", original_image)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn selection(&self) -> LpEditCropSelection {
|
||||
self.imp().selection.clone()
|
||||
}
|
||||
}
|
||||
189
src/widgets/edit/crop.ui
Normal file
189
src/widgets/edit/crop.ui
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="aspect_ratio_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Free</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">free</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Original</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">original</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Square</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">square</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">5:4</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">r5to4</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">4:3</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">r4to3</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">3:2</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">r3to2</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">16:10</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">r16to10</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">16:9</attribute>
|
||||
<attribute name="action">edit-crop.aspect-ratio</attribute>
|
||||
<attribute name="target">r16to9</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="custom">orientation</attribute>
|
||||
</item>
|
||||
|
||||
</section>
|
||||
</menu>
|
||||
<template class="LpEditCrop" parent="GtkWidget">
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkOverlay">
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="margin-start">12</property>
|
||||
<child>
|
||||
<object class="LpImage" id="image">
|
||||
</object>
|
||||
</child>
|
||||
<child type="overlay">
|
||||
<object class="LpEditCropSelection" id="selection">
|
||||
<property name="crop">LpEditCrop</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="homogeneous">true</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="margin-bottom">16</property>
|
||||
<property name="margin-start">16</property>
|
||||
<property name="margin-end">16</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">aspect-ratio-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">Aspect Ratio</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="direction">up</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
<property name="popover">
|
||||
<object class="GtkPopoverMenu">
|
||||
<property name="menu-model">aspect_ratio_menu</property>
|
||||
<child type="orientation">
|
||||
<object class="GtkBox">
|
||||
<property name="homogeneous">true</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="landscape">
|
||||
<property name="icon-name">page-orientation-landscape-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">Orientation Landscape</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="action-name">edit-crop.orientation</property>
|
||||
<property name="action-target">'landscape'</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="portrait">
|
||||
<property name="icon-name">page-orientation-portrait-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">Orientation Portrait</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="action-name">edit-crop.orientation</property>
|
||||
<property name="action-target">'portrait'</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">edit-mirror-symbolic</property>
|
||||
<property name="valign">start</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">object-rotate-left-symbolic</property>
|
||||
<property name="valign">start</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="icon-name">object-rotate-right-symbolic</property>
|
||||
<property name="valign">start</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label">Reset</property>
|
||||
<property name="valign">start</property>
|
||||
<style>
|
||||
<class name="flat" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
||||
749
src/widgets/edit/crop_selection.rs
Normal file
749
src/widgets/edit/crop_selection.rs
Normal file
|
|
@ -0,0 +1,749 @@
|
|||
use std::cell::{Cell, OnceCell};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use adw::gtk::graphene;
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{glib, gtk};
|
||||
|
||||
use super::crop::{LpAspectRatio, LpEditCrop, LpOrientation};
|
||||
|
||||
const MIN_SELECTION_SIZE: f32 = 50.;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct InResize {
|
||||
corner: Corner,
|
||||
initial_selection: graphene::Rect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct InMove {
|
||||
initial_selection: graphene::Rect,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Corner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum HEdge {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum VEdge {
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl Corner {
|
||||
fn cursor_name(&self) -> &str {
|
||||
match self {
|
||||
Self::TopLeft => "nw-resize",
|
||||
Self::TopRight => "ne-resize",
|
||||
Self::BottomLeft => "sw-resize",
|
||||
Self::BottomRight => "se-resize",
|
||||
}
|
||||
}
|
||||
|
||||
fn h_edge(&self) -> HEdge {
|
||||
match self {
|
||||
Self::TopLeft => HEdge::Left,
|
||||
Self::TopRight => HEdge::Right,
|
||||
Self::BottomLeft => HEdge::Left,
|
||||
Self::BottomRight => HEdge::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn v_edge(&self) -> VEdge {
|
||||
match self {
|
||||
Self::TopLeft => VEdge::Top,
|
||||
Self::TopRight => VEdge::Top,
|
||||
Self::BottomLeft => VEdge::Bottom,
|
||||
Self::BottomRight => VEdge::Bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LpAspectRatio {
|
||||
fn frac(&self) -> Option<(u32, u32)> {
|
||||
match self {
|
||||
Self::Free => None,
|
||||
Self::Original => None,
|
||||
Self::Square => Some((1, 1)),
|
||||
Self::R5to4 => Some((5, 4)),
|
||||
Self::R4to3 => Some((4, 3)),
|
||||
Self::R3to2 => Some((3, 2)),
|
||||
Self::R16to10 => Some((16, 10)),
|
||||
Self::R16to9 => Some((16, 9)),
|
||||
}
|
||||
}
|
||||
|
||||
fn num(&self, crop: &LpEditCrop) -> Option<f32> {
|
||||
let (x, y) = if matches!(self, Self::Original) {
|
||||
let width = crop.selection().total_width() as u32;
|
||||
let height = crop.selection().total_height() as u32;
|
||||
|
||||
if width > height {
|
||||
(width, height)
|
||||
} else {
|
||||
(height, width)
|
||||
}
|
||||
} else {
|
||||
self.frac()?
|
||||
};
|
||||
|
||||
let result = match crop.orientation() {
|
||||
LpOrientation::Landscape => x as f32 / y as f32,
|
||||
LpOrientation::Portrait => y as f32 / x as f32,
|
||||
};
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
trait RectExt: Into<graphene::Rect> {
|
||||
fn coordiantes(self) -> (f32, f32, f32, f32) {
|
||||
let rect: graphene::Rect = self.into();
|
||||
|
||||
(rect.x(), rect.y(), rect.width(), rect.height())
|
||||
}
|
||||
}
|
||||
|
||||
impl RectExt for graphene::Rect {}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
|
||||
#[properties(wrapper_type = super::LpEditCropSelection)]
|
||||
#[template(file = "crop_selection.ui")]
|
||||
pub struct LpEditCropSelection {
|
||||
#[template_child]
|
||||
pub(super) space_top_left: TemplateChild<adw::Bin>,
|
||||
#[template_child]
|
||||
pub(super) selection: TemplateChild<gtk::Grid>,
|
||||
|
||||
#[template_child]
|
||||
handle_top_left: TemplateChild<adw::Bin>,
|
||||
#[template_child]
|
||||
handle_top_right: TemplateChild<adw::Bin>,
|
||||
#[template_child]
|
||||
handle_bottom_right: TemplateChild<adw::Bin>,
|
||||
#[template_child]
|
||||
handle_bottom_left: TemplateChild<adw::Bin>,
|
||||
|
||||
#[template_child]
|
||||
selection_drag: TemplateChild<gtk::GestureDrag>,
|
||||
|
||||
/// Set while in drag gesture for changing crop area
|
||||
selection_in_resize: Cell<Option<InResize>>,
|
||||
/// Set while in drag gesture for moving crop area
|
||||
selection_in_move: Cell<Option<InMove>>,
|
||||
|
||||
// Animates changes between different fixed aspect ratios
|
||||
aspect_ratio_animation: OnceCell<adw::TimedAnimation>,
|
||||
|
||||
#[property(get, construct_only)]
|
||||
crop: OnceCell<LpEditCrop>,
|
||||
|
||||
#[property(get=Self::total_width, set=Self::set_total_width)]
|
||||
total_width: PhantomData<i32>,
|
||||
#[property(get=Self::total_height, set=Self::set_total_height)]
|
||||
total_height: PhantomData<i32>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LpEditCropSelection {
|
||||
const NAME: &'static str = "LpEditCropSelection";
|
||||
type Type = super::LpEditCropSelection;
|
||||
type ParentType = gtk::Grid;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
klass.set_css_name("lpcrop");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for LpEditCropSelection {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
obj.set_direction(gtk::TextDirection::Ltr);
|
||||
|
||||
// List of widgets that are drag handles
|
||||
let drag_corners = std::collections::BTreeMap::from([
|
||||
(self.handle_top_left.clone(), Corner::TopLeft),
|
||||
(self.handle_top_right.clone(), Corner::TopRight),
|
||||
(self.handle_bottom_left.clone(), Corner::BottomLeft),
|
||||
(self.handle_bottom_right.clone(), Corner::BottomRight),
|
||||
]);
|
||||
|
||||
// Set cursor style for each corner
|
||||
for (handle_widget, corner) in drag_corners.iter() {
|
||||
handle_widget.set_cursor_from_name(Some(corner.cursor_name()));
|
||||
}
|
||||
|
||||
self.selection.set_cursor_from_name(Some("move"));
|
||||
|
||||
obj.crop()
|
||||
.connect_aspect_ratio_notify(|x| x.selection().imp().on_aspect_ratio_changed());
|
||||
|
||||
obj.crop()
|
||||
.connect_orientation_notify(|x| x.selection().imp().on_aspect_ratio_changed());
|
||||
|
||||
// Drag begin
|
||||
self.selection_drag.connect_drag_begin(glib::clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |gesture, _, _| {
|
||||
let imp = obj.imp();
|
||||
|
||||
imp.aspect_ratio_animation().pause();
|
||||
|
||||
let hovered_widget = gesture
|
||||
.start_point()
|
||||
.and_then(|(x, y)| obj.pick(x, y, gtk::PickFlags::DEFAULT));
|
||||
|
||||
// Lookup if the start-point of the gesture is above a drag handle widget and
|
||||
// what corner it is
|
||||
if let Some(corner) = hovered_widget
|
||||
.as_ref()
|
||||
.and_then(|x| drag_corners.get(x).copied())
|
||||
{
|
||||
gesture.set_state(gtk::EventSequenceState::Claimed);
|
||||
|
||||
let crop_area = imp.crop_area();
|
||||
|
||||
imp.selection_in_resize.set(Some(InResize {
|
||||
corner,
|
||||
initial_selection: crop_area,
|
||||
}));
|
||||
} else if hovered_widget.map_or(false, |x| x == *imp.selection) {
|
||||
gesture.set_state(gtk::EventSequenceState::Claimed);
|
||||
|
||||
let crop_area = imp.crop_area();
|
||||
imp.selection_in_move.set(Some(InMove {
|
||||
initial_selection: crop_area,
|
||||
}));
|
||||
} else {
|
||||
gesture.set_state(gtk::EventSequenceState::Denied);
|
||||
return;
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// Drag moved
|
||||
self.selection_drag.connect_drag_update(glib::clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, x, y| {
|
||||
let imp = obj.imp();
|
||||
|
||||
imp.aspect_ratio_animation().pause();
|
||||
|
||||
if let Some(in_move) = imp.selection_in_move.get() {
|
||||
let moved_area = imp.moved_crop_area(&in_move, (x, y));
|
||||
|
||||
imp.set_crop(moved_area);
|
||||
} else if let Some(drag) = imp.selection_in_resize.get() {
|
||||
let coord = (x, y);
|
||||
|
||||
let new_area = if let Some(aspect_ratio) = imp.aspect_ratio() {
|
||||
let u =
|
||||
imp.new_crop_area_aspect_ratio(&drag, true, coord, aspect_ratio);
|
||||
let v =
|
||||
imp.new_crop_area_aspect_ratio(&drag, false, coord, aspect_ratio);
|
||||
|
||||
if u.area() > v.area() {
|
||||
u
|
||||
} else {
|
||||
v
|
||||
}
|
||||
} else {
|
||||
imp.new_crop_area_aspect_free(&drag, coord)
|
||||
};
|
||||
|
||||
imp.set_crop(new_area);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// Drag finished
|
||||
self.selection_drag.connect_drag_end(glib::clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |_, _, _| {
|
||||
obj.imp().selection_in_resize.set(None);
|
||||
obj.imp().selection_in_move.set(None);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for LpEditCropSelection {}
|
||||
impl GridImpl for LpEditCropSelection {}
|
||||
|
||||
impl LpEditCropSelection {
|
||||
/// Current crop area
|
||||
fn crop_area(&self) -> graphene::Rect {
|
||||
graphene::Rect::new(
|
||||
self.space_top_left.width_request() as f32,
|
||||
self.space_top_left.height_request() as f32,
|
||||
self.selection.width_request() as f32,
|
||||
self.selection.height_request() as f32,
|
||||
)
|
||||
}
|
||||
|
||||
fn new_crop_area(&self, resize: &InResize, (x, y): (f64, f64)) -> graphene::Rect {
|
||||
let (x_offset, width_offset) = match resize.corner.h_edge() {
|
||||
// Change the crop x position and reduce width accordingly
|
||||
HEdge::Left => (x, -x),
|
||||
// Just change the crop width
|
||||
HEdge::Right => (0., x),
|
||||
};
|
||||
|
||||
let (y_offset, height_offset) = match resize.corner.v_edge() {
|
||||
VEdge::Top => (y, -y),
|
||||
VEdge::Bottom => (0., y),
|
||||
};
|
||||
|
||||
let mut x = resize.initial_selection.x() + x_offset as f32;
|
||||
let mut y = resize.initial_selection.y() + y_offset as f32;
|
||||
|
||||
let mut width = resize.initial_selection.width() + width_offset as f32;
|
||||
let mut height = resize.initial_selection.height() + height_offset as f32;
|
||||
|
||||
if width < 0. {
|
||||
if matches!(resize.corner.h_edge(), HEdge::Left) {
|
||||
x += width;
|
||||
}
|
||||
width = 0.;
|
||||
}
|
||||
|
||||
if height < 0. {
|
||||
if matches!(resize.corner.v_edge(), VEdge::Top) {
|
||||
y += height;
|
||||
}
|
||||
height = 0.;
|
||||
}
|
||||
|
||||
graphene::Rect::new(x, y, width, height)
|
||||
}
|
||||
|
||||
fn moved_crop_area(
|
||||
&self,
|
||||
shift: &InMove,
|
||||
(x_shift, y_shift): (f64, f64),
|
||||
) -> graphene::Rect {
|
||||
let obj = self.obj();
|
||||
|
||||
let (mut x, mut y, width, height) = shift.initial_selection.coordiantes();
|
||||
|
||||
x += x_shift as f32;
|
||||
y += y_shift as f32;
|
||||
|
||||
if x < 0. {
|
||||
x = 0.;
|
||||
}
|
||||
|
||||
if y < 0. {
|
||||
y = 0.;
|
||||
}
|
||||
|
||||
if x + width > obj.width_request() as f32 {
|
||||
x = obj.width_request() as f32 - width;
|
||||
}
|
||||
|
||||
if y + height > obj.height_request() as f32 {
|
||||
y = obj.height_request() as f32 - height;
|
||||
}
|
||||
|
||||
graphene::Rect::new(x, y, width, height)
|
||||
}
|
||||
|
||||
fn new_crop_area_aspect_free(
|
||||
&self,
|
||||
resize: &InResize,
|
||||
(x, y): (f64, f64),
|
||||
) -> graphene::Rect {
|
||||
let obj = self.obj();
|
||||
|
||||
let (mut x, mut y, mut width, mut height) =
|
||||
self.new_crop_area(resize, (x, y)).coordiantes();
|
||||
|
||||
if width < MIN_SELECTION_SIZE {
|
||||
let x_diff = width - MIN_SELECTION_SIZE;
|
||||
width = MIN_SELECTION_SIZE;
|
||||
|
||||
if matches!(resize.corner.h_edge(), HEdge::Left) {
|
||||
x += x_diff;
|
||||
}
|
||||
}
|
||||
|
||||
if height < MIN_SELECTION_SIZE {
|
||||
let y_diff = height - MIN_SELECTION_SIZE;
|
||||
height = MIN_SELECTION_SIZE;
|
||||
|
||||
if matches!(resize.corner.v_edge(), VEdge::Top) {
|
||||
y += y_diff;
|
||||
}
|
||||
}
|
||||
|
||||
if x < 0. {
|
||||
width += x;
|
||||
x = 0.;
|
||||
}
|
||||
|
||||
if y < 0. {
|
||||
height += y;
|
||||
y = 0.;
|
||||
}
|
||||
|
||||
if x + width > obj.width_request() as f32 {
|
||||
width = obj.width_request() as f32 - x;
|
||||
}
|
||||
|
||||
if y + height > obj.height_request() as f32 {
|
||||
height = obj.height_request() as f32 - y;
|
||||
}
|
||||
|
||||
graphene::Rect::new(x, y, width, height)
|
||||
}
|
||||
|
||||
fn new_crop_area_aspect_ratio(
|
||||
&self,
|
||||
resize: &InResize,
|
||||
vertical: bool,
|
||||
(x, y): (f64, f64),
|
||||
aspect_ratio: f32,
|
||||
) -> graphene::Rect {
|
||||
let new_area = self.new_crop_area(resize, (x, y));
|
||||
|
||||
let (mut x, mut y, mut width, mut height) = new_area.coordiantes();
|
||||
|
||||
if vertical {
|
||||
width = new_area.height() * aspect_ratio;
|
||||
if matches!(resize.corner.h_edge(), HEdge::Left) {
|
||||
x += new_area.width() - width;
|
||||
}
|
||||
} else {
|
||||
height = new_area.width() / aspect_ratio;
|
||||
if matches!(resize.corner.v_edge(), VEdge::Top) {
|
||||
y += new_area.height() - height;
|
||||
}
|
||||
}
|
||||
|
||||
let mut rect = graphene::Rect::new(x, y, width, height);
|
||||
|
||||
self.rect_limit_top(&mut rect, aspect_ratio, resize.corner.v_edge());
|
||||
self.rect_limit_left(&mut rect, aspect_ratio, resize.corner.h_edge());
|
||||
self.rect_limit_bottom(&mut rect, aspect_ratio, resize.corner.h_edge());
|
||||
self.rect_limit_right(&mut rect, aspect_ratio, resize.corner.v_edge());
|
||||
|
||||
self.rect_limit_minumum_size(&mut rect, aspect_ratio, resize.corner);
|
||||
|
||||
(x, y, width, height) = rect.coordiantes();
|
||||
graphene::Rect::new(x, y, width, height)
|
||||
}
|
||||
|
||||
/// Ensure selection is not smaller than minimum size
|
||||
fn rect_limit_minumum_size(
|
||||
&self,
|
||||
rect: &mut graphene::Rect,
|
||||
aspect_ratio: f32,
|
||||
corner: Corner,
|
||||
) {
|
||||
let (mut x, mut y, mut width, mut height) = rect.coordiantes();
|
||||
|
||||
if width < MIN_SELECTION_SIZE {
|
||||
let x_diff = width - MIN_SELECTION_SIZE;
|
||||
width = MIN_SELECTION_SIZE;
|
||||
let y_diff = height - MIN_SELECTION_SIZE / aspect_ratio;
|
||||
height = MIN_SELECTION_SIZE / aspect_ratio;
|
||||
|
||||
if matches!(corner.v_edge(), VEdge::Top) {
|
||||
y += y_diff;
|
||||
}
|
||||
if matches!(corner.h_edge(), HEdge::Left) {
|
||||
x += x_diff;
|
||||
}
|
||||
}
|
||||
|
||||
if height < MIN_SELECTION_SIZE {
|
||||
let x_diff = width - MIN_SELECTION_SIZE * aspect_ratio;
|
||||
width = MIN_SELECTION_SIZE * aspect_ratio;
|
||||
let y_diff = height - MIN_SELECTION_SIZE;
|
||||
height = MIN_SELECTION_SIZE;
|
||||
|
||||
if matches!(corner.v_edge(), VEdge::Top) {
|
||||
y += y_diff;
|
||||
}
|
||||
if matches!(corner.h_edge(), HEdge::Left) {
|
||||
x += x_diff;
|
||||
}
|
||||
}
|
||||
|
||||
*rect = graphene::Rect::new(x, y, width, height);
|
||||
}
|
||||
|
||||
/// Make sure Rect is inside image area by moving it if necessary
|
||||
fn rect_make_contained(&self, rect: &mut graphene::Rect) {
|
||||
let (mut x, mut y, width, height) = rect.coordiantes();
|
||||
|
||||
if x + width > self.total_width() as f32 {
|
||||
x = self.total_width() as f32 - width;
|
||||
}
|
||||
|
||||
if y + height > self.total_height() as f32 {
|
||||
y = self.total_height() as f32 - height;
|
||||
}
|
||||
|
||||
*rect = graphene::Rect::new(x, y, width, height);
|
||||
}
|
||||
|
||||
/// Limit Rect to not leave image area to the left
|
||||
fn rect_limit_top(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: VEdge) {
|
||||
let (mut x, mut y, mut width, mut height) = rect.coordiantes();
|
||||
|
||||
if x < 0. {
|
||||
width += x;
|
||||
x = 0.;
|
||||
let old_height = height;
|
||||
height = width / aspect_ratio;
|
||||
if matches!(edge, VEdge::Top) {
|
||||
y += old_height - height;
|
||||
}
|
||||
}
|
||||
|
||||
*rect = graphene::Rect::new(x, y, width, height);
|
||||
}
|
||||
|
||||
/// Limit Rect to not leave image area at to the left
|
||||
fn rect_limit_left(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: HEdge) {
|
||||
let (mut x, mut y, mut width, mut height) = rect.coordiantes();
|
||||
|
||||
if y < 0. {
|
||||
height += y;
|
||||
y = 0.;
|
||||
let old_width = width;
|
||||
width = height * aspect_ratio;
|
||||
|
||||
if matches!(edge, HEdge::Left) {
|
||||
x += old_width - width;
|
||||
}
|
||||
|
||||
*rect = graphene::Rect::new(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// Limit Rect to not leave image area at the bottom
|
||||
fn rect_limit_bottom(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: HEdge) {
|
||||
let obj = self.obj();
|
||||
let (mut x, y, mut width, mut height) = rect.coordiantes();
|
||||
|
||||
if y + height > obj.height_request() as f32 {
|
||||
let overshoot = y + height - obj.height_request() as f32;
|
||||
|
||||
height -= overshoot;
|
||||
let old_width = width;
|
||||
width = height * aspect_ratio;
|
||||
|
||||
if matches!(edge, HEdge::Left) {
|
||||
x += old_width - width;
|
||||
}
|
||||
|
||||
*rect = graphene::Rect::new(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_limit_right(&self, rect: &mut graphene::Rect, aspect_ratio: f32, edge: VEdge) {
|
||||
let obj = self.obj();
|
||||
let (x, mut y, mut width, mut height) = rect.coordiantes();
|
||||
|
||||
if x + width > obj.width_request() as f32 {
|
||||
let overshoot = x + width - obj.width_request() as f32;
|
||||
|
||||
width -= overshoot;
|
||||
let old_height = height;
|
||||
height = width / aspect_ratio;
|
||||
|
||||
if matches!(edge, VEdge::Top) {
|
||||
y += old_height - height;
|
||||
}
|
||||
|
||||
*rect = graphene::Rect::new(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_aspect_ratio_changed(&self) {
|
||||
let Some(aspect_ratio) = self.aspect_ratio() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let obj = self.obj();
|
||||
|
||||
// Store current state as reference for animation
|
||||
let current_area = self.crop_area();
|
||||
let (_, y, _, height) = current_area.coordiantes();
|
||||
|
||||
let width = current_area.height() * aspect_ratio;
|
||||
let x = f32::max(0., current_area.x() + (current_area.width() - width) / 2.);
|
||||
|
||||
let mut new_area = graphene::Rect::new(x, y, width, height);
|
||||
|
||||
self.rect_limit_top(&mut new_area, aspect_ratio, VEdge::Bottom);
|
||||
self.rect_limit_left(&mut new_area, aspect_ratio, HEdge::Right);
|
||||
self.rect_limit_bottom(&mut new_area, aspect_ratio, HEdge::Left);
|
||||
self.rect_limit_right(&mut new_area, aspect_ratio, VEdge::Top);
|
||||
|
||||
self.rect_limit_minumum_size(&mut new_area, aspect_ratio, Corner::BottomLeft);
|
||||
|
||||
self.rect_make_contained(&mut new_area);
|
||||
|
||||
self.aspect_ratio_animation()
|
||||
.set_target(&adw::CallbackAnimationTarget::new(glib::clone!(
|
||||
#[weak]
|
||||
obj,
|
||||
move |progress| {
|
||||
let imp = obj.imp();
|
||||
|
||||
// Linear interpolate between old and target value
|
||||
imp.set_crop(current_area.interpolate(&new_area, progress));
|
||||
}
|
||||
)));
|
||||
|
||||
self.aspect_ratio_animation().play();
|
||||
}
|
||||
|
||||
/// Fixed aspect ratio of selection if set
|
||||
fn aspect_ratio(&self) -> Option<f32> {
|
||||
let crop = self.obj().crop();
|
||||
crop.aspect_ratio().num(&crop)
|
||||
}
|
||||
|
||||
/// Set all coordinates of crop selection
|
||||
fn set_crop(&self, rect: graphene::Rect) {
|
||||
let rect = rect.round_extents();
|
||||
|
||||
self.set_crop_x(rect.x() as i32);
|
||||
self.set_crop_y(rect.y() as i32);
|
||||
self.set_crop_width(rect.width() as i32);
|
||||
self.set_crop_height(rect.height() as i32);
|
||||
}
|
||||
|
||||
/// Set x coordinate of crop selection rectangle origin
|
||||
pub(super) fn set_crop_x(&self, x: i32) {
|
||||
if x < 0 {
|
||||
eprintln!("Tried to set x coordinate to {x}");
|
||||
return;
|
||||
}
|
||||
|
||||
self.space_top_left.set_width_request(x);
|
||||
}
|
||||
|
||||
/// Set y coordinate of crop selection rectangle origin
|
||||
pub(super) fn set_crop_y(&self, y: i32) {
|
||||
if y < 0 {
|
||||
eprintln!("Tried to set y coordinate to {y}");
|
||||
return;
|
||||
}
|
||||
|
||||
self.space_top_left.set_height_request(y);
|
||||
}
|
||||
|
||||
/// Set width of crop selection rectangle
|
||||
pub(super) fn set_crop_width(&self, width: i32) {
|
||||
if width < 0 {
|
||||
eprintln!("Tried to set width to {width}");
|
||||
return;
|
||||
}
|
||||
|
||||
self.selection.set_width_request(width);
|
||||
}
|
||||
|
||||
/// Set height of crop selection rectangle
|
||||
pub(super) fn set_crop_height(&self, height: i32) {
|
||||
if height < 0 {
|
||||
eprintln!("Tried to set height to {height}");
|
||||
return;
|
||||
}
|
||||
|
||||
self.selection.set_height_request(height);
|
||||
}
|
||||
|
||||
/// Width of area in which the crop selection can exist
|
||||
fn total_width(&self) -> i32 {
|
||||
self.obj().width_request()
|
||||
}
|
||||
|
||||
fn set_total_width(&self, width: i32) {
|
||||
self.obj().set_width_request(width);
|
||||
}
|
||||
|
||||
/// Height of area in which the crop selection can exist
|
||||
fn total_height(&self) -> i32 {
|
||||
self.obj().height_request()
|
||||
}
|
||||
|
||||
fn set_total_height(&self, width: i32) {
|
||||
self.obj().set_height_request(width);
|
||||
}
|
||||
|
||||
fn aspect_ratio_animation(&self) -> &adw::TimedAnimation {
|
||||
self.aspect_ratio_animation.get_or_init(|| {
|
||||
adw::TimedAnimation::builder()
|
||||
.duration(200)
|
||||
.value_from(0.)
|
||||
.value_to(1.)
|
||||
.easing(adw::Easing::EaseOutSine)
|
||||
.widget(&*self.obj())
|
||||
.target(&adw::CallbackAnimationTarget::new(|_| {}))
|
||||
.build()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LpEditCropSelection(ObjectSubclass<imp::LpEditCropSelection>)
|
||||
@extends gtk::Widget;
|
||||
}
|
||||
|
||||
impl LpEditCropSelection {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
}
|
||||
|
||||
pub fn reset(&self, x: i32, y: i32, width: i32, height: i32) {
|
||||
let imp = self.imp();
|
||||
|
||||
self.set_margin_start(x);
|
||||
self.set_margin_top(y);
|
||||
self.set_total_width(width);
|
||||
self.set_total_height(height);
|
||||
|
||||
imp.set_crop_x(0);
|
||||
imp.set_crop_y(0);
|
||||
imp.set_crop_width(width);
|
||||
imp.set_crop_height(height);
|
||||
}
|
||||
|
||||
pub fn set_size(&self, x: i32, y: i32, width: i32, height: i32) {
|
||||
self.set_margin_start(x);
|
||||
self.set_margin_top(y);
|
||||
self.set_total_width(width);
|
||||
self.set_total_height(height);
|
||||
}
|
||||
}
|
||||
173
src/widgets/edit/crop_selection.ui
Normal file
173
src/widgets/edit/crop_selection.ui
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0" />
|
||||
<template class="LpEditCropSelection" parent="GtkGrid">
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<child>
|
||||
<object class="GtkGestureDrag" id="selection_drag">
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin" id="space_top_left">
|
||||
<layout>
|
||||
<property name="row">0</property>
|
||||
<property name="column">0</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<layout>
|
||||
<property name="row">0</property>
|
||||
<property name="column">1</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<layout>
|
||||
<property name="row">1</property>
|
||||
<property name="column">0</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<layout>
|
||||
<property name="row">1</property>
|
||||
<property name="column">2</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<property name="vexpand">true</property>
|
||||
<layout>
|
||||
<property name="row">2</property>
|
||||
<property name="column">0</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<layout>
|
||||
<property name="row">2</property>
|
||||
<property name="column">1</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<layout>
|
||||
<property name="row">2</property>
|
||||
<property name="column">2</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin">
|
||||
<property name="hexpand">true</property>
|
||||
<layout>
|
||||
<property name="row">0</property>
|
||||
<property name="column">2</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="discarded-area" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="selection">
|
||||
<property name="hexpand-set">true</property>
|
||||
<property name="vexpand-set">true</property>
|
||||
<layout>
|
||||
<property name="row">1</property>
|
||||
<property name="column">1</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="selection" />
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwBin" id="handle_top_left">
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<layout>
|
||||
<property name="row">0</property>
|
||||
<property name="column">0</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="handle" />
|
||||
<class name="handle-top-left" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin" id="handle_top_right">
|
||||
<property name="halign">end</property>
|
||||
<property name="hexpand">true</property>
|
||||
<layout>
|
||||
<property name="row">0</property>
|
||||
<property name="column">1</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="handle" />
|
||||
<class name="handle-top-right" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin" id="handle_bottom_left">
|
||||
<property name="valign">end</property>
|
||||
<property name="vexpand">true</property>
|
||||
<layout>
|
||||
<property name="row">1</property>
|
||||
<property name="column">0</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="handle" />
|
||||
<class name="handle-bottom-left" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwBin" id="handle_bottom_right">
|
||||
<property name="halign">end</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="vexpand">true</property>
|
||||
<layout>
|
||||
<property name="row">1</property>
|
||||
<property name="column">1</property>
|
||||
</layout>
|
||||
<style>
|
||||
<class name="handle" />
|
||||
<class name="handle-bottom-right" />
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
|
@ -15,24 +15,34 @@
|
|||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{glib, gtk};
|
||||
|
||||
use super::edit::LpEditCrop;
|
||||
use super::LpImage;
|
||||
use crate::deps::*;
|
||||
mod imp {
|
||||
|
||||
use super::*;
|
||||
use crate::widgets::LpImage;
|
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
|
||||
#[properties(wrapper_type = super::LpImageEdit)]
|
||||
#[properties(wrapper_type = super::LpEditWindow)]
|
||||
#[template(file = "edit_window.ui")]
|
||||
pub struct LpImageEdit {
|
||||
#[property(get)]
|
||||
example: u64,
|
||||
pub struct LpEditWindow {
|
||||
#[template_child]
|
||||
toolbar_view: TemplateChild<adw::ToolbarView>,
|
||||
|
||||
#[property(get, construct_only)]
|
||||
original_image: OnceCell<LpImage>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LpImageEdit {
|
||||
const NAME: &'static str = "LpImageEdit";
|
||||
type Type = super::LpImageEdit;
|
||||
impl ObjectSubclass for LpEditWindow {
|
||||
const NAME: &'static str = "LpEditWindow";
|
||||
type Type = super::LpEditWindow;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
|
|
@ -45,23 +55,30 @@ mod imp {
|
|||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for LpImageEdit {
|
||||
impl ObjectImpl for LpEditWindow {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
|
||||
self.toolbar_view
|
||||
.set_content(Some(&LpEditCrop::new(obj.original_image())));
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for LpImageEdit {}
|
||||
impl BinImpl for LpImageEdit {}
|
||||
impl WidgetImpl for LpEditWindow {}
|
||||
impl BinImpl for LpEditWindow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LpImageEdit(ObjectSubclass<imp::LpImageEdit>)
|
||||
pub struct LpEditWindow(ObjectSubclass<imp::LpEditWindow>)
|
||||
@extends gtk::Widget, adw::Bin;
|
||||
}
|
||||
|
||||
impl LpImageEdit {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new()
|
||||
impl LpEditWindow {
|
||||
pub fn new(image: LpImage) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("original_image", image)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="LpImageEdit" parent="AdwBin">
|
||||
<template class="LpEditWindow" parent="AdwBin">
|
||||
<property name="child">
|
||||
<object class="AdwToolbarView" id="toolbar_view">
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar"></object>
|
||||
</child>
|
||||
<property name="content">
|
||||
<object class="GtkLabel">
|
||||
<property name="label">Edit Image</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -370,4 +370,14 @@ impl LpImage {
|
|||
image.imp().still.replace(true);
|
||||
image
|
||||
}
|
||||
|
||||
pub fn duplicate_from(&self, original: &LpImage) {
|
||||
let original_imp = original.imp();
|
||||
|
||||
let imp = self.imp();
|
||||
|
||||
*imp.file.borrow_mut() = original_imp.file.borrow().clone();
|
||||
imp.frame_buffer
|
||||
.swap(Arc::new((*(original_imp.frame_buffer).load_full()).clone()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,19 +29,6 @@ impl imp::LpImage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Image width with current zoom factor and rotation
|
||||
///
|
||||
/// During rotation it is an interpolated size that does not
|
||||
/// represent the actual size. The size returned might well be
|
||||
/// larger than what can actually be displayed within the widget.
|
||||
pub fn image_displayed_width(&self) -> f64 {
|
||||
self.image_width(self.applicable_zoom())
|
||||
}
|
||||
|
||||
pub fn image_displayed_height(&self) -> f64 {
|
||||
self.image_height(self.applicable_zoom())
|
||||
}
|
||||
|
||||
pub fn image_width(&self, zoom: f64) -> f64 {
|
||||
let (width, height) = self.original_dimensions();
|
||||
|
||||
|
|
@ -107,4 +94,19 @@ impl LpImage {
|
|||
self.imp().original_dimensions()
|
||||
}
|
||||
}
|
||||
|
||||
/// Image width with current zoom factor and rotation
|
||||
///
|
||||
/// During rotation it is an interpolated size that does not
|
||||
/// represent the actual size. The size returned might well be
|
||||
/// larger than what can actually be displayed within the widget.
|
||||
pub fn image_displayed_width(&self) -> f64 {
|
||||
let imp = self.imp();
|
||||
imp.image_width(imp.applicable_zoom())
|
||||
}
|
||||
|
||||
pub fn image_displayed_height(&self) -> f64 {
|
||||
let imp = self.imp();
|
||||
imp.image_height(imp.applicable_zoom())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ impl WidgetImpl for imp::LpImage {
|
|||
let obj = self.obj();
|
||||
let widget_width = self.widget_width();
|
||||
let widget_height = self.widget_height();
|
||||
let display_width = self.image_displayed_width();
|
||||
let display_height = self.image_displayed_height();
|
||||
let display_width = obj.image_displayed_width();
|
||||
let display_height = obj.image_displayed_height();
|
||||
|
||||
let applicable_zoom = self.applicable_zoom();
|
||||
|
||||
|
|
@ -333,3 +333,19 @@ impl imp::LpImage {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl LpImage {
|
||||
pub fn image_rendering_x(&self) -> f64 {
|
||||
let imp = self.imp();
|
||||
let widget_width = imp.widget_width();
|
||||
let display_width = self.image_displayed_width();
|
||||
imp.round_f64(f64::max((widget_width - display_width) / 2.0, 0.))
|
||||
}
|
||||
|
||||
pub fn image_rendering_y(&self) -> f64 {
|
||||
let imp = self.imp();
|
||||
let widget_height = imp.widget_height();
|
||||
let display_height = self.image_displayed_height();
|
||||
imp.round_f64(f64::max((widget_height - display_height) / 2.0, 0.))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ impl imp::LpImage {
|
|||
|
||||
let hadjustment = obj.hadj();
|
||||
// round to application pixels to avoid tiny rounding errors from zoom
|
||||
let content_width = self.round_f64(self.image_displayed_width());
|
||||
let content_width = self.round_f64(obj.image_displayed_width());
|
||||
let widget_width = self.widget_width();
|
||||
|
||||
hadjustment.configure(
|
||||
|
|
@ -93,7 +93,7 @@ impl imp::LpImage {
|
|||
|
||||
let vadjustment = obj.vadj();
|
||||
// round to application pixels to avoid tiny rounding errors from zoom
|
||||
let content_height = self.round_f64(self.image_displayed_height());
|
||||
let content_height = self.round_f64(obj.image_displayed_height());
|
||||
let widget_height = self.widget_height();
|
||||
|
||||
vadjustment.configure(
|
||||
|
|
@ -132,11 +132,13 @@ impl imp::LpImage {
|
|||
}
|
||||
|
||||
pub fn max_hadjustment_value(&self) -> f64 {
|
||||
f64::max(self.image_displayed_width() - self.widget_width(), 0.)
|
||||
let obj = self.obj();
|
||||
f64::max(obj.image_displayed_width() - self.widget_width(), 0.)
|
||||
}
|
||||
|
||||
pub fn max_vadjustment_value(&self) -> f64 {
|
||||
f64::max(self.image_displayed_height() - self.widget_height(), 0.)
|
||||
let obj = self.obj();
|
||||
f64::max(obj.image_displayed_height() - self.widget_height(), 0.)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,16 +104,15 @@ impl imp::LpImage {
|
|||
}
|
||||
|
||||
fn horizontal_bar(&self) -> f64 {
|
||||
f64::max(
|
||||
0.,
|
||||
(self.widget_width() - self.image_displayed_width()) / 2.,
|
||||
)
|
||||
let obj = self.obj();
|
||||
f64::max(0., (self.widget_width() - obj.image_displayed_width()) / 2.)
|
||||
}
|
||||
|
||||
fn vertical_bar(&self) -> f64 {
|
||||
let obj = self.obj();
|
||||
f64::max(
|
||||
0.,
|
||||
(self.widget_height() - self.image_displayed_height()) / 2.,
|
||||
(self.widget_height() - obj.image_displayed_height()) / 2.,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ use gtk::CompositeTemplate;
|
|||
|
||||
use crate::config;
|
||||
use crate::deps::*;
|
||||
use crate::widgets::{LpImageEdit, LpImageView, LpImageWindow};
|
||||
use crate::widgets::{LpEditWindow, LpImageView, LpImageWindow};
|
||||
|
||||
/// Show window after X milliseconds even if image dimensions are not known yet
|
||||
const SHOW_WINDOW_AFTER: u64 = 2000;
|
||||
|
|
@ -55,7 +55,7 @@ mod imp {
|
|||
#[template_child]
|
||||
pub(super) image_window: TemplateChild<LpImageWindow>,
|
||||
#[template_child]
|
||||
pub(super) edit_window: TemplateChild<LpImageEdit>,
|
||||
pub(super) edit_window_child: TemplateChild<adw::Bin>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
|
@ -144,8 +144,7 @@ impl LpWindow {
|
|||
pub fn resize_default(&self) {
|
||||
let imp = self.imp();
|
||||
|
||||
if imp
|
||||
.image_window
|
||||
if self
|
||||
.image_view()
|
||||
.current_image()
|
||||
.is_some_and(|img| img.image_size_available())
|
||||
|
|
@ -191,7 +190,14 @@ impl LpWindow {
|
|||
}
|
||||
|
||||
pub fn show_edit(&self) {
|
||||
self.imp().stack.set_visible_child(&*self.imp().edit_window);
|
||||
if let Some(image) = self.image_view().current_image() {
|
||||
let edit_child = &*self.imp().edit_window_child;
|
||||
|
||||
edit_child.set_child(Some(&LpEditWindow::new(image)));
|
||||
self.imp().stack.set_visible_child(edit_child);
|
||||
} else {
|
||||
log::error!("Can't open image editor since no current image exists");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_view(&self) -> LpImageView {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="LpImageEdit" id="edit_window"></object>
|
||||
<object class="AdwBin" id="edit_window_child">
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue