very badly coded gtk4 ui.

This commit is contained in:
elliot speck 2024-08-20 20:07:34 +10:00
parent 0bfe9f1ef2
commit 024b259bbe
Signed by: arcayr
SSH key fingerprint: SHA256:ACNNWlqwQA5pfEvX1dnTlr8r4fdg1taXA0lae2FSjto
12 changed files with 1570 additions and 10 deletions

973
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
[package]
name = "noteriety-gtk4"
version = "0.1.0"
edition = "2021"
[dependencies]
relm4 = { version = "0.9.0", features = ["adw", "libadwaita"] }
libadwaita = { version = "0.7.0", features = ["gtk_v4_6", "v1_5"] }
markdown-ast = "0.1.1"
crop = "0.4.2"
noteriety = { version = "0.1.0", path = "../noteriety" }
cap-std = "3.2.0"

View file

@ -0,0 +1,11 @@
pub(crate) struct Configuration {
pub notes_dir: String,
}
impl Default for Configuration {
fn default() -> Self {
Self {
notes_dir: String::from("/home/arcayr/.local/share/noteriety/notebooks"),
}
}
}

View file

@ -0,0 +1,11 @@
use configuration::Configuration;
use relm4::prelude::*;
mod window;
mod configuration;
fn main() {
let config = Configuration::default();
let app = RelmApp::new("expert.mischief.noteriety");
app.run::<window::Window>(config);
}

View file

@ -0,0 +1,30 @@
pub struct NoteTitle(String);
impl Default for NoteTitle {
fn default() -> Self {
Self(String::from("Untitled Note"))
}
}
impl NoteTitle {
pub fn as_filename(&self) -> String {
filenamify::filenamify(&self.0)
}
}
pub struct NoteBody(crop::Rope);
pub struct Note {
title: NoteTitle,
body: NoteBody,
}
pub struct Notebook {
basedir: String,
notes: Vec<Note>,
}
pub struct Outline {
basedir: String,
body: NoteBody,
}

View file

@ -0,0 +1,86 @@
use gtk::{prelude::*, TextBuffer};
use markdown_ast::markdown_to_ast;
use relm4::prelude::*;
pub(crate) struct NoteContentEntry {
pub(crate) content: gtk::TextBuffer,
pub(crate) rope: crop::Rope,
}
#[derive(Debug)]
pub(crate) enum NoteContentEntryInput {
SetContent(String),
Clear,
}
#[derive(Debug)]
pub(crate) enum NoteContentEntryOutput {
Content(String),
}
#[relm4::component(pub(crate))]
impl SimpleComponent for NoteContentEntry {
type Init = ();
type Input = NoteContentEntryInput;
type Output = NoteContentEntryOutput;
view! {
#[root]
gtk::ScrolledWindow {
set_margin_horizontal: 10,
set_margin_vertical: 10,
// libadwaita doesn't provide styling for gtk::TextView.
// reason unknown.
// https://gitlab.gnome.org/GNOME/libadwaita/-/issues/353
gtk::TextView {
inline_css: "padding: 10px; border-radius: 6px",
set_hexpand: true,
set_vexpand: true,
set_buffer: Some(&model.content),
}
}
}
fn init(
_init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {
content: TextBuffer::default(),
rope: crop::Rope::new(),
};
model.content.connect_text_notify(move |tb| {
let (min, max) = tb.bounds();
// let ast = markdown_to_ast(.as_str());
sender.output(NoteContentEntryOutput::Content(tb.text(&min, &max, true).to_string()));
});
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
Self::Input::Clear => {
self.content = TextBuffer::default();
},
Self::Input::SetContent(body) => {
self.content = TextBuffer::builder().text(body.to_string()).build();
}
}
}
}
impl Default for NoteContentEntry {
fn default() -> Self {
Self {
content: TextBuffer::default(),
rope: crop::Rope::new(),
}
}
}

View file

