Mobile

TabletSidebar

A sidebar that folds to icons, then to an overlay, by width class, with sections, badges and a pin kept per device.

#mobile#tablet#interactive#navigation#sidebar#layout

Demo

Preview
Field

jobs

Turn the tablet, or take it into Split View. The sidebar decides by the room it has, not the screen.

The pin: not changed

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/tablet_sidebar.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/tablet_sidebar.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/tablet_sidebar.qwe
let tablet_sidebar_demo_css = css`.demo-side-page { padding: 16px 20px; font: 15px/1.6 var(--ui-font); }.demo-side-page h4 { margin: 0 0 8px; font-size: 20px; }.demo-side-out { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }`let tablet_sidebar_demo_sections = [    { title: "Work", items: [        { id: "jobs", label: "Jobs", icon: "M4 8h16v11H4zM9 8V5h6v3", badge: 3 },        { id: "map", label: "Map", icon: "M12 21s7-6.5 7-12a7 7 0 1 0-14 0c0 5.5 7 12 7 12zM12 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4" },        { id: "chat", label: "Messages", icon: "M4 5h16v11H9l-5 4z", badge: 240 },        { id: "docs", label: "Documents", icon: "M6 3h9l4 4v14H6zM14 3v5h5", badge: 0 },    ] },    { title: "Crew", items: [        { id: "rota", label: "Rota", icon: "M4 6h16v14H4zM4 10h16M9 3v4M15 3v4" },        { id: "time", label: "Timesheets", icon: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2", badge: "soon" },    ] },    { items: [        { id: "help", label: "Help", icon: "?" },    ] },]component TabletSidebarDemoLive() {    style global tablet_sidebar_demo_css    state page = "jobs"    state pin = "not changed"    <div.demo-stack>        <div.demo-tablet>            <TabletSidebar id="demo-field" title="Field" current="jobs" sections={tablet_sidebar_demo_sections}                           on_select={(it) => { page = it.label }} on_pin={(p) => { pin = p ? "pinned" : "unpinned" }}>                <div.demo-side-page>                    <h4 id="side-demo-page">{page}</h4>                    <p>Turn the tablet, or take it into Split View. The sidebar decides by the room it has, not the screen.</p>                    <Button id="side-demo-behind" size="lg" variant="outline">Something on the page</Button>                </div>            </TabletSidebar>        </div>        <p.demo-note id="side-demo-pin">The pin: {pin}</p>    </div>}view TabletSidebarDemo() = <TabletSidebarDemoLive/>

Source

ui/tablet_sidebar.qwe
// TabletSidebar: the side of an app with sections of places in it. With room it stands// open; at medium widths it folds to a rail of icons, which opens over the page when// asked; on a narrow screen it is out of sight and slides in. The pin keeps it open at// medium widths, and is remembered on this device. Width means its own box, so a turn,// Split View and Slide Over are all the same to it.////   import ui/tablet_sidebar.qwe use TabletSidebar////   <TabletSidebar id="main" title="Field" current={page} sections={[//       { title: "Work", items: [//           { id: "jobs", label: "Jobs", icon: "M4 7h16v12H4zM9 7V4h6v3", badge: 3 },//           { id: "map", label: "Map", icon: "M12 21s7-6.5 7-12a7 7 0 1 0-14 0c0 5.5 7 12 7 12z" },//       ] },//       { title: "You", items: [{ id: "me", label: "Profile", icon: "P", href: "/me" }] },//   ]} on_select={(item) => { page = item.id }}>//       <Page/>//   </TabletSidebar>//// sections: { title?, items }. items: { id, label, icon, badge?, href? }. icon: a// character, or an SVG path when it starts with "M". badge: a count; 0 or anything that is// not a number shows nothing, and over 99 shows 99+. href makes it a link; without one// it is a button, and inside one of your components calls on_select(item). A press also// marks it as the current one and closes the sidebar if it lay over the page.// current: the id of the place showing. title: the heading at its top, also its name for// screen readers ("Menu").// id: names what is remembered: the pin is kept in this browser's localStorage under// `ui-tablet-side:<id>`. In a phone app that storage comes back empty at each launch: keep// the pin yourself by on_pin, through a server fn, and give it back as `saved`.// saved: true or false: the pin as you kept it, in place of what the browser remembers.// on_pin(pinned): inside one of your components, when the pin changes.// children: the page beside it.//// expanded, 840 px and up: open, and the pin waits for when it matters. medium, from// 600: a rail, unless pinned. compact: out of sight, even if pinned. A swipe from the// left edge, or right along the rail, brings it over the page, following the finger; a// swipe left, Close, a tap beside it or Escape put it away; the menu button does both. Up and// Down move along the places.import ui/theme.qwe use ui_clslet tablet_sidebar_css = css`.ui-tablet-side {  position: relative; box-sizing: border-box; min-width: 0; height: 100%; overflow: hidden; container: ui-tablet-side / inline-size;  background: var(--ui-bg); color: var(--ui-fg); font: 15px/1.4 var(--ui-font);}.ui-tablet-side-frame { position: relative; display: flex; height: 100%; min-height: 0; }.ui-tablet-side-slot { position: relative; flex: 0 0 260px; min-width: 0; }.ui-tablet-side-panel {  position: absolute; inset: 0 auto 0 0; z-index: 3; display: flex; flex-direction: column; box-sizing: border-box; width: 260px; min-height: 0; overflow: hidden;  padding-left: env(safe-area-inset-left); background: var(--ui-subtle); border-right: 1px solid var(--ui-border); touch-action: pan-y;}.ui-tablet-side-top { display: flex; align-items: center; gap: 2px; flex: none; min-height: 56px; box-sizing: border-box; padding: max(4px, env(safe-area-inset-top)) 6px 4px; }.ui-tablet-side-top h2 { flex: 1; min-width: 0; margin: 0; padding: 0 8px; overflow: hidden; font-size: 17px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }.ui-tablet-side-btn {  display: grid; place-items: center; flex: none; width: 44px; height: 44px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent;  color: var(--ui-muted); cursor: pointer; -webkit-tap-highlight-color: transparent;}.ui-tablet-side-btn[aria-pressed="true"] { color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-tablet-side-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-tablet-side-btn svg { width: 22px; height: 22px; }.ui-tablet-side-menu, .ui-tablet-side-close { display: none; }.ui-tablet-side-list { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; touch-action: pan-y; padding: 0 8px calc(8px + env(safe-area-inset-bottom)); }.ui-tablet-side-section { padding: 6px 0; }.ui-tablet-side-section + .ui-tablet-side-section { border-top: 1px solid var(--ui-border); }.ui-tablet-side-section h3 { margin: 6px 10px 4px; color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }.ui-tablet-side-item {  position: relative; display: flex; align-items: center; gap: 12px; box-sizing: border-box; width: 100%; min-height: 48px; padding: 6px 10px; border: 0; border-radius: var(--ui-radius);  background: transparent; color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; cursor: pointer; -webkit-tap-highlight-color: transparent;}.ui-tablet-side-item[aria-current] { background: var(--ui-accent-soft); color: var(--ui-accent); font-weight: 600; }.ui-tablet-side-item:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (hover: hover) { .ui-tablet-side-item:hover, .ui-tablet-side-btn:hover { background: var(--ui-border); } }.ui-tablet-side-icon { display: inline-grid; place-items: center; flex: none; width: 28px; height: 28px; font-size: 19px; line-height: 1; }.ui-tablet-side-icon svg { width: 24px; height: 24px; }.ui-tablet-side-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-tablet-side-badge { flex: none; min-width: 20px; padding: 0 6px; box-sizing: border-box; border-radius: 999px; background: var(--ui-accent); color: var(--ui-accent-fg); font-size: 12px; font-weight: 700; line-height: 20px; text-align: center; }.ui-tablet-side-main { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; }.ui-tablet-side-bar { display: none; align-items: center; gap: 4px; flex: none; min-height: 52px; box-sizing: border-box; padding: 3px 6px; border-bottom: 1px solid var(--ui-border); }.ui-tablet-side-bar b { font-weight: 600; }.ui-tablet-side-page { flex: 1; min-height: 0; overflow: auto; }.ui-tablet-side-scrim { display: none; position: absolute; inset: 0; z-index: 2; padding: 0; border: 0; background: var(--ui-overlay); }.ui-tablet-side-edge { display: none; position: absolute; inset: 0 auto 0 0; z-index: 1; width: 16px; touch-action: pan-y; }.ui-tablet-side-moving .ui-tablet-side-panel { transition: width .22s cubic-bezier(.2, .8, .2, 1), transform .24s cubic-bezier(.2, .8, .2, 1); }@container ui-tablet-side (min-width: 600px) and (max-width: 839.98px) {  .ui-tablet-side:not(.ui-tablet-side-pinned) .ui-tablet-side-slot { flex-basis: 76px; }  .ui-tablet-side:not(.ui-tablet-side-pinned) .ui-tablet-side-panel { width: 76px; }  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-top { justify-content: center; }  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-top h2,  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-pin,  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-section h3 { display: none; }  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-list { padding-inline: 6px; }  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-item { justify-content: center; padding-inline: 4px; }  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-name { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }  .ui-tablet-side:not(.ui-tablet-side-pinned):not(.ui-tablet-side-peek) .ui-tablet-side-badge { position: absolute; top: 2px; right: 6px; min-width: 18px; padding: 0 4px; font-size: 11px; line-height: 18px; }  .ui-tablet-side:not(.ui-tablet-side-pinned) .ui-tablet-side-menu { display: grid; }}@container ui-tablet-side (max-width: 839.98px) {  .ui-tablet-side-peek .ui-tablet-side-panel { width: 260px !important; box-shadow: var(--ui-shadow-lg); }  .ui-tablet-side-peek .ui-tablet-side-menu { display: none !important; }  .ui-tablet-side-peek .ui-tablet-side-close { display: grid; }  .ui-tablet-side-peek .ui-tablet-side-scrim { display: block; }}@container ui-tablet-side (max-width: 599.98px) {  .ui-tablet-side .ui-tablet-side-slot { flex-basis: 0; width: 0; }  .ui-tablet-side .ui-tablet-side-panel { width: min(280px, 85vw) !important; transform: translateX(-105%); visibility: hidden; box-shadow: var(--ui-shadow-lg); }  .ui-tablet-side.ui-tablet-side-peek .ui-tablet-side-panel, .ui-tablet-side-peeking .ui-tablet-side-panel { transform: none; visibility: visible; }  .ui-tablet-side-peeking .ui-tablet-side-panel { transform: translateX(-105%); }  .ui-tablet-side .ui-tablet-side-menu { display: none; }  .ui-tablet-side-bar { display: flex; }  .ui-tablet-side-bar .ui-tablet-side-menu { display: grid; }  .ui-tablet-side-edge { display: block; }  .ui-tablet-side .ui-tablet-side-pin { display: none; }}@media (prefers-reduced-motion: reduce) { .ui-tablet-side-panel { transition: none !important; } }`// Which way it stands. A width not measured yet is "": the CSS has already said.fn tablet_sidebar_mode(width: num, pinned: bool) -> str {    if width <= 0 { return "" }    if width < 600 { return "overlay" }    if width < 840 && !pinned { return "rail" }    "full"}// What a badge says: nothing for 0, a count, or 99+. Anything else says nothing.fn tablet_sidebar_badge(n) -> str? {    if type_of(n) != "int" && type_of(n) != "float" { return null }    if n <= 0 { return null }    n > 99 ? "99+" : str(round(n))}// Whether a pull is carried through: 40% of the way, or a flick.fn tablet_sidebar_goes(moved: num, speed: num, room: num) -> bool = moved > room * 0.4 || (speed > 0.5 && moved > 24)fn tablet_sidebar_step(key: str, at: int, count: int) -> int? {    match key {        "ArrowDown" => at + 1 < count ? at + 1 : at        "ArrowUp" => at > 0 ? at - 1 : 0        "Home" => 0        "End" => count > 0 ? count - 1 : 0        _ => null    }}// The pin as a browser keeps it: "1", "0", or anything else for never set.fn tablet_sidebar_read(text) -> bool? = text == "1" ? true : text == "0" ? false : nullview TabletSidebarIcon(icon) =    <span.ui-tablet-side-icon aria-hidden="true">        {type_of(icon) == "str" && starts_with(icon, "M") && len(icon) > 2            ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={icon}/></svg>            : str(icon ?? "")}    </span>view TabletSidebarInner(it) {    let badge = tablet_sidebar_badge(get(it, "badge"))    <span.ui-tablet-side-inner style="display: contents">        <TabletSidebarIcon icon={get(it, "icon")}/>        <span.ui-tablet-side-name>{str(get(it, "label", ""))}{badge != null && <span.ui-sr>, {badge} new</span>}</span>        {badge != null && <span.ui-tablet-side-badge aria-hidden="true">{badge}</span>}    </span>}component TabletSidebar(sections: list = [], current = null, title: str = "Menu", id: str = "ui", saved = null,                        on_select = null, on_pin = null, class: str? = null, children) {    style global tablet_sidebar_css    state here = current    state pinned = saved == true    state peek = false    state mode = ""    state root = null    state win = null    state opener = null        // what had the focus before it came over the page    state pull = null          // a swipe: { x, y, last_x, last_at, speed, on, off, opening }    let slot = `ui-tablet-side:${id}`    // Classes that come and go go on the frame: the root's class={...} is written whole when    // peek or pinned changes, and anything added by hand would go with it.    let frame = () => root.querySelector(".ui-tablet-side-frame")    let moving = () => {        if root != null {            let f = frame()            f.classList.add("ui-tablet-side-moving")            f.addEventListener("transitionend", (_e) => { f.classList.remove("ui-tablet-side-moving") }, { once: true })        }    }    let show = () => {        moving()        opener = win.document.activeElement        peek = true        root.classList.add("ui-tablet-side-peek")        let first = root.querySelector(".ui-tablet-side-item[aria-current]") ?? root.querySelector(".ui-tablet-side-item")        if first != null { first.focus({ preventScroll: true }) }    }    let hide = () => {        if peek {            moving()            peek = false            root.classList.remove("ui-tablet-side-peek")            // At once, not when the page next updates: an inert page cannot take the focus.            root.querySelector(".ui-tablet-side-main").inert = false            // Back where it came from; a phone's tap leaves no focus, so else to the menu button.            let panel = root.querySelector(".ui-tablet-side-panel")            let fine = opener != null && opener.tagName != "BODY" && !panel.contains(opener) && opener.getClientRects().length > 0            let menus = root.querySelectorAll(".ui-tablet-side-menu")            let shown = range(menus.length) | filter((n) => menus[n].getClientRects().length > 0)            if fine {                opener.focus({ preventScroll: true })            } else if len(shown) > 0 {                menus[shown[0]].focus({ preventScroll: true })            }        }    }    let pin = () => {        let next = !pinned        moving()        pinned = next        mode = tablet_sidebar_mode(root.clientWidth, next)        if next { hide() }        js("Promise.resolve", js_new("Promise", (done) => { done(js("localStorage.setItem", slot, next ? "1" : "0") ?? true) }))        on_pin && on_pin(next)    }    let look = (el) => {        mode = tablet_sidebar_mode(el.clientWidth, pinned)        if mode == "full" && peek { hide() }    }    let press = (it) => {        here = it.id        hide()        on_select && on_select(it)    }    let edge = (e) => {        if !peek && mode != "full" && mode != "" && e.button == 0 {            pull = { x: e.clientX, y: e.clientY, last_x: e.clientX, last_at: e.timeStamp, speed: 0, on: false, off: false, opening: true }        }    }    let track = (e) => {        if pull != null && !pull.off {            let dx = pull.opening ? e.clientX - pull.x : pull.x - e.clientX            let dy = e.clientY - pull.y            let early = !pull.on && abs(dx) < 8 && abs(dy) < 8            let off = !pull.on && !early && abs(dy) > abs(dx)            let on = pull.on || (!early && !off && dx > 0)            if on {                let panel = root.querySelector(".ui-tablet-side-panel")                if !pull.on {                    e.target.setPointerCapture(e.pointerId)                    frame().classList.remove("ui-tablet-side-moving")                    if pull.opening { frame().classList.add("ui-tablet-side-peeking") }                }                let d = max([0, dx])                if mode == "rail" {                    panel.style.width = pull.opening ? `${min([260, 76 + d])}px` : `${max([76, 260 - d])}px`                } else {                    panel.style.visibility = "visible"                    panel.style.transform = pull.opening ? `translateX(calc(-100% + ${min([d, panel.offsetWidth])}px))` : `translateX(${-d}px)`                }            }            let dt = e.timeStamp - pull.last_at            let step = dt > 0 ? (pull.opening ? e.clientX - pull.last_x : pull.last_x - e.clientX) / dt : pull.speed            pull = { x: pull.x, y: pull.y, last_x: e.clientX, last_at: e.timeStamp, speed: pull.speed == 0 ? step : pull.speed * 0.4 + step * 0.6, on: on, off: off, opening: pull.opening }        }    }    let clean = () => {        let panel = root.querySelector(".ui-tablet-side-panel")        panel.style.transform = ""        panel.style.width = ""        panel.style.visibility = ""        frame().classList.remove("ui-tablet-side-peeking")        moving()    }    let lift = (e) => {        if pull != null {            let was = pull            pull = null            if was.on {                let moved = was.opening ? e.clientX - was.x : was.x - e.clientX                let run = e.timeStamp - was.last_at < 100 ? was.speed : 0                let room = mode == "rail" ? 184 : root.querySelector(".ui-tablet-side-panel").offsetWidth                clean()                if tablet_sidebar_goes(moved, run, room) {                    if was.opening { show() } else { hide() }                }            }        }    }    let cancel = () => {        if pull != null {            let was = pull            pull = null            if was.on { clean() }        }    }    <div class={ui_cls(["ui-tablet-side", pinned && "ui-tablet-side-pinned", peek && "ui-tablet-side-peek", class])} data-mode={mode == "" ? null : mode}         @mount={(el) => {             let w = js("window")             let remembered = saved == null ? js("Promise.resolve", js_new("Promise", (done) => { done(js("localStorage.getItem", slot)) })) : null             let kept = tablet_sidebar_read(remembered)             win = w             root = el             if kept != null { pinned = kept }             let seen = js_new("ResizeObserver", (_e) => { look(el) })             seen.observe(el)             () => { seen.disconnect() }         }}         @keydown={(e) => {             if e.key == "Escape" && peek {                 e.preventDefault()                 hide()             }         }}>        <div.ui-tablet-side-frame>            <div.ui-tablet-side-slot>                <nav.ui-tablet-side-panel aria-label={title}                     @pointerdown={(e) => {                         if e.button == 0 {                             if peek {                                 pull = { x: e.clientX, y: e.clientY, last_x: e.clientX, last_at: e.timeStamp, speed: 0, on: false, off: false, opening: false }                             } else if mode == "rail" {                                 edge(e)                             }                         }                     }}                     @pointermove={track} @pointerup={lift} @pointercancel={cancel}>                    <div.ui-tablet-side-top>                        <button class="ui-tablet-side-btn ui-tablet-side-menu" type="button" aria-label={`Open ${title}`} @click={show}>                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>                        </button>                        <h2>{title}</h2>                        <button class="ui-tablet-side-btn ui-tablet-side-pin" type="button" aria-label="Keep it open" aria-pressed={pinned ? "true" : "false"} @click={pin}>                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 4h6l-1 6 4 4H6l4-4-1-6zM12 14v7"/></svg>                        </button>                        <button class="ui-tablet-side-btn ui-tablet-side-close" type="button" aria-label="Close" @click={hide}>                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>                        </button>                    </div>                    <div.ui-tablet-side-list                         @keydown={(e) => {                             let rows = root.querySelectorAll(".ui-tablet-side-item")                             let at = range(rows.length) | filter((n) => rows[n].isSameNode(e.target))                             let next = len(at) > 0 ? tablet_sidebar_step(e.key, at[0], rows.length) : null                             if next != null {                                 e.preventDefault()                                 rows[next].focus()                             }                         }}>                        {for s in sections =>                            <div.ui-tablet-side-section key={str(get(s, "title", ""))}>                                {get(s, "title") != null && <h3>{s.title}</h3>}                                {for it in get(s, "items", []) =>                                    get(it, "href") != null                                        ? <a.ui-tablet-side-item key={str(it.id)} href={it.href} data-id={str(it.id)} aria-current={str(it.id) == str(here) ? "page" : null}                                             @click={() => { press(it) }}><TabletSidebarInner it={it}/></a>                                        : <button.ui-tablet-side-item key={str(it.id)} type="button" data-id={str(it.id)} aria-current={str(it.id) == str(here) ? "page" : null}                                                  @click={() => { press(it) }}><TabletSidebarInner it={it}/></button>}                            </div>}                    </div>                </nav>            </div>            <div.ui-tablet-side-main inert={peek}>                <div.ui-tablet-side-bar>                    <button class="ui-tablet-side-btn ui-tablet-side-menu" type="button" aria-label={`Open ${title}`} @click={show}>                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>                    </button>                    <b>{title}</b>                </div>                <div.ui-tablet-side-page>{children}</div>            </div>            <div.ui-tablet-side-edge aria-hidden="true" @pointerdown={edge} @pointermove={track} @pointerup={lift} @pointercancel={cancel}></div>            <button.ui-tablet-side-scrim type="button" tabindex="-1" aria-label="Close" data-small-target @click={hide}></button>        </div>    </div>}
Thread
23Useful? Like it, or add to its thread below.

Thread 0 posts

On TabletSidebar: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.