Source code for iskg.widgets._display

from __future__ import annotations

import contextlib
from typing import Any

from ..base import Widget


[docs] class Label(Widget): """A static text label.""" def __init__( self, parent: Widget | None = None, text: str = "", wraplength: int | None = None, anchor: str = "", justify: str = "", **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["text"] = ( self._textvariable.get() if self._textvariable is not None else text ) if wraplength is not None: self._config_dict["wraplength"] = wraplength if anchor: self._config_dict["anchor"] = anchor if justify: self._config_dict["justify"] = justify @property def text(self) -> str: return self._config_dict.get("text", "") @text.setter def text(self, value: str) -> None: self._config_dict["text"] = str(value) self._sync() @property def wraplength(self) -> int | None: return self._config_dict.get("wraplength") @wraplength.setter def wraplength(self, value: int | None) -> None: if value is not None: self._config_dict["wraplength"] = value else: self._config_dict.pop("wraplength", None) self._sync() @property def anchor(self) -> str: return self._config_dict.get("anchor", "") @anchor.setter def anchor(self, value: str) -> None: self._config_dict["anchor"] = value self._sync() @property def justify(self) -> str: return self._config_dict.get("justify", "") @justify.setter def justify(self, value: str) -> None: self._config_dict["justify"] = value self._sync() def _render(self) -> str: text = self._config_dict.get("text", "") escaped = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") cls = "iskg-label" color = self._config_dict.get("color", "") if color: cls += f" {color}" style = self._render_style() wl = self._config_dict.get("wraplength") if wl is not None: style += f"max-width:{wl}px;" anc = self._config_dict.get("anchor", "") or self._config_dict.get("justify", "") if anc == "center": style += "text-align:center;" elif anc == "e" or anc == "right": style += "text-align:right;" elif anc == "w" or anc == "left": style += "text-align:left;" return f'<span id="{self._id}" class="{cls}" style="{style}">{escaped}</span>' def _var_updated(self, var: Any) -> None: if var is self._textvariable: self._config_dict["text"] = var.get() self._sync() def _render_update_js(self) -> str: text = self._config_dict.get("text", "") escaped = text.replace("\\", "\\\\").replace("'", "\\'") return f'iskg_set_text("{self._id}","{escaped}");'
[docs] class ProgressBar(Widget): """A clickable progress bar widget. Supports click-to-set-value, ``command`` callbacks, ``bind("change", cb)``, and variable binding. Use ``max_`` to set the range maximum. Usage:: pb = ProgressBar(value=50, max_=100, command=lambda: print("clicked")) """ def __init__( self, parent: Widget | None = None, value: float = 0, max_: int = 100, **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["value"] = value self._config_dict["max"] = max_ @property def value(self) -> float: return self._config_dict.get("value", 0) @value.setter def value(self, v: float) -> None: self._config_dict["value"] = float(v) self._sync() def _render(self) -> str: val = self._config_dict.get("value", 0) mx = self._config_dict.get("max", 100) pct = min(100, max(0, (val / mx * 100))) if mx > 0 else 0 show_text = self._config_dict.get("show_text", False) style = self._render_style() width = self._config_dict.get("width", 200) text_html = "" if show_text: text_html = f'<span class="iskg-progress-text">{int(pct)}%</span>' return f'''<div class="iskg-progress-wrap" id="{self._id}" style="{style}width:{width}px;"> <div class="iskg-progress-fill" id="{self._id}-fill" style="width:{pct}%"></div> {text_html} </div>''' def _render_update_js(self) -> str: val = self._config_dict.get("value", 0) mx = self._config_dict.get("max", 100) pct = min(100, max(0, (val / mx * 100))) if mx > 0 else 0 return f'''var el=document.getElementById("{self._id}-fill"); if(el)el.style.width="{pct}%"; var t=document.getElementById("{self._id}").querySelector(".iskg-progress-text"); if(t)t.innerText="{int(pct)}%";''' def _handle_bridge_event(self, event_name: str, event_data: Any) -> str | None: if event_name == "change": with contextlib.suppress(ValueError, TypeError): self._config_dict["value"] = float(event_data) return super()._handle_bridge_event(event_name, event_data) def _render_js(self) -> str: mx = self._config_dict.get("max", 100) return f'''document.getElementById("{self._id}").onclick=function(e){{ var r=this.getBoundingClientRect(); var pct=Math.max(0,Math.min(100,(e.clientX-r.left)/r.width*100)); var val=pct/100*{mx}; this.querySelector(".iskg-progress-fill").style.width=pct+"%"; var t=this.querySelector(".iskg-progress-text"); if(t)t.innerText=Math.round(pct)+"%"; iskg_bridge_event("{self._id}","change",val.toString()); }};'''
[docs] class LEDDisplay(Widget): """A seven-segment LED-style digital display.""" def __init__( self, parent: Widget | None = None, value: Any = 0, digits: int = 4, color: str = "green", label: str = "", height: int = 32, **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["value"] = value self._config_dict["digits"] = digits self._config_dict["color"] = color self._config_dict["label"] = label self._config_dict["height"] = height @property def value(self) -> Any: return self._config_dict.get("value", 0) @value.setter def value(self, v: Any) -> None: self._config_dict["value"] = v self._sync() def _render(self) -> str: digits = self._config_dict.get("digits", 4) val = self._config_dict.get("value", 0) color = self._config_dict.get("color", "green") label = self._config_dict.get("label", "") height = self._config_dict.get("height", 32) style = self._render_style() disp = str(val).rjust(digits, "0")[:digits] font_size = max(12, height - 10) padt = (height - font_size) // 2 lbl_html = f'<div class="iskg-led-label">{label}</div>' if label else "" return f'''<div id="{self._id}" class="iskg-led-wrap" style="{style}"> {lbl_html} <div class="iskg-led-digits iskg-led-{color}" style="font-size:{font_size}px;height:{height}px;line-height:{height - padt * 2}px;padding:{padt}px 8px;"> {disp} </div> </div>''' def _render_update_js(self) -> str: digits = self._config_dict.get("digits", 4) val = self._config_dict.get("value", 0) disp = str(val).rjust(digits, "0")[:digits] return f'''var el=document.getElementById("{self._id}"); if(el)el.querySelector(".iskg-led-digits").innerText="{disp}";'''
[docs] class IndicatorLED(Widget): """A small colored indicator light.""" def __init__( self, parent: Widget | None = None, color: str = "green", size: int = 8, active: bool = True, label: str = "", **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["color"] = color self._config_dict["size"] = size self._config_dict["active"] = active self._config_dict["label"] = label @property def active(self) -> bool: return self._config_dict.get("active", True) @active.setter def active(self, v: bool) -> None: self._config_dict["active"] = bool(v) self._sync() def _render(self) -> str: size = self._config_dict.get("size", 8) color = self._config_dict.get("color", "green") active = self._config_dict.get("active", True) label = self._config_dict.get("label", "") style = self._render_style() col_map = { "green": "#4ade80", "red": "#ef4444", "amber": "#f59e0b", "cyan": "#22d3ee", "blue": "#60a5fa", } col = col_map.get(color, "#4ade80") on_cls = "iskg-indicator-on" if active else "iskg-indicator-off" shadow = f"0 0 {size}px {col}" if active else "none" lbl_html = f'<span class="iskg-indicator-label">{label}</span>' if label else "" return f'''<div id="{self._id}" class="iskg-indicator" style="{style}"> <span class="iskg-indicator-dot {on_cls}" style="width:{size}px;height:{size}px;background:{col};box-shadow:{shadow};"></span> {lbl_html} </div>''' def _render_update_js(self) -> str: active = self._config_dict.get("active", True) on_cls = "iskg-indicator-on" if active else "iskg-indicator-off" return f'''var el=document.getElementById("{self._id}"); if(el){{var d=el.querySelector(".iskg-indicator-dot"); d.className="iskg-indicator-dot {on_cls}";}}'''
[docs] class RadialGauge(Widget): """A radial gauge dial for displaying values.""" def __init__( self, parent: Widget | None = None, from_: int = 0, to: int = 100, value: float = 50, size: int = 100, label: str = "", units: str = "", **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["from"] = from_ self._config_dict["to"] = to self._config_dict["value"] = value self._config_dict["size"] = size self._config_dict["label"] = label self._config_dict["units"] = units @property def value(self) -> float: return self._config_dict.get("value", 0) @value.setter def value(self, v: float) -> None: self._config_dict["value"] = float(v) self._sync() def _render(self) -> str: size = self._config_dict.get("size", 100) label = self._config_dict.get("label", "") units = self._config_dict.get("units", "") style = self._render_style() lbl_html = ( f'<div class="iskg-gauge-label">{label}{" " + units if units else ""}</div>' if label or units else "" ) return f'''<div id="{self._id}" class="iskg-gauge-wrap" style="{style}width:{size}px;"> <canvas id="{self._id}-cv" class="iskg-gauge-canvas" width="{size}" height="{size + 20}"></canvas> {lbl_html} </div>''' def _render_js(self) -> str: size = self._config_dict.get("size", 100) lo = self._config_dict.get("from", 0) hi = self._config_dict.get("to", 100) val = self._config_dict.get("value", 50) cx, cy = size // 2, size // 2 + 5 r = cx - 10 return f'''(function(){{ var cv=document.getElementById("{self._id}-cv"); var ctx=cv.getContext("2d"); var cx={cx},cy={cy},r={r},lo={lo},hi={hi},val={val}; var arcStart=135,arcSweep=270; function draw(v){{ ctx.clearRect(0,0,cv.width,cv.height); var p=(v-lo)/(hi-lo); var endA=arcStart+p*arcSweep; ctx.strokeStyle="#1a2636"; ctx.lineWidth=12; ctx.beginPath(); ctx.arc(cx,cy,r,(-arcStart-arcSweep)*Math.PI/180,-arcStart*Math.PI/180); ctx.stroke(); var grad=ctx.createLinearGradient(0,0,cv.width,0); grad.addColorStop(0,"#4ade80"); grad.addColorStop(0.5,"#f59e0b"); grad.addColorStop(1,"#ef4444"); ctx.strokeStyle=grad; ctx.lineWidth=10; ctx.shadowColor="#22d3ee"; ctx.shadowBlur=8; ctx.beginPath(); ctx.arc(cx,cy,r,-endA*Math.PI/180,-arcStart*Math.PI/180); ctx.stroke(); ctx.shadowBlur=0; for(var i=0;i<=10;i++){{ var a=(arcStart+i/10*arcSweep)*Math.PI/180; var len=i%5==0?8:4; ctx.strokeStyle=i%5==0?"#c8d6e5":"#4a5a6a"; ctx.lineWidth=i%5==0?1.5:1; ctx.beginPath(); ctx.moveTo(cx+(r-2)*Math.cos(-a),cy+(r-2)*Math.sin(-a)); ctx.lineTo(cx+(r-len)*Math.cos(-a),cy+(r-len)*Math.sin(-a)); ctx.stroke(); }} ctx.fillStyle="#c8d6e5"; ctx.font="bold "+Math.round({size}/8)+"px 'Share Tech Mono',monospace"; ctx.textAlign="center"; ctx.textBaseline="middle"; ctx.fillText(Math.round(v),cx,cy+r*0.6); }} draw(val); window.iskg_gauge_{self._id}=draw; }})();''' def _render_update_js(self) -> str: val = self._config_dict.get("value", 0) return f"var fn=window.iskg_gauge_{self._id};if(fn)fn({val});"
[docs] class StatusBar(Widget): """A status bar with multiple sections.""" def __init__( self, parent: Widget | None = None, sections: list | None = None, **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["sections"] = sections or [] def _render(self) -> str: sections = self._config_dict.get("sections", []) style = self._render_style() parts = [] for sec in sections: if isinstance(sec, dict): text = sec.get("text", "") color = sec.get("color", "") style_attr = f"color:var(--{color})" if color else "" parts.append( f'<span class="iskg-statusbar-section" style="{style_attr}">{text}</span>' ) else: parts.append(f'<span class="iskg-statusbar-section">{sec}</span>') inner = "".join(parts) return f'''<div id="{self._id}" class="iskg-statusbar" style="{style}"> {inner} </div>''' def _render_update_js(self) -> str: return ""
[docs] class IconLabel(Widget): """A label with an inline icon.""" def __init__( self, parent: Widget | None = None, text: str = "", icon: str = "", icon_size: int = 14, **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["text"] = text self._config_dict["icon"] = icon self._config_dict["icon_size"] = icon_size def _render(self) -> str: text = self._config_dict.get("text", "") icon = self._config_dict.get("icon", "") icon_size = self._config_dict.get("icon_size", 14) style = self._render_style() icon_html = "" if icon: if icon.startswith("<"): icon_html = f'<span class="iskg-icon" style="width:{icon_size}px;height:{icon_size}px;">{icon}</span>' else: icon_html = f'<span class="iskg-icon" style="font-size:{icon_size}px;width:{icon_size}px;height:{icon_size}px;">{icon}</span>' return f'''<span id="{self._id}" class="iskg-iconlabel" style="{style}"> {icon_html} <span>{text}</span> </span>'''
[docs] class ImageBox(Widget): """A widget that displays an image.""" def __init__( self, parent: Widget | None = None, src: str = "", width: int = 100, height: int = 100, fit: str = "contain", **kwargs: Any, ) -> None: super().__init__(parent, **kwargs) self._config_dict["src"] = src self._config_dict["width"] = width self._config_dict["height"] = height self._config_dict["fit"] = fit def _render(self) -> str: src = self._config_dict.get("src", "") width = self._config_dict.get("width", 100) height = self._config_dict.get("height", 100) fit = self._config_dict.get("fit", "contain") style = self._render_style() return f'''<div id="{self._id}" class="iskg-imagebox" style="{style}width:{width}px;height:{height}px;"> <img src="{src}" style="object-fit:{fit};"/> </div>'''