text shadow + battery display poc

This commit is contained in:
galister
2025-09-29 17:43:16 +09:00
parent a78ae55bdc
commit 90bdf99e32
11 changed files with 105 additions and 32 deletions

View File

@@ -100,6 +100,16 @@ _Text size in pixel units_
`weight`: "normal" | "bold" `weight`: "normal" | "bold"
`shadow`: #112233 | #112233CC (default: None)
`shadow_x`: **float** (default: 1.5)
_Horizontal offset of the shadow from the original text. Positive is right._
`shadow_y`: **float** (default: 1.5)
_Vertical offset of the shadow from the original text. Positive is down._
--- ---
## rectangle widget ## rectangle widget

View File

@@ -7,7 +7,7 @@ use taffy::TraversePartialTree;
use crate::{ use crate::{
drawing, drawing,
layout::Widget, layout::Widget,
renderer_vk::text::custom_glyph::CustomGlyph, renderer_vk::text::{custom_glyph::CustomGlyph, TextShadow},
stack::{self, ScissorStack, TransformStack}, stack::{self, ScissorStack, TransformStack},
widget::{self}, widget::{self},
}; };
@@ -135,7 +135,7 @@ pub struct PrimitiveExtent {
pub enum RenderPrimitive { pub enum RenderPrimitive {
Rectangle(PrimitiveExtent, Rectangle), Rectangle(PrimitiveExtent, Rectangle),
Text(PrimitiveExtent, Rc<RefCell<Buffer>>), Text(PrimitiveExtent, Rc<RefCell<Buffer>>, Option<TextShadow>),
Sprite(PrimitiveExtent, Option<CustomGlyph>), //option because we want as_slice Sprite(PrimitiveExtent, Option<CustomGlyph>), //option because we want as_slice
ScissorEnable(Boundary), ScissorEnable(Boundary),
ScissorDisable, ScissorDisable,

View File

@@ -78,6 +78,25 @@ pub fn parse_text_style(attribs: &[AttribPair]) -> TextStyle {
print_invalid_attrib(key, value); print_invalid_attrib(key, value);
} }
} }
"shadow" => {
if let Some(color) = parse_color_hex(value) {
style.shadow.get_or_insert_default().color = color;
}
}
"shadow_x" => {
if let Ok(x) = value.parse::<f32>() {
style.shadow.get_or_insert_default().x = x;
} else {
print_invalid_attrib(key, value);
}
}
"shadow_y" => {
if let Ok(y) = value.parse::<f32>() {
style.shadow.get_or_insert_default().y = y;
} else {
print_invalid_attrib(key, value);
}
}
_ => {} _ => {}
} }
} }

View File

