From bc4a3453f8e6812ffb78ecc059a19e73f8012088 Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 2 Mar 2026 00:05:10 +0200 Subject: [PATCH] Add log view UI with transaction entry form Expense/Income toggle, amount entry, currency dropdown, category dropdown (filtered by transaction type), date picker with calendar popover, optional note field, save button, and recent transactions placeholder section. --- outlay-gtk/src/log_view.rs | 204 +++++++++++++++++++++++++++++++++++++ outlay-gtk/src/main.rs | 1 + outlay-gtk/src/window.rs | 15 ++- 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 outlay-gtk/src/log_view.rs diff --git a/outlay-gtk/src/log_view.rs b/outlay-gtk/src/log_view.rs new file mode 100644 index 0000000..3f78f5a --- /dev/null +++ b/outlay-gtk/src/log_view.rs @@ -0,0 +1,204 @@ +use adw::prelude::*; +use gtk::glib; + +pub struct LogView { + pub container: gtk::Box, + pub type_toggle: gtk::ToggleButton, + pub amount_row: adw::EntryRow, + pub currency_row: adw::ComboRow, + pub category_row: adw::ComboRow, + pub date_label: gtk::Label, + pub note_row: adw::EntryRow, + pub save_button: gtk::Button, + pub recent_group: adw::PreferencesGroup, +} + +impl LogView { + pub fn new() -> Self { + let container = gtk::Box::new(gtk::Orientation::Vertical, 0); + + let clamp = adw::Clamp::new(); + clamp.set_maximum_size(600); + clamp.set_margin_top(24); + clamp.set_margin_bottom(24); + clamp.set_margin_start(12); + clamp.set_margin_end(12); + + let inner = gtk::Box::new(gtk::Orientation::Vertical, 24); + + // -- Transaction type toggle -- + let type_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + type_box.add_css_class("linked"); + type_box.set_halign(gtk::Align::Center); + + let expense_btn = gtk::ToggleButton::with_label("Expense"); + expense_btn.set_active(true); + expense_btn.set_hexpand(true); + + let income_btn = gtk::ToggleButton::with_label("Income"); + income_btn.set_group(Some(&expense_btn)); + income_btn.set_hexpand(true); + + type_box.append(&expense_btn); + type_box.append(&income_btn); + + // -- Form group -- + let form_group = adw::PreferencesGroup::builder() + .title("New Transaction") + .build(); + + // Amount + let amount_row = adw::EntryRow::builder() + .title("Amount") + .build(); + amount_row.set_input_purpose(gtk::InputPurpose::Number); + form_group.add(&amount_row); + + // Currency + let currency_model = gtk::StringList::new(&[ + "USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY", "INR", "BRL", + ]); + let currency_row = adw::ComboRow::builder() + .title("Currency") + .model(¤cy_model) + .build(); + form_group.add(¤cy_row); + + // Category + let category_model = gtk::StringList::new(&[]); + let category_row = adw::ComboRow::builder() + .title("Category") + .model(&category_model) + .build(); + + // Populate with default expense categories + Self::populate_categories(&category_model, true); + + form_group.add(&category_row); + + // Date picker + let today = glib::DateTime::now_local().unwrap(); + let date_str = today.format("%Y-%m-%d").unwrap().to_string(); + + let date_label = gtk::Label::new(Some(&date_str)); + date_label.set_halign(gtk::Align::End); + date_label.set_hexpand(true); + + let calendar = gtk::Calendar::new(); + let popover = gtk::Popover::new(); + popover.set_child(Some(&calendar)); + + let date_menu_btn = gtk::MenuButton::new(); + date_menu_btn.set_popover(Some(&popover)); + date_menu_btn.set_icon_name("x-office-calendar-symbolic"); + date_menu_btn.add_css_class("flat"); + + let date_box = gtk::Box::new(gtk::Orientation::Horizontal, 8); + date_box.append(&date_label); + date_box.append(&date_menu_btn); + + let date_row = adw::ActionRow::builder() + .title("Date") + .build(); + date_row.add_suffix(&date_box); + date_row.set_activatable_widget(Some(&date_menu_btn)); + + // Connect calendar day-selected to update the label + let date_label_ref = date_label.clone(); + let popover_ref = popover.clone(); + calendar.connect_day_selected(move |cal| { + let dt = cal.date(); + let formatted = dt.format("%Y-%m-%d").unwrap().to_string(); + date_label_ref.set_label(&formatted); + popover_ref.popdown(); + }); + + form_group.add(&date_row); + + // Note + let note_row = adw::EntryRow::builder() + .title("Note (optional)") + .build(); + form_group.add(¬e_row); + + // -- Save button -- + let save_button = gtk::Button::with_label("Save"); + save_button.add_css_class("suggested-action"); + save_button.add_css_class("pill"); + save_button.set_halign(gtk::Align::Center); + save_button.set_margin_top(8); + + // -- Wire type toggle to filter categories -- + let category_model_ref = category_model.clone(); + expense_btn.connect_toggled(move |btn| { + if btn.is_active() { + Self::populate_categories(&category_model_ref, true); + } + }); + let category_model_ref2 = category_model.clone(); + income_btn.connect_toggled(move |btn| { + if btn.is_active() { + Self::populate_categories(&category_model_ref2, false); + } + }); + + // -- Recent transactions (placeholder) -- + let recent_group = adw::PreferencesGroup::builder() + .title("Recent") + .build(); + + let placeholder = adw::ActionRow::builder() + .title("No transactions yet") + .build(); + placeholder.add_css_class("dim-label"); + recent_group.add(&placeholder); + + // -- Assemble -- + inner.append(&type_box); + inner.append(&form_group); + inner.append(&save_button); + inner.append(&recent_group); + + clamp.set_child(Some(&inner)); + container.append(&clamp); + + LogView { + container, + type_toggle: expense_btn, + amount_row, + currency_row, + category_row, + date_label, + note_row, + save_button, + recent_group, + } + } + + fn populate_categories(model: >k::StringList, is_expense: bool) { + // Remove existing items + while model.n_items() > 0 { + model.remove(0); + } + + if is_expense { + let cats = [ + "Food & Dining", "Groceries", "Transport", "Housing", + "Utilities", "Healthcare", "Entertainment", "Shopping", + "Education", "Personal Care", "Travel", "Insurance", + "Gifts & Donations", "Other Expense", + ]; + for c in cats { + model.append(c); + } + } else { + let cats = [ + "Salary", "Freelance", "Investments", "Gifts Received", + "Refunds", "Other Income", + ]; + for c in cats { + model.append(c); + } + } + } +} diff --git a/outlay-gtk/src/main.rs b/outlay-gtk/src/main.rs index fa5ebab..73b075d 100644 --- a/outlay-gtk/src/main.rs +++ b/outlay-gtk/src/main.rs @@ -1,3 +1,4 @@ +mod log_view; mod window; use adw::prelude::*; diff --git a/outlay-gtk/src/window.rs b/outlay-gtk/src/window.rs index 2691737..8f4a896 100644 --- a/outlay-gtk/src/window.rs +++ b/outlay-gtk/src/window.rs @@ -1,9 +1,12 @@ use adw::prelude::*; +use crate::log_view::LogView; + pub struct MainWindow { pub window: adw::ApplicationWindow, pub split_view: adw::NavigationSplitView, pub content_stack: gtk::Stack, + pub log_view: LogView, } struct SidebarItem { @@ -26,7 +29,16 @@ impl MainWindow { let content_stack = gtk::Stack::new(); content_stack.set_transition_type(gtk::StackTransitionType::Crossfade); - for item in SIDEBAR_ITEMS { + // Log view - real widget + let log_view = LogView::new(); + let log_scroll = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) + .child(&log_view.container) + .build(); + content_stack.add_named(&log_scroll, Some("log")); + + // Remaining pages are placeholders for now + for item in &SIDEBAR_ITEMS[1..] { let page = adw::StatusPage::builder() .title(item.label) .icon_name(item.icon) @@ -98,6 +110,7 @@ impl MainWindow { window, split_view, content_stack, + log_view, } }