creates new project

This commit is contained in:
falsycat 2025-01-26 23:42:21 +09:00
parent 184a31ca3e
commit 516920667a
7 changed files with 914 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/dev/
__pycache__/
/hourtrack.ini

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2025 falsycat <me@falsy.cat>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

19
hourtrack/__main__.py Normal file
View File

@ -0,0 +1,19 @@
from imgui_bundle import imgui, imgui_ctx, immapp
from hourtrack.compo import GanttView, TriageView
gctx = GanttView.Context(GanttView.SimpleIO())
tctx = TriageView.Context(TriageView.SimpleIO())
def main():
with imgui_ctx.begin("TicketView"):
pass
with imgui_ctx.begin("GanttView"):
GanttView.compo(gctx)
with imgui_ctx.begin("TriageView"):
TriageView.compo(tctx)
immapp.run(
window_title="hourtrack",
gui_function=main
)

View File

@ -0,0 +1,505 @@
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from imgui_bundle import imgui, imgui_ctx
import random
from typing import Callable
DATE_FMT = "%Y-%m-%d"
WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"]
# ---- data types ----
TicketId = int
Point = float
@dataclass
class Period:
begin: date
end : date
@dataclass
class Hours:
estimation: Point = 1
actual : Point = .5
@dataclass
class Ticket:
id : TicketId = 0
summary: str = "this is summary"
hours : list[Hours] = field(default_factory=list)
calculated: bool = False
selected : bool = False
# ---- GUI volatile data (selections, text user modifying, or etc...) ----
@dataclass
class VolatileData:
editing_period_begin: str = ""
editing_period_end : str = ""
editing_cell : None|tuple[TicketId, date] = None
editing_cell_text: str = ""
revision: None|str = None
# ---- this I/F allows widget to store or load permanent data
class IO(ABC):
@abstractmethod
def get_period(self) -> Period:
pass
@abstractmethod
def set_period(self, p: Period) -> None:
pass
@abstractmethod
def get_tickets(self) -> list[Ticket]:
pass
@abstractmethod
def set_hours(self, t: list[TicketId], p: Period, act: None|Point, est: None|Point) -> None:
pass
@abstractmethod
def get_today(self) -> date:
pass
@abstractmethod
def sort(self) -> None:
pass
@abstractmethod
def select_ticket(self, t: TicketId) -> None:
pass
@abstractmethod
def get_revisions(self) -> list[str]:
pass
@abstractmethod
def load_revision(self, name: None|str) -> None:
pass
@abstractmethod
def delete_revision(self, name: str) -> None:
pass
@abstractmethod
def save_as_new_revision(self, name: str) -> bool:
pass
class SimpleIO(IO):
_period : Period
_tickets: list[Ticket]
def __init__(self):
self._period = Period(date(2025, 1, 1), date(2025, 2, 1))
self._tickets = [
Ticket(i, "hello", [
*([Hours(0, 0)]*random.randint(1,30)),
*([Hours(1, 2)]*random.randint(2,8)),
*([Hours(0, 0)]*100),
]) for i in range(70)
]
def get_period(self) -> Period:
return self._period
def set_period(self, p: Period) -> None:
self._period = p
def get_tickets(self) -> list[Ticket]:
return self._tickets
def get_today(self) -> date:
return datetime.now().date()
def set_hours(self, t: list[TicketId], p: Period, act: None|float, est: None|float) -> None:
pass
def sort(self) -> None:
pass
def select_ticket(self, t: TicketId) -> None:
pass
def get_revisions(self) -> list[str]:
return ["first rev", "second rev"]
def load_revision(self, name: None|str) -> None:
pass
def delete_revision(self, name: str) -> None:
pass
def save_as_new_revision(self, name: str) -> bool:
return False
# ---- component definition
@dataclass
class Context:
io : IO
vola: VolatileData = field(default_factory=lambda: VolatileData())
def compo(ctx: Context) -> None:
io = ctx.io
vola = ctx.vola
em = imgui.get_font_size()
# view config / period specs
imgui.align_text_to_frame_padding()
with imgui_ctx.push_item_width(6*em):
period = io.get_period()
changed_begin, vola.editing_period_begin = _compo_editable_text(
"begin date",
period.begin.strftime(DATE_FMT),
vola.editing_period_begin,
_check_if_str_is_date,
)
imgui.same_line()
imgui.text("~")
imgui.same_line()
period = io.get_period()
changed_end, vola.editing_period_end = _compo_editable_text(
"end date",
period.end.strftime(DATE_FMT),
vola.editing_period_end,
_check_if_str_is_date,
)
if changed_begin or changed_end:
io.set_period(Period(
_str2date(vola.editing_period_begin) if vola.editing_period_begin else period.begin,
_str2date(vola.editing_period_end) if vola.editing_period_end else period.end,
))
imgui.same_line()
imgui.text("/")
imgui.same_line()
# revisions
with imgui_ctx.push_item_width(6*em):
revs = io.get_revisions()
imgui.text("revision:")
imgui.same_line()
if imgui.begin_combo("##rev", vola.revision or "(latest)"):
clicked, _ = imgui.selectable("(latest)", False)
if clicked:
vola.revision = None
for name in revs:
clicked, _ = imgui.selectable(name, False)
if clicked:
io.load_revision(name)
vola.revision = name
imgui.end_combo()
imgui.same_line()
if vola.revision is not None:
if imgui.button("del"):
io.delete_revision(vola.revision)
else:
if imgui.button("new"):
vola.editing_revision_name = ""
imgui.open_popup("NEW_REVISION")
if imgui.begin_popup("NEW_REVISION"):
imgui.set_keyboard_focus_here()
entered, vola.editing_revision_name = imgui.input_text(
"revision name",
vola.editing_revision_name,
imgui.InputTextFlags_.enter_returns_true,
)
if imgui.is_item_deactivated():
imgui.close_current_popup()
if entered:
if io.save_as_new_revision(vola.editing_revision_name):
imgui.close_current_popup()
imgui.end_popup()
imgui.same_line()
imgui.text("/")
imgui.same_line()
# tool buttons
if imgui.button("sort"):
io.sort()
# gantt
today = io.get_today()
days = (period.end - period.begin).days + 1
past_days = min(days, (period.end - today).days)
if days <= 0:
imgui.text("invalid period")
else:
tickets = io.get_tickets()
days = [period.begin + timedelta(days=i) for i in range(days)]
daily_estimations = [sum(t.hours[i].estimation for t in tickets) for i in range(len(days))]
daily_actuals = [sum(t.hours[i].actual for t in tickets) for i in range(len(days))]
daily_estimations_quantiles = _calc_quantiles(daily_estimations)
table_flags = (
imgui.TableFlags_.scroll_x |
imgui.TableFlags_.scroll_y |
imgui.TableFlags_.borders |
imgui.TableFlags_.resizable
)
with imgui_ctx.begin_table("Table", 2+len(days), table_flags):
# column setup
imgui.table_setup_scroll_freeze(2, 6)
imgui.table_setup_column("#")
imgui.table_setup_column(
"summary", imgui.TableColumnFlags_.angled_header)
for d in days:
flags = (
imgui.TableColumnFlags_.width_fixed |
imgui.TableColumnFlags_.no_resize
)
if d == today:
imgui.table_setup_column(f"{d.day}", flags, 3*1.2*em)
else:
imgui.table_setup_column(f"{d.day}", flags, 1.2*em)
# month
imgui.table_next_row(imgui.TableRowFlags_.headers)
imgui.table_next_column()
imgui.table_next_column()
_aligned_text("month ->", 1)
for d in days:
imgui.table_next_column()
if d.day == 1:
_aligned_text(f"{d.month}", .5)
# days
imgui.table_next_row(imgui.TableRowFlags_.headers)
imgui.table_next_column()
imgui.table_next_column()
_aligned_text("day ->", 1)
for d in days:
imgui.table_next_column()
if d == today:
with imgui_ctx.push_style_color(imgui.Col_.text, imgui.get_color_u32((.5,1,.5,1))):
_aligned_text(f"{d.day}", .5)
else:
_aligned_text(f"{d.day}", .5)
# weekday
imgui.table_next_row(imgui.TableRowFlags_.headers)
imgui.table_next_column()
imgui.table_next_column()
_aligned_text("weekday ->", 1)
for d in days:
imgui.table_next_column()
w = d.weekday()
if w == 0:
color = imgui.get_color_u32((.8, .6, .6, 1))
elif w == 6:
color = imgui.get_color_u32((.6, .6, .8, 1))
else:
color = imgui.get_color_u32((.8, .8, .8, 1))
with imgui_ctx.push_style_color(imgui.Col_.text, color):
_aligned_text(f"{WEEKDAYS[w]}", .5)
# estimation
imgui.table_next_row()
imgui.table_set_column_index(1)
_aligned_text("estimation ->", 1)
for i,d in enumerate(days):
imgui.table_next_column()
col = _get_deviation_color(
_calc_deviation_coe4(daily_estimations[i], *daily_estimations_quantiles))
with imgui_ctx.push_style_color(imgui.Col_.text, col):
_aligned_text(f"{daily_estimations[i]}", 1)
# actual
imgui.table_next_row()
imgui.table_set_column_index(1)
_aligned_text("actual ->", 1)
for i,d in enumerate(days):
imgui.table_next_column()
a = daily_actuals[i]
e = daily_estimations[i]
col = _get_deviation_color(_calc_deviation_coe1(a, e))
with imgui_ctx.push_style_color(imgui.Col_.text, col):
if d <= today:
_aligned_text(f"{a}", 1)
# header
imgui.table_next_row(imgui.TableRowFlags_.headers)
imgui.table_next_column()
_aligned_text("#", .5)
imgui.table_next_column()
imgui.text("summary")
# tickets
for t in tickets:
imgui.table_next_row()
with imgui_ctx.push_id(t.id):
imgui.table_next_column()
_aligned_text(f"#{t.id}", 1)
imgui.table_next_column()
if imgui.text_link(f"{t.summary}##summary"):
io.select_ticket(t.id)
with imgui_ctx.push_style_var(imgui.StyleVar_.cell_padding, (0, 0)):
for i,d in enumerate(days):
with imgui_ctx.push_id(f"@{_date2str(d)}"):
hours = t.hours[i]
imgui.table_next_column()
imgui.table_set_bg_color(
imgui.TableBgTarget_.cell_bg,
imgui.get_color_u32(_get_cell_color(d, today))
)
if vola.editing_cell == (t.id, d):
imgui.set_keyboard_focus_here()
imgui.set_next_item_width(-1)
with imgui_ctx.push_style_var(imgui.StyleVar_.frame_rounding, 0):
with imgui_ctx.push_style_var(imgui.StyleVar_.frame_padding, (0, 0)):
entered, vola.editing_cell_text = imgui.input_text(
"##input", vola.editing_cell_text,
imgui.InputTextFlags_.enter_returns_true
)
imgui.set_item_default_focus()
if entered or imgui.is_item_deactivated():
vola.editing_cell = None
error = False
try:
est, act = _parse_hours(vola.editing_cell_text)
except ValueError:
error = True
if not error:
io.set_hours([t.id], Period(d, d), est, act)
else:
edit = False
if d > today:
edit = _compo_cell_selectable("##cell", False)
if hours.estimation > 0:
_aligned_text(f"/{hours.estimation}", 1)
elif d < today:
edit = _compo_cell_selectable("##cell", False)
if (hours.estimation > 0 or hours.actual > 0):
_aligned_text(f"{hours.actual}/", 1)
elif d == today:
edit = _compo_cell_selectable("##cell", False)
_aligned_text(f"{hours.actual}", 1., w=1.2*em)
imgui.same_line(0, 0)
_aligned_text("/", .5, w=1.2*em)
imgui.same_line(0, 0)
_aligned_text(f"{hours.estimation}", 1., w=1.2*em)
if edit and not t.calculated:
vola.editing_cell = (t.id, d)
vola.editing_cell_text = f"{hours.actual}/{hours.estimation}"
io.select_ticket(t.id)
def _compo_editable_text(id: str, text: str, editing: str, checker: Callable[[str], None|str]) -> tuple[bool, str]:
with imgui_ctx.push_id(id):
submitted = False
if imgui.text_link(text):
imgui.open_popup("input")
editing = text
if imgui.begin_popup("input"):
imgui.set_keyboard_focus_here()
entered, editing = imgui.input_text(
id, editing, imgui.InputTextFlags_.enter_returns_true)
if err := checker(editing):
imgui.text(err)
elif entered:
submitted = True
imgui.close_current_popup()
imgui.end_popup()
return submitted, editing
def _aligned_text(text: str, pos: float, w: float = None, func = imgui.text):
w = w or imgui.get_content_region_avail().x
tw = imgui.calc_text_size(text).x
ox = imgui.get_cursor_pos_x()
imgui.set_cursor_pos_x(ox + (w-tw)*pos)
return func(text)
def _compo_cell_selectable(name: str, selected: bool, flags: int = 0) -> bool:
pos = imgui.get_cursor_pos()
imgui.selectable(name, selected, flags)
imgui.set_cursor_pos(pos)
return imgui.is_item_clicked()
def _str2date(v: str) -> date:
return datetime.strptime(v, DATE_FMT).date()
def _date2str(v: str) -> date:
return v.strftime(DATE_FMT)
def _check_if_str_is_date(v: str) -> str:
try:
_str2date(v)
return None
except ValueError as e:
return "invalid format (example: 2002-06-30)"
def _get_cell_color(d: date, today: date) -> tuple[float, float, float, float]:
if d > today:
return (0, 0, 0, 0)
elif d < today:
return (.6, .6, .6, .05)
else:
return (.3, .9, .3, .2)
def _get_deviation_color(coe: float) -> tuple[float, float, float, float]:
if coe > .8:
return (.8, .2, .2, 1)
elif coe < -.8:
return (.2, .8, .2, 1)
else:
return (.8, .8, .8, 1)
def _calc_deviation_coe1(v: float, avg: float) -> float:
return _calc_deviation_coe4(v, avg*.8, avg, avg*1.2)
def _calc_deviation_coe4(v: float, low: float, avg: float, high: float) -> float:
if v < avg:
return (v - avg) / (avg - low) if avg != low else 0
else:
return (v - avg) / (high - avg) if avg != high else 0
def _calc_quantiles(data: list[float]) -> tuple[float, float, float]:
return (
min(data), sum(data)/len(data), max(data),
)
def _parse_hours(v: str) -> tuple[float, float]:
if v == "":
return 0, 0
terms = v.split("/")
n = len(terms)
if n == 2:
return _parse_point(terms[0]), _parse_point(terms[1])
elif n == 1:
return _parse_point(terms[0]), None
else:
raise ValueError()
def _parse_point(v: str) -> None|Point:
if v == "":
return None
return Point(v)

