Skip to content

Commit 68e7182

Browse files
authored
feat(leaves): add Paginator component (#37)
* WIP * Paginator: view * Paginator working * Paginator demo * Paginator demo update * Paginator demo update again * Paginator: mli file done * Paginator: fixed the formatting type ;) * Paginator: finish docs
1 parent aee9633 commit 68e7182

File tree

6 files changed

+254
-0
lines changed

6 files changed

+254
-0
lines changed

examples/paginator/demo.gif

101 KB
Loading

examples/paginator/demo.tape

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Output demo.gif
2+
3+
Require echo
4+
5+
Set Shell "bash"
6+
Set Framerate 24
7+
Set FontSize 32
8+
Set Width 1200
9+
Set Height 600
10+
11+
Type "dune exec --no-print-directory ./main.exe"
12+
Enter
13+
Sleep 1s
14+
Right
15+
Sleep 1s
16+
Right
17+
Sleep 1s
18+
Right
19+
Sleep 1s
20+
Left
21+
Sleep 1s
22+
Left
23+
Sleep 1s
24+
Left
25+
Sleep 1s
26+
Type @0.s "q"

examples/paginator/dune

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(executable
2+
(name main)
3+
(libraries minttea spices leaves))

examples/paginator/main.ml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
open Minttea
2+
open Leaves
3+
4+
type model = { paginator : Paginator.t; items : string list }
5+
6+
let items = List.init 9 (fun x -> "Item " ^ string_of_int (x + 1))
7+
let paginator = Paginator.make ~per_page:3 ~style:Paginator.Dots ()
8+
let paginator, _ = Paginator.set_total_pages paginator 9
9+
let initial_model = { paginator; items }
10+
let init _ = Command.Hide_cursor
11+
12+
let update (event : Event.t) model =
13+
match event with
14+
| Event.KeyDown (Key "q") -> (model, Command.Quit)
15+
| _ ->
16+
( { model with paginator = Paginator.update model.paginator event },
17+
Command.Noop )
18+
19+
let view model =
20+
let start, end_pos = Paginator.get_slice_bounds model.paginator 9 in
21+
let x = List.to_seq model.items in
22+
let y = Seq.drop start x in
23+
let z = Seq.take (end_pos - start) y in
24+
"\n Look! We have 9 items!\n\n "
25+
^ String.concat "\n " (List.of_seq z)
26+
^ Format.sprintf "\n %s\n" (Paginator.view model.paginator)
27+
^ "\n h/l ←/→ page • q: quit\n"
28+
29+
let app = Minttea.app ~init ~update ~view ()
30+
let () = Minttea.start ~initial_model app

leaves/paginator.ml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
type style = Dots | Numerals
2+
3+
type t = {
4+
style : style;
5+
page : int;
6+
per_page : int;
7+
total_pages : int;
8+
active_dot : string;
9+
inactive_dot : string;
10+
numerals_format : (int -> int -> string, unit, string) format;
11+
text_style : Spices.style;
12+
}
13+
14+
let set_total_pages t items =
15+
if items < 1 then (t, t.total_pages)
16+
else
17+
let n = items / t.per_page in
18+
let n = if items mod t.per_page > 0 then n + 1 else n in
19+
let new_t = { t with total_pages = n } in
20+
(new_t, n)
21+
22+
let get_slice_bounds t length =
23+
let start = t.page * t.per_page in
24+
let end_pos = min ((t.page * t.per_page) + t.per_page) length in
25+
(start, end_pos)
26+
27+
let items_on_page t total_items =
28+
if total_items < 1 then 0
29+
else
30+
let start, end_pos = get_slice_bounds t total_items in
31+
end_pos - start
32+
33+
let on_last_page t = t.page = t.total_pages - 1
34+
let on_first_page t = t.page = 0
35+
let prev_page t = { t with page = max (t.page - 1) 0 }
36+
let next_page t = if on_last_page t then t else { t with page = t.page + 1 }
37+
38+
let make ?(style = Numerals) ?(page = 0) ?(per_page = 1) ?(total_pages = 1)
39+
?(active_dot = "") ?(inactive_dot = "")
40+
?(numerals_format : (int -> int -> string, unit, string) format = "%d/%d")
41+
?(text_style = Spices.default) () =
42+
{
43+
style;
44+
page;
45+
per_page;
46+
total_pages;
47+
active_dot;
48+
inactive_dot;
49+
numerals_format;
50+
text_style;
51+
}
52+
53+
let update t (e : Minttea.Event.t) =
54+
match e with
55+
| KeyDown (Key "h" | Left) -> prev_page t
56+
| KeyDown (Key "l" | Right) -> next_page t
57+
| _ -> t
58+
59+
let dots_view t text_style =
60+
let text_style = text_style |> Spices.build in
61+
let result = ref "" in
62+
for i = 0 to t.total_pages - 1 do
63+
let dot =
64+
if i == t.page then text_style "%s" t.active_dot
65+
else text_style "%s" t.inactive_dot
66+
in
67+
result := !result ^ dot
68+
done;
69+
!result
70+
71+
let numerals_view t text_style =
72+
let text_style = text_style |> Spices.build in
73+
let txt = Format.sprintf t.numerals_format (t.page + 1) t.total_pages in
74+
text_style "%s" txt
75+
76+
let view t =
77+
match t.style with
78+
| Dots -> dots_view t t.text_style
79+
| Numerals -> numerals_view t t.text_style

