Commit 4b3f0e7
12
files
+658
added
-60
deleted
@@ -95,6 +95,27 @@ pub enum DrawCommand {
95
95
width: f64,
96
96
color: Color,
97
97
},
98
+ /// Wypełniony okrąg.
99
+ FillCircle {
100
+ center: Point,
101
+ radius: f64,
102
+ color: Color,
103
+ },
104
+ /// Obwód okręgu (linia o zadanej grubości).
105
+ StrokeCircle {
106
+ center: Point,
107
+ radius: f64,
108
+ width: f64,
109
+ color: Color,
110
+ },
111
+ /// Łamana (otwarta; powtórz pierwszy punkt, by zamknąć kontur).
112
+ StrokePolyline {
113
+ points: Vec<Point>,
114
+ width: f64,
115
+ color: Color,
116
+ },
117
+ /// Wypełniony wielokąt (domykany automatycznie).
118
+ FillPolygon { points: Vec<Point>, color: Color },
98
119
/// Ogranicza kolejne polecenia do prostokąta (zastosowanie kumulacyjne).
99
120
PushClip { rect: Rect },
100
121
/// Zdejmuje ostatnie ograniczenie.
@@ -175,6 +196,52 @@ impl Scene {
175
196
self
176
197
}
177
198
199
+ /// Wypełniony okrąg.
200
+ pub fn fill_circle(&mut self, center: Point, radius: f64, color: Color) -> &mut Self {
201
+ self.commands.push(DrawCommand::FillCircle {
202
+ center,
203
+ radius,
204
+ color,
205
+ });
206
+ self
207
+ }
208
+
209
+ /// Obwód okręgu.
210
+ pub fn stroke_circle(
211
+ &mut self,
212
+ center: Point,
213
+ radius: f64,
214
+ width: f64,
215
+ color: Color,
216
+ ) -> &mut Self {
217
+ self.commands.push(DrawCommand::StrokeCircle {
218
+ center,
219
+ radius,
220
+ width,
221
+ color,
222
+ });
223
+ self
224
+ }
225
+
226
+ /// Łamana (szkielet ikon); z zaokrąglonymi końcami i zgięciami.
227
+ pub fn stroke_polyline(&mut self, points: &[Point], width: f64, color: Color) -> &mut Self {
228
+ self.commands.push(DrawCommand::StrokePolyline {
229
+ points: points.to_vec(),
230
+ width,
231
+ color,
232
+ });
233
+ self
234
+ }
235
+
236
+ /// Wypełniony wielokąt (domykany automatycznie).
237
+ pub fn fill_polygon(&mut self, points: &[Point], color: Color) -> &mut Self {
238
+ self.commands.push(DrawCommand::FillPolygon {
239
+ points: points.to_vec(),
240
+ color,
241
+ });
242
+ self
243
+ }
244
+
178
245
pub fn push_clip(&mut self, rect: Rect) -> &mut Self {
179
246
self.commands.push(DrawCommand::PushClip { rect });
180
247
self
@@ -124,6 +124,39 @@ impl SoftwareRenderer {
124
124
}
125
125
}
126
126
}
127
+ DrawCommand::FillCircle {
128
+ center,
129
+ radius,
130
+ color,
131
+ } => {
132
+ if let Some(path) = circle_path(*center, *radius) {
133
+ fill(pixmap, &path, *color, transform, mask.as_ref());
134
+ }
135
+ }
136
+ DrawCommand::StrokeCircle {
137
+ center,
138
+ radius,
139
+ width,
140
+ color,
141
+ } => {
142
+ if let Some(path) = circle_path(*center, *radius) {
143
+ stroke(pixmap, &path, *width, *color, transform, mask.as_ref());
144
+ }
145
+ }
146
+ DrawCommand::StrokePolyline {
147
+ points,
148
+ width,
149
+ color,
150
+ } => {
151
+ if let Some(path) = polyline_path(points) {
152
+ stroke(pixmap, &path, *width, *color, transform, mask.as_ref());
153
+ }
154
+ }
155
+ DrawCommand::FillPolygon { points, color } => {
156
+ if let Some(path) = polygon_path(points) {
157
+ fill(pixmap, &path, *color, transform, mask.as_ref());
158
+ }
159
+ }
127
160
DrawCommand::PushClip { rect } => {
128
161
let effective = match clip_stack.last() {
129
162
Some(current) => current.intersection(rect).unwrap_or(Rect::ZERO),
@@ -214,6 +247,38 @@ fn rect_path(rect: Rect, radius: f64) -> Option<Path> {
214
247
pb.finish()
215
248
}
216
249
250
+fn circle_path(center: Point, radius: f64) -> Option<Path> {
251
+ if radius <= 0.0 {
252
+ return None;
253
+ }
254
+ PathBuilder::from_circle(center.x as f32, center.y as f32, radius as f32)
255
+}
256
+
257
+fn polyline_path(points: &[Point]) -> Option<Path> {
258
+ if points.len() < 2 {
259
+ return None;
260
+ }
261
+ let mut pb = PathBuilder::new();
262
+ pb.move_to(points[0].x as f32, points[0].y as f32);
263
+ for p in &points[1..] {
264
+ pb.line_to(p.x as f32, p.y as f32);
265
+ }
266
+ pb.finish()
267
+}
268
+
269
+fn polygon_path(points: &[Point]) -> Option<Path> {
270
+ if points.len() < 3 {
271
+ return None;
272
+ }
273
+ let mut pb = PathBuilder::new();
274
+ pb.move_to(points[0].x as f32, points[0].y as f32);
275
+ for p in &points[1..] {
276
+ pb.line_to(p.x as f32, p.y as f32);
277
+ }
278
+ pb.close();
279
+ pb.finish()
280
+}
281
+
217
282
fn paint(color: Color) -> Paint<'static> {
218
283
Paint {
219
284
shader: Shader::SolidColor(ts_color(color)),
@@ -242,6 +307,8 @@ fn stroke(
242
307
}
243
308
let stroke = Stroke {
244
309
width: width as f32,
310
+ line_cap: LineCap::Round,
311
+ line_join: LineJoin::Round,
245
312
..Default::default()
246
313
};
247
314
pixmap.stroke_path(path, &paint(color), &stroke, transform, mask);
@@ -276,6 +343,52 @@ mod tests {
276
343
assert_eq!(pixel(&r, 7, 7), [255, 255, 255, 255]);
277
344
}
278
345
346
+ #[test]
347
+ fn fill_circle_paints_center_only() {
348
+ let mut r = SoftwareRenderer::new(16, 16);
349
+ let mut s = Scene::new();
350
+ s.fill_circle(Point::new(8.0, 8.0), 4.0, Color::from_rgba8(255, 0, 0, 255));
351
+ r.draw(&s, 1.0, Color::WHITE);
352
+ assert_eq!(pixel(&r, 8, 8), [255, 0, 0, 255]);
353
+ assert_eq!(pixel(&r, 0, 0), [255, 255, 255, 255]);
354
+ }
355
+
356
+ #[test]
357
+ fn fill_polygon_covers_interior() {
358
+ let mut r = SoftwareRenderer::new(16, 16);
359
+ let mut s = Scene::new();
360
+ s.fill_polygon(
361
+ &[
362
+ Point::new(2.0, 2.0),
363
+ Point::new(14.0, 2.0),
364
+ Point::new(8.0, 14.0),
365
+ ],
366
+ Color::from_rgba8(0, 255, 0, 255),
367
+ );
368
+ r.draw(&s, 1.0, Color::WHITE);
369
+ assert_eq!(pixel(&r, 8, 6), [0, 255, 0, 255]);
370
+ assert_eq!(pixel(&r, 0, 0), [255, 255, 255, 255]);
371
+ }
372
+
373
+ #[test]
374
+ fn stroke_polyline_paints_segment() {
375
+ let mut r = SoftwareRenderer::new(16, 16);
376
+ let mut s = Scene::new();
377
+ s.stroke_polyline(
378
+ &[
379
+ Point::new(2.0, 8.0),
380
+ Point::new(8.0, 8.0),
381
+ Point::new(8.0, 14.0),
382
+ ],
383
+ 3.0,
384
+ Color::from_rgba8(0, 0, 255, 255),
385
+ );
386
+ r.draw(&s, 1.0, Color::WHITE);
387
+ assert_eq!(pixel(&r, 5, 8), [0, 0, 255, 255]);
388
+ assert_eq!(pixel(&r, 8, 12), [0, 0, 255, 255]);
389
+ assert_eq!(pixel(&r, 14, 14), [255, 255, 255, 255]);
390
+ }
391
+
279
392
#[test]
280
393
fn draws_red_rect_center_but_not_corner() {
281
394
let mut r = SoftwareRenderer::new(60, 60);
@@ -216,3 +216,393 @@ pub fn burger(paint: &mut PaintCtx, color: Color, center: Point, size: f64, widt
216
216
);
217
217
}
218
218
}
219
+
220
+// ─────────────────────────────────────────────────────────────────────────────
221
+// Nazwany pakiet ikon symbolicznych — rysowany wektorowo, w stylu Luny.
222
+// ─────────────────────────────────────────────────────────────────────────────
223
+
224
+/// Nazwa ikony z pakietu Luny.
225
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226
+pub enum IconName {
227
+ Menu,
228
+ Search,
229
+ Files,
230
+ Settings,
231
+ Terminal,
232
+ Music,
233
+ Star,
234
+ Clock,
235
+ Store,
236
+ Edit,
237
+ Wifi,
238
+ Volume,
239
+ Battery,
240
+ Power,
241
+ Grid,
242
+ List,
243
+ Carousel,
244
+ ChevronLeft,
245
+ ChevronRight,
246
+ Close,
247
+ Minimize,
248
+ Maximize,
249
+ Check,
250
+ Plus,
251
+ Home,
252
+ User,
253
+ Info,
254
+}
255
+
256
+/// Współrzędne w siatce 24×24 mapowane na prostokąt docelowy.
257
+struct Grid {
258
+ ox: f64,
259
+ oy: f64,
260
+ s: f64,
261
+}
262
+
263
+impl Grid {
264
+ fn new(rect: Rect) -> Self {
265
+ let side = rect.width().min(rect.height()).max(1.0);
266
+ let s = side / 24.0;
267
+ Self {
268
+ ox: rect.x() + (rect.width() - 24.0 * s) / 2.0,
269
+ oy: rect.y() + (rect.height() - 24.0 * s) / 2.0,
270
+ s,
271
+ }
272
+ }
273
+
274
+ fn p(&self, x: f64, y: f64) -> Point {
275
+ Point::new(self.ox + x * self.s, self.oy + y * self.s)
276
+ }
277
+
278
+ fn d(&self, v: f64) -> f64 {
279
+ v * self.s
280
+ }
281
+}
282
+
283
+/// Punkty łuku (środek `c`, promień `r`, kąty w stopniach, y w dół).
284
+fn arc(c: Point, r: f64, a0: f64, a1: f64, segs: usize) -> Vec<Point> {
285
+ (0..=segs)
286
+ .map(|i| {
287
+ let t = a0 + (a1 - a0) * i as f64 / segs as f64;
288
+ let a = t.to_radians();
289
+ Point::new(c.x + r * a.cos(), c.y + r * a.sin())
290
+ })
291
+ .collect()
292
+}
293
+
294
+/// Rysuje ikonę z pakietu w prostokącie `rect` w zadanym kolorze (styl symboliczny).
295
+pub fn paint_icon(paint: &mut PaintCtx, name: IconName, rect: Rect, color: Color) {
296
+ let g = Grid::new(rect);
297
+ let w = g.d(2.0);
298
+ let scene = &mut *paint.scene;
299
+
300
+ match name {
301
+ IconName::Menu => {
302
+ for y in [6.5, 12.0, 17.5] {
303
+ scene.stroke_line(g.p(4.0, y), g.p(20.0, y), w, color);
304
+ }
305
+ }
306
+ IconName::Search => {
307
+ scene.stroke_circle(g.p(10.5, 10.5), g.d(6.5), w, color);
308
+ scene.stroke_line(g.p(15.3, 15.3), g.p(20.5, 20.5), w, color);
309
+ }
310
+ IconName::Files => {
311
+ scene.fill_polygon(
312
+ &[
313
+ g.p(3.0, 6.5),
314
+ g.p(9.0, 6.5),
315
+ g.p(11.0, 8.8),
316
+ g.p(21.0, 8.8),
317
+ g.p(21.0, 19.5),
318
+ g.p(3.0, 19.5),
319
+ ],
320
+ color,
321
+ );
322
+ }
323
+ IconName::Settings => {
324
+ scene.stroke_circle(g.p(12.0, 12.0), g.d(3.4), w, color);
325
+ for k in 0..8 {
326
+ let a = (k as f64 * 45.0).to_radians();
327
+ let (c, s) = (a.cos(), a.sin());
328
+ scene.stroke_line(
329
+ g.p(12.0 + 5.2 * c, 12.0 + 5.2 * s),
330
+ g.p(12.0 + 8.4 * c, 12.0 + 8.4 * s),
331
+ g.d(2.2),
332
+ color,
333
+ );
334
+ }
335
+ }
336
+ IconName::Terminal => {
337
+ scene.stroke_rounded_rect(
338
+ Rect::from_xywh(g.p(3.0, 4.5).x, g.p(3.0, 4.5).y, g.d(18.0), g.d(15.0)),
339
+ g.d(2.5),
340
+ w,
341
+ color,
342
+ );
343
+ scene.stroke_polyline(&[g.p(7.0, 10.0), g.p(9.5, 12.0), g.p(7.0, 14.0)], w, color);
344
+ scene.stroke_line(g.p(12.0, 15.0), g.p(16.0, 15.0), w, color);
345
+ }
346
+ IconName::Music => {
347
+ scene.stroke_circle(g.p(9.5, 17.0), g.d(2.6), w, color);
348
+ scene.stroke_polyline(&[g.p(12.0, 17.0), g.p(12.0, 6.5), g.p(17.0, 8.0)], w, color);
349
+ }
350
+ IconName::Star => {
351
+ let pts: Vec<Point> = (0..10)
352
+ .map(|i| {
353
+ let a = (-90.0 + i as f64 * 36.0).to_radians();
354
+ let r = if i % 2 == 0 { 9.0 } else { 3.8 };
355
+ g.p(12.0 + r * a.cos(), 12.5 + r * a.sin())
356
+ })
357
+ .collect();
358
+ scene.fill_polygon(&pts, color);
359
+ }
360
+ IconName::Clock => {
361
+ scene.stroke_circle(g.p(12.0, 12.0), g.d(8.0), w, color);
362
+ scene.stroke_polyline(&[g.p(12.0, 12.0), g.p(12.0, 7.5)], w, color);
363
+ scene.stroke_polyline(&[g.p(12.0, 12.0), g.p(16.0, 13.8)], w, color);
364
+ }
365
+ IconName::Store => {
366
+ scene.fill_polygon(
367
+ &[
368
+ g.p(5.5, 8.0),
369
+ g.p(18.5, 8.0),
370
+ g.p(17.5, 20.0),
371
+ g.p(6.5, 20.0),
372
+ ],
373
+ color,
374
+ );
375
+ scene.stroke_polyline(
376
+ &[g.p(9.0, 8.0), g.p(9.0, 6.0), g.p(15.0, 6.0), g.p(15.0, 8.0)],
377
+ g.d(1.8),
378
+ color,
379
+ );
380
+ }
381
+ IconName::Edit => {
382
+ scene.fill_polygon(
383
+ &[
384
+ g.p(15.5, 3.5),
385
+ g.p(20.5, 8.5),
386
+ g.p(9.0, 20.0),
387
+ g.p(4.0, 21.0),
388
+ g.p(5.0, 16.0),
389
+ ],
390
+ color,
391
+ );
392
+ }
393
+ IconName::Wifi => {
394
+ scene.fill_circle(g.p(12.0, 18.0), g.d(1.4), color);
395
+ for r in [4.0, 7.5, 11.0] {
396
+ scene.stroke_polyline(
397
+ &arc(g.p(12.0, 18.0), g.d(r), 200.0, 340.0, 10),
398
+ g.d(1.9),
399
+ color,
400
+ );
401
+ }
402
+ }
403
+ IconName::Volume => {
404
+ scene.fill_polygon(
405
+ &[
406
+ g.p(3.0, 9.5),
407
+ g.p(7.0, 9.5),
408
+ g.p(11.0, 5.0),
409
+ g.p(11.0, 19.0),
410
+ g.p(7.0, 14.5),
411
+ g.p(3.0, 14.5),
412
+ ],
413
+ color,
414
+ );
415
+ for r in [5.0, 8.0] {
416
+ scene.stroke_polyline(
417
+ &arc(g.p(11.0, 12.0), g.d(r), -50.0, 50.0, 8),
418
+ g.d(1.9),
419
+ color,
420
+ );
421
+ }
422
+ }
423
+ IconName::Battery => {
424
+ scene.stroke_rounded_rect(
425
+ Rect::from_xywh(g.p(2.0, 7.0).x, g.p(2.0, 7.0).y, g.d(17.0), g.d(10.0)),
426
+ g.d(2.5),
427
+ w,
428
+ color,
429
+ );
430
+ scene.fill_rounded_rect(
431
+ Rect::from_xywh(g.p(19.5, 10.5).x, g.p(19.5, 10.5).y, g.d(2.5), g.d(3.0)),
432
+ g.d(1.0),
433
+ color,
434
+ );
435
+ scene.fill_rounded_rect(
436
+ Rect::from_xywh(g.p(4.5, 9.5).x, g.p(4.5, 9.5).y, g.d(9.5), g.d(5.0)),
437
+ g.d(1.2),
438
+ color,
439
+ );
440
+ }
441
+ IconName::Power => {
442
+ scene.stroke_polyline(&arc(g.p(12.0, 12.0), g.d(7.5), -60.0, 240.0, 28), w, color);
443
+ scene.stroke_line(g.p(12.0, 3.5), g.p(12.0, 12.0), w, color);
444
+ }
445
+ IconName::Grid => {
446
+ for (x, y) in [(4.0, 4.0), (13.5, 4.0), (4.0, 13.5), (13.5, 13.5)] {
447
+ scene.fill_rounded_rect(
448
+ Rect::from_xywh(g.p(x, y).x, g.p(x, y).y, g.d(6.5), g.d(6.5)),
449
+ g.d(1.6),
450
+ color,
451
+ );
452
+ }
453
+ }
454
+ IconName::List => {
455
+ for y in [6.5, 12.0, 17.5] {
456
+ scene.fill_circle(g.p(5.5, y), g.d(1.4), color);
457
+ scene.stroke_line(g.p(9.5, y), g.p(19.5, y), g.d(1.8), color);
458
+ }
459
+ }
460
+ IconName::Carousel => {
461
+ scene.fill_rounded_rect(
462
+ Rect::from_xywh(g.p(3.0, 8.5).x, g.p(3.0, 8.5).y, g.d(3.2), g.d(7.0)),
463
+ g.d(1.2),
464
+ color,
465
+ );
466
+ scene.fill_rounded_rect(
467
+ Rect::from_xywh(g.p(17.8, 8.5).x, g.p(17.8, 8.5).y, g.d(3.2), g.d(7.0)),
468
+ g.d(1.2),
469
+ color,
470
+ );
471
+ scene.fill_rounded_rect(
472
+ Rect::from_xywh(g.p(8.0, 6.0).x, g.p(8.0, 6.0).y, g.d(8.0), g.d(12.0)),
473
+ g.d(1.6),
474
+ color,
475
+ );
476
+ }
477
+ IconName::ChevronLeft => {
478
+ scene.stroke_polyline(
479
+ &[g.p(14.5, 6.0), g.p(8.5, 12.0), g.p(14.5, 18.0)],
480
+ g.d(2.4),
481
+ color,
482
+ );
483
+ }
484
+ IconName::ChevronRight => {
485
+ scene.stroke_polyline(
486
+ &[g.p(9.5, 6.0), g.p(15.5, 12.0), g.p(9.5, 18.0)],
487
+ g.d(2.4),
488
+ color,
489
+ );
490
+ }
491
+ IconName::Close => {
492
+ scene.stroke_line(g.p(6.5, 6.5), g.p(17.5, 17.5), w, color);
493
+ scene.stroke_line(g.p(17.5, 6.5), g.p(6.5, 17.5), w, color);
494
+ }
495
+ IconName::Minimize => {
496
+ scene.stroke_line(g.p(5.0, 12.0), g.p(19.0, 12.0), w, color);
497
+ }
498
+ IconName::Maximize => {
499
+ scene.stroke_rounded_rect(
500
+ Rect::from_xywh(g.p(5.0, 5.0).x, g.p(5.0, 5.0).y, g.d(14.0), g.d(14.0)),
501
+ g.d(1.5),
502
+ w,
503
+ color,
504
+ );
505
+ }
506
+ IconName::Check => {
507
+ scene.stroke_polyline(
508
+ &[g.p(5.0, 12.5), g.p(10.0, 17.5), g.p(19.0, 6.5)],
509
+ g.d(2.2),
510
+ color,
511
+ );
512
+ }
513
+ IconName::Plus => {
514
+ scene.stroke_line(g.p(12.0, 5.0), g.p(12.0, 19.0), w, color);
515
+ scene.stroke_line(g.p(5.0, 12.0), g.p(19.0, 12.0), w, color);
516
+ }
517
+ IconName::Home => {
518
+ scene.fill_polygon(
519
+ &[
520
+ g.p(12.0, 3.5),
521
+ g.p(21.0, 11.5),
522
+ g.p(18.5, 11.5),
523
+ g.p(18.5, 20.0),
524
+ g.p(13.5, 20.0),
525
+ g.p(13.5, 15.0),
526
+ g.p(10.5, 15.0),
527
+ g.p(10.5, 20.0),
528
+ g.p(5.5, 20.0),
529
+ g.p(5.5, 11.5),
530
+ g.p(3.0, 11.5),
531
+ ],
532
+ color,
533
+ );
534
+ }
535
+ IconName::User => {
536
+ scene.stroke_circle(g.p(12.0, 9.0), g.d(4.0), w, color);
537
+ scene.stroke_polyline(
538
+ &[
539
+ g.p(5.5, 20.5),
540
+ g.p(5.5, 18.5),
541
+ g.p(8.5, 15.5),
542
+ g.p(15.5, 15.5),
543
+ g.p(18.5, 18.5),
544
+ g.p(18.5, 20.5),
545
+ ],
546
+ w,
547
+ color,
548
+ );
549
+ }
550
+ IconName::Info => {
551
+ scene.stroke_circle(g.p(12.0, 12.0), g.d(8.0), w, color);
552
+ scene.fill_circle(g.p(12.0, 7.8), g.d(1.3), color);
553
+ scene.stroke_line(g.p(12.0, 11.0), g.p(12.0, 16.5), w, color);
554
+ }
555
+ }
556
+}
557
+
558
+#[cfg(test)]
559
+mod icon_tests {
560
+ use super::*;
561
+ use libluna_core::Theme;
562
+ use libluna_render::{Scene, TextEngine};
563
+
564
+ #[test]
565
+ fn every_icon_draws_without_panic() {
566
+ let theme = Theme::dark();
567
+ let text = TextEngine::none();
568
+ for name in [
569
+ IconName::Menu,
570
+ IconName::Search,
571
+ IconName::Files,
572
+ IconName::Settings,
573
+ IconName::Terminal,
574
+ IconName::Music,
575
+ IconName::Star,
576
+ IconName::Clock,
577
+ IconName::Store,
578
+ IconName::Edit,
579
+ IconName::Wifi,
580
+ IconName::Volume,
581
+ IconName::Battery,
582
+ IconName::Power,
583
+ IconName::Grid,
584
+ IconName::List,
585
+ IconName::Carousel,
586
+ IconName::ChevronLeft,
587
+ IconName::ChevronRight,
588
+ IconName::Close,
589
+ IconName::Minimize,
590
+ IconName::Maximize,
591
+ IconName::Check,
592
+ IconName::Plus,
593
+ IconName::Home,
594
+ IconName::User,
595
+ IconName::Info,
596
+ ] {
597
+ let mut scene = Scene::new();
598
+ let mut ctx = PaintCtx::new(&mut scene, &theme, &text);
599
+ paint_icon(
600
+ &mut ctx,
601
+ name,
602
+ Rect::from_xywh(0.0, 0.0, 24.0, 24.0),
603
+ theme.colors.text,
604
+ );
605
+ assert!(!scene.is_empty(), "ikona {name:?} nic nie rysuje");
606
+ }
607
+ }
608
+}
@@ -172,6 +172,7 @@ pub use helpers::{
172
172
BreakpointBin, ButtonContent, Clamp, ToolbarRects, ToolbarView, WindowHandle, WindowTitle,
173
173
WindowTitleRects, WrapBox,
174
174
};
175
+pub use icon::{paint_icon, IconName};
175
176
pub use image::{Image, ImageIcon};
176
177
pub use infobar::{InfoBar, InfoBarKind, InfoBarRects};
177
178
pub use item_view::{Item, ItemKind, ItemView, ViewMode};
@@ -71,8 +71,9 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
71
71
Selene.
72
72
73
73
- `de/luna/src/panel.rs` — **panel górny** (sam przycisk burger, tytuł okna, zegar/data,
74
- przełączniki, przestrzenie robocze, użytkownik) i **launcher** w trzech widokach:
74
+ przełączniki z ikonami, przestrzenie robocze, użytkownik) i **launcher** w trzech widokach:
75
75
**Lista**, **Siatka**, **Karuzela** (segmentowy przełącznik w nagłówku).
76
+ Ikony z pakietu `libluna-widgets::icon` (`IconName`/`paint_icon`) — wektorowe, spójne ze stylem.
76
77
- `luna-panel` (bin) — render headless: `docs/images/luna-shell-{light,dark}.png`
77
78
oraz `docs/images/luna-launcher-{list,grid,carousel}.png`.
78
79
- `luna-shell` (bin) — powierzchnia „chrome” na żywo: burger otwiera/zamyka launcher,
@@ -9,7 +9,7 @@
9
9
use libluna_core::style::weight;
10
10
use libluna_core::{Color, Point, Rect, Size, Theme};
11
11
use libluna_render::TextEngine;
12
-use libluna_widgets::PaintCtx;
12
+use libluna_widgets::{paint_icon, IconName, PaintCtx};
13
13
14
14
/// Wysokość panelu (piksele logiczne).
15
15
pub const THICKNESS: f64 = 40.0;
@@ -24,12 +24,12 @@ const LIST_ROW_H: f64 = 52.0;
24
24
const CARD_W: f64 = 168.0;
25
25
const CARD_H: f64 = 200.0;
26
26
27
-/// Przełącznik szybkiego dostępu (Wi-Fi, dźwięk, jasność…).
27
+/// Przełącznik szybkiego dostępu (Wi-Fi, dźwięk, bateria…).
28
28
#[derive(Debug, Clone)]
29
29
pub struct Toggle {
30
30
pub label: String,
31
- /// Symbol rysowany fontem interfejsu (może być pusty).
32
- pub glyph: String,
31
+ /// Ikona z pakietu Luny.
32
+ pub icon: IconName,
33
33
pub active: bool,
34
34
}
35
35
@@ -64,17 +64,17 @@ impl PanelState {
64
64
toggles: vec![
65
65
Toggle {
66
66
label: "Wi-Fi".to_owned(),
67
- glyph: "▲".to_owned(),
67
+ icon: IconName::Wifi,
68
68
active: true,
69
69
},
70
70
Toggle {
71
71
label: "Dźwięk".to_owned(),
72
- glyph: "◆".to_owned(),
72
+ icon: IconName::Volume,
73
73
active: true,
74
74
},
75
75
Toggle {
76
76
label: "82%".to_owned(),
77
- glyph: "■".to_owned(),
77
+ icon: IconName::Battery,
78
78
active: false,
79
79
},
80
80
],
@@ -143,8 +143,8 @@ impl Launcher {
143
143
/// Pozycja w launcherze.
144
144
#[derive(Debug, Clone)]
145
145
pub struct App {
146
- /// Symbol rysowany fontem interfejsu.
147
- pub glyph: String,
146
+ /// Ikona z pakietu Luny.
147
+ pub icon: IconName,
148
148
pub name: String,
149
149
/// Krótki opis/kategoria (widok listy).
150
150
pub category: String,
@@ -153,19 +153,19 @@ pub struct App {
153
153
impl App {
154
154
/// Przykładowy zestaw ikon (podglądy).
155
155
pub fn sample() -> Vec<App> {
156
- const APPS: [(&str, &str, &str); 8] = [
157
- ("◆", "Pliki", "System"),
158
- ("●", "Ustawienia", "System"),
159
- ("▲", "Edytor", "Akcesoria"),
160
- ("▶", "Odtwarzacz", "Multimedia"),
161
- ("★", "Ulubione", "System"),
162
- ("☀", "Zegar", "Akcesoria"),
163
- ("■", "Terminal", "System"),
164
- ("♥", "Sklep", "System"),
156
+ const APPS: [(IconName, &str, &str); 8] = [
157
+ (IconName::Files, "Pliki", "System"),
158
+ (IconName::Settings, "Ustawienia", "System"),
159
+ (IconName::Edit, "Edytor", "Akcesoria"),
160
+ (IconName::Music, "Odtwarzacz", "Multimedia"),
161
+ (IconName::Star, "Ulubione", "System"),
162
+ (IconName::Clock, "Zegar", "Akcesoria"),
163
+ (IconName::Terminal, "Terminal", "System"),
164
+ (IconName::Store, "Sklep", "System"),
165
165
];
166
166
APPS.iter()
167
- .map(|(g, n, c)| App {
168
- glyph: (*g).to_owned(),
167
+ .map(|(i, n, c)| App {
168
+ icon: *i,
169
169
name: (*n).to_owned(),
170
170
category: (*c).to_owned(),
171
171
})
@@ -192,16 +192,20 @@ pub fn paint_panel(panel: &PanelState, ctx: &mut PaintCtx, rect: Rect) {
192
192
t.colors.border,
193
193
);
194
194
195
- // ── Lewa grupa: sam burger + separator + tytuł okna ──
195
+ // ── Lewa grupa: sam burger (ikona Menu) + separator + tytuł okna ──
196
196
let burger = burger_rect(rect, t);
197
197
ctx.scene
198
198
.fill_rounded_rect(burger, t.radius.small, t.colors.shade);
199
- ctx.label_center(
200
- burger.center().x,
201
- burger.center().y,
202
- "☰",
203
- body,
204
- weight::BOLD,
199
+ let isz = burger.width() * 0.64;
200
+ paint_icon(
201
+ ctx,
202
+ IconName::Menu,
203
+ Rect::from_xywh(
204
+ burger.center().x - isz / 2.0,
205
+ burger.center().y - isz / 2.0,
206
+ isz,
207
+ isz,
208
+ ),
205
209
t.colors.text,
206
210
);
207
211
let mut x = burger.right() + t.spacing.md;
@@ -232,15 +236,12 @@ pub fn paint_panel(panel: &PanelState, ctx: &mut PaintCtx, rect: Rect) {
232
236
right -= t.spacing.md;
233
237
234
238
for toggle in panel.toggles.iter().rev() {
235
- let label = if toggle.glyph.is_empty() {
236
- toggle.label.clone()
237
- } else {
238
- format!("{} {}", toggle.glyph, toggle.label)
239
- };
240
239
let cap = t.typography.caption.size;
241
- let w = ctx.measure(&label, cap, weight::MEDIUM) + t.spacing.lg;
240
+ let lw = ctx.measure(&toggle.label, cap, weight::MEDIUM);
241
+ let icon = 13.0;
242
+ let w = t.spacing.sm * 2.0 + icon + t.spacing.xs + lw;
242
243
let pill = Rect::from_xywh(right - w, cy - 12.0, w, 24.0);
243
- paint_pill(ctx, pill, &label, toggle.active);
244
+ paint_toggle_pill(ctx, pill, toggle);
244
245
right = pill.x() - t.spacing.sm;
245
246
}
246
247
@@ -257,7 +258,43 @@ pub fn paint_panel(panel: &PanelState, ctx: &mut PaintCtx, rect: Rect) {
257
258
}
258
259
}
259
260
260
-/// Pigułka przełącznika (aktywna = akcent).
261
+/// Pigułka przełącznika szybkiego dostępu (ikona + etykieta).
262
+fn paint_toggle_pill(ctx: &mut PaintCtx, rect: Rect, toggle: &Toggle) {
263
+ let t = ctx.theme;
264
+ let bg = if toggle.active {
265
+ t.colors.accent
266
+ } else {
267
+ t.colors.card_bg
268
+ };
269
+ let fg = if toggle.active {
270
+ t.colors.accent_fg
271
+ } else {
272
+ t.colors.text
273
+ };
274
+ ctx.scene.fill_rounded_rect(rect, rect.height() / 2.0, bg);
275
+ if !toggle.active {
276
+ ctx.scene
277
+ .stroke_rounded_rect(rect, rect.height() / 2.0, 1.0, t.colors.border);
278
+ }
279
+ let isz = 13.0;
280
+ let ix = rect.x() + t.spacing.sm;
281
+ paint_icon(
282
+ ctx,
283
+ toggle.icon,
284
+ Rect::from_xywh(ix, rect.center().y - isz / 2.0, isz, isz),
285
+ fg,
286
+ );
287
+ ctx.label_left(
288
+ ix + isz + t.spacing.xs,
289
+ rect.center().y,
290
+ &toggle.label,
291
+ t.typography.caption.size,
292
+ weight::MEDIUM,
293
+ fg,
294
+ );
295
+}
296
+
297
+/// Pigułka segmentu (np. przełącznik widoku launchera).
261
298
fn paint_pill(ctx: &mut PaintCtx, rect: Rect, label: &str, active: bool) {
262
299
let t = ctx.theme;
263
300
let bg = if active {
@@ -497,14 +534,8 @@ fn paint_grid_tile(app: &App, ctx: &mut PaintCtx, rect: Rect) {
497
534
let icon = Rect::from_xywh(rect.center().x - 18.0, rect.y() + 12.0, 36.0, 36.0);
498
535
ctx.scene
499
536
.fill_rounded_rect(icon, t.radius.small, t.colors.accent.scale_alpha(0.16));
500
- ctx.label_center(
501
- icon.center().x,
502
- icon.center().y,
503
- &app.glyph,
504
- 18.0,
505
- weight::MEDIUM,
506
- t.colors.accent,
507
- );
537
+ let inner = Rect::from_xywh(icon.x() + 9.0, icon.y() + 9.0, 18.0, 18.0);
538
+ paint_icon(ctx, app.icon, inner, t.colors.accent);
508
539
ctx.label_center(
509
540
rect.center().x,
510
541
icon.bottom() + 12.0,
@@ -526,14 +557,8 @@ fn paint_list_row(app: &App, ctx: &mut PaintCtx, rect: Rect) {
526
557
let icon = Rect::from_xywh(rect.x() + t.spacing.md, rect.center().y - 16.0, 32.0, 32.0);
527
558
ctx.scene
528
559
.fill_rounded_rect(icon, t.radius.small, t.colors.accent.scale_alpha(0.16));
529
- ctx.label_center(
530
- icon.center().x,
531
- icon.center().y,
532
- &app.glyph,
533
- 16.0,
534
- weight::MEDIUM,
535
- t.colors.accent,
536
- );
560
+ let inner = Rect::from_xywh(icon.x() + 7.0, icon.y() + 7.0, 18.0, 18.0);
561
+ paint_icon(ctx, app.icon, inner, t.colors.accent);
537
562
ctx.label_left(
538
563
icon.right() + t.spacing.md,
539
564
rect.center().y,
@@ -576,14 +601,8 @@ fn paint_carousel_card(app: &App, selected: bool, ctx: &mut PaintCtx, rect: Rect
576
601
let icon = Rect::from_xywh(rect.center().x - 34.0, rect.y() + 36.0, 68.0, 68.0);
577
602
ctx.scene
578
603
.fill_rounded_rect(icon, t.radius.medium, t.colors.accent.scale_alpha(0.18));
579
- ctx.label_center(
580
- icon.center().x,
581
- icon.center().y,
582
- &app.glyph,
583
- 32.0,
584
- weight::MEDIUM,
585
- t.colors.accent,
586
- );
604
+ let inner = Rect::from_xywh(icon.x() + 16.0, icon.y() + 16.0, 36.0, 36.0);
605
+ paint_icon(ctx, app.icon, inner, t.colors.accent);
587
606
ctx.label_center(
588
607
rect.center().x,
589
608
icon.bottom() + 24.0,
590
609
Binary files a/docs/images/luna-launcher-carousel.png and b/docs/images/luna-launcher-carousel.png differ
591
610
Binary files a/docs/images/luna-launcher-grid.png and b/docs/images/luna-launcher-grid.png differ
592
611
Binary files a/docs/images/luna-launcher-list.png and b/docs/images/luna-launcher-list.png differ
593
612
Binary files a/docs/images/luna-shell-dark.png and b/docs/images/luna-shell-dark.png differ
594
613
Binary files a/docs/images/luna-shell-light.png and b/docs/images/luna-shell-light.png differ
@@ -62,6 +62,11 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
62
62
Doszły odpowiedniki GTK4: `InfoBar` (4 rodzaje + zamknięcie), `Picture` (`ContentFit`:
63
63
contain/cover/fill/fit-width/fit-height) i `MediaControls` (play/pauza + pasek postępu +
64
64
czasy); podgląd: `cargo run -p luna-demo --bin new-widgets` → `docs/images/new-widgets.png`.
65
+- **Pakiet ikon symbolicznych** (`libluna-widgets::icon`): `IconName` + `paint_icon`,
66
+ ~27 ikon rysowanych wektorowo w naszym stylu (Menu/Search/Files/Settings/Terminal/Music/
67
+ Star/Clock/Store/Edit/Wifi/Volume/Battery/Power/Grid/List/Carousel/…). Renderer zyskał
68
+ prymitywy: `fill_circle`, `stroke_circle`, `stroke_polyline`, `fill_polygon` (z zaokrąglonymi
69
+ końcami). Ikony użyte w panelu i launcherze Luny (zamiast glifów fontu).
65
70
Tokeny metryk kontenerów: `Theme::metrics.container`. Animacje: `libluna-core` `motion`
66
71
(`Easing`, `lerp`, `Animation`), użyte w `Stack` (przejścia `StackTransition`), `Revealer`,
67
72
`Toast`, `BottomSheet` i `Pager` (sterowane `advance(dt, …)`).
@@ -180,6 +185,8 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
180
185
181
186
## Później (poza MVP)
182
187
- Dostępność AT-SPI, IME, schowek, drag & drop, animacje, backend GPU, mini-CSS dla motywów.
188
+- Renderer: **klip nie obejmuje tekstu** (`DrawCommand::Text` ignoruje maskę) — naprawić,
189
+ bo dotyczy `ScrollView` i innych przewijanych widoków.
183
190
184
191
## Inwentarz widgetów (z makiet → moduł docelowy)
185
192