@@ -2,20 +2,20 @@ use std::{cell::RefCell, rc::Rc, sync::Arc};
use cosmic_text::Buffer; use cosmic_text::Buffer;
use glam::{Mat4, Vec2, Vec3}; use glam::{Mat4, Vec2, Vec3};
use slotmap::{SlotMap, new_key_type}; use slotmap::{new_key_type, SlotMap};
use vulkano::pipeline::graphics::viewport; use vulkano::pipeline::graphics::viewport;
use crate::{ use crate::{
drawing::{self}, drawing::{self},
gfx::{WGfx, cmd::GfxCommandBuffer}, gfx::{cmd::GfxCommandBuffer, WGfx},
}; };
use super::{ use super::{
rect::{RectPipeline, RectRenderer}, rect::{RectPipeline, RectRenderer},
text::{ text::{
DEFAULT_METRICS, FONT_SYSTEM, SWASH_CACHE, TextArea, TextBounds,
text_atlas::{TextAtlas, TextPipeline}, text_atlas::{TextAtlas, TextPipeline},
text_renderer::TextRenderer, text_renderer::TextRenderer,
TextArea, TextBounds, DEFAULT_METRICS, FONT_SYSTEM, SWASH_CACHE,
}, },
viewport::Viewport, viewport::Viewport,
}; };
@@ -248,7 +248,20 @@ impl Context {
.rect_renderer .rect_renderer
.add_rect(extent.boundary, *rectangle, &extent.transform); .add_rect(extent.boundary, *rectangle, &extent.transform);
} }
drawing::RenderPrimitive::Text(extent, text) => { drawing::RenderPrimitive::Text(extent, text, shadow) => {
if let Some(shadow) = shadow {
pass.text_areas.push(TextArea {
buffer: text.clone(),
left: (extent.boundary.pos.x + shadow.x) * self.pixel_scale,
top: (extent.boundary.pos.y + shadow.y) * self.pixel_scale,
bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work
scale: self.pixel_scale,
default_color: cosmic_text::Color::rgb(0, 0, 0),
override_color: Some(shadow.color.into()),
custom_glyphs: &[],
transform: extent.transform,
});
}
pass.text_areas.push(TextArea { pass.text_areas.push(TextArea {
buffer: text.clone(), buffer: text.clone(),
left: extent.boundary.pos.x * self.pixel_scale, left: extent.boundary.pos.x * self.pixel_scale,
@@ -256,6 +269,7 @@ impl Context {
bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work
scale: self.pixel_scale, scale: self.pixel_scale,
default_color: cosmic_text::Color::rgb(0, 0, 0), default_color: cosmic_text::Color::rgb(0, 0, 0),
override_color: None,
custom_glyphs: &[], custom_glyphs: &[],
transform: extent.transform, transform: extent.transform,
}); });
@@ -269,6 +283,7 @@ impl Context {
scale: self.pixel_scale, scale: self.pixel_scale,
custom_glyphs: sprites.as_slice(), custom_glyphs: sprites.as_slice(),
default_color: cosmic_text::Color::rgb(255, 0, 255), default_color: cosmic_text::Color::rgb(255, 0, 255),
override_color: None,
transform: extent.transform, transform: extent.transform,
}); });
} }

View File

