mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
32
examples/calculator.css
Normal file
32
examples/calculator.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
Screen {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#calculator {
|
||||||
|
layout: table;
|
||||||
|
table-size: 4;
|
||||||
|
table-gutter: 1 2;
|
||||||
|
table-columns: 1fr;
|
||||||
|
table-rows: 2fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
margin: 1 2;
|
||||||
|
min-height:25;
|
||||||
|
min-width: 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#numbers {
|
||||||
|
column-span: 4;
|
||||||
|
content-align: right middle;
|
||||||
|
padding: 0 1;
|
||||||
|
height: 100%;
|
||||||
|
background: $primary-lighten-2;
|
||||||
|
color: $text-primary-lighten-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#number-0 {
|
||||||
|
column-span: 2;
|
||||||
|
}
|
||||||
142
examples/calculator.py
Normal file
142
examples/calculator.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual import events
|
||||||
|
from textual.layout import Container
|
||||||
|
from textual.reactive import Reactive
|
||||||
|
from textual.widgets import Button, Static
|
||||||
|
|
||||||
|
|
||||||
|
class CalculatorApp(App):
|
||||||
|
"""A working 'desktop' calculator."""
|
||||||
|
|
||||||
|
numbers = Reactive.var("0")
|
||||||
|
show_ac = Reactive.var(True)
|
||||||
|
left = Reactive.var(Decimal("0"))
|
||||||
|
right = Reactive.var(Decimal("0"))
|
||||||
|
value = Reactive.var("")
|
||||||
|
operator = Reactive.var("plus")
|
||||||
|
|
||||||
|
KEY_MAP = {
|
||||||
|
"+": "plus",
|
||||||
|
"-": "minus",
|
||||||
|
".": "point",
|
||||||
|
"*": "multiply",
|
||||||
|
"/": "divide",
|
||||||
|
"_": "plus-minus",
|
||||||
|
"%": "percent",
|
||||||
|
"=": "equals",
|
||||||
|
}
|
||||||
|
|
||||||
|
def watch_numbers(self, value: str) -> None:
|
||||||
|
"""Called when numbers is updated."""
|
||||||
|
# Update the Numbers widget
|
||||||
|
self.query_one("#numbers", Static).update(value)
|
||||||
|
|
||||||
|
def compute_show_ac(self) -> bool:
|
||||||
|
"""Compute switch to show AC or C button"""
|
||||||
|
return self.value in ("", "0") and self.numbers == "0"
|
||||||
|
|
||||||
|
def watch_show_ac(self, show_ac: bool) -> None:
|
||||||
|
"""Called when show_ac changes."""
|
||||||
|
self.query_one("#c").display = not show_ac
|
||||||
|
self.query_one("#ac").display = show_ac
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Add our buttons."""
|
||||||
|
yield Container(
|
||||||
|
Static(id="numbers"),
|
||||||
|
Button("AC", id="ac", variant="primary"),
|
||||||
|
Button("C", id="c", variant="primary"),
|
||||||
|
Button("+/-", id="plus-minus", variant="primary"),
|
||||||
|
Button("%", id="percent", variant="primary"),
|
||||||
|
Button("÷", id="divide", variant="warning"),
|
||||||
|
Button("7", id="number-7"),
|
||||||
|
Button("8", id="number-8"),
|
||||||
|
Button("9", id="number-9"),
|
||||||
|
Button("×", id="multiply", variant="warning"),
|
||||||
|
Button("4", id="number-4"),
|
||||||
|
Button("5", id="number-5"),
|
||||||
|
Button("6", id="number-6"),
|
||||||
|
Button("-", id="minus", variant="warning"),
|
||||||
|
Button("1", id="number-1"),
|
||||||
|
Button("2", id="number-2"),
|
||||||
|
Button("3", id="number-3"),
|
||||||
|
Button("+", id="plus", variant="warning"),
|
||||||
|
Button("0", id="number-0"),
|
||||||
|
Button(".", id="point"),
|
||||||
|
Button("=", id="equals", variant="warning"),
|
||||||
|
id="calculator",
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_key(self, event: events.Key) -> None:
|
||||||
|
"""Called when the user presses a key."""
|
||||||
|
|
||||||
|
def press(button_id: str) -> None:
|
||||||
|
self.query_one(f"#{button_id}", Button).press()
|
||||||
|
self.set_focus(None)
|
||||||
|
|
||||||
|
key = event.key
|
||||||
|
if key.isdecimal():
|
||||||
|
press(f"number-{key}")
|
||||||
|
elif key == "c":
|
||||||
|
press("c")
|
||||||
|
press("ac")
|
||||||
|
elif key in self.KEY_MAP:
|
||||||
|
press(self.KEY_MAP[key])
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Called when a button is pressed."""
|
||||||
|
|
||||||
|
button_id = event.button.id
|
||||||
|
assert button_id is not None
|
||||||
|
|
||||||
|
self.bell() # Terminal bell
|
||||||
|
|
||||||
|
def do_math() -> None:
|
||||||
|
"""Does the math: LEFT OPERATOR RIGHT"""
|
||||||
|
try:
|
||||||
|
if self.operator == "plus":
|
||||||
|
self.left += self.right
|
||||||
|
elif self.operator == "minus":
|
||||||
|
self.left -= self.right
|
||||||
|
elif self.operator == "divide":
|
||||||
|
self.left /= self.right
|
||||||
|
elif self.operator == "multiply":
|
||||||
|
self.left *= self.right
|
||||||
|
self.numbers = str(self.left)
|
||||||
|
self.value = ""
|
||||||
|
except Exception:
|
||||||
|
self.numbers = "Error"
|
||||||
|
|
||||||
|
if button_id.startswith("number-"):
|
||||||
|
number = button_id.partition("-")[-1]
|
||||||
|
self.numbers = self.value = self.value.lstrip("0") + number
|
||||||
|
elif button_id == "plus-minus":
|
||||||
|
self.numbers = self.value = str(Decimal(self.value or "0") * -1)
|
||||||
|
elif button_id == "percent":
|
||||||
|
self.numbers = self.value = str(Decimal(self.value or "0") / Decimal(100))
|
||||||
|
elif button_id == "point":
|
||||||
|
if "." not in self.value:
|
||||||
|
self.numbers = self.value = (self.value or "0") + "."
|
||||||
|
elif button_id == "ac":
|
||||||
|
self.value = ""
|
||||||
|
self.left = self.right = Decimal(0)
|
||||||
|
self.operator = "plus"
|
||||||
|
self.numbers = "0"
|
||||||
|
elif button_id == "c":
|
||||||
|
self.value = ""
|
||||||
|
self.numbers = "0"
|
||||||
|
elif button_id in ("plus", "minus", "divide", "multiply"):
|
||||||
|
self.right = Decimal(self.value or "0")
|
||||||
|
do_math()
|
||||||
|
self.operator = button_id
|
||||||
|
elif button_id == "equals":
|
||||||
|
if self.value:
|
||||||
|
self.right = Decimal(self.value)
|
||||||
|
do_math()
|
||||||
|
|
||||||
|
|
||||||
|
app = CalculatorApp(css_path="calculator.css")
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -251,6 +251,19 @@ class DOMQuery:
|
|||||||
if isinstance(node, filter_type):
|
if isinstance(node, filter_type):
|
||||||
yield node
|
yield node
|
||||||
|
|
||||||
|
def set_class(self, add: bool, *class_names: str) -> DOMQuery:
|
||||||
|
"""Set the given class name(s) according to a condition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
add (bool): Add the classes if True, otherwise remove them.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DOMQuery: Self.
|
||||||
|
"""
|
||||||
|
for node in self:
|
||||||
|
node.set_class(add, *class_names)
|
||||||
|
return self
|
||||||
|
|
||||||
def add_class(self, *class_names: str) -> DOMQuery:
|
def add_class(self, *class_names: str) -> DOMQuery:
|
||||||
"""Add the given class name(s) to nodes."""
|
"""Add the given class name(s) to nodes."""
|
||||||
for node in self:
|
for node in self:
|
||||||
|
|||||||
@@ -705,6 +705,17 @@ class DOMNode(MessagePump):
|
|||||||
"""
|
"""
|
||||||
return self._classes.issuperset(class_names)
|
return self._classes.issuperset(class_names)
|
||||||
|
|
||||||
|
def set_class(self, add: bool, *class_names: str) -> None:
|
||||||
|
"""Add or remove class(es) based on a condition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
add (bool): Add the classes if True, otherwise remove them.
|
||||||
|
"""
|
||||||
|
if add:
|
||||||
|
self.add_class(*class_names)
|
||||||
|
else:
|
||||||
|
self.remove_class(*class_names)
|
||||||
|
|
||||||
def add_class(self, *class_names: str) -> None:
|
def add_class(self, *class_names: str) -> None:
|
||||||
"""Add class names to this Node.
|
"""Add class names to this Node.
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
"""
|
"""
|
||||||
return cls(default, layout=layout, repaint=repaint, init=True)
|
return cls(default, layout=layout, repaint=repaint, init=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def var(
|
||||||
|
cls,
|
||||||
|
default: ReactiveType | Callable[[], ReactiveType],
|
||||||
|
) -> Reactive:
|
||||||
|
"""A reactive variable that doesn't update or layout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Reactive: A Reactive descriptor.
|
||||||
|
"""
|
||||||
|
return cls(default, layout=False, repaint=False, init=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def initialize_object(cls, obj: object) -> None:
|
async def initialize_object(cls, obj: object) -> None:
|
||||||
"""Call any watchers / computes for the first time.
|
"""Call any watchers / computes for the first time.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from ..message import Message
|
|||||||
from ..reactive import Reactive
|
from ..reactive import Reactive
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
ButtonVariant = Literal["default", "success", "warning", "error"]
|
ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
|
||||||
_VALID_BUTTON_VARIANTS = {"default", "success", "warning", "error"}
|
_VALID_BUTTON_VARIANTS = {"default", "primary", "success", "warning", "error"}
|
||||||
|
|
||||||
|
|
||||||
class InvalidButtonVariant(Exception):
|
class InvalidButtonVariant(Exception):
|
||||||
@@ -57,6 +57,29 @@ class Button(Widget, can_focus=True):
|
|||||||
background: $panel;
|
background: $panel;
|
||||||
border-bottom: tall $panel-lighten-2;
|
border-bottom: tall $panel-lighten-2;
|
||||||
border-top: tall $panel-darken-2;
|
border-top: tall $panel-darken-2;
|
||||||
|
tint: $background 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary variant */
|
||||||
|
Button.-primary {
|
||||||
|
background: $primary;
|
||||||
|
color: $text-primary;
|
||||||
|
border-top: tall $primary-lighten-3;
|
||||||
|
border-bottom: tall $primary-darken-3;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.-primary:hover {
|
||||||
|
background: $primary-darken-2;
|
||||||
|
color: $text-primary-darken-2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Button.-primary.-active {
|
||||||
|
background: $primary;
|
||||||
|
border-bottom: tall $primary-lighten-3;
|
||||||
|
border-top: tall $primary-darken-3;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -188,14 +211,18 @@ class Button(Widget, can_focus=True):
|
|||||||
label.stylize(self.text_style)
|
label.stylize(self.text_style)
|
||||||
return label
|
return label
|
||||||
|
|
||||||
async def on_click(self, event: events.Click) -> None:
|
async def _on_click(self, event: events.Click) -> None:
|
||||||
event.stop()
|
event.stop()
|
||||||
if self.disabled:
|
self.press()
|
||||||
|
|
||||||
|
def press(self) -> None:
|
||||||
|
"""Respond to a button press."""
|
||||||
|
if self.disabled or not self.display:
|
||||||
return
|
return
|
||||||
# Manage the "active" effect:
|
# Manage the "active" effect:
|
||||||
self._start_active_affect()
|
self._start_active_affect()
|
||||||
# ...and let other components know that we've just been clicked:
|
# ...and let other components know that we've just been clicked:
|
||||||
await self.emit(Button.Pressed(self))
|
self.emit_no_wait(Button.Pressed(self))
|
||||||
|
|
||||||
def _start_active_affect(self) -> None:
|
def _start_active_affect(self) -> None:
|
||||||
"""Start a small animation to show the button was clicked."""
|
"""Start a small animation to show the button was clicked."""
|
||||||
@@ -204,7 +231,7 @@ class Button(Widget, can_focus=True):
|
|||||||
self.ACTIVE_EFFECT_DURATION, partial(self.remove_class, "-active")
|
self.ACTIVE_EFFECT_DURATION, partial(self.remove_class, "-active")
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_key(self, event: events.Key) -> None:
|
async def _on_key(self, event: events.Key) -> None:
|
||||||
if event.key == "enter" and not self.disabled:
|
if event.key == "enter" and not self.disabled:
|
||||||
self._start_active_affect()
|
self._start_active_affect()
|
||||||
await self.emit(Button.Pressed(self))
|
await self.emit(Button.Pressed(self))
|
||||||
|
|||||||
Reference in New Issue
Block a user