Skip to content

Commit

Permalink
feat: Frustration relief
Browse files Browse the repository at this point in the history
Stops the test and displays a helpful message when
frustration (random key mashing) is detected
  • Loading branch information
bragefuglseth committed Jan 11, 2025
1 parent b563f60 commit ffe00b9
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 58 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 12 additions & 20 deletions src/widgets/text_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ use adw::subclass::prelude::*;
use glib::subclass::Signal;
use gtk::glib;
use gtk::{gdk, gsk};
use std::cell::{Cell, OnceCell, RefCell};
use std::cell::{Ref, Cell, OnceCell, RefCell};
use std::sync::OnceLock;
use std::time::Instant;
use unicode_segmentation::UnicodeSegmentation;

const LINE_HEIGHT: i32 = 50;
Expand Down Expand Up @@ -61,8 +62,7 @@ mod imp {
pub(super) original_text: RefCell<String>,
pub(super) typed_text: RefCell<String>,
pub(super) previous_preedit: RefCell<String>,
pub(super) total_keystrokes: Cell<usize>,
pub(super) correct_keystrokes: Cell<usize>,
pub(super) keystrokes: RefCell<Vec<(Instant, bool)>>,
pub(super) input_context: RefCell<Option<gtk::IMMulticontext>>,
pub(super) scroll_animation: OnceCell<adw::TimedAnimation>,
pub(super) caret_x_animation: OnceCell<adw::TimedAnimation>,
Expand Down Expand Up @@ -178,15 +178,12 @@ mod imp {
}

pub(super) fn typed_text_changed(&self) {
let original = self.original_text.borrow();
let typed = self.typed_text.borrow();

let input_context = self.input_context.borrow();
let (preedit, _, _) = input_context.as_ref().unwrap().preedit_string();

let comparison = validate_with_replacements(
&original,
&typed,
&self.original_text.borrow(),
&self.typed_text.borrow(),
preedit.as_str().graphemes(true).count(),
);

Expand All @@ -196,13 +193,11 @@ mod imp {
.map(|(state, _, _, _)| *state)
.unwrap_or(GraphemeState::Unfinished);

let total_keystrokes = self.total_keystrokes.get();
self.total_keystrokes.set(total_keystrokes + 1);
let correct = last_grapheme_state != GraphemeState::Mistake;

if last_grapheme_state != GraphemeState::Mistake {
let correct_keystrokes = self.correct_keystrokes.get();
self.correct_keystrokes.set(correct_keystrokes + 1);
}
let keystroke = (Instant::now(), correct);

self.keystrokes.borrow_mut().push(keystroke);

self.update_colors(&comparison);
self.update_caret_position(!self.running.get());
Expand Down Expand Up @@ -287,10 +282,8 @@ impl KpTextView {
}

// Returns a tuple with the amount of correct keystrokes and the total amount of keystrokes
pub fn keystrokes(&self) -> (usize, usize) {
let imp = self.imp();

(imp.correct_keystrokes.get(), imp.total_keystrokes.get())
pub fn keystrokes(&self) -> Ref<Vec<(Instant, bool)>> {
self.imp().keystrokes.borrow()
}

pub fn reset(&self) {
Expand All @@ -302,7 +295,6 @@ impl KpTextView {
imp.scroll_animation().skip();
imp.caret_x_animation().skip();
imp.caret_y_animation().skip();
imp.total_keystrokes.set(0);
imp.correct_keystrokes.set(0);
imp.keystrokes.borrow_mut().clear();
}
}
54 changes: 52 additions & 2 deletions src/widgets/window.blp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ template $KpWindow: Adw.ApplicationWindow {
Button stop_button {
icon-name: "arrow-circular-top-right-symbolic";
tooltip-text: _("Restart");

clicked => $ready() swapped;
}

[session_status]
Expand Down Expand Up @@ -230,7 +232,7 @@ template $KpWindow: Adw.ApplicationWindow {

Button focus_button {
// Translators: "Entry" here is the act of entering text
label: _("Enable Entry");
label: _("_Enable Entry");
use-underline: true;

styles [
Expand Down Expand Up @@ -280,10 +282,58 @@ template $KpWindow: Adw.ApplicationWindow {
Box {
halign: center;

Button continue_button {
Button results_continue_button {
label: _("_Continue");
use-underline: true;

clicked => $ready() swapped;

styles [
"pill",
"suggested-action"
]
}

styles [
"toolbar-thick"
]
}
};
}

StackPage {
name: "frustration-relief";

child: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
// Translators: "Frustration Relief" is the name of the functionality
// that automatically cancels a test when random key mashing is detected,
// and displays a recommendation to take a break.
title: _("Frustration Relief");
};
}

content: Adw.StatusPage {
icon-name: "heart-outline-thick-symbolic";
title: _("Frustration Detected");
// Translators: Make sure to convey this as politely and sensitively as possible,
// since it is likely to be shown to people while they are in a state of
// self-disappointment / anger
description: _("You learn faster in a calm state of mind. Consider taking a break. The test has been cancelled, and will not affect your typing statistics.");
};

[bottom]
Box {
halign: center;

Button frustration_continue_button {
label: "_Continue";
use-underline: true;

clicked => $ready() swapped;

styles [
"pill",
"suggested-action"
Expand Down
7 changes: 4 additions & 3 deletions src/widgets/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ mod imp {
#[template_child]
pub results_view: TemplateChild<KpResultsView>,
#[template_child]
pub continue_button: TemplateChild<gtk::Button>,
pub results_continue_button: TemplateChild<gtk::Button>,
#[template_child]
pub frustration_continue_button: TemplateChild<gtk::Button>,

pub settings: OnceCell<gio::Settings>,

Expand Down Expand Up @@ -101,6 +103,7 @@ mod imp {

fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();

klass.install_action("win.about", None, move |window, _, _| {
window.imp().show_about_dialog();
Expand Down Expand Up @@ -143,8 +146,6 @@ mod imp {

self.setup_text_view();
self.setup_focus();
self.setup_stop_button();
self.setup_continue_button();
self.setup_ui_hiding();
self.show_cursor();
}
Expand Down
24 changes: 22 additions & 2 deletions src/widgets/window/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ use std::iter::once;
use strum::{EnumMessage, IntoEnumIterator};
use text_generation::CHUNK_GRAPHEME_COUNT;

// The lower this is, the more sensitive Keypunch is to "frustration" (random key mashing).
// If enough frustration is detected, the session will be cancelled, and a helpful
// message will be displayed.
const FRUSTRATION_THRESHOLD: usize = 3;

impl imp::KpWindow {
pub(super) fn setup_session_config(&self) {
let session_type_model: gtk::StringList = SessionType::iter()
Expand Down Expand Up @@ -139,6 +144,18 @@ impl imp::KpWindow {
}
}

let frustration_score = text_view.keystrokes().iter()
.rev()
.take_while(|(timestamp, _)| {
timestamp.elapsed().as_secs() <= FRUSTRATION_THRESHOLD as u64
})
.filter(|(_, correct)| !*correct)
.count();

if frustration_score > FRUSTRATION_THRESHOLD * 10 {
imp.frustration_relief();
}

None
}
),
Expand Down Expand Up @@ -432,7 +449,7 @@ impl imp::KpWindow {
}

pub(super) fn show_results_view(&self) {
let continue_button = self.continue_button.get();
let continue_button = self.results_continue_button.get();
let original_text = if self.session_type.get() == SessionType::Custom {
process_custom_text(&self.text_view.original_text())
} else {
Expand All @@ -454,7 +471,10 @@ impl imp::KpWindow {
let wpm = calculate_wpm(duration, &original_text, &typed_text);
results_view.set_wpm(wpm);

let (correct_keystrokes, total_keystrokes) = self.text_view.keystrokes();
let keystrokes = self.text_view.keystrokes();

let correct_keystrokes = keystrokes.iter().filter(|(_, correct)| *correct).count();
let total_keystrokes = keystrokes.len();

let accuracy = calculate_accuracy(correct_keystrokes, total_keystrokes);
results_view.set_accuracy(accuracy);
Expand Down
72 changes: 41 additions & 31 deletions src/widgets/window/ui_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,8 @@
use super::*;
use crate::application::KpApplication;

#[gtk::template_callbacks]
impl imp::KpWindow {
pub(super) fn setup_stop_button(&self) {
self.stop_button.connect_clicked(glib::clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.ready();
}
));
}

pub(super) fn setup_continue_button(&self) {
self.continue_button.connect_clicked(glib::clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.ready();
}
));
}

pub(super) fn setup_ui_hiding(&self) {
let obj = self.obj();

Expand Down Expand Up @@ -107,6 +88,7 @@ impl imp::KpWindow {
obj.add_controller(click_gesture);
}

#[template_callback]
pub(super) fn ready(&self) {
self.running.set(false);
self.text_view.set_running(false);
Expand Down Expand Up @@ -203,18 +185,8 @@ impl imp::KpWindow {
return;
}

self.running.set(false);
self.text_view.set_running(false);
self.text_view.set_accepts_input(false);
self.finish_time.set(Some(Instant::now()));
self.end_session();
self.show_results_view();
self.stop_button.set_visible(false);

self.obj()
.action_set_enabled("win.text-language-dialog", false);
self.obj().action_set_enabled("win.cancel-session", false);

self.end_existing_inhibit();

// Discord IPC
self.obj()
Expand All @@ -230,6 +202,44 @@ impl imp::KpWindow {
);
}

pub(super) fn frustration_relief(&self) {
if !self.running.get() {
return;
}

self.end_session();
self.main_stack.set_visible_child_name("frustration-relief");

// Avoid continue button being activated from a keypress immediately
let continue_button = self.frustration_continue_button.get();
self.obj().set_focus(None::<&gtk::Widget>);
glib::timeout_add_local_once(
Duration::from_millis(1000),
glib::clone!(
#[weak]
continue_button,
move || {
continue_button.grab_focus();
}
),
);

}

pub(super) fn end_session(&self) {
self.running.set(false);
self.text_view.set_running(false);
self.text_view.set_accepts_input(false);
self.finish_time.set(Some(Instant::now()));


self.obj()
.action_set_enabled("win.text-language-dialog", false);
self.obj().action_set_enabled("win.cancel-session", false);

self.end_existing_inhibit();
}

pub(super) fn hide_cursor(&self) {
let device = self
.obj()
Expand Down

0 comments on commit ffe00b9

Please sign in to comment.