@@ -25,6 +25,23 @@ const DEFAULT_LINE_HEIGHT_RATIO: f32 = 1.43;
pub(crate) const DEFAULT_METRICS: Metrics = pub(crate) const DEFAULT_METRICS: Metrics =
Metrics::new(DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_RATIO); Metrics::new(DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_RATIO);
#[derive(Clone)]
pub struct TextShadow {
pub y: f32,
pub x: f32,
pub color: drawing::Color,
}
impl Default for TextShadow {
fn default() -> Self {
Self {
y: 1.5,
x: 1.5,
color: drawing::Color::default(),
}
}
}
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct TextStyle { pub struct TextStyle {
pub size: Option<f32>, pub size: Option<f32>,
@@ -34,6 +51,7 @@ pub struct TextStyle {
pub weight: Option<FontWeight>, pub weight: Option<FontWeight>,
pub align: Option<HorizontalAlign>, pub align: Option<HorizontalAlign>,
pub wrap: bool, pub wrap: bool,
pub shadow: Option<TextShadow>,
} }
impl From<&TextStyle> for Attrs<'_> { impl From<&TextStyle> for Attrs<'_> {
@@ -60,7 +78,11 @@ impl From<&TextStyle> for Metrics {
impl From<&TextStyle> for Wrap { impl From<&TextStyle> for Wrap {
fn from(value: &TextStyle) -> Self { fn from(value: &TextStyle) -> Self {
if value.wrap { Self::WordOrGlyph } else { Self::None } if value.wrap {
Self::WordOrGlyph
} else {
Self::None
}
} }
} }
@@ -199,6 +221,8 @@ pub struct TextArea<'a> {
pub bounds: TextBounds, pub bounds: TextBounds,
/// The default color of the text area. /// The default color of the text area.
pub default_color: Color, pub default_color: Color,
/// Override text color. Used for shadow.
pub override_color: Option<Color>,
/// Additional custom glyphs to render. /// Additional custom glyphs to render.
pub custom_glyphs: &'a [CustomGlyph], pub custom_glyphs: &'a [CustomGlyph],
/// Text transformation /// Text transformation

View File

@@ -4,9 +4,9 @@ use crate::{
}; };
use super::{ use super::{
ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea,
custom_glyph::{CustomGlyphCacheKey, RasterizeCustomGlyphRequest, RasterizedCustomGlyph}, custom_glyph::{CustomGlyphCacheKey, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
text_atlas::{GlyphVertex, TextAtlas, TextPipeline}, text_atlas::{GlyphVertex, TextAtlas, TextPipeline},
ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea,
}; };
use cosmic_text::{Color, SubpixelBin, SwashContent}; use cosmic_text::{Color, SubpixelBin, SwashContent};
use glam::{Mat4, Vec2, Vec3}; use glam::{Mat4, Vec2, Vec3};
@@ -96,7 +96,10 @@ impl TextRenderer {
y_bin, y_bin,
}); });
let color = glyph.color.unwrap_or(text_area.default_color); let color = text_area
.override_color
.or(glyph.color)
.unwrap_or(text_area.default_color);
if let Some(glyph_to_render) = prepare_glyph( if let Some(glyph_to_render) = prepare_glyph(
&mut PrepareGlyphParams { &mut PrepareGlyphParams {
@@ -168,10 +171,10 @@ impl TextRenderer {
for glyph in run.glyphs { for glyph in run.glyphs {
let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale);
let color = match glyph.color_opt { let color = text_area
Some(some) => some, .override_color
None => text_area.default_color, .or(glyph.color_opt)
}; .unwrap_or(text_area.default_color);
if let Some(glyph_to_render) = prepare_glyph( if let Some(glyph_to_render) = prepare_glyph(
&mut PrepareGlyphParams { &mut PrepareGlyphParams {

View File

@@ -10,7 +10,7 @@ use crate::{
globals::Globals, globals::Globals,
i18n::{I18n, Translation}, i18n::{I18n, Translation},
layout::WidgetID, layout::WidgetID,
renderer_vk::text::{FONT_SYSTEM, TextStyle}, renderer_vk::text::{TextStyle, FONT_SYSTEM},
}; };
use super::{WidgetObj, WidgetState}; use super::{WidgetObj, WidgetState};
@@ -124,6 +124,7 @@ impl WidgetObj for WidgetLabel {
transform: state.transform_stack.get().transform, transform: state.transform_stack.get().transform,
}, },
self.buffer.clone(), self.buffer.clone(),
self.params.style.shadow.clone(),
)); ));
} }

View File

@@ -7,8 +7,8 @@ use crate::{
drawing::{self, PrimitiveExtent}, drawing::{self, PrimitiveExtent},
layout::WidgetID, layout::WidgetID,
renderer_vk::text::{ renderer_vk::text::{
DEFAULT_METRICS, FONT_SYSTEM,
custom_glyph::{CustomGlyph, CustomGlyphData}, custom_glyph::{CustomGlyph, CustomGlyphData},
DEFAULT_METRICS, FONT_SYSTEM,
}, },
}; };
@@ -81,6 +81,7 @@ impl WidgetObj for WidgetSprite {
transform: state.transform_stack.get().transform, transform: state.transform_stack.get().transform,
}, },
Rc::new(RefCell::new(buffer)), Rc::new(RefCell::new(buffer)),
None,
)); ));
} }
} }

View File

