← luna

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