Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(leaves): add placeholder and smart cursor blinking to text-input #47

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,24 +121,24 @@ possibility of using custom events.
let update event model =
match event with
(* if we press `q` or the escape key, we exit *)
| Event.KeyDown (Key "q" | Escape) -> (model, Command.Quit)
| Event.KeyDown ((Key "q" | Escape), _modifier) -> (model, Command.Quit)
(* if we press up or `k`, we move up in the list *)
| Event.KeyDown (Up | Key "k") ->
| Event.KeyDown ((Up | Key "k"), _modifier) ->
let cursor =
if model.cursor = 0 then List.length model.choices - 1
else model.cursor - 1
in
({ model with cursor }, Command.Noop)
(* if we press down or `j`, we move down in the list *)
| Event.KeyDown (Down | Key "j") ->
| Event.KeyDown ((Down | Key "j"), _modifier) ->
let cursor =
if model.cursor = List.length model.choices - 1 then 0
else model.cursor + 1
in
({ model with cursor }, Command.Noop)
(* when we press enter or space we toggle the item in the list
that the cursor points to *)
| Event.KeyDown (Enter | Space) ->
| Event.KeyDown ((Enter | Space), _modifier) ->
let toggle status =
match status with `selected -> `unselected | `unselected -> `selected
in
Expand Down
Binary file modified examples/text-input/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion examples/text-input/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ let white = Spices.color "#FFFFFF"
let cursor =
Cursor.make ~style:Spices.(default |> bg mint |> fg white |> bold true) ()

let initial_model = { text = Text_input.make "" ~cursor (); quitting = false }
let initial_model =
{
text = Text_input.make "" ~placeholder:"Type something" ~cursor ();
quitting = false;
}

let init _ = Command.Hide_cursor

let update (event : Event.t) model =
Expand Down
2 changes: 2 additions & 0 deletions leaves/cursor.ml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ let view t ~text_style str =

let focus t = { t with focus = true; show = true }
let unfocus t = { t with focus = false }
let disable_blink t = { t with blink = false; show = true }
let enable_blink t = { t with blink = true; show = true }
6 changes: 6 additions & 0 deletions leaves/cursor.mli
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ val focus : t -> t

val unfocus : t -> t
(** [unfocus t] hides the cursor [t]. *)

val enable_blink : t -> t
(** [enable_blink t] enables blinking for the cursor [t]. *)

val disable_blink : t -> t
(** [disable_blink t] disables blinking for the cursor [t]. *)
115 changes: 86 additions & 29 deletions leaves/text_input.ml
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
type t = {
value : string;
pos : int;
cursor : Cursor.t;
placeholder : string;
prompt : string;
last_action : float;
(* cursor *)
cursor : Cursor.t;
pos : int;
(* styles *)
text_style : Spices.style;
placeholder_style : Spices.style;
}

(* Utils *)
open struct
let cursor_at_beginning t = t.pos = 0
let cursor_at_end t = t.pos = String.length t.value
let now_secs () = Ptime_clock.now () |> Ptime.to_float_s

(** Cursor will be at the start of the right part. *)
let split s ~at =
Expand All @@ -19,36 +25,69 @@ open struct
end

let default_prompt = "> "
let default_placeholder = ""
let default_text_style = Spices.default
let default_placeholder_style = Spices.default |> Spices.faint true
let default_cursor () = Cursor.make ()
let resume_blink_after = 0.25

let make value ?(text_style = default_text_style) ?(cursor = default_cursor ())
let make value ?(text_style = default_text_style)
?(placeholder_style = default_placeholder_style)
?(cursor = default_cursor ()) ?(placeholder = default_placeholder)
?(prompt = default_prompt) () =
let v, pos =
match value with "" -> ("", 0) | v -> (value, String.length v)
let value, pos =
if String.length value = 0 then ("", 0) else (value, String.length value)
in
{ value = v; pos; text_style; cursor; prompt }
{
value;
placeholder;
pos;
text_style;
placeholder_style;
cursor;
prompt;
last_action = now_secs ();
}

let empty () = make "" ()

let view t =
let placeholder_view t =
let placeholder_style = Spices.(t.placeholder_style |> build) in
let text_style = Spices.(t.text_style |> build) in

let result = ref "" in
for i = 0 to String.length t.value - 1 do
let s = String.make 1 @@ t.value.[i] in

for i = 0 to String.length t.placeholder - 1 do
let s = String.make 1 @@ t.placeholder.[i] in
let txt =
if i = t.pos then Cursor.view t.cursor ~text_style:t.text_style s
else text_style "%s" s
if i = 0 then Cursor.view t.cursor ~text_style:t.placeholder_style s
else placeholder_style "%s" s
in
result := !result ^ txt
done;

if cursor_at_end t then
result := !result ^ Cursor.view t.cursor ~text_style:t.text_style " ";

text_style "%s" t.prompt ^ !result

let view t =
if String.length t.value = 0 then placeholder_view t
else
let text_style = Spices.(t.text_style |> build) in

let result = ref "" in
for i = 0 to String.length t.value - 1 do
let s = String.make 1 @@ t.value.[i] in
let txt =
if i = t.pos then Cursor.view t.cursor ~text_style:t.text_style s
else text_style "%s" s
in
result := !result ^ txt
done;

if cursor_at_end t then
result := !result ^ Cursor.view t.cursor ~text_style:t.text_style " ";

text_style "%s" t.prompt ^ !result

let current_text t = t.value

let write t s =
Expand Down Expand Up @@ -91,20 +130,38 @@ let jump_to_end t = move_cursor t `Jump_to_end

let update t (e : Minttea.Event.t) =
match e with
| KeyDown (k, _) ->
{
(match k with
| Backspace -> backspace t
| Key s -> write t s
| Left -> character_backward t
| Right -> character_forward t
| Space -> space t
| Up -> jump_to_beginning t
| Down -> jump_to_end t
| Escape | Enter -> t)
with
cursor = Cursor.focus t.cursor;
}
| _ -> { t with cursor = Cursor.update t.cursor e }
| KeyDown (key, modifier) ->
let s =
match (key, modifier) with
(* Movement *)
| Up, _ -> jump_to_beginning t
| Key s, Ctrl when s = "a" -> jump_to_beginning t

| Down, _ -> jump_to_end t
| Key s, Ctrl when s = "e" -> jump_to_end t

| Left, _ -> character_backward t
| Key s, Ctrl when s = "b" -> character_backward t

| Right, _ -> character_forward t
| Key s, Ctrl when s = "f" -> character_forward t

(* Typing *)
| Backspace, _ -> backspace t
| Key s, _ -> write t s
| Space, _ -> space t
| Escape, _ | Enter, _ -> t
in

{ s with cursor = Cursor.focus t.cursor; last_action = now_secs () }
| _ ->
let time_since_last_action = now_secs () -. t.last_action in

let updated_cursor =
if time_since_last_action <= resume_blink_after then
Cursor.enable_blink t.cursor
else t.cursor
in
{ t with cursor = Cursor.update updated_cursor e }

let set_text value t = { t with value } |> jump_to_end
2 changes: 2 additions & 0 deletions leaves/text_input.mli
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ type t
val make :
string ->
?text_style:Spices.style ->
?placeholder_style:Spices.style ->
?cursor:Cursor.t ->
?placeholder:string ->
?prompt:string ->
unit ->
t
Expand Down