From c52e7766028b99f5e221b451e475a8f138c7e8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C8=9Bca=20Dumitru?= Date: Wed, 15 Feb 2023 15:25:18 +0200 Subject: [PATCH] Use blueprints for `TextInput` --- build.rs | 46 +++++- resources/resources.gresource.xml | 3 + src/editor/textdialog.blp | 77 ++++++++++ src/editor/textdialog.rs | 247 +++++++++++------------------- 4 files changed, 210 insertions(+), 163 deletions(-) create mode 100644 src/editor/textdialog.blp diff --git a/build.rs b/build.rs index 0b8c2d6..7a2cbeb 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,46 @@ +use std::{env, process::Command}; + fn main() { // See: https://docs.rs/diesel_migrations/2.0.0-rc.1/diesel_migrations/macro.embed_migrations.html#automatic-rebuilds println!("cargo:rerun-if-changed=migrations/"); - glib_build_tools::compile_resources( - &["resources"], - "resources/resources.gresource.xml", - "compiled.gresource", - ); + if std::option_env!("KCSHOT_LINTING").is_none() { + let blueprint_dir = env::var("OUT_DIR").unwrap() + "/resources"; + compile_blueprints(&["src/editor/textdialog.blp"], &blueprint_dir); + glib_build_tools::compile_resources( + &[blueprint_dir.as_str(), "resources"], + "resources/resources.gresource.xml", + "compiled.gresource", + ); + } else { + println!("cargo:rustc-cfg=kcshot_linting"); + } +} + +fn compile_blueprints(blueprints: &[&str], target: &str) { + let blueprint_compiler = std::env::var("BLUEPRINT_COMPILER_PATH") + .unwrap_or_else(|_| "blueprint-compiler".to_owned()); + + let mut blueprint_compiler = Command::new(blueprint_compiler); + blueprint_compiler + .arg("batch-compile") + .arg(target) + .arg("src/"); + + for blueprint in blueprints { + println!("cargo:rerun-if-changed={blueprint}"); + blueprint_compiler.arg(blueprint); + } + + let output = blueprint_compiler + .output() + .expect("Failed to execute blueprint-compiler"); + + if !output.status.success() { + panic!( + "blueprint-compiler returned {}, with stderr:\n{}", + output.status, + std::str::from_utf8(&output.stderr).unwrap() + ); + } } diff --git a/resources/resources.gresource.xml b/resources/resources.gresource.xml index ea5ec28..551dee1 100644 --- a/resources/resources.gresource.xml +++ b/resources/resources.gresource.xml @@ -14,5 +14,8 @@ editor/tool-text.png editor/tool-pencil.png editor/tool-colourpicker.png + + + editor/textdialog.ui diff --git a/src/editor/textdialog.blp b/src/editor/textdialog.blp new file mode 100644 index 0000000..5a97082 --- /dev/null +++ b/src/editor/textdialog.blp @@ -0,0 +1,77 @@ +using Gtk 4.0; + +template KCShotTextInput : Gtk.Box { + orientation: vertical; + spacing: 2; + + // Button area + Gtk.Box { + orientation: horizontal; + spacing: 4; + margin-start: 10; + margin-end: 10; + + Gtk.FontButton font_button { + margin-bottom: 5; + } + + Gtk.Button colour_button { + margin-bottom: 5; + width-request: 25; + height-request: 25; + + clicked => on_colour_button_clicked() swapped; + + child: Gtk.DrawingArea colour_button_drawing_area { + accessible-role: img; + width-request: 25; + height-request: 25; + }; + } + } + + // Text input area + Gtk.Box { + orientation: horizontal; + spacing: 2; + + Gtk.Box { + orientation: vertical; + spacing: 2; + + Gtk.Label { label: "Input"; } + Gtk.TextView input_view { + margin-top: 7; + margin-start: 10; + margin-bottom: 5; + width-request: 250; + height-request: 250; + wrap-mode: word; + + buffer: Gtk.TextBuffer { + changed => on_input_textbuffer_changed() swapped; + }; + } + } + + Gtk.Box { + orientation: vertical; + spacing: 2; + + Gtk.Label { label: "Preview"; } + Gtk.TextView preview { + margin-top: 7; + margin-end: 10; + margin-bottom: 5; + width-request: 250; + height-request: 250; + wrap-mode: word; + } + } + } + + Gtk.Label { + label: "You can use CommonMark Markdown or Pango markup to format your text."; + use-markup: true; + } +} diff --git a/src/editor/textdialog.rs b/src/editor/textdialog.rs index 07e50aa..bda3065 100644 --- a/src/editor/textdialog.rs +++ b/src/editor/textdialog.rs @@ -17,7 +17,7 @@ impl TextInput { } fn text(&self) -> String { - let input = self.imp().input.get().unwrap(); + let input = self.imp().input_view.get(); let buffer = input.buffer(); let markdown = buffer @@ -37,7 +37,6 @@ impl TextInput { self.imp() .font_button .get() - .unwrap() .font_desc() .expect("There should be a font description") } @@ -96,8 +95,8 @@ mod underlying { glib::{self, Properties, WeakRef}, prelude::*, subclass::prelude::*, + CompositeTemplate, }; - use once_cell::unsync::OnceCell; use super::parse; use crate::{ @@ -105,15 +104,21 @@ mod underlying { log_if_err, }; - #[derive(Debug, Default, Properties)] + #[derive(Debug, Default, Properties, CompositeTemplate)] #[properties(wrapper_type = super::TextInput)] + #[template(resource = "/kc/kcshot/ui/textdialog.ui")] pub struct TextInput { #[property(get, set, construct_only)] editor: WeakRef, - pub(super) font_button: OnceCell, - pub(super) input: OnceCell, - content: OnceCell, + #[template_child] + colour_button_drawing_area: TemplateChild, + #[template_child] + pub(super) font_button: TemplateChild, + #[template_child] + pub(super) input_view: TemplateChild, + #[template_child] + preview: TemplateChild, } #[glib::object_subclass] @@ -121,40 +126,68 @@ mod underlying { const NAME: &'static str = "KCShotTextInput"; type Type = super::TextInput; type ParentType = gtk4::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } } impl ObjectImpl for TextInput { fn constructed(&self) { - self.parent_constructed(); - - let content = self - .content - .get_or_init(|| gtk4::Box::new(gtk4::Orientation::Vertical, 2)); + /// Size of the DrawingArea, must be kept in sync with the blueprint file + const SIZE: f64 = 25.0; - let hbox = gtk4::Box::new(gtk4::Orientation::Horizontal, 2); - let font_button = place_format_buttons(content, &self.obj()); - self.font_button.set(font_button).unwrap(); - self.input.set(make_text_view(&hbox)).unwrap(); - content.append(&hbox); + self.parent_constructed(); - let info_label = make_info_label(); - content.append(&info_label); + let text_input = self.obj(); + + self.colour_button_drawing_area.set_draw_func( + glib::clone!(@weak text_input => move |_this, cairo, _w, _h| { + cairo.set_operator(cairo::Operator::Over); + + let editor = text_input.editor().unwrap(); + + let secondary_colour = editor.secondary_colour(); + if secondary_colour.alpha != 0 { + cairo.rectangle(0.0, 0.0, SIZE, SIZE); + cairo.set_source_colour(secondary_colour); + log_if_err!(cairo.fill()); + } else { + // Instead of drawing nothing (what a fully transparent colour is) we draw a + // checkerboard pattern instead + cairo.set_source_colour(Colour { + red: 0xff, + green: 0x00, + blue: 0xdc, + alpha: 0xff, + }); + cairo.rectangle(0.0, 0.0, SIZE / 2.0, SIZE / 2.0); + log_if_err!(cairo.fill()); + cairo.rectangle(SIZE / 2.0,SIZE / 2.0,SIZE / 2.0, SIZE / 2.0); + log_if_err!(cairo.fill()); + + cairo.set_source_colour(Colour::BLACK); + cairo.rectangle(0.0, SIZE / 2.0,SIZE / 2.0,SIZE / 2.0); + log_if_err!(cairo.fill()); + cairo.rectangle(SIZE / 2.0, 0.0, SIZE / 2.0,SIZE / 2.0); + log_if_err!(cairo.fill()); + } - self.obj().append(content); + cairo.set_source_colour(Colour::BLACK); + cairo.rectangle(0.0, 0.0, SIZE, SIZE); + cairo.set_line_width(1.0); + log_if_err!(cairo.stroke()); + }), + ); } fn dispose(&self) { - if let Some(content) = self.content.get() { - content.unparent(); - } - - if let Some(input) = self.input.get() { - input.unparent(); - } - - if let Some(font_button) = self.font_button.get() { - font_button.unparent(); - } + self.obj().first_child().unwrap().unparent(); } fn properties() -> &'static [glib::ParamSpec] { @@ -169,138 +202,36 @@ mod underlying { Self::derived_property(self, id, pspec) } } + impl BoxImpl for TextInput {} + impl WidgetImpl for TextInput {} - fn place_format_buttons(vbox: >k4::Box, text_input: &super::TextInput) -> gtk4::FontButton { - let hbox = gtk4::Box::new(gtk4::Orientation::Horizontal, 4); - let font_button = gtk4::FontButton::new(); - font_button.set_margin_bottom(5); - let colour_button = make_colour_chooser_button(text_input); - colour_button.set_margin_bottom(5); - - hbox.append(&colour_button); - hbox.append(&font_button); - hbox.set_margin_start(10); - hbox.set_margin_top(10); - - vbox.append(&hbox); - - font_button - } - - fn make_info_label() -> gtk4::Label { - let label = gtk4::Label::new(None); - label.set_markup(r#"You can use CommonMark Markdown or Pango markup to format your text."#); - label.set_margin_bottom(10); - label - } - - fn make_text_view(hbox: >k4::Box) -> gtk4::TextView { - let input_view_box = gtk4::Box::new(gtk4::Orientation::Vertical, 2); - input_view_box.append(>k4::Label::new(Some("Input"))); - - let input_view = gtk4::TextView::new(); - input_view.set_margin_top(7); - input_view.set_margin_start(10); - input_view.set_margin_bottom(5); - input_view.set_size_request(250, 250); - input_view.set_wrap_mode(gtk4::WrapMode::Word); - input_view_box.append(&input_view); - hbox.append(&input_view_box); - - let preview_box = gtk4::Box::new(gtk4::Orientation::Vertical, 2); - preview_box.append(>k4::Label::new(Some("Preview"))); - - let preview = gtk4::TextView::new(); - preview.set_editable(false); - preview.set_margin_top(7); - preview.set_margin_end(10); - preview.set_margin_bottom(5); - preview.set_size_request(250, 250); - preview.set_wrap_mode(gtk4::WrapMode::Word); - preview_box.append(&preview); - hbox.append(&preview_box); - - input_view - .buffer() - .connect_changed(glib::clone!(@weak preview => move |this| { - let text = this.text(&this.start_iter(), &this.end_iter(), true); - - let markup = parse::markdown2pango(&text); - - preview.buffer().set_text(""); - preview.buffer().insert_markup(&mut preview.buffer().start_iter(), &markup); + #[gtk4::template_callbacks] + impl TextInput { + #[template_callback] + fn on_colour_button_clicked(&self, _colour_button: >k4::Button) { + let editor = self.obj().editor().unwrap(); + let dialog = colourchooser::dialog(&editor); + let drawing_area = self.colour_button_drawing_area.get(); + + dialog.connect_response(glib::clone!(@weak drawing_area => move |editor, colour| { + editor.set_secondary_colour(colour); + drawing_area.queue_draw(); })); - input_view - } + dialog.show(); + } - impl BoxImpl for TextInput {} - impl WidgetImpl for TextInput {} + #[template_callback] + fn on_input_textbuffer_changed(&self, text_buffer: >k4::TextBuffer) { + let text = text_buffer.text(&text_buffer.start_iter(), &text_buffer.end_iter(), true); + let preview = self.preview.get(); - fn make_colour_chooser_button(text_input: &super::TextInput) -> gtk4::Button { - const SIZEI: i32 = 25; - const SIZEF: f64 = 25.0; - - let drawing_area = gtk4::DrawingArea::new(); - drawing_area.set_accessible_role(gtk4::AccessibleRole::Img); - drawing_area.set_size_request(SIZEI, SIZEI); - drawing_area.set_draw_func( - glib::clone!(@weak text_input => move |_this, cairo, _w, _h| { - cairo.set_operator(cairo::Operator::Over); - - let editor = text_input.editor().unwrap(); - - let secondary_colour = editor.secondary_colour(); - if secondary_colour.alpha != 0 { - cairo.rectangle(0.0, 0.0, SIZEF, SIZEF); - cairo.set_source_colour(secondary_colour); - log_if_err!(cairo.fill()); - } else { - // Instead of drawing nothing (what a fully transparent colour is) we draw a - // checkerboard pattern instead - cairo.set_source_colour(Colour { - red: 0xff, - green: 0x00, - blue: 0xdc, - alpha: 0xff, - }); - cairo.rectangle(0.0, 0.0, SIZEF / 2.0, SIZEF / 2.0); - log_if_err!(cairo.fill()); - cairo.rectangle(SIZEF / 2.0,SIZEF / 2.0,SIZEF / 2.0, SIZEF / 2.0); - log_if_err!(cairo.fill()); + let markup = parse::markdown2pango(&text); - cairo.set_source_colour(Colour::BLACK); - cairo.rectangle(0.0, SIZEF / 2.0,SIZEF / 2.0,SIZEF / 2.0); - log_if_err!(cairo.fill()); - cairo.rectangle(SIZEF / 2.0, 0.0, SIZEF / 2.0,SIZEF / 2.0); - log_if_err!(cairo.fill()); - } - - cairo.set_source_colour(Colour::BLACK); - cairo.rectangle(0.0, 0.0, SIZEF, SIZEF); - cairo.set_line_width(1.0); - log_if_err!(cairo.stroke()); - }), - ); - - let button = gtk4::Button::new(); - button.set_child(Some(&drawing_area)); - button.set_size_request(SIZEI, SIZEI); - - button.connect_clicked( - glib::clone!(@weak text_input => @default-panic, move |_this| { - let editor = text_input.editor().unwrap(); - let dialog = colourchooser::dialog(&editor); - - dialog.connect_response(glib::clone!(@weak drawing_area => move |editor, colour| { - editor.set_secondary_colour(colour); - drawing_area.queue_draw(); - })); - - dialog.show(); - }), - ); - - button + preview.buffer().set_text(""); + preview + .buffer() + .insert_markup(&mut preview.buffer().start_iter(), &markup); + } } }