Commit 9980997
22
files
+635
added
-177
deleted
@@ -195,13 +195,14 @@ cargo run -p libluna --example minimal
195
195
lub kontrolkę **języka** (`DropDown`). Przycisk **„Opcje ▾”** otwiera **popover opcji**
196
196
(`Stepper` rozmiaru ikon, `RadioGroup` sortowania, `CheckButton` ukrytych plików).
197
197
Z menu **`☰` → „O aplikacji”** otwiera się **dolny panel**
198
- (`BottomSheet` z karuzelą stron `Pager`), zamykany klikiem w tło, przyciskiem `✕` lub
198
+ (`BottomSheet` z karuzelą stron `Pager` i polem tekstowym `Entry` — klik = fokus, wpisywanie),
199
+ zamykany klikiem w tło, przyciskiem `✕` lub
199
200
przeciągnięciem w dół. W karcie **Pliki** kliknij element **okruszków** (nawigacja „w górę”),
200
201
**pole wyszukiwania** (fokus) lub **plik/katalog** (zaznaczenie); przełącznik `Lista | Siatka`
201
202
zmienia tryb `ItemView`, a **rolka/przeciąganie paska** przewija listę. Wpisany tekst w polu
202
203
**wyszukiwania filtruje** elementy; gdy nic nie pasuje, pojawia się ekran stanu (`StatusPage`)
203
204
z przyciskiem **„Wyczyść”**. Zmiana **koloru akcentu** pokazuje `Toast` z akcją **„Cofnij”**,
204
- która przywraca poprzedni akcent. Klikaj segmenty `Jasny | Ciemny | Auto`, kropki
205
+ która przywraca poprzedni akcent (klik w treść powiadomienia zamyka je bez cofania). Klikaj segmenty `Jasny | Ciemny | Auto`, kropki
205
206
**koloru akcentu**, przełączniki OPCJA A / **Pasek dolny** / **Pasek boczny** i **przeciągaj**
206
207
suwak głośności oraz krawędź paska bocznego (150–460 px). Kontrolki okna `[—][□][✕]` działają
207
208
(minimalizacja/maksymalizacja/zamknięcie przez warstwę platformy).
@@ -300,8 +301,9 @@ XDG_CONFIG_HOME="$PWD/examples" cargo run -p luna-demo --bin render -- --lang fr
300
301
`HeaderBar` (pasek tytułu: tytuł, kontrolki okna, obszar na treść), `TabBar`/`TabView`
301
302
(pasek zakładek i model stron: wybór, dodawanie, zamykanie), `BottomSheet` (dolny panel
302
303
modalny z „grabberem”, przeciąganiem i zamykaniem), `Pager` (karuzela stron z kropkami),
303
- `PathBar` (okruszki z separatorem `›`, wskaźnikiem `▾` i zwijaniem do `…`), `SearchEntry`
304
- (pole wyszukiwania: lupka, kursor, czyszczenie), `ItemView` (pliki/katalogi w siatce lub
304
+ `PathBar` (okruszki z separatorem `›`, wskaźnikiem `▾` i zwijaniem do `…`), `Entry`
305
+ (pole tekstowe: kursor, edycja, opcjonalna ikona wiodąca i czyszczenie) — z `SearchEntry`
306
+ jako jego wyszukiwarkową nakładką (lupka + czyszczenie), `ItemView` (pliki/katalogi w siatce lub
305
307
liście, z zaznaczaniem i przewijaniem) i `ScrollView` (pionowe przewijanie: rolka + „kciuk”)
306
308
oraz `Stepper`, `CheckButton` i `RadioGroup` (krokowiec, pole wyboru, wybór jednokrotny),
307
309
a także `Card` (karta z nagłówkiem), `ActionRow` (wiersz ustawień), `Toast` (powiadomienie
@@ -110,6 +110,12 @@ fn main() -> Result<(), Box<dyn Error>> {
110
110
frames.push(render_pixels(&settings, &demo, &text, size));
111
111
labels.push("3. Accent: Purple".into());
112
112
113
+ // Zamknięcie powiadomienia klikiem w jego treść (akcent zostaje).
114
+ let theme = settings.resolve(false);
115
+ let toast = demo.toast.rect(size, &theme, &text);
116
+ click!(Point::new(toast.x() + 8.0, toast.center().y));
117
+ assert!(!demo.toast.is_open(), "toast did not dismiss");
118
+
113
119
// 4. Klik pozycji „Sieć" w menu nawigacji (indeks elementu 5).
114
120
let theme = settings.resolve(false);
115
121
let c = make_nav().rows(layout!().sidebar, &theme)[5].rect.center();
@@ -174,8 +180,26 @@ fn main() -> Result<(), Box<dyn Error>> {
174
180
.expect("menu is open");
175
181
click!(about);
176
182
assert!(demo.sheet.is_open(), "bottom sheet did not open");
183
+
184
+ // Pole `Entry` w panelu: fokus i wpisanie tekstu.
185
+ let entry = ui::sheet_entry_rect(size, &settings.resolve(false));
186
+ click!(entry.center());
187
+ assert!(demo.sheet_entry.is_focused(), "entry not focused");
188
+ for ch in ['A', 'd', 'a'] {
189
+ let key = Event::Key(KeyEvent {
190
+ keysym: ch as u32,
191
+ text: Some(ch.to_string()),
192
+ state: ButtonState::Pressed,
193
+ modifiers: Modifiers::NONE,
194
+ repeat: false,
195
+ });
196
+ let theme = settings.resolve(false);
197
+ ui::handle_events(&mut settings, &[key], size, &theme, &text, &mut demo);
198
+ }
199
+ assert_eq!(demo.sheet_entry.text(), "Ada", "entry text mismatch");
200
+
177
201
frames.push(render_pixels(&settings, &demo, &text, size));
178
- labels.push("9. Bottom sheet \u{201c}About\u{201d}".into());
202
+ labels.push("9. Bottom sheet \u{201c}About\u{201d} + Entry".into());
179
203
180
204
// 10. Zamknięcie panelu (klik w tło) i rozwinięcie listy języków (DropDown + Menu).
181
205
click!(Point::new(120.0, 300.0));
@@ -289,12 +313,14 @@ fn main() -> Result<(), Box<dyn Error>> {
289
313
quality: FilterQuality::Bilinear,
290
314
..Default::default()
291
315
};
292
- let scale = Transform::from_scale(cell_w as f32 / W as f32, cell_h as f32 / H as f32);
293
316
for (i, pixels) in frames.iter().enumerate() {
294
317
let src = Pixmap::from_vec(pixels.clone(), IntSize::from_wh(W, H).unwrap()).unwrap();
295
318
let cx = (i % cols) as u32 * cell_w;
296
319
let cy = (i / cols) as u32 * cell_h;
297
- montage.draw_pixmap(cx as i32, cy as i32, src.as_ref(), &paint, scale, None);
320
+ // Skalowanie i przesunięcie w jednej transformacji (inaczej komórki lądują w rogu).
321
+ let t = Transform::from_scale(cell_w as f32 / W as f32, cell_h as f32 / H as f32)
322
+ .post_translate(cx as f32, cy as f32);
323
+ montage.draw_pixmap(0, 0, src.as_ref(), &paint, t, None);
298
324
}
299
325
montage.save_png("docs/images/demo-montage.png")?;
300
326
println!("saved docs/images/demo-montage.png");
@@ -12,9 +12,9 @@ use libluna_i18n::{tr, Locale};
12
12
use libluna_render::TextEngine;
13
13
use libluna_widgets::{
14
14
ActionRow, BottomSheet, Button, Card, CheckButton, ControlsLayout, ControlsSide, DropDown,
15
- HeaderBar, Item, ItemView, Menu, MenuEntry, NavEntry, NavList, Pager, PaintCtx, PathBar,
15
+ Entry, HeaderBar, Item, ItemView, Menu, MenuEntry, NavEntry, NavList, Pager, PaintCtx, PathBar,
16
16
RadioGroup, ScrollView, SearchEntry, Slider, StatusPage, Stepper, StepperSide, Switch, Tab,
17
- TabBar, TabView, Toast, ViewMode, ViewSwitcher, WindowControl,
17
+ TabBar, TabView, TextInput, Toast, ViewMode, ViewSwitcher, WindowControl,
18
18
};
19
19
20
20
const STATUS_H: f64 = 28.0;
@@ -76,6 +76,8 @@ pub struct Demo {
76
76
pub sheet: BottomSheet,
77
77
pub pager: Pager,
78
78
pub pager_dragging: bool,
79
+ /// Pole tekstowe w dolny panelu (demonstracja `Entry`).
80
+ pub sheet_entry: Entry,
79
81
/// Pasek ścieżki (składniki to klucze i18n) i pole wyszukiwania.
80
82
pub path: PathBar,
81
83
pub path_hover: Option<usize>,
@@ -133,6 +135,7 @@ impl Default for Demo {
133
135
sheet: BottomSheet::new(),
134
136
pager: Pager::new(3),
135
137
pager_dragging: false,
138
+ sheet_entry: Entry::new(""),
136
139
path: PathBar::new(["Home", "Documents", "Projects"]),
137
140
path_hover: None,
138
141
search: SearchEntry::new("Search"),
@@ -618,24 +621,42 @@ pub fn handle_events(
618
621
let area = demo.sheet.content_rect(size, theme);
619
622
if demo.sheet.close_at(size, theme, p) {
620
623
demo.sheet.set_open(false);
624
+ demo.sheet_entry.set_focused(false);
621
625
} else if demo.sheet.drag_zone_at(size, theme, p) {
622
626
demo.sheet.begin_drag(p.y);
623
627
} else if demo.sheet.scrim_at(size, theme, p) {
624
628
demo.sheet.set_open(false);
625
- } else if area.contains(p)
626
- && !demo.pager.click_indicator(area, theme, p)
627
- {
628
- demo.pager_dragging = true;
629
- demo.pager.begin_drag(p.x);
629
+ demo.sheet_entry.set_focused(false);
630
+ } else if area.contains(p) {
631
+ if sheet_entry_rect(size, theme).contains(p) {
632
+ demo.sheet_entry.set_focused(true);
633
+ } else if demo.pager.click_indicator(
634
+ sheet_pages_rect(size, theme),
635
+ theme,
636
+ p,
637
+ ) {
638
+ demo.sheet_entry.set_focused(false);
639
+ } else {
640
+ demo.sheet_entry.set_focused(false);
641
+ demo.pager_dragging = true;
642
+ demo.pager.begin_drag(p.x);
643
+ }
630
644
}
631
645
continue;
632
646
}
633
647
634
- // Akcja powiadomienia (nakładka) ma pierwszeństwo nad treścią.
635
- if demo.toast.is_open() && demo.toast.action_at(size, theme, text, p) {
636
- settings.set_accent(demo.prev_accent);
637
- demo.toast.set_open(false);
638
- continue;
648
+ // Powiadomienie (nakładka) ma pierwszeństwo nad treścią.
649
+ if demo.toast.is_open() {
650
+ if demo.toast.action_at(size, theme, text, p) {
651
+ settings.set_accent(demo.prev_accent);
652
+ demo.toast.set_open(false);
653
+ continue;
654
+ }
655
+ // Klik w treść powiadomienia je zamyka (akcent zostaje).
656
+ if demo.toast.dismiss_at(size, theme, text, p) {
657
+ demo.toast.set_open(false);
658
+ continue;
659
+ }
639
660
}
640
661
641
662
// Popover „Opcje” obsługuje własne kontrolki.
@@ -845,7 +866,14 @@ pub fn handle_events(
845
866
}
846
867
Event::Key(ke) if ke.state == ButtonState::Pressed => {
847
868
// Gdy pole wyszukiwania ma fokus, klawisze idą do niego.
848
- if demo.search.is_focused() && handle_search_key(&mut demo.search, ke) {
869
+ if demo.search.is_focused() && handle_text_key(&mut demo.search, ke) {
870
+ continue;
871
+ }
872
+ // Pole `Entry` w dolnym panelu ma własny fokus.
873
+ if demo.sheet.is_open()
874
+ && demo.sheet_entry.is_focused()
875
+ && handle_text_key(&mut demo.sheet_entry, ke)
876
+ {
849
877
continue;
850
878
}
851
879
match ke.keysym {
@@ -906,10 +934,10 @@ fn cycle_language() {
906
934
libluna_i18n::set_locale(Locale::new(&next));
907
935
}
908
936
909
-/// Kieruje pojedyncze zdarzenie klawiatury do pola wyszukiwania.
937
+/// Kieruje pojedyncze zdarzenie klawiatury do pola tekstowego.
910
938
///
911
939
/// Zwraca `true`, jeśli klawisz został zużyty (wtedy skróty demo są pomijane).
912
-fn handle_search_key(entry: &mut SearchEntry, ke: &KeyEvent) -> bool {
940
+fn handle_text_key<T: TextInput>(entry: &mut T, ke: &KeyEvent) -> bool {
913
941
// Tekst złożony przez platformę (obsługuje też znaki spoza ASCII).
914
942
if let Some(t) = &ke.text {
915
943
if !t.is_empty() && !t.chars().all(char::is_control) {
@@ -992,28 +1020,61 @@ pub fn build_scene(
992
1020
}
993
1021
}
994
1022
995
-/// Zawartość dolnego panelu: karuzela stron (`Pager`) z kropkami-wskaźnikami.
996
-fn draw_sheet_content(paint: &mut PaintCtx, demo: &Demo, size: Size) {
997
- let theme = *paint.theme;
998
- let content = demo.sheet.content_rect(size, &theme);
999
- let pager = &demo.pager;
1023
+/// Obszar zawartości dolnego panelu (poniżej jego nagłówka).
1024
+fn sheet_content_rect(size: Size, theme: &Theme) -> Rect {
1025
+ let m = theme.metrics.sheet;
1026
+ Rect::from_xywh(
1027
+ 0.0,
1028
+ size.height - m.height + m.header,
1029
+ size.width,
1030
+ (m.height - m.header).max(0.0),
1031
+ )
1032
+}
1000
1033
1001
- // Obszar stron (nad kropkami).
1002
- let dots_h = theme.metrics.pager.dot + theme.metrics.pager.pad;
1003
- let pages = Rect::from_xywh(
1034
+/// Prostokąt pola tekstowego w dolnym panelu (demonstracja `Entry`).
1035
+pub fn sheet_entry_rect(size: Size, theme: &Theme) -> Rect {
1036
+ let content = sheet_content_rect(size, theme);
1037
+ let pad = theme.spacing.md;
1038
+ Rect::from_xywh(
1039
+ content.x() + pad,
1040
+ content.y() + pad,
1041
+ (content.width() - 2.0 * pad).max(0.0),
1042
+ theme.metrics.entry.height,
1043
+ )
1044
+}
1045
+
1046
+/// Obszar stron karuzeli w dolnym panelu (poniżej pola tekstowego).
1047
+fn sheet_pages_rect(size: Size, theme: &Theme) -> Rect {
1048
+ let content = sheet_content_rect(size, theme);
1049
+ let pad = theme.spacing.md;
1050
+ let top = content.y() + pad + theme.metrics.entry.height + pad;
1051
+ Rect::from_xywh(
1004
1052
content.x(),
1005
- content.y(),
1053
+ top,
1006
1054
content.width(),
1007
- (content.height() - dots_h).max(0.0),
1008
- );
1055
+ (content.bottom() - top).max(0.0),
1056
+ )
1057
+}
1009
1058
1059
+/// Zawartość dolnego panelu: pole `Entry` (demonstracja) + karuzela stron (`Pager`).
1060
+fn draw_sheet_content(paint: &mut PaintCtx, demo: &Demo, size: Size) {
1061
+ let theme = *paint.theme;
1062
+
1063
+ // Pole tekstowe (`Entry`) na górze panelu.
1064
+ let mut entry = demo.sheet_entry.clone();
1065
+ entry.set_placeholder(tr("Your name"));
1066
+ entry.paint(paint, sheet_entry_rect(size, &theme));
1067
+
1068
+ // Strony karuzeli poniżej.
1069
+ let pages = sheet_pages_rect(size, &theme);
1070
+ let pager = &demo.pager;
1010
1071
paint.scene.push_clip(pages);
1011
1072
for i in 0..pager.count() {
1012
1073
draw_about_page(paint, pager.page_rect(pages, i), i);
1013
1074
}
1014
1075
paint.scene.pop_clip();
1015
1076
1016
- pager.paint(paint, content);
1077
+ pager.paint(paint, pages);
1017
1078
}
1018
1079
1019
1080
/// Pojedyncza strona karuzeli w panelu „O aplikacji”.
@@ -375,3 +375,58 @@ fn search_filters_items_and_selection_maps_to_full_list() {
375
375
click(&mut settings, &mut demo, slot);
376
376
assert_eq!(demo.items.selected(), Some(visible[0]));
377
377
}
378
+
379
+#[test]
380
+fn sheet_entry_focus_and_typing() {
381
+ let mut settings = ThemeSettings::default();
382
+ let mut demo = Demo::default();
383
+
384
+ // Otwórz dolny panel: menu ☰ → „O aplikacji”.
385
+ let burger = layout(&settings, &demo).burger.center();
386
+ click(&mut settings, &mut demo, burger);
387
+ let theme = settings.resolve(false);
388
+ let text = TextEngine::system();
389
+ let about = ui::popover_menu(size(), &theme, &text, &layout(&settings, &demo), &demo)
390
+ .map(|(panel, menu)| menu.rows(panel, &theme)[4].rect.center())
391
+ .expect("about entry");
392
+ click(&mut settings, &mut demo, about);
393
+ assert!(demo.sheet.is_open());
394
+
395
+ // Klik pola nadaje fokus; wpisany znak trafia do `Entry`.
396
+ let entry = ui::sheet_entry_rect(size(), &theme).center();
397
+ click(&mut settings, &mut demo, entry);
398
+ assert!(demo.sheet_entry.is_focused());
399
+
400
+ let key = Event::Key(KeyEvent {
401
+ keysym: 0x41,
402
+ text: Some("A".to_owned()),
403
+ state: ButtonState::Pressed,
404
+ modifiers: Modifiers::NONE,
405
+ repeat: false,
406
+ });
407
+ ui::handle_events(&mut settings, &[key], size(), &theme, &text, &mut demo);
408
+ assert_eq!(demo.sheet_entry.text(), "A");
409
+}
410
+
411
+#[test]
412
+fn clicking_toast_body_dismisses_without_reverting() {
413
+ let mut settings = ThemeSettings::default();
414
+ let mut demo = Demo::default();
415
+
416
+ let dot = layout(&settings, &demo).accents[1].center();
417
+ click(&mut settings, &mut demo, dot);
418
+ assert!(demo.toast.is_open());
419
+ let accent_after = settings.accent();
420
+
421
+ // Klik w treść powiadomienia zamyka je, ale nie cofa akcentu.
422
+ let theme = settings.resolve(false);
423
+ let text = TextEngine::system();
424
+ let r = demo.toast.rect(size(), &theme, &text);
425
+ click(
426
+ &mut settings,
427
+ &mut demo,
428
+ Point::new(r.x() + 6.0, r.center().y),
429
+ );
430
+ assert!(!demo.toast.is_open());
431
+ assert_eq!(settings.accent(), accent_after);
432
+}
@@ -51,6 +51,7 @@ Name = Name
51
51
Date modified = Änderungsdatum
52
52
Size = Größe
53
53
Show hidden files = Versteckte Dateien anzeigen
54
+Your name = Dein Name
54
55
Accent changed = Akzent geändert
55
56
Undo = Rückgängig
56
57
No results = Keine Ergebnisse
@@ -52,6 +52,7 @@ Name = Name
52
52
Date modified = Date modified
53
53
Size = Size
54
54
Show hidden files = Show hidden files
55
+Your name = Your name
55
56
Accent changed = Accent changed
56
57
Undo = Undo
57
58
No results = No results
@@ -51,6 +51,7 @@ Name = Nazwy
51
51
Date modified = Daty modyfikacji
52
52
Size = Rozmiar
53
53
Show hidden files = Pokazuj ukryte pliki
54
+Your name = Imię
54
55
Accent changed = Zmieniono akcent
55
56
Undo = Cofnij
56
57
No results = Brak wyników
57
58
new file mode 100644
@@ -0,0 +1,367 @@
1
+//! `Entry` — pole tekstowe (jednowierszowe) z kursorem, opcjonalną ikoną wiodącą i czyszczeniem.
2
+//!
3
+//! Widget immediate: trzyma treść i pozycję kursora (indeks bajtowy, zawsze na granicy znaku).
4
+//! Aplikacja dostarcza zdarzenia klawiatury jako wywołania metod ([`Entry::insert_str`],
5
+//! [`Entry::backspace`], [`Entry::move_left`]…) i sama decyduje o fokusie ([`Entry::set_focused`]).
6
+//!
7
+//! [`SearchEntry`](crate::SearchEntry) to cienka nakładka: `Entry` z ikoną lupy i czyszczeniem.
8
+
9
+use libluna_core::style::weight;
10
+use libluna_core::{Point, Rect, Theme};
11
+
12
+use crate::ctx::PaintCtx;
13
+use crate::icon;
14
+
15
+/// Ikona wiodąca pola (przed tekstem).
16
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
+pub enum EntryIcon {
18
+ /// Lupka (pola wyszukiwania).
19
+ Search,
20
+}
21
+
22
+/// Pole tekstowe.
23
+#[derive(Debug, Clone, Default)]
24
+pub struct Entry {
25
+ text: String,
26
+ /// Pozycja kursora jako indeks bajtowy (zawsze na granicy znaku).
27
+ cursor: usize,
28
+ placeholder: String,
29
+ leading: Option<EntryIcon>,
30
+ clearable: bool,
31
+ focused: bool,
32
+ hover_clear: bool,
33
+}
34
+
35
+impl Entry {
36
+ pub fn new(placeholder: impl Into<String>) -> Self {
37
+ Self {
38
+ placeholder: placeholder.into(),
39
+ ..Self::default()
40
+ }
41
+ }
42
+
43
+ /// Dodaje ikonę wiodącą (np. lupkę).
44
+ pub fn leading(mut self, icon: EntryIcon) -> Self {
45
+ self.leading = Some(icon);
46
+ self
47
+ }
48
+
49
+ /// Włącza przycisk czyszczenia („×”) widoczny, gdy pole niepuste.
50
+ pub fn clearable(mut self, clearable: bool) -> Self {
51
+ self.clearable = clearable;
52
+ self
53
+ }
54
+
55
+ pub fn text(&self) -> &str {
56
+ &self.text
57
+ }
58
+
59
+ pub fn is_empty(&self) -> bool {
60
+ self.text.is_empty()
61
+ }
62
+
63
+ pub fn cursor(&self) -> usize {
64
+ self.cursor
65
+ }
66
+
67
+ pub fn is_focused(&self) -> bool {
68
+ self.focused
69
+ }
70
+
71
+ pub fn set_text(&mut self, text: impl Into<String>) {
72
+ self.text = text.into();
73
+ self.cursor = self.text.len();
74
+ }
75
+
76
+ pub fn set_focused(&mut self, focused: bool) {
77
+ self.focused = focused;
78
+ }
79
+
80
+ pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
81
+ self.placeholder = placeholder.into();
82
+ }
83
+
84
+ pub fn set_hover_clear(&mut self, hovered: bool) {
85
+ self.hover_clear = hovered;
86
+ }
87
+
88
+ pub fn clear(&mut self) {
89
+ self.text.clear();
90
+ self.cursor = 0;
91
+ }
92
+
93
+ /// Wstawia tekst w miejscu kursora.
94
+ pub fn insert_str(&mut self, s: &str) {
95
+ self.text.insert_str(self.cursor, s);
96
+ self.cursor += s.len();
97
+ }
98
+
99
+ pub fn insert_char(&mut self, c: char) {
100
+ let mut buf = [0u8; 4];
101
+ self.insert_str(c.encode_utf8(&mut buf));
102
+ }
103
+
104
+ /// Usuwa znak przed kursorem.
105
+ pub fn backspace(&mut self) {
106
+ if self.cursor > 0 {
107
+ let prev = prev_boundary(&self.text, self.cursor);
108
+ self.text.replace_range(prev..self.cursor, "");
109
+ self.cursor = prev;
110
+ }
111
+ }
112
+
113
+ /// Usuwa znak za kursorem.
114
+ pub fn delete_forward(&mut self) {
115
+ if self.cursor < self.text.len() {
116
+ let next = next_boundary(&self.text, self.cursor);
117
+ self.text.replace_range(self.cursor..next, "");
118
+ }
119
+ }
120
+
121
+ pub fn move_left(&mut self) {
122
+ self.cursor = prev_boundary(&self.text, self.cursor);
123
+ }
124
+
125
+ pub fn move_right(&mut self) {
126
+ self.cursor = next_boundary(&self.text, self.cursor);
127
+ }
128
+
129
+ pub fn home(&mut self) {
130
+ self.cursor = 0;
131
+ }
132
+
133
+ pub fn end(&mut self) {
134
+ self.cursor = self.text.len();
135
+ }
136
+
137
+ pub fn height(&self, theme: &Theme) -> f64 {
138
+ theme.metrics.entry.height
139
+ }
140
+
141
+ pub fn contains(&self, rect: Rect, p: Point) -> bool {
142
+ rect.contains(p)
143
+ }
144
+
145
+ /// Prostokąt przycisku czyszczenia („×”).
146
+ pub fn clear_rect(&self, rect: Rect, theme: &Theme) -> Rect {
147
+ let m = theme.metrics.entry;
148
+ Rect::from_xywh(
149
+ rect.right() - m.pad_x - m.icon,
150
+ rect.center().y - m.icon / 2.0,
151
+ m.icon,
152
+ m.icon,
153
+ )
154
+ }
155
+
156
+ /// Czy punkt trafia w przycisk czyszczenia (tylko gdy pole czyszczalne i niepuste).
157
+ pub fn clear_at(&self, rect: Rect, theme: &Theme, p: Point) -> bool {
158
+ self.clearable && !self.text.is_empty() && self.clear_rect(rect, theme).contains(p)
159
+ }
160
+
161
+ /// Pozycja (x), od której zaczyna się tekst (za ikoną wiodącą, jeśli jest).
162
+ fn text_x(&self, rect: Rect, theme: &Theme) -> f64 {
163
+ let m = theme.metrics.entry;
164
+ match self.leading {
165
+ Some(_) => rect.x() + m.pad_x + m.icon + 8.0,
166
+ None => rect.x() + m.pad_x,
167
+ }
168
+ }
169
+
170
+ pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
171
+ let theme = *ctx.theme;
172
+ let m = theme.metrics.entry;
173
+ let colors = theme.colors;
174
+ let radius = theme.radius.medium;
175
+
176
+ ctx.scene.fill_rounded_rect(rect, radius, colors.shade);
177
+ let (border, width) = if self.focused {
178
+ (colors.accent, 2.0)
179
+ } else {
180
+ (colors.border, 1.0)
181
+ };
182
+ ctx.scene.stroke_rounded_rect(rect, radius, width, border);
183
+
184
+ let cy = rect.center().y;
185
+ if let Some(icon_kind) = self.leading {
186
+ match icon_kind {
187
+ EntryIcon::Search => icon::search(
188
+ ctx,
189
+ colors.dim_text,
190
+ Point::new(rect.x() + m.pad_x + m.icon / 2.0, cy),
191
+ m.icon,
192
+ ),
193
+ }
194
+ }
195
+ let text_x = self.text_x(rect, &theme);
196
+
197
+ if self.text.is_empty() {
198
+ ctx.label_left(
199
+ text_x,
200
+ cy,
201
+ &self.placeholder,
202
+ theme.typography.body.size,
203
+ weight::NORMAL,
204
+ colors.dim_text,
205
+ );
206
+ } else {
207
+ let clear_space = if self.clearable { m.icon + 8.0 } else { 0.0 };
208
+ let text_right = rect.right() - m.pad_x - clear_space;
209
+ ctx.scene.push_clip(Rect::from_xywh(
210
+ text_x,
211
+ rect.y(),
212
+ (text_right - text_x).max(0.0),
213
+ rect.height(),
214
+ ));
215
+ ctx.label_left(
216
+ text_x,
217
+ cy,
218
+ &self.text,
219
+ theme.typography.body.size,
220
+ weight::NORMAL,
221
+ colors.text,
222
+ );
223
+ if self.focused {
224
+ let cx = text_x
225
+ + ctx.measure(
226
+ &self.text[..self.cursor],
227
+ theme.typography.body.size,
228
+ weight::NORMAL,
229
+ );
230
+ ctx.scene.fill_rect(
231
+ Rect::from_xywh(cx, rect.y() + 6.0, 1.5, (rect.height() - 12.0).max(0.0)),
232
+ colors.text,
233
+ );
234
+ }
235
+ ctx.scene.pop_clip();
236
+ }
237
+
238
+ if self.clearable && !self.text.is_empty() {
239
+ let cb = self.clear_rect(rect, &theme);
240
+ if self.hover_clear {
241
+ ctx.scene
242
+ .fill_rounded_rect(cb, cb.width() / 2.0, colors.shade.scale_alpha(1.8));
243
+ }
244
+ icon::close(ctx, colors.dim_text, cb, 4.0);
245
+ }
246
+ }
247
+}
248
+
249
+/// Wspólny interfejs edycji tekstu dla pól ([`Entry`], [`SearchEntry`](crate::SearchEntry)).
250
+///
251
+/// Pozwala aplikacji obsłużyć zdarzenia klawiatury jedną funkcją, niezależnie od tego,
252
+/// czy pole jest „gołym” `Entry`, czy wyspecjalizowanym wariantem.
253
+pub trait TextInput {
254
+ fn insert_str(&mut self, s: &str);
255
+ fn insert_char(&mut self, c: char);
256
+ fn backspace(&mut self);
257
+ fn delete_forward(&mut self);
258
+ fn move_left(&mut self);
259
+ fn move_right(&mut self);
260
+ fn home(&mut self);
261
+ fn end(&mut self);
262
+}
263
+
264
+impl TextInput for Entry {
265
+ fn insert_str(&mut self, s: &str) {
266
+ Entry::insert_str(self, s);
267
+ }
268
+ fn insert_char(&mut self, c: char) {
269
+ Entry::insert_char(self, c);
270
+ }
271
+ fn backspace(&mut self) {
272
+ Entry::backspace(self);
273
+ }
274
+ fn delete_forward(&mut self) {
275
+ Entry::delete_forward(self);
276
+ }
277
+ fn move_left(&mut self) {
278
+ Entry::move_left(self);
279
+ }
280
+ fn move_right(&mut self) {
281
+ Entry::move_right(self);
282
+ }
283
+ fn home(&mut self) {
284
+ Entry::home(self);
285
+ }
286
+ fn end(&mut self) {
287
+ Entry::end(self);
288
+ }
289
+}
290
+
291
+fn prev_boundary(s: &str, i: usize) -> usize {
292
+ s[..i]
293
+ .char_indices()
294
+ .next_back()
295
+ .map(|(j, _)| j)
296
+ .unwrap_or(0)
297
+}
298
+
299
+fn next_boundary(s: &str, i: usize) -> usize {
300
+ s[i..].chars().next().map(|c| i + c.len_utf8()).unwrap_or(i)
301
+}
302
+
303
+#[cfg(test)]
304
+mod tests {
305
+ use super::*;
306
+
307
+ #[test]
308
+ fn typing_and_cursor() {
309
+ let mut e = Entry::new("Nazwa");
310
+ e.insert_str("abc");
311
+ assert_eq!(e.text(), "abc");
312
+ assert_eq!(e.cursor(), 3);
313
+ e.move_left();
314
+ e.insert_char('X');
315
+ assert_eq!(e.text(), "abXc");
316
+ e.backspace();
317
+ assert_eq!(e.text(), "abc");
318
+ assert_eq!(e.cursor(), 2);
319
+ e.delete_forward();
320
+ assert_eq!(e.text(), "ab");
321
+ e.home();
322
+ e.delete_forward();
323
+ assert_eq!(e.text(), "b");
324
+ }
325
+
326
+ #[test]
327
+ fn multibyte_is_char_aware() {
328
+ let mut e = Entry::new("");
329
+ e.insert_str("ząb");
330
+ e.backspace();
331
+ assert_eq!(e.text(), "zą");
332
+ e.move_left();
333
+ e.backspace();
334
+ assert_eq!(e.text(), "ą");
335
+ }
336
+
337
+ #[test]
338
+ fn clear_resets_state() {
339
+ let mut e = Entry::new("");
340
+ e.insert_str("x");
341
+ e.clear();
342
+ assert!(e.is_empty());
343
+ assert_eq!(e.cursor(), 0);
344
+ }
345
+
346
+ #[test]
347
+ fn clear_hit_requires_clearable_and_text() {
348
+ let theme = Theme::light();
349
+ let rect = Rect::from_xywh(0.0, 0.0, 200.0, 34.0);
350
+ let mut plain = Entry::new("");
351
+ plain.insert_str("abc");
352
+ let c = plain.clear_rect(rect, &theme).center();
353
+ // Pole nieczyszczalne: brak trafienia nawet z tekstem.
354
+ assert!(!plain.clear_at(rect, &theme, c));
355
+
356
+ let mut e = Entry::new("").clearable(true);
357
+ assert!(!e.clear_at(rect, &theme, c));
358
+ e.insert_str("abc");
359
+ assert!(e.clear_at(rect, &theme, c));
360
+ }
361
+
362
+ #[test]
363
+ fn height_from_metrics() {
364
+ let theme = Theme::light();
365
+ assert_eq!(Entry::new("").height(&theme), theme.metrics.entry.height);
366
+ }
367
+}
@@ -20,6 +20,7 @@
20
20
//! - [`BottomSheet`] — dolny panel (modalny) z „grabberem” i zamykaniem.
21
21
//! - [`Pager`] — karuzela stron (przewijanie w poziomie + kropki-wskaźniki).
22
22
//! - [`PathBar`] — pasek ścieżki (okruszki) z separatorami i zwijaniem w wielokropek.
23
+//! - [`Entry`] — pole tekstowe (kursor, edycja, opcjonalna ikona i czyszczenie).
23
24
//! - [`SearchEntry`] — pole tekstowe wyszukiwania (lupka, kursor, czyszczenie).
24
25
//! - [`ItemView`] — widok elementów (pliki/katalogi) w siatce lub liście.
25
26
//! - [`ScrollView`] — przewijanie pionowe (rolka + „kciuk”).
@@ -46,6 +47,7 @@ pub mod card;
46
47
pub mod check_button;
47
48
pub mod ctx;
48
49
pub mod dropdown;
50
+pub mod entry;
49
51
pub mod header_bar;
50
52
pub mod icon;
51
53
pub mod item_view;
@@ -72,6 +74,7 @@ pub use card::Card;
72
74
pub use check_button::CheckButton;
73
75
pub use ctx::PaintCtx;
74
76
pub use dropdown::DropDown;
77
+pub use entry::{Entry, EntryIcon, TextInput};
75
78
pub use header_bar::{ControlsLayout, ControlsSide, HeaderBar, TitleAlign, WindowControl};
76
79
pub use item_view::{Item, ItemKind, ItemView, ViewMode};
77
80
pub use menu::{Menu, MenuEntry, MenuRow};
@@ -1,229 +1,142 @@
1
-//! `SearchEntry` — pole tekstowe wyszukiwania (lupka, tekst, kursor, „×” czyszczenia).
1
+//! `SearchEntry` — pole wyszukiwania (lupka, tekst, kursor, „×” czyszczenia).
2
2
//!
3
-//! Widget immediate: trzyma treść i pozycję kursora. Aplikacja dostarcza zdarzenia klawiatury
4
-//! jako wywołania metod ([`SearchEntry::insert_str`], [`SearchEntry::backspace`],
5
-//! [`SearchEntry::move_left`]…), a sama decyduje o fokusie ([`SearchEntry::set_focused`]).
3
+//! Cienka nakładka na [`Entry`]: pole tekstowe z ikoną lupy i przyciskiem czyszczenia.
4
+//! Cała logika edycji żyje w [`Entry`]; tutaj delegujemy, zachowując dawny interfejs.
6
5
7
-use libluna_core::style::weight;
8
6
use libluna_core::{Point, Rect, Theme};
9
7
10
8
use crate::ctx::PaintCtx;
11
-use crate::icon;
9
+use crate::entry::{Entry, EntryIcon};
12
10
13
11
/// Pole wyszukiwania.
14
12
#[derive(Debug, Clone, Default)]
15
13
pub struct SearchEntry {
16
- text: String,
17
- /// Pozycja kursora jako indeks bajtowy (zawsze na granicy znaku).
18
- cursor: usize,
19
- placeholder: String,
20
- focused: bool,
21
- hover_clear: bool,
14
+ entry: Entry,
22
15
}
23
16
24
17
impl SearchEntry {
25
18
pub fn new(placeholder: impl Into<String>) -> Self {
26
19
Self {
27
- placeholder: placeholder.into(),
28
- ..Self::default()
20
+ entry: Entry::new(placeholder)
21
+ .leading(EntryIcon::Search)
22
+ .clearable(true),
29
23
}
30
24
}
31
25
32
26
pub fn text(&self) -> &str {
33
- &self.text
27
+ self.entry.text()
34
28
}
35
29
36
30
pub fn is_empty(&self) -> bool {
37
- self.text.is_empty()
31
+ self.entry.is_empty()
38
32
}
39
33
40
34
pub fn cursor(&self) -> usize {
41
- self.cursor
35
+ self.entry.cursor()
42
36
}
43
37
44
38
pub fn is_focused(&self) -> bool {
45
- self.focused
39
+ self.entry.is_focused()
46
40
}
47
41
48
42
pub fn set_text(&mut self, text: impl Into<String>) {
49
- self.text = text.into();
50
- self.cursor = self.text.len();
43
+ self.entry.set_text(text);
51
44
}
52
45
53
46
pub fn set_focused(&mut self, focused: bool) {
54
- self.focused = focused;
47
+ self.entry.set_focused(focused);
55
48
}
56
49
57
50
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
58
- self.placeholder = placeholder.into();
51
+ self.entry.set_placeholder(placeholder);
59
52
}
60
53
61
54
pub fn set_hover_clear(&mut self, hovered: bool) {
62
- self.hover_clear = hovered;
55
+ self.entry.set_hover_clear(hovered);
63
56
}
64
57
65
58
pub fn clear(&mut self) {
66
- self.text.clear();
67
- self.cursor = 0;
59
+ self.entry.clear();
68
60
}
69
61
70
- /// Wstawia tekst w miejscu kursora.
71
62
pub fn insert_str(&mut self, s: &str) {
72
- self.text.insert_str(self.cursor, s);
73
- self.cursor += s.len();
63
+ self.entry.insert_str(s);
74
64
}
75
65
76
66
pub fn insert_char(&mut self, c: char) {
77
- let mut buf = [0u8; 4];
78
- self.insert_str(c.encode_utf8(&mut buf));
67
+ self.entry.insert_char(c);
79
68
}
80
69
81
- /// Usuwa znak przed kursorem.
82
70
pub fn backspace(&mut self) {
83
- if self.cursor > 0 {
84
- let prev = prev_boundary(&self.text, self.cursor);
85
- self.text.replace_range(prev..self.cursor, "");
86
- self.cursor = prev;
87
- }
71
+ self.entry.backspace();
88
72
}
89
73
90
- /// Usuwa znak za kursorem.
91
74
pub fn delete_forward(&mut self) {
92
- if self.cursor < self.text.len() {
93
- let next = next_boundary(&self.text, self.cursor);
94
- self.text.replace_range(self.cursor..next, "");
95
- }
75
+ self.entry.delete_forward();
96
76
}
97
77
98
78
pub fn move_left(&mut self) {
99
- self.cursor = prev_boundary(&self.text, self.cursor);
79
+ self.entry.move_left();
100
80
}
101
81
102
82
pub fn move_right(&mut self) {
103
- self.cursor = next_boundary(&self.text, self.cursor);
83
+ self.entry.move_right();
104
84
}
105
85
106
86
pub fn home(&mut self) {
107
- self.cursor = 0;
87
+ self.entry.home();
108
88
}
109
89
110
90
pub fn end(&mut self) {
111
- self.cursor = self.text.len();
91
+ self.entry.end();
112
92
}
113
93
114
94
pub fn height(&self, theme: &Theme) -> f64 {
115
- theme.metrics.entry.height
95
+ self.entry.height(theme)
116
96
}
117
97
118
98
pub fn contains(&self, rect: Rect, p: Point) -> bool {
119
- rect.contains(p)
99
+ self.entry.contains(rect, p)
120
100
}
121
101
122
- /// Prostokąt przycisku czyszczenia („×”).
123
102
pub fn clear_rect(&self, rect: Rect, theme: &Theme) -> Rect {
124
- let m = theme.metrics.entry;
125
- Rect::from_xywh(
126
- rect.right() - m.pad_x - m.icon,
127
- rect.center().y - m.icon / 2.0,
128
- m.icon,
129
- m.icon,
130
- )
103
+ self.entry.clear_rect(rect, theme)
131
104
}
132
105
133
- /// Czy punkt trafia w przycisk czyszczenia (tylko gdy pole niepuste).
134
106
pub fn clear_at(&self, rect: Rect, theme: &Theme, p: Point) -> bool {
135
- !self.text.is_empty() && self.clear_rect(rect, theme).contains(p)
107
+ self.entry.clear_at(rect, theme, p)
136
108
}
137
109
138
110
pub fn paint(&self, ctx: &mut PaintCtx, rect: Rect) {
139
- let theme = *ctx.theme;
140
- let m = theme.metrics.entry;
141
- let colors = theme.colors;
142
- let radius = theme.radius.medium;
143
-
144
- ctx.scene.fill_rounded_rect(rect, radius, colors.shade);
145
- let (border, width) = if self.focused {
146
- (colors.accent, 2.0)
147
- } else {
148
- (colors.border, 1.0)
149
- };
150
- ctx.scene.stroke_rounded_rect(rect, radius, width, border);
151
-
152
- let cy = rect.center().y;
153
- icon::search(
154
- ctx,
155
- colors.dim_text,
156
- Point::new(rect.x() + m.pad_x + m.icon / 2.0, cy),
157
- m.icon,
158
- );
159
- let text_x = rect.x() + m.pad_x + m.icon + 8.0;
160
-
161
- if self.text.is_empty() {
162
- ctx.label_left(
163
- text_x,
164
- cy,
165
- &self.placeholder,
166
- theme.typography.body.size,
167
- weight::NORMAL,
168
- colors.dim_text,
169
- );
170
- } else {
171
- let clear_space = if self.text.is_empty() {
172
- 0.0
173
- } else {
174
- m.icon + 8.0
175
- };
176
- let text_right = rect.right() - m.pad_x - clear_space;
177
- ctx.scene.push_clip(Rect::from_xywh(
178
- text_x,
179
- rect.y(),
180
- (text_right - text_x).max(0.0),
181
- rect.height(),
182
- ));
183
- ctx.label_left(
184
- text_x,
185
- cy,
186
- &self.text,
187
- theme.typography.body.size,
188
- weight::NORMAL,
189
- colors.text,
190
- );
191
- if self.focused {
192
- let cx = text_x
193
- + ctx.measure(
194
- &self.text[..self.cursor],
195
- theme.typography.body.size,
196
- weight::NORMAL,
197
- );
198
- ctx.scene.fill_rect(
199
- Rect::from_xywh(cx, rect.y() + 6.0, 1.5, (rect.height() - 12.0).max(0.0)),
200
- colors.text,
201
- );
202
- }
203
- ctx.scene.pop_clip();
204
- }
205
-
206
- if !self.text.is_empty() {
207
- let cb = self.clear_rect(rect, &theme);
208
- if self.hover_clear {
209
- ctx.scene
210
- .fill_rounded_rect(cb, cb.width() / 2.0, colors.shade.scale_alpha(1.8));
211
- }
212
- icon::close(ctx, colors.dim_text, cb, 4.0);
213
- }
111
+ self.entry.paint(ctx, rect);
214
112
}
215
113
}
216
114
217
-fn prev_boundary(s: &str, i: usize) -> usize {
218
- s[..i]
219
- .char_indices()
220
- .next_back()
221
- .map(|(j, _)| j)
222
- .unwrap_or(0)
223
-}
224
-
225
-fn next_boundary(s: &str, i: usize) -> usize {
226
- s[i..].chars().next().map(|c| i + c.len_utf8()).unwrap_or(i)
115
+impl crate::entry::TextInput for SearchEntry {
116
+ fn insert_str(&mut self, s: &str) {
117
+ SearchEntry::insert_str(self, s);
118
+ }
119
+ fn insert_char(&mut self, c: char) {
120
+ SearchEntry::insert_char(self, c);
121
+ }
122
+ fn backspace(&mut self) {
123
+ SearchEntry::backspace(self);
124
+ }
125
+ fn delete_forward(&mut self) {
126
+ SearchEntry::delete_forward(self);
127
+ }
128
+ fn move_left(&mut self) {
129
+ SearchEntry::move_left(self);
130
+ }
131
+ fn move_right(&mut self) {
132
+ SearchEntry::move_right(self);
133
+ }
134
+ fn home(&mut self) {
135
+ SearchEntry::home(self);
136
+ }
137
+ fn end(&mut self) {
138
+ SearchEntry::end(self);
139
+ }
227
140
}
228
141
229
142
#[cfg(test)]
@@ -106,6 +106,15 @@ impl Toast {
106
106
.is_some_and(|r| r.contains(p))
107
107
}
108
108
109
+ /// Czy punkt trafia w powiadomienie poza jego akcją (klik zamykający).
110
+ pub fn dismiss_at(&self, size: Size, theme: &Theme, text: &TextEngine, p: Point) -> bool {
111
+ self.open
112
+ && self.rect(size, theme, text).contains(p)
113
+ && !self
114
+ .action_rect(size, theme, text)
115
+ .is_some_and(|r| r.contains(p))
116
+ }
117
+
109
118
/// Rysuje powiadomienie (na wierzchu sceny).
110
119
pub fn paint(&self, ctx: &mut PaintCtx, size: Size) {
111
120
if !self.open {
@@ -176,6 +185,22 @@ mod tests {
176
185
assert!(!toast.action_at(size(), &theme, &text, Point::new(5.0, 5.0)));
177
186
}
178
187
188
+ #[test]
189
+ fn body_dismisses_but_action_does_not() {
190
+ let theme = Theme::light();
191
+ let text = TextEngine::system();
192
+ let mut toast = Toast::new("Saved").action("Undo");
193
+ toast.show();
194
+ let r = toast.rect(size(), &theme, &text);
195
+ // Klik w lewy skraj treści zamyka.
196
+ assert!(toast.dismiss_at(size(), &theme, &text, Point::new(r.x() + 6.0, r.center().y)));
197
+ // Klik w akcję nie zamyka (obsługiwany osobno).
198
+ let ar = toast.action_rect(size(), &theme, &text).unwrap();
199
+ assert!(!toast.dismiss_at(size(), &theme, &text, ar.center()));
200
+ // Klik poza powiadomieniem nie zamyka.
201
+ assert!(!toast.dismiss_at(size(), &theme, &text, Point::new(5.0, 5.0)));
202
+ }
203
+
179
204
#[test]
180
205
fn toast_is_centered_and_above_bottom() {
181
206
let theme = Theme::light();
182
207
Binary files a/docs/images/demo-montage.png and b/docs/images/demo-montage.png differ
183
208
Binary files a/docs/images/demo-step-10.png and b/docs/images/demo-step-10.png differ
184
209
Binary files a/docs/images/demo-step-11.png and b/docs/images/demo-step-11.png differ
185
210
Binary files a/docs/images/demo-step-4.png and b/docs/images/demo-step-4.png differ
186
211
Binary files a/docs/images/demo-step-5.png and b/docs/images/demo-step-5.png differ
187
212
Binary files a/docs/images/demo-step-6.png and b/docs/images/demo-step-6.png differ
188
213
Binary files a/docs/images/demo-step-7.png and b/docs/images/demo-step-7.png differ
189
214
Binary files a/docs/images/demo-step-8.png and b/docs/images/demo-step-8.png differ
190
215
Binary files a/docs/images/demo-step-9.png and b/docs/images/demo-step-9.png differ
@@ -37,7 +37,8 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
37
37
`Menu` (popover `☰`), `Button` (warianty + chevron), `Switch`, `Slider`, `DropDown`
38
38
(rozwijany przez `Menu`), `HeaderBar` (pasek tytułu z kontrolkami okna), `TabBar`/`TabView`
39
39
(zakładki: pasek + model stron), `BottomSheet` (dolny panel modalny), `Pager` (karuzela stron),
40
- `PathBar` (okruszki), `SearchEntry` (pole wyszukiwania), `ItemView` (pliki/katalogi: siatka
40
+ `PathBar` (okruszki), `Entry` (pole tekstowe) i `SearchEntry` (wyszukiwanie na jego bazie),
41
+ `ItemView` (pliki/katalogi: siatka
41
42
lub lista) i `ScrollView` (przewijanie), oraz `Stepper`, `CheckButton` i `RadioGroup`
42
43
(krokowiec, checkbox, wybór jednokrotny); do tego `Card` i `ActionRow` (karty i wiersze
43
44
ustawień), `Toast` (powiadomienie nakładkowe z akcją) i `StatusPage` (ekran stanu: ikona,
@@ -100,6 +101,7 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
100
101
| Pager (karuzela stron) | `libluna-widgets` | (przeglądarka, onboarding) |
101
102
| Sidebar (sekcje, wiersze) | `libluna-adwaita` | obie |
102
103
| PathBar (okruszki) | `libluna-widgets` | Files |
104
+| Entry (pole tekstowe) | `libluna-widgets` | Settings, Files |
103
105
| SearchEntry / SearchBar | `libluna-widgets` | Files |
104
106
| GridView / ListView | `libluna-widgets` (`ItemView`) | Files |
105
107
| ScrollView | `libluna-widgets` | obie |
@@ -50,6 +50,7 @@ Name = Nom
50
50
Date modified = Date de modification
51
51
Size = Taille
52
52
Show hidden files = Afficher les fichiers cachés
53
+Your name = Votre nom
53
54
Accent changed = Accent modifié
54
55
Undo = Annuler
55
56
No results = Aucun résultat