@ -0,0 +1,54 @@
use gtk::prelude::{
ButtonExt, GtkWindowExt, ToggleButtonExt, WidgetExt,
};
use relm4::*;
pub(crate) struct HeaderBar;
#[derive(Debug)]
pub(crate) enum Output {
OutlineMode,
NotesMode,
}
#[relm4::component(pub(crate))]
impl SimpleComponent for HeaderBar {
type Init = ();
type Input = ();
type Output = Output;
view! {
#[root]
&gtk::Box {
add_css_class: "linked",
#[name = "group"]
gtk::ToggleButton {
set_label: "Outline",
set_active: false,
connect_toggled[sender] => move |btn| {
if btn.is_active() {
sender.output(Self::Output::OutlineMode).unwrap()
}
},
},
gtk::ToggleButton {
set_label: "Notes",
set_active: true,
set_group: Some(&group),
connect_toggled[sender] => move |btn| {
if btn.is_active() {
sender.output(Self::Output::NotesMode).unwrap()
}
},
},
}
}
fn init(_params: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
let model = HeaderBar;
let widgets = view_output!();
ComponentParts { model, widgets }
}
}

View file

@ -0,0 +1,190 @@
use core::panic;
use std::{cell::RefCell, rc::Rc};
use libadwaita::prelude::{ButtonExt, NavigationPageExt, WidgetExt};
use libadwaita::{
self as adw,
prelude::{GtkWindowExt, OrientableExt},
};
use noteriety::{self, note::Note, notebook::Notebook};
use relm4::factory::Position;
use relm4::gtk;
use relm4::{
Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmWidgetExt,
SimpleComponent,
};
use crate::configuration::Configuration;
mod content_entry;
mod header_bar;
mod sidebar;
mod title_entry;
pub(crate) struct Window {
header_bar: Controller<header_bar::HeaderBar>,
sidebar: Controller<sidebar::Sidebar>,
title_entry: Controller<title_entry::NoteTitleEntry>,
content_entry: Controller<content_entry::NoteContentEntry>,
config: Configuration,
root_notebook: Notebook,
active_note: Note,
}
#[derive(Debug)]
pub(crate) enum WindowInput {
SetActiveNote(Note),
UpdateActiveNoteTitle(String),
UpdateActiveNoteContent(String),
CreateNote(Note),
CreateNotebook(Notebook),
CreateNewNote,
SaveActiveNote,
}
#[derive(Debug)]
enum WindowOutput {}
#[relm4::component(pub(crate))]
impl SimpleComponent for Window {
type Init = Configuration;
type Input = WindowInput;
type Output = ();
view! {
main_window = adw::ApplicationWindow {
set_default_width: 640,
set_default_height: 480,
adw::NavigationSplitView {
#[wrap(Some)]
set_sidebar = &adw::NavigationPage {
#[wrap(Some)]
set_child = &adw::ToolbarView {
#[wrap(Some)]
set_content = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 10,
model.header_bar.widget(),
model.sidebar.widget(),
}
},
},
#[wrap(Some)]
set_content = &adw::NavigationPage {
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
adw::HeaderBar {
#[wrap(Some)]
set_title_widget = &adw::WindowTitle {
#[watch]
set_title: &model.active_note.title.to_string()
},
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
model.title_entry.widget(),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
gtk::Button {
set_label: "new note",
set_margin_bottom: 6,
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
connect_clicked => WindowInput::CreateNewNote
},
gtk::Button {
set_label: "save note",
set_halign: gtk::Align::Center,
set_valign: gtk::Align::Center,
connect_clicked => WindowInput::SaveActiveNote
},
},
},
model.content_entry.widget(),
}
}
}
}
}
fn init(
config: Self::Init,
window: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let root_notebook = Notebook::read(&config.notes_dir).unwrap();
let title_entry =
title_entry::NoteTitleEntry::builder()
.launch(())
.forward(sender.input_sender(), |m| match m {
title_entry::NoteTitleEntryOutput::Content(v) => {
WindowInput::UpdateActiveNoteTitle(v)
}
});
let content_entry = content_entry::NoteContentEntry::builder()
.launch(())
.forward(sender.input_sender(), |m| match m {
content_entry::NoteContentEntryOutput::Content(content) => {
WindowInput::UpdateActiveNoteContent(content)
}
});
let sidebar = sidebar::Sidebar::builder().launch(()).detach();
let header_bar = header_bar::HeaderBar::builder().launch(()).detach();
let model = Self {
title_entry,
header_bar,
sidebar,
content_entry,
config,
root_notebook: root_notebook.clone(),
active_note: Note::new("untitled note"),
};
model.sidebar.emit(sidebar::Input::UpdateRoot(root_notebook.clone()));
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, input: Self::Input, sender: ComponentSender<Self>) {
match input {
WindowInput::UpdateActiveNoteTitle(title) => {
self.active_note.title = title;
},
WindowInput::UpdateActiveNoteContent(content) => {
self.active_note.content = content;
},
WindowInput::SetActiveNote(note) => {
self.active_note = note;
},
WindowInput::SaveActiveNote => {
self.active_note
.write_in(&self.root_notebook.root_path)
.unwrap();
self.root_notebook = Notebook::read(&self.root_notebook.root_path).unwrap();
self.sidebar.emit(sidebar::Input::UpdateRoot(self.root_notebook.clone()));
},
WindowInput::CreateNewNote => {
self.active_note = Note::new("");
},
_ => (),
}
}
}

