Skip to content

Commit d4c14ce

Browse files
committed
Normalize color handling and add ODP properties UI
Unify and sanitize color logic across shape and ODP plugins: properly resolve theme_auto using layout.darkMode, treat "transparent" consistently (map to null or fallback black where needed), and prefer explicit bg_color/border_color over color when set. Update rendering and export paths for arc, ellipse, polygon, rectangle_pattern, icon_sequence, rounded_rect, shape_circle, and shape_rect to use the effective colors and fixed outline/fill fallbacks, adjust dither/export calls accordingly, and ensure opacity is applied in render. Add a Fill checkbox and new ODP-specific property sections (ODP Style, Plot Config, Multiline Appearance) to the properties panel for editing ODP widgets and multiline content. Misc: tweak default circle size, use LVGL "CIRCLE" radius for circles, and improve generated comments/metadata for code exports.
1 parent e0be518 commit d4c14ce

File tree

10 files changed

+209
-73
lines changed

10 files changed

+209
-73
lines changed

custom_components/esphome_designer/frontend/features/odp_arc/plugin.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,29 +65,37 @@ export default {
6565
const p = w.props || {};
6666
const cx = Math.round(w.x + w.width / 2);
6767
const cy = Math.round(w.y + w.height / 2);
68+
69+
let outline = (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : (p.outline || "black");
70+
if (outline === "transparent") outline = "black";
71+
6872
return {
6973
type: "arc",
7074
x: cx,
7175
y: cy,
7276
radius: p.radius || Math.round(Math.min(w.width, w.height) / 2),
7377
start_angle: p.start_angle || 0,
7478
end_angle: p.end_angle || 90,
75-
outline: (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : (p.outline || "black"),
79+
outline: outline,
7680
width: p.border_width || 2
7781
};
7882
},
7983
exportOEPL: (w, { layout, page }) => {
8084
const p = w.props || {};
8185
const cx = Math.round(w.x + w.width / 2);
8286
const cy = Math.round(w.y + w.height / 2);
87+
88+
let outline = p.outline || "black";
89+
if (outline === "transparent") outline = "black";
90+
8391
return {
8492
type: "arc",
8593
x: cx,
8694
y: cy,
8795
radius: p.radius || Math.round(Math.min(w.width, w.height) / 2),
8896
start_angle: p.start_angle || 0,
8997
end_angle: p.end_angle || 90,
90-
outline: p.outline || "black",
98+
outline: outline,
9199
width: p.border_width || 2
92100
};
93101
}

custom_components/esphome_designer/frontend/features/odp_ellipse/plugin.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,36 @@ export default {
2727
render,
2828
exportOpenDisplay: (w, { layout, page }) => {
2929
const p = w.props || {};
30+
let fill = (p.fill === "theme_auto") ? (layout?.darkMode ? "white" : "black") : (p.fill || null);
31+
if (fill === "transparent") fill = null;
32+
33+
let outline = (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : (p.outline || "black");
34+
if (outline === "transparent") outline = "black"; // Outline shouldn't be transparent in ODP or it vanishes
35+
3036
return {
3137
type: "ellipse",
3238
visible: true,
3339
x_start: Math.round(w.x),
3440
y_start: Math.round(w.y),
3541
x_end: Math.round(w.x + w.width),
3642
y_end: Math.round(w.y + w.height),
37-
fill: (p.fill === "theme_auto") ? (layout?.darkMode ? "white" : "black") : (p.fill || null),
38-
outline: (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : (p.outline || "black"),
43+
fill: fill,
44+
outline: outline,
3945
width: p.border_width || 1
4046
};
4147
},
4248
exportOEPL: (w, { layout, page }) => {
4349
const p = w.props || {};
50+
let fill = p.fill || null;
51+
if (fill === "transparent") fill = null;
52+
4453
return {
4554
type: "ellipse",
4655
x_start: Math.round(w.x),
4756
y_start: Math.round(w.y),
4857
x_end: Math.round(w.x + w.width),
4958
y_end: Math.round(w.y + w.height),
50-
fill: p.fill || null,
59+
fill: fill,
5160
outline: p.outline || "black",
5261
width: p.border_width || 1
5362
};

custom_components/esphome_designer/frontend/features/odp_icon_sequence/plugin.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export default {
6666
if (typeof icons === "string") {
6767
icons = icons.split(",").map(s => s.trim()).filter(s => s);
6868
}
69+
70+
let color = p.fill || "black";
71+
if (color === "theme_auto") color = layout?.darkMode ? "white" : "black";
72+
if (color === "transparent") color = "black";
73+
6974
return {
7075
type: "icon_sequence",
7176
visible: true,
@@ -75,7 +80,7 @@ export default {
7580
size: p.size || 24,
7681
direction: p.direction || "right",
7782
spacing: p.spacing || 6,
78-
fill: p.fill || "black"
83+
fill: color
7984
};
8085
},
8186
exportOEPL: (w, { layout, page }) => {
@@ -84,6 +89,10 @@ export default {
8489
if (typeof icons === "string") {
8590
icons = icons.split(",").map(s => s.trim()).filter(s => s);
8691
}
92+
93+
let color = p.fill || "black";
94+
if (color === "transparent") color = "black";
95+
8796
return {
8897
type: "icon_sequence",
8998
x: Math.round(w.x),
@@ -92,7 +101,7 @@ export default {
92101
size: p.size || 24,
93102
direction: p.direction || "right",
94103
spacing: p.spacing || 6,
95-
fill: p.fill || "black"
104+
fill: color
96105
};
97106
}
98107
};

custom_components/esphome_designer/frontend/features/odp_multiline/plugin.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export default {
5454
if (color === "theme_auto") {
5555
color = layout?.darkMode ? "white" : "black";
5656
}
57+
if (color === "transparent") color = "black";
5758

5859
return {
5960
type: "multiline",
@@ -75,6 +76,7 @@ export default {
7576
if (color === "theme_auto") {
7677
color = layout?.darkMode ? "white" : "black";
7778
}
79+
if (color === "transparent") color = "black";
7880

7981
const size = p.font_size || 16;
8082
const lineSpacing = p.line_spacing || 4;

custom_components/esphome_designer/frontend/features/odp_polygon/plugin.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,18 @@ export default {
7676
}
7777
// Offset points by widget position
7878
const offsetPoints = points.map(([x, y]) => [Math.round(w.x + x), Math.round(w.y + y)]);
79+
80+
let fill = (p.fill === "theme_auto" || !p.fill) ? (layout?.darkMode ? "white" : "black") : (p.fill || "red");
81+
if (fill === "transparent") fill = null;
82+
83+
let outline = (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : (p.outline || "black");
84+
if (outline === "transparent") outline = "black";
85+
7986
return {
8087
type: "polygon",
8188
points: offsetPoints,
82-
fill: (p.fill === "theme_auto" || !p.fill) ? (layout?.darkMode ? "white" : "black") : (p.fill || "red"),
83-
outline: (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : (p.outline || "black"),
89+
fill: fill,
90+
outline: outline,
8491
width: p.border_width || 1
8592
};
8693
},
@@ -91,10 +98,14 @@ export default {
9198
try { points = JSON.parse(points); } catch (e) { points = [[0, 0], [50, 0], [50, 50], [0, 50]]; }
9299
}
93100
const offsetPoints = points.map(([x, y]) => [Math.round(w.x + x), Math.round(w.y + y)]);
101+
102+
let fill = p.fill || "red";
103+
if (fill === "transparent") fill = null;
104+
94105
return {
95106
type: "polygon",
96107
points: offsetPoints,
97-
fill: p.fill || "red",
108+
fill: fill,
98109
outline: p.outline || "black",
99110
width: p.border_width || 1
100111
};

custom_components/esphome_designer/frontend/features/odp_rectangle_pattern/plugin.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export default {
5656
render,
5757
exportOpenDisplay: (w, { layout, page }) => {
5858
const p = w.props || {};
59+
let fill = (p.fill === "theme_auto" || !p.fill) ? (layout?.darkMode ? "white" : "black") : p.fill;
60+
if (fill === "transparent") fill = null;
61+
62+
let outline = (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : p.outline;
63+
if (outline === "transparent") outline = "black";
64+
5965
return {
6066
type: "rectangle_pattern",
6167
x_start: Math.round(w.x),
@@ -66,15 +72,18 @@ export default {
6672
y_offset: p.y_offset || 5,
6773
x_repeat: p.x_repeat || 3,
6874
y_repeat: p.y_repeat || 2,
69-
fill: (p.fill === "theme_auto" || !p.fill) ? (layout?.darkMode ? "white" : "black") : p.fill,
70-
outline: (p.outline === "theme_auto" || !p.outline) ? (layout?.darkMode ? "white" : "black") : p.outline,
75+
fill: fill,
76+
outline: outline,
7177
width: p.border_width || 1,
7278
x_end: Math.round(w.x + w.width),
7379
y_end: Math.round(w.y + w.height)
7480
};
7581
},
7682
exportOEPL: (w, { layout, page }) => {
7783
const p = w.props || {};
84+
let fill = p.fill || "white";
85+
if (fill === "transparent") fill = null;
86+
7887
return {
7988
type: "rectangle_pattern",
8089
x_start: Math.round(w.x),
@@ -85,7 +94,7 @@ export default {
8594
y_offset: p.y_offset || 5,
8695
x_repeat: p.x_repeat || 3,
8796
y_repeat: p.y_repeat || 2,
88-
fill: p.fill || "white",
97+
fill: fill,
8998
outline: p.outline || "black",
9099
width: p.border_width || 1,
91100
x_end: Math.round(w.x + w.width),

custom_components/esphome_designer/frontend/features/rounded_rect/plugin.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@ const render = (el, widget, { getColorStyle }) => {
66
const props = widget.props || {};
77
const radius = parseInt(props.radius || 10, 10);
88
const borderWidth = parseInt(props.border_width || 4, 10);
9+
910
const color = props.color || "theme_auto";
11+
// bg_color/border_color only override 'color' when explicitly set to a real color
12+
const effectiveBg = (props.bg_color && props.bg_color !== "theme_auto") ? props.bg_color : null;
13+
const effectiveBorder = (props.border_color && props.border_color !== "theme_auto") ? props.border_color : null;
14+
15+
const bgCol = effectiveBg || color;
16+
const borderCol = effectiveBorder || (props.fill ? (props.show_border === false ? bgCol : "black") : color);
17+
18+
el.style.backgroundColor = props.fill ? getColorStyle(bgCol) : "transparent";
1019

11-
el.style.backgroundColor = props.fill ? getColorStyle(color) : "transparent";
1220
const borderColor = (props.fill && (props.show_border === false || props.show_border === "false"))
13-
? getColorStyle(color)
14-
: getColorStyle(props.border_color || (props.fill ? "black" : color));
21+
? getColorStyle(bgCol)
22+
: getColorStyle(borderCol);
1523

1624
el.style.border = `${borderWidth}px solid ${borderColor}`;
1725
el.style.borderRadius = `${radius}px`;
@@ -49,8 +57,6 @@ export default {
4957
fill: false,
5058
border_width: 4,
5159
color: "theme_auto",
52-
bg_color: "theme_auto",
53-
border_color: "theme_auto",
5460
show_border: true,
5561
opa: 255
5662
},
@@ -103,16 +109,20 @@ export default {
103109
const showBorder = p.show_border !== false;
104110
const r = parseInt(p.radius || 10, 10);
105111
const thickness = parseInt(p.border_width || 4, 10);
112+
106113
const colorProp = p.color || "theme_auto";
107-
const borderColorProp = p.border_color || (fill ? "black" : colorProp);
108-
const color = getColorConst(colorProp);
114+
const fillColorProp = (p.bg_color && p.bg_color !== "theme_auto") ? p.bg_color : colorProp;
115+
const borderColorProp = (p.border_color && p.border_color !== "theme_auto") ? p.border_color : (fill ? "black" : colorProp);
116+
117+
const fillColor = getColorConst(fillColorProp);
109118
const borderColor = getColorConst(borderColorProp);
119+
110120
const rrectX = Math.floor(w.x);
111121
const rrectY = Math.floor(w.y + (typeof RECT_Y_OFFSET !== 'undefined' ? RECT_Y_OFFSET : 0));
112122
const rrectW = Math.floor(w.width);
113123
const rrectH = Math.floor(w.height);
114124

115-
lines.push(` // widget:rounded_rect id:${w.id} type:rounded_rect x:${rrectX} y:${rrectY} w:${rrectW} h:${rrectH} fill:${fill} show_border:${showBorder} border:${thickness} radius:${r} color:${colorProp} border_color:${borderColorProp} ${getCondProps(w)}`);
125+
lines.push(` // widget:rounded_rect id:${w.id} type:rounded_rect x:${rrectX} y:${rrectY} w:${rrectW} h:${rrectH} fill:${fill} show_border:${showBorder} border:${thickness} radius:${r} color:${fillColorProp} border_color:${borderColorProp} ${getCondProps(w)}`);
116126

117127
const cond = getConditionCheck(w);
118128
if (cond) lines.push(` ${cond}`);
@@ -137,9 +147,9 @@ export default {
137147
if (fr < 0) fr = 0;
138148
}
139149
if (colorProp.toLowerCase() === "gray" && isEpaper) {
140-
addDitherMask(lines, colorProp, isEpaper, fx, fy, fw, fh, fr);
150+
addDitherMask(lines, fillColorProp, isEpaper, fx, fy, fw, fh, fr);
141151
} else {
142-
if (fw > 0 && fh > 0) lines.push(` draw_filled_rrect(${fx}, ${fy}, ${fw}, ${fh}, ${fr}, ${color});`);
152+
if (fw > 0 && fh > 0) lines.push(` draw_filled_rrect(${fx}, ${fy}, ${fw}, ${fh}, ${fr}, ${fillColor});`);
143153
}
144154
} else {
145155
// Transparent Border logic

0 commit comments

Comments
 (0)