Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d880cb7fb
|
Generated
+1
-1
@@ -1344,7 +1344,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "inkr"
|
||||
version = "1.0.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"eframe",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "inkr"
|
||||
version = "1.0.0"
|
||||
version = "0.1.0"
|
||||
authors = []
|
||||
edition = "2024"
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
pkgname=inkr
|
||||
pkgver=1.0.0
|
||||
pkgrel=1
|
||||
pkgdesc="A note-taking and handwriting tool"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://git.nubo.sh/hulthe/inkr"
|
||||
#license=('GPL')
|
||||
groups=('base-devel')
|
||||
depends=('glibc')
|
||||
makedepends=('cargo')
|
||||
#optdepends=('ed: for "patch -e" functionality')
|
||||
#source=(" ftp://ftp.gnu.org/gnu/$pkgname/$pkgname-$pkgver.tar.xz"{,.sig})
|
||||
#sha256sums=('SKIP')
|
||||
prepare() {
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')"
|
||||
}
|
||||
build() {
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
cargo build --frozen --release
|
||||
}
|
||||
package() {
|
||||
cd ..
|
||||
install -Dm0755 -t "$pkgdir/usr/bin/" "${CARGO_TARGET_DIR:-target}/release/$pkgname"
|
||||
install -Dm0755 -t "$pkgdir/usr/share/applications/" "assets/$pkgname.desktop"
|
||||
install -Dm0755 "assets/icon.svg" "$pkgdir/usr/share/pixmaps/$pkgname.svg"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=inkr
|
||||
Exec=inkr
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=inkr
|
||||
StartupWMClass=inkr
|
||||
MimeType=x-scheme-handler/inkr;
|
||||
Categories=Office;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+54
-110
@@ -2,14 +2,11 @@ use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::{Arc, mpsc},
|
||||
thread::JoinHandle,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender};
|
||||
use egui::{
|
||||
Align, Button, Color32, Context, FontData, FontDefinitions, Key, Modifiers, PointerButton,
|
||||
RichText, ScrollArea, Stroke,
|
||||
Align, Button, Color32, FontData, FontDefinitions, PointerButton, RichText, ScrollArea, Stroke,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
@@ -21,8 +18,6 @@ pub struct App {
|
||||
actions_tx: mpsc::Sender<Action>,
|
||||
#[serde(skip)]
|
||||
actions_rx: mpsc::Receiver<Action>,
|
||||
#[serde(skip)]
|
||||
jobs: Jobs,
|
||||
|
||||
tabs: Vec<(TabId, Tab)>,
|
||||
open_tab_index: Option<usize>,
|
||||
@@ -30,33 +25,6 @@ pub struct App {
|
||||
next_tab_id: TabId,
|
||||
}
|
||||
|
||||
pub struct Jobs {
|
||||
handles: Vec<JoinHandle<()>>,
|
||||
actions_tx: mpsc::Sender<Action>,
|
||||
}
|
||||
|
||||
impl Jobs {
|
||||
fn start(&mut self, ctx: &Context, job: impl FnOnce() -> Option<Action> + Send + 'static) {
|
||||
let ctx = ctx.clone();
|
||||
let actions_tx = self.actions_tx.clone();
|
||||
self.handles.push(std::thread::spawn(move || {
|
||||
// start rendering the spinner thingy
|
||||
ctx.request_repaint();
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
if let Some(action) = job() {
|
||||
let _ = actions_tx.send(action);
|
||||
ctx.request_repaint();
|
||||
};
|
||||
|
||||
// Make sure that task takes at least 250ms to run, so that the spinner won't blink
|
||||
let sleep_for = Duration::from_millis(250).saturating_sub(start.elapsed());
|
||||
std::thread::sleep(sleep_for);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
enum Tab {
|
||||
File(FileEditor),
|
||||
@@ -68,12 +36,6 @@ impl Tab {
|
||||
Tab::File(file_editor) => file_editor.title(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
match self {
|
||||
Tab::File(file_editor) => file_editor.is_dirty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type TabId = usize;
|
||||
@@ -93,12 +55,8 @@ impl Default for App {
|
||||
let (actions_tx, actions_rx) = mpsc::channel();
|
||||
Self {
|
||||
preferences: Preferences::default(),
|
||||
actions_tx: actions_tx.clone(/* this is silly, i know */),
|
||||
actions_rx,
|
||||
jobs: Jobs {
|
||||
handles: Default::default(),
|
||||
actions_tx,
|
||||
},
|
||||
actions_rx,
|
||||
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
||||
open_tab_index: None,
|
||||
next_tab_id: 2,
|
||||
@@ -188,7 +146,7 @@ impl App {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn actions_tx(&self, ctx: &Context) -> GuiSender<Action> {
|
||||
fn actions_tx(&self, ctx: &egui::Context) -> GuiSender<Action> {
|
||||
GuiSender::new(self.actions_tx.clone(), ctx)
|
||||
}
|
||||
|
||||
@@ -217,11 +175,9 @@ impl eframe::App for App {
|
||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.preferences.apply(ctx);
|
||||
|
||||
self.jobs.handles.retain(|job| !job.is_finished());
|
||||
|
||||
while let Ok(action) = self.actions_rx.try_recv() {
|
||||
self.handle_action(action);
|
||||
}
|
||||
@@ -230,11 +186,11 @@ impl eframe::App for App {
|
||||
self.open_tab_index = Some(self.tabs.len().saturating_sub(1));
|
||||
}
|
||||
|
||||
ctx.input_mut(|input| {
|
||||
if input.consume_key(Modifiers::CTRL, Key::S) {
|
||||
self.save_active_tab(ctx);
|
||||
}
|
||||
});
|
||||
//ctx.input_mut(|input| {
|
||||
// if input.consume_key(Modifiers::CTRL, Key::H) {
|
||||
// self.buffer.push(BufferItem::Painting(Default::default()));
|
||||
// }
|
||||
//});
|
||||
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
egui::containers::menu::Bar::new().ui(ui, |ui| {
|
||||
@@ -249,15 +205,22 @@ impl eframe::App for App {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if ui.button("Open File").clicked() {
|
||||
self.jobs.start(ui.ctx(), move || {
|
||||
let file_path = rfd::FileDialog::new().pick_file()?;
|
||||
let actions_tx = self.actions_tx(ui.ctx());
|
||||
std::thread::spawn(move || {
|
||||
let file = rfd::FileDialog::new().pick_file();
|
||||
|
||||
let text = fs::read_to_string(&file_path)
|
||||
.inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}"))
|
||||
.ok()?;
|
||||
let Some(file_path) = file else { return };
|
||||
|
||||
let text = match fs::read_to_string(&file_path) {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
log::error!("Failed to read {file_path:?}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let editor = FileEditor::from_file(file_path, &text);
|
||||
Some(Action::OpenFile(editor))
|
||||
let _ = actions_tx.send(Action::OpenFile(editor));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,19 +237,6 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
|
||||
let can_save_file = self
|
||||
.open_tab_index
|
||||
.and_then(|i| self.tabs.get(i))
|
||||
.and_then(|(id, tab)| match tab {
|
||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
||||
})
|
||||
.and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)))
|
||||
.is_some();
|
||||
|
||||
if ui.add_enabled(can_save_file, Button::new("Save")).clicked() {
|
||||
self.save_active_tab(ui.ctx());
|
||||
}
|
||||
|
||||
let open_file =
|
||||
self.open_tab_index
|
||||
.and_then(|i| self.tabs.get(i))
|
||||
@@ -294,22 +244,45 @@ impl eframe::App for App {
|
||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
||||
});
|
||||
|
||||
let open_file_with_path = open_file
|
||||
.clone()
|
||||
.and_then(|(_, file_editor)| file_editor.path().zip(Some(file_editor)));
|
||||
|
||||
if ui
|
||||
.add_enabled(open_file_with_path.is_some(), Button::new("Save"))
|
||||
.clicked()
|
||||
{
|
||||
if let Some((file_path, file_editor)) = open_file_with_path {
|
||||
let text = file_editor.to_string();
|
||||
let file_path = file_path.to_owned();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = fs::write(file_path, text.as_bytes()) {
|
||||
log::error!("{e}");
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if ui
|
||||
.add_enabled(open_file.is_some(), Button::new("Save As"))
|
||||
.clicked()
|
||||
{
|
||||
let actions_tx = self.actions_tx(ui.ctx());
|
||||
let (tab_id, editor) =
|
||||
open_file.expect("We checked that open_file is_some");
|
||||
let text = editor.to_string();
|
||||
self.jobs.start(ui.ctx(), move || {
|
||||
let file_path = rfd::FileDialog::new().save_file()?;
|
||||
std::thread::spawn(move || {
|
||||
let Some(file_path) = rfd::FileDialog::new().save_file() else {
|
||||
return;
|
||||
};
|
||||
|
||||
fs::write(&file_path, text.as_bytes())
|
||||
.inspect_err(|e| log::error!("{e}"))
|
||||
.ok()?;
|
||||
if let Err(e) = fs::write(&file_path, text.as_bytes()) {
|
||||
log::error!("{e}");
|
||||
return;
|
||||
};
|
||||
|
||||
Some(Action::MoveFile(tab_id, file_path))
|
||||
let _ = actions_tx.send(Action::MoveFile(tab_id, file_path));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -324,9 +297,7 @@ impl eframe::App for App {
|
||||
}
|
||||
});
|
||||
|
||||
if !self.jobs.handles.is_empty() {
|
||||
ui.spinner();
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
@@ -335,7 +306,8 @@ impl eframe::App for App {
|
||||
let selected = self.open_tab_index == Some(i);
|
||||
let mut button = Button::new(tab.title()).selected(selected);
|
||||
|
||||
if tab.is_dirty() {
|
||||
let dirty = i == 0; // TODO: mark as dirty when contents hasn't been saved
|
||||
if dirty {
|
||||
button = button.right_text(RichText::new("*").strong())
|
||||
}
|
||||
|
||||
@@ -382,33 +354,5 @@ impl App {
|
||||
let id = self.next_tab_id;
|
||||
self.next_tab_id += 1;
|
||||
self.tabs.insert(i, (id, tab));
|
||||
self.open_tab_index = Some(i);
|
||||
}
|
||||
|
||||
fn save_active_tab(&mut self, ctx: &Context) {
|
||||
let open_file = self
|
||||
.open_tab_index
|
||||
.and_then(|i| self.tabs.get_mut(i))
|
||||
.and_then(|(id, tab)| match tab {
|
||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
||||
})
|
||||
.and_then(|(_, file_editor)| {
|
||||
file_editor
|
||||
.path()
|
||||
.map(ToOwned::to_owned)
|
||||
.zip(Some(file_editor))
|
||||
});
|
||||
|
||||
if let Some((file_path, file_editor)) = open_file {
|
||||
file_editor.is_dirty = false;
|
||||
let text = file_editor.to_string();
|
||||
let file_path = file_path.to_owned();
|
||||
self.jobs.start(ctx, move || {
|
||||
if let Err(e) = fs::write(file_path, text.as_bytes()) {
|
||||
log::error!("{e}");
|
||||
};
|
||||
None
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-17
@@ -12,7 +12,7 @@ use egui::{
|
||||
|
||||
use crate::{
|
||||
custom_code_block::{MdItem, iter_lines_and_code_blocks},
|
||||
handwriting::{self, Handwriting, HandwritingStyle},
|
||||
painting::{self, Handwriting, HandwritingStyle},
|
||||
preferences::Preferences,
|
||||
text_editor::MdTextEdit,
|
||||
};
|
||||
@@ -22,9 +22,6 @@ pub struct FileEditor {
|
||||
title: String,
|
||||
pub path: Option<PathBuf>,
|
||||
pub buffer: Vec<BufferItem>,
|
||||
|
||||
/// Whether the file has been edited since it was laste saved to disk.
|
||||
pub is_dirty: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
@@ -40,7 +37,6 @@ impl FileEditor {
|
||||
title: title.into(),
|
||||
path: None,
|
||||
buffer,
|
||||
is_dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +69,9 @@ impl FileEditor {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("new");
|
||||
if ui.button("text").clicked() {
|
||||
self.is_dirty = true;
|
||||
self.buffer.push(BufferItem::Text(Default::default()));
|
||||
}
|
||||
if ui.button("writing").clicked() {
|
||||
self.is_dirty = true;
|
||||
self.buffer
|
||||
.push(BufferItem::Handwriting(Default::default()));
|
||||
}
|
||||
@@ -144,18 +138,14 @@ impl FileEditor {
|
||||
|
||||
let item_response = ui.allocate_ui(item_size, |ui| match item {
|
||||
BufferItem::Text(text_edit) => {
|
||||
if text_edit.ui(ui).changed {
|
||||
self.is_dirty = true;
|
||||
text_edit.ui(ui);
|
||||
}
|
||||
}
|
||||
BufferItem::Handwriting(handwriting) => {
|
||||
BufferItem::Handwriting(painting) => {
|
||||
let style = HandwritingStyle {
|
||||
animate: preferences.animations,
|
||||
..HandwritingStyle::from_theme(ui.ctx().theme())
|
||||
};
|
||||
if handwriting.ui(&style, ui).changed {
|
||||
self.is_dirty = true;
|
||||
}
|
||||
painting.ui(&style, ui);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -221,12 +211,10 @@ impl FileEditor {
|
||||
Ordering::Greater => {
|
||||
let item = self.buffer.remove(from);
|
||||
self.buffer.insert(to, item);
|
||||
self.is_dirty = true;
|
||||
}
|
||||
Ordering::Less => {
|
||||
let item = self.buffer.remove(from);
|
||||
self.buffer.insert(to - 1, item);
|
||||
self.is_dirty = true;
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
@@ -287,7 +275,7 @@ impl From<&str> for FileEditor {
|
||||
match item {
|
||||
MdItem::Line(line) => push_text(buffer, line),
|
||||
MdItem::CodeBlock { key, content, span } => match key {
|
||||
handwriting::CODE_BLOCK_KEY => match Handwriting::from_str(span) {
|
||||
painting::CODE_BLOCK_KEY => match Handwriting::from_str(span) {
|
||||
Ok(handwriting) => {
|
||||
if let Some(BufferItem::Text(text_edit)) = buffer.last_mut() {
|
||||
if text_edit.text.ends_with('\n') {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
//! see [Packet]
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use half::f16;
|
||||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
/// A `u16` encoded in little-endian.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, PartialEq, Eq)]
|
||||
#[repr(C, packed)]
|
||||
pub struct u16_le([u8; 2]);
|
||||
|
||||
/// An `f16` encoded in little-endian.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||
#[repr(C, packed)]
|
||||
pub struct f16_le(u16_le);
|
||||
|
||||
/// Top-level type describing the handwriting disk-format.
|
||||
#[derive(FromBytes, KnownLayout, Immutable)]
|
||||
#[repr(C, packed)]
|
||||
pub struct DiskFormat {
|
||||
pub header: Header,
|
||||
|
||||
/// A packed array of [Stroke]s.
|
||||
pub strokes: [u8],
|
||||
}
|
||||
|
||||
pub const V1: u16_le = u16_le::new(1);
|
||||
|
||||
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||
#[repr(C, packed)]
|
||||
pub struct Header {
|
||||
/// Version of the disk format
|
||||
pub version: u16_le,
|
||||
}
|
||||
|
||||
#[derive(FromBytes, IntoBytes, KnownLayout, Immutable)]
|
||||
#[repr(C, packed)]
|
||||
pub struct RawStrokeHeader {
|
||||
/// Number of points in the stroke.
|
||||
pub len: u16_le,
|
||||
}
|
||||
|
||||
#[derive(FromBytes, KnownLayout, Immutable)]
|
||||
#[repr(C, packed)]
|
||||
pub struct RawStroke {
|
||||
pub header: RawStrokeHeader,
|
||||
pub positions: [f16_le],
|
||||
}
|
||||
|
||||
impl RawStroke {
|
||||
pub const MIN_LEN: usize = size_of::<RawStrokeHeader>();
|
||||
}
|
||||
|
||||
impl u16_le {
|
||||
pub const fn new(init: u16) -> Self {
|
||||
u16_le(init.to_le_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl f16_le {
|
||||
pub const fn new(init: f16) -> Self {
|
||||
f16_le(u16_le::new(init.to_bits()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for u16_le {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
u16::from(*self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16_le> for u16 {
|
||||
fn from(value: u16_le) -> Self {
|
||||
u16::from_le_bytes(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f16_le> for f16 {
|
||||
fn from(value: f16_le) -> Self {
|
||||
f16::from_bits(u16::from(value.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for u16_le {
|
||||
fn from(value: u16) -> Self {
|
||||
u16_le::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f16> for f16_le {
|
||||
fn from(value: f16) -> Self {
|
||||
f16_le::new(value)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ pub mod constants;
|
||||
pub mod custom_code_block;
|
||||
pub mod easy_mark;
|
||||
pub mod file_editor;
|
||||
pub mod handwriting;
|
||||
pub mod painting;
|
||||
pub mod preferences;
|
||||
pub mod rasterizer;
|
||||
pub mod text_editor;
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::{
|
||||
};
|
||||
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
use disk_format::{DiskFormat, RawStroke, RawStrokeHeader, f16_le};
|
||||
use egui::{
|
||||
Color32, ColorImage, CornerRadius, Event, Frame, Id, Mesh, PointerButton, Pos2, Rect, Sense,
|
||||
Shape, Stroke, TextureHandle, Theme, Ui, Vec2,
|
||||
@@ -17,7 +16,7 @@ use egui::{
|
||||
use eyre::{Context, bail};
|
||||
use eyre::{OptionExt, eyre};
|
||||
use half::f16;
|
||||
use zerocopy::{FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
use crate::{
|
||||
custom_code_block::try_from_custom_code_block,
|
||||
@@ -25,8 +24,6 @@ use crate::{
|
||||
};
|
||||
use crate::{custom_code_block::write_custom_code_block, util::random_id};
|
||||
|
||||
mod disk_format;
|
||||
|
||||
const HANDWRITING_MIN_HEIGHT: f32 = 100.0;
|
||||
const HANDWRITING_BOTTOM_PADDING: f32 = 80.0;
|
||||
const HANDWRITING_MARGIN: f32 = 0.05;
|
||||
@@ -97,10 +94,6 @@ pub struct Handwriting {
|
||||
last_mesh_ctx: Option<MeshContext>,
|
||||
}
|
||||
|
||||
pub struct HandwritingResponse {
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
/// Context of a mesh render.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
struct MeshContext {
|
||||
@@ -171,7 +164,6 @@ impl Handwriting {
|
||||
&mut self,
|
||||
style: Option<&mut HandwritingStyle>,
|
||||
ui: &mut egui::Ui,
|
||||
response: &mut HandwritingResponse,
|
||||
) -> egui::Response {
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(style) = style {
|
||||
@@ -183,14 +175,12 @@ impl Handwriting {
|
||||
if ui.button("Clear Painting").clicked() {
|
||||
self.strokes.clear();
|
||||
self.refresh_texture = true;
|
||||
response.changed = true;
|
||||
}
|
||||
|
||||
ui.add_enabled_ui(!self.strokes.is_empty(), |ui| {
|
||||
if ui.button("Undo").clicked() {
|
||||
self.strokes.pop();
|
||||
self.refresh_texture = true;
|
||||
response.changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -200,18 +190,12 @@ impl Handwriting {
|
||||
.response
|
||||
}
|
||||
|
||||
fn commit_current_line(&mut self, response: &mut HandwritingResponse) {
|
||||
fn commit_current_line(&mut self) {
|
||||
debug_assert!(!self.current_stroke.is_empty());
|
||||
self.strokes.push(mem::take(&mut self.current_stroke));
|
||||
response.changed = true;
|
||||
}
|
||||
|
||||
pub fn ui_content(
|
||||
&mut self,
|
||||
style: &HandwritingStyle,
|
||||
ui: &mut Ui,
|
||||
hw_response: &mut HandwritingResponse,
|
||||
) -> egui::Response {
|
||||
pub fn ui_content(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> egui::Response {
|
||||
if style.animate {
|
||||
self.height = ui.ctx().animate_value_with_time(
|
||||
self.id.with("height animation"),
|
||||
@@ -222,8 +206,8 @@ impl Handwriting {
|
||||
self.height = self.desired_height;
|
||||
}
|
||||
|
||||
let desired_size = Vec2::new(ui.available_width(), self.height);
|
||||
let (response, painter) = ui.allocate_painter(desired_size, Sense::drag());
|
||||
let size = Vec2::new(ui.available_width(), self.height);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::drag());
|
||||
|
||||
let mut response = response
|
||||
//.on_hover_cursor(CursorIcon::Crosshair)
|
||||
@@ -232,24 +216,20 @@ impl Handwriting {
|
||||
|
||||
let size = response.rect.size();
|
||||
|
||||
// Calculate matrices that convert between screen-space and image-space.
|
||||
// - image-space: 0,0 is the top-left of the texture.
|
||||
// - screen-space: 0,0 is the top-left of the window.
|
||||
// Both spaces use the same logical points, not pixels.
|
||||
let to_screen =
|
||||
emath::RectTransform::from_to(Rect::from_min_size(Pos2::ZERO, size), response.rect);
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
//Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
|
||||
Rect::from_min_size(Pos2::ZERO, size),
|
||||
response.rect,
|
||||
);
|
||||
let from_screen = to_screen.inverse();
|
||||
|
||||
// Was the user in the process of drawing a stroke last frame?
|
||||
let is_drawing = response.interact_pointer_pos().is_some();
|
||||
let was_drawing = !self.current_stroke.is_empty();
|
||||
|
||||
// Is the user in the process of drawing a stroke now?
|
||||
let is_drawing = response.interact_pointer_pos().is_some();
|
||||
|
||||
if !is_drawing {
|
||||
if was_drawing {
|
||||
// commit current line
|
||||
self.commit_current_line(hw_response);
|
||||
if was_drawing {
|
||||
self.commit_current_line();
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
@@ -261,8 +241,6 @@ impl Handwriting {
|
||||
.map(|p| p.y + HANDWRITING_BOTTOM_PADDING)
|
||||
.fold(HANDWRITING_MIN_HEIGHT, |max, y| max.max(y));
|
||||
|
||||
// Change the height of the handwriting item.
|
||||
// We don't do this mid-stroke, only when the user e.g. lifts the pen.
|
||||
if self.desired_height != lines_max_y {
|
||||
self.desired_height = lines_max_y;
|
||||
response.mark_changed();
|
||||
@@ -297,7 +275,6 @@ impl Handwriting {
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// Process input events and turn them into strokes
|
||||
for event in events {
|
||||
let last_canvas_pos = self.current_stroke.last();
|
||||
|
||||
@@ -347,7 +324,7 @@ impl Handwriting {
|
||||
(PointerButton::Primary, false) => {
|
||||
if last_canvas_pos.is_some() {
|
||||
self.push_to_stroke(from_screen * pos);
|
||||
self.commit_current_line(hw_response);
|
||||
self.commit_current_line();
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
@@ -364,7 +341,7 @@ impl Handwriting {
|
||||
// in the same frame. Should handle this.
|
||||
Event::PointerGone | Event::WindowFocused(false) => {
|
||||
if !self.current_stroke.is_empty() {
|
||||
self.commit_current_line(hw_response);
|
||||
self.commit_current_line();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -384,7 +361,6 @@ impl Handwriting {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the horizontal ruled lines
|
||||
(1..)
|
||||
.map(|n| n as f32 * HANDWRITING_LINE_SPACING)
|
||||
.take_while(|&y| y < size.y)
|
||||
@@ -397,12 +373,9 @@ impl Handwriting {
|
||||
painter.add(shape);
|
||||
});
|
||||
|
||||
// Get the dimensions of the image
|
||||
let mesh_rect = response
|
||||
.rect
|
||||
.with_max_y(response.rect.min.y + self.desired_height);
|
||||
|
||||
// These are the values that, if changed, would require the mesh to be re-rendered.
|
||||
let new_context = MeshContext {
|
||||
ui_theme: ui.ctx().theme(),
|
||||
pixels_per_point: ui.pixels_per_point(),
|
||||
@@ -410,25 +383,24 @@ impl Handwriting {
|
||||
stroke: style.stroke,
|
||||
};
|
||||
|
||||
// Figure out if we need to re-rasterize the mesh.
|
||||
if Some(&new_context) != self.last_mesh_ctx.as_ref() {
|
||||
self.refresh_texture = true;
|
||||
}
|
||||
|
||||
if self.refresh_texture {
|
||||
// ...if we do, rasterize the entire texture from scratch
|
||||
// rasterize the entire texture from scratch
|
||||
self.refresh_texture(style, new_context, ui);
|
||||
self.unblitted_lines.clear();
|
||||
} else if !self.unblitted_lines.is_empty() {
|
||||
// ...if we don't, we can get away with only rasterizing the *new* lines onto the
|
||||
// existing texture.
|
||||
// only rasterize the new lines onto the existing texture
|
||||
for [from, to] in std::mem::take(&mut self.unblitted_lines) {
|
||||
self.draw_line_to_texture(from, to, &new_context, ui);
|
||||
}
|
||||
self.unblitted_lines.clear();
|
||||
}
|
||||
|
||||
// Draw the texture
|
||||
//painter.add(self.mesh.clone());
|
||||
|
||||
if let Some(texture) = &self.texture {
|
||||
let texture = SizedTexture::new(texture.id(), texture.size_vec2());
|
||||
let shape = RectShape {
|
||||
@@ -488,13 +460,6 @@ impl Handwriting {
|
||||
tesselator.tessellate_shape(shape, mesh);
|
||||
});
|
||||
|
||||
// sanity-check that tesselation did not produce any NaNs.
|
||||
// this can happen if the line contains duplicated consecutive positions
|
||||
//for vertex in &mesh.vertices {
|
||||
// debug_assert!(vertex.pos.x.is_finite(), "{} must be finite", vertex.pos.x);
|
||||
// debug_assert!(vertex.pos.y.is_finite(), "{} must be finite", vertex.pos.y);
|
||||
//}
|
||||
|
||||
let texture = texture!(self, ui, &mesh_context);
|
||||
let triangles = mesh_triangles(&self.mesh);
|
||||
|
||||
@@ -510,11 +475,9 @@ impl Handwriting {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> HandwritingResponse {
|
||||
let mut response = HandwritingResponse { changed: false };
|
||||
|
||||
pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
self.ui_control(None, ui, &mut response);
|
||||
self.ui_control(None, ui);
|
||||
|
||||
//ui.label("Paint with your mouse/touch!");
|
||||
Frame::canvas(ui.style())
|
||||
@@ -522,11 +485,9 @@ impl Handwriting {
|
||||
.stroke(Stroke::new(5.0, Color32::from_black_alpha(40)))
|
||||
.fill(style.bg_color)
|
||||
.show(ui, |ui| {
|
||||
self.ui_content(style, ui, &mut response);
|
||||
self.ui_content(style, ui);
|
||||
});
|
||||
});
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn push_to_stroke(&mut self, new_canvas_pos: Pos2) {
|
||||
@@ -596,40 +557,24 @@ impl Handwriting {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_as_disk_format(&self) -> Box<[u8]> {
|
||||
let mut bytes = vec![];
|
||||
let header = disk_format::Header {
|
||||
version: disk_format::V1,
|
||||
};
|
||||
|
||||
bytes.extend_from_slice(header.as_bytes());
|
||||
|
||||
for stroke in &self.strokes {
|
||||
let Ok(len) = u16::try_from(stroke.len()) else {
|
||||
log::error!("More than u16::MAX points in a stroke!");
|
||||
continue;
|
||||
};
|
||||
|
||||
let header = RawStrokeHeader { len: len.into() };
|
||||
bytes.extend_from_slice(header.as_bytes());
|
||||
|
||||
for position in stroke {
|
||||
for v in [position.x, position.y] {
|
||||
let v = f16::from_f32(v);
|
||||
let v = f16_le::from(v);
|
||||
bytes.extend_from_slice(v.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes.into_boxed_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Handwriting {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let raw = self.encode_as_disk_format();
|
||||
let mut raw = vec![];
|
||||
|
||||
for stroke in &self.strokes {
|
||||
raw.push((stroke.len() as u16).to_le_bytes());
|
||||
for position in stroke {
|
||||
let x = half::f16::from_f32(position.x);
|
||||
let y = half::f16::from_f32(position.y);
|
||||
raw.push(x.to_bits().to_le_bytes());
|
||||
raw.push(y.to_bits().to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
let raw = raw.as_slice().as_bytes();
|
||||
|
||||
write_custom_code_block(f, CODE_BLOCK_KEY, BASE64_STANDARD.encode(raw))
|
||||
}
|
||||
}
|
||||
@@ -645,71 +590,53 @@ impl FromStr for Handwriting {
|
||||
.decode(s)
|
||||
.wrap_err("Failed to decode painting data from base64")?;
|
||||
|
||||
// HACK: first iteration of disk format did not have version header
|
||||
//bytes.insert(0, 0);
|
||||
//bytes.insert(0, 1);
|
||||
#[allow(non_camel_case_types)]
|
||||
type u16_le = [u8; 2];
|
||||
|
||||
let disk_format = DiskFormat::ref_from_bytes(&bytes[..]).map_err(|_| eyre!("Too short"))?;
|
||||
#[allow(non_camel_case_types)]
|
||||
type f16_le = [u8; 2];
|
||||
|
||||
if disk_format.header.version != disk_format::V1 {
|
||||
bail!(
|
||||
"Unknown disk_format version: {}",
|
||||
disk_format.header.version
|
||||
);
|
||||
#[derive(FromBytes, KnownLayout, Immutable)]
|
||||
#[repr(C, packed)]
|
||||
struct Stroke {
|
||||
pub len: u16_le,
|
||||
pub positions: [f16_le],
|
||||
}
|
||||
|
||||
let mut raw_strokes = &disk_format.strokes[..];
|
||||
let mut bytes = &bytes[..];
|
||||
let mut strokes = vec![];
|
||||
|
||||
while !raw_strokes.is_empty() {
|
||||
if raw_strokes.len() < RawStroke::MIN_LEN {
|
||||
bail!("Invalid remaining length: {}", raw_strokes.len());
|
||||
while !bytes.is_empty() {
|
||||
let header_len = size_of::<u16_le>();
|
||||
if bytes.len() < header_len {
|
||||
bail!("Invalid remaining length: {}", bytes.len());
|
||||
}
|
||||
|
||||
let stroke = RawStroke::ref_from_bytes(&raw_strokes[..RawStroke::MIN_LEN])
|
||||
.expect("length is correct");
|
||||
let stroke = Stroke::ref_from_bytes(&bytes[..header_len]).expect("length is correct");
|
||||
let len = usize::from(u16::from_le_bytes(stroke.len));
|
||||
let len = len * size_of::<f16_le>() * 2;
|
||||
|
||||
// get length as number of points
|
||||
let len = usize::from(u16::from(stroke.header.len));
|
||||
|
||||
// convert to length in bytes
|
||||
let byte_len = 2 * size_of::<f16_le>() * len;
|
||||
|
||||
if raw_strokes.len() < byte_len {
|
||||
bail!("Invalid remaining length: {}", raw_strokes.len());
|
||||
if bytes.len() < len {
|
||||
bail!("Invalid remaining length: {}", bytes.len());
|
||||
}
|
||||
|
||||
let (stroke, rest) = raw_strokes.split_at(RawStroke::MIN_LEN + byte_len);
|
||||
raw_strokes = rest;
|
||||
let (stroke, rest) = bytes.split_at(header_len + len);
|
||||
bytes = rest;
|
||||
let stroke = Stroke::ref_from_bytes(stroke)
|
||||
.map_err(|e| eyre!("Failed to decode stroke bytes: {e}"))?;
|
||||
|
||||
let stroke = RawStroke::ref_from_bytes(stroke).expect("length is correct");
|
||||
|
||||
debug_assert_eq!(
|
||||
stroke.positions.len().rem_euclid(2),
|
||||
0,
|
||||
"{} must be divisible by 2",
|
||||
stroke.positions.len()
|
||||
);
|
||||
debug_assert_eq!(stroke.positions.len(), len * 2);
|
||||
|
||||
let mut last_pos = Pos2::new(f32::NEG_INFINITY, f32::INFINITY);
|
||||
|
||||
// positions are encoded as an array of f16s [x, y, x, y, x, y, ..]
|
||||
let stroke: Vec<Pos2> = stroke
|
||||
let mut positions = stroke
|
||||
.positions
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| [chunk[0], chunk[1]])
|
||||
.map(|pos| pos.map(f16::from)) // interpret bytes as f16
|
||||
.map(|pos| pos.map(f32::from)) // widen to f32
|
||||
.filter(|pos| pos.iter().all(|f| f.is_finite())) // filter out NaNs and Infs
|
||||
.map(|[x, y]| Pos2::new(x, y))
|
||||
.filter(|pos| {
|
||||
let is_duplicate = pos == &last_pos;
|
||||
last_pos = *pos;
|
||||
!is_duplicate // skip duplicates
|
||||
})
|
||||
.collect();
|
||||
.iter()
|
||||
.map(|&position| f16::from_bits(u16::from_le_bytes(position)));
|
||||
|
||||
let mut stroke = vec![];
|
||||
while let Some(x) = positions.next() {
|
||||
let Some(y) = positions.next() else {
|
||||
unreachable!("len is a multiple of two");
|
||||
};
|
||||
stroke.push(Pos2::new(x.into(), y.into()));
|
||||
}
|
||||
strokes.push(stroke);
|
||||
}
|
||||
|
||||
+8
-10
@@ -72,11 +72,15 @@ pub fn rasterize_onto<'a, Blend: BlendFn>(
|
||||
|
||||
// If the pixel is within the triangle, fill it in.
|
||||
if point_in_triangle.inside {
|
||||
let [c0, c1, c2] = [0, 1, 2].map(|i| {
|
||||
triangle[i]
|
||||
let c0 = triangle[0]
|
||||
.color
|
||||
.linear_multiply(point_in_triangle.weights[i])
|
||||
});
|
||||
.linear_multiply(point_in_triangle.weights[0]);
|
||||
let c1 = triangle[1]
|
||||
.color
|
||||
.linear_multiply(point_in_triangle.weights[1]);
|
||||
let c2 = triangle[2]
|
||||
.color
|
||||
.linear_multiply(point_in_triangle.weights[2]);
|
||||
|
||||
let color = c0 + c1 + c2;
|
||||
|
||||
@@ -165,12 +169,6 @@ fn point_in_triangle(point: Pos2, triangle: [&Vertex; 3]) -> PointInTriangle {
|
||||
// Normalize the weights.
|
||||
let weights = areas.map(|area| area / triangle_area);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
if weights.into_iter().any(f32::is_nan) {
|
||||
panic!("weights must not be NaN! {weights:?} {triangle_area:?} {areas:?} {sides:?}");
|
||||
}
|
||||
}
|
||||
|
||||
PointInTriangle { inside, weights }
|
||||
}
|
||||
|
||||
|
||||
+1
-9
@@ -24,10 +24,6 @@ pub struct MdTextEdit {
|
||||
cursor: Option<CCursorRange>,
|
||||
}
|
||||
|
||||
pub struct MdTextEditOutput {
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
impl MdTextEdit {
|
||||
pub fn new() -> Self {
|
||||
MdTextEdit::default()
|
||||
@@ -40,7 +36,7 @@ impl MdTextEdit {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) -> MdTextEditOutput {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
text,
|
||||
highlighter,
|
||||
@@ -76,10 +72,6 @@ impl MdTextEdit {
|
||||
*cursor = text_edit.cursor_range;
|
||||
//ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
MdTextEditOutput {
|
||||
changed: text_edit.response.changed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user