@@ -20,13 +20,16 @@
<template name="Device"> <template name="Device">
<sprite color="~device_color" width="${size}" height="${size}" src="${src}" /> <sprite color="~device_color" width="${size}" height="${size}" src="${src}" />
<div position="absolute" margin_top="10" margin_left="9">
<label _source="battery" _device="${device}" size="18" shadow="#000000" weight="bold" />
</div>
</template> </template>
<template name="Set"> <template name="Set">
<Button macro="button_style" _press="::OverlayToggle ${handle}"> <Button macro="button_style" _press="::OverlayToggle ${handle}">
<sprite width="40" height="40" color="~set_color" src="watch/set2.svg" /> <sprite width="40" height="40" color="~set_color" src="watch/set2.svg" />
<div position="absolute" margin_top="11"> <div position="absolute" margin_top="9">
<label text="${display}" size="24" color="#000000" weight="bold" /> <label text="${display}" size="24" color="#00050F" weight="bold" />
</div> </div>
</Button> </Button>
</template> </template>
@@ -35,12 +38,12 @@
<div width="400" height="200"> <div width="400" height="200">
<rectangle width="100%" height="100%" padding="4" box_sizing="content_box" flex_wrap="wrap" flex_direction="column" gap="4" color="~bg_color"> <rectangle width="100%" height="100%" padding="4" box_sizing="content_box" flex_wrap="wrap" flex_direction="column" gap="4" color="~bg_color">
<div width="100%" flex_direction="row"> <div width="100%" flex_direction="row">
<Device src="watch/hmd.svg" size="40" /> <Device src="watch/hmd.svg" size="40" device="0" />
<Device src="watch/controller_l.svg" size="36" /> <Device src="watch/controller_l.svg" size="36" device="1" />
<Device src="watch/controller_r.svg" size="36" /> <Device src="watch/controller_r.svg" size="36" device="2" />
<Device src="watch/track3.svg" size="40" /> <Device src="watch/track3.svg" size="40" device="3" />
<Device src="watch/track3.svg" size="40" /> <Device src="watch/track3.svg" size="40" device="4" />
<Device src="watch/track3.svg" size="40" /> <Device src="watch/track3.svg" size="40" device="5" />
</div> </div>
<div flex_direction="row"> <div flex_direction="row">
<div flex_direction="column" padding="4"> <div flex_direction="column" padding="4">

View File

@@ -376,16 +376,13 @@ fn battery_on_tick(
app: &AppState, app: &AppState,
) { ) {
let device = app.input_state.devices.get(state.device); let device = app.input_state.devices.get(state.device);
let tags = ["", "H", "L", "R", "T"];
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap(); let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
if let Some(device) = device if let Some(device) = device
&& let Some(soc) = device.soc && let Some(soc) = device.soc
{ {
let soc = (soc * 100.).min(99.) as u32; let soc = (soc * 100.).min(99.) as u32;
let text = format!("{}{}", tags[device.role as usize], soc); let text = soc.to_string();
let color = if device.charging { let color = if device.charging {
state.charging_color state.charging_color
} else if soc < state.low_threshold { } else if soc < state.low_threshold {

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc};
use glam::{Mat4, Vec2, Vec3, vec2, vec3a}; use glam::{vec2, vec3a, Mat4, Vec2, Vec3};
use wgui::{ use wgui::{
animation::{Animation, AnimationEasing}, animation::{Animation, AnimationEasing},
drawing::Color, drawing::Color,
@@ -20,13 +20,13 @@ use crate::{
backend::overlay::{OverlayData, OverlayState, Positioning}, backend::overlay::{OverlayData, OverlayState, Positioning},
gui::panel::GuiPanel, gui::panel::GuiPanel,
state::AppState, state::AppState,
subsystem::hid::{ALT, CTRL, META, SHIFT, SUPER, XkbKeymap}, subsystem::hid::{XkbKeymap, ALT, CTRL, META, SHIFT, SUPER},
}; };
use super::{ use super::{
KEYBOARD_NAME, KeyButtonData, KeyState, KeyboardBackend, KeyboardState, handle_press, handle_press, handle_release,
handle_release,
layout::{self, AltModifier, KeyCapType}, layout::{self, AltModifier, KeyCapType},
KeyButtonData, KeyState, KeyboardBackend, KeyboardState, KEYBOARD_NAME,
}; };
const BACKGROUND_PADDING: f32 = 4.; const BACKGROUND_PADDING: f32 = 4.;