begin request entry refactory

This commit is contained in:
yggverse 2025-01-26 19:58:24 +02:00
parent 5255708be3
commit e7bd5bbdc6
24 changed files with 351 additions and 849 deletions

View file

@ -32,12 +32,10 @@ impl App {
// Init components // Init components
let browser = Rc::new(Browser::build(profile)); let browser = Rc::new(Browser::build(profile));
// Init events // Prevent startup warning @TODO
application.connect_activate({ application.connect_activate(|_| {});
let browser = browser.clone();
move |_| browser.update()
});
// Init events
application.connect_startup({ application.connect_startup({
let browser = browser.clone(); let browser = browser.clone();
let profile = profile.clone(); let profile = profile.clone();
@ -150,11 +148,7 @@ impl App {
&["<Primary>i"], &["<Primary>i"],
), ),
( (
format!( format!("{}.{}", browser.action.id, browser.action.escape.name()),
"{}.{}",
browser.action.id,
browser.action.escape.simple_action.name()
),
&["Escape"], &["Escape"],
), ),
// Tab actions // Tab actions

View file

@ -77,8 +77,8 @@ impl Browser {
action.escape.connect_activate({ action.escape.connect_activate({
let widget = widget.clone(); let widget = widget.clone();
let window = window.clone(); let window = window.clone();
move |tab_item_id| { move |_, _| {
window.escape(tab_item_id); window.escape();
widget.application_window.set_focus(gtk::Window::NONE); widget.application_window.set_focus(gtk::Window::NONE);
} }
}); });
@ -98,11 +98,6 @@ impl Browser {
} }
}); });
action.update.connect_activate({
let window = window.clone();
move |tab_item_id| window.update(tab_item_id)
});
// Return new activated `Self` // Return new activated `Self`
Self { Self {
action, action,
@ -185,10 +180,6 @@ impl Browser {
self.widget.application_window.present(); self.widget.application_window.present();
self self
} }
pub fn update(&self) {
self.window.update(None);
}
} }
// Tools // Tools

View file

@ -3,17 +3,15 @@ mod close;
mod debug; mod debug;
mod escape; mod escape;
mod profile; mod profile;
mod update;
use about::About; use about::About;
use close::Close; use close::Close;
use debug::Debug; use debug::Debug;
use escape::Escape; use escape::Escape;
use profile::Profile; use profile::Profile;
use update::Update;
use gtk::{ use gtk::{
gio::SimpleActionGroup, gio::{SimpleAction, SimpleActionGroup},
glib::{uuid_string_random, GString}, glib::{uuid_string_random, GString},
prelude::ActionMapExt, prelude::ActionMapExt,
}; };
@ -25,9 +23,8 @@ pub struct Action {
pub about: Rc<About>, pub about: Rc<About>,
pub close: Rc<Close>, pub close: Rc<Close>,
pub debug: Rc<Debug>, pub debug: Rc<Debug>,
pub escape: Rc<Escape>, pub escape: SimpleAction,
pub profile: Rc<Profile>, pub profile: Rc<Profile>,
pub update: Rc<Update>,
// Group // Group
pub id: GString, pub id: GString,
pub simple_action_group: SimpleActionGroup, pub simple_action_group: SimpleActionGroup,
@ -48,9 +45,8 @@ impl Action {
let about = Rc::new(About::new()); let about = Rc::new(About::new());
let close = Rc::new(Close::new()); let close = Rc::new(Close::new());
let debug = Rc::new(Debug::new()); let debug = Rc::new(Debug::new());
let escape = Rc::new(Escape::new()); let escape = SimpleAction::escape();
let profile = Rc::new(Profile::new()); let profile = Rc::new(Profile::new());
let update = Rc::new(Update::new());
// Generate unique group ID // Generate unique group ID
let id = uuid_string_random(); let id = uuid_string_random();
@ -62,9 +58,8 @@ impl Action {
simple_action_group.add_action(&about.simple_action); simple_action_group.add_action(&about.simple_action);
simple_action_group.add_action(&close.simple_action); simple_action_group.add_action(&close.simple_action);
simple_action_group.add_action(&debug.simple_action); simple_action_group.add_action(&debug.simple_action);
simple_action_group.add_action(&escape.simple_action); simple_action_group.add_action(&escape);
simple_action_group.add_action(&profile.simple_action); simple_action_group.add_action(&profile.simple_action);
simple_action_group.add_action(&update.simple_action);
// Done // Done
Self { Self {
@ -73,7 +68,6 @@ impl Action {
debug, debug,
escape, escape,
profile, profile,
update,
id, id,
simple_action_group, simple_action_group,
} }

View file

@ -1,85 +1,12 @@
use gtk::{ use gtk::{gio::SimpleAction, glib::uuid_string_random};
gio::SimpleAction,
glib::{uuid_string_random, GString},
prelude::{ActionExt, ToVariant},
};
/// [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) wrapper for `Escape` action of `Browser` group /// [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) wrapper for `Escape` action of `Browser` group
pub struct Escape { pub trait Escape {
pub simple_action: SimpleAction, fn escape() -> Self;
} }
impl Default for Escape { impl Escape for SimpleAction {
fn default() -> Self { fn escape() -> Self {
Self::new() SimpleAction::new(&uuid_string_random(), None)
}
}
impl Escape {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
simple_action: SimpleAction::new_stateful(
&uuid_string_random(),
None,
&String::new().to_variant(),
),
}
}
// Actions
/// Emit [activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal
/// * this action reset previous state for action after activation
pub fn activate_stateful_once(&self, tab_item_id: Option<GString>) {
// Save current state in memory
let _tab_item_id = state(&self.simple_action);
// Apply requested state
self.change_state(tab_item_id);
// Activate action
self.simple_action.activate(None);
// Return previous state
self.change_state(_tab_item_id);
}
/// Change action [state](https://docs.gtk.org/gio/method.SimpleAction.set_state.html)
/// * set default state on `None`
pub fn change_state(&self, state: Option<GString>) {
self.simple_action.change_state(
&match state {
Some(value) => value.to_string(),
None => String::new(),
}
.to_variant(),
)
}
// Events
/// Define callback function for
/// [SimpleAction::activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal
pub fn connect_activate(&self, callback: impl Fn(Option<GString>) + 'static) {
self.simple_action
.connect_activate(move |this, _| callback(state(this)));
}
}
/// Shared helper to get C-based action state in Optional format
fn state(this: &SimpleAction) -> Option<GString> {
let state = this
.state()
.expect("State value required")
.get::<String>()
.expect("Parameter type does not match `String`");
if state.is_empty() {
None
} else {
Some(state.into())
} }
} }

View file

@ -1,62 +0,0 @@
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, GString},
prelude::{ActionExt, StaticVariantType, ToVariant},
};
/// [SimpleAction](https://docs.gtk.org/gio/class.SimpleAction.html) wrapper for `Update` action of `Browser` group
pub struct Update {
pub simple_action: SimpleAction,
}
impl Default for Update {
fn default() -> Self {
Self::new()
}
}
impl Update {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
simple_action: SimpleAction::new(
&uuid_string_random(),
Some(&String::static_variant_type()),
),
}
}
// Actions
/// Emit [activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal
/// with formatted for this action [Variant](https://docs.gtk.org/glib/struct.Variant.html) value
pub fn activate(&self, tab_item_id: Option<&str>) {
self.simple_action.activate(Some(
&match tab_item_id {
Some(value) => String::from(value),
None => String::new(),
}
.to_variant(),
));
}
// Events
/// Define callback function for
/// [SimpleAction::activate](https://docs.gtk.org/gio/signal.SimpleAction.activate.html) signal
pub fn connect_activate(&self, callback: impl Fn(Option<GString>) + 'static) {
self.simple_action.connect_activate(move |_, variant| {
let tab_item_id = variant
.expect("Variant required to call this action")
.get::<String>()
.expect("Parameter type does not match `String`");
callback(match tab_item_id.is_empty() {
true => None,
false => Some(tab_item_id.into()),
})
});
}
}

View file

