Compare commits

...
4 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 8fddd9e2c3 chore(release): bump version to 0.27.0
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 21m31s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:18:34 +02:00
haraldandClaude Opus 5.5 1a98c9b8bb feat: Knopf für das Seitenverhältnis-Schloss der Auswahl (Issue #29)
Im Tablet-Betrieb gibt es keine Umschalttaste. Beim Auswahlwerkzeug
steht unter den Stiftfarben jetzt ein Schloss: 🔒 skaliert gleichmäßig,
🔓 frei. Vorgabe ist gesperrt, sobald ein Bild ausgewählt ist, sonst
frei. Das Umschalten gilt für die aktuelle Auswahl; eine neue Auswahl
beginnt wieder mit der Vorgabe. Shift sperrt am Rechner zusätzlich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:13:15 +02:00
haraldandClaude Opus 5.5 98b78d29e6 feat: Bilder beim Skalieren nicht verzerren (Issue #29)
Enthält die Auswahl ein Bild, skaliert der Eckgriff gleichmäßig, das
Seitenverhältnis bleibt erhalten. Mit gedrückter Umschalttaste gilt das
auch für Striche und Formen. Der Faktor ist die Projektion der
Zugposition auf die Diagonale der Auswahl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:02:56 +02:00
haraldandClaude Opus 5.5 81ccd93438 docs: Code of Conduct, Mitmach-Anleitung, Sicherheitsrichtlinie und Vorlagen
Für die Community-Standards auf GitHub (Spiegel) und für Gitea:

- CODE_OF_CONDUCT.md: Contributor Covenant 2.1, Kontakt harald@pansi.eu
- CONTRIBUTING.md (Deutsch/Englisch): Gitea als Hauptrepository,
  Prüfungen wie in der CI, Tests, Dateiformat, Übersetzungen
- SECURITY.md: unterstützt wird die neueste Version, Meldungen
  vertraulich per E-Mail
- Issue-Vorlagen für Fehler und Ideen, Kontakt-Links (auf GitHub mit
  Verweis auf Gitea), Pull-Request-Vorlage
- README: Abschnitt „Mitmachen“

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:40:36 +02:00
18 changed files with 444 additions and 19 deletions
+7
View File
@@ -0,0 +1,7 @@
# Gitea liest die Vorlagen aus .github/ISSUE_TEMPLATE; diese Datei ersetzt dort nur die
# Kontakt-Links (der Verweis auf Gitea selbst entfällt).
blank_issues_enabled: true
contact_links:
- name: "Sicherheitslücke melden / Report a security issue"
url: https://gitea.pansi.eu/harald/ink-notes/src/branch/main/SECURITY.md
about: "Bitte vertraulich per E-Mail, nicht als öffentliches Issue. / Please report privately by e-mail, not as a public issue."
+28
View File
@@ -0,0 +1,28 @@
---
name: "Fehler melden / Bug report"
about: "Etwas funktioniert nicht wie erwartet / Something does not work as expected"
title: ""
---
<!-- Sicherheitslücken bitte nicht hier melden, siehe SECURITY.md. / Please report security issues privately, see SECURITY.md. -->
**Was passiert? / What happens?**
**Was hättest du erwartet? / What did you expect?**
**Schritte zum Nachstellen / Steps to reproduce**
1.
2.
3.
**Umgebung / Environment**
- ink-notes-Version / version (☰ → Updates):
- Installiert als / installed as (.deb, AppImage, Flatpak, selbst gebaut / built from source):
- Distribution:
- Wayland oder / or X11:
- Eingabegerät / input device (Stift / pen, Finger / touch, Maus / mouse, Touchpad):
**Bildschirmfoto, Aufnahme oder Ausgabe im Terminal / Screenshot, recording or terminal output**
<!-- Wenn möglich: ink-notes im Terminal starten und die Ausgabe hier einfügen. / If possible, start ink-notes in a terminal and paste its output here. -->
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: "Issues auf Gitea (Hauptrepository) / Issues on Gitea (main repository)"
url: https://gitea.pansi.eu/harald/ink-notes/issues
about: "GitHub ist ein Spiegel; Issues werden bevorzugt auf Gitea bearbeitet. / GitHub is a mirror; issues are preferably handled on Gitea."
- name: "Sicherheitslücke melden / Report a security issue"
url: https://gitea.pansi.eu/harald/ink-notes/src/branch/main/SECURITY.md
about: "Bitte vertraulich per E-Mail, nicht als öffentliches Issue. / Please report privately by e-mail, not as a public issue."
+14
View File
@@ -0,0 +1,14 @@
---
name: "Idee vorschlagen / Feature request"
about: "Eine neue Funktion oder Verbesserung / A new feature or improvement"
title: ""
---
**Was möchtest du tun können? / What would you like to be able to do?**
**Wie machst du es bisher, und was stört dich daran? / How do you do it today, and what bothers you about it?**
**Wie könnte es aussehen? / How could it work?**
<!-- Skizzen, Bildschirmfotos oder Beispiele aus anderen Apps helfen. / Sketches, screenshots or examples from other apps help. -->
+13
View File
@@ -0,0 +1,13 @@
<!-- GitHub ist ein Spiegel von https://gitea.pansi.eu/harald/ink-notes; Pull Requests werden dort von Hand übernommen. / GitHub is a mirror; pull requests are applied on Gitea by hand. -->
**Was ändert sich und warum? / What changes and why?**
**Zugehöriges Issue / Related issue:** #
**Checkliste / Checklist**
- [ ] `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` und / and `cargo test` laufen durch / pass
- [ ] Tests für neue Funktionen oder behobene Fehler / tests for new features or fixes
- [ ] Neue Texte der Oberfläche auch auf Englisch (`src/i18n/`) / new UI texts translated to English
- [ ] Änderungen am Dateiformat in `docs/FILE_FORMAT.md` beschrieben / file format changes documented (falls nötig / if needed)
- [ ] Ich habe CONTRIBUTING.md gelesen / I have read CONTRIBUTING.md
+85
View File
@@ -0,0 +1,85 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at harald@pansi.eu. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+43
View File
@@ -0,0 +1,43 @@
# Mitmachen bei ink-notes
Danke, dass du ink-notes verbessern willst! *(English below.)*
## Wo?
Das Hauptrepository liegt auf **Gitea**: <https://gitea.pansi.eu/harald/ink-notes>.
[GitHub](https://github.com/haraldpansi-gif/ink-notes) ist ein Spiegel davon – Issues und Pull Requests sind dort auch willkommen, Pull Requests werden aber von Hand nach Gitea übernommen.
## Fehler melden und Ideen vorschlagen
- Bitte zuerst nachsehen, ob es schon ein [Issue](https://gitea.pansi.eu/harald/ink-notes/issues) dazu gibt.
- Für Fehler die Vorlage „Fehler melden“ verwenden: Version (☰ → Updates), Distribution, Wayland oder X11, Eingabegerät (Stift, Finger, Maus, Touchpad) und die Schritte, mit denen der Fehler auftritt. Ein Bildschirmfoto oder eine kurze Aufnahme hilft oft sehr.
- Deutsch und Englisch sind beide in Ordnung.
- **Sicherheitslücken bitte nicht öffentlich melden**, sondern wie in [SECURITY.md](SECURITY.md) beschrieben.
## Code beitragen
1. Größere Änderungen am besten vorher in einem Issue absprechen.
2. Bauen und testen, wie im [README](README.md#entwicklung) beschrieben. Vor dem Einreichen müssen diese Prüfungen durchlaufen (die CI prüft sie ebenfalls):
```bash
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
```
3. Neue Funktionen und behobene Fehler bekommen einen Test, wo das sinnvoll geht.
4. Änderungen am Dateiformat bitte in [docs/FILE_FORMAT.md](docs/FILE_FORMAT.md) nachtragen; ältere Notizbücher müssen weiter lesbar bleiben.
5. Commit-Nachrichten im Stil von [Conventional Commits](https://www.conventionalcommits.org/), z. B. `fix: …`, `feat: …`, `perf: …`, `docs: …`; ein Bezug auf ein Issue als `(Issue #28)`.
6. Texte der Oberfläche gibt es auf Deutsch und Englisch: Neue Texte mit `t!("…")` schreiben und die englische Übersetzung unter `src/i18n/` ergänzen.
Mit dem Einreichen stimmst du zu, dass dein Beitrag unter der [MIT-Lizenz](LICENSE) des Projekts steht. Für den Umgang miteinander gilt der [Code of Conduct](CODE_OF_CONDUCT.md).
---
## Contributing (English)
The main repository is on **Gitea**: <https://gitea.pansi.eu/harald/ink-notes>. [GitHub](https://github.com/haraldpansi-gif/ink-notes) is a mirror; issues and pull requests are welcome there too, but pull requests are applied to Gitea by hand.
- **Bugs and ideas:** check the [existing issues](https://gitea.pansi.eu/harald/ink-notes/issues) first, then use the bug report or feature request template. Please include the version (☰ → Updates), distribution, Wayland or X11, the input device and the steps to reproduce. German and English are both fine.
- **Security issues:** please do not report them publicly; see [SECURITY.md](SECURITY.md).
- **Code:** discuss larger changes in an issue first. Before submitting, `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` and `cargo test` must pass. Add tests where it makes sense, document file format changes in `docs/FILE_FORMAT.md`, use Conventional Commits, and add English translations for new UI texts (`t!("…")`, see `src/i18n/`).
By contributing you agree that your contribution is licensed under the project's [MIT License](LICENSE). Everyone taking part is expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
Generated
+1 -1
View File
@@ -1393,7 +1393,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.26.2" version = "0.27.0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"arboard", "arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.26.2" version = "0.27.0"
edition = "2021" edition = "2021"
description = "Notizbuch-App für Stift, Touch und Maus unter Linux" description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
repository = "https://gitea.pansi.eu/harald/ink-notes" repository = "https://gitea.pansi.eu/harald/ink-notes"
+6
View File
@@ -240,6 +240,12 @@ Bleibt ein Job auf „Waiting to run“ stehen: Ist der Runner online (*Einstell
--- ---
## Mitmachen
Fehlermeldungen, Ideen und Beiträge sind willkommen – am liebsten als [Issue auf Gitea](https://gitea.pansi.eu/harald/ink-notes/issues); [GitHub](https://github.com/haraldpansi-gif/ink-notes) ist ein Spiegel. Details in [CONTRIBUTING.md](CONTRIBUTING.md), Sicherheitslücken bitte vertraulich wie in [SECURITY.md](SECURITY.md) beschrieben. Es gilt der [Code of Conduct](CODE_OF_CONDUCT.md).
---
## Lizenz ## Lizenz
[MIT](LICENSE), Copyright © 2026 Harald Pansi. [MIT](LICENSE), Copyright © 2026 Harald Pansi.
+26
View File
@@ -0,0 +1,26 @@
# Sicherheit / Security Policy
## Unterstützte Versionen / Supported versions
Sicherheitskorrekturen gibt es nur für die jeweils **neueste Version** (siehe [Releases](https://gitea.pansi.eu/harald/ink-notes/releases)). Die App bietet Updates selbst an (☰ → Updates); als Flatpak kommen sie über die Software-Verwaltung.
Security fixes are only provided for the **latest release**. The app offers updates itself (☰ → Updates); the Flatpak is updated through the software manager.
## Sicherheitslücke melden / Reporting a vulnerability
Bitte Sicherheitslücken **nicht** in einem öffentlichen Issue melden, sondern per E-Mail an **harald@pansi.eu** – möglichst mit:
- betroffener Version und Installationsart (.deb, AppImage, Flatpak, selbst gebaut),
- einer Beschreibung, was passieren kann,
- den Schritten oder einer Datei (z. B. ein präpariertes Notizbuch oder PDF), mit denen es sich nachstellen lässt.
Du bekommst innerhalb einer Woche eine Antwort. Ist die Lücke bestätigt, erscheint die Korrektur als neue Version; auf Wunsch wirst du in den Release-Notes genannt.
Please do **not** report vulnerabilities in a public issue. Send an e-mail to **harald@pansi.eu** instead, ideally with the affected version and installation type, a description of the impact and steps or a file (e.g. a crafted notebook or PDF) to reproduce it. You will get an answer within a week. Confirmed issues are fixed in a new release; on request you will be credited in the release notes.
## Besonders interessant / Areas of interest
- **Updates:** Prüfung der Minisign-Signatur und der SHA-256-Prüfsummen, Download und Austausch der Pakete (siehe „Sicherheit der Updates“ im [README](README.md#sicherheit-der-updates)).
- **Dateien öffnen:** importierte PDFs und Bilder, Notizbuch-Dateien ([Dateiformat](docs/FILE_FORMAT.md)) – z. B. Abstürze oder Schreiben außerhalb des Datenordners durch präparierte Dateien.
- **Updates:** verification of the Minisign signature and SHA-256 checksums, downloading and replacing packages.
- **Opening files:** imported PDFs and images, notebook files – e.g. crashes or writes outside the data folder caused by crafted files.
@@ -84,6 +84,11 @@
</branding> </branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.27.0" date="2026-10-18">
<description>
<p>Pictures keep their proportions when resized. A lock button in the selection tool switches between proportional and free resizing; pictures start locked, other objects free.</p>
</description>
</release>
<release version="0.26.2" date="2026-10-13"> <release version="0.26.2" date="2026-10-13">
<description> <description>
<p>Smoother scrolling through large imported PDFs: pages no longer stall the display while they are prepared, and pages scrolled past quickly are skipped.</p> <p>Smoother scrolling through large imported PDFs: pages no longer stall the display while they are prepared, and pages scrolled past quickly are skipped.</p>
+5
View File
@@ -84,6 +84,11 @@
</branding> </branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="0.27.0" date="2026-10-18">
<description>
<p>Pictures keep their proportions when resized. A lock button in the selection tool switches between proportional and free resizing; pictures start locked, other objects free.</p>
</description>
</release>
<release version="0.26.2" date="2026-10-13"> <release version="0.26.2" date="2026-10-13">
<description> <description>
<p>Smoother scrolling through large imported PDFs: pages no longer stall the display while they are prepared, and pages scrolled past quickly are skipped.</p> <p>Smoother scrolling through large imported PDFs: pages no longer stall the display while they are prepared, and pages scrolled past quickly are skipped.</p>
+1
View File
@@ -3,6 +3,7 @@
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04). Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
### Highlights: ### Highlights:
- **Bilder ohne Verzerren skalieren**: Beim Auswahlwerkzeug schaltet ein Schloss unter den Stiftfarben zwischen gleichmäßigem (🔒) und freiem Skalieren (🔓) um. Bilder sind zu Beginn gesperrt, Striche und Formen frei; am Rechner sperrt zusätzlich die Umschalttaste
- **Flüssigeres Scrollen in großen PDFs**: Neu gerenderte PDF-Seiten halten die Anzeige nicht mehr an, und Seiten, über die man schnell hinwegscrollt, werden übersprungen statt der Reihe nach gerendert - **Flüssigeres Scrollen in großen PDFs**: Neu gerenderte PDF-Seiten halten die Anzeige nicht mehr an, und Seiten, über die man schnell hinwegscrollt, werden übersprungen statt der Reihe nach gerendert
- **Screenshots für Flathub**: Die App-Beschreibung (Metainfo) zeigt jetzt drei Screenshots; an der App selbst ändert sich nichts - **Screenshots für Flathub**: Die App-Beschreibung (Metainfo) zeigt jetzt drei Screenshots; an der App selbst ändert sich nichts
- **Auswählen mit dem Finger**: Im Auswahlwerkzeug wählt und verschiebt ein Finger Objekte (auf leerer Fläche scrollt er weiter); Text und Platz schaffen gehen ebenfalls mit dem Finger - **Auswählen mit dem Finger**: Im Auswahlwerkzeug wählt und verschiebt ein Finger Objekte (auf leerer Fläche scrollt er weiter); Text und Platz schaffen gehen ebenfalls mit dem Finger
+2
View File
@@ -286,6 +286,8 @@ pub const EN: &[(&str, &str)] = &[
("Seite {number}", "Page {number}"), ("Seite {number}", "Page {number}"),
("Seiten ({count})", "Pages ({count})"), ("Seiten ({count})", "Pages ({count})"),
("Seiten durchgehend scrollen", "Scroll pages continuously"), ("Seiten durchgehend scrollen", "Scroll pages continuously"),
("Seitenverhältnis frei – tippen zum Sperren", "Aspect ratio free – tap to lock"),
("Seitenverhältnis gesperrt – tippen zum Freigeben", "Aspect ratio locked – tap to unlock"),
("Serialisierungsfehler: {e}", "Serialization error: {e}"), ("Serialisierungsfehler: {e}", "Serialization error: {e}"),
("Server-Einstellungen gespeichert", "Server settings saved"), ("Server-Einstellungen gespeichert", "Server settings saved"),
("Server:", "Server:"), ("Server:", "Server:"),
+23 -17
View File
@@ -31,7 +31,7 @@ use ink_notes::osk::{self, KeyboardRequester};
use ink_notes::pdf_doc::PdfRenderService; use ink_notes::pdf_doc::PdfRenderService;
use ink_notes::platform::{self, DisplayBackend}; use ink_notes::platform::{self, DisplayBackend};
use ink_notes::selection::{ use ink_notes::selection::{
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState, render_selection_overlay, scale_factors, DragMode, SelectionHandleHit, SelectionState,
}; };
use ink_notes::settings::{ use ink_notes::settings::{
SidebarSide, StylusAction, StylusButton, UiSettings, MAX_STYLUS_BUTTONS, SidebarSide, StylusAction, StylusButton, UiSettings, MAX_STYLUS_BUTTONS,
@@ -649,12 +649,18 @@ fn selection_press(
.. ..
}, },
_, _,
) => DragMode::Scale { ) => {
fixed_corner_page, let initial_strokes = page.get_strokes(&selection_state.selected_ids);
start_pos_page, // Schloss der Auswahl (Bilder: an, Issue #29); Shift sperrt zusätzlich
initial_box_size, let keep_aspect = shift || selection_state.keeps_aspect(page);
initial_strokes: page.get_strokes(&selection_state.selected_ids), DragMode::Scale {
}, fixed_corner_page,
start_pos_page,
initial_box_size,
initial_strokes,
keep_aspect,
}
}
(SelectionHandleHit::Body, Some(p)) => DragMode::Move { (SelectionHandleHit::Body, Some(p)) => DragMode::Move {
start_page: p, start_page: p,
initial_strokes: page.get_strokes(&selection_state.selected_ids), initial_strokes: page.get_strokes(&selection_state.selected_ids),
@@ -729,18 +735,10 @@ fn selection_drag(
DragMode::Scale { DragMode::Scale {
fixed_corner_page, fixed_corner_page,
start_pos_page, start_pos_page,
keep_aspect,
.. ..
} => { } => {
let orig = *start_pos_page - *fixed_corner_page; let factors = scale_factors(*fixed_corner_page, *start_pos_page, page_pt, *keep_aspect);
let new = page_pt - *fixed_corner_page;
let factor = |n: f32, o: f32| {
if o.abs() > 1.0 {
(n / o).clamp(0.05, 20.0)
} else {
1.0
}
};
let factors = Vec2::new(factor(new.x, orig.x), factor(new.y, orig.y));
updated updated
.iter_mut() .iter_mut()
.for_each(|s| s.scale(*fixed_corner_page, factors)); .for_each(|s| s.scale(*fixed_corner_page, factors));
@@ -3103,6 +3101,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 3. Toolbar & Widgets rendern // 3. Toolbar & Widgets rendern
updater.update(); updater.update();
ui_state.page_view = Some(page_view(&canvas, window.scale_factor() as f32)); ui_state.page_view = Some(page_view(&canvas, window.scale_factor() as f32));
ui_state.selection_aspect_lock = (!selection_state.is_empty())
.then(|| selection_state.keeps_aspect(notebook.current_page()));
ui_state.restorable_page = last_deleted_page ui_state.restorable_page = last_deleted_page
.as_ref() .as_ref()
.filter(|(id, _, _)| *id == manifest.id) .filter(|(id, _, _)| *id == manifest.id)
@@ -3203,6 +3203,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
update_window_title(&window, &notebook, &manifest, ui_state.active_tool, autosave.status()); update_window_title(&window, &notebook, &manifest, ui_state.active_tool, autosave.status());
window.request_redraw(); window.request_redraw();
} }
UiAction::ToggleAspectLock => {
if !selection_state.is_empty() {
selection_state.toggle_aspect_lock(notebook.current_page());
window.request_redraw();
}
}
UiAction::RotateSelected => { UiAction::RotateSelected => {
if !selection_state.is_empty() if !selection_state.is_empty()
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) { && selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
+118
View File
@@ -36,9 +36,34 @@ pub enum DragMode {
start_pos_page: Vec2, start_pos_page: Vec2,
initial_box_size: Vec2, initial_box_size: Vec2,
initial_strokes: Vec<Stroke>, initial_strokes: Vec<Stroke>,
/// Seitenverhältnis beibehalten (Schloss der Auswahl oder Shift gedrückt)
keep_aspect: bool,
}, },
} }
/// Skalierungsfaktoren, wenn der Eckgriff von `start` (relativ zur festen Ecke `fixed`)
/// nach `current` gezogen wird. Mit `keep_aspect` wird gleichmäßig skaliert: der Faktor ist
/// die Projektion der Zugposition auf die Diagonale, damit das Seitenverhältnis bleibt.
pub fn scale_factors(fixed: Vec2, start: Vec2, current: Vec2, keep_aspect: bool) -> Vec2 {
let orig = start - fixed;
let new = current - fixed;
if keep_aspect {
let len_sq = orig.length_squared();
if len_sq <= 1.0 {
return Vec2::ONE;
}
return Vec2::splat((new.dot(orig) / len_sq).clamp(0.05, 20.0));
}
let factor = |n: f32, o: f32| {
if o.abs() > 1.0 {
(n / o).clamp(0.05, 20.0)
} else {
1.0
}
};
Vec2::new(factor(new.x, orig.x), factor(new.y, orig.y))
}
/// Ergebnis eines Hit-Tests auf Auswahl-Griffe (Handles). /// Ergebnis eines Hit-Tests auf Auswahl-Griffe (Handles).
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum SelectionHandleHit { pub enum SelectionHandleHit {
@@ -64,6 +89,9 @@ pub struct SelectionState {
pub selected_ids: Vec<u64>, pub selected_ids: Vec<u64>,
pub drag_mode: DragMode, pub drag_mode: DragMode,
pub clipboard: Vec<Stroke>, pub clipboard: Vec<Stroke>,
/// Von Hand umgeschaltetes Seitenverhältnis-Schloss und die Auswahl (IDs), für die es
/// gilt. Ändert sich die Auswahl, gilt wieder die Vorgabe (siehe [`Self::keeps_aspect`]).
aspect_lock: Option<(Vec<u64>, bool)>,
} }
impl SelectionState { impl SelectionState {
@@ -72,9 +100,28 @@ impl SelectionState {
selected_ids: Vec::new(), selected_ids: Vec::new(),
drag_mode: DragMode::None, drag_mode: DragMode::None,
clipboard: Vec::new(), clipboard: Vec::new(),
aspect_lock: None,
} }
} }
/// Ob Skalieren an den Eckgriffen das Seitenverhältnis der Auswahl beibehält. Vorgabe:
/// ja, sobald ein Bild ausgewählt ist (Issue #29); per Knopf für diese Auswahl umschaltbar.
pub fn keeps_aspect(&self, page: &Page) -> bool {
match &self.aspect_lock {
Some((ids, locked)) if *ids == self.selected_ids => *locked,
_ => page
.get_strokes(&self.selected_ids)
.iter()
.any(|s| s.is_image()),
}
}
/// Schaltet das Seitenverhältnis-Schloss der aktuellen Auswahl um.
pub fn toggle_aspect_lock(&mut self, page: &Page) {
let locked = !self.keeps_aspect(page);
self.aspect_lock = Some((self.selected_ids.clone(), locked));
}
/// Prüft, ob aktuell Objekte ausgewählt sind. /// Prüft, ob aktuell Objekte ausgewählt sind.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.selected_ids.is_empty() self.selected_ids.is_empty()
@@ -523,6 +570,77 @@ mod tests {
assert!((orig_pos.x - restored_pos.x).abs() < 1e-4); assert!((orig_pos.x - restored_pos.x).abs() < 1e-4);
} }
#[test]
fn test_scale_factors_free_and_keep_aspect() {
let fixed = Vec2::new(0.0, 0.0);
let start = Vec2::new(200.0, 100.0);
// Frei: Breite und Höhe unabhängig
let free = scale_factors(fixed, start, Vec2::new(400.0, 100.0), false);
assert_eq!(free, Vec2::new(2.0, 1.0));
// Seitenverhältnis fest: gleicher Faktor in beiden Richtungen
let locked = scale_factors(fixed, start, Vec2::new(400.0, 100.0), true);
assert!((locked.x - locked.y).abs() < 1e-6);
assert!((locked.x - 1.8).abs() < 1e-4);
// Entlang der Diagonale genau verdoppelt
let diag = scale_factors(fixed, start, Vec2::new(400.0, 200.0), true);
assert!((diag - Vec2::splat(2.0)).length() < 1e-5);
// Über die feste Ecke hinaus: kein Spiegeln
let past = scale_factors(fixed, start, Vec2::new(-50.0, -50.0), true);
assert_eq!(past, Vec2::splat(0.05));
// Senkrechte Linie (Breite 0): frei bleibt x unverändert, fest skaliert beides
let line = Vec2::new(0.0, 100.0);
assert_eq!(
scale_factors(fixed, line, Vec2::new(30.0, 200.0), false),
Vec2::new(1.0, 2.0)
);
assert_eq!(
scale_factors(fixed, line, Vec2::new(30.0, 200.0), true),
Vec2::splat(2.0)
);
}
#[test]
fn test_aspect_lock_defaults_to_images_and_toggles_per_selection() {
let mut page = Page::new(1, PageBackground::Blank);
let corners = [
Vec2::new(0.0, 0.0),
Vec2::new(100.0, 0.0),
Vec2::new(100.0, 50.0),
Vec2::new(0.0, 50.0),
];
page.add_stroke(Stroke::new_image(1, "a.png".into(), corners));
page.add_stroke(make_test_stroke(2, 200.0, 200.0, [0.1, 0.1, 0.1, 1.0]));
let mut sel = SelectionState::new();
// Vorgabe: Bilder gesperrt, Striche frei
sel.select_single(1);
assert!(sel.keeps_aspect(&page));
sel.select_single(2);
assert!(!sel.keeps_aspect(&page));
// Umschalten gilt für diese Auswahl
sel.toggle_aspect_lock(&page);
assert!(sel.keeps_aspect(&page));
sel.select_single(1);
sel.toggle_aspect_lock(&page);
assert!(!sel.keeps_aspect(&page));
sel.toggle_aspect_lock(&page);
assert!(sel.keeps_aspect(&page));
sel.toggle_aspect_lock(&page);
// Neue Auswahl: wieder die Vorgabe
sel.select_multiple(vec![1, 2]);
assert!(sel.keeps_aspect(&page));
sel.select_single(1);
assert!(
!sel.keeps_aspect(&page),
"gleiche Auswahl behält das Umschalten"
);
sel.clear();
sel.select_single(1);
assert!(!sel.keeps_aspect(&page));
}
#[test] #[test]
fn test_recolor_ignores_images() { fn test_recolor_ignores_images() {
let mut page = Page::new(1, PageBackground::Blank); let mut page = Page::new(1, PageBackground::Blank);
+58
View File
@@ -255,6 +255,8 @@ pub enum UiAction {
/// Zwischenablage an dieser Stelle der aktuellen Seite einfügen (Kontextmenü) /// Zwischenablage an dieser Stelle der aktuellen Seite einfügen (Kontextmenü)
PasteAt(glam::Vec2), PasteAt(glam::Vec2),
RotateSelected, RotateSelected,
/// Seitenverhältnis beim Skalieren der Auswahl sperren bzw. freigeben
ToggleAspectLock,
// Dokumenten- & Notizbuch-Verwaltung // Dokumenten- & Notizbuch-Verwaltung
OpenNotebook(String), OpenNotebook(String),
/// Neues Notizbuch im Ordner `folder` anlegen und öffnen /// Neues Notizbuch im Ordner `folder` anlegen und öffnen
@@ -410,6 +412,8 @@ pub struct UiState {
pub confirm_delete_page: bool, pub confirm_delete_page: bool,
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann /// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
pub restorable_page: Option<usize>, pub restorable_page: Option<usize>,
/// Seitenverhältnis-Schloss der Auswahl (`None`: nichts ausgewählt)
pub selection_aspect_lock: Option<bool>,
/// Fehler beim Flushen vor dem Beenden /// Fehler beim Flushen vor dem Beenden
pub shutdown_error: Option<crate::autosave::AutosaveError>, pub shutdown_error: Option<crate::autosave::AutosaveError>,
/// Fensterseite der Werkzeugleiste /// Fensterseite der Werkzeugleiste
@@ -508,6 +512,7 @@ impl Default for UiState {
pressed_stylus_button: None, pressed_stylus_button: None,
confirm_delete_page: false, confirm_delete_page: false,
restorable_page: None, restorable_page: None,
selection_aspect_lock: None,
shutdown_error: None, shutdown_error: None,
sidebar_side: SidebarSide::Left, sidebar_side: SidebarSide::Left,
language: None, language: None,
@@ -3045,6 +3050,30 @@ pub fn render_ui(
used += btn + spacing; used += btn + spacing;
} }
} }
// Auswahl: unter den 4 Stiftfarben ist Platz für das Schloss des
// Seitenverhältnisses beim Skalieren (Bilder: gesperrt, Issue #29)
if state.active_tool == ActiveTool::Select {
let locked = state.selection_aspect_lock;
let (icon, tooltip) = if locked == Some(true) {
("🔒", t!("Seitenverhältnis gesperrt – tippen zum Freigeben"))
} else {
("🔓", t!("Seitenverhältnis frei – tippen zum Sperren"))
};
if sidebar_button(
ui,
swatch,
SidebarIcon::Glyph(icon),
locked == Some(true),
locked.is_some(),
None,
tooltip,
)
.clicked()
{
actions.push(UiAction::ToggleAspectLock);
}
used += swatch + spacing;
}
let reserved = let reserved =
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing); UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
if reserved > used { if reserved > used {
@@ -5189,6 +5218,35 @@ mod tests {
} }
} }
#[test]
fn test_aspect_lock_button_only_with_selection() {
// Den Knopf in der Leiste des Auswahlwerkzeugs suchen: ohne Auswahl gesperrt
let notebook = Notebook::new();
let toggles = |lock: Option<bool>| {
let mut probe = UiState::new();
probe.active_tool = ActiveTool::Select;
render_frames(&mut probe, &notebook, 10);
let bar = probe.toolbar_rect.unwrap();
let mut found = 0;
let mut y = bar.min.y + 4.0;
while y < bar.max.y {
let mut state = UiState::new();
state.active_tool = ActiveTool::Select;
state.selection_aspect_lock = lock;
let ctx = render_frames(&mut state, &notebook, 10);
let actions = click_at(&ctx, &mut state, &notebook, egui::pos2(bar.center().x, y));
if actions.contains(&UiAction::ToggleAspectLock) {
found += 1;
}
y += 4.0;
}
found
};
assert_eq!(toggles(None), 0);
assert!(toggles(Some(true)) > 0);
assert!(toggles(Some(false)) > 0);
}
#[test] #[test]
fn test_sidebar_fits_window_height_for_every_tool() { fn test_sidebar_fits_window_height_for_every_tool() {
let notebook = Notebook::new(); let notebook = Notebook::new();