init commit
This commit is contained in:
Binary file not shown.
@@ -0,0 +1 @@
|
||||
*.env
|
||||
@@ -0,0 +1,28 @@
|
||||
# A Simple Notes Application
|
||||
`osn` is a Notes application written in C, using GTK bindings, and is meant for use on Linux / Wayland / Hyprland setups and the Spaces ecosystem, more to come soon on that.
|
||||
|
||||
Currently, the default (and only) option is to toggle `osn` when `osn` is ran again. This keeps the Notes in memory, keeping the launch nice and responsive.
|
||||
|
||||
## Build from source
|
||||
Run the following commands:
|
||||
```
|
||||
git clone https://github.com/weave-space/osnotes
|
||||
cd osnotes
|
||||
meson compile -C build
|
||||
./build/osn
|
||||
```
|
||||
|
||||
## TODO
|
||||
- [ ] Responsive design sizes
|
||||
- [ ] CLI flags
|
||||
- [ ] Man page
|
||||
- [ ] Documentation
|
||||
- [ ] In-depth testing
|
||||
- [ ] Customization
|
||||
|
||||
## Customization
|
||||
There is currently no customization available for OSNotes.
|
||||
|
||||
## Credits
|
||||
- Google Gemini 3 (Explanation of docs and concepts and debugging.)
|
||||
- https://docs.gtk.org/gtk4/index.html
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
project('osnotes', 'c',
|
||||
version : '0.1',
|
||||
default_options : ['warning_level=0'])
|
||||
|
||||
# Import the GNOME module to handle GResources
|
||||
gnome = import('gnome')
|
||||
|
||||
gtk_dep = dependency('gtk4')
|
||||
libevdev_dep = dependency('libevdev')
|
||||
layer_shell_dep = dependency('gtk4-layer-shell-0')
|
||||
|
||||
subdir('src')
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
#include "glib-object.h"
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
typedef enum { TOOL_PEN, TOOL_HIGHLIGHTER, TOOL_ERASER } ToolType;
|
||||
|
||||
typedef struct {
|
||||
double x, y, pressure;
|
||||
} Point;
|
||||
|
||||
typedef struct {
|
||||
GQueue *points;
|
||||
GdkRGBA color;
|
||||
double width;
|
||||
ToolType tool_type;
|
||||
} Stroke;
|
||||
|
||||
GList *strokes = NULL;
|
||||
Stroke *current_stroke = NULL;
|
||||
cairo_surface_t *surface = NULL;
|
||||
|
||||
GtkWidget *canvas = NULL;
|
||||
GtkWidget *main_window = NULL;
|
||||
|
||||
ToolType current_tool = TOOL_PEN;
|
||||
int bg_template = 0;
|
||||
|
||||
static void clear_strokes() {
|
||||
if (!strokes)
|
||||
return;
|
||||
|
||||
for (GList *l = strokes; l != NULL; l = l->next) {
|
||||
Stroke *s = (Stroke *)l->data;
|
||||
if (s->points) {
|
||||
// Free every Point struct inside the queue
|
||||
while (!g_queue_is_empty(s->points)) {
|
||||
g_free(g_queue_pop_head(s->points));
|
||||
}
|
||||
g_queue_free(s->points);
|
||||
}
|
||||
g_free(s);
|
||||
}
|
||||
g_list_free(strokes);
|
||||
strokes = NULL;
|
||||
}
|
||||
|
||||
static void set_cairo_for_stroke(cairo_t *cr, Stroke *s) {
|
||||
gdk_cairo_set_source_rgba(cr, &s->color);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
if (s->tool_type == TOOL_HIGHLIGHTER) {
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_MULTIPLY);
|
||||
} else if (s->tool_type == TOOL_ERASER) {
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_DEST_OUT);
|
||||
} else {
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_stroke_path(cairo_t *cr, Stroke *s) {
|
||||
if (!s || !s->points)
|
||||
return;
|
||||
|
||||
if (g_queue_get_length(s->points) == 1) {
|
||||
set_cairo_for_stroke(cr, s);
|
||||
Point *p = (Point *)s->points->head->data;
|
||||
cairo_arc(cr, p->x, p->y, s->width / 2.0, 0, 2 * G_PI);
|
||||
cairo_fill(cr);
|
||||
return;
|
||||
}
|
||||
|
||||
set_cairo_for_stroke(cr, s);
|
||||
|
||||
GList *l = s->points->head;
|
||||
Point *p1 = (Point *)l->data;
|
||||
cairo_move_to(cr, p1->x, p1->y);
|
||||
|
||||
for (l = l->next; l != NULL; l = l->next) {
|
||||
Point *p2 = (Point *)l->data;
|
||||
double pressure = (p1->pressure + p2->pressure) / 2.0;
|
||||
double line_width = s->width;
|
||||
|
||||
if (s->tool_type == TOOL_PEN) {
|
||||
line_width *= MAX(0.2, pressure * 1.5);
|
||||
}
|
||||
|
||||
cairo_set_line_width(cr, line_width);
|
||||
cairo_line_to(cr, p2->x, p2->y);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_move_to(cr, p2->x, p2->y);
|
||||
p1 = p2;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_background(cairo_t *cr, int w, int h) {
|
||||
cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
|
||||
cairo_paint(cr);
|
||||
}
|
||||
|
||||
static void ensure_surface(GtkWidget *widget) {
|
||||
int width = gtk_widget_get_width(widget);
|
||||
int height = gtk_widget_get_height(widget);
|
||||
|
||||
// 1. Prevent 0-sized surface creation (The Portrait fix)
|
||||
if (width <= 1 || height <= 1)
|
||||
return;
|
||||
|
||||
if (surface && (cairo_image_surface_get_width(surface) != width ||
|
||||
cairo_image_surface_get_height(surface) != height)) {
|
||||
|
||||
// 2. Optional: Copy old content to new surface so you don't lose notes on
|
||||
// rotation
|
||||
cairo_surface_t *old_surface = surface;
|
||||
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
|
||||
draw_background(cr, width, height);
|
||||
|
||||
// Re-draw all strokes onto the new size
|
||||
for (GList *l = strokes; l != NULL; l = l->next) {
|
||||
draw_stroke_path(cr, (Stroke *)l->data);
|
||||
}
|
||||
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(old_surface);
|
||||
}
|
||||
|
||||
if (!surface) {
|
||||
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void on_draw(GtkDrawingArea *area, cairo_t *cr, int width, int height,
|
||||
gpointer data) {
|
||||
draw_background(cr, width, height);
|
||||
ensure_surface(GTK_WIDGET(area));
|
||||
cairo_set_source_surface(cr, surface, 0, 0);
|
||||
cairo_paint(cr);
|
||||
if (current_stroke) {
|
||||
draw_stroke_path(cr, current_stroke);
|
||||
}
|
||||
}
|
||||
|
||||
void start_new_stroke(double x, double y, GdkEvent *event) {
|
||||
current_stroke = g_new0(Stroke, 1);
|
||||
current_stroke->points = g_queue_new();
|
||||
current_stroke->tool_type = current_tool;
|
||||
|
||||
GdkDevice *device = gdk_event_get_device(event);
|
||||
GdkDeviceTool *tool = gdk_device_get_device_tool(device);
|
||||
|
||||
if (tool) {
|
||||
GdkDeviceToolType type = gdk_device_tool_get_tool_type(tool);
|
||||
g_print("DEBUG: Tool Type: %d\n", type);
|
||||
if (type == GDK_DEVICE_TOOL_TYPE_ERASER) {
|
||||
current_stroke->tool_type = TOOL_ERASER;
|
||||
}
|
||||
}
|
||||
|
||||
GdkModifierType state = gdk_event_get_modifier_state(event);
|
||||
if (state & GDK_BUTTON3_MASK) {
|
||||
current_stroke->tool_type = TOOL_HIGHLIGHTER;
|
||||
g_print("DEBUG: Button 3 Mask Detected\n");
|
||||
}
|
||||
|
||||
if (current_stroke->tool_type == TOOL_PEN) {
|
||||
gdk_rgba_parse(¤t_stroke->color, "black");
|
||||
current_stroke->width = 2.0;
|
||||
} else if (current_stroke->tool_type == TOOL_HIGHLIGHTER) {
|
||||
gdk_rgba_parse(¤t_stroke->color, "#FFF500");
|
||||
current_stroke->color.alpha = 0.4;
|
||||
current_stroke->width = 20.0;
|
||||
} else if (current_stroke->tool_type == TOOL_ERASER) {
|
||||
gdk_rgba_parse(¤t_stroke->color, "#FFFFFF");
|
||||
current_stroke->color.alpha = 1.0;
|
||||
current_stroke->width = 30.0;
|
||||
}
|
||||
|
||||
Point *p = g_new0(Point, 1);
|
||||
p->x = x;
|
||||
p->y = y;
|
||||
p->pressure = 0.5;
|
||||
gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &p->pressure);
|
||||
|
||||
g_queue_push_tail(current_stroke->points, p);
|
||||
gtk_widget_queue_draw(canvas);
|
||||
}
|
||||
|
||||
static void on_motion(GtkEventControllerMotion *controller, double x, double y,
|
||||
gpointer user_data) {
|
||||
GdkEvent *event =
|
||||
gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(controller));
|
||||
GdkModifierType state = gdk_event_get_modifier_state(event);
|
||||
|
||||
gboolean is_drawing =
|
||||
(state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK));
|
||||
|
||||
if (is_drawing) {
|
||||
if (!current_stroke) {
|
||||
start_new_stroke(x, y, event);
|
||||
} else {
|
||||
Point *p = g_new0(Point, 1);
|
||||
p->x = x;
|
||||
p->y = y;
|
||||
p->pressure = 0.5;
|
||||
gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &p->pressure);
|
||||
g_queue_push_tail(current_stroke->points, p);
|
||||
gtk_widget_queue_draw(canvas);
|
||||
}
|
||||
} else {
|
||||
if (current_stroke) {
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
draw_stroke_path(cr, current_stroke);
|
||||
cairo_destroy(cr);
|
||||
strokes = g_list_append(strokes, current_stroke);
|
||||
current_stroke = NULL;
|
||||
gtk_widget_queue_draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void on_pressed(GtkGestureClick *gesture, int n_press, double x,
|
||||
double y, gpointer user_data) {
|
||||
if (current_stroke)
|
||||
return;
|
||||
|
||||
GdkEvent *event =
|
||||
gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(gesture));
|
||||
start_new_stroke(x, y, event);
|
||||
}
|
||||
|
||||
static void on_released(GtkGestureClick *gesture, int n_press, double x,
|
||||
double y, gpointer user_data) {
|
||||
if (current_stroke) {
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
draw_stroke_path(cr, current_stroke);
|
||||
cairo_destroy(cr);
|
||||
strokes = g_list_append(strokes, current_stroke);
|
||||
current_stroke = NULL;
|
||||
}
|
||||
gtk_widget_queue_draw(canvas);
|
||||
}
|
||||
|
||||
static void on_tool_changed(GtkToggleButton *btn, gpointer user_data) {
|
||||
if (!gtk_toggle_button_get_active(btn))
|
||||
return;
|
||||
const char *id = gtk_buildable_get_buildable_id(GTK_BUILDABLE(btn));
|
||||
if (g_strcmp0(id, "btn_pen") == 0)
|
||||
current_tool = TOOL_PEN;
|
||||
else if (g_strcmp0(id, "btn_highlighter") == 0)
|
||||
current_tool = TOOL_HIGHLIGHTER;
|
||||
else if (g_strcmp0(id, "btn_eraser") == 0)
|
||||
current_tool = TOOL_ERASER;
|
||||
}
|
||||
|
||||
static void on_template_changed(GtkComboBoxText *combo, gpointer user_data) {
|
||||
bg_template = gtk_combo_box_get_active(GTK_COMBO_BOX(combo));
|
||||
if (surface) {
|
||||
cairo_surface_destroy(surface);
|
||||
surface = NULL;
|
||||
}
|
||||
gtk_widget_queue_draw(canvas);
|
||||
}
|
||||
|
||||
static void on_export_response(GtkNativeDialog *d, int r, gpointer u) {
|
||||
if (r == GTK_RESPONSE_ACCEPT) {
|
||||
GFile *f = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(d));
|
||||
char *p = g_file_get_path(f);
|
||||
if (surface)
|
||||
cairo_surface_write_to_png(surface, p);
|
||||
g_free(p);
|
||||
g_object_unref(f);
|
||||
}
|
||||
g_object_unref(d);
|
||||
}
|
||||
|
||||
static void action_export(GSimpleAction *a, GVariant *p, gpointer u) {
|
||||
if (!surface)
|
||||
return;
|
||||
GtkFileChooserNative *d = gtk_file_chooser_native_new(
|
||||
"Export", GTK_WINDOW(main_window), GTK_FILE_CHOOSER_ACTION_SAVE, "Save",
|
||||
"Cancel");
|
||||
g_signal_connect(d, "response", G_CALLBACK(on_export_response), NULL);
|
||||
gtk_native_dialog_show(GTK_NATIVE_DIALOG(d));
|
||||
}
|
||||
|
||||
static void on_save_response(GtkNativeDialog *d, int r, gpointer data) {}
|
||||
|
||||
static void action_save(GSimpleAction *a, GVariant *p, gpointer u) {}
|
||||
|
||||
static void on_load_response(GtkNativeDialog *d, int r, gpointer u) {}
|
||||
|
||||
static void action_load(GSimpleAction *a, GVariant *p, gpointer u) {}
|
||||
|
||||
static void activate(GtkApplication *app, gpointer user_data) {
|
||||
GtkBuilderCScope *scope = GTK_BUILDER_CSCOPE(gtk_builder_cscope_new());
|
||||
gtk_builder_cscope_add_callback_symbol(scope, "on_tool_changed",
|
||||
G_CALLBACK(on_tool_changed));
|
||||
|
||||
GtkBuilder *builder = gtk_builder_new();
|
||||
gtk_builder_set_scope(builder, GTK_BUILDER_SCOPE(scope));
|
||||
g_object_unref(scope);
|
||||
gtk_builder_add_from_resource(builder, "/org/spaces/osnotes/notes.ui", NULL);
|
||||
|
||||
main_window = GTK_WIDGET(gtk_builder_get_object(builder, "main_window"));
|
||||
canvas = GTK_WIDGET(gtk_builder_get_object(builder, "canvas"));
|
||||
|
||||
const GActionEntry app_entries[] = {
|
||||
{"save", action_save, NULL, NULL, NULL},
|
||||
{"load", action_load, NULL, NULL, NULL},
|
||||
{"export", action_export, NULL, NULL, NULL}};
|
||||
g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries,
|
||||
G_N_ELEMENTS(app_entries), app);
|
||||
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(canvas), on_draw, NULL, NULL);
|
||||
|
||||
GtkEventController *motion = gtk_event_controller_motion_new();
|
||||
g_signal_connect(motion, "motion", G_CALLBACK(on_motion), NULL);
|
||||
gtk_widget_add_controller(canvas, motion);
|
||||
|
||||
GtkGesture *click = gtk_gesture_click_new();
|
||||
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0);
|
||||
g_signal_connect(click, "pressed", G_CALLBACK(on_pressed), NULL);
|
||||
g_signal_connect(click, "released", G_CALLBACK(on_released), NULL);
|
||||
gtk_widget_add_controller(canvas, GTK_EVENT_CONTROLLER(click));
|
||||
|
||||
gtk_window_set_application(GTK_WINDOW(main_window), app);
|
||||
gtk_window_present(GTK_WINDOW(main_window));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GtkApplication *app =
|
||||
gtk_application_new("org.spaces.osnotes", G_APPLICATION_DEFAULT_FLAGS);
|
||||
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
||||
int status = g_application_run(G_APPLICATION(app), argc, argv);
|
||||
g_object_unref(app);
|
||||
return status;
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
@@ -0,0 +1,13 @@
|
||||
# Compile the resources into a C file
|
||||
res = gnome.compile_resources(
|
||||
'osnotes-resources',
|
||||
'resources.xml',
|
||||
source_dir: '.', # Look for .ui and .css in the current directory
|
||||
)
|
||||
|
||||
executable('osn',
|
||||
'main.c',
|
||||
res, # Add the compiled resource file to the sources
|
||||
dependencies : [gtk_dep, libevdev_dep, layer_shell_dep],
|
||||
export_dynamic : true,
|
||||
install : true)
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="main_menu">
|
||||
<section>
|
||||
<!-- <item> -->
|
||||
<!-- <attribute name="label">Save Note</attribute> -->
|
||||
<!-- <attribute name="action">app.save</attribute> -->
|
||||
<!-- </item> -->
|
||||
<!-- <item> -->
|
||||
<!-- <attribute name="label">Open Note</attribute> -->
|
||||
<!-- <attribute name="action">app.load</attribute> -->
|
||||
<!-- </item> -->
|
||||
<item>
|
||||
<attribute name="label">Export to PNG</attribute>
|
||||
<attribute name="action">app.export</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
|
||||
<object class="GtkApplicationWindow" id="main_window">
|
||||
<property name="title">OSNotes</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_layout">
|
||||
<property name="orientation">vertical</property>
|
||||
|
||||
<child>
|
||||
<object class="GtkHeaderBar" id="header">
|
||||
<property name="show-title-buttons">FALSE</property>
|
||||
|
||||
<child type="start">
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">6</property>
|
||||
<style><class name="linked"/></style>
|
||||
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="btn_pen">
|
||||
<property name="icon-name">document-edit-symbolic</property>
|
||||
<property name="active">TRUE</property>
|
||||
<property name="tooltip-text">Pen</property>
|
||||
<signal name="toggled" handler="on_tool_changed"/>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="btn_highlighter">
|
||||
<property name="icon-name">edit-select-symbolic</property>
|
||||
<property name="tooltip-text">Highlighter</property>
|
||||
<property name="group">btn_pen</property>
|
||||
<signal name="toggled" handler="on_tool_changed"/>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="btn_eraser">
|
||||
<property name="icon-name">edit-clear-symbolic</property>
|
||||
<property name="tooltip-text">Eraser</property>
|
||||
<property name="group">btn_pen</property>
|
||||
<signal name="toggled" handler="on_tool_changed"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child type="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">main_menu</property>
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<!-- <child type="end"> -->
|
||||
<!-- <object class="GtkComboBoxText" id="template_combo"> -->
|
||||
<!-- <property name="tooltip-text">Background Template</property> -->
|
||||
<!-- <items> -->
|
||||
<!-- <item id="blank">Blank</item> -->
|
||||
<!-- <item id="ruled">Ruled</item> -->
|
||||
<!-- <item id="grid">Grid</item> -->
|
||||
<!-- <item id="dotted">Dotted</item> -->
|
||||
<!-- </items> -->
|
||||
<!-- </object> -->
|
||||
<!-- </child> -->
|
||||
</object>
|
||||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkBox" id="canvas_box">
|
||||
<child>
|
||||
<object class="GtkDrawingArea" id="canvas">
|
||||
<property name="vexpand">TRUE</property>
|
||||
<property name="hexpand">TRUE</property>
|
||||
<property name="focusable">TRUE</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/spaces/osnotes">
|
||||
<file>notes.ui</file>
|
||||
<file>style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
@@ -0,0 +1,5 @@
|
||||
#canvas_box,
|
||||
#canvas_box > * {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
Reference in New Issue
Block a user