leaves/paginator.mli

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
type style = Dots | Numerals
2+
type t
3+
4+
val set_total_pages : t -> int -> t * int
5+
(** [set_total_pages paginator total] calculates the total number of pages
6+
from a given number of items. Its use is optional since this pager can be
7+
used for other things beyond navigating sets. Note that it both returns the
8+
number of total pages and alters the model.
9+
10+
{@ocaml[
11+
let paginator = Paginator.make () in
12+
let total_pages, paginator = Paginator.set_total_pages paginator 3
13+
]}
14+
*)
15+
16+
val get_slice_bounds : t -> int -> int * int
17+
(** [get_slice_bounds paginator item_count] returns the start and end bounds of slice you're
18+
rendering for the current page based on total number of items
19+
20+
{@ocaml[
21+
let bunch_of_stuff = List.to_seq [...] in
22+
let start, end_pos = Paginator.get_slice_bounds (Seq.length bunch_of_stuff) in
23+
let slice_to_render = Seq.take (end_pos - start) @@ Seq.drop start bunch_of_stuff
24+
]}
25+
*)
26+
27+
val items_on_page : t -> int -> int
28+
(** [items_on_page paginator items_count] returns the number of items on the
29+
current page given the total number of items passed as an argument.
30+
31+
{@ocaml[
32+
let paginator = Paginator.make () in
33+
let item_count = Paginator.items_on_page paginator 10
34+
]}
35+
*)
36+
37+
val on_last_page : t -> bool
38+
(** [on_last_page paginator] returns whether or not we're on the last page.
39+
40+
{@ocaml[
41+
let paginator = Paginator.make () in
42+
Paginator.on_last_page paginator
43+
]}
44+
*)
45+
46+
val on_first_page : t -> bool
47+
(** [on_first_page paginator] returns whether or not we're on the first page.
48+
49+
{@ocaml[
50+
let paginator = Paginator.make () in
51+
Paginator.on_first_page paginator
52+
]}
53+
*)
54+
55+
val prev_page : t -> t
56+
(** [prev_page paginator] navigates one page backward with a lower bound of 0.
57+
58+
{@ocaml[
59+
let paginator = Paginator.make () in
60+
let paginator = Paginator.prev_page paginator
61+
]}
62+
*)
63+
64+
val next_page : t -> t
65+
(** [next_page paginator] navigates one page forward with an upper bound of total_pages - 1.
66+
67+
{@ocaml[
68+
let paginator = Paginator.make () in
69+
let paginator = Paginator.next_page paginator
70+
]}
71+
*)
72+
73+
val make :
74+
?style:style ->
75+
?page:int ->
76+
?per_page:int ->
77+
?total_pages:int ->
78+
?active_dot:string ->
79+
?inactive_dot:string ->
80+
?numerals_format:(int -> int -> string, unit, string) format ->
81+
?text_style:Spices.style ->
82+
unit ->
83+
t
84+
(** [make ()] creates a new {Paginator.t}
85+
86+
A different start page can be specified using `page`.
87+
88+
For helper functions like `get_slice_bounds` to work correctly, the number of items to be rendered on each page can be passed to `per_page`.
89+
90+
By default, Paginator uses Numerals format, displaying page 1 of 3 as "1/3".
91+
The format for numerals can be changed using `numerals_format` to any format that takes two integers, e.g. "%d of %d" rendering "1 of 3" instead.
92+
Alternatively, `style` can be specified as `Paginator.Dots` to display pages using characters instead with the `active_dot` "•" and inactive_dot "○" defaults, which can be specified/overriden.
93+
94+
By default, Spices.default is used, which can be overriden using `text_style`.
95+
96+
{@ocaml[
97+
let paginator = Paginator.make ()
98+
]}
99+
*)
100+
101+
val update : t -> Minttea.Event.t -> t
102+
(** [update paginator event] is the Tea update function which binds keystrokes to pagination.
103+
104+
{@ocaml[
105+
let paginator = Paginator.update paginator event
106+
]}
107+
*)
108+
109+
val view : t -> string
110+
(** [view paginator] renders the pagination to a string.
111+
112+
{@ocaml[
113+
let paginator = Paginator.make () in
114+
let pagination_string = Paginator.view paginator
115+
]}
116+
*)

0 commit comments

Comments
 (0)