View file

@ -0,0 +1,124 @@
use std::rc::Rc;
use binding::U8Binding;
use gtk::{prelude::{
ButtonExt, GtkWindowExt, ListItemExt, WidgetExt,
}};
use noteriety::{note::Note, notebook::Notebook};
use relm4::*;
use typed_view::list::{RelmListItem, TypedListView};
pub struct SidebarItemWidgets {
label: gtk::Label,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct SidebarItem {
note: Note,
title: String,
binding: U8Binding,
}
impl SidebarItem {
fn new(note: Note) -> Self {
Self {
note: note.clone(),
title: note.title.clone(),
binding: U8Binding::new(0),
}
}
}
impl RelmListItem for SidebarItem {
type Root = gtk::Box;
type Widgets = SidebarItemWidgets;
fn setup(_list_item: &gtk::ListItem) -> (Self::Root, Self::Widgets) {
view! {
list_item = gtk::Box {
inline_css: "padding: 10px; border-radius: 6px",
#[name = "label"]
gtk::Label,
}
}
let widgets = SidebarItemWidgets {
label,
};
(list_item, widgets)
}
fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
let SidebarItemWidgets {
label
} = widgets;
label.set_label(&self.note.title.to_string())
}
}
pub(crate) struct Sidebar {
list_view: TypedListView<SidebarItem, gtk::SingleSelection>,
root: Option<Notebook>,
}
#[derive(Debug)]
pub(crate) enum Output {
}
#[derive(Debug)]
pub(crate) enum Input {
UpdateRoot(Notebook),
}
#[relm4::component(pub(crate))]
impl SimpleComponent for Sidebar {
type Init = ();
type Input = Input;
type Output = Output;
view! {
#[root]
#[wrap(Some)]
&gtk::Box{
set_margin_top: 12,
gtk::ScrolledWindow {
set_vexpand: true,
set_hexpand: true,
#[local_ref]
sidebar_view -> gtk::ListView {}
}
}
}
fn init(root_notebook: Self::Init, root: Self::Root, sender: ComponentSender<Self>) -> ComponentParts<Self> {
let list_view_wrapper: TypedListView<SidebarItem, gtk::SingleSelection> =
TypedListView::with_sorting();
let model = Self {
list_view: list_view_wrapper,
root: None,
};
let sidebar_view = &model.list_view.view;
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
match message {
Input::UpdateRoot(notebook) => {
self.list_view.clear();
notebook.child_notes.into_iter().for_each(|n| {
self.list_view.append(SidebarItem::new(n.clone()));
});
},
_ => todo!()
}
}
}

View file