@ -11,7 +11,7 @@ use tab::Tab;
use super::Action as BrowserAction; use super::Action as BrowserAction;
use crate::Profile; use crate::Profile;
use gtk::{glib::GString, prelude::BoxExt, Box, Orientation}; use gtk::{prelude::BoxExt, Box, Orientation};
use std::rc::Rc; use std::rc::Rc;
pub struct Window { pub struct Window {
@ -131,12 +131,8 @@ impl Window {
} }
// Actions // Actions
pub fn escape(&self, tab_item_id: Option<GString>) { pub fn escape(&self) {
self.tab.escape(tab_item_id); self.tab.escape();
}
pub fn update(&self, tab_item_id: Option<GString>) {
self.tab.update(tab_item_id);
} }
pub fn clean(&self, transaction: &Transaction, app_browser_id: i64) -> Result<(), String> { pub fn clean(&self, transaction: &Transaction, app_browser_id: i64) -> Result<(), String> {

View file

@ -210,7 +210,7 @@ impl Menu {
// Recently closed history // Recently closed history
main_history_tab.remove_all(); main_history_tab.remove_all();
for item in profile.history.memory.tab.recent() { for item in profile.history.memory.tab.recent() {
let item_request = item.page.navigation.request.widget.entry.text(); // @TODO restore entire `Item` let item_request = item.page.navigation.request.entry.text(); // @TODO restore entire `Item`
let menu_item = gio::MenuItem::new(Some(&ellipsize(&item_request, LABEL_MAX_LENGTH)), None); let menu_item = gio::MenuItem::new(Some(&ellipsize(&item_request, LABEL_MAX_LENGTH)), None);
menu_item.set_action_and_target_value(Some(&format!( menu_item.set_action_and_target_value(Some(&format!(
"{}.{}", "{}.{}",

View file

@ -6,6 +6,7 @@ mod menu;
mod widget; mod widget;
use action::Action; use action::Action;
use adw::TabPage;
use error::Error; use error::Error;
pub use item::Item; pub use item::Item;
use menu::Menu; use menu::Menu;
@ -14,7 +15,7 @@ use widget::Widget;
use super::{Action as WindowAction, BrowserAction, Position}; use super::{Action as WindowAction, BrowserAction, Position};
use crate::Profile; use crate::Profile;
use gtk::{ use gtk::{
glib::{DateTime, GString, Propagation}, glib::{DateTime, Propagation},
prelude::{ActionExt, EditableExt, WidgetExt}, prelude::{ActionExt, EditableExt, WidgetExt},
}; };
use sqlite::Transaction; use sqlite::Transaction;
@ -25,7 +26,7 @@ pub struct Tab {
browser_action: Rc<BrowserAction>, browser_action: Rc<BrowserAction>,
window_action: Rc<WindowAction>, window_action: Rc<WindowAction>,
profile: Rc<Profile>, profile: Rc<Profile>,
index: Rc<RefCell<HashMap<Rc<GString>, Rc<Item>>>>, index: Rc<RefCell<HashMap<TabPage, Rc<Item>>>>,
pub action: Rc<Action>, pub action: Rc<Action>,
pub widget: Rc<Widget>, pub widget: Rc<Widget>,
} }
@ -41,8 +42,7 @@ impl Tab {
let action = Rc::new(Action::new()); let action = Rc::new(Action::new());
// Init empty HashMap index // Init empty HashMap index
let index: Rc<RefCell<HashMap<Rc<GString>, Rc<Item>>>> = let index: Rc<RefCell<HashMap<TabPage, Rc<Item>>>> = Rc::new(RefCell::new(HashMap::new()));
Rc::new(RefCell::new(HashMap::new()));
// Init context menu // Init context menu
let menu = Menu::new(window_action); let menu = Menu::new(window_action);
@ -58,21 +58,18 @@ impl Tab {
move |tab_view, tab_page| { move |tab_view, tab_page| {
let state = match tab_page { let state = match tab_page {
// on menu open // on menu open
Some(this) => { Some(tab_page) => {
if let Some(id) = this.keyword() { if let Some(item) = index.borrow().get(tab_page) {
if let Some(item) = index.borrow().get(&id) { item.page.update(); // update window actions using page of tab activated
item.page.update(); // update window actions using page of tab activated
}
} }
Some(tab_view.page_position(this)) // activated tab
Some(tab_view.page_position(tab_page)) // activated tab
} }
// on menu close // on menu close
None => { None => {
if let Some(page) = widget.page(None) { if let Some(tab_page) = widget.page(None) {
if let Some(id) = page.keyword() { if let Some(item) = index.borrow().get(&tab_page) {
if let Some(item) = index.borrow().get(&id) { item.page.update(); // update window actions using page of current tab
item.page.update(); // update window actions using page of current tab
}
} }
} }
None // current tab None // current tab
@ -97,57 +94,39 @@ impl Tab {
widget.tab_view.connect_close_page({ widget.tab_view.connect_close_page({
let index = index.clone(); let index = index.clone();
let profile = profile.clone(); let profile = profile.clone();
move |_, item| { move |_, tab_page| {
// Get index ID by keyword saved // Cleanup HashMap index
match item.keyword() { if let Some(item) = index.borrow_mut().remove(tab_page) {
Some(id) => { // Add history record into profile memory pool
if id.is_empty() { // * this action allows to recover recently closed tab (e.g. from the main menu)
panic!("Tab index can not be empty!") profile
} .history
// Cleanup HashMap index .memory
if let Some(item) = index.borrow_mut().remove(&id) { .tab
// Add history record into profile memory pool .add(item, DateTime::now_local().unwrap().to_unix());
// * this action allows to recover recently closed tab (e.g. from the main menu)
profile
.history
.memory
.tab
.add(item, DateTime::now_local().unwrap().to_unix());
}
}
None => panic!("Undefined tab index!"),
} }
Propagation::Proceed Propagation::Proceed
} }
}); });
widget.tab_view.connect_page_attached({
let window_action = window_action.clone();
let index = index.clone();
move |_, tab_page, _| {
if tab_page.is_selected() {
update_actions(tab_page, &index, &window_action);
}
}
});
widget.tab_view.connect_selected_page_notify({ widget.tab_view.connect_selected_page_notify({
let window_action = window_action.clone(); let window_action = window_action.clone();
let index = index.clone(); let index = index.clone();
move |this| { move |this| {
if let Some(page) = this.selected_page() { if let Some(tab_page) = this.selected_page() {
if let Some(id) = page.keyword() { tab_page.set_needs_attention(false);
if let Some(item) = index.borrow().get(&id) { update_actions(&tab_page, &index, &window_action);
window_action
.home
.simple_action
.set_enabled(item.action.home.is_enabled());
window_action
.reload
.simple_action
.set_enabled(item.action.reload.is_enabled());
window_action
.history_back
.simple_action
.set_enabled(item.action.history.back.is_enabled());
window_action
.history_forward
.simple_action
.set_enabled(item.action.history.forward.is_enabled());
}
}
page.set_needs_attention(false);
} }
} }
}); });
@ -193,13 +172,13 @@ impl Tab {
// Expect user input on tab appended has empty request entry // Expect user input on tab appended has empty request entry
// * this action initiated here because should be applied on tab appending event only // * this action initiated here because should be applied on tab appending event only
if request.is_none() || request.is_some_and(|value| value.is_empty()) { if request.is_none() || request.is_some_and(|value| value.is_empty()) {
item.page.navigation.request.widget.entry.grab_focus(); item.page.navigation.request.entry.grab_focus();
} }
// Register dynamically created tab components in the HashMap index // Register dynamically created tab components in the HashMap index
self.index self.index
.borrow_mut() .borrow_mut()
.insert(item.id.clone(), item.clone()); .insert(item.widget.tab_page.clone(), item.clone());
item item
} }
@ -215,18 +194,9 @@ impl Tab {
} }
// Toggle escape action for specified or current item // Toggle escape action for specified or current item
pub fn escape(&self, item_id: Option<GString>) { pub fn escape(&self) {
match item_id { if let Some(item) = self.item(None) {
Some(id) => { item.page.escape();
if let Some(item) = self.index.borrow().get(&id) {
item.page.escape()
}
}
None => {
if let Some(item) = self.item(None) {
item.page.escape();
}
}
} }
} }
@ -274,7 +244,7 @@ impl Tab {
if let Some(item) = self.item(page_position) { if let Some(item) = self.item(page_position) {
if let Some(home) = item.page.navigation.request.home() { if let Some(home) = item.page.navigation.request.home() {
let home = home.to_string(); let home = home.to_string();
item.page.navigation.request.widget.entry.set_text(&home); item.page.navigation.request.entry.set_text(&home);
item.client.handle(&home, true); item.client.handle(&home, true);
} }
} }
@ -296,23 +266,7 @@ impl Tab {
pub fn page_reload(&self, page_position: Option<i32>) { pub fn page_reload(&self, page_position: Option<i32>) {
if let Some(item) = self.item(page_position) { if let Some(item) = self.item(page_position) {
item.client item.client
.handle(&item.page.navigation.request.widget.entry.text(), true); .handle(&item.page.navigation.request.entry.text(), true);
}
}
pub fn update(&self, item_id: Option<GString>) {
let key = item_id.unwrap_or_default();
match self.index.borrow().get(&key) {
Some(item) => {
item.update();
}
None => {
// update all tabs
for (_, item) in self.index.borrow().iter() {
item.update();
}
}
} }
} }
@ -361,7 +315,7 @@ impl Tab {
// Register dynamically created tab item in the HashMap index // Register dynamically created tab item in the HashMap index
self.index self.index
.borrow_mut() .borrow_mut()
.insert(item.id.clone(), item.clone()); .insert(item.widget.tab_page.clone(), item.clone());
} }
} }
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
@ -412,11 +366,9 @@ impl Tab {
} }
fn item(&self, position: Option<i32>) -> Option<Rc<Item>> { fn item(&self, position: Option<i32>) -> Option<Rc<Item>> {
if let Some(page) = self.widget.page(position) { if let Some(tab_page) = self.widget.page(position) {
if let Some(id) = page.keyword() { if let Some(item) = self.index.borrow().get(&tab_page) {
if let Some(item) = self.index.borrow().get(&id) { return Some(item.clone());
return Some(item.clone());
}
} }
} }
None None
@ -424,6 +376,7 @@ impl Tab {
} }
// Tools // Tools
pub fn migrate(tx: &Transaction) -> Result<(), String> { pub fn migrate(tx: &Transaction) -> Result<(), String> {
// Migrate self components // Migrate self components
if let Err(e) = database::init(tx) { if let Err(e) = database::init(tx) {
@ -436,3 +389,37 @@ pub fn migrate(tx: &Transaction) -> Result<(), String> {
// Success // Success
Ok(()) Ok(())
} }
fn update_actions(
tab_page: &TabPage,
index: &Rc<RefCell<HashMap<TabPage, Rc<Item>>>>,
window_action: &Rc<WindowAction>,
) {
if let Some(item) = index.borrow().get(tab_page) {
window_action
.home
.simple_action
.set_enabled(item.action.home.is_enabled());
window_action
.reload
.simple_action
.set_enabled(item.action.reload.is_enabled());
window_action
.history_back
.simple_action
.set_enabled(item.action.history.back.is_enabled());
window_action
.history_forward
.simple_action
.set_enabled(item.action.history.forward.is_enabled());
return;
}
window_action.home.simple_action.set_enabled(false);
window_action.reload.simple_action.set_enabled(false);
window_action.history_back.simple_action.set_enabled(false);
window_action
.history_forward
.simple_action
.set_enabled(false);
}

View file

@ -95,14 +95,13 @@ impl Item {
this.set_enabled(false); this.set_enabled(false);
if let Some(uri) = page.navigation.request.home() { if let Some(uri) = page.navigation.request.home() {
let request = uri.to_string(); let request = uri.to_string();
page.navigation.request.widget.entry.set_text(&request); page.navigation.request.entry.set_text(&request);
client.handle(&request, true); client.handle(&request, true);
} }
} }
}); });
action.ident.connect_activate({ action.ident.connect_activate({
let browser_action = browser_action.clone();
let page = page.clone(); let page = page.clone();
let parent = tab_view.clone().upcast::<gtk::Widget>(); let parent = tab_view.clone().upcast::<gtk::Widget>();
let profile = profile.clone(); let profile = profile.clone();
@ -111,12 +110,8 @@ impl Item {
if let Some(uri) = page.navigation.request.uri() { if let Some(uri) = page.navigation.request.uri() {
let scheme = uri.scheme(); let scheme = uri.scheme();
if scheme == "gemini" || scheme == "titan" { if scheme == "gemini" || scheme == "titan" {
return identity::default( return identity::default(&window_action, &profile, &uri)
(&browser_action, &window_action), .present(Some(&parent));
&profile,
&uri,
)
.present(Some(&parent));
} }
} }
identity::unsupported().present(Some(&parent)); identity::unsupported().present(Some(&parent));
@ -128,7 +123,7 @@ impl Item {
let client = client.clone(); let client = client.clone();
move |request, is_history| { move |request, is_history| {
if let Some(text) = request { if let Some(text) = request {
page.navigation.request.widget.entry.set_text(&text); page.navigation.request.entry.set_text(&text);
client.handle(&text, is_history); client.handle(&text, is_history);
} }
} }
@ -138,13 +133,13 @@ impl Item {
let page = page.clone(); let page = page.clone();
let client = client.clone(); let client = client.clone();
move |_, _| { move |_, _| {
client.handle(&page.navigation.request.widget.entry.text(), false); client.handle(&page.navigation.request.entry.text(), false);
} }
}); });
// Handle immediately on request // Handle immediately on request
if let Some(text) = request { if let Some(text) = request {
page.navigation.request.widget.entry.set_text(text); page.navigation.request.entry.set_text(text);
if is_load { if is_load {
client.handle(text, true); client.handle(text, true);
} }
@ -162,15 +157,17 @@ impl Item {
// Actions // Actions
pub fn update(&self) { pub fn update(&self) {
// Update self actions // Update self actions
self.action self.action.home.set_enabled(
.home self.page
.set_enabled(self.page.navigation.request.home().is_some_and(|home| { .navigation
home.to_string() != self.page.navigation.request.widget.entry.text() .request
})); .home()
.is_some_and(|home| home.to_string() != self.page.navigation.request.entry.text()),
);
self.action self.action
.reload .reload
.set_enabled(!self.page.navigation.request.widget.entry.text().is_empty()); .set_enabled(!self.page.navigation.request.entry.text().is_empty());
// Update child components // Update child components
self.page.update(); self.page.update();

View file

@ -9,7 +9,7 @@ use feature::Feature;
use gtk::{ use gtk::{
gio::Cancellable, gio::Cancellable,
glib::{Uri, UriFlags}, glib::{Uri, UriFlags},
prelude::{CancellableExt, EditableExt, EntryExt}, prelude::{ActionExt, CancellableExt, EditableExt, EntryExt},
}; };
use std::{cell::Cell, rc::Rc}; use std::{cell::Cell, rc::Rc};
use subject::Subject; use subject::Subject;
@ -43,12 +43,8 @@ impl Client {
/// Route tab item `request` to protocol driver /// Route tab item `request` to protocol driver
/// * or `navigation` entry if the value not provided /// * or `navigation` entry if the value not provided
pub fn handle(&self, request: &str, is_snap_history: bool) { pub fn handle(&self, request: &str, is_snap_history: bool) {
// Move focus out from navigation entry // Move focus out from navigation entry @TODO
self.subject self.subject.page.browser_action.escape.activate(None);
.page
.browser_action
.escape
.activate_stateful_once(Some(self.subject.page.id.as_str().into()));
// Initially disable find action // Initially disable find action
self.subject self.subject
@ -66,7 +62,6 @@ impl Client {
.page .page
.navigation .navigation
.request .request
.widget
.entry .entry
.set_progress_fraction(0.1); .set_progress_fraction(0.1);
@ -96,7 +91,6 @@ impl Client {
.page .page
.navigation .navigation
.request .request
.widget
.entry .entry
.set_progress_fraction(0.0); .set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
@ -207,7 +201,7 @@ fn search(query: &str) -> Uri {
/// Make new history record in related components /// Make new history record in related components
/// * optional [Uri](https://docs.gtk.org/glib/struct.Uri.html) reference wanted only for performance reasons, to not parse it twice /// * optional [Uri](https://docs.gtk.org/glib/struct.Uri.html) reference wanted only for performance reasons, to not parse it twice
fn snap_history(subject: &Rc<Subject>, uri: Option<&Uri>) { fn snap_history(subject: &Rc<Subject>, uri: Option<&Uri>) {
let request = subject.page.navigation.request.widget.entry.text(); let request = subject.page.navigation.request.entry.text();
// Add new record into the global memory index (used in global menu) // Add new record into the global memory index (used in global menu)
// * if the `Uri` is `None`, try parse it from `request` // * if the `Uri` is `None`, try parse it from `request`

View file

@ -60,7 +60,6 @@ impl Gemini {
.page .page
.navigation .navigation
.request .request
.widget
.entry .entry
.set_progress_fraction(progress_fraction); .set_progress_fraction(progress_fraction);
} }
@ -135,7 +134,6 @@ impl Gemini {
.page .page
.navigation .navigation
.request .request
.widget
.entry .entry
.set_progress_fraction(0.0); .set_progress_fraction(0.0);
self.subject.tab_page.set_loading(false); self.subject.tab_page.set_loading(false);
@ -205,7 +203,7 @@ fn handle(
Some(1024), Some(1024),
); );
} }
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&title); subject.tab_page.set_title(&title);
redirects.replace(0); // reset redirects.replace(0); // reset
@ -277,7 +275,7 @@ fn handle(
} }
}, },
); );
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -300,7 +298,7 @@ fn handle(
Some(title) => title.into(), // @TODO Some(title) => title.into(), // @TODO
None => uri_to_title(&uri), None => uri_to_title(&uri),
}); });
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.page.window_action subject.page.window_action
.find .find
@ -311,7 +309,7 @@ fn handle(
Err(e) => { Err(e) => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some(&e.to_string())); status.set_description(Some(&e.to_string()));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -355,7 +353,7 @@ fn handle(
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
} }
} }
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
redirects.replace(0); // reset redirects.replace(0); // reset
}, },
@ -364,7 +362,7 @@ fn handle(
Err(e) => { Err(e) => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some(&e.to_string())); status.set_description(Some(&e.to_string()));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -378,7 +376,7 @@ fn handle(
.content .content
.to_status_mime(mime, Some((&subject.page.item_action, &uri))); .to_status_mime(mime, Some((&subject.page.item_action, &uri)));
status.set_description(Some(&format!("Content type `{mime}` yet not supported"))); status.set_description(Some(&format!("Content type `{mime}` yet not supported")));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -387,7 +385,7 @@ fn handle(
None => { None => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some("MIME type not found")); status.set_description(Some("MIME type not found"));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -425,7 +423,7 @@ fn handle(
if total > 5 { if total > 5 {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some("Redirection limit reached")); status.set_description(Some("Redirection limit reached"));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -436,7 +434,7 @@ fn handle(
|| uri.host() != target.host() { || uri.host() != target.host() {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some("External redirects not allowed by protocol specification")); status.set_description(Some("External redirects not allowed by protocol specification"));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -445,7 +443,6 @@ fn handle(
if matches!(response.meta.status, Status::PermanentRedirect) { if matches!(response.meta.status, Status::PermanentRedirect) {
subject.page.navigation subject.page.navigation
.request .request
.widget
.entry .entry
.set_text(&uri.to_string()); .set_text(&uri.to_string());
} }
@ -456,7 +453,7 @@ fn handle(
Err(e) => { Err(e) => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some(&e.to_string())); status.set_description(Some(&e.to_string()));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -465,7 +462,7 @@ fn handle(
None => { None => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some("Redirection target not found")); status.set_description(Some("Redirection target not found"));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -484,7 +481,7 @@ fn handle(
None => response.meta.status.to_string(), None => response.meta.status.to_string(),
})); }));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -492,7 +489,7 @@ fn handle(
error => { error => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some(&error.to_string())); status.set_description(Some(&error.to_string()));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset
@ -502,7 +499,7 @@ fn handle(
Err(e) => { Err(e) => {
let status = subject.page.content.to_status_failure(); let status = subject.page.content.to_status_failure();
status.set_description(Some(&e.to_string())); status.set_description(Some(&e.to_string()));
subject.page.navigation.request.widget.entry.set_progress_fraction(0.0); subject.page.navigation.request.entry.set_progress_fraction(0.0);
subject.tab_page.set_loading(false); subject.tab_page.set_loading(false);
subject.tab_page.set_title(&status.title()); subject.tab_page.set_title(&status.title());
redirects.replace(0); // reset redirects.replace(0); // reset

View file

@ -4,17 +4,13 @@ mod unsupported;
use default::Default; use default::Default;
use unsupported::Unsupported; use unsupported::Unsupported;
use super::{BrowserAction, Profile, WindowAction}; use super::{Profile, WindowAction};
use gtk::glib::Uri; use gtk::glib::Uri;
use std::rc::Rc; use std::rc::Rc;
/// Create new identity widget for Gemini protocol match given URI /// Create new identity widget for Gemini protocol match given URI
pub fn default( pub fn default(window_action: &Rc<WindowAction>, profile: &Rc<Profile>, request: &Uri) -> Default {
action: (&Rc<BrowserAction>, &Rc<WindowAction>), Default::build(window_action, profile, request)
profile: &Rc<Profile>,
request: &Uri,
) -> Default {
Default::build(action, profile, request)
} }
/// Create new identity widget for unknown request /// Create new identity widget for unknown request

View file

@ -1,7 +1,7 @@
mod widget; mod widget;
use widget::{form::list::item::value::Value, Widget}; use widget::{form::list::item::value::Value, Widget};
use super::{BrowserAction, Profile, WindowAction}; use super::{Profile, WindowAction};
use gtk::{glib::Uri, prelude::IsA}; use gtk::{glib::Uri, prelude::IsA};
use std::rc::Rc; use std::rc::Rc;
@ -14,26 +14,12 @@ impl Default {
// Construct // Construct
/// Create new `Self` for given `Profile` /// Create new `Self` for given `Profile`
pub fn build( pub fn build(window_action: &Rc<WindowAction>, profile: &Rc<Profile>, request: &Uri) -> Self {
(browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>),
profile: &Rc<Profile>,
request: &Uri,
) -> Self {
// Init widget // Init widget
let widget = Rc::new(Widget::build( let widget = Rc::new(Widget::build(profile, request));
(browser_action, window_action),
profile,
request,
));
// Init events // Init events
widget.on_cancel({
let browser_action = browser_action.clone();
move || browser_action.update.activate(None)
});
widget.on_apply({ widget.on_apply({
let browser_action = browser_action.clone();
let profile = profile.clone(); let profile = profile.clone();
let request = request.clone(); let request = request.clone();
let widget = widget.clone(); let widget = widget.clone();
@ -79,7 +65,6 @@ impl Default {
} }
// Apply changes // Apply changes
browser_action.update.activate(None);
window_action.reload.activate(); window_action.reload.activate();
} }
}); });

View file

@ -4,10 +4,7 @@ pub mod form;
use action::Action as WidgetAction; use action::Action as WidgetAction;
use form::{list::item::value::Value, Form}; use form::{list::item::value::Value, Form};
use crate::{ use crate::Profile;
app::browser::{action::Action as BrowserAction, window::action::Action as WindowAction},
Profile,
};
use adw::{ use adw::{
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
AlertDialog, ResponseAppearance, AlertDialog, ResponseAppearance,
@ -36,20 +33,12 @@ impl Widget {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn build( pub fn build(profile: &Rc<Profile>, request: &Uri) -> Self {
(browser_action, window_action): (&Rc<BrowserAction>, &Rc<WindowAction>),
profile: &Rc<Profile>,
request: &Uri,
) -> Self {
// Init actions // Init actions
let widget_action = Rc::new(WidgetAction::new()); let action = Rc::new(WidgetAction::new());
// Init child container // Init child container
let form = Rc::new(Form::build( let form = Rc::new(Form::build(&action, profile, request));
(browser_action, window_action, &widget_action),
profile,
request,
));
// Init main widget // Init main widget
let alert_dialog = AlertDialog::builder() let alert_dialog = AlertDialog::builder()
@ -76,7 +65,7 @@ impl Widget {
alert_dialog.set_response_appearance(RESPONSE_CANCEL.0, ResponseAppearance::Destructive); */ alert_dialog.set_response_appearance(RESPONSE_CANCEL.0, ResponseAppearance::Destructive); */
// Init events // Init events
widget_action.update.connect_activate({ action.update.connect_activate({
let form = form.clone(); let form = form.clone();
let alert_dialog = alert_dialog.clone(); let alert_dialog = alert_dialog.clone();
move || { move || {
@ -89,7 +78,7 @@ impl Widget {
}); });
// Make initial update // Make initial update
widget_action.update.activate(); action.update.activate();
// Return new activated `Self` // Return new activated `Self`
Self { Self {
@ -116,20 +105,6 @@ impl Widget {
}); });
} }
/// Callback wrapper to cancel
/// [response](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/signal.AlertDialog.response.html)
/// * return require reload state
pub fn on_cancel(&self, callback: impl Fn() + 'static) {
self.alert_dialog
.connect_response(Some(RESPONSE_CANCEL.0), move |this, response| {
// Prevent double-click action
this.set_response_enabled(response, false);
// Result
callback()
});
}
/// Show dialog with new preset /// Show dialog with new preset
pub fn present(&self, parent: Option<&impl IsA<gtk::Widget>>) { pub fn present(&self, parent: Option<&impl IsA<gtk::Widget>>) {
self.alert_dialog.present(parent) self.alert_dialog.present(parent)

View file

@ -13,10 +13,7 @@ use name::Name;
use save::Save; use save::Save;
use super::WidgetAction; use super::WidgetAction;
use crate::{ use crate::Profile;
app::browser::{action::Action as BrowserAction, window::action::Action as WindowAction},
Profile,
};
use gtk::{glib::Uri, prelude::BoxExt, Box, Orientation}; use gtk::{glib::Uri, prelude::BoxExt, Box, Orientation};
use std::rc::Rc; use std::rc::Rc;
@ -37,27 +34,14 @@ impl Form {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn build( pub fn build(widget_action: &Rc<WidgetAction>, profile: &Rc<Profile>, request: &Uri) -> Self {
(browser_action, _window_action, widget_action): (
&Rc<BrowserAction>,
&Rc<WindowAction>,
&Rc<WidgetAction>,
),
profile: &Rc<Profile>,
request: &Uri,
) -> Self {
// Init components // Init components
let list = Rc::new(List::build(widget_action, profile, request)); let list = Rc::new(List::build(widget_action, profile, request));
let file = Rc::new(File::build(widget_action)); let file = Rc::new(File::build(widget_action));
let name = Rc::new(Name::build(widget_action)); let name = Rc::new(Name::build(widget_action));
let save = Rc::new(Save::build(profile, &list)); let save = Rc::new(Save::build(profile, &list));
let drop = Rc::new(Drop::build(profile, &list)); let drop = Rc::new(Drop::build(profile, &list));
let exit = Rc::new(Exit::build( let exit = Rc::new(Exit::build(widget_action, profile, &list, request));
(browser_action, widget_action),
profile,
&list,
request,
));
// Init main container // Init main container
let g_box = Box::builder().orientation(Orientation::Vertical).build(); let g_box = Box::builder().orientation(Orientation::Vertical).build();

View file

@ -2,7 +2,7 @@ use super::{
list::{item::Value, List}, list::{item::Value, List},
WidgetAction, WidgetAction,
}; };
use crate::{app::browser::Action as BrowserAction, Profile}; use crate::Profile;
use adw::{ use adw::{
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
AlertDialog, ResponseAppearance, AlertDialog, ResponseAppearance,
@ -34,7 +34,7 @@ impl Exit {
/// Create new `Self` /// Create new `Self`
pub fn build( pub fn build(
(browser_action, widget_action): (&Rc<BrowserAction>, &Rc<WidgetAction>), widget_action: &Rc<WidgetAction>,
profile: &Rc<Profile>, profile: &Rc<Profile>,
list: &Rc<List>, list: &Rc<List>,
request: &Uri, request: &Uri,
@ -49,7 +49,6 @@ impl Exit {
// Init events // Init events
button.connect_clicked({ button.connect_clicked({
let browser_action = browser_action.clone();
let button = button.clone(); let button = button.clone();
let list = list.clone(); let list = list.clone();
let profile = profile.clone(); let profile = profile.clone();
@ -84,7 +83,6 @@ impl Exit {
// Connect confirmation event // Connect confirmation event
alert_dialog.connect_response(Some(RESPONSE_CONFIRM.0), { alert_dialog.connect_response(Some(RESPONSE_CONFIRM.0), {
let browser_action = browser_action.clone();
let button = button.clone(); let button = button.clone();
let list = list.clone(); let list = list.clone();
let profile = profile.clone(); let profile = profile.clone();
@ -111,7 +109,6 @@ impl Exit {
button.set_label(&e.to_string()) button.set_label(&e.to_string())
} }
} }
browser_action.update.activate(None);
widget_action.update.activate(); widget_action.update.activate();
} }
}); });

View file

@ -54,7 +54,7 @@ impl Page {
let navigation = Rc::new(Navigation::build( let navigation = Rc::new(Navigation::build(
profile, profile,
(browser_action, window_action, tab_action, item_action), (window_action, tab_action, item_action),
)); ));
let input = Rc::new(Input::new()); let input = Rc::new(Input::new());
@ -92,7 +92,7 @@ impl Page {
let result = match self let result = match self
.profile .profile
.bookmark .bookmark
.toggle(self.navigation.request.widget.entry.text().as_str()) .toggle(self.navigation.request.entry.text().as_str())
{ {
Ok(result) => Ok(result), Ok(result) => Ok(result),
Err(_) => Err(Error::Bookmark), // @TODO Err(_) => Err(Error::Bookmark), // @TODO

View file

@ -6,7 +6,7 @@ mod reload;
mod request; mod request;
mod widget; mod widget;
use super::{BrowserAction, ItemAction, Profile, TabAction, WindowAction}; use super::{ItemAction, Profile, TabAction, WindowAction};
use bookmark::Bookmark; use bookmark::Bookmark;
use gtk::{Box, Button}; use gtk::{Box, Button};
use history::History; use history::History;
@ -29,8 +29,7 @@ pub struct Navigation {
impl Navigation { impl Navigation {
pub fn build( pub fn build(
profile: &Rc<Profile>, profile: &Rc<Profile>,
(browser_action, window_action, tab_action, item_action): ( (window_action, tab_action, item_action): (
&Rc<BrowserAction>,
&Rc<WindowAction>, &Rc<WindowAction>,
&Rc<TabAction>, &Rc<TabAction>,
&Rc<ItemAction>, &Rc<ItemAction>,
@ -39,7 +38,7 @@ impl Navigation {
// init children components // init children components
let history = Box::history((window_action, tab_action, item_action)); let history = Box::history((window_action, tab_action, item_action));
let request = Rc::new(Request::build((browser_action, item_action))); let request = Rc::new(Request::build(item_action, profile));
let reload = Button::reload((window_action, tab_action, item_action), &request); let reload = Button::reload((window_action, tab_action, item_action), &request);
let home = Button::home((window_action, tab_action, item_action), &request); let home = Button::home((window_action, tab_action, item_action), &request);
let bookmark = Button::bookmark(window_action); let bookmark = Button::bookmark(window_action);
@ -49,7 +48,7 @@ impl Navigation {
&home, &home,
&history, &history,
&reload, &reload,
&request.widget.entry, // @TODO &request.entry, // @TODO
&bookmark, &bookmark,
)); ));
@ -73,8 +72,6 @@ impl Navigation {
// update children components // update children components
self.bookmark self.bookmark
.update(self.profile.bookmark.get(&request).is_ok()); .update(self.profile.bookmark.get(&request).is_ok());
self.request
.update(self.profile.identity.get(&request).is_some());
} }
pub fn clean( pub fn clean(

View file

@ -1,38 +1,132 @@
mod database; mod database;
mod test; mod primary_icon;
mod widget;
use widget::Widget; use primary_icon::PrimaryIcon;
use crate::app::browser::{window::tab::item::Action as ItemAction, Action as BrowserAction}; use super::{ItemAction, Profile};
use gtk::{ use gtk::{
glib::{gformat, GString, Uri, UriFlags}, glib::{gformat, GString, Uri, UriFlags},
prelude::EditableExt, prelude::{EditableExt, EntryExt, WidgetExt},
Entry, EntryIconPosition, StateFlags,
}; };
use sqlite::Transaction; use sqlite::Transaction;
use std::rc::Rc; use std::{cell::Cell, rc::Rc};
const PLACEHOLDER_TEXT: &str = "URL or search term...";
// Main
pub struct Request { pub struct Request {
pub widget: Rc<Widget>, pub entry: Entry,
} }
impl Request { impl Request {
// Constructors // Constructors
/// Build new `Self` /// Build new `Self`
pub fn build((browser_action, item_action): (&Rc<BrowserAction>, &Rc<ItemAction>)) -> Self { pub fn build(item_action: &Rc<ItemAction>, profile: &Rc<Profile>) -> Self {
Self { // Init main widget
widget: Rc::new(Widget::build((browser_action, item_action))), let entry = Entry::builder()
} .placeholder_text(PLACEHOLDER_TEXT)
.secondary_icon_tooltip_text("Go to the location")
.hexpand(true)
.build();
// Connect events
entry.connect_icon_release({
let item_action = item_action.clone();
move |this, position| match position {
EntryIconPosition::Primary => item_action.ident.activate(), // @TODO PrimaryIcon impl
EntryIconPosition::Secondary => item_action.load.activate(Some(&this.text()), true),
_ => todo!(), // unexpected
}
});
entry.connect_has_focus_notify(|this| {
if this.focus_child().is_some_and(|text| text.has_focus()) {
this.set_secondary_icon_name(Some("pan-end-symbolic"));
} else {
this.set_secondary_icon_name(None);
this.select_region(0, 0);
}
});
entry.connect_changed({
let profile = profile.clone();
let item_action = item_action.clone();
move |this| {
// Update actions
item_action.reload.set_enabled(!this.text().is_empty());
item_action
.home
.set_enabled(home(uri(&this.text())).is_some());
// Update primary icon
this.first_child().unwrap().remove_css_class("success"); // @TODO handle
this.set_primary_icon_activatable(false);
this.set_primary_icon_sensitive(false);
match primary_icon::from(&this.text()) {
PrimaryIcon::Download { name, tooltip } => {
this.set_primary_icon_name(Some(name));
this.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Gemini { name, tooltip }
| PrimaryIcon::Titan { name, tooltip } => {
this.set_primary_icon_activatable(true);
this.set_primary_icon_sensitive(true);
this.set_primary_icon_name(Some(name));
if profile.identity.get(&strip_prefix(this.text())).is_some() {
this.first_child().unwrap().add_css_class("success"); // @TODO handle
this.set_primary_icon_tooltip_text(Some(tooltip.1));
} else {
this.set_primary_icon_tooltip_text(Some(tooltip.0));
}
}
PrimaryIcon::Search { name, tooltip } => {
this.set_primary_icon_name(Some(name));
this.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Source { name, tooltip } => {
this.set_primary_icon_name(Some(name));
this.set_primary_icon_tooltip_text(Some(tooltip));
}
}
}
});
entry.connect_activate({
let item_action = item_action.clone();
move |entry| {
item_action.load.activate(Some(&entry.text()), true);
}
});
entry.connect_state_flags_changed({
// Define last focus state container
let has_focus = Cell::new(false);
move |entry, state| {
// Select entire text on first click (release)
// this behavior implemented in most web-browsers,
// to simply overwrite current request with new value
// Note:
// * Custom GestureClick is not an option here, as GTK Entry has default controller
// * This is experimental feature does not follow native GTK behavior @TODO make optional
if !has_focus.take()
&& state.contains(StateFlags::ACTIVE | StateFlags::FOCUS_WITHIN)
&& entry.selection_bounds().is_none()
{
entry.select_region(0, entry.text_length().into());
}
// Update last focus state
has_focus.replace(state.contains(StateFlags::FOCUS_WITHIN));
}
});
// Return activated `Self`
Self { entry }
} }
// Actions // Actions
pub fn update(&self, is_identity_active: bool) {
self.widget.update(is_identity_active);
}
pub fn clean( pub fn clean(
&self, &self,
transaction: &Transaction, transaction: &Transaction,
@ -44,7 +138,7 @@ impl Request {
match database::delete(transaction, &record.id) { match database::delete(transaction, &record.id) {
Ok(_) => { Ok(_) => {
// Delegate clean action to the item childs // Delegate clean action to the item childs
self.widget.clean(transaction, &record.id)?; // nothing yet..
} }
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
} }
@ -64,8 +158,12 @@ impl Request {
match database::select(transaction, app_browser_window_tab_item_page_navigation_id) { match database::select(transaction, app_browser_window_tab_item_page_navigation_id) {
Ok(records) => { Ok(records) => {
for record in records { for record in records {
if let Some(text) = record.text {
self.entry.set_text(&text);
}
// Delegate restore action to the item childs // Delegate restore action to the item childs
self.widget.restore(transaction, &record.id)?; // nothing yet..
} }
} }
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
@ -79,12 +177,22 @@ impl Request {
transaction: &Transaction, transaction: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64, app_browser_window_tab_item_page_navigation_id: &i64,
) -> Result<(), String> { ) -> Result<(), String> {
match database::insert(transaction, app_browser_window_tab_item_page_navigation_id) { // Keep value in memory until operation complete
let text = self.entry.text();
match database::insert(
transaction,
app_browser_window_tab_item_page_navigation_id,
match text.is_empty() {
true => None,
false => Some(text.as_str()),
},
) {
Ok(_) => { Ok(_) => {
let id = database::last_insert_id(transaction); // let id = database::last_insert_id(transaction);
// Delegate save action to childs // Delegate save action to childs
self.widget.save(transaction, &id)?; // nothing yet..
} }
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
} }
@ -95,48 +203,19 @@ impl Request {
// Setters // Setters
pub fn to_download(&self) { pub fn to_download(&self) {
self.widget.entry.set_text(&self.download()); self.entry.set_text(&self.download());
} }
pub fn to_source(&self) { pub fn to_source(&self) {
self.widget.entry.set_text(&self.source()); self.entry.set_text(&self.source());
} }
// Getters // Getters
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// * `strip_prefix` on parse
pub fn uri(&self) -> Option<Uri> {
match Uri::parse(&strip_prefix(self.widget.entry.text()), UriFlags::NONE) {
Ok(uri) => Some(uri),
_ => None,
}
}
/// Get current request value without system prefix /// Get current request value without system prefix
/// * the `prefix` is not `scheme` /// * the `prefix` is not `scheme`
pub fn strip_prefix(&self) -> GString { pub fn strip_prefix(&self) -> GString {
strip_prefix(self.widget.entry.text()) strip_prefix(self.entry.text())
}
/// Parse home [Uri](https://docs.gtk.org/glib/struct.Uri.html) of `Self`
pub fn home(&self) -> Option<Uri> {
self.uri().map(|uri| {
Uri::build(
UriFlags::NONE,
&if uri.scheme() == "titan" {
GString::from("gemini")
} else {
uri.scheme()
},
uri.userinfo().as_deref(),
uri.host().as_deref(),
uri.port(),
"/",
None,
None,
)
})
} }
/// Get request value in `download:` format /// Get request value in `download:` format
@ -148,13 +227,37 @@ impl Request {
pub fn source(&self) -> GString { pub fn source(&self) -> GString {
gformat!("source:{}", self.strip_prefix()) gformat!("source:{}", self.strip_prefix())
} }
/// Try get current request value as [Uri](https://docs.gtk.org/glib/struct.Uri.html)
/// * `strip_prefix` on parse
pub fn uri(&self) -> Option<Uri> {
uri(&strip_prefix(self.entry.text()))
}
/// Try build home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
pub fn home(&self) -> Option<Uri> {
home(self.uri())
}
} }
// Tools // Tools
pub fn migrate(tx: &Transaction) -> Result<(), String> {
// Migrate self components
if let Err(e) = database::init(tx) {
return Err(e.to_string());
}
// Delegate migration to childs
// nothing yet..
// Success
Ok(())
}
/// Strip system prefix from request string /// Strip system prefix from request string
/// * the `prefix` is not `scheme` /// * the `prefix` is not `scheme`
pub fn strip_prefix(mut request: GString) -> GString { fn strip_prefix(mut request: GString) -> GString {
if let Some(postfix) = request.strip_prefix("source:") { if let Some(postfix) = request.strip_prefix("source:") {
request = postfix.into() request = postfix.into()
}; };
@ -166,15 +269,29 @@ pub fn strip_prefix(mut request: GString) -> GString {
request request
} // @TODO move prefix features to page client } // @TODO move prefix features to page client
pub fn migrate(tx: &Transaction) -> Result<(), String> { fn uri(value: &str) -> Option<Uri> {
// Migrate self components match Uri::parse(value, UriFlags::NONE) {
if let Err(e) = database::init(tx) { Ok(uri) => Some(uri),
return Err(e.to_string()); _ => None,
} }
}
// Delegate migration to childs
widget::migrate(tx)?; /// Parse home [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `subject`
fn home(subject: Option<Uri>) -> Option<Uri> {
// Success subject.map(|uri| {
Ok(()) Uri::build(
UriFlags::NONE,
&if uri.scheme() == "titan" {
GString::from("gemini")
} else {
uri.scheme()
},
uri.userinfo().as_deref(),
uri.host().as_deref(),
uri.port(),
"/",
None,
None,
)
})
} }

View file

@ -3,6 +3,7 @@ use sqlite::{Error, Transaction};
pub struct Table { pub struct Table {
pub id: i64, pub id: i64,
// pub app_browser_window_tab_item_page_navigation_id: i64, not in use // pub app_browser_window_tab_item_page_navigation_id: i64, not in use
pub text: Option<String>, // can be stored as NULL
} }
pub fn init(tx: &Transaction) -> Result<usize, Error> { pub fn init(tx: &Transaction) -> Result<usize, Error> {
@ -11,6 +12,7 @@ pub fn init(tx: &Transaction) -> Result<usize, Error> {
( (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`app_browser_window_tab_item_page_navigation_id` INTEGER NOT NULL, `app_browser_window_tab_item_page_navigation_id` INTEGER NOT NULL,
`text` VARCHAR(1024),
FOREIGN KEY (`app_browser_window_tab_item_page_navigation_id`) REFERENCES `app_browser_window_tab_item_page_navigation`(`id`) FOREIGN KEY (`app_browser_window_tab_item_page_navigation_id`) REFERENCES `app_browser_window_tab_item_page_navigation`(`id`)
)", )",
@ -21,12 +23,14 @@ pub fn init(tx: &Transaction) -> Result<usize, Error> {
pub fn insert( pub fn insert(
tx: &Transaction, tx: &Transaction,
app_browser_window_tab_item_page_navigation_id: &i64, app_browser_window_tab_item_page_navigation_id: &i64,
text: Option<&str>,
) -> Result<usize, Error> { ) -> Result<usize, Error> {
tx.execute( tx.execute(
"INSERT INTO `app_browser_window_tab_item_page_navigation_request` ( "INSERT INTO `app_browser_window_tab_item_page_navigation_request` (
`app_browser_window_tab_item_page_navigation_id` `app_browser_window_tab_item_page_navigation_id`,
) VALUES (?)", `text`
[app_browser_window_tab_item_page_navigation_id], ) VALUES (?, ?)",
(app_browser_window_tab_item_page_navigation_id, text),
) )
} }
@ -36,7 +40,8 @@ pub fn select(
) -> Result<Vec<Table>, Error> { ) -> Result<Vec<Table>, Error> {
let mut stmt = tx.prepare( let mut stmt = tx.prepare(
"SELECT `id`, "SELECT `id`,
`app_browser_window_tab_item_page_navigation_id` `app_browser_window_tab_item_page_navigation_id`,
`text`
FROM `app_browser_window_tab_item_page_navigation_request` FROM `app_browser_window_tab_item_page_navigation_request`
WHERE `app_browser_window_tab_item_page_navigation_id` = ?", WHERE `app_browser_window_tab_item_page_navigation_id` = ?",
)?; )?;
@ -45,6 +50,7 @@ pub fn select(
Ok(Table { Ok(Table {
id: row.get(0)?, id: row.get(0)?,
// app_browser_window_tab_item_page_navigation_id: row.get(1)?, not in use // app_browser_window_tab_item_page_navigation_id: row.get(1)?, not in use
text: row.get(2)?,
}) })
})?; })?;
@ -65,6 +71,7 @@ pub fn delete(tx: &Transaction, id: &i64) -> Result<usize, Error> {
) )
} }
/* not in use
pub fn last_insert_id(tx: &Transaction) -> i64 { pub fn last_insert_id(tx: &Transaction) -> i64 {
tx.last_insert_rowid() tx.last_insert_rowid()
} } */

View file

@ -1,5 +0,0 @@
#[test]
fn strip_prefix() {
assert_eq!(super::strip_prefix("source:gemini".into()), "gemini");
assert_eq!(super::strip_prefix("download:gemini".into()), "gemini");
}

View file

@ -1,286 +0,0 @@
mod database;
mod primary_icon;
use primary_icon::PrimaryIcon;
use super::{BrowserAction, ItemAction};
use gtk::{
glib::{timeout_add_local, ControlFlow, SourceId},
prelude::{EditableExt, EntryExt, WidgetExt},
Entry, EntryIconPosition, StateFlags,
};
use sqlite::Transaction;
use std::{
cell::{Cell, RefCell},
rc::Rc,
time::Duration,
};
const PLACEHOLDER_TEXT: &str = "URL or search term...";
// Progress bar animation setup
const PROGRESS_ANIMATION_STEP: f64 = 0.05;
const PROGRESS_ANIMATION_TIME: u64 = 20; //ms
struct Progress {
fraction: RefCell<f64>,
source_id: RefCell<Option<SourceId>>,
}
pub struct Widget {
pub entry: Entry,
progress: Rc<Progress>,
}
impl Widget {
// Constructors
/// Build new `Self`
pub fn build((browser_action, item_action): (&Rc<BrowserAction>, &Rc<ItemAction>)) -> Self {
// Init animated progress bar state
let progress = Rc::new(Progress {
fraction: RefCell::new(0.0),
source_id: RefCell::new(None),
});
// Init main widget
let entry = Entry::builder()
.placeholder_text(PLACEHOLDER_TEXT)
.secondary_icon_tooltip_text("Go to the location")
.hexpand(true)
.build();
// Connect events
entry.connect_icon_release({
let item_action = item_action.clone();
move |this, position| match position {
EntryIconPosition::Primary => item_action.ident.activate(), // @TODO PrimaryIcon impl
EntryIconPosition::Secondary => item_action.load.activate(Some(&this.text()), true),
_ => todo!(), // unexpected
}
});
entry.connect_has_focus_notify(|this| {
if this.focus_child().is_some_and(|text| text.has_focus()) {
this.set_secondary_icon_name(Some("pan-end-symbolic"));
} else {
this.set_secondary_icon_name(None);
this.select_region(0, 0);
}
});
entry.connect_changed({
let browser_action = browser_action.clone();
move |_| {
browser_action.update.activate(None);
}
});
entry.connect_activate({
let item_action = item_action.clone();
move |entry| {
item_action.load.activate(Some(&entry.text()), true);
}
});
entry.connect_state_flags_changed({
// Define last focus state container
let has_focus = Cell::new(false);
move |entry, state| {
// Select entire text on first click (release)
// this behavior implemented in most web-browsers,
// to simply overwrite current request with new value
// Note:
// * Custom GestureClick is not an option here, as GTK Entry has default controller
// * This is experimental feature does not follow native GTK behavior @TODO make optional
if !has_focus.take()
&& state.contains(StateFlags::ACTIVE | StateFlags::FOCUS_WITHIN)
&& entry.selection_bounds().is_none()
{
entry.select_region(0, entry.text_length().into());
}
// Update last focus state
has_focus.replace(state.contains(StateFlags::FOCUS_WITHIN));
}
});
// Return activated `Self`
Self { entry, progress }
}
// Actions
pub fn clean(
&self,
transaction: &Transaction,
app_browser_window_tab_item_page_navigation_request_id: &i64,
) -> Result<(), String> {
match database::select(
transaction,
app_browser_window_tab_item_page_navigation_request_id,
) {
Ok(records) => {
for record in records {
match database::delete(transaction, &record.id) {
Ok(_) => {
// Delegate clean action to the item childs
// nothing yet..
}
Err(e) => return Err(e.to_string()),
}
}
}
Err(e) => return Err(e.to_string()),
}
Ok(())
}
pub fn restore(
&self,
transaction: &Transaction,
app_browser_window_tab_item_page_navigation_request_id: &i64,
) -> Result<(), String> {
match database::select(
transaction,
app_browser_window_tab_item_page_navigation_request_id,
) {
Ok(records) => {
for record in records {
if let Some(text) = record.text {
self.entry.set_text(&text);
}
// Delegate restore action to the item childs
// nothing yet..
}
}
Err(e) => return Err(e.to_string()),
}
Ok(())
}
pub fn save(
&self,
transaction: &Transaction,
app_browser_window_tab_item_page_navigation_request_id: &i64,
) -> Result<(), String> {
// Keep value in memory until operation complete
let text = self.entry.text();
match database::insert(
transaction,
app_browser_window_tab_item_page_navigation_request_id,
match text.is_empty() {
true => None,
false => Some(text.as_str()),
},
) {
Ok(_) => {
// let id = database::last_insert_id(transaction);
// Delegate save action to childs
// nothing yet..
}
Err(e) => return Err(e.to_string()),
}
Ok(())
}
pub fn update(&self, is_identity_active: bool) {
// Update primary icon
self.entry
.first_child()
.unwrap()
.remove_css_class("success"); // @TODO handle
self.entry.set_primary_icon_activatable(false);
self.entry.set_primary_icon_sensitive(false);
match primary_icon::from(&self.entry.text()) {
PrimaryIcon::Download { name, tooltip } => {
self.entry.set_primary_icon_name(Some(name));
self.entry.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Gemini { name, tooltip } | PrimaryIcon::Titan { name, tooltip } => {
self.entry.set_primary_icon_activatable(true);
self.entry.set_primary_icon_sensitive(true);
self.entry.set_primary_icon_name(Some(name));
if is_identity_active {
self.entry.first_child().unwrap().add_css_class("success"); // @TODO handle
self.entry.set_primary_icon_tooltip_text(Some(tooltip.1));
} else {
self.entry.set_primary_icon_tooltip_text(Some(tooltip.0));
}
}
PrimaryIcon::Search { name, tooltip } => {
self.entry.set_primary_icon_name(Some(name));
self.entry.set_primary_icon_tooltip_text(Some(tooltip));
}
PrimaryIcon::Source { name, tooltip } => {
self.entry.set_primary_icon_name(Some(name));
self.entry.set_primary_icon_tooltip_text(Some(tooltip));
}
}
// Update progress
// * @TODO skip update animation for None value
let value = self.entry.progress_fraction();
// Update shared fraction on new value was changed
if value != self.progress.fraction.replace(value) {
// Start new frame on previous process function completed (`source_id` changed to None)
// If previous process still active, we have just updated shared fraction value before, to use it inside the active process
if self.progress.source_id.borrow().is_none() {
// Start new animation frame iterator, update `source_id`
self.progress.source_id.replace(Some(timeout_add_local(
Duration::from_millis(PROGRESS_ANIMATION_TIME),
{
// Clone async pointers dependency
let entry = self.entry.clone();
let progress = self.progress.clone();
// Frame
move || {
// Animate
if *progress.fraction.borrow() > entry.progress_fraction() {
entry.set_progress_fraction(
// Currently, here is no outrange validation, seems that wrapper make this work @TODO
entry.progress_fraction() + PROGRESS_ANIMATION_STEP,
);
return ControlFlow::Continue;
}
// Deactivate
progress.source_id.replace(None);
// Reset on 100% (to hide progress bar)
// or, just await for new value request
if entry.progress_fraction() == 1.0 {
entry.set_progress_fraction(0.0);
}
// Stop iteration
ControlFlow::Break
}
},
)));
}
}
}
}
// Tools
pub fn migrate(tx: &Transaction) -> Result<(), String> {
// Migrate self components
if let Err(e) = database::init(tx) {
return Err(e.to_string());
}
// Delegate migration to childs
// nothing yet..
// Success
Ok(())
}

View file

@ -1,80 +0,0 @@
use sqlite::{Error, Transaction};
pub struct Table {
pub id: i64,
// pub app_browser_window_tab_item_page_navigation_request_id: i64, not in use
pub text: Option<String>, // can be stored as NULL
}
pub fn init(tx: &Transaction) -> Result<usize, Error> {
tx.execute(
"CREATE TABLE IF NOT EXISTS `app_browser_window_tab_item_page_navigation_request_widget`
(
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`app_browser_window_tab_item_page_navigation_request_id` INTEGER NOT NULL,
`text` VARCHAR(1024),
FOREIGN KEY (`app_browser_window_tab_item_page_navigation_request_id`) REFERENCES `app_browser_window_tab_item_page_navigation_request`(`id`)
)",
[],
)
}
pub fn insert(
tx: &Transaction,
app_browser_window_tab_item_page_navigation_request_id: &i64,
text: Option<&str>,
) -> Result<usize, Error> {
tx.execute(
"INSERT INTO `app_browser_window_tab_item_page_navigation_request_widget` (
`app_browser_window_tab_item_page_navigation_request_id`,
`text`
) VALUES (?, ?)",
(app_browser_window_tab_item_page_navigation_request_id, text),
)
}
pub fn select(
tx: &Transaction,
app_browser_window_tab_item_page_navigation_request_id: &i64,
) -> Result<Vec<Table>, Error> {
let mut stmt = tx.prepare(
"SELECT `id`,
`app_browser_window_tab_item_page_navigation_request_id`,
`text`
FROM `app_browser_window_tab_item_page_navigation_request_widget`
WHERE `app_browser_window_tab_item_page_navigation_request_id` = ?",
)?;
let result = stmt.query_map(
[app_browser_window_tab_item_page_navigation_request_id],
|row| {
Ok(Table {
id: row.get(0)?,
// app_browser_window_tab_item_page_navigation_request_id: row.get(1)?, not in use
text: row.get(2)?,
})
},
)?;
let mut records = Vec::new();
for record in result {
let table = record?;
records.push(table);
}
Ok(records)
}
pub fn delete(tx: &Transaction, id: &i64) -> Result<usize, Error> {
tx.execute(
"DELETE FROM `app_browser_window_tab_item_page_navigation_request_widget` WHERE `id` = ?",
[id],
)
}
/* not in use
pub fn last_insert_id(tx: &Transaction) -> i64 {
tx.last_insert_rowid()
} */