Skip to content

Commit

Permalink
Pluto - Press and Hold Button (#884)
Browse files Browse the repository at this point in the history
* [pluto] - added delayed button activation option

* [pluto/input] - added additional docs to input number changes

* [pluto] - fixed issues with onMouseUp event handler double triggering in button

* [x/telem] - improved numeric primitive evaluation

* [client] - removed export of extra value

* [driver] update mbedtls

* [x/ts] - fixed typo in comment

---------

Co-authored-by: Elham Islam <eislam@umich.edu>
  • Loading branch information
emilbon99 and Lham42 authored Nov 3, 2024
1 parent d5bd019 commit 72c4950
Show file tree
Hide file tree
Showing 23 changed files with 257 additions and 108 deletions.
2 changes: 1 addition & 1 deletion client/ts/src/channel/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export class Client implements AsyncTermSearcher<string, Key, Channel> {
* channel. If this value is specified, the 'rate' parameter will be ignored.
* @param isIndex - Set to true if the channel is an index channel, and false
* otherwise. Index channels must have a data type of `DataType.TIMESTAMP`.
* @returns the created channel. {@see Channel}
* @returns the created channel. {@link Channel}
* @throws {ValidationError} if any of the parameters for creating the channel are
* invalid.
*
Expand Down
1 change: 0 additions & 1 deletion client/ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export {
DataType,
Density,
MultiSeries,
type NumericTelemValue,
Rate,
Series,
type TelemValue,
Expand Down
2 changes: 1 addition & 1 deletion client/ts/src/ontology/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
/**
* Opens an observable that can be used to subscribe to changes in both the ontology's
* resources and relationships.
* @see ChangeTracker for more information.
* @link ChangeTracker for more information.
* @returns An observable that emits changes to the ontology's resources and relationships.
*/
async openChangeTracker(): Promise<ChangeTracker> {
Expand Down
29 changes: 29 additions & 0 deletions pluto/src/button/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@
}
}

@keyframes delay {
0% {
width: 0%;
backdrop-filter: brightness(0.6);
}
99% {
width: 100%;
backdrop-filter: brightness(0.6);
}
100% {
width: 100%;
backdrop-filter: brightness(1);
}
}

/* |||| FILLED |||| */

.pluto-btn--filled {
Expand Down Expand Up @@ -125,6 +140,20 @@
border-color: var(--pluto-bg) !important;
}
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0%;
z-index: 1;
border-radius: var(--pluto-border-radius);
}
&:active::after {
animation: delay var(--pluto-btn-delay) linear;
width: 100%;
}
}

/* |||| OUTLINED |||| */
Expand Down
37 changes: 33 additions & 4 deletions pluto/src/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type ReactElement,
ReactNode,
useCallback,
useRef,
} from "react";

import { type Align } from "@/align";
Expand Down Expand Up @@ -64,7 +65,7 @@ export type ButtonProps = Omit<
endIcon?: ReactElement | ReactElement[];
iconSpacing?: Align.SpaceProps["size"];
disabled?: boolean;
delay?: number | TimeSpan;
onClickDelay?: number | TimeSpan;
endContent?: ReactNode;
};