@ -0,0 +1,55 @@
use gtk::prelude::*;
use relm4::prelude::*;
#[derive(Default)]
pub(crate) struct NoteTitleEntry {
pub(crate) content: gtk::EntryBuffer,
}
#[derive(Debug)]
pub(crate) enum NoteTitleEntryInput {
}
#[derive(Debug)]
pub(crate) enum NoteTitleEntryOutput {
Content(String),
}
#[relm4::component(pub(crate))]
impl SimpleComponent for NoteTitleEntry {
type Init = ();
type Input = NoteTitleEntryInput;
type Output = NoteTitleEntryOutput;
view! {
#[root]
gtk::Entry {
set_hexpand: true,
set_margin_all: 12,
set_buffer: &model.content,
set_placeholder_text: Some("note title"),
connect_has_focus_notify: move |entry| {
if !entry.is_focus() {
sender.output(NoteTitleEntryOutput::Content(entry.buffer().text().to_string().into())).unwrap();
}
},
},
}
fn init(
_init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {
content: gtk::EntryBuffer::default(),
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
match message {
}
}
}

View file

@ -20,10 +20,10 @@ pub struct Note {
pub title: String,
/// todo: replace body type with something more appropriate.
/// a rope-backed buffer, perhaps.
body: String,
created_at: chrono::DateTime<chrono::Utc>,
pub content: String,
pub created_at: chrono::DateTime<chrono::Utc>,
/// if a note has never been updated since its creation, updated_at will be none.
updated_at: Option<chrono::DateTime<chrono::Utc>>,
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl PartialEq for Note {
@ -40,6 +40,12 @@ impl PartialOrd for Note {
}
}
impl Ord for Note {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.created_at.partial_cmp(&other.created_at).unwrap_or(std::cmp::Ordering::Equal)
}
}
impl Note {
pub fn new<T>(title: T) -> Self
where
@ -48,7 +54,7 @@ impl Note {
Self {
id: Uuid::new_v4(),
title: title.as_ref().to_string(),
body: Default::default(),
content: Default::default(),
created_at: chrono::Utc::now(),
updated_at: None,
}
@ -61,7 +67,7 @@ impl Note {
Self {
id: self.id,
title: title.as_ref().to_string(),
body: self.body,
content: self.content,
created_at: self.created_at,
updated_at: Some(Utc::now())
}
@ -71,7 +77,7 @@ impl Note {
Self {
id: self.id,
title: self.title,
body: body.as_ref().to_string(),
content: body.as_ref().to_string(),
created_at: self.created_at,
updated_at: Some(Utc::now()),
}
@ -88,7 +94,15 @@ impl Note {
where
P: AsRef<Path>,
{
let file = File::open(path).map_err(|_| Error::NoteFileRead)?;
let file = File::create(path).map_err(|_| Error::NoteFileRead)?;
self.write(file)
}
pub fn write_in<P>(&self, path: P) -> Result<(), Error>
where
P: AsRef<Path>,
{
let file = File::create(path.as_ref().join(self.id.to_string())).map_err(|_| Error::NoteFileRead)?;
self.write(file)
}
@ -128,7 +142,7 @@ mod test {
let note1 = Note {
id: Default::default(),
title: Default::default(),
body: Default::default(),
content: Default::default(),
created_at: DateTime::from_timestamp(2000000000, 0).unwrap(),
updated_at: Default::default(),
};
@ -136,7 +150,7 @@ mod test {
let note2 = Note {
id: Default::default(),
title: Default::default(),
body: Default::default(),
content: Default::default(),
created_at: DateTime::from_timestamp(1000000000, 0).unwrap(),
updated_at: Default::default(),
};

View file

@ -8,7 +8,7 @@ use crate::{error::Error, note::Note};
/// a notebook is a representation of a named collection of notes and child notebooks.
/// notebooks are ephemeral, and are represented on-disk by the directory tree.
#[derive(Default)]
#[derive(Clone, Debug, Default)]
pub struct Notebook {
pub root_path: PathBuf,
pub name: String,