Compare commits
3 Commits
all-fonts
...
7d234641cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
7d234641cb
|
|||
| 1ed278cc55 | |||
|
2a830f0539
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1344,7 +1344,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inkr"
|
name = "inkr"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"eframe",
|
"eframe",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "inkr"
|
name = "inkr"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
authors = []
|
authors = []
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
|||||||
27
PKGBUILD
Normal file
27
PKGBUILD
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
9
assets/inkr.desktop
Executable file
9
assets/inkr.desktop
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
[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.
163
src/app.rs
163
src/app.rs
@ -2,11 +2,14 @@ use std::{
|
|||||||
fs,
|
fs,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, mpsc},
|
sync::{Arc, mpsc},
|
||||||
|
thread::JoinHandle,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender};
|
use crate::{file_editor::FileEditor, preferences::Preferences, util::GuiSender};
|
||||||
use egui::{
|
use egui::{
|
||||||
Align, Button, Color32, FontData, FontDefinitions, PointerButton, RichText, ScrollArea, Stroke,
|
Align, Button, Color32, Context, FontData, FontDefinitions, Key, Modifiers, PointerButton,
|
||||||
|
RichText, ScrollArea, Stroke,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(serde::Deserialize, serde::Serialize)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
@ -18,6 +21,8 @@ pub struct App {
|
|||||||
actions_tx: mpsc::Sender<Action>,
|
actions_tx: mpsc::Sender<Action>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
actions_rx: mpsc::Receiver<Action>,
|
actions_rx: mpsc::Receiver<Action>,
|
||||||
|
#[serde(skip)]
|
||||||
|
jobs: Jobs,
|
||||||
|
|
||||||
tabs: Vec<(TabId, Tab)>,
|
tabs: Vec<(TabId, Tab)>,
|
||||||
open_tab_index: Option<usize>,
|
open_tab_index: Option<usize>,
|
||||||
@ -25,6 +30,33 @@ pub struct App {
|
|||||||
next_tab_id: TabId,
|
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)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
enum Tab {
|
enum Tab {
|
||||||
File(FileEditor),
|
File(FileEditor),
|
||||||
@ -36,6 +68,12 @@ impl Tab {
|
|||||||
Tab::File(file_editor) => file_editor.title(),
|
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;
|
pub type TabId = usize;
|
||||||
@ -55,8 +93,12 @@ impl Default for App {
|
|||||||
let (actions_tx, actions_rx) = mpsc::channel();
|
let (actions_tx, actions_rx) = mpsc::channel();
|
||||||
Self {
|
Self {
|
||||||
preferences: Preferences::default(),
|
preferences: Preferences::default(),
|
||||||
actions_tx,
|
actions_tx: actions_tx.clone(/* this is silly, i know */),
|
||||||
actions_rx,
|
actions_rx,
|
||||||
|
jobs: Jobs {
|
||||||
|
handles: Default::default(),
|
||||||
|
actions_tx,
|
||||||
|
},
|
||||||
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
tabs: vec![(1, Tab::File(FileEditor::new("note.md")))],
|
||||||
open_tab_index: None,
|
open_tab_index: None,
|
||||||
next_tab_id: 2,
|
next_tab_id: 2,
|
||||||
@ -146,7 +188,7 @@ impl App {
|
|||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn actions_tx(&self, ctx: &egui::Context) -> GuiSender<Action> {
|
fn actions_tx(&self, ctx: &Context) -> GuiSender<Action> {
|
||||||
GuiSender::new(self.actions_tx.clone(), ctx)
|
GuiSender::new(self.actions_tx.clone(), ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,9 +217,11 @@ impl eframe::App for App {
|
|||||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||||
self.preferences.apply(ctx);
|
self.preferences.apply(ctx);
|
||||||
|
|
||||||
|
self.jobs.handles.retain(|job| !job.is_finished());
|
||||||
|
|
||||||
while let Ok(action) = self.actions_rx.try_recv() {
|
while let Ok(action) = self.actions_rx.try_recv() {
|
||||||
self.handle_action(action);
|
self.handle_action(action);
|
||||||
}
|
}
|
||||||
@ -186,11 +230,11 @@ impl eframe::App for App {
|
|||||||
self.open_tab_index = Some(self.tabs.len().saturating_sub(1));
|
self.open_tab_index = Some(self.tabs.len().saturating_sub(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
//ctx.input_mut(|input| {
|
ctx.input_mut(|input| {
|
||||||
// if input.consume_key(Modifiers::CTRL, Key::H) {
|
if input.consume_key(Modifiers::CTRL, Key::S) {
|
||||||
// self.buffer.push(BufferItem::Painting(Default::default()));
|
self.save_active_tab(ctx);
|
||||||
// }
|
}
|
||||||
//});
|
});
|
||||||
|
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
egui::containers::menu::Bar::new().ui(ui, |ui| {
|
egui::containers::menu::Bar::new().ui(ui, |ui| {
|
||||||
@ -205,22 +249,15 @@ impl eframe::App for App {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if ui.button("Open File").clicked() {
|
if ui.button("Open File").clicked() {
|
||||||
let actions_tx = self.actions_tx(ui.ctx());
|
self.jobs.start(ui.ctx(), move || {
|
||||||
std::thread::spawn(move || {
|
let file_path = rfd::FileDialog::new().pick_file()?;
|
||||||
let file = rfd::FileDialog::new().pick_file();
|
|
||||||
|
|
||||||
let Some(file_path) = file else { return };
|
let text = fs::read_to_string(&file_path)
|
||||||
|
.inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}"))
|
||||||
let text = match fs::read_to_string(&file_path) {
|
.ok()?;
|
||||||
Ok(text) => text,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to read {file_path:?}: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let editor = FileEditor::from_file(file_path, &text);
|
let editor = FileEditor::from_file(file_path, &text);
|
||||||
let _ = actions_tx.send(Action::OpenFile(editor));
|
Some(Action::OpenFile(editor))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +274,19 @@ 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 =
|
let open_file =
|
||||||
self.open_tab_index
|
self.open_tab_index
|
||||||
.and_then(|i| self.tabs.get(i))
|
.and_then(|i| self.tabs.get(i))
|
||||||
@ -244,45 +294,22 @@ impl eframe::App for App {
|
|||||||
Tab::File(file_editor) => Some((*id, file_editor)),
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(open_file.is_some(), Button::new("Save As"))
|
.add_enabled(open_file.is_some(), Button::new("Save As"))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
let actions_tx = self.actions_tx(ui.ctx());
|
|
||||||
let (tab_id, editor) =
|
let (tab_id, editor) =
|
||||||
open_file.expect("We checked that open_file is_some");
|
open_file.expect("We checked that open_file is_some");
|
||||||
let text = editor.to_string();
|
let text = editor.to_string();
|
||||||
std::thread::spawn(move || {
|
self.jobs.start(ui.ctx(), move || {
|
||||||
let Some(file_path) = rfd::FileDialog::new().save_file() else {
|
let file_path = rfd::FileDialog::new().save_file()?;
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = fs::write(&file_path, text.as_bytes()) {
|
fs::write(&file_path, text.as_bytes())
|
||||||
log::error!("{e}");
|
.inspect_err(|e| log::error!("{e}"))
|
||||||
return;
|
.ok()?;
|
||||||
};
|
|
||||||
|
|
||||||
let _ = actions_tx.send(Action::MoveFile(tab_id, file_path));
|
Some(Action::MoveFile(tab_id, file_path))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +324,9 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(16.0);
|
if !self.jobs.handles.is_empty() {
|
||||||
|
ui.spinner();
|
||||||
|
}
|
||||||
|
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
|
||||||
@ -306,8 +335,7 @@ impl eframe::App for App {
|
|||||||
let selected = self.open_tab_index == Some(i);
|
let selected = self.open_tab_index == Some(i);
|
||||||
let mut button = Button::new(tab.title()).selected(selected);
|
let mut button = Button::new(tab.title()).selected(selected);
|
||||||
|
|
||||||
let dirty = i == 0; // TODO: mark as dirty when contents hasn't been saved
|
if tab.is_dirty() {
|
||||||
if dirty {
|
|
||||||
button = button.right_text(RichText::new("*").strong())
|
button = button.right_text(RichText::new("*").strong())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,4 +383,31 @@ impl App {
|
|||||||
self.next_tab_id += 1;
|
self.next_tab_id += 1;
|
||||||
self.tabs.insert(i, (id, tab));
|
self.tabs.insert(i, (id, tab));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ pub struct FileEditor {
|
|||||||
title: String,
|
title: String,
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
pub buffer: Vec<BufferItem>,
|
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)]
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
@ -37,6 +40,7 @@ impl FileEditor {
|
|||||||
title: title.into(),
|
title: title.into(),
|
||||||
path: None,
|
path: None,
|
||||||
buffer,
|
buffer,
|
||||||
|
is_dirty: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +73,11 @@ impl FileEditor {
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("new");
|
ui.label("new");
|
||||||
if ui.button("text").clicked() {
|
if ui.button("text").clicked() {
|
||||||
|
self.is_dirty = true;
|
||||||
self.buffer.push(BufferItem::Text(Default::default()));
|
self.buffer.push(BufferItem::Text(Default::default()));
|
||||||
}
|
}
|
||||||
if ui.button("writing").clicked() {
|
if ui.button("writing").clicked() {
|
||||||
|
self.is_dirty = true;
|
||||||
self.buffer
|
self.buffer
|
||||||
.push(BufferItem::Handwriting(Default::default()));
|
.push(BufferItem::Handwriting(Default::default()));
|
||||||
}
|
}
|
||||||
@ -138,14 +144,18 @@ impl FileEditor {
|
|||||||
|
|
||||||
let item_response = ui.allocate_ui(item_size, |ui| match item {
|
let item_response = ui.allocate_ui(item_size, |ui| match item {
|
||||||
BufferItem::Text(text_edit) => {
|
BufferItem::Text(text_edit) => {
|
||||||
text_edit.ui(ui);
|
if text_edit.ui(ui).changed {
|
||||||
|
self.is_dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BufferItem::Handwriting(painting) => {
|
BufferItem::Handwriting(handwriting) => {
|
||||||
let style = HandwritingStyle {
|
let style = HandwritingStyle {
|
||||||
animate: preferences.animations,
|
animate: preferences.animations,
|
||||||
..HandwritingStyle::from_theme(ui.ctx().theme())
|
..HandwritingStyle::from_theme(ui.ctx().theme())
|
||||||
};
|
};
|
||||||
painting.ui(&style, ui);
|
if handwriting.ui(&style, ui).changed {
|
||||||
|
self.is_dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -211,10 +221,12 @@ impl FileEditor {
|
|||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
let item = self.buffer.remove(from);
|
let item = self.buffer.remove(from);
|
||||||
self.buffer.insert(to, item);
|
self.buffer.insert(to, item);
|
||||||
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
Ordering::Less => {
|
Ordering::Less => {
|
||||||
let item = self.buffer.remove(from);
|
let item = self.buffer.remove(from);
|
||||||
self.buffer.insert(to - 1, item);
|
self.buffer.insert(to - 1, item);
|
||||||
|
self.is_dirty = true;
|
||||||
}
|
}
|
||||||
Ordering::Equal => {}
|
Ordering::Equal => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,10 @@ pub struct Handwriting {
|
|||||||
last_mesh_ctx: Option<MeshContext>,
|
last_mesh_ctx: Option<MeshContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct HandwritingResponse {
|
||||||
|
pub changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Context of a mesh render.
|
/// Context of a mesh render.
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
struct MeshContext {
|
struct MeshContext {
|
||||||
@ -164,6 +168,7 @@ impl Handwriting {
|
|||||||
&mut self,
|
&mut self,
|
||||||
style: Option<&mut HandwritingStyle>,
|
style: Option<&mut HandwritingStyle>,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
|
response: &mut HandwritingResponse,
|
||||||
) -> egui::Response {
|
) -> egui::Response {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if let Some(style) = style {
|
if let Some(style) = style {
|
||||||
@ -175,12 +180,14 @@ impl Handwriting {
|
|||||||
if ui.button("Clear Painting").clicked() {
|
if ui.button("Clear Painting").clicked() {
|
||||||
self.strokes.clear();
|
self.strokes.clear();
|
||||||
self.refresh_texture = true;
|
self.refresh_texture = true;
|
||||||
|
response.changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.add_enabled_ui(!self.strokes.is_empty(), |ui| {
|
ui.add_enabled_ui(!self.strokes.is_empty(), |ui| {
|
||||||
if ui.button("Undo").clicked() {
|
if ui.button("Undo").clicked() {
|
||||||
self.strokes.pop();
|
self.strokes.pop();
|
||||||
self.refresh_texture = true;
|
self.refresh_texture = true;
|
||||||
|
response.changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -190,12 +197,18 @@ impl Handwriting {
|
|||||||
.response
|
.response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit_current_line(&mut self) {
|
fn commit_current_line(&mut self, response: &mut HandwritingResponse) {
|
||||||
debug_assert!(!self.current_stroke.is_empty());
|
debug_assert!(!self.current_stroke.is_empty());
|
||||||
self.strokes.push(mem::take(&mut self.current_stroke));
|
self.strokes.push(mem::take(&mut self.current_stroke));
|
||||||
|
response.changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui_content(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> egui::Response {
|
pub fn ui_content(
|
||||||
|
&mut self,
|
||||||
|
style: &HandwritingStyle,
|
||||||
|
ui: &mut Ui,
|
||||||
|
hw_response: &mut HandwritingResponse,
|
||||||
|
) -> egui::Response {
|
||||||
if style.animate {
|
if style.animate {
|
||||||
self.height = ui.ctx().animate_value_with_time(
|
self.height = ui.ctx().animate_value_with_time(
|
||||||
self.id.with("height animation"),
|
self.id.with("height animation"),
|
||||||
@ -216,11 +229,8 @@ impl Handwriting {
|
|||||||
|
|
||||||
let size = response.rect.size();
|
let size = response.rect.size();
|
||||||
|
|
||||||
let to_screen = emath::RectTransform::from_to(
|
let to_screen =
|
||||||
//Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
|
emath::RectTransform::from_to(Rect::from_min_size(Pos2::ZERO, size), response.rect);
|
||||||
Rect::from_min_size(Pos2::ZERO, size),
|
|
||||||
response.rect,
|
|
||||||
);
|
|
||||||
let from_screen = to_screen.inverse();
|
let from_screen = to_screen.inverse();
|
||||||
|
|
||||||
let is_drawing = response.interact_pointer_pos().is_some();
|
let is_drawing = response.interact_pointer_pos().is_some();
|
||||||
@ -229,7 +239,7 @@ impl Handwriting {
|
|||||||
if !is_drawing {
|
if !is_drawing {
|
||||||
// commit current line
|
// commit current line
|
||||||
if was_drawing {
|
if was_drawing {
|
||||||
self.commit_current_line();
|
self.commit_current_line(hw_response);
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +334,7 @@ impl Handwriting {
|
|||||||
(PointerButton::Primary, false) => {
|
(PointerButton::Primary, false) => {
|
||||||
if last_canvas_pos.is_some() {
|
if last_canvas_pos.is_some() {
|
||||||
self.push_to_stroke(from_screen * pos);
|
self.push_to_stroke(from_screen * pos);
|
||||||
self.commit_current_line();
|
self.commit_current_line(hw_response);
|
||||||
response.mark_changed();
|
response.mark_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +351,7 @@ impl Handwriting {
|
|||||||
// in the same frame. Should handle this.
|
// in the same frame. Should handle this.
|
||||||
Event::PointerGone | Event::WindowFocused(false) => {
|
Event::PointerGone | Event::WindowFocused(false) => {
|
||||||
if !self.current_stroke.is_empty() {
|
if !self.current_stroke.is_empty() {
|
||||||
self.commit_current_line();
|
self.commit_current_line(hw_response);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -475,9 +485,11 @@ impl Handwriting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) {
|
pub fn ui(&mut self, style: &HandwritingStyle, ui: &mut Ui) -> HandwritingResponse {
|
||||||
|
let mut response = HandwritingResponse { changed: false };
|
||||||
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
ui.vertical_centered_justified(|ui| {
|
||||||
self.ui_control(None, ui);
|
self.ui_control(None, ui, &mut response);
|
||||||
|
|
||||||
//ui.label("Paint with your mouse/touch!");
|
//ui.label("Paint with your mouse/touch!");
|
||||||
Frame::canvas(ui.style())
|
Frame::canvas(ui.style())
|
||||||
@ -485,9 +497,11 @@ impl Handwriting {
|
|||||||
.stroke(Stroke::new(5.0, Color32::from_black_alpha(40)))
|
.stroke(Stroke::new(5.0, Color32::from_black_alpha(40)))
|
||||||
.fill(style.bg_color)
|
.fill(style.bg_color)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
self.ui_content(style, ui);
|
self.ui_content(style, ui, &mut response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_to_stroke(&mut self, new_canvas_pos: Pos2) {
|
fn push_to_stroke(&mut self, new_canvas_pos: Pos2) {
|
||||||
|
|||||||
@ -24,6 +24,10 @@ pub struct MdTextEdit {
|
|||||||
cursor: Option<CCursorRange>,
|
cursor: Option<CCursorRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct MdTextEditOutput {
|
||||||
|
pub changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl MdTextEdit {
|
impl MdTextEdit {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
MdTextEdit::default()
|
MdTextEdit::default()
|
||||||
@ -36,7 +40,7 @@ impl MdTextEdit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut Ui) {
|
pub fn ui(&mut self, ui: &mut Ui) -> MdTextEditOutput {
|
||||||
let Self {
|
let Self {
|
||||||
text,
|
text,
|
||||||
highlighter,
|
highlighter,
|
||||||
@ -72,6 +76,10 @@ impl MdTextEdit {
|
|||||||
*cursor = text_edit.cursor_range;
|
*cursor = text_edit.cursor_range;
|
||||||
//ui.ctx().request_repaint();
|
//ui.ctx().request_repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MdTextEditOutput {
|
||||||
|
changed: text_edit.response.changed(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user