Expand All @@ -81,6 +82,12 @@ export type ButtonProps = Omit<
* to match the color and size of the button.
* @param props.endIcon - The same as {@link startIcon}, but renders after the button
* text.
* @param props.iconSpacing - The spacing between the optional start and end icons
* and the button text. Can be "small", "medium", "large", or a number representing
* the spacing in rem.
* @param props.onClickDelay - An optional delay to wait before calling the `onClick`
* handler. This will cause the button to render a progress bar that fills up over the
* specified time before calling the handler.
*/
export const Button = Tooltip.wrap(
({
Expand All @@ -96,14 +103,17 @@ export const Button = Tooltip.wrap(
level,
triggers,
startIcon = [] as ReactElement[],
delay = 0,
onClickDelay = 0,
onClick,
color,
status,
style,
endContent,
onMouseDown,
...props
}: ButtonProps): ReactElement => {
const parsedDelay = TimeSpan.fromMilliseconds(onClickDelay);

if (loading) startIcon = [...toArray(startIcon), <Icon.Loading key="loader" />];
if (iconSpacing == null) iconSpacing = size === "small" ? "small" : "medium";
// We implement the shadow variant to maintain compatibility with the input
Expand All @@ -112,8 +122,22 @@ export const Button = Tooltip.wrap(

const handleClick: ButtonProps["onClick"] = (e) => {
if (disabled || variant === "preview") return;
const span = delay instanceof TimeSpan ? delay : TimeSpan.milliseconds(delay);
if (span.isZero) return onClick?.(e);
if (parsedDelay.isZero) return onClick?.(e);
};

const toRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const handleMouseDown: ButtonProps["onMouseDown"] = (e) => {
onMouseDown?.(e);
if (disabled || variant === "preview" || parsedDelay.isZero) return;
document.addEventListener(
"mouseup",
() => toRef.current != null && clearTimeout(toRef.current),
);
toRef.current = setTimeout(() => {
onClick?.(e);
toRef.current = null;
}, parsedDelay.milliseconds);
};

Triggers.use({
Expand Down Expand Up @@ -143,6 +167,10 @@ export const Button = Tooltip.wrap(
).rgbCSS;
}

if (!parsedDelay.isZero)
// @ts-expect-error - css variable
pStyle[CSS.var("btn-delay")] = `${parsedDelay.seconds.toString()}s`;

if (size == null && level != null) size = Text.LevelComponentSizes[level];
else if (size != null && level == null) level = Text.ComponentSizeLevels[size];
else if (size == null) size = "medium";
Expand All @@ -164,6 +192,7 @@ export const Button = Tooltip.wrap(
level={level ?? Text.ComponentSizeLevels[size]}
size={iconSpacing}
onClick={handleClick}
onMouseDown={handleMouseDown}
noWrap
style={pStyle}
startIcon={startIcon}
Expand Down
6 changes: 3 additions & 3 deletions pluto/src/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { Input } from "@/input";
import { state } from "@/state";
import { type status } from "@/status/aether";

/** Props for the @see useField hook */
/** Props for the @link useField hook */
export interface UseFieldProps<I, O = I> {
path: string;
optional?: false;
Expand All @@ -41,7 +41,7 @@ export interface UseNullableFieldProps<I, O = I>
optional: true;
}

/** Return type for the @see useField hook */
/** Return type for the @link useField hook */
export interface UseFieldReturn<I extends Input.Value, O extends Input.Value = I>
extends FieldState<I> {
onChange: (value: O) => void;
Expand Down Expand Up @@ -735,7 +735,7 @@ export const use = <Z extends z.ZodTypeAny>({
setStatus,
clearStatuses,
reset,
mode
mode,
],
);
};
Expand Down
1 change: 0 additions & 1 deletion pluto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export type {
CrudeSize,
CrudeTimeSpan,
CrudeTimeStamp,
NumericTelemValue,
TelemValue,
TimeStampStringFormat,
TypedArray,
Expand Down
7 changes: 5 additions & 2 deletions pluto/src/input/Numeric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// included in the file licenses/APL.txt.

import { bounds } from "@synnaxlabs/x";
import { evaluate } from "mathjs";
import { evaluate, Unit } from "mathjs";
import { forwardRef, type ReactElement, useCallback, useEffect } from "react";

import { useCombinedStateAndRef, useSyncedRef } from "@/hooks";
Expand Down Expand Up @@ -86,7 +86,10 @@ export const Numeric = forwardRef<HTMLInputElement, NumericProps>(
let ok = false;
let v = 0;
try {
v = evaluate(internalValueRef.current);
const ev = evaluate(internalValueRef.current);
// Sometimes mathjs returns a Unit object, so we need to convert it to a number.
if (ev instanceof Unit) v = ev.toNumber();
else v = ev;
ok = v != null;
} catch {
ok = false;
Expand Down
16 changes: 8 additions & 8 deletions pluto/src/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const rename = (key: string, name: string, tabs: Tab[]): Tab[] => {
return tabs.map((t) => (t.tabKey === key ? { ...t, name: name } : t));
};

/** Props for the {@see useStatic} hook. */
/** Props for the {@link useStatic} hook. */
export interface UseStaticTabsProps {
tabs: Tab[];
content?: RenderProp;
Expand All @@ -77,7 +77,7 @@ export interface UseStaticTabsProps {
* instead of pulling from the 'content' property of the tab object.
* @pram onSelect An optional callback to be called when a tab is selected.
* @param selected The key of the tab to be selected by default.
* @returns props to pass to the {@see Tabs} component.
* @returns props to pass to the {@link Tabs} component.
*/
export const useStatic = ({
tabs,
Expand Down Expand Up @@ -163,7 +163,7 @@ export interface TabsContextValue {
onCreate?: () => void;
}

/** Props for the {@see Tabs} component. */
/** Props for the {@link Tabs} component. */
export interface TabsProps
extends Omit<
Align.SpaceProps,
Expand All @@ -183,23 +183,23 @@ export interface TabsProps
}

/**
* Context used to propagate tab related information to children. See the {@see TabsContextValue}
* Context used to propagate tab related information to children. See the {@link TabsContextValue}
* type for information on the shape of the context.
*/
export const Context = createContext<TabsContextValue>({ tabs: [] });

/**
* Provider for the {@see Context} context. See the {@see TabsContextValue} type for information
* Provider for the {@link Context} context. See the {@link TabsContextValue} type for information
* on the shape of the context.
*/
export const Provider = Context.Provider;

/** @returns The current value of the {@see Context} context. */
/** @returns The current value of the {@link Context} context. */
export const useContext = (): TabsContextValue => reactUseContext(Context);

/**
* High-level component for creating a tabbed interface. This component is a composition
* of the {@see Selector}, {@see Content}, and {@see Context} components to provide a
* of the {@link Selector}, {@link Content}, and {@link Context} components to provide a
* complete tabbed interface. It's also possible to use these components individually
* to create a custom tabbed interface.
*
Expand Down Expand Up @@ -230,7 +230,7 @@ export const useContext = (): TabsContextValue => reactUseContext(Context);
* @param onDrop A callback executed when a tab is dropped. Identical to a onDrop handler in
* react.
* @param size The size of the tabs selector to display. Can be "small", "medium", or "large".
* @note all other props are inherited from the {@see Align.Space} component and are passed
* @note all other props are inherited from the {@link Align.Space} component and are passed
* through to that component.
* @param direction The direction in which to show the tabs selector. An 'x' direction
* will show the selector on the left side of the tabs, while a 'y' direction will show
Expand Down
7 changes: 2 additions & 5 deletions pluto/src/telem/aether/convertSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// License, use of this software will be governed by the Apache License, Version 2.0,
// included in the file licenses/APL.txt.

import { DataType, type NumericTelemValue, type Series } from "@synnaxlabs/x";
import { DataType, type math, type Series } from "@synnaxlabs/x";

/**
* Converts the given serie
Expand All @@ -16,10 +16,7 @@ import { DataType, type NumericTelemValue, type Series } from "@synnaxlabs/x";
* @param offset
* @returns
*/
export const convertSeriesFloat32 = (
series: Series,
offset?: NumericTelemValue,
): Series => {
export const convertSeriesFloat32 = (series: Series, offset?: math.Numeric): Series => {
if (series.dataType.isVariable) return series;
if (offset == null && series.dataType.equals(DataType.TIMESTAMP))
offset = BigInt(series.data[0]);
Expand Down
2 changes: 1 addition & 1 deletion pluto/src/telem/client/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Unary } from "@/telem/client/cache/unary";

export const CACHE_BUFFER_SIZE: TimeSpan = TimeSpan.seconds(60);

/** Props for instantiating an @see Cache */
/** Props for instantiating an @link Cache */
export interface CacheProps
extends StaticProps,
Partial<Pick<DynamicProps, "dynamicBufferSize">> {
Expand Down
4 changes: 2 additions & 2 deletions pluto/src/telem/client/cache/dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DataType, math, Series, TimeSpan, TimeStamp } from "@synnaxlabs/x";

import { convertSeriesFloat32 } from "@/telem/aether/convertSeries";

/** Response from a write to the @see Dynamic cache. */
/** Response from a write to the @link Dynamic cache. */
export interface DynamicWriteResponse {
/** A list of series that were flushed from the cache during the write i.e. the new
* writes were not able to fit in the current buffer, so a new one was allocated
Expand All @@ -21,7 +21,7 @@ export interface DynamicWriteResponse {
allocated: Series[];
}

/** Props for the @see Dynamic cache. */
/** Props for the @link Dynamic cache. */
export interface DynamicProps {
/**
* Sets the maximum size of the buffer that the cache will maintain before flushing
Expand Down
2 changes: 1 addition & 1 deletion pluto/src/telem/client/cache/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const zeroCacheGCMetrics = (): CacheGCMetrics => ({
purgedBytes: Size.bytes(0),
});

/** Props for the @see Static cache. */
/** Props for the @link Static cache. */
export interface StaticProps {
/** Used for logging */
instrumentation?: alamos.Instrumentation;
Expand Down
27 changes: 10 additions & 17 deletions pluto/src/vis/schematic/Forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -626,18 +626,10 @@ export const ButtonTelemForm = ({ path }: { path: string }): ReactElement => {
const handleSinkChange = (v: channel.Key): void => {
v = v ?? 0;
const t = telem.sinkPipeline("boolean", {
connections: [
{
from: "setpoint",
to: "setter",
},
],
connections: [{ from: "setpoint", to: "setter" }],
segments: {
setter: control.setChannelValue({ channel: v }),
setpoint: telem.setpoint({
truthy: 1,
falsy: 0,
}),
setpoint: telem.setpoint({ truthy: 1, falsy: 0 }),
},
inlet: "setpoint",
});
Expand All @@ -655,14 +647,9 @@ export const ButtonTelemForm = ({ path }: { path: string }): ReactElement => {
control: {
...value.control,
showChip: true,
chip: {
sink: controlChipSink,
source: authSource,
},
chip: { sink: controlChipSink, source: authSource },
showIndicator: true,
indicator: {
statusSource: authSource,
},
indicator: { statusSource: authSource },
},
});
};
Expand All @@ -672,6 +659,12 @@ export const ButtonTelemForm = ({ path }: { path: string }): ReactElement => {
<Input.Item label="Output Channel" grow>
<Channel.SelectSingle value={sink.channel} onChange={handleSinkChange} />
</Input.Item>
<Form.NumericField
label="Activation Delay"
path="onClickDelay"
inputProps={{ endContent: "ms" }}
hideIfNull
/>
<Form.SwitchField
path="control.show"
label="Show Control Chip"
Expand Down
Loading

0 comments on commit 72c4950

Please sign in to comment.