View File

@ -0,0 +1,159 @@
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass, field
from imgui_bundle import imgui, imgui_ctx
from typing import Any, List, Set, Tuple
import webbrowser
Field = Tuple[str, str]
@dataclass
class Item:
id : int = 0
summary: str = "this is summary"
detail : str = "helloworld"
fields : List[Field] = field(default_factory=list)
feats : Set[str] = field(default_factory=set)
def __post_init__(self):
self.fields = [
("begin", "2024-01-24"),
("end", "2025-02-12"),
]
self.feats = {"subtickets", "gantt"}
class IO(ABC):
@abstractmethod
def get_item(self) -> Item:
pass
@abstractmethod
def set_summary(self, v: str) -> None:
pass
@abstractmethod
def set_detail(self, v: str) -> None:
pass
@abstractmethod
def set_field(self, idx: int, field: Field) -> None:
pass
@abstractmethod
def delete_field(self, idx: int) -> None:
pass
@abstractmethod
def reorder_field(self, before: int, after: int) -> None:
pass
def compo(io: IO, item: Item = None) -> None:
em = imgui.get_font_size()
if item is None:
item = io.get_item()
with imgui_ctx.push_id(f"TicketEdit#{item.id}"):
if imgui.collapsing_header("basic info", imgui.TreeNodeFlags_.default_open.value):
changed, new_summary = imgui.input_text("summary", item.summary)
if changed:
io.set_summary(new_summary)
changed, new_detail = imgui.input_text_multiline("detail", item.detail)
if changed:
io.set_detail(new_detail)
if imgui.collapsing_header("fields", imgui.TreeNodeFlags_.default_open.value):
count = len(item.fields)
drag_src = imgui.get_drag_drop_payload_py_id()
if drag_src is not None and drag_src.type == "TicketEdit_Fields":
drag_src = drag_src.data_id
for idx in range(0, count+1):
field = item.fields[idx] if idx < count else ("", "")
with imgui_ctx.push_id(idx):
with imgui_ctx.begin_group():
w = imgui.get_content_region_avail().x
value_changed = False
name_changed = False
imgui.begin_disabled(idx == count)
imgui.text_link(":::")
if imgui.begin_drag_drop_source():
imgui.set_drag_drop_payload_py_id("TicketEdit_Fields", idx, imgui.Cond_.once.value);
imgui.text(f"{field[0]}")
imgui.end_drag_drop_source()
if imgui.is_item_hovered():
imgui.set_tooltip("right click to delete")
if imgui.is_mouse_released(1):
io.delete_field(idx)
imgui.end_disabled()
imgui.same_line()
with imgui_ctx.push_item_width(6*em):
name_changed, new_name = imgui.input_text_with_hint("##name", "name", field[0])
imgui.same_line()
with imgui_ctx.push_item_width(w - 10*em):
value_changed, new_value = imgui.input_text_with_hint("##value", "value...", field[1])
if name_changed or value_changed:
io.set_field(idx, (new_name, new_value))
if idx < count and imgui.begin_drag_drop_target():
payload = imgui.accept_drag_drop_payload_py_id("TicketEdit_Fields")
if payload is not None:
io.reorder_field(payload.data_id, idx)
imgui.end_drag_drop_target()
if imgui.collapsing_header("features", imgui.TreeNodeFlags_.default_open.value):
pass
def win(io: IO) -> None:
item = io.get_item()
with imgui_ctx.begin(f"#{item.id}: {item.summary}###TicketEdit#{item.id}"):
compo(io, item)
class SimpleIO(IO):
_item: Item = Item()
def get_item(self) -> Item:
return deepcopy(self._item)
def set_summary(self, v: str) -> None:
self._item.summary = v
def set_detail(self, v: str) -> None:
self._item.detail = v
def set_field(self, idx: int, field: Field) -> None:
if 0 <= idx and idx < len(self._item.fields):
self._item.fields[idx] = field
else:
self._item.fields.append(field)
def delete_field(self, idx: int) -> None:
del self._item.fields[idx]
def reorder_field(self, before: int, after: int) -> None:
n = len(self._item.fields)
if n == 0:
return
before = min(max(0, before), n-1)
after = min(max(0, after ), n-1)
if before == after:
return
temp = self._item.fields[before]
del self._item.fields[before]
self._item.fields.insert(after, temp)

View File

@ -0,0 +1,214 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from imgui_bundle import imgui, imgui_ctx
PRIORITY_LABELS = ["A", "B", "C", "D"]
DND_PAYLOAD_TYPE = "TICKET"
# ---- data types ----
TicketId = int
@dataclass
class Ticket:
id : TicketId = 0
summary : str = "this is tooooo too long summary"
selected: bool = False
# ---- volatile ----
@dataclass
class VolatileData:
selected: TicketId = 0
editing_new_summaries: list[str] = field(default_factory=lambda: ["", "", "", ""])
# ---- I/O ----
class IO(ABC):
@abstractmethod
def get_tickets(self) -> list[list[Ticket]]:
pass
@abstractmethod
def move_ticket(self, t: TicketId, priority: int, index: int) -> bool:
pass
@abstractmethod
def select_ticket(self, t: TicketId) -> None:
pass
@abstractmethod
def add_ticket(self, priority: int, summary: str) -> TicketId:
pass
class SimpleIO(IO):
_items = [
[Ticket(0), Ticket(1), Ticket(2)],
[Ticket(3)],
[Ticket(4), Ticket(5)],
[],
]
_next_id = 100
def get_tickets(self) -> list[list[Ticket]]:
return self._items
def move_ticket(self, t: TicketId, priority: int, index: int) -> bool:
for pi,pl in enumerate(self._items):
i = next((i for i,v in enumerate(pl) if v.id == t), None)
if i is not None:
self._items[priority].insert(index, pl[i])
if priority == pi and i > index:
i += 1
del pl[i]
return True
return False
def select_ticket(self, tid: TicketId) -> None:
for pl in self._items:
for t in pl:
t.selected = (tid == t.id)
def add_ticket(self, priority: int, summary: str) -> TicketId:
id = self._next_id
self._next_id += 1
self._items[priority].append(Ticket(id, summary))
return id
# ---- component definition ----
@dataclass
class Context:
io : IO
vola: VolatileData = field(default_factory=lambda: VolatileData())
def compo(ctx: Context) -> None:
io = ctx.io
vola = ctx.vola
tickets = io.get_tickets()
style = imgui.get_style()
selected = None
dragging = imgui.get_drag_drop_payload_py_id()
if dragging is not None:
dragging = dragging.data_id if dragging.type == DND_PAYLOAD_TYPE else None
ln = len(PRIORITY_LABELS)
lw = (imgui.get_content_region_avail().x - style.frame_padding.x * (ln+1)) / ln
for pi,pl in enumerate(PRIORITY_LABELS):
with imgui_ctx.begin_child(pl, (lw, 0), imgui.ChildFlags_.borders):
imgui.text(pl)
basepos = imgui.get_cursor_screen_pos()
lh = imgui.get_content_region_avail().y - imgui.get_frame_height_with_spacing()
with imgui_ctx.begin_child("list", (0, lh), imgui.ChildFlags_.borders):
seps = []
for ticket in tickets[pi]:
seps.append(imgui.get_cursor_pos_y())
with imgui_ctx.push_id(ticket.id):
_compo_ticket(
ticket,
selected=ticket.selected,
gray=(dragging == ticket.id),
)
if ticket.selected:
selected = ticket.id
if vola.selected != ticket.id:
imgui.set_scroll_here_x()
imgui.set_scroll_here_y()
if imgui.is_item_clicked():
io.select_ticket(ticket.id)
selected = ticket.id
flags = imgui.DragDropFlags_.source_allow_null_id
if imgui.begin_drag_drop_source(flags):
imgui.set_drag_drop_payload_py_id(DND_PAYLOAD_TYPE, ticket.id)
_compo_ticket_detail(ticket)
imgui.end_drag_drop_source()
seps.append(imgui.get_cursor_pos_y())
if imgui.begin_drag_drop_target():
my = imgui.get_mouse_pos().y - basepos.y
if dragging is not None:
# find the nearest pos to insert
idx, pos = min(enumerate(seps), key=lambda d: abs(d[1]-my))
# draws line showing pos to insert
y = basepos.y + pos - style.item_spacing.y/2
imgui.get_window_draw_list().add_line(
(imgui.get_item_rect_min().x, y),
(imgui.get_item_rect_max().x, y),
imgui.get_color_u32((1,1,1,1)),
)
# completes the moving
if imgui.accept_drag_drop_payload_py_id(DND_PAYLOAD_TYPE) is not None:
io.move_ticket(dragging, pi, idx)
imgui.end_drag_drop_target()
# new ticket creation
imgui.set_next_item_width(-1)
entered, vola.editing_new_summaries[pi] = imgui.input_text_with_hint(
"##new_summary",
"new ticket summary...",
vola.editing_new_summaries[pi],
imgui.InputTextFlags_.enter_returns_true,
)
if entered:
io.add_ticket(pi, vola.editing_new_summaries[pi])
vola.editing_new_summaries[pi] = ""
imgui.same_line()
vola.selected = selected
def _compo_ticket(t: Ticket, selected: bool = False, gray: bool = False) -> None:
style = imgui.get_style()
ltop = imgui.get_cursor_screen_pos()
avail = imgui.get_content_region_avail()
dlist = imgui.get_window_draw_list()
with imgui_ctx.begin_group():
imgui.dummy((avail.x, 0))
imgui.dummy((0, 0))
imgui.same_line()
imgui.text_disabled(f"#{t.id}")
imgui.same_line()
imgui.text_wrapped(t.summary)
imgui.same_line()
imgui.dummy((0, 0))
imgui.dummy((avail.x, 0))
# tooltip
if imgui.is_item_hovered():
imgui.set_mouse_cursor(imgui.MouseCursor_.hand)
with imgui_ctx.begin_tooltip():
_compo_ticket_detail(t)
# border
dlist.add_rect(
imgui.get_item_rect_min(),
imgui.get_item_rect_max(),
imgui.get_color_u32((1,1,1,1)),
style.frame_rounding,
)
# background
bg = None
if gray : bg = (.0,.0,.0,.4)
if selected: bg = (.4,.4,.8,.4)
if bg is not None:
dlist.add_rect_filled(
imgui.get_item_rect_min(),
imgui.get_item_rect_max(),
imgui.get_color_u32(bg),
style.frame_rounding,
)
def _compo_ticket_detail(t: Ticket):
imgui.text_disabled(f"#{t.id}")
imgui.text(t.summary)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